From 475d5995bb6b85b712b159e1827cb460ab4676b2 Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sun, 29 Jun 2025 17:07:55 +0900 Subject: [PATCH 01/66] =?UTF-8?q?hotfix(CookieUtils):=20addCookieToRespons?= =?UTF-8?q?e=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SameSite 추가 --- .../devdevdev/global/utils/CookieUtils.java | 23 +++++++++++-------- .../global/utils/CookieUtilsTest.java | 4 +++- 2 files changed, 17 insertions(+), 10 deletions(-) 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..e12b39dd 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()); } // 쿠키를 삭제하려면 클라이언트에게 해당 쿠키가 더 이상 유효하지 않음을 알려야 합니다. 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..f3fad464 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) ); } From 1a7ef3b2c28899f6139bf89b656a902100f37afe Mon Sep 17 00:00:00 2001 From: soyoung Date: Sun, 29 Jun 2025 17:47:02 +0900 Subject: [PATCH 02/66] =?UTF-8?q?feat:=20=EB=B3=80=EA=B2=BD=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=20=EB=8B=89=EB=84=A4=EC=9E=84=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devdevdev/LocalInitData.java | 9 +++---- .../global/constant/SecurityConstant.java | 6 +++-- .../controller/member/NicknameController.java | 26 +++++++++++++++++++ 3 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/NicknameController.java 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/global/constant/SecurityConstant.java b/src/main/java/com/dreamypatisiel/devdevdev/global/constant/SecurityConstant.java index 2e249471..7b573bca 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/global/constant/SecurityConstant.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/constant/SecurityConstant.java @@ -34,7 +34,8 @@ public class SecurityConstant { "/devdevdev/api/v1/articles/**", "/devdevdev/api/v1/keywords/**", "/devdevdev/api/v1/subscriptions/**", - "/devdevdev/api/v1/notifications/**" + "/devdevdev/api/v1/notifications/**", + "/devdevdev/api/v1/nickname/**" }; public static final String[] DEV_JWT_FILTER_WHITELIST_URL = new String[]{ @@ -71,7 +72,8 @@ public class SecurityConstant { "/devdevdev/api/v1/articles/**", "/devdevdev/api/v1/keywords/**", "/devdevdev/api/v1/subscriptions/**", - "/devdevdev/api/v1/notifications/**" + "/devdevdev/api/v1/notifications/**", + "/devdevdev/api/v1/nickname/**" }; public static final String[] PROD_JWT_FILTER_WHITELIST_URL = new String[]{ diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/NicknameController.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/NicknameController.java new file mode 100644 index 00000000..2f971342 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/NicknameController.java @@ -0,0 +1,26 @@ +package com.dreamypatisiel.devdevdev.web.controller.member; + +import com.dreamypatisiel.devdevdev.domain.service.member.MemberNicknameDictionaryService; +import com.dreamypatisiel.devdevdev.web.dto.response.BasicResponse; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +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.RestController; + + +@RestController +@RequestMapping("/devdevdev/api/v1") +@RequiredArgsConstructor +public class NicknameController { + + private final MemberNicknameDictionaryService memberNicknameDictionaryService; + + @Operation(summary = "랜덤 닉네임 요청", description = "랜덤 닉네임을 생성합니다.") + @GetMapping("/nickname/random") + public ResponseEntity> getRandomNickname() { + String response = memberNicknameDictionaryService.createRandomNickname(); + return ResponseEntity.ok(BasicResponse.success(response)); + } +} From e85fed3e36b0e956b7d99d0f9b6eee8b773cf2b5 Mon Sep 17 00:00:00 2001 From: soyoung Date: Sun, 29 Jun 2025 17:58:51 +0900 Subject: [PATCH 03/66] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/controller/{ => member}/LogoutControllerTest.java | 3 ++- .../web/controller/{ => member}/MyPageControllerTest.java | 3 ++- .../{ => member}/MyPageControllerUsedMockServiceTest.java | 3 ++- .../web/controller/{ => member}/TokenControllerTest.java | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) rename src/test/java/com/dreamypatisiel/devdevdev/web/controller/{ => member}/LogoutControllerTest.java (96%) rename src/test/java/com/dreamypatisiel/devdevdev/web/controller/{ => member}/MyPageControllerTest.java (99%) rename src/test/java/com/dreamypatisiel/devdevdev/web/controller/{ => member}/MyPageControllerUsedMockServiceTest.java (99%) rename src/test/java/com/dreamypatisiel/devdevdev/web/controller/{ => member}/TokenControllerTest.java (98%) 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 99% 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..b5d24be4 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,4 +1,4 @@ -package com.dreamypatisiel.devdevdev.web.controller; +package com.dreamypatisiel.devdevdev.web.controller.member; import static com.dreamypatisiel.devdevdev.web.dto.response.ResultType.SUCCESS; import static org.mockito.ArgumentMatchers.any; @@ -18,6 +18,7 @@ import com.dreamypatisiel.devdevdev.domain.service.member.MemberService; 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.response.ResultType; import com.dreamypatisiel.devdevdev.web.dto.response.comment.MyWrittenCommentResponse; 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; From fddc7c9bc424f3545794494825a50f79424bc2c6 Mon Sep 17 00:00:00 2001 From: soyoung Date: Sun, 29 Jun 2025 18:24:50 +0900 Subject: [PATCH 04/66] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B0=8F=20mypage=20=ED=95=98=EC=9C=84?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/api/mypage/mypage.adoc | 1 + .../asciidoc/api/mypage/random-nickname.adoc | 20 +++++++++++ .../global/constant/SecurityConstant.java | 6 ++-- .../controller/member/MypageController.java | 9 +++++ .../controller/member/NicknameController.java | 26 -------------- .../MyPageControllerUsedMockServiceTest.java | 24 +++++++++++++ ...PageControllerDocsUsedMockServiceTest.java | 34 +++++++++++++++++++ 7 files changed, 90 insertions(+), 30 deletions(-) create mode 100644 src/docs/asciidoc/api/mypage/random-nickname.adoc delete mode 100644 src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/NicknameController.java diff --git a/src/docs/asciidoc/api/mypage/mypage.adoc b/src/docs/asciidoc/api/mypage/mypage.adoc index b11e06bb..a8fb562a 100644 --- a/src/docs/asciidoc/api/mypage/mypage.adoc +++ b/src/docs/asciidoc/api/mypage/mypage.adoc @@ -7,3 +7,4 @@ include::exit-survey.adoc[] include::record-exit-survey.adoc[] include::comment-get.adoc[] include::subscribed-companies.adoc[] +include::random-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/main/java/com/dreamypatisiel/devdevdev/global/constant/SecurityConstant.java b/src/main/java/com/dreamypatisiel/devdevdev/global/constant/SecurityConstant.java index 7b573bca..2e249471 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/global/constant/SecurityConstant.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/constant/SecurityConstant.java @@ -34,8 +34,7 @@ public class SecurityConstant { "/devdevdev/api/v1/articles/**", "/devdevdev/api/v1/keywords/**", "/devdevdev/api/v1/subscriptions/**", - "/devdevdev/api/v1/notifications/**", - "/devdevdev/api/v1/nickname/**" + "/devdevdev/api/v1/notifications/**" }; public static final String[] DEV_JWT_FILTER_WHITELIST_URL = new String[]{ @@ -72,8 +71,7 @@ public class SecurityConstant { "/devdevdev/api/v1/articles/**", "/devdevdev/api/v1/keywords/**", "/devdevdev/api/v1/subscriptions/**", - "/devdevdev/api/v1/notifications/**", - "/devdevdev/api/v1/nickname/**" + "/devdevdev/api/v1/notifications/**" }; public static final String[] PROD_JWT_FILTER_WHITELIST_URL = new String[]{ 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..203a5cf3 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,6 +1,7 @@ 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; @@ -42,6 +43,7 @@ public class MypageController { private final MemberService memberService; + private final MemberNicknameDictionaryService memberNicknameDictionaryService; @Operation(summary = "북마크 목록 조회") @GetMapping("/mypage/bookmarks") @@ -133,4 +135,11 @@ 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)); + } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/NicknameController.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/NicknameController.java deleted file mode 100644 index 2f971342..00000000 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/NicknameController.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.dreamypatisiel.devdevdev.web.controller.member; - -import com.dreamypatisiel.devdevdev.domain.service.member.MemberNicknameDictionaryService; -import com.dreamypatisiel.devdevdev.web.dto.response.BasicResponse; -import io.swagger.v3.oas.annotations.Operation; -import lombok.RequiredArgsConstructor; -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.RestController; - - -@RestController -@RequestMapping("/devdevdev/api/v1") -@RequiredArgsConstructor -public class NicknameController { - - private final MemberNicknameDictionaryService memberNicknameDictionaryService; - - @Operation(summary = "랜덤 닉네임 요청", description = "랜덤 닉네임을 생성합니다.") - @GetMapping("/nickname/random") - public ResponseEntity> getRandomNickname() { - String response = memberNicknameDictionaryService.createRandomNickname(); - return ResponseEntity.ok(BasicResponse.success(response)); - } -} diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java index b5d24be4..06434509 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java @@ -15,6 +15,7 @@ import com.dreamypatisiel.devdevdev.domain.entity.enums.Role; import com.dreamypatisiel.devdevdev.domain.entity.enums.SocialType; 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.global.constant.SecurityConstant; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; @@ -44,6 +45,29 @@ public class MyPageControllerUsedMockServiceTest extends SupportControllerTest { MemberRepository memberRepository; @MockBean MemberService memberService; + @MockBean + MemberNicknameDictionaryService memberNicknameDictionaryService; + + @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("회원이 내가 썼어요 댓글을 조회한다.") 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..64542688 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsUsedMockServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsUsedMockServiceTest.java @@ -30,6 +30,7 @@ import com.dreamypatisiel.devdevdev.domain.entity.enums.Role; import com.dreamypatisiel.devdevdev.domain.entity.enums.SocialType; 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.global.constant.SecurityConstant; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; @@ -57,6 +58,39 @@ 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("회원이 내가 썼어요 댓글을 조회한다.") From a8e8f9ab6c7d78c2f34ed7a46b48c19b2638545a Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sun, 29 Jun 2025 22:30:17 +0900 Subject: [PATCH 05/66] =?UTF-8?q?feat(GuestPickCommentServiceV2):=20regist?= =?UTF-8?q?erPickComment=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 익명회원 댓글 작성 서비스 개발 및 테스트 코드 작성 --- .../pick-commnet/pick-comment-register.adoc | 6 +- .../domain/entity/AnonymousMember.java | 13 +- .../devdevdev/domain/entity/PickComment.java | 110 +++-- .../member/AnonymousMemberService.java | 33 +- .../MemberNicknameDictionaryService.java | 3 +- .../service/pick/GuestPickCommentService.java | 8 +- .../pick/GuestPickCommentServiceV2.java | 171 ++++++++ .../pick/MemberPickCommentService.java | 26 +- .../service/pick/PickCommentService.java | 12 +- .../service/pick/PickServiceStrategy.java | 2 +- .../service/pick/dto/PickCommentDto.java | 35 ++ .../utils/AuthenticationMemberUtils.java | 6 +- .../global/utils/BigDecimalUtils.java | 6 +- .../devdevdev/global/utils/FileUtils.java | 6 +- .../global/utils/HttpRequestUtils.java | 14 + .../devdevdev/global/utils/UriUtils.java | 2 +- .../pick/PickCommentController.java | 14 +- .../blame/MemberPickBlameServiceTest.java | 2 +- .../pick/GuestPickCommentServiceTest.java | 4 +- .../pick/GuestPickCommentServiceV2Test.java | 363 +++++++++++++++++ .../pick/MemberPickCommentServiceTest.java | 46 +-- .../domain/service/pick/PickTestUtils.java | 377 ++++++++++++++++++ .../pick/PickCommentControllerTest.java | 8 +- .../docs/PickCommentControllerDocsTest.java | 13 +- 24 files changed, 1165 insertions(+), 115 deletions(-) create mode 100644 src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java create mode 100644 src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/dto/PickCommentDto.java create mode 100644 src/main/java/com/dreamypatisiel/devdevdev/global/utils/HttpRequestUtils.java create mode 100644 src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java create mode 100644 src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickTestUtils.java 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/main/java/com/dreamypatisiel/devdevdev/domain/entity/AnonymousMember.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/AnonymousMember.java index ae22a1cb..8fc113db 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,12 @@ 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; + } } 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..36737c20 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,58 @@ 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.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.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 +208,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; 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..e0e54020 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,19 +1,20 @@ 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 { - + private final AnonymousMemberRepository anonymousMemberRepository; @Transactional @@ -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/pick/GuestPickCommentService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentService.java index 94de9ce2..f70f5fcd 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 @@ -8,11 +8,11 @@ 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.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; @@ -40,16 +40,14 @@ public GuestPickCommentService(EmbeddingsService embeddingsService, } @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, RegisterPickRepliedCommentRequest pickSubCommentRequest, Authentication authentication) { throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); 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..3cf6a458 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java @@ -0,0 +1,171 @@ +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_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.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.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 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 PickPopularScorePolicy pickPopularScorePolicy; + + private final PickVoteRepository pickVoteRepository; + + public GuestPickCommentServiceV2(EmbeddingsService embeddingsService, + PickBestCommentsPolicy pickBestCommentsPolicy, + PickRepository pickRepository, + PickCommentRepository pickCommentRepository, + PickCommentRecommendRepository pickCommentRecommendRepository, + AnonymousMemberService anonymousMemberService, + PickPopularScorePolicy pickPopularScorePolicy, + PickVoteRepository pickVoteRepository) { + super(embeddingsService, pickBestCommentsPolicy, pickRepository, pickCommentRepository, + pickCommentRecommendRepository); + this.anonymousMemberService = anonymousMemberService; + this.pickPopularScorePolicy = pickPopularScorePolicy; + 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 anonymousMember = 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, anonymousMember) + .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_VOTE_MESSAGE)); + + // 픽픽픽 투표한 픽 옵션의 댓글 작성 + PickComment pickComment = PickComment.createPublicVoteCommentByAnonymousMember(new CommentContents(contents), + anonymousMember, findPick, findPickVote); + pickCommentRepository.save(pickComment); + + return new PickCommentResponse(pickComment.getId()); + } + + // 픽픽픽 선택지 투표 비공개인 경우 + PickComment pickComment = PickComment.createPrivateVoteCommentByAnonymousMember(new CommentContents(contents), + anonymousMember, findPick); + pickCommentRepository.save(pickComment); + + return new PickCommentResponse(pickComment.getId()); + } + + @Override + public PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, Long pickCommentOriginParentId, + Long pickId, RegisterPickRepliedCommentRequest pickSubCommentRequest, + Authentication authentication) { + + throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); + } + + @Override + public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, + ModifyPickCommentRequest modifyPickCommentRequest, + Authentication authentication) { + + throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); + } + + @Override + public PickCommentResponse deletePickComment(Long pickCommentId, Long pickId, Authentication authentication) { + throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); + } + + /** + * @Note: 정렬 조건에 따라서 커서 방식으로 픽픽픽 댓글/답글을 조회한다. + * @Author: 장세웅 + * @Since: 2024.10.02 + */ + @Override + public SliceCustom findPickComments(Pageable pageable, Long pickId, Long pickCommentId, + PickCommentSort pickCommentSort, + EnumSet pickOptionTypes, + Authentication authentication) { + + // 익명 회원인지 검증 + AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); + + // 픽픽픽 댓글/답글 조회 + return super.findPickComments(pageable, pickId, pickCommentId, pickCommentSort, pickOptionTypes, null); + } + + @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 + public List findPickBestComments(int size, Long pickId, + Authentication authentication) { + // 익명 회원인지 검증 + AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); + + return super.findPickBestComments(size, pickId, null); + } +} 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..e72e1267 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 @@ -22,13 +22,13 @@ 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; @@ -45,11 +45,6 @@ @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; @@ -80,12 +75,10 @@ public MemberPickCommentService(TimeProvider timeProvider, MemberProvider member * @Since: 2024.08.23 */ @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 +86,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 +102,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 +110,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); @@ -159,7 +153,7 @@ public PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, findOriginParentPickComment.incrementReplyTotalCount(); // 픽픽픽 서브 댓글(답글) 생성 - PickComment pickRepliedComment = PickComment.createRepliedComment(new CommentContents(contents), + PickComment pickRepliedComment = PickComment.createRepliedCommentByMember(new CommentContents(contents), findParentPickComment, findOriginParentPickComment, findMember, findPick); pickCommentRepository.save(pickRepliedComment); @@ -237,7 +231,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 +246,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()); } 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..9845b3e9 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,9 +2,9 @@ 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; @@ -15,14 +15,18 @@ import org.springframework.security.core.Authentication; public interface PickCommentService { + String MODIFY = "수정"; + String REGISTER = "작성"; + String DELETE = "삭제"; + String RECOMMEND = "추천"; + PickCommentResponse registerPickComment(Long pickId, - RegisterPickCommentRequest pickMainCommentRequest, + PickCommentDto pickRegisterCommentDto, Authentication authentication); PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, Long pickCommentOriginParentId, - Long pickId, - RegisterPickRepliedCommentRequest pickSubCommentRequest, + Long pickId, RegisterPickRepliedCommentRequest pickSubCommentRequest, Authentication authentication); PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, 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..e845054a --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/dto/PickCommentDto.java @@ -0,0 +1,35 @@ +package com.dreamypatisiel.devdevdev.domain.service.pick.dto; + +import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickCommentRequest; +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(String contents, String anonymousMemberId) { + return PickCommentDto.builder() + .contents(contents) + .anonymousMemberId(anonymousMemberId) + .build(); + } +} 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/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/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/web/controller/pick/PickCommentController.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/pick/PickCommentController.java index e1a1ebb9..ec820507 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,17 +46,21 @@ 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)); } 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/pick/GuestPickCommentServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceTest.java index db3c8d4a..25f9eec1 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 @@ -158,7 +158,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)); @@ -837,7 +837,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)); 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..15d7aa8c --- /dev/null +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java @@ -0,0 +1,363 @@ +package com.dreamypatisiel.devdevdev.domain.service.pick; + +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_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_FOUND_PICK_VOTE_MESSAGE; +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.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.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.PickOption; +import com.dreamypatisiel.devdevdev.domain.entity.PickOptionImage; +import com.dreamypatisiel.devdevdev.domain.entity.PickVote; +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.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.response.pick.PickCommentResponse; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.EnumSource.Mode; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.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); + } +} \ 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..e73bdd7f 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 @@ -43,6 +43,7 @@ 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; @@ -51,7 +52,6 @@ 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; @@ -171,11 +171,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 +238,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 +275,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 +302,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 +348,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 +393,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); } @@ -575,7 +575,7 @@ 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의 답글"); @@ -616,7 +616,7 @@ 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의 답글"); @@ -846,7 +846,7 @@ 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(); @@ -1201,7 +1201,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(); @@ -1287,7 +1287,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)); @@ -2349,7 +2349,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 +2431,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)); 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..e0f82ca7 --- /dev/null +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickTestUtils.java @@ -0,0 +1,377 @@ +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 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 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 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/web/controller/pick/PickCommentControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/pick/PickCommentControllerTest.java index 10bb1b5f..ec50082c 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 @@ -535,7 +535,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 +684,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 +912,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)); @@ -1038,7 +1038,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)); 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..76c81242 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; @@ -60,6 +61,7 @@ import com.dreamypatisiel.devdevdev.domain.repository.pick.PickVoteRepository; import com.dreamypatisiel.devdevdev.global.constant.SecurityConstant; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; +import com.dreamypatisiel.devdevdev.web.WebConstant; 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; @@ -115,7 +117,7 @@ public class PickCommentControllerDocsTest extends SupportControllerDocsTest { AmazonS3 amazonS3Client; @Test - @DisplayName("회원이 승인 상태의 픽픽픽에 댓글을 작성한다.") + @DisplayName("승인 상태의 픽픽픽에 댓글을 작성한다.") void registerPickComment() throws Exception { // given // 회원 생성 @@ -165,7 +167,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("픽픽픽 아이디") @@ -645,7 +648,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, originParentPickComment3, originParentPickComment2, originParentPickComment1)); @@ -659,7 +662,7 @@ void getPickComments(PickCommentSort pickCommentSort) throws Exception { originParentPickComment2, originParentPickComment2); PickComment pickReply4 = createReplidPickComment(new CommentContents("벌써 9월이당"), member5, pick, originParentPickComment2, originParentPickComment2); - pickReply4.changeDeletedAt(LocalDateTime.now(), member5); + pickReply4.changeDeletedAtByMember(LocalDateTime.now(), member5); pickCommentRepository.saveAll(List.of(pickReply4, pickReply3, pickReply2, pickReply1)); em.flush(); @@ -959,7 +962,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)); From 897222495c0115ae313a9dd70236abdfec7ce62e Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Wed, 2 Jul 2025 22:53:07 +0900 Subject: [PATCH 06/66] =?UTF-8?q?fix(PickComment):=20PickComment=20static?= =?UTF-8?q?=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/dreamypatisiel/devdevdev/domain/entity/PickComment.java | 2 ++ 1 file changed, 2 insertions(+) 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 36737c20..ec539ac8 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/PickComment.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/PickComment.java @@ -149,6 +149,7 @@ public static PickComment createPublicVoteCommentByMember(CommentContents conten 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); @@ -182,6 +183,7 @@ public static PickComment createRepliedCommentByAnonymousMember(CommentContents PickComment originParent, AnonymousMember createdAnonymousBy, Pick pick) { PickComment pickComment = createPickComment(content, parent, originParent); + pickComment.isPublic = false; pickComment.createdAnonymousBy = createdAnonymousBy; pickComment.changePick(pick); From 869e218c2ee8ffcc384e174b600c88a218ee1053 Mon Sep 17 00:00:00 2001 From: soyoung Date: Fri, 4 Jul 2025 01:26:47 +0900 Subject: [PATCH 07/66] =?UTF-8?q?feat:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devdevdev/domain/entity/Member.java | 4 ++++ .../domain/service/member/MemberService.java | 11 +++++++++++ .../web/controller/member/MypageController.java | 12 ++++++++++++ .../dto/request/member/ChangeNicknameRequest.java | 14 ++++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 src/main/java/com/dreamypatisiel/devdevdev/web/dto/request/member/ChangeNicknameRequest.java 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..2dac2d86 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java @@ -187,4 +187,8 @@ public void deleteMember(LocalDateTime now) { this.isDeleted = true; this.deletedAt = now; } + + public void changeNickname(String nickname) { + this.nickname = new Nickname(nickname); + } } 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..9cbdc14a 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 @@ -286,4 +286,15 @@ public SliceCustom findMySubscribedCompanies(Pageable return new SliceCustom<>(subscribedCompanyResponses, pageable, subscribedCompanies.getTotalElements()); } + + /** + * @Note: 유저의 닉네임을 변경합니다. + * @Author: 유소영 + * @Since: 2025.07.03 + */ + @Transactional + public void changeNickname(String nickname, Authentication authentication) { + Member member = memberProvider.getMemberByAuthentication(authentication); + member.changeNickname(nickname); + } } 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 203a5cf3..3708783a 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 @@ -8,6 +8,7 @@ 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; @@ -30,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; @@ -142,4 +144,14 @@ 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(); + memberService.changeNickname(request.getNickname(), authentication); + return ResponseEntity.ok(BasicResponse.success()); + } } 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..73001119 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/request/member/ChangeNicknameRequest.java @@ -0,0 +1,14 @@ +package com.dreamypatisiel.devdevdev.web.dto.request.member; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class ChangeNicknameRequest { + @NotBlank(message = "닉네임은 필수입니다.") + private String nickname; +} From 5205a0be1cc90c06f2f97250b9da6e437b348e76 Mon Sep 17 00:00:00 2001 From: soyoung Date: Fri, 4 Jul 2025 01:59:47 +0900 Subject: [PATCH 08/66] =?UTF-8?q?fix:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94?= =?UTF-8?q?=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../asciidoc/api/mypage/change-nickname.adoc | 21 +++++++++ .../request/member/ChangeNicknameRequest.java | 6 +++ .../service/member/MemberServiceTest.java | 22 +++++++++ .../MyPageControllerUsedMockServiceTest.java | 40 +++++++++++++++- ...PageControllerDocsUsedMockServiceTest.java | 46 +++++++++++++++++-- 5 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 src/docs/asciidoc/api/mypage/change-nickname.adoc 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..1d0ffa17 --- /dev/null +++ b/src/docs/asciidoc/api/mypage/change-nickname.adoc @@ -0,0 +1,21 @@ +[[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[] \ No newline at end of file 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 index 73001119..b6a8ba75 100644 --- 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 @@ -1,6 +1,7 @@ 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; @@ -11,4 +12,9 @@ public class ChangeNicknameRequest { @NotBlank(message = "닉네임은 필수입니다.") private String nickname; + + @Builder + public ChangeNicknameRequest(String nickname) { + this.nickname = nickname; + } } 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..7ba79e31 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 @@ -1177,6 +1177,28 @@ 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 + memberService.changeNickname(newNickname, authentication); + + // then + assertThat(member.getNickname().getNickname()).isEqualTo(newNickname); + } + 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/web/controller/member/MyPageControllerUsedMockServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java index 06434509..bd01e95d 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java @@ -3,9 +3,9 @@ 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; @@ -21,6 +21,7 @@ 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; @@ -28,6 +29,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; @@ -47,6 +49,8 @@ public class MyPageControllerUsedMockServiceTest extends SupportControllerTest { MemberService memberService; @MockBean MemberNicknameDictionaryService memberNicknameDictionaryService; + @Autowired + EntityManager em; @Test @DisplayName("회원은 랜덤 닉네임을 생성할 수 있다.") @@ -69,6 +73,32 @@ void getRandomNickname() throws Exception { .andExpect(jsonPath("$.data").isString()); } + @Test + @DisplayName("회원은 닉네임을 변경할 수 있다.") + void changeNickname() throws Exception { + // given + String newNickname = "변경된 닉네임"; + ChangeNicknameRequest request = createChangeNicknameRequest(newNickname); + request.setNickname(newNickname); + + // when + doNothing().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)) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.resultType").value(SUCCESS.name())); + + // 서비스 메서드가 호출되었는지 검증 + verify(memberService).changeNickname(eq(newNickname), any()); + } + @Test @DisplayName("회원이 내가 썼어요 댓글을 조회한다.") void getMyWrittenComments() throws Exception { @@ -302,4 +332,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/MyPageControllerDocsUsedMockServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsUsedMockServiceTest.java index 64542688..8fb04036 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsUsedMockServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsUsedMockServiceTest.java @@ -8,18 +8,17 @@ import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.uniqueCommentIdType; 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; @@ -35,6 +34,7 @@ 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.comment.MyWrittenCommentResponse; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; @@ -92,6 +92,38 @@ void getRandomNickname() throws Exception { )); } + @Test + @DisplayName("회원은 닉네임을 변경할 수 있다.") + void changeNickname() throws Exception { + // given + String newNickname = "변경된 닉네임"; + ChangeNicknameRequest request = createChangeNicknameRequest(newNickname); + + // when + doNothing().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)) + .andExpect(status().isOk()) + .andDo(document("change-nickname", + requestFields( + fieldWithPath("nickname").description("변경할 닉네임") + ), + requestHeaders( + headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") + ), + responseFields( + fieldWithPath("resultType").description("성공 여부") + ) + )); + + verify(memberService).changeNickname(eq(newNickname), any()); + } + @Test @DisplayName("회원이 내가 썼어요 댓글을 조회한다.") void getMyWrittenComments() throws Exception { @@ -423,4 +455,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(); + } } From 240fbd2116eae0a48b27dad456b41a77bca10325 Mon Sep 17 00:00:00 2001 From: soyoung Date: Sun, 6 Jul 2025 14:58:38 +0900 Subject: [PATCH 09/66] =?UTF-8?q?fix(nickname):=2024=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=9D=B4=EB=82=B4=20=EB=B3=80=EA=B2=BD=20=EC=A0=95=EC=B1=85=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../asciidoc/api/mypage/change-nickname.adoc | 3 +- .../devdevdev/domain/entity/Member.java | 11 ++++- .../exception/NicknameExceptionMessage.java | 5 +++ .../domain/service/member/MemberService.java | 11 ++++- .../exception/ApiControllerAdvice.java | 14 +++--- .../service/member/MemberServiceTest.java | 42 +++++++++++++++++- .../MyPageControllerUsedMockServiceTest.java | 32 ++++++++++++++ ...PageControllerDocsUsedMockServiceTest.java | 44 +++++++++++++++++++ 8 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/dreamypatisiel/devdevdev/domain/exception/NicknameExceptionMessage.java diff --git a/src/docs/asciidoc/api/mypage/change-nickname.adoc b/src/docs/asciidoc/api/mypage/change-nickname.adoc index 1d0ffa17..7529bda3 100644 --- a/src/docs/asciidoc/api/mypage/change-nickname.adoc +++ b/src/docs/asciidoc/api/mypage/change-nickname.adoc @@ -18,4 +18,5 @@ include::{snippets}/change-nickname/response-fields.adoc[] === 예외 ==== HTTP Response -include::{snippets}/not-found-member-exception/response-body.adoc[] \ No newline at end of file +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/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java index 2dac2d86..beb7141a 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<>(); @@ -188,7 +191,13 @@ public void deleteMember(LocalDateTime now) { this.deletedAt = now; } - public void changeNickname(String nickname) { + public void changeNickname(String nickname, LocalDateTime now) { this.nickname = new Nickname(nickname); + this.nicknameUpdatedAt = now; + } + + public boolean isAvailableToChangeNickname() { + return nicknameUpdatedAt == null + || ChronoUnit.HOURS.between(nicknameUpdatedAt, LocalDateTime.now()) >= 24; } } 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/service/member/MemberService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberService.java index 9cbdc14a..39fa4418 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 @@ -14,6 +14,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; @@ -44,6 +45,7 @@ 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; @Service @RequiredArgsConstructor @@ -288,13 +290,18 @@ public SliceCustom findMySubscribedCompanies(Pageable } /** - * @Note: 유저의 닉네임을 변경합니다. + * @Note: 유저의 닉네임을 변경합니다. 최근 24시간 이내에 변경한 이력이 있다면 닉네임 변경이 불가능합니다. * @Author: 유소영 * @Since: 2025.07.03 */ @Transactional public void changeNickname(String nickname, Authentication authentication) { Member member = memberProvider.getMemberByAuthentication(authentication); - member.changeNickname(nickname); + + if (member.isAvailableToChangeNickname()) { + member.changeNickname(nickname, timeProvider.getLocalDateTimeNow()); + } else { + throw new NicknameException(NICKNAME_CHANGE_RATE_LIMIT_MESSAGE); + } } } 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/test/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberServiceTest.java index 7ba79e31..97dac996 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,6 +34,7 @@ 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.security.oauth2.model.SocialMemberDto; @@ -51,12 +53,13 @@ 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.data.auditing.AuditingHandler; @@ -1199,6 +1202,41 @@ void changeNickname() { assertThat(member.getNickname().getNickname()).isEqualTo(newNickname); } + @DisplayName("회원이 24시간 이내에 닉네임을 변경한 적이 있다면 예외가 발생한다.") + @ParameterizedTest + @CsvSource({ + "0, true", + "1, true", + "23, true", + "24, false", // 변경 허용 + "25, false" // 변경 허용 + }) + void changeNicknameThrowsExceptionWhenChangedWithin24Hours(long hoursAgo, boolean shouldThrowException) { + // given + String oldNickname = "이전 닉네임"; + String newNickname = "새 닉네임"; + SocialMemberDto socialMemberDto = createSocialDto(userId, name, oldNickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + member.changeNickname(oldNickname, LocalDateTime.now().minusHours(hoursAgo)); + 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); + } + } + 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/web/controller/member/MyPageControllerUsedMockServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java index bd01e95d..47c9d847 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java @@ -1,5 +1,6 @@ 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; @@ -14,9 +15,11 @@ 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; @@ -99,6 +102,35 @@ void changeNickname() throws Exception { 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 getMyWrittenComments() throws Exception { 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 8fb04036..42e9065e 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsUsedMockServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsUsedMockServiceTest.java @@ -22,19 +22,23 @@ 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; @@ -48,6 +52,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; @@ -124,6 +129,45 @@ void changeNickname() throws Exception { 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 getMyWrittenComments() throws Exception { From 72403731a04b1168fa2bb2de0b96c04b1fd1878f Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sun, 6 Jul 2025 15:07:00 +0900 Subject: [PATCH 10/66] =?UTF-8?q?feat(GuestPickCommentServiceV2):=20?= =?UTF-8?q?=EC=9D=B5=EB=AA=85=ED=9A=8C=EC=9B=90=20=EB=8B=B5=EA=B8=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/pick/GuestPickCommentService.java | 7 +- .../pick/GuestPickCommentServiceV2.java | 33 +- .../domain/service/pick/GuestPickService.java | 4 +- .../pick/MemberPickCommentService.java | 54 +--- .../service/pick/MemberPickService.java | 19 +- .../service/pick/PickCommentService.java | 3 +- .../service/pick/PickCommonService.java | 56 ++++ .../service/pick/dto/PickCommentDto.java | 6 +- .../pick/PickCommentController.java | 8 +- .../web/controller/pick/PickController.java | 9 +- .../pick/GuestPickCommentServiceV2Test.java | 286 ++++++++++++++++++ .../pick/MemberPickCommentServiceTest.java | 25 +- .../domain/service/pick/PickTestUtils.java | 18 ++ 13 files changed, 440 insertions(+), 88 deletions(-) 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 f70f5fcd..eaa67ba9 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,6 +4,7 @@ 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; @@ -13,7 +14,6 @@ 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.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; @@ -33,9 +33,10 @@ public class GuestPickCommentService extends PickCommonService implements PickCo public GuestPickCommentService(EmbeddingsService embeddingsService, PickBestCommentsPolicy pickBestCommentsPolicy, PickRepository pickRepository, + PickPopularScorePolicy pickPopularScorePolicy, PickCommentRepository pickCommentRepository, PickCommentRecommendRepository pickCommentRecommendRepository) { - super(embeddingsService, pickBestCommentsPolicy, pickRepository, pickCommentRepository, + super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, pickRepository, pickCommentRepository, pickCommentRecommendRepository); } @@ -47,7 +48,7 @@ public PickCommentResponse registerPickComment(Long pickId, PickCommentDto pickC @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); 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 index 3cf6a458..5f5734f3 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java @@ -25,7 +25,6 @@ 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.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; @@ -42,7 +41,6 @@ public class GuestPickCommentServiceV2 extends PickCommonService implements PickCommentService { private final AnonymousMemberService anonymousMemberService; - private final PickPopularScorePolicy pickPopularScorePolicy; private final PickVoteRepository pickVoteRepository; @@ -54,10 +52,9 @@ public GuestPickCommentServiceV2(EmbeddingsService embeddingsService, AnonymousMemberService anonymousMemberService, PickPopularScorePolicy pickPopularScorePolicy, PickVoteRepository pickVoteRepository) { - super(embeddingsService, pickBestCommentsPolicy, pickRepository, pickCommentRepository, - pickCommentRecommendRepository); + super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, pickRepository, + pickCommentRepository, pickCommentRecommendRepository); this.anonymousMemberService = anonymousMemberService; - this.pickPopularScorePolicy = pickPopularScorePolicy; this.pickVoteRepository = pickVoteRepository; } @@ -110,11 +107,33 @@ public PickCommentResponse registerPickComment(Long pickId, PickCommentDto pickC } @Override + @Transactional 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); + // 익명 회원인지 검증 + AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); + + String contents = pickCommentDto.getContents(); + String anonymousMemberId = pickCommentDto.getAnonymousMemberId(); + + // 익명 회원 추출 + AnonymousMember anonymousMember = 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, anonymousMember, findPick); + pickCommentRepository.save(pickRepliedComment); + + return new PickCommentResponse(pickRepliedComment.getId()); } @Override 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..cbad78dc 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,7 +55,6 @@ 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; @@ -67,9 +66,8 @@ public GuestPickService(PickRepository pickRepository, EmbeddingsService embeddi PickPopularScorePolicy pickPopularScorePolicy, PickVoteRepository pickVoteRepository, TimeProvider timeProvider, AnonymousMemberService anonymousMemberService) { - super(embeddingsService, pickBestCommentsPolicy, pickRepository, pickCommentRepository, + super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, pickRepository, pickCommentRepository, pickCommentRecommendRepository); - this.pickPopularScorePolicy = pickPopularScorePolicy; this.pickVoteRepository = pickVoteRepository; this.anonymousMemberService = anonymousMemberService; this.timeProvider = timeProvider; 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 e72e1267..4a109d4a 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; @@ -29,7 +27,6 @@ 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.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; @@ -47,7 +44,6 @@ public class MemberPickCommentService extends PickCommonService implements PickC private final TimeProvider timeProvider; private final MemberProvider memberProvider; - private final PickPopularScorePolicy pickPopularScorePolicy; private final PickRepository pickRepository; private final PickVoteRepository pickVoteRepository; @@ -59,11 +55,10 @@ public MemberPickCommentService(TimeProvider timeProvider, MemberProvider member PickRepository pickRepository, PickVoteRepository pickVoteRepository, PickCommentRepository pickCommentRepository, PickCommentRecommendRepository pickCommentRecommendRepository) { - super(embeddingsService, pickBestCommentsPolicy, pickRepository, pickCommentRepository, + super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, pickRepository, pickCommentRepository, pickCommentRecommendRepository); this.timeProvider = timeProvider; this.memberProvider = memberProvider; - this.pickPopularScorePolicy = pickPopularScorePolicy; this.pickRepository = pickRepository; this.pickVoteRepository = pickVoteRepository; this.pickCommentRepository = pickCommentRepository; @@ -126,31 +121,20 @@ public PickCommentResponse registerPickComment(Long pickId, PickCommentDto pickC 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.createRepliedCommentByMember(new CommentContents(contents), @@ -160,28 +144,6 @@ public PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, 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: 장세웅 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..3bf47be8 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,7 +85,7 @@ 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, @@ -96,7 +96,7 @@ public MemberPickService(EmbeddingsService embeddingsService, PickRepository pic PickCommentRecommendRepository pickCommentRecommendRepository, PickPopularScorePolicy pickPopularScorePolicy, PickBestCommentsPolicy pickBestCommentsPolicy, TimeProvider timeProvider) { - super(embeddingsService, pickBestCommentsPolicy, pickRepository, pickCommentRepository, + super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, pickRepository, pickCommentRepository, pickCommentRecommendRepository); this.awsS3Properties = awsS3Properties; this.awsS3Uploader = awsS3Uploader; @@ -104,7 +104,6 @@ public MemberPickService(EmbeddingsService embeddingsService, PickRepository pic this.pickOptionRepository = pickOptionRepository; this.pickOptionImageRepository = pickOptionImageRepository; this.pickVoteRepository = pickVoteRepository; - this.pickPopularScorePolicy = pickPopularScorePolicy; this.timeProvider = timeProvider; } @@ -129,14 +128,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 +279,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 9845b3e9..2311132e 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 @@ -5,7 +5,6 @@ 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.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; @@ -26,7 +25,7 @@ PickCommentResponse registerPickComment(Long pickId, PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, Long pickCommentOriginParentId, - Long pickId, RegisterPickRepliedCommentRequest pickSubCommentRequest, + Long pickId, PickCommentDto pickCommentDto, Authentication authentication); PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, 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..89c73faf 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,7 +1,11 @@ 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.Member; import com.dreamypatisiel.devdevdev.domain.entity.Pick; @@ -9,6 +13,7 @@ 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; @@ -45,6 +50,7 @@ public class PickCommonService { private final EmbeddingsService embeddingsService; private final PickBestCommentsPolicy pickBestCommentsPolicy; + protected final PickPopularScorePolicy pickPopularScorePolicy; protected final PickRepository pickRepository; protected final PickCommentRepository pickCommentRepository; @@ -231,4 +237,54 @@ protected List findPickBestComments(int size, Long pickId, 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/dto/PickCommentDto.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/dto/PickCommentDto.java index e845054a..8895b8fb 100644 --- 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 @@ -1,6 +1,7 @@ package com.dreamypatisiel.devdevdev.domain.service.pick.dto; import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickCommentRequest; +import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickRepliedCommentRequest; import lombok.Builder; import lombok.Data; @@ -26,9 +27,10 @@ public static PickCommentDto createRegisterCommentDto(RegisterPickCommentRequest .build(); } - public static PickCommentDto createRepliedCommentDto(String contents, String anonymousMemberId) { + public static PickCommentDto createRepliedCommentDto(RegisterPickRepliedCommentRequest registerPickRepliedCommentRequest, + String anonymousMemberId) { return PickCommentDto.builder() - .contents(contents) + .contents(registerPickRepliedCommentRequest.getContents()) .anonymousMemberId(anonymousMemberId) .build(); } 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 ec820507..7b7d9972 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 @@ -65,7 +65,7 @@ public ResponseEntity> registerPickComment( return ResponseEntity.ok(BasicResponse.success(pickCommentResponse)); } - @Operation(summary = "픽픽픽 답글 작성", description = "회원은 픽픽픽 댓글에 답글을 작성할 수 있습니다.") + @Operation(summary = "픽픽픽 답글 작성", description = "픽픽픽 댓글에 답글을 작성할 수 있습니다.") @PostMapping("/picks/{pickId}/comments/{pickOriginParentCommentId}/{pickParentCommentId}") public ResponseEntity> registerPickRepliedComment( @PathVariable Long pickId, @@ -74,10 +74,14 @@ 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)); 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/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java index 15d7aa8c..6b2433f0 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java @@ -1,13 +1,18 @@ 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.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.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; @@ -26,6 +31,7 @@ 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; @@ -47,15 +53,18 @@ 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.request.pick.RegisterPickRepliedCommentRequest; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentResponse; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; +import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.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.security.core.Authentication; @@ -360,4 +369,281 @@ void registerPickCommentNotFoundPickMainVote() { .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); + } } \ 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 e73bdd7f..5b1f0c11 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 @@ -441,10 +441,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 +483,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 +512,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 +546,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); } @@ -579,11 +583,12 @@ void registerPickRepliedCommentDeleted() { 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); } @@ -620,11 +625,12 @@ void registerPickRepliedCommentRepliedDeleted() { 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 +659,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); } 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 index e0f82ca7..1c6b7599 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickTestUtils.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickTestUtils.java @@ -124,6 +124,24 @@ public static PickComment createReplidPickComment(CommentContents contents, Memb 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) From e5ebd34678083971c583c732b933c6ac998ba7d2 Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sun, 6 Jul 2025 15:11:33 +0900 Subject: [PATCH 11/66] =?UTF-8?q?document(PickCommentControllerDocsTest):?= =?UTF-8?q?=20=ED=94=BD=ED=94=BD=ED=94=BD=20=EB=8B=B5=EA=B8=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20API=20documentation=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8(=EC=9D=B5=EB=AA=85=ED=9A=8C=EC=9B=90=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=20=EC=B6=94=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/pick-commnet/pick-comment-reply-register.adoc | 5 +++-- .../devdevdev/web/docs/PickCommentControllerDocsTest.java | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) 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/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java index 76c81242..559fdc22 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java @@ -301,7 +301,8 @@ void registerPickRepliedComment() 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("픽픽픽 아이디"), @@ -367,7 +368,8 @@ void registerPickRepliedCommentBindException(String contents) 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("픽픽픽 아이디"), From 15addc22ed8a501db6ba4c38041a93ff3a80c056 Mon Sep 17 00:00:00 2001 From: soyoung Date: Sun, 6 Jul 2025 15:33:09 +0900 Subject: [PATCH 12/66] =?UTF-8?q?fix(nickname):=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/member/MemberService.java | 6 ++-- .../devdevdev/domain/entity/MemberTest.java | 33 +++++++++++++++++++ .../service/member/MemberServiceTest.java | 5 +++ 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java 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 39fa4418..90e259d3 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 @@ -298,10 +298,10 @@ public SliceCustom findMySubscribedCompanies(Pageable public void changeNickname(String nickname, Authentication authentication) { Member member = memberProvider.getMemberByAuthentication(authentication); - if (member.isAvailableToChangeNickname()) { - member.changeNickname(nickname, timeProvider.getLocalDateTimeNow()); - } else { + if (!member.isAvailableToChangeNickname()) { throw new NicknameException(NICKNAME_CHANGE_RATE_LIMIT_MESSAGE); } + + member.changeNickname(nickname, timeProvider.getLocalDateTimeNow()); } } 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..4ffe63e9 --- /dev/null +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java @@ -0,0 +1,33 @@ +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; +import org.junit.jupiter.api.Test; + +class MemberTest { + + @ParameterizedTest + @CsvSource({ + ", true", // 변경 이력 없음(null) + "0, false", // 24시간 이내 + "1, false", // 24시간 이내 + "24, true", // 24시간 경과(경계) + "25, true", // 24시간 초과 + }) + @DisplayName("닉네임 변경 가능 여부 파라미터 테스트") + void isAvailableToChangeNickname_Parameterized(Long hoursAgo, boolean expected) { + // given + Member member = new Member(); + if (hoursAgo != null) { + member.changeNickname("닉네임", LocalDateTime.now().minusHours(hoursAgo)); + } + // when + boolean result = member.isAvailableToChangeNickname(); + // then + assertThat(result).isEqualTo(expected); + } +} 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 97dac996..c96a244b 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 @@ -1186,9 +1186,11 @@ 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(), @@ -1215,10 +1217,13 @@ void changeNicknameThrowsExceptionWhenChangedWithin24Hours(long hoursAgo, boolea // given String oldNickname = "이전 닉네임"; String newNickname = "새 닉네임"; + SocialMemberDto socialMemberDto = createSocialDto(userId, name, oldNickname, password, email, socialType, role); Member member = Member.createMemberBy(socialMemberDto); + member.changeNickname(oldNickname, LocalDateTime.now().minusHours(hoursAgo)); memberRepository.save(member); + UserPrincipal userPrincipal = UserPrincipal.createByMember(member); SecurityContext context = SecurityContextHolder.getContext(); context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), From c08408047d66bf26e8f874a3e3caf984b98604ad Mon Sep 17 00:00:00 2001 From: soyoung Date: Sun, 6 Jul 2025 16:01:24 +0900 Subject: [PATCH 13/66] =?UTF-8?q?feat(nickname):=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=EB=B3=80=EA=B2=BD=20=EA=B0=80=EB=8A=A5=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/mypage/can-change-nickname.adoc | 19 +++++++++++ .../devdevdev/domain/entity/Member.java | 2 +- .../domain/service/member/MemberService.java | 12 ++++++- .../controller/member/MypageController.java | 8 +++++ .../devdevdev/domain/entity/MemberTest.java | 5 ++- .../service/member/MemberServiceTest.java | 33 ++++++++++++++++++ .../MyPageControllerUsedMockServiceTest.java | 21 ++++++++++++ ...PageControllerDocsUsedMockServiceTest.java | 34 +++++++++++++++++++ 8 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 src/docs/asciidoc/api/mypage/can-change-nickname.adoc 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..57387b1d --- /dev/null +++ b/src/docs/asciidoc/api/mypage/can-change-nickname.adoc @@ -0,0 +1,19 @@ +[[ChangeNickname]] +== 닉네임 변경 가능 여부 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/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java index beb7141a..0f7b6c26 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java @@ -196,7 +196,7 @@ public void changeNickname(String nickname, LocalDateTime now) { this.nicknameUpdatedAt = now; } - public boolean isAvailableToChangeNickname() { + public boolean canChangeNickname() { return nicknameUpdatedAt == null || ChronoUnit.HOURS.between(nicknameUpdatedAt, LocalDateTime.now()) >= 24; } 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 90e259d3..d47d2f82 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 @@ -298,10 +298,20 @@ public SliceCustom findMySubscribedCompanies(Pageable public void changeNickname(String nickname, Authentication authentication) { Member member = memberProvider.getMemberByAuthentication(authentication); - if (!member.isAvailableToChangeNickname()) { + if (!member.canChangeNickname()) { throw new NicknameException(NICKNAME_CHANGE_RATE_LIMIT_MESSAGE); } member.changeNickname(nickname, timeProvider.getLocalDateTimeNow()); } + + /** + * @Note: 유저가 닉네임을 변경할 수 있는지 여부를 반환합니다. + * @Author: 유소영 + * @Since: 2025.07.06 + */ + public boolean canChangeNickname(Authentication authentication) { + Member member = memberProvider.getMemberByAuthentication(authentication); + return member.canChangeNickname(); + } } 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 3708783a..3b0c6a60 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 @@ -154,4 +154,12 @@ public ResponseEntity> changeNickname( memberService.changeNickname(request.getNickname(), authentication); return ResponseEntity.ok(BasicResponse.success()); } + + @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/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java index 4ffe63e9..84f77efe 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.api.Test; class MemberTest { @@ -19,14 +18,14 @@ class MemberTest { "25, true", // 24시간 초과 }) @DisplayName("닉네임 변경 가능 여부 파라미터 테스트") - void isAvailableToChangeNickname_Parameterized(Long hoursAgo, boolean expected) { + void canChangeNickname(Long hoursAgo, boolean expected) { // given Member member = new Member(); if (hoursAgo != null) { member.changeNickname("닉네임", LocalDateTime.now().minusHours(hoursAgo)); } // when - boolean result = member.isAvailableToChangeNickname(); + boolean result = member.canChangeNickname(); // then assertThat(result).isEqualTo(expected); } 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 c96a244b..6bbaa0cf 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 @@ -1242,6 +1242,39 @@ void changeNicknameThrowsExceptionWhenChangedWithin24Hours(long hoursAgo, boolea } } + @DisplayName("회원의 닉네임 변경 가능 여부를 반환한다.") + @ParameterizedTest + @CsvSource({ + "0, false", + "1, false", + "23, false", + "24, true", + "25, true" + }) + void canChangeNickname(long hoursAgo, boolean expected) { + // given + String oldNickname = "이전 닉네임"; + String newNickname = "새 닉네임"; + + SocialMemberDto socialMemberDto = createSocialDto(userId, name, oldNickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + + member.changeNickname(newNickname, LocalDateTime.now().minusHours(hoursAgo)); + 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/web/controller/member/MyPageControllerUsedMockServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java index 47c9d847..98922142 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java @@ -131,6 +131,27 @@ void changeNicknameThrowsExceptionWhenChangedWithin24Hours() throws Exception { 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("회원이 내가 썼어요 댓글을 조회한다.") void getMyWrittenComments() throws Exception { 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 42e9065e..388b4c0c 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsUsedMockServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsUsedMockServiceTest.java @@ -6,6 +6,7 @@ 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.*; @@ -168,6 +169,39 @@ void changeNicknameThrowsExceptionWhenChangedWithin24Hours() throws Exception { )); } + @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("회원이 내가 썼어요 댓글을 조회한다.") void getMyWrittenComments() throws Exception { From e94282c2641d2be7d2b9b9af7700039214b14b27 Mon Sep 17 00:00:00 2001 From: soyoung Date: Sun, 6 Jul 2025 16:05:53 +0900 Subject: [PATCH 14/66] =?UTF-8?q?docs(nickname):=20RestDocs=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/api/mypage/can-change-nickname.adoc | 2 +- src/docs/asciidoc/api/mypage/mypage.adoc | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/docs/asciidoc/api/mypage/can-change-nickname.adoc b/src/docs/asciidoc/api/mypage/can-change-nickname.adoc index 57387b1d..c2810804 100644 --- a/src/docs/asciidoc/api/mypage/can-change-nickname.adoc +++ b/src/docs/asciidoc/api/mypage/can-change-nickname.adoc @@ -1,4 +1,4 @@ -[[ChangeNickname]] +[[CanChangeNickname]] == 닉네임 변경 가능 여부 API(GET: /devdevdev/api/v1/mypage/nickname/changeable) * 회원은 닉네임 변경 가능 여부를 확인할 수 있다. * 비회원은 닉네임 변경 가능 여부를 확인할 수 없다. diff --git a/src/docs/asciidoc/api/mypage/mypage.adoc b/src/docs/asciidoc/api/mypage/mypage.adoc index a8fb562a..5dfd81ca 100644 --- a/src/docs/asciidoc/api/mypage/mypage.adoc +++ b/src/docs/asciidoc/api/mypage/mypage.adoc @@ -8,3 +8,5 @@ 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[] From 9ee885c3bd3570b16cbc47b098d02e6cf3bbd97f Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sun, 6 Jul 2025 16:20:01 +0900 Subject: [PATCH 15/66] =?UTF-8?q?feat(GuestPickCommentServiceV2):=20?= =?UTF-8?q?=EC=9D=B5=EB=AA=85=ED=9A=8C=EC=9B=90=20=ED=94=BD=ED=94=BD?= =?UTF-8?q?=ED=94=BD=20=EB=8C=93=EA=B8=80/=EB=8B=B5=EA=B8=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B0=9C=EB=B0=9C=20?= =?UTF-8?q?=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pick/PickCommentRepository.java | 4 + .../service/pick/GuestPickCommentService.java | 9 +- .../pick/GuestPickCommentServiceV2.java | 38 ++- .../domain/service/pick/GuestPickService.java | 6 +- .../pick/MemberPickCommentService.java | 11 +- .../service/pick/MemberPickService.java | 6 +- .../service/pick/PickCommentService.java | 21 +- .../service/pick/PickCommonService.java | 2 + .../service/pick/dto/PickCommentDto.java | 9 + .../pick/PickCommentController.java | 6 +- .../pick/GuestPickCommentServiceV2Test.java | 224 ++++++++++++++++++ .../pick/MemberPickCommentServiceTest.java | 18 +- .../domain/service/pick/PickTestUtils.java | 15 ++ 13 files changed, 321 insertions(+), 48 deletions(-) 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..ce8dc401 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,6 +16,10 @@ 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"}) 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 eaa67ba9..f6fbb5d9 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 @@ -10,10 +10,10 @@ 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.response.pick.PickCommentRecommendResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentsResponse; @@ -32,12 +32,13 @@ 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, pickPopularScorePolicy, pickRepository, pickCommentRepository, - pickCommentRecommendRepository); + super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, timeProvider, pickRepository, + pickCommentRepository, pickCommentRecommendRepository); } @Override @@ -56,7 +57,7 @@ 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); 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 index 5f5734f3..98b145f3 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java @@ -2,6 +2,7 @@ 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; @@ -21,10 +22,10 @@ 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.request.pick.ModifyPickCommentRequest; 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; @@ -46,13 +47,14 @@ public class GuestPickCommentServiceV2 extends PickCommonService implements Pick public GuestPickCommentServiceV2(EmbeddingsService embeddingsService, PickBestCommentsPolicy pickBestCommentsPolicy, + TimeProvider timeProvider, PickRepository pickRepository, PickCommentRepository pickCommentRepository, PickCommentRecommendRepository pickCommentRecommendRepository, AnonymousMemberService anonymousMemberService, PickPopularScorePolicy pickPopularScorePolicy, PickVoteRepository pickVoteRepository) { - super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, pickRepository, + super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, timeProvider, pickRepository, pickCommentRepository, pickCommentRecommendRepository); this.anonymousMemberService = anonymousMemberService; this.pickVoteRepository = pickVoteRepository; @@ -109,14 +111,14 @@ public PickCommentResponse registerPickComment(Long pickId, PickCommentDto pickC @Override @Transactional public PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, Long pickCommentOriginParentId, - Long pickId, PickCommentDto pickCommentDto, + Long pickId, PickCommentDto pickRegisterRepliedCommentDto, Authentication authentication) { // 익명 회원인지 검증 AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); - String contents = pickCommentDto.getContents(); - String anonymousMemberId = pickCommentDto.getAnonymousMemberId(); + String contents = pickRegisterRepliedCommentDto.getContents(); + String anonymousMemberId = pickRegisterRepliedCommentDto.getAnonymousMemberId(); // 익명 회원 추출 AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); @@ -137,11 +139,31 @@ public PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, } @Override - public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, - ModifyPickCommentRequest modifyPickCommentRequest, + public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, PickCommentDto pickCommentDto, Authentication authentication) { - throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); + // 익명 회원인지 검증 + AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); + + String contents = pickCommentDto.getContents(); + String anonymousMemberId = pickCommentDto.getAnonymousMemberId(); + + // 익명 회원 추출 + AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + + // 픽픽픽 댓글 조회(익명 회원 본인이 댓글 작성, 삭제되지 않은 댓글) + PickComment findPickComment = pickCommentRepository.findWithPickByIdAndPickIdAndCreatedAnonymousByIdAndDeletedAtIsNull( + pickCommentId, pickId, anonymousMember.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 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 cbad78dc..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 @@ -56,7 +56,6 @@ public class GuestPickService extends PickCommonService implements PickService { public static final String INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE = "비회원은 현재 해당 기능을 이용할 수 없습니다."; private final PickVoteRepository pickVoteRepository; - private final TimeProvider timeProvider; private final AnonymousMemberService anonymousMemberService; public GuestPickService(PickRepository pickRepository, EmbeddingsService embeddingsService, @@ -66,11 +65,10 @@ public GuestPickService(PickRepository pickRepository, EmbeddingsService embeddi PickPopularScorePolicy pickPopularScorePolicy, PickVoteRepository pickVoteRepository, TimeProvider timeProvider, AnonymousMemberService anonymousMemberService) { - super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, pickRepository, pickCommentRepository, - pickCommentRecommendRepository); + 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 4a109d4a..143b9aae 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 @@ -26,7 +26,6 @@ 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.response.pick.PickCommentRecommendResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentsResponse; @@ -42,7 +41,6 @@ @Transactional(readOnly = true) public class MemberPickCommentService extends PickCommonService implements PickCommentService { - private final TimeProvider timeProvider; private final MemberProvider memberProvider; private final PickRepository pickRepository; @@ -55,9 +53,8 @@ public MemberPickCommentService(TimeProvider timeProvider, MemberProvider member PickRepository pickRepository, PickVoteRepository pickVoteRepository, PickCommentRepository pickCommentRepository, PickCommentRecommendRepository pickCommentRecommendRepository) { - super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, pickRepository, pickCommentRepository, - pickCommentRecommendRepository); - this.timeProvider = timeProvider; + super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, timeProvider, pickRepository, + pickCommentRepository, pickCommentRecommendRepository); this.memberProvider = memberProvider; this.pickRepository = pickRepository; this.pickVoteRepository = pickVoteRepository; @@ -151,10 +148,10 @@ public PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, */ @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); 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 3bf47be8..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 @@ -86,8 +86,6 @@ public class MemberPickService extends PickCommonService implements PickService private final PickOptionImageRepository pickOptionImageRepository; private final PickVoteRepository pickVoteRepository; - private final TimeProvider timeProvider; - public MemberPickService(EmbeddingsService embeddingsService, PickRepository pickRepository, AwsS3Properties awsS3Properties, AwsS3Uploader awsS3Uploader, MemberProvider memberProvider, PickOptionRepository pickOptionRepository, @@ -96,7 +94,8 @@ public MemberPickService(EmbeddingsService embeddingsService, PickRepository pic PickCommentRecommendRepository pickCommentRecommendRepository, PickPopularScorePolicy pickPopularScorePolicy, PickBestCommentsPolicy pickBestCommentsPolicy, TimeProvider timeProvider) { - super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, pickRepository, pickCommentRepository, + super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, timeProvider, pickRepository, + pickCommentRepository, pickCommentRecommendRepository); this.awsS3Properties = awsS3Properties; this.awsS3Uploader = awsS3Uploader; @@ -104,7 +103,6 @@ public MemberPickService(EmbeddingsService embeddingsService, PickRepository pic this.pickOptionRepository = pickOptionRepository; this.pickOptionImageRepository = pickOptionImageRepository; this.pickVoteRepository = pickVoteRepository; - this.timeProvider = timeProvider; } /** 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 2311132e..aaa6cb28 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 @@ -4,7 +4,6 @@ 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.response.pick.PickCommentRecommendResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentsResponse; @@ -19,28 +18,22 @@ public interface PickCommentService { String DELETE = "삭제"; String RECOMMEND = "추천"; - PickCommentResponse registerPickComment(Long pickId, - PickCommentDto pickRegisterCommentDto, - Authentication authentication); + PickCommentResponse registerPickComment(Long pickId, PickCommentDto pickRegisterCommentDto, Authentication authentication); - PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, - Long pickCommentOriginParentId, - Long pickId, PickCommentDto pickCommentDto, + 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); - SliceCustom findPickComments(Pageable pageable, Long pickId, - Long pickCommentId, PickCommentSort pickCommentSort, - EnumSet pickOptionTypes, + SliceCustom findPickComments(Pageable pageable, Long pickId, Long pickCommentId, + PickCommentSort pickCommentSort, EnumSet pickOptionTypes, 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); } 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 89c73faf..87c7b332 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 @@ -20,6 +20,7 @@ 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; @@ -52,6 +53,7 @@ public class PickCommonService { private final PickBestCommentsPolicy pickBestCommentsPolicy; protected final PickPopularScorePolicy pickPopularScorePolicy; + protected final TimeProvider timeProvider; protected final PickRepository pickRepository; protected final PickCommentRepository pickCommentRepository; protected final PickCommentRecommendRepository pickCommentRecommendRepository; 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 index 8895b8fb..9e348b05 100644 --- 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 @@ -1,5 +1,6 @@ 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; @@ -34,4 +35,12 @@ public static PickCommentDto createRepliedCommentDto(RegisterPickRepliedCommentR .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/web/controller/pick/PickCommentController.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/pick/PickCommentController.java index 7b7d9972..133c1514 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 @@ -95,10 +95,14 @@ 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)); } 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 index 6b2433f0..edfb86e4 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java @@ -6,6 +6,7 @@ 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.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; @@ -53,6 +54,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.AuthenticationMemberUtils; +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 jakarta.persistence.EntityManager; @@ -646,4 +648,226 @@ void registerPickRepliedCommentRepliedNotFoundException() { .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); + } } \ 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 5b1f0c11..ff81c9a4 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 @@ -710,10 +710,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(); @@ -742,9 +743,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); @@ -779,9 +781,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); @@ -820,9 +823,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); @@ -862,9 +866,10 @@ void modifyPickCommentNotFoundPickCommentIsDeletedAt() { 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); @@ -904,9 +909,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); 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 index 1c6b7599..cadb37a0 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickTestUtils.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickTestUtils.java @@ -156,6 +156,21 @@ public static PickComment createPickComment(CommentContents contents, Boolean is 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) From 7dbb2d3feb465754e17e24c44cc30ab9b5e132cc Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sun, 6 Jul 2025 16:28:57 +0900 Subject: [PATCH 16/66] =?UTF-8?q?document(PickCommentControllerDocsTest):?= =?UTF-8?q?=20=ED=94=BD=ED=94=BD=ED=94=BD=20=EB=8C=93=EA=B8=80/=EB=8B=B5?= =?UTF-8?q?=EA=B8=80=20=EC=88=98=EC=A0=95=20API=20=EC=9D=B5=EB=AA=85?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EA=B4=80=EB=A0=A8=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../asciidoc/api/pick-commnet/pick-comment-modify.adoc | 5 ++++- .../web/controller/pick/PickCommentController.java | 4 ++-- .../devdevdev/web/docs/PickCommentControllerDocsTest.java | 7 ++++--- 3 files changed, 10 insertions(+), 6 deletions(-) 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..c6546a80 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` 헤더에 익명 회원 아이디를 포함시켜야 한다. +* 회원 또는 익명회원 본인이 작성한 픽픽픽 댓글/답글 만 수정 할 수 있다. * 픽픽픽 공개 여부는 수정 할 수 없다. * 삭제된 댓글/답글을 수정 할 수 없다. @@ -41,5 +43,6 @@ include::{snippets}/modify-pick-comment/response-fields.adoc[] * `승인 상태가 아닌 픽픽픽에는 댓글을 수정할 수 없습니다.`: 픽픽픽이 승인 상태가 아닌 경우 * `익명 회원은 사용할 수 없는 기능 입니다.`: 익명 회원인 경우 * `회원을 찾을 수 없습니다.`: 회원이 존재하지 않는 경우 +* `익명 사용자가 아닙니다. 잘못된 메소드 호출 입니다.`: 회원이 익명 회원 메소드를 호출한 경우 include::{snippets}/modify-pick-comment-bind-exception/response-body.adoc[] \ No newline at end of file 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 133c1514..610e468a 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 @@ -87,7 +87,7 @@ public ResponseEntity> registerPickRepliedCom return ResponseEntity.ok(BasicResponse.success(pickCommentResponse)); } - @Operation(summary = "픽픽픽 댓글/답글 수정", description = "회원은 자신이 작성한 픽픽픽 댓글/답글을 수정할 수 있습니다.") + @Operation(summary = "픽픽픽 댓글/답글 수정", description = "회원/익명회원 본인이 작성한 픽픽픽 댓글/답글만 수정할 수 있습니다.") @PatchMapping("/picks/{pickId}/comments/{pickCommentId}") public ResponseEntity> modifyPickComment( @PathVariable Long pickId, @@ -107,7 +107,7 @@ public ResponseEntity> modifyPickComment( 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, 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 559fdc22..ece419fc 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java @@ -61,7 +61,6 @@ import com.dreamypatisiel.devdevdev.domain.repository.pick.PickVoteRepository; import com.dreamypatisiel.devdevdev.global.constant.SecurityConstant; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; -import com.dreamypatisiel.devdevdev.web.WebConstant; 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; @@ -421,7 +420,8 @@ void modifyPickComment() 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("픽픽픽 아이디"), @@ -477,7 +477,8 @@ void modifyPickCommentBindException(String contents) 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("픽픽픽 아이디"), From 9b63b6060b1b015f5922657830f0b9c4f43205ba Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sun, 6 Jul 2025 17:13:43 +0900 Subject: [PATCH 17/66] =?UTF-8?q?fix(PR):=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pick/GuestPickCommentServiceV2Test.java | 22 +++++++++---------- .../docs/PickCommentControllerDocsTest.java | 8 +++---- 2 files changed, 15 insertions(+), 15 deletions(-) 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 index 6b2433f0..edfbce7b 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java @@ -398,9 +398,9 @@ void registerPickRepliedComment(Boolean isPublic) { pickCommentRepository.save(pickComment); // 픽픽픽 답글 생성 - PickComment replidPickComment = createReplidPickComment(new CommentContents("댓글1의 답글1"), anonymousMember, pick, + PickComment repliedPickComment = createReplidPickComment(new CommentContents("댓글1의 답글1"), anonymousMember, pick, pickComment, pickComment); - pickCommentRepository.save(replidPickComment); + pickCommentRepository.save(repliedPickComment); em.flush(); em.clear(); @@ -410,7 +410,7 @@ void registerPickRepliedComment(Boolean isPublic) { // when PickCommentResponse response = guestPickCommentServiceV2.registerPickRepliedComment( - replidPickComment.getId(), pickComment.getId(), pick.getId(), repliedCommentDto, authentication); + repliedPickComment.getId(), pickComment.getId(), pick.getId(), repliedCommentDto, authentication); em.flush(); em.clear(); @@ -427,7 +427,7 @@ void registerPickRepliedComment(Boolean isPublic) { () -> 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.getParent().getId()).isEqualTo(repliedPickComment.getId()), () -> assertThat(findPickComment.getOriginParent().getId()).isEqualTo(pickComment.getId()), () -> assertThat(findPickComment.getOriginParent().getReplyTotalCount().getCount()).isEqualTo(1L) ); @@ -589,10 +589,10 @@ void registerPickRepliedCommentRepliedDeleted() { pickCommentRepository.save(pickComment); // 삭제상태의 픽픽픽 댓글의 답글 생성 - PickComment replidPickComment = createReplidPickComment(new CommentContents("댓글1의 답글"), member, pick, + PickComment repliedPickComment = createReplidPickComment(new CommentContents("댓글1의 답글"), member, pick, pickComment, pickComment); - replidPickComment.changeDeletedAtByMember(LocalDateTime.now(), member); - pickCommentRepository.save(replidPickComment); + repliedPickComment.changeDeletedAtByMember(LocalDateTime.now(), member); + pickCommentRepository.save(repliedPickComment); RegisterPickRepliedCommentRequest request = new RegisterPickRepliedCommentRequest("댓글1의 답글1의 답글"); PickCommentDto repliedCommentDto = PickCommentDto.createRepliedCommentDto(request, "anonymousMemberId"); @@ -600,7 +600,7 @@ void registerPickRepliedCommentRepliedDeleted() { // when // then assertThatThrownBy( () -> guestPickCommentServiceV2.registerPickRepliedComment( - replidPickComment.getId(), pickComment.getId(), pick.getId(), repliedCommentDto, authentication)) + repliedPickComment.getId(), pickComment.getId(), pick.getId(), repliedCommentDto, authentication)) .isInstanceOf(IllegalArgumentException.class) .hasMessage(INVALID_CAN_NOT_REPLY_DELETED_PICK_COMMENT_MESSAGE, REGISTER); } @@ -631,10 +631,10 @@ void registerPickRepliedCommentRepliedNotFoundException() { pickCommentRepository.save(pickComment); // 삭제상태의 픽픽픽 댓글의 답글(삭제 상태) - PickComment replidPickComment = createReplidPickComment(new CommentContents("댓글1의 답글"), member, pick, + PickComment repliedPickComment = createReplidPickComment(new CommentContents("댓글1의 답글"), member, pick, pickComment, pickComment); - pickCommentRepository.save(replidPickComment); - replidPickComment.changeDeletedAtByMember(LocalDateTime.now(), member); + pickCommentRepository.save(repliedPickComment); + repliedPickComment.changeDeletedAtByMember(LocalDateTime.now(), member); RegisterPickRepliedCommentRequest request = new RegisterPickRepliedCommentRequest("댓글1의 답글1의 답글"); PickCommentDto repliedCommentDto = PickCommentDto.createRepliedCommentDto(request, "anonymousMemberId"); 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 559fdc22..f2f1264a 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java @@ -61,7 +61,6 @@ import com.dreamypatisiel.devdevdev.domain.repository.pick.PickVoteRepository; import com.dreamypatisiel.devdevdev.global.constant.SecurityConstant; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; -import com.dreamypatisiel.devdevdev.web.WebConstant; 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; @@ -239,7 +238,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("픽픽픽 아이디") @@ -301,7 +301,7 @@ 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( @@ -368,7 +368,7 @@ 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( From be2ed0ee59f35a3a898c2fdfa6174db3fa6d4094 Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sun, 6 Jul 2025 21:44:37 +0900 Subject: [PATCH 18/66] =?UTF-8?q?fix(PR):=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/docs/PickCommentControllerDocsTest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 ece419fc..9bb0ead0 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java @@ -238,7 +238,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("픽픽픽 아이디") @@ -300,7 +301,7 @@ 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( @@ -367,7 +368,7 @@ 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( @@ -420,7 +421,7 @@ 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( @@ -477,7 +478,7 @@ 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( From dbb536d3b36cb902b0bc570961094277176f863f Mon Sep 17 00:00:00 2001 From: soyoung Date: Wed, 9 Jul 2025 23:19:12 +0900 Subject: [PATCH 19/66] =?UTF-8?q?fix(login):=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EC=8B=9C=20=EC=8B=A0=EA=B7=9C=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=EC=BF=A0=ED=82=A4=EC=97=90=20=ED=8F=AC?= =?UTF-8?q?=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/jwt/model/JwtCookieConstant.java | 1 + .../oauth2/handler/OAuth2SuccessHandler.java | 7 ++++++- .../security/oauth2/model/UserPrincipal.java | 5 ++++- .../oauth2/service/OAuth2MemberService.java | 4 ++-- .../devdevdev/global/utils/CookieUtils.java | 4 +++- .../oauth2/handler/OAuth2SuccessHandlerTest.java | 7 +++++-- .../security/oauth2/model/UserPrincipalTest.java | 2 +- .../service/AppOAuth2MemberServiceTest.java | 5 ++++- .../devdevdev/global/utils/CookieUtilsTest.java | 15 +++++++++++++-- 9 files changed, 39 insertions(+), 11 deletions(-) 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/CookieUtils.java b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/CookieUtils.java index e12b39dd..fcdabd09 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/global/utils/CookieUtils.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/CookieUtils.java @@ -99,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); @@ -109,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/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 f3fad464..d55d547e 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/global/utils/CookieUtilsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/global/utils/CookieUtilsTest.java @@ -267,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( @@ -305,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, From 893ca217613ddc9bdbd4ad6dd1d701380171e89c Mon Sep 17 00:00:00 2001 From: ralph Date: Wed, 9 Jul 2025 23:47:21 +0900 Subject: [PATCH 20/66] fix(PickCommentService): deletePickComment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 익명회원 댓글 삭제 서비스 개발 및 테스트 코드 작성 --- .../service/pick/GuestPickCommentService.java | 7 +- .../pick/GuestPickCommentServiceV2.java | 48 ++-- .../pick/MemberPickCommentService.java | 4 +- .../service/pick/PickCommentService.java | 4 +- .../pick/PickCommentController.java | 3 +- .../pick/GuestPickCommentServiceV2Test.java | 243 ++++++++++++++++++ .../pick/MemberPickCommentServiceTest.java | 16 +- 7 files changed, 295 insertions(+), 30 deletions(-) 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 f6fbb5d9..822e5225 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 @@ -19,6 +19,7 @@ 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; @@ -64,7 +65,8 @@ public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, } @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); } @@ -87,8 +89,7 @@ public SliceCustom findPickComments(Pageable pageable, Lon } @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); } 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 index 98b145f3..a2c4b409 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java @@ -31,6 +31,7 @@ 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; @@ -72,7 +73,7 @@ public PickCommentResponse registerPickComment(Long pickId, PickCommentDto pickC Boolean isPickVotePublic = pickCommentDto.getIsPickVotePublic(); // 익명 회원 추출 - AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); // 픽픽픽 조회 Pick findPick = pickRepository.findById(pickId) @@ -89,12 +90,12 @@ public PickCommentResponse registerPickComment(Long pickId, PickCommentDto pickC if (isPickVotePublic) { // 익명회원이 투표한 픽픽픽 투표 조회 PickVote findPickVote = pickVoteRepository.findWithPickAndPickOptionByPickIdAndAnonymousMemberAndDeletedAtIsNull( - pickId, anonymousMember) + pickId, findAnonymousMember) .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_VOTE_MESSAGE)); // 픽픽픽 투표한 픽 옵션의 댓글 작성 PickComment pickComment = PickComment.createPublicVoteCommentByAnonymousMember(new CommentContents(contents), - anonymousMember, findPick, findPickVote); + findAnonymousMember, findPick, findPickVote); pickCommentRepository.save(pickComment); return new PickCommentResponse(pickComment.getId()); @@ -102,7 +103,7 @@ public PickCommentResponse registerPickComment(Long pickId, PickCommentDto pickC // 픽픽픽 선택지 투표 비공개인 경우 PickComment pickComment = PickComment.createPrivateVoteCommentByAnonymousMember(new CommentContents(contents), - anonymousMember, findPick); + findAnonymousMember, findPick); pickCommentRepository.save(pickComment); return new PickCommentResponse(pickComment.getId()); @@ -121,7 +122,7 @@ public PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, String anonymousMemberId = pickRegisterRepliedCommentDto.getAnonymousMemberId(); // 익명 회원 추출 - AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); // 픽픽픽 댓글 로직 수행 PickReplyContext pickReplyContext = prepareForReplyRegistration(pickParentCommentId, pickCommentOriginParentId, pickId); @@ -132,7 +133,7 @@ public PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, // 픽픽픽 서브 댓글(답글) 생성 PickComment pickRepliedComment = PickComment.createRepliedCommentByAnonymousMember(new CommentContents(contents), - findParentPickComment, findOriginParentPickComment, anonymousMember, findPick); + findParentPickComment, findOriginParentPickComment, findAnonymousMember, findPick); pickCommentRepository.save(pickRepliedComment); return new PickCommentResponse(pickRepliedComment.getId()); @@ -149,16 +150,15 @@ public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, Pi String anonymousMemberId = pickCommentDto.getAnonymousMemberId(); // 익명 회원 추출 - AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); // 픽픽픽 댓글 조회(익명 회원 본인이 댓글 작성, 삭제되지 않은 댓글) PickComment findPickComment = pickCommentRepository.findWithPickByIdAndPickIdAndCreatedAnonymousByIdAndDeletedAtIsNull( - pickCommentId, pickId, anonymousMember.getId()) + pickCommentId, pickId, findAnonymousMember.getId()) .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE)); // 픽픽픽 게시글의 승인 상태 검증 - validateIsApprovalPickContentStatus(findPickComment.getPick(), INVALID_NOT_APPROVAL_STATUS_PICK_COMMENT_MESSAGE, - MODIFY); + validateIsApprovalPickContentStatus(findPickComment.getPick(), INVALID_NOT_APPROVAL_STATUS_PICK_COMMENT_MESSAGE, MODIFY); // 댓글 수정 findPickComment.modifyCommentContents(new CommentContents(contents), timeProvider.getLocalDateTimeNow()); @@ -167,8 +167,26 @@ public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, Pi } @Override - public PickCommentResponse deletePickComment(Long pickCommentId, Long pickId, Authentication authentication) { - throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); + 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()); } /** @@ -190,8 +208,7 @@ public SliceCustom findPickComments(Pageable pageable, Lon } @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); } @@ -202,8 +219,7 @@ 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, Authentication authentication) { // 익명 회원인지 검증 AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); 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 143b9aae..fef8f76f 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 @@ -32,6 +32,7 @@ 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; @@ -177,7 +178,8 @@ public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, * @Since: 2024.08.11 */ @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); 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 aaa6cb28..43415c0c 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 @@ -9,6 +9,7 @@ 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; @@ -27,7 +28,8 @@ PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, Long pi 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, 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 610e468a..65b4dd64 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 @@ -132,10 +132,11 @@ public ResponseEntity> deletePickComment( @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)); } 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 index edfb86e4..6a35e1c7 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java @@ -6,6 +6,7 @@ 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; @@ -870,4 +871,246 @@ void modifyPickCommentNotApproval(ContentStatus contentStatus) { .isInstanceOf(IllegalArgumentException.class) .hasMessage(INVALID_NOT_APPROVAL_STATUS_PICK_COMMENT_MESSAGE, MODIFY); } + + @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 ff81c9a4..30a6fbd6 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 @@ -953,7 +953,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(); @@ -1000,7 +1000,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(); @@ -1029,7 +1029,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); } @@ -1064,7 +1064,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); } @@ -1103,7 +1103,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); @@ -1143,7 +1143,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); } @@ -1183,7 +1183,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); } @@ -1224,7 +1224,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); } From 99b986a98a70e691d4c0b04e301939d3bb6056d4 Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sat, 12 Jul 2025 17:37:22 +0900 Subject: [PATCH 21/66] =?UTF-8?q?fix(PickCommentControllerDocsTest):=20?= =?UTF-8?q?=ED=94=BD=ED=94=BD=ED=94=BD=20=EB=8C=93=EA=B8=80/=EB=8B=B5?= =?UTF-8?q?=EA=B8=80=20=EC=82=AD=EC=A0=9C=20API=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../asciidoc/api/pick-commnet/pick-comment-delete.adoc | 6 ++++-- .../asciidoc/api/pick-commnet/pick-comment-modify.adoc | 1 - .../web/controller/pick/PickCommentController.java | 7 +++---- .../devdevdev/web/docs/PickCommentControllerDocsTest.java | 6 ++++-- 4 files changed, 11 insertions(+), 9 deletions(-) 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 c6546a80..9605aa83 100644 --- a/src/docs/asciidoc/api/pick-commnet/pick-comment-modify.adoc +++ b/src/docs/asciidoc/api/pick-commnet/pick-comment-modify.adoc @@ -41,7 +41,6 @@ include::{snippets}/modify-pick-comment/response-fields.adoc[] * `내용을 작성해주세요.`: 댓글(contents)을 작성하지 않는 경우(공백 이거나 빈문자열) * `픽픽픽 댓글이 없습니다.`: 픽픽픽 댓글이 존재하지 않거나 본인이 작성하지 않았거나 픽픽픽 댓글 삭제된 경우 * `승인 상태가 아닌 픽픽픽에는 댓글을 수정할 수 없습니다.`: 픽픽픽이 승인 상태가 아닌 경우 -* `익명 회원은 사용할 수 없는 기능 입니다.`: 익명 회원인 경우 * `회원을 찾을 수 없습니다.`: 회원이 존재하지 않는 경우 * `익명 사용자가 아닙니다. 잘못된 메소드 호출 입니다.`: 회원이 익명 회원 메소드를 호출한 경우 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 65b4dd64..cf36df17 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 @@ -125,11 +125,10 @@ public ResponseEntity>> getPickC 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); 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 9bb0ead0..f9c43a14 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java @@ -527,7 +527,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("픽픽픽 아이디"), @@ -582,7 +583,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("픽픽픽 아이디"), From 608cf3d2ce919e12e66b34d64cad6feafc640f42 Mon Sep 17 00:00:00 2001 From: soyoung Date: Sun, 13 Jul 2025 16:16:38 +0900 Subject: [PATCH 22/66] =?UTF-8?q?fix(nickname):=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=EB=B3=80=EA=B2=BD=20API=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD(=EB=B3=80=EA=B2=BD=EB=90=9C=20=EB=8B=89?= =?UTF-8?q?=EB=84=A4=EC=9E=84=20=EB=AC=B8=EC=9E=90=EC=97=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devdevdev/domain/service/member/MemberService.java | 3 ++- .../devdevdev/web/controller/member/MypageController.java | 6 +++--- .../devdevdev/domain/service/member/MemberServiceTest.java | 3 ++- .../member/MyPageControllerUsedMockServiceTest.java | 5 +++-- .../web/docs/MyPageControllerDocsUsedMockServiceTest.java | 5 +++-- 5 files changed, 13 insertions(+), 9 deletions(-) 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 d47d2f82..622c5439 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 @@ -295,7 +295,7 @@ public SliceCustom findMySubscribedCompanies(Pageable * @Since: 2025.07.03 */ @Transactional - public void changeNickname(String nickname, Authentication authentication) { + public String changeNickname(String nickname, Authentication authentication) { Member member = memberProvider.getMemberByAuthentication(authentication); if (!member.canChangeNickname()) { @@ -303,6 +303,7 @@ public void changeNickname(String nickname, Authentication authentication) { } member.changeNickname(nickname, timeProvider.getLocalDateTimeNow()); + return member.getNicknameAsString(); } /** 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 3b0c6a60..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 @@ -147,12 +147,12 @@ public ResponseEntity> getRandomNickname() { @Operation(summary = "닉네임 변경", description = "유저의 닉네임을 변경합니다.") @PatchMapping("/mypage/nickname") - public ResponseEntity> changeNickname( + public ResponseEntity> changeNickname( @RequestBody @Valid ChangeNicknameRequest request ) { Authentication authentication = AuthenticationMemberUtils.getAuthentication(); - memberService.changeNickname(request.getNickname(), authentication); - return ResponseEntity.ok(BasicResponse.success()); + String response = memberService.changeNickname(request.getNickname(), authentication); + return ResponseEntity.ok(BasicResponse.success(response)); } @Operation(summary = "닉네임 변경 가능 여부 조회", description = "닉네임 변경 가능 여부를 true/false로 반환합니다.") 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 6bbaa0cf..47e17764 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 @@ -1198,10 +1198,11 @@ void changeNickname() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // when - memberService.changeNickname(newNickname, authentication); + String changedNickname = memberService.changeNickname(newNickname, authentication); // then assertThat(member.getNickname().getNickname()).isEqualTo(newNickname); + assertThat(changedNickname).isEqualTo(newNickname); } @DisplayName("회원이 24시간 이내에 닉네임을 변경한 적이 있다면 예외가 발생한다.") diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java index 98922142..fe0b13b2 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java @@ -85,7 +85,7 @@ void changeNickname() throws Exception { request.setNickname(newNickname); // when - doNothing().when(memberService).changeNickname(any(), any()); + when(memberService.changeNickname(any(), any())).thenReturn(newNickname); // then mockMvc.perform(patch("/devdevdev/api/v1/mypage/nickname") @@ -96,7 +96,8 @@ void changeNickname() throws Exception { .andExpect(status().isOk()) .andDo(print()) .andExpect(status().isOk()) - .andExpect(jsonPath("$.resultType").value(SUCCESS.name())); + .andExpect(jsonPath("$.resultType").value(SUCCESS.name())) + .andExpect(jsonPath("$.data").value(newNickname)); // 서비스 메서드가 호출되었는지 검증 verify(memberService).changeNickname(eq(newNickname), any()); 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 388b4c0c..926d5e03 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsUsedMockServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsUsedMockServiceTest.java @@ -106,7 +106,7 @@ void changeNickname() throws Exception { ChangeNicknameRequest request = createChangeNicknameRequest(newNickname); // when - doNothing().when(memberService).changeNickname(any(), any()); + when(memberService.changeNickname(any(), any())).thenReturn(newNickname); // then mockMvc.perform(patch("/devdevdev/api/v1/mypage/nickname") @@ -123,7 +123,8 @@ void changeNickname() throws Exception { headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") ), responseFields( - fieldWithPath("resultType").description("성공 여부") + fieldWithPath("resultType").description("성공 여부"), + fieldWithPath("data").description("변경된 닉네임") ) )); From 849d849d53a0943223d841070e09a3cefdc27ab9 Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sun, 13 Jul 2025 19:02:46 +0900 Subject: [PATCH 23/66] =?UTF-8?q?feat(PickCommentService):=20findPickComme?= =?UTF-8?q?nts,=20findPickBestComments=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/entity/AnonymousMember.java | 2 +- .../devdevdev/domain/entity/Member.java | 2 +- .../devdevdev/domain/entity/PickComment.java | 12 +- .../pick/PickCommentRepository.java | 6 +- .../custom/PickCommentRepositoryImpl.java | 17 +- .../member/AnonymousMemberService.java | 2 +- .../service/pick/GuestPickCommentService.java | 7 +- .../pick/GuestPickCommentServiceV2.java | 18 +- .../pick/MemberPickCommentService.java | 14 +- .../service/pick/PickCommentService.java | 5 +- .../service/pick/PickCommonService.java | 38 +- .../pick/PickCommentController.java | 11 +- .../response/pick/PickCommentsResponse.java | 62 +- .../pick/PickRepliedCommentsResponse.java | 137 ++- .../web/dto/util/CommentResponseUtil.java | 68 +- .../pick/GuestPickCommentServiceTest.java | 13 +- .../pick/GuestPickCommentServiceV2Test.java | 979 ++++++++++++++++++ .../pick/MemberPickCommentServiceTest.java | 11 +- .../domain/service/pick/PickTestUtils.java | 18 + .../pick/PickCommentControllerTest.java | 6 + .../docs/PickCommentControllerDocsTest.java | 28 +- 21 files changed, 1350 insertions(+), 106 deletions(-) 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 8fc113db..1a833999 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/AnonymousMember.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/AnonymousMember.java @@ -47,7 +47,7 @@ public boolean isEqualAnonymousMemberId(Long id) { } public boolean hasNickName() { - return nickname == null || nickname.isBlank(); + return nickname != null && !nickname.isBlank(); } public void changeNickname(String nickname) { 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 0f7b6c26..7ba25497 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java @@ -113,7 +113,7 @@ public class Member extends BasicTime { @OneToMany(mappedBy = "member") private List recommends = new ArrayList<>(); - + public Member(Long id) { this.id = id; } 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 ec539ac8..f07e5fd7 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/PickComment.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/PickComment.java @@ -227,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; + } + + public boolean isDeletedByAnonymousMember() { + return this.deletedAnonymousBy != null; } public boolean isEqualsId(Long id) { 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 ce8dc401..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 @@ -25,9 +25,9 @@ Optional findWithPickByIdAndPickIdAndCreatedAnonymousByIdAndDeleted @EntityGraph(attributePaths = {"pick"}) Optional findWithPickByIdAndPickId(Long id, Long pickId); - @EntityGraph(attributePaths = {"createdBy", "deletedBy", "pickVote", "pick", "pick.member", - "pickCommentRecommends"}) - List findWithMemberWithPickWithPickVoteWithPickCommentRecommendsByOriginParentIdInAndParentIsNotNullAndOriginParentIsNotNull( + @EntityGraph(attributePaths = {"createdBy", "deletedBy", "createdAnonymousBy", "deletedAnonymousBy", "pickVote", "pick", + "pick.member", "pickCommentRecommends"}) + List findWithDetailsByOriginParentIdInAndParentIsNotNullAndOriginParentIsNotNull( Set originParentIds); @Modifying diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/pick/custom/PickCommentRepositoryImpl.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/pick/custom/PickCommentRepositoryImpl.java index f1deb0a6..d38d138b 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/pick/custom/PickCommentRepositoryImpl.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/pick/custom/PickCommentRepositoryImpl.java @@ -1,11 +1,13 @@ package com.dreamypatisiel.devdevdev.domain.repository.pick.custom; -import com.dreamypatisiel.devdevdev.domain.entity.PickComment; +import static com.dreamypatisiel.devdevdev.domain.entity.QAnonymousMember.anonymousMember; import static com.dreamypatisiel.devdevdev.domain.entity.QMember.member; import static com.dreamypatisiel.devdevdev.domain.entity.QPick.pick; import static com.dreamypatisiel.devdevdev.domain.entity.QPickComment.pickComment; import static com.dreamypatisiel.devdevdev.domain.entity.QPickOption.pickOption; import static com.dreamypatisiel.devdevdev.domain.entity.QPickVote.pickVote; + +import com.dreamypatisiel.devdevdev.domain.entity.PickComment; import com.dreamypatisiel.devdevdev.domain.entity.enums.ContentStatus; import com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType; import com.dreamypatisiel.devdevdev.domain.repository.comment.MyWrittenCommentDto; @@ -41,7 +43,8 @@ public Slice findOriginParentPickCommentsByCursor(Pageable pageable List contents = query.selectFrom(pickComment) .innerJoin(pickComment.pick, pick).on(pick.id.eq(pickId)) - .innerJoin(pickComment.createdBy, member).fetchJoin() + .leftJoin(pickComment.createdBy, member).fetchJoin() + .leftJoin(pickComment.createdAnonymousBy, anonymousMember).fetchJoin() .leftJoin(pickComment.pickVote, pickVote).fetchJoin() .leftJoin(pickVote.pickOption, pickOption).fetchJoin() .where(pick.contentStatus.eq(ContentStatus.APPROVAL) @@ -62,7 +65,8 @@ public List findOriginParentPickBestCommentsByPickIdAndOffset(Long return query.selectFrom(pickComment) .innerJoin(pickComment.pick, pick).on(pick.id.eq(pickId)) - .innerJoin(pickComment.createdBy, member).fetchJoin() + .leftJoin(pickComment.createdBy, member).fetchJoin() + .leftJoin(pickComment.createdAnonymousBy, anonymousMember).fetchJoin() .leftJoin(pickComment.pickVote, pickVote).fetchJoin() .leftJoin(pickVote.pickOption, pickOption).fetchJoin() .where(pick.contentStatus.eq(ContentStatus.APPROVAL) @@ -92,7 +96,8 @@ public SliceCustom findMyWrittenPickCommentsByCursor(Long m Pageable pageable) { // 회원이 작성한 픽픽픽 댓글 조회 List contents = query.select( - new QMyWrittenCommentDto(pick.id, + new QMyWrittenCommentDto( + pick.id, pick.title.title, pickComment.id, Expressions.constant(MyWrittenCommentFilter.PICK.name()), @@ -100,7 +105,9 @@ public SliceCustom findMyWrittenPickCommentsByCursor(Long m pickComment.recommendTotalCount.count, pickComment.createdAt, pickOption.title.title, - pickOption.pickOptionType.stringValue())) + pickOption.pickOptionType.stringValue() + ) + ) .from(pickComment) .leftJoin(pickComment.pickVote, pickVote) .leftJoin(pickVote.pickOption, pickOption) diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/AnonymousMemberService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/AnonymousMemberService.java index e0e54020..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 @@ -14,7 +14,7 @@ @Transactional(readOnly = true) @RequiredArgsConstructor public class AnonymousMemberService { - + private final AnonymousMemberRepository anonymousMemberRepository; @Transactional 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 f6fbb5d9..0639de44 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 @@ -77,13 +77,14 @@ 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 @@ -99,11 +100,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 index 98b145f3..5f2bcef9 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java @@ -139,6 +139,7 @@ public PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, } @Override + @Transactional public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, PickCommentDto pickCommentDto, Authentication authentication) { @@ -174,19 +175,24 @@ public PickCommentResponse deletePickComment(Long pickCommentId, Long pickId, Au /** * @Note: 정렬 조건에 따라서 커서 방식으로 픽픽픽 댓글/답글을 조회한다. * @Author: 장세웅 - * @Since: 2024.10.02 + * @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); + return super.findPickComments(pageable, pickId, pickCommentId, pickCommentSort, pickOptionTypes, null, anonymousMember); } @Override @@ -202,11 +208,15 @@ public PickCommentRecommendResponse recommendPickComment(Long pickId, Long pickC * @Since: 2024.10.09 */ @Override - public List findPickBestComments(int size, Long pickId, + @Transactional + public List findPickBestComments(int size, Long pickId, String anonymousMemberId, Authentication authentication) { // 익명 회원인지 검증 AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); - return super.findPickBestComments(size, pickId, null); + // 익명 회원 추출 + AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + + return super.findPickBestComments(size, pickId, null, anonymousMember); } } 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 143b9aae..7c35bb82 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 @@ -66,6 +66,7 @@ public MemberPickCommentService(TimeProvider timeProvider, MemberProvider member * @Author: 장세웅 * @Since: 2024.08.23 */ + @Override @Transactional public PickCommentResponse registerPickComment(Long pickId, PickCommentDto pickCommentDto, Authentication authentication) { @@ -114,6 +115,7 @@ public PickCommentResponse registerPickComment(Long pickId, PickCommentDto pickC * @Author: 장세웅 * @Since: 2024.08.24 */ + @Override @Transactional public PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, Long pickCommentOriginParentId, @@ -146,6 +148,7 @@ public PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, * @Author: 장세웅 * @Since: 2024.08.10 */ + @Override @Transactional public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, PickCommentDto pickModifyCommentDto, @@ -176,6 +179,7 @@ public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, * @Author: 장세웅 * @Since: 2024.08.11 */ + @Override @Transactional public PickCommentResponse deletePickComment(Long pickCommentId, Long pickId, Authentication authentication) { @@ -216,17 +220,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); } /** @@ -234,6 +240,7 @@ public SliceCommentCustom findPickComments(Pageable pageab * @Author: 장세웅 * @Since: 2024.09.07 */ + @Override @Transactional public PickCommentRecommendResponse recommendPickComment(Long pickId, Long pickCommendId, Authentication authentication) { @@ -260,12 +267,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/PickCommentService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickCommentService.java index aaa6cb28..f73ec300 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 @@ -31,9 +31,10 @@ PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, PickComme SliceCustom findPickComments(Pageable pageable, Long pickId, Long pickCommentId, PickCommentSort pickCommentSort, EnumSet pickOptionTypes, - Authentication authentication); + String anonymousMemberId, 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 87c7b332..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 @@ -7,6 +7,7 @@ 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; @@ -110,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( @@ -124,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(); // 픽픽픽 최상위 댓글 추출 @@ -151,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); @@ -178,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) { // 최상위 댓글 아이디 추출 @@ -187,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(); } @@ -213,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); @@ -229,14 +234,13 @@ 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(); } 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 610e468a..f4b3faec 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 @@ -117,10 +117,11 @@ 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)); } @@ -158,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/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..8c998e7d 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; @@ -58,26 +64,137 @@ public PickRepliedCommentsResponse(Long pickCommentId, Long memberId, Long pickP this.isModified = isModified; this.isDeleted = isDeleted; } + + public static PickRepliedCommentsResponse of(@Nullable Member member, @Nullable AnonymousMember anonymousMember, + PickComment repliedPickComment) { - // member 가 null 인 경우 익명회원 응답 - public static PickRepliedCommentsResponse of(@Nullable Member member, PickComment repliedPickComment) { + // 댓글 + Member repliedCreatedBy = repliedPickComment.getCreatedBy(); + AnonymousMember repliedCreatedAnonymousBy = repliedPickComment.getCreatedAnonymousBy(); - Member createdBy = repliedPickComment.getCreatedBy(); + // 부모 댓글 PickComment parentPickComment = repliedPickComment.getParent(); + Member parentCreatedBy = parentPickComment.getCreatedBy(); + AnonymousMember parentCreatedAnonymousBy = parentPickComment.getCreatedAnonymousBy(); + // 댓글을 익명회원이 작성한 경우 + if (repliedCreatedBy == null) { + // 부모 댓글을 익명회원이 작성한 경우 + if (parentCreatedBy == null) { + return createResponseForAnonymousReplyToAnonymous(member, anonymousMember, repliedPickComment, + repliedCreatedAnonymousBy, parentCreatedAnonymousBy, parentPickComment); + } + // 부모 댓글을 회원이 작성한 경우 + return createResponseForAnonymousReplyToMember(member, anonymousMember, repliedPickComment, repliedCreatedAnonymousBy, + parentCreatedBy, parentPickComment); + } + + // 댓글을 회원이 작성한 경우 + // 부모 댓글을 익명회원이 작성한 경우 + if (parentCreatedBy == null) { + return createResponseForMemberReplyToAnonymous(member, anonymousMember, repliedPickComment, repliedCreatedBy, + parentCreatedAnonymousBy, parentPickComment); + } + + // 부모 댓글을 회원이 작성한 경우 + return createResponseForMemberReplyToMember(member, anonymousMember, repliedPickComment, repliedCreatedBy, + parentPickComment); + } + + private static PickRepliedCommentsResponse createResponseForMemberReplyToMember(Member member, + AnonymousMember anonymousMember, + PickComment repliedPickComment, + Member repliedCreatedBy, + PickComment parentPickComment) { return PickRepliedCommentsResponse.builder() .pickCommentId(repliedPickComment.getId()) - .memberId(createdBy.getId()) + .memberId(repliedCreatedBy.getId()) .pickParentCommentMemberId(parentPickComment.getCreatedBy().getId()) - .author(createdBy.getNickname().getNickname()) + .author(repliedCreatedBy.getNickname().getNickname()) .pickParentCommentAuthor(parentPickComment.getCreatedBy().getNicknameAsString()) .pickParentCommentId(parentPickComment.getId()) .pickOriginParentCommentId(repliedPickComment.getOriginParent().getId()) .createdAt(repliedPickComment.getCreatedAt()) - .isCommentOfPickAuthor(CommentResponseUtil.isPickAuthor(createdBy, repliedPickComment.getPick())) - .isCommentAuthor(CommentResponseUtil.isPickCommentAuthor(member, repliedPickComment)) + .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, + Member repliedCreatedBy, + AnonymousMember parentCreatedAnonymousBy, + PickComment parentPickComment) { + 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, + AnonymousMember repliedCreatedAnonymousBy, + Member parentCreatedBy, + PickComment parentPickComment) { + 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, + AnonymousMember repliedCreatedAnonymousBy, + AnonymousMember parentCreatedAnonymousBy, + PickComment parentPickComment) { + return PickRepliedCommentsResponse.builder() + .pickCommentId(repliedPickComment.getId()) + .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(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/util/CommentResponseUtil.java b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/util/CommentResponseUtil.java index e8c66ec1..741ed7f7 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,14 +11,37 @@ 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 String getCommentByPickCommentStatus(PickComment pickComment) { if (pickComment.isDeleted()) { - // 댓글 작성자에 의해 삭제된 경우 - if (pickComment.getDeletedBy().isEqualsId(pickComment.getCreatedBy().getId())) { - return "댓글 작성자에 의해 삭제된 댓글입니다."; + // 익명회원 작성자에 의해 삭제된 경우 + if (pickComment.isDeletedByAnonymousMember()) { + AnonymousMember createdAnonymousBy = pickComment.getCreatedAnonymousBy(); + AnonymousMember deletedAnonymousBy = pickComment.getDeletedAnonymousBy(); + + if (deletedAnonymousBy.isEqualAnonymousMemberId(createdAnonymousBy.getId())) { + return DELETE_COMMENT_MESSAGE; + } } - return "커뮤니티 정책을 위반하여 삭제된 댓글입니다."; + + // 회원 작성자에 의해 삭제된 경우 + Member createdBy = pickComment.getCreatedBy(); + Member deletedBy = pickComment.getDeletedBy(); + + // 익명회원이 작성한 댓글인 경우 + if (createdBy == null) { + // 어드민이 삭제함 + return DELETE_INVALID_COMMUNITY_POLICY_COMMENT_MESSAGE; + } + + if (deletedBy.isEqualsId(createdBy.getId())) { + return DELETE_COMMENT_MESSAGE; + } + + return DELETE_INVALID_COMMUNITY_POLICY_COMMENT_MESSAGE; } return pickComment.getContents().getCommentContents(); @@ -27,9 +51,9 @@ public static String getCommentByTechCommentStatus(TechComment techComment) { if (techComment.isDeleted()) { // 댓글 작성자에 의해 삭제된 경우 if (techComment.getDeletedBy().isEqualsId(techComment.getCreatedBy().getId())) { - return "댓글 작성자에 의해 삭제된 댓글입니다."; + return DELETE_COMMENT_MESSAGE; } - return "커뮤니티 정책을 위반하여 삭제된 댓글입니다."; + return DELETE_INVALID_COMMUNITY_POLICY_COMMENT_MESSAGE; } return techComment.getContents().getCommentContents(); @@ -49,12 +73,32 @@ 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) { + Member createdBy = pickComment.getCreatedBy(); + // createdBy가 null인 경우는 익명회원이 작성한 댓글 + if (createdBy == null) { + return false; + } + + return createdBy.isEqualsId(member.getId()); } - return pickComment.getCreatedBy().isEqualsId(member.getId()); + + // 익명회원이 조회한 경우 + if (anonymousMember != null) { + AnonymousMember createdAnonymousBy = pickComment.getCreatedAnonymousBy(); + // createdAnonymousBy 가 null인 경우는 회원이 작성한 댓글 + if (createdAnonymousBy == null) { + return false; + } + + return createdAnonymousBy.isEqualAnonymousMemberId(anonymousMember.getId()); + } + + return false; } public static boolean isPickCommentRecommended(@Nullable Member member, PickComment pickComment) { 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 25f9eec1..7851309c 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 @@ -172,7 +172,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 +459,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 +683,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 +746,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 +766,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); } @@ -854,8 +854,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 // 최상위 댓글 검증 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 index edfb86e4..3bce599c 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java @@ -10,6 +10,7 @@ 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; @@ -29,6 +30,7 @@ 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; @@ -45,6 +47,7 @@ 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; @@ -54,13 +57,20 @@ 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; @@ -69,6 +79,8 @@ 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; @@ -870,4 +882,971 @@ void modifyPickCommentNotApproval(ContentStatus contentStatus) { .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()) + ); + } } \ 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 ff81c9a4..496d1cf7 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 @@ -1317,7 +1317,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 // 최상위 댓글 검증 @@ -1610,8 +1610,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 // 최상위 댓글 검증 @@ -1842,7 +1841,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 // 최상위 댓글 검증 @@ -1975,7 +1974,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 // 최상위 댓글 검증 @@ -2460,7 +2459,7 @@ void findPickBestComments() { // when List response = memberPickCommentService.findPickBestComments(3, pick.getId(), - authentication); + null, authentication); // then // 최상위 댓글 검증 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 index cadb37a0..39e8a8bb 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickTestUtils.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickTestUtils.java @@ -106,6 +106,24 @@ public static PickComment createPickComment(CommentContents contents, Boolean is 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() 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 ec50082c..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; @@ -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()); } @@ -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/docs/PickCommentControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java index 9bb0ead0..39efc2fe 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java @@ -691,7 +691,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("픽픽픽 아이디") @@ -711,7 +712,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("댓글 작성자가 픽픽픽 작성자인지 여부"), @@ -736,7 +738,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) @@ -758,8 +762,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("픽픽픽 부모 댓글 작성자 닉네임"), @@ -993,7 +999,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("픽픽픽 아이디") @@ -1007,7 +1014,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("댓글 작성자가 픽픽픽 작성자인지 여부"), @@ -1032,7 +1040,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) @@ -1054,8 +1063,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("픽픽픽 부모 댓글 작성자 닉네임") ) From 41d1239a5d09275fc9b121c7cfa19956bed8c29e Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sun, 13 Jul 2025 19:19:29 +0900 Subject: [PATCH 24/66] =?UTF-8?q?feat(PickCommentControllerDocsTest):=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=A1=B0=ED=9A=8C,=20=EB=B2=A0=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=8C=93=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=EC=97=90=20=EC=9D=B5?= =?UTF-8?q?=EB=AA=85=ED=9A=8C=EC=9B=90=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/PickCommentControllerDocsTest.java | 67 ++++++++++++++++--- 1 file changed, 57 insertions(+), 10 deletions(-) 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 39efc2fe..a6343223 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java @@ -35,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; @@ -51,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; @@ -101,6 +103,8 @@ public class PickCommentControllerDocsTest extends SupportControllerDocsTest { @Autowired MemberRepository memberRepository; @Autowired + AnonymousMemberRepository anonymousMemberRepository; + @Autowired PickPopularScorePolicy pickPopularScorePolicy; @Autowired PickOptionImageRepository pickOptionImageRepository; @@ -621,6 +625,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); @@ -649,9 +657,9 @@ 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); + new Count(0), anonymousMember, pick, null); originParentPickComment6.changeDeletedAtByMember(LocalDateTime.now(), member6); pickCommentRepository.saveAll( List.of(originParentPickComment6, originParentPickComment5, originParentPickComment4, @@ -664,7 +672,7 @@ 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.changeDeletedAtByMember(LocalDateTime.now(), member5); pickCommentRepository.saveAll(List.of(pickReply4, pickReply3, pickReply2, pickReply1)); @@ -721,7 +729,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) @@ -753,8 +761,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("픽픽픽 답글 좋아요 총 갯수"), @@ -931,6 +938,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); @@ -951,7 +962,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); @@ -968,7 +979,7 @@ 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); @@ -1023,7 +1034,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) @@ -1054,7 +1065,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) @@ -1136,6 +1147,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() @@ -1154,6 +1183,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() From 3060e55d5208c7d243f7623f5cd2ad1f5d2bdb91 Mon Sep 17 00:00:00 2001 From: ralph Date: Wed, 9 Jul 2025 23:47:21 +0900 Subject: [PATCH 25/66] fix(PickCommentService): deletePickComment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 익명회원 댓글 삭제 서비스 개발 및 테스트 코드 작성 --- .../service/pick/GuestPickCommentService.java | 7 +- .../pick/GuestPickCommentServiceV2.java | 45 +++- .../pick/MemberPickCommentService.java | 4 +- .../service/pick/PickCommentService.java | 4 +- .../pick/PickCommentController.java | 3 +- .../pick/GuestPickCommentServiceV2Test.java | 243 ++++++++++++++++++ .../pick/MemberPickCommentServiceTest.java | 16 +- 7 files changed, 294 insertions(+), 28 deletions(-) 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 0639de44..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 @@ -19,6 +19,7 @@ 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; @@ -64,7 +65,8 @@ public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, } @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); } @@ -88,8 +90,7 @@ public SliceCustom findPickComments(Pageable pageable, Lon } @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); } 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 index 5f2bcef9..2de9167f 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java @@ -31,6 +31,7 @@ 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; @@ -72,7 +73,7 @@ public PickCommentResponse registerPickComment(Long pickId, PickCommentDto pickC Boolean isPickVotePublic = pickCommentDto.getIsPickVotePublic(); // 익명 회원 추출 - AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); // 픽픽픽 조회 Pick findPick = pickRepository.findById(pickId) @@ -89,12 +90,12 @@ public PickCommentResponse registerPickComment(Long pickId, PickCommentDto pickC if (isPickVotePublic) { // 익명회원이 투표한 픽픽픽 투표 조회 PickVote findPickVote = pickVoteRepository.findWithPickAndPickOptionByPickIdAndAnonymousMemberAndDeletedAtIsNull( - pickId, anonymousMember) + pickId, findAnonymousMember) .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_VOTE_MESSAGE)); // 픽픽픽 투표한 픽 옵션의 댓글 작성 PickComment pickComment = PickComment.createPublicVoteCommentByAnonymousMember(new CommentContents(contents), - anonymousMember, findPick, findPickVote); + findAnonymousMember, findPick, findPickVote); pickCommentRepository.save(pickComment); return new PickCommentResponse(pickComment.getId()); @@ -102,7 +103,7 @@ public PickCommentResponse registerPickComment(Long pickId, PickCommentDto pickC // 픽픽픽 선택지 투표 비공개인 경우 PickComment pickComment = PickComment.createPrivateVoteCommentByAnonymousMember(new CommentContents(contents), - anonymousMember, findPick); + findAnonymousMember, findPick); pickCommentRepository.save(pickComment); return new PickCommentResponse(pickComment.getId()); @@ -121,7 +122,7 @@ public PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, String anonymousMemberId = pickRegisterRepliedCommentDto.getAnonymousMemberId(); // 익명 회원 추출 - AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); // 픽픽픽 댓글 로직 수행 PickReplyContext pickReplyContext = prepareForReplyRegistration(pickParentCommentId, pickCommentOriginParentId, pickId); @@ -132,7 +133,7 @@ public PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, // 픽픽픽 서브 댓글(답글) 생성 PickComment pickRepliedComment = PickComment.createRepliedCommentByAnonymousMember(new CommentContents(contents), - findParentPickComment, findOriginParentPickComment, anonymousMember, findPick); + findParentPickComment, findOriginParentPickComment, findAnonymousMember, findPick); pickCommentRepository.save(pickRepliedComment); return new PickCommentResponse(pickRepliedComment.getId()); @@ -150,16 +151,15 @@ public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, Pi String anonymousMemberId = pickCommentDto.getAnonymousMemberId(); // 익명 회원 추출 - AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); // 픽픽픽 댓글 조회(익명 회원 본인이 댓글 작성, 삭제되지 않은 댓글) PickComment findPickComment = pickCommentRepository.findWithPickByIdAndPickIdAndCreatedAnonymousByIdAndDeletedAtIsNull( - pickCommentId, pickId, anonymousMember.getId()) + pickCommentId, pickId, findAnonymousMember.getId()) .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE)); // 픽픽픽 게시글의 승인 상태 검증 - validateIsApprovalPickContentStatus(findPickComment.getPick(), INVALID_NOT_APPROVAL_STATUS_PICK_COMMENT_MESSAGE, - MODIFY); + validateIsApprovalPickContentStatus(findPickComment.getPick(), INVALID_NOT_APPROVAL_STATUS_PICK_COMMENT_MESSAGE, MODIFY); // 댓글 수정 findPickComment.modifyCommentContents(new CommentContents(contents), timeProvider.getLocalDateTimeNow()); @@ -168,8 +168,26 @@ public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, Pi } @Override - public PickCommentResponse deletePickComment(Long pickCommentId, Long pickId, Authentication authentication) { - throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); + 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()); } /** @@ -196,8 +214,7 @@ public SliceCustom findPickComments(Pageable pageable, Lon } @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); } 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 7c35bb82..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 @@ -32,6 +32,7 @@ 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; @@ -181,7 +182,8 @@ public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, */ @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); 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 f73ec300..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 @@ -9,6 +9,7 @@ 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; @@ -27,7 +28,8 @@ PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, Long pi 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, 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 f4b3faec..d2bacb18 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 @@ -133,10 +133,11 @@ public ResponseEntity> deletePickComment( @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)); } 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 index 3bce599c..678b9706 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java @@ -6,6 +6,7 @@ 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; @@ -883,6 +884,248 @@ void modifyPickCommentNotApproval(ContentStatus contentStatus) { .hasMessage(INVALID_NOT_APPROVAL_STATUS_PICK_COMMENT_MESSAGE, MODIFY); } + @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); + } + @ParameterizedTest @EnumSource(PickCommentSort.class) @DisplayName("익명회원이 픽픽픽 모든 댓글/답글을 알맞게 정렬하여 커서 방식으로 조회한다.") 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 496d1cf7..62f7fbee 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 @@ -953,7 +953,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(); @@ -1000,7 +1000,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(); @@ -1029,7 +1029,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); } @@ -1064,7 +1064,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); } @@ -1103,7 +1103,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); @@ -1143,7 +1143,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); } @@ -1183,7 +1183,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); } @@ -1224,7 +1224,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); } From a083714d34536a26bf2ed9fb6c55a42274cdc8e3 Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sat, 12 Jul 2025 17:37:22 +0900 Subject: [PATCH 26/66] =?UTF-8?q?fix(PickCommentControllerDocsTest):=20?= =?UTF-8?q?=ED=94=BD=ED=94=BD=ED=94=BD=20=EB=8C=93=EA=B8=80/=EB=8B=B5?= =?UTF-8?q?=EA=B8=80=20=EC=82=AD=EC=A0=9C=20API=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../asciidoc/api/pick-commnet/pick-comment-delete.adoc | 6 ++++-- .../asciidoc/api/pick-commnet/pick-comment-modify.adoc | 1 - .../web/controller/pick/PickCommentController.java | 7 +++---- .../devdevdev/web/docs/PickCommentControllerDocsTest.java | 6 ++++-- 4 files changed, 11 insertions(+), 9 deletions(-) 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 c6546a80..9605aa83 100644 --- a/src/docs/asciidoc/api/pick-commnet/pick-comment-modify.adoc +++ b/src/docs/asciidoc/api/pick-commnet/pick-comment-modify.adoc @@ -41,7 +41,6 @@ include::{snippets}/modify-pick-comment/response-fields.adoc[] * `내용을 작성해주세요.`: 댓글(contents)을 작성하지 않는 경우(공백 이거나 빈문자열) * `픽픽픽 댓글이 없습니다.`: 픽픽픽 댓글이 존재하지 않거나 본인이 작성하지 않았거나 픽픽픽 댓글 삭제된 경우 * `승인 상태가 아닌 픽픽픽에는 댓글을 수정할 수 없습니다.`: 픽픽픽이 승인 상태가 아닌 경우 -* `익명 회원은 사용할 수 없는 기능 입니다.`: 익명 회원인 경우 * `회원을 찾을 수 없습니다.`: 회원이 존재하지 않는 경우 * `익명 사용자가 아닙니다. 잘못된 메소드 호출 입니다.`: 회원이 익명 회원 메소드를 호출한 경우 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 d2bacb18..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 @@ -126,11 +126,10 @@ public ResponseEntity>> getPickC 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); 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 a6343223..f7aca371 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java @@ -531,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("픽픽픽 아이디"), @@ -586,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("픽픽픽 아이디"), From 8c550f199e406df26b4f4e6a700cec5074f23759 Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sun, 13 Jul 2025 19:02:46 +0900 Subject: [PATCH 27/66] =?UTF-8?q?feat(PickCommentService):=20findPickComme?= =?UTF-8?q?nts,=20findPickBestComments=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/pick/GuestPickCommentServiceV2.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 index 2de9167f..3618a3e1 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java @@ -31,7 +31,6 @@ 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; @@ -232,8 +231,8 @@ public List findPickBestComments(int size, Long pickId, St AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); // 익명 회원 추출 - AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); - return super.findPickBestComments(size, pickId, null, anonymousMember); + return super.findPickBestComments(size, pickId, null, findAnonymousMember); } } From ad176e233025ca7ae11a4bfcad406caa294bcf89 Mon Sep 17 00:00:00 2001 From: ralph Date: Sat, 19 Jul 2025 23:29:39 +0900 Subject: [PATCH 28/66] =?UTF-8?q?fix(PR):=20PickRepliedCommentsResponse=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PR 리뷰 반영 --- .../devdevdev/domain/entity/PickComment.java | 8 ++++ .../pick/PickRepliedCommentsResponse.java | 37 +++++++++---------- 2 files changed, 26 insertions(+), 19 deletions(-) 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 f07e5fd7..f5909d32 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/PickComment.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/PickComment.java @@ -265,4 +265,12 @@ 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; + } } 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 8c998e7d..60c203b8 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 @@ -64,39 +64,38 @@ public PickRepliedCommentsResponse(Long pickCommentId, Long memberId, Long anony this.isModified = isModified; this.isDeleted = isDeleted; } - + public static PickRepliedCommentsResponse of(@Nullable Member member, @Nullable AnonymousMember anonymousMember, PickComment repliedPickComment) { - // 댓글 - Member repliedCreatedBy = repliedPickComment.getCreatedBy(); - AnonymousMember repliedCreatedAnonymousBy = repliedPickComment.getCreatedAnonymousBy(); - // 부모 댓글 PickComment parentPickComment = repliedPickComment.getParent(); Member parentCreatedBy = parentPickComment.getCreatedBy(); AnonymousMember parentCreatedAnonymousBy = parentPickComment.getCreatedAnonymousBy(); - // 댓글을 익명회원이 작성한 경우 - if (repliedCreatedBy == null) { - // 부모 댓글을 익명회원이 작성한 경우 - if (parentCreatedBy == null) { - return createResponseForAnonymousReplyToAnonymous(member, anonymousMember, repliedPickComment, - repliedCreatedAnonymousBy, parentCreatedAnonymousBy, parentPickComment); - } - // 부모 댓글을 회원이 작성한 경우 - return createResponseForAnonymousReplyToMember(member, anonymousMember, repliedPickComment, repliedCreatedAnonymousBy, - parentCreatedBy, parentPickComment); + // 댓글 + Member repliedCreatedBy = repliedPickComment.getCreatedBy(); + AnonymousMember repliedCreatedAnonymousBy = repliedPickComment.getCreatedAnonymousBy(); + + // 부모 댓글/답글 익명회원이 작성한 경우 + if (parentPickComment.isCreatedAnonymousMember() && repliedPickComment.isCreatedAnonymousMember()) { + return createResponseForAnonymousReplyToAnonymous(member, anonymousMember, repliedPickComment, + repliedCreatedAnonymousBy, parentCreatedAnonymousBy, parentPickComment); } - // 댓글을 회원이 작성한 경우 - // 부모 댓글을 익명회원이 작성한 경우 - if (parentCreatedBy == null) { + // 부모 댓글은 익명회원이 작성하고 답글은 회원이 작성한 경우 + if (parentPickComment.isCreatedAnonymousMember() && repliedPickComment.isCreatedMember()) { return createResponseForMemberReplyToAnonymous(member, anonymousMember, repliedPickComment, repliedCreatedBy, parentCreatedAnonymousBy, parentPickComment); } - // 부모 댓글을 회원이 작성한 경우 + // 부모 댓글은 회원이 작성하고 답글은 익명회원이 작성한 경우 + if (parentPickComment.isCreatedMember() && repliedPickComment.isCreatedAnonymousMember()) { + return createResponseForAnonymousReplyToMember(member, anonymousMember, repliedPickComment, + repliedCreatedAnonymousBy, parentCreatedBy, parentPickComment); + } + + // 부모 댓글/답글 회원이 작성한 경우 return createResponseForMemberReplyToMember(member, anonymousMember, repliedPickComment, repliedCreatedBy, parentPickComment); } From 91bdfc31a7bbcfe218717f749780af348d81de58 Mon Sep 17 00:00:00 2001 From: ralph Date: Sun, 20 Jul 2025 14:53:19 +0900 Subject: [PATCH 29/66] =?UTF-8?q?hotfix(GuestPickCommentServiceV2):=20dele?= =?UTF-8?q?tePickComment=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Transactional 추가 --- .../devdevdev/domain/service/pick/GuestPickCommentServiceV2.java | 1 + 1 file changed, 1 insertion(+) 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 index a6402efe..3c033729 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java @@ -168,6 +168,7 @@ public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, Pi } @Override + @Transactional public PickCommentResponse deletePickComment(Long pickCommentId, Long pickId, @Nullable String anonymousMemberId, Authentication authentication) { // 익명 회원인지 검증 From 77c45266e5658d865101a1fc08e94722c664a305 Mon Sep 17 00:00:00 2001 From: ralph Date: Sun, 20 Jul 2025 23:16:44 +0900 Subject: [PATCH 30/66] =?UTF-8?q?feat(GuestTechCommentServiceV2):=20?= =?UTF-8?q?=EA=B8=B0=EC=88=A0=EB=B8=94=EB=A1=9C=EA=B7=B8=20=EC=9D=B5?= =?UTF-8?q?=EB=AA=85=ED=9A=8C=EC=9B=90=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/entity/AnonymousMember.java | 4 + .../devdevdev/domain/entity/PickComment.java | 12 +- .../devdevdev/domain/entity/TechComment.java | 48 ++++-- .../custom/TechCommentRepositoryImpl.java | 5 +- .../techComment/GuestTechCommentService.java | 16 +- .../GuestTechCommentServiceV2.java | 108 ++++++++++++++ .../techComment/MemberTechCommentService.java | 20 +-- .../techComment/TechCommentCommonService.java | 31 ++-- .../techComment/TechCommentService.java | 5 +- .../sub/RedisNotificationSubscriber.java | 2 +- .../TechArticleCommentController.java | 14 +- .../pick/PickRepliedCommentsResponse.java | 44 +++--- .../techArticle/TechCommentsResponse.java | 63 ++++++-- .../TechRepliedCommentsResponse.java | 141 ++++++++++++++++-- .../web/dto/util/CommentResponseUtil.java | 86 ++++++----- .../GuestTechCommentServiceTest.java | 27 ++-- .../MemberTechCommentServiceTest.java | 16 +- .../TechArticleCommentControllerDocsTest.java | 74 +++------ 18 files changed, 502 insertions(+), 214 deletions(-) create mode 100644 src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2.java 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 1a833999..b7cef6b2 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/AnonymousMember.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/AnonymousMember.java @@ -53,4 +53,8 @@ public boolean hasNickName() { 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/PickComment.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/PickComment.java index f5909d32..c00c3446 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/PickComment.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/PickComment.java @@ -235,11 +235,11 @@ public boolean isDeleted() { } public boolean isDeletedByMember() { - return this.deletedBy != null; + return this.deletedBy != null && this.deletedAnonymousBy == null; } public boolean isDeletedByAnonymousMember() { - return this.deletedAnonymousBy != null; + return this.deletedBy == null && this.deletedAnonymousBy != null; } public boolean isEqualsId(Long id) { @@ -273,4 +273,12 @@ public boolean isCreatedAnonymousMember() { 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..ede50c29 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,6 +107,8 @@ 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; } @@ -159,4 +181,12 @@ 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; + } } 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..3d249293 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)) @@ -52,6 +54,7 @@ public List findOriginParentTechBestCommentsByTechArticleIdAndOffse return query.selectFrom(techComment) .innerJoin(techComment.techArticle, techArticle).on(techArticle.id.eq(techArticleId)) .innerJoin(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/service/techArticle/techComment/GuestTechCommentService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentService.java index 93f2d7cb..185a4f92 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 @@ -5,9 +5,9 @@ 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.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; @@ -24,9 +24,13 @@ @Transactional(readOnly = true) public class GuestTechCommentService extends TechCommentCommonService implements TechCommentService { + private final AnonymousMemberService anonymousMemberService; + public GuestTechCommentService(TechCommentRepository techCommentRepository, - TechBestCommentsPolicy techBestCommentsPolicy) { + TechBestCommentsPolicy techBestCommentsPolicy, + AnonymousMemberService anonymousMemberService) { super(techCommentRepository, techBestCommentsPolicy); + this.anonymousMemberService = anonymousMemberService; } @Override @@ -60,12 +64,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 +84,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..1af1098f --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2.java @@ -0,0 +1,108 @@ +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 com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; +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.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.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 AnonymousMemberService anonymousMemberService; + + public GuestTechCommentServiceV2(TechCommentRepository techCommentRepository, + TechBestCommentsPolicy techBestCommentsPolicy, + AnonymousMemberService anonymousMemberService) { + super(techCommentRepository, techBestCommentsPolicy); + this.anonymousMemberService = anonymousMemberService; + } + + @Override + public TechCommentResponse registerMainTechComment(Long techArticleId, + RegisterTechCommentRequest registerTechCommentRequest, + Authentication authentication) { + throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); + } + + @Override + public TechCommentResponse registerRepliedTechComment(Long techArticleId, Long originParentTechCommentId, + Long parentTechCommentId, + RegisterTechCommentRequest registerRepliedTechCommentRequest, + Authentication authentication) { + throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); + } + + @Override + public TechCommentResponse modifyTechComment(Long techArticleId, Long techCommentId, + ModifyTechCommentRequest modifyTechCommentRequest, + Authentication authentication) { + throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); + } + + @Override + public TechCommentResponse deleteTechComment(Long techArticleId, Long techCommentId, + Authentication authentication) { + throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); + } + + /** + * @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 + 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..569b69f4 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,10 @@ 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_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.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; import com.dreamypatisiel.devdevdev.domain.entity.TechComment; @@ -20,17 +25,13 @@ 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 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 { @@ -224,12 +225,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 +265,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..ab9171ed 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,5 +1,6 @@ package com.dreamypatisiel.devdevdev.domain.service.techArticle.techComment; +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; @@ -37,8 +38,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); @@ -57,7 +59,7 @@ public SliceCommentCustom getTechComments(Long techArticle // 기술블로그 댓글/답글 응답 생성 List techCommentsResponse = originParentTechComments.stream() - .map(originParentTechComment -> getTechCommentsResponse(member, originParentTechComment, + .map(originParentTechComment -> getTechCommentsResponse(member, anonymousMember, originParentTechComment, techCommentReplies)) .toList(); @@ -75,14 +77,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 +97,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 +118,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); @@ -133,7 +140,7 @@ protected List findTechBestComments(int size, Long techArt // 기술블로그 댓글/답글 응답 생성 return findOriginTechBestComments.stream() - .map(originParentTechComment -> getTechCommentsResponse(member, originParentTechComment, + .map(originParentTechComment -> getTechCommentsResponse(member, anonymousMember, originParentTechComment, techBestCommentReplies)) .toList(); } 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..a6ac42bf 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 @@ -2,7 +2,6 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentSort; 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; @@ -32,10 +31,12 @@ TechCommentResponse modifyTechComment(Long techArticleId, Long techCommentId, SliceCommentCustom getTechComments(Long techArticleId, Long techCommentId, TechCommentSort techCommentSort, Pageable pageable, + String anonymousMemberId, 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/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/techArticle/TechArticleCommentController.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleCommentController.java index dce201c3..7e5e8a43 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,12 @@ 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.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; @@ -102,20 +105,20 @@ public ResponseEntity> deleteTechComment( 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 +146,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/response/pick/PickRepliedCommentsResponse.java b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/pick/PickRepliedCommentsResponse.java index 60c203b8..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 @@ -70,47 +70,39 @@ public static PickRepliedCommentsResponse of(@Nullable Member member, @Nullable // 부모 댓글 PickComment parentPickComment = repliedPickComment.getParent(); - Member parentCreatedBy = parentPickComment.getCreatedBy(); - AnonymousMember parentCreatedAnonymousBy = parentPickComment.getCreatedAnonymousBy(); - - // 댓글 - Member repliedCreatedBy = repliedPickComment.getCreatedBy(); - AnonymousMember repliedCreatedAnonymousBy = repliedPickComment.getCreatedAnonymousBy(); // 부모 댓글/답글 익명회원이 작성한 경우 if (parentPickComment.isCreatedAnonymousMember() && repliedPickComment.isCreatedAnonymousMember()) { - return createResponseForAnonymousReplyToAnonymous(member, anonymousMember, repliedPickComment, - repliedCreatedAnonymousBy, parentCreatedAnonymousBy, parentPickComment); + return createResponseForAnonymousReplyToAnonymous(member, anonymousMember, repliedPickComment, parentPickComment); } // 부모 댓글은 익명회원이 작성하고 답글은 회원이 작성한 경우 if (parentPickComment.isCreatedAnonymousMember() && repliedPickComment.isCreatedMember()) { - return createResponseForMemberReplyToAnonymous(member, anonymousMember, repliedPickComment, repliedCreatedBy, - parentCreatedAnonymousBy, parentPickComment); + return createResponseForMemberReplyToAnonymous(member, anonymousMember, repliedPickComment, parentPickComment); } // 부모 댓글은 회원이 작성하고 답글은 익명회원이 작성한 경우 if (parentPickComment.isCreatedMember() && repliedPickComment.isCreatedAnonymousMember()) { - return createResponseForAnonymousReplyToMember(member, anonymousMember, repliedPickComment, - repliedCreatedAnonymousBy, parentCreatedBy, parentPickComment); + return createResponseForAnonymousReplyToMember(member, anonymousMember, repliedPickComment, parentPickComment); } // 부모 댓글/답글 회원이 작성한 경우 - return createResponseForMemberReplyToMember(member, anonymousMember, repliedPickComment, repliedCreatedBy, - parentPickComment); + return createResponseForMemberReplyToMember(member, anonymousMember, repliedPickComment, parentPickComment); } private static PickRepliedCommentsResponse createResponseForMemberReplyToMember(Member member, AnonymousMember anonymousMember, PickComment repliedPickComment, - Member repliedCreatedBy, PickComment parentPickComment) { + Member parentCreatedBy = parentPickComment.getCreatedBy(); + Member repliedCreatedBy = repliedPickComment.getCreatedBy(); + return PickRepliedCommentsResponse.builder() .pickCommentId(repliedPickComment.getId()) .memberId(repliedCreatedBy.getId()) - .pickParentCommentMemberId(parentPickComment.getCreatedBy().getId()) + .pickParentCommentMemberId(parentCreatedBy.getId()) .author(repliedCreatedBy.getNickname().getNickname()) - .pickParentCommentAuthor(parentPickComment.getCreatedBy().getNicknameAsString()) + .pickParentCommentAuthor(parentCreatedBy.getNicknameAsString()) .pickParentCommentId(parentPickComment.getId()) .pickOriginParentCommentId(repliedPickComment.getOriginParent().getId()) .createdAt(repliedPickComment.getCreatedAt()) @@ -128,9 +120,11 @@ private static PickRepliedCommentsResponse createResponseForMemberReplyToMember( private static PickRepliedCommentsResponse createResponseForMemberReplyToAnonymous(Member member, AnonymousMember anonymousMember, PickComment repliedPickComment, - Member repliedCreatedBy, - AnonymousMember parentCreatedAnonymousBy, PickComment parentPickComment) { + + AnonymousMember parentCreatedAnonymousBy = parentPickComment.getCreatedAnonymousBy(); + Member repliedCreatedBy = repliedPickComment.getCreatedBy(); + return PickRepliedCommentsResponse.builder() .pickCommentId(repliedPickComment.getId()) .memberId(repliedCreatedBy.getId()) @@ -154,9 +148,11 @@ private static PickRepliedCommentsResponse createResponseForMemberReplyToAnonymo private static PickRepliedCommentsResponse createResponseForAnonymousReplyToMember(Member member, AnonymousMember anonymousMember, PickComment repliedPickComment, - AnonymousMember repliedCreatedAnonymousBy, - Member parentCreatedBy, PickComment parentPickComment) { + + Member parentCreatedBy = parentPickComment.getCreatedBy(); + AnonymousMember repliedCreatedAnonymousBy = repliedPickComment.getCreatedAnonymousBy(); + return PickRepliedCommentsResponse.builder() .pickCommentId(repliedPickComment.getId()) .anonymousMemberId(repliedCreatedAnonymousBy.getId()) @@ -179,9 +175,11 @@ private static PickRepliedCommentsResponse createResponseForAnonymousReplyToMemb private static PickRepliedCommentsResponse createResponseForAnonymousReplyToAnonymous(Member member, AnonymousMember anonymousMember, PickComment repliedPickComment, - AnonymousMember repliedCreatedAnonymousBy, - AnonymousMember parentCreatedAnonymousBy, PickComment parentPickComment) { + + AnonymousMember parentCreatedAnonymousBy = parentPickComment.getCreatedAnonymousBy(); + AnonymousMember repliedCreatedAnonymousBy = repliedPickComment.getCreatedAnonymousBy(); + return PickRepliedCommentsResponse.builder() .pickCommentId(repliedPickComment.getId()) .anonymousMemberId(repliedCreatedAnonymousBy.getId()) 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..fd01c276 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()) + .techParentCommentMemberId(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()) + .anonymousMemberId(repliedCreatedBy.getId()) + .author(repliedCreatedBy.getNickname().getNickname()) + .techParentCommentMemberId(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 741ed7f7..16a79668 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 @@ -14,37 +14,45 @@ 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.isDeletedByAnonymousMember()) { - AnonymousMember createdAnonymousBy = pickComment.getCreatedAnonymousBy(); - AnonymousMember deletedAnonymousBy = pickComment.getDeletedAnonymousBy(); - if (deletedAnonymousBy.isEqualAnonymousMemberId(createdAnonymousBy.getId())) { - return DELETE_COMMENT_MESSAGE; - } - } + if (!pickComment.isDeleted()) { + return pickComment.getContents().getCommentContents(); + } - // 회원 작성자에 의해 삭제된 경우 - Member createdBy = pickComment.getCreatedBy(); - Member deletedBy = pickComment.getDeletedBy(); + // 익명회원이 작성한 댓글인 경우 + if (pickComment.isCreatedAnonymousMember()) { + // 자기자신이 삭제한 경우 + if (pickComment.isDeletedByAnonymousMember()) { + return DELETE_COMMENT_MESSAGE; + } - // 익명회원이 작성한 댓글인 경우 - if (createdBy == null) { - // 어드민이 삭제함 + // 어드민이 삭제한 경우 + if (pickComment.getDeletedBy().isAdmin()) { return DELETE_INVALID_COMMUNITY_POLICY_COMMENT_MESSAGE; } - if (deletedBy.isEqualsId(createdBy.getId())) { + return CONTACT_ADMIN_MESSAGE; + } + + // 회원이 작성한 댓글인 경우 + if (pickComment.isCreatedMember()) { + // 자기 자신인 경우 + if (pickComment.isDeletedMemberByMySelf()) { return DELETE_COMMENT_MESSAGE; } - return DELETE_INVALID_COMMUNITY_POLICY_COMMENT_MESSAGE; + // 어드민이 삭제한 경우 + 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) { @@ -76,26 +84,15 @@ public static boolean isPickAuthor(@Nullable Member member, Pick pick) { public static boolean isPickCommentAuthor(@Nullable Member member, @Nullable AnonymousMember anonymousMember, PickComment pickComment) { - // 회원이 조회한 경우 - if (member != null) { - Member createdBy = pickComment.getCreatedBy(); - // createdBy가 null인 경우는 익명회원이 작성한 댓글 - if (createdBy == null) { - return false; - } - - return createdBy.isEqualsId(member.getId()); + // 회원이 조회하고 픽픽픽 댓글을 회원이 작성한 경우 + if (member != null && pickComment.isCreatedMember()) { + // 픽픽픽 댓글을 회원이 작성한 경우 + return pickComment.getCreatedBy().isEqualsId(member.getId()); } - // 익명회원이 조회한 경우 - if (anonymousMember != null) { - AnonymousMember createdAnonymousBy = pickComment.getCreatedAnonymousBy(); - // createdAnonymousBy 가 null인 경우는 회원이 작성한 댓글 - if (createdAnonymousBy == null) { - return false; - } - - return createdAnonymousBy.isEqualAnonymousMemberId(anonymousMember.getId()); + // 익명회원이 조회하고 픽픽픽 댓글을 익명회원이 작성한 경우 + if (anonymousMember != null && pickComment.isCreatedAnonymousMember()) { + return pickComment.getCreatedAnonymousBy().isEqualAnonymousMemberId(anonymousMember.getId()); } return false; @@ -112,12 +109,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/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechCommentServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechCommentServiceTest.java index eade4b29..a24496f8 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,5 +1,12 @@ 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.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; @@ -12,7 +19,6 @@ 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; @@ -24,7 +30,6 @@ 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 +38,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; @@ -246,7 +247,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 +541,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 +710,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 +984,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 +1133,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 +1250,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 +1307,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 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..5e3502d2 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 @@ -894,7 +894,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 +1191,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 +1363,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 +1640,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 +1793,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); // 삭제된 댓글은 카운트하지 않는다 @@ -2084,7 +2084,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 +2145,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 +2296,7 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), // when List response = memberTechCommentService.findTechBestComments(3, techArticle.getId(), - authentication); + null, authentication); // then assertThat(response).hasSize(1) 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..abab1764 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java @@ -837,53 +837,7 @@ 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", @@ -908,7 +862,9 @@ 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[].contents").type(STRING).description("기술블로그 댓글 내용"), @@ -929,13 +885,17 @@ 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("기술블로그 답글 작성자 닉네임"), @@ -1170,7 +1130,8 @@ 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("로그인한 회원이 댓글 작성자인지 여부"), @@ -1189,7 +1150,9 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), 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,16 +1172,17 @@ 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) { + private TechCommentRecommend createTechCommentRecommend(Boolean recommendedStatus, TechComment techComment, Member member) { TechCommentRecommend techCommentRecommend = TechCommentRecommend.builder() .recommendedStatus(recommendedStatus) .techComment(techComment) From 939c004ce1bdd6b4903dbbb09901a0d8c42a5736 Mon Sep 17 00:00:00 2001 From: soyoung Date: Tue, 22 Jul 2025 11:47:39 +0900 Subject: [PATCH 31/66] =?UTF-8?q?fix(dockerfile):=20prod=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20=ED=95=80=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile-prod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile-prod b/Dockerfile-prod index dab010b9..7c6e0949 100644 --- a/Dockerfile-prod +++ b/Dockerfile-prod @@ -6,4 +6,4 @@ COPY build/libs/*.jar app.jar ENV TZ Asia/Seoul # 시스템 진입점 정의 -CMD java -jar -Dspring.profiles.active=prod -javaagent:/pinpoint-agent/pinpoint-bootstrap-3.0.0.jar -Dpinpoint.agentId=devdevdev -Dpinpoint.applicationName=devdevdev-server /app.jar \ No newline at end of file +CMD java -jar -Dspring.profiles.active=prod /app.jar \ No newline at end of file From 9319cf1bd5f9b4161e7003c0adffa1d750846f03 Mon Sep 17 00:00:00 2001 From: ralph Date: Wed, 23 Jul 2025 01:11:48 +0900 Subject: [PATCH 32/66] =?UTF-8?q?feat(GuestTechCommentServiceV2):=20?= =?UTF-8?q?=EA=B8=B0=EC=88=A0=EB=B8=94=EB=A1=9C=EA=B7=B8=20=EC=9D=B5?= =?UTF-8?q?=EB=AA=85=ED=9A=8C=EC=9B=90=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TechRepliedCommentsResponse.java | 6 +- .../pick/GuestPickCommentServiceTest.java | 124 +- .../pick/MemberPickCommentServiceTest.java | 339 +---- .../GuestTechCommentServiceTest.java | 73 +- .../MemberTechCommentServiceTest.java | 73 +- .../service/techArticle/TechTestUtils.java | 112 ++ .../GuestTechCommentServiceV2Test.java | 1342 +++++++++++++++++ 7 files changed, 1482 insertions(+), 587 deletions(-) create mode 100644 src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechTestUtils.java create mode 100644 src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java 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 fd01c276..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 @@ -99,7 +99,7 @@ private static TechRepliedCommentsResponse createResponseForAnonymousReplyToAnon .techCommentId(repliedTechComment.getId()) .anonymousMemberId(repliedCreatedAnonymousBy.getId()) .author(repliedCreatedAnonymousBy.getNickname()) - .techParentCommentMemberId(parentCreatedAnonymousBy.getId()) + .techParentCommentAnonymousMemberId(parentCreatedAnonymousBy.getId()) .techParentCommentAuthor(parentCreatedAnonymousBy.getNickname()) .techParentCommentId(repliedTechComment.getParent().getId()) .techOriginParentCommentId(repliedTechComment.getOriginParent().getId()) @@ -123,9 +123,9 @@ private static TechRepliedCommentsResponse createResponseForMemberReplyToAnonymo return TechRepliedCommentsResponse.builder() .techCommentId(repliedTechComment.getId()) - .anonymousMemberId(repliedCreatedBy.getId()) + .memberId(repliedCreatedBy.getId()) .author(repliedCreatedBy.getNickname().getNickname()) - .techParentCommentMemberId(parentCreatedAnonymousBy.getId()) + .techParentCommentAnonymousMemberId(parentCreatedAnonymousBy.getId()) .techParentCommentAuthor(parentCreatedAnonymousBy.getNickname()) .techParentCommentId(repliedTechComment.getParent().getId()) .techOriginParentCommentId(repliedTechComment.getOriginParent().getId()) 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 7851309c..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; @@ -996,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/MemberPickCommentServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickCommentServiceTest.java index 62f7fbee..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; @@ -50,11 +58,7 @@ 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.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; @@ -2601,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/techArticle/GuestTechCommentServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechCommentServiceTest.java index a24496f8..7aaf1e6f 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,6 +1,11 @@ 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; @@ -13,7 +18,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; @@ -1400,71 +1404,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 5e3502d2..7bdbed6d 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; @@ -2364,71 +2368,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/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..765b9377 --- /dev/null +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java @@ -0,0 +1,1342 @@ +package com.dreamypatisiel.devdevdev.domain.service.techArticle.techComment; + +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.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.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.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.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; + @Autowired + 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.getNickname().getNickname(), + 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.getNickname().getNickname(), + 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.getNickname().getNickname(), + 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.getNickname().getNickname(), + 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"), member, + 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, null, authentication); + + // then + assertThat(response.getTotalOriginParentComments()).isEqualTo(6L); + assertThat(response).hasSizeLessThanOrEqualTo(pageable.getPageSize()) + .extracting( + "techCommentId", + "memberId", + "anonymousMemberId", + "author", + "maskedEmail", + "contents", + "replyTotalCount", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted" + ) + .containsExactly( + Tuple.tuple(originParentTechComment6.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment6.getContents().getCommentContents(), + originParentTechComment6.getReplyTotalCount().getCount(), + originParentTechComment6.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + Tuple.tuple(originParentTechComment5.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment5.getContents().getCommentContents(), + originParentTechComment5.getReplyTotalCount().getCount(), + originParentTechComment5.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + Tuple.tuple(originParentTechComment4.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment4.getContents().getCommentContents(), + originParentTechComment4.getReplyTotalCount().getCount(), + originParentTechComment4.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + Tuple.tuple(originParentTechComment3.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment3.getContents().getCommentContents(), + originParentTechComment3.getReplyTotalCount().getCount(), + originParentTechComment3.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + Tuple.tuple(originParentTechComment2.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment2.getContents().getCommentContents(), + originParentTechComment2.getReplyTotalCount().getCount(), + originParentTechComment2.getRecommendTotalCount().getCount(), + false, + false, + false, + false) + ); + + 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); + + 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"), member, + 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, null, authentication); + + // then + assertThat(response.getTotalOriginParentComments()).isEqualTo(6L); + assertThat(response).hasSizeLessThanOrEqualTo(pageable.getPageSize()) + .extracting( + "techCommentId", + "memberId", + "anonymousMemberId", + "author", + "maskedEmail", + "contents", + "replyTotalCount", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted" + ) + .containsExactly( + Tuple.tuple(originParentTechComment2.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment2.getContents().getCommentContents(), + originParentTechComment2.getReplyTotalCount().getCount(), + originParentTechComment2.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + Tuple.tuple(originParentTechComment4.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment4.getContents().getCommentContents(), + originParentTechComment4.getReplyTotalCount().getCount(), + originParentTechComment4.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + Tuple.tuple(originParentTechComment6.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment6.getContents().getCommentContents(), + originParentTechComment6.getReplyTotalCount().getCount(), + originParentTechComment6.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + Tuple.tuple(originParentTechComment5.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment5.getContents().getCommentContents(), + originParentTechComment5.getReplyTotalCount().getCount(), + originParentTechComment5.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + Tuple.tuple(originParentTechComment3.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment3.getContents().getCommentContents(), + originParentTechComment3.getReplyTotalCount().getCount(), + originParentTechComment3.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ) + ); + + TechCommentsResponse techCommentsResponse1 = response.getContent().get(0); + List replies1 = techCommentsResponse1.getReplies(); + assertThat(replies1).hasSize(4) + .extracting( + "techCommentId", + "memberId", + "anonymousMemberId", + "techParentCommentId", + "techParentCommentMemberId", + "techParentCommentAnonymousMemberId", + "techParentCommentAuthor", + "techOriginParentCommentId", + "author", + "maskedEmail", + "contents", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted" + ) + .containsExactly( + Tuple.tuple(parentTechComment1.getId(), + member.getId(), + originParentTechComment2.getId(), + originParentTechComment2.getCreatedBy().getId(), + member.getNicknameAsString(), + originParentTechComment2.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + parentTechComment1.getContents().getCommentContents(), + parentTechComment1.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + 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 + ), + Tuple.tuple(techcomment1.getId(), + member.getId(), + parentTechComment1.getId(), + parentTechComment1.getCreatedBy().getId(), + member.getNicknameAsString(), + originParentTechComment2.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + techcomment1.getContents().getCommentContents(), + techcomment1.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + 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 + ) + ); + + TechCommentsResponse techCommentsResponse2 = response.getContent().get(1); + List replies2 = techCommentsResponse2.getReplies(); + assertThat(replies2).hasSize(2) + .extracting( + "techCommentId", + "memberId", + "anonymousMemberId", + "techParentCommentId", + "techParentCommentMemberId", + "techParentCommentAnonymousMemberId", + "techParentCommentAuthor", + "techOriginParentCommentId", + "author", + "maskedEmail", + "contents", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted" + ) + .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 + ), + 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 + ) + ); + + 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); + + 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"), member, + 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, null, authentication); + + // then + assertThat(response.getTotalOriginParentComments()).isEqualTo(6L); + assertThat(response).hasSizeLessThanOrEqualTo(pageable.getPageSize()) + .extracting( + "techCommentId", + "memberId", + "anonymousMemberId", + "author", + "maskedEmail", + "contents", + "replyTotalCount", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted" + ) + .containsExactly( + Tuple.tuple(originParentTechComment6.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment6.getContents().getCommentContents(), + originParentTechComment6.getReplyTotalCount().getCount(), + originParentTechComment6.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + Tuple.tuple(originParentTechComment3.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment3.getContents().getCommentContents(), + originParentTechComment3.getReplyTotalCount().getCount(), + originParentTechComment3.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + Tuple.tuple(originParentTechComment4.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment4.getContents().getCommentContents(), + originParentTechComment4.getReplyTotalCount().getCount(), + originParentTechComment4.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + Tuple.tuple(originParentTechComment1.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment1.getContents().getCommentContents(), + originParentTechComment1.getReplyTotalCount().getCount(), + originParentTechComment1.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + Tuple.tuple(originParentTechComment5.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment5.getContents().getCommentContents(), + originParentTechComment5.getReplyTotalCount().getCount(), + originParentTechComment5.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ) + ); + + 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); + + 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"), 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, null, authentication); + + // then + assertThat(response.getTotalOriginParentComments()).isEqualTo(5L); // 삭제된 댓글은 카운트하지 않는다 + assertThat(response).hasSizeLessThanOrEqualTo(pageable.getPageSize()) + .extracting( + "techCommentId", + "memberId", + "anonymousMemberId", + "author", + "maskedEmail", + "contents", + "replyTotalCount", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted" + ) + .containsExactly( + Tuple.tuple(originParentTechComment5.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment5.getContents().getCommentContents(), + originParentTechComment5.getReplyTotalCount().getCount(), + originParentTechComment5.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + Tuple.tuple(originParentTechComment4.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment4.getContents().getCommentContents(), + originParentTechComment4.getReplyTotalCount().getCount(), + originParentTechComment4.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + Tuple.tuple(originParentTechComment3.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment3.getContents().getCommentContents(), + originParentTechComment3.getReplyTotalCount().getCount(), + originParentTechComment3.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + Tuple.tuple(originParentTechComment2.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment2.getContents().getCommentContents(), + originParentTechComment2.getReplyTotalCount().getCount(), + originParentTechComment2.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + Tuple.tuple(originParentTechComment1.getId(), + member.getId(), + member.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment1.getContents().getCommentContents(), + originParentTechComment1.getReplyTotalCount().getCount(), + originParentTechComment1.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ) + ); + + 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); + + 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, null, 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)); + + // 회사 생성 + 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"), member1, + 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(), null, + authentication); + + // then + assertThat(response).hasSize(3) + .extracting( + "techCommentId", + "memberId", + "anonymousMemberId", + "author", + "maskedEmail", + "contents", + "replyTotalCount", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted" + ) + .containsExactly( + Tuple.tuple(originParentTechComment1.getId(), + member1.getId(), + member1.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member1.getEmailAsString()), + originParentTechComment1.getContents().getCommentContents(), + originParentTechComment1.getReplyTotalCount().getCount(), + originParentTechComment1.getRecommendTotalCount().getCount(), + false, + false, + true, + false + ), + Tuple.tuple(originParentTechComment2.getId(), + member2.getId(), + member2.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member2.getEmailAsString()), + originParentTechComment2.getContents().getCommentContents(), + originParentTechComment2.getReplyTotalCount().getCount(), + originParentTechComment2.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ), + Tuple.tuple(originParentTechComment3.getId(), + member3.getId(), + member3.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member3.getEmailAsString()), + originParentTechComment3.getContents().getCommentContents(), + originParentTechComment3.getReplyTotalCount().getCount(), + originParentTechComment3.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ) + ); + + TechCommentsResponse techCommentsResponse = response.get(0); + List replies = techCommentsResponse.getReplies(); + assertThat(replies).hasSize(1) + .extracting( + "techCommentId", + "memberId", + "anonymousMemberId", + "techParentCommentId", + "techParentCommentMemberId", + "techParentCommentAnonymousMemberId", + "techParentCommentAuthor", + "techOriginParentCommentId", + "author", + "maskedEmail", + "contents", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted" + ).containsExactly( + Tuple.tuple(repliedTechComment.getId(), + member3.getId(), + repliedTechComment.getParent().getCreatedBy().getId(), + repliedTechComment.getParent().getId(), + repliedTechComment.getOriginParent().getId(), + repliedTechComment.getOriginParent().getCreatedBy().getNicknameAsString(), + member3.getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(member3.getEmailAsString()), + repliedTechComment.getContents().getCommentContents(), + repliedTechComment.getRecommendTotalCount().getCount(), + false, + false, + false, + false + ) + ); + } +} \ No newline at end of file From 8baba41660154ee1aba6480e5731e8a5ae0c5ee5 Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Wed, 23 Jul 2025 23:15:17 +0900 Subject: [PATCH 33/66] =?UTF-8?q?feat(GuestTechCommentServiceV2):=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=A1=B0=ED=9A=8C,=20=EB=B2=A0=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=8C=93=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=EC=97=90=20=EC=9D=B5?= =?UTF-8?q?=EB=AA=85=ED=9A=8C=EC=9B=90=20=EA=B4=80=EB=A0=A8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../techArticle/TechCommentRepository.java | 5 +- .../custom/TechCommentRepositoryImpl.java | 2 +- .../techComment/TechCommentCommonService.java | 4 +- .../GuestTechCommentServiceV2Test.java | 288 +++++++++++------- 4 files changed, 176 insertions(+), 123 deletions(-) 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..620bd09c 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 @@ -18,9 +18,8 @@ Optional findByIdAndTechArticleIdAndCreatedByIdAndDeletedAtIsNull(L 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/custom/TechCommentRepositoryImpl.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechCommentRepositoryImpl.java index 3d249293..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 @@ -53,7 +53,7 @@ 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()) 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 ab9171ed..f06d8df5 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 @@ -53,7 +53,7 @@ public SliceCommentCustom getTechComments(Long techArticle // 최상위 댓글 아이디들의 댓글 답글 조회(최상위 댓글의 아이디가 key) Map> techCommentReplies = techCommentRepository - .findWithMemberWithTechArticleByOriginParentIdInAndParentIsNotNullAndOriginParentIsNotNull( + .findWithDetailsByOriginParentIdInAndParentIsNotNullAndOriginParentIsNotNull( originParentIds).stream() .collect(Collectors.groupingBy(techCommentReply -> techCommentReply.getOriginParent().getId())); @@ -134,7 +134,7 @@ 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())); diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java index 765b9377..b8460ce0 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java @@ -189,7 +189,7 @@ void getTechCommentsSortByOLDEST() { ), Tuple.tuple(originParentTechComment2.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment2.getContents().getCommentContents(), originParentTechComment2.getReplyTotalCount().getCount(), @@ -202,7 +202,7 @@ void getTechCommentsSortByOLDEST() { ), Tuple.tuple(originParentTechComment3.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment3.getContents().getCommentContents(), originParentTechComment3.getReplyTotalCount().getCount(), @@ -215,7 +215,7 @@ void getTechCommentsSortByOLDEST() { ), Tuple.tuple(originParentTechComment4.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment4.getContents().getCommentContents(), originParentTechComment4.getReplyTotalCount().getCount(), @@ -228,7 +228,7 @@ void getTechCommentsSortByOLDEST() { ), Tuple.tuple(originParentTechComment5.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment5.getContents().getCommentContents(), originParentTechComment5.getReplyTotalCount().getCount(), @@ -438,11 +438,11 @@ void getTechCommentsSortByLATEST() { 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 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, @@ -469,7 +469,7 @@ void getTechCommentsSortByLATEST() { // when SliceCommentCustom response = guestTechCommentServiceV2.getTechComments(techArticleId, - null, TechCommentSort.LATEST, pageable, null, authentication); + null, TechCommentSort.LATEST, pageable, anonymousMember.getAnonymousMemberId(), authentication); // then assertThat(response.getTotalOriginParentComments()).isEqualTo(6L); @@ -477,7 +477,6 @@ void getTechCommentsSortByLATEST() { .extracting( "techCommentId", "memberId", - "anonymousMemberId", "author", "maskedEmail", "contents", @@ -486,24 +485,26 @@ void getTechCommentsSortByLATEST() { "isCommentAuthor", "isRecommended", "isModified", - "isDeleted" + "isDeleted", + "anonymousMemberId" ) .containsExactly( Tuple.tuple(originParentTechComment6.getId(), - member.getId(), - member.getNickname().getNickname(), - CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + null, + anonymousMember.getNickname(), + null, originParentTechComment6.getContents().getCommentContents(), originParentTechComment6.getReplyTotalCount().getCount(), originParentTechComment6.getRecommendTotalCount().getCount(), + true, false, false, false, - false + anonymousMember.getId() ), Tuple.tuple(originParentTechComment5.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment5.getContents().getCommentContents(), originParentTechComment5.getReplyTotalCount().getCount(), @@ -511,11 +512,12 @@ void getTechCommentsSortByLATEST() { false, false, false, - false + false, + null ), Tuple.tuple(originParentTechComment4.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment4.getContents().getCommentContents(), originParentTechComment4.getReplyTotalCount().getCount(), @@ -523,11 +525,12 @@ void getTechCommentsSortByLATEST() { false, false, false, - false + false, + null ), Tuple.tuple(originParentTechComment3.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment3.getContents().getCommentContents(), originParentTechComment3.getReplyTotalCount().getCount(), @@ -535,11 +538,12 @@ void getTechCommentsSortByLATEST() { false, false, false, - false + false, + null ), Tuple.tuple(originParentTechComment2.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment2.getContents().getCommentContents(), originParentTechComment2.getReplyTotalCount().getCount(), @@ -547,7 +551,9 @@ void getTechCommentsSortByLATEST() { false, false, false, - false) + false, + null + ) ); TechCommentsResponse techCommentsResponse6 = response.getContent().get(0); @@ -581,6 +587,10 @@ void getTechCommentsSortByMostCommented() { 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); @@ -607,18 +617,14 @@ void getTechCommentsSortByMostCommented() { TechComment originParentTechComment6 = createMainTechComment(new CommentContents("최상위 댓글6"), member, techArticle, new Count(0L), new Count(0L), new Count(0L)); - TechComment parentTechComment1 = createRepliedTechComment(new CommentContents("최상위 댓글2의 답글1"), member, - techArticle, originParentTechComment2, originParentTechComment2, 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)); + 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)); + 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)); + 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)); @@ -639,7 +645,7 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), // when SliceCommentCustom response = guestTechCommentServiceV2.getTechComments(techArticleId, - null, TechCommentSort.MOST_COMMENTED, pageable, null, authentication); + null, TechCommentSort.MOST_COMMENTED, pageable, anonymousMember.getAnonymousMemberId(), authentication); // then assertThat(response.getTotalOriginParentComments()).isEqualTo(6L); @@ -647,7 +653,6 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), .extracting( "techCommentId", "memberId", - "anonymousMemberId", "author", "maskedEmail", "contents", @@ -656,12 +661,13 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), "isCommentAuthor", "isRecommended", "isModified", - "isDeleted" + "isDeleted", + "anonymousMemberId" ) .containsExactly( Tuple.tuple(originParentTechComment2.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment2.getContents().getCommentContents(), originParentTechComment2.getReplyTotalCount().getCount(), @@ -669,11 +675,12 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), false, false, false, - false + false, + null ), Tuple.tuple(originParentTechComment4.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment4.getContents().getCommentContents(), originParentTechComment4.getReplyTotalCount().getCount(), @@ -681,11 +688,12 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), false, false, false, - false + false, + null ), Tuple.tuple(originParentTechComment6.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment6.getContents().getCommentContents(), originParentTechComment6.getReplyTotalCount().getCount(), @@ -693,11 +701,12 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), false, false, false, - false + false, + null ), Tuple.tuple(originParentTechComment5.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment5.getContents().getCommentContents(), originParentTechComment5.getReplyTotalCount().getCount(), @@ -705,11 +714,12 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), false, false, false, - false + false, + null ), Tuple.tuple(originParentTechComment3.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment3.getContents().getCommentContents(), originParentTechComment3.getReplyTotalCount().getCount(), @@ -717,7 +727,8 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), false, false, false, - false + false, + null ) ); @@ -727,10 +738,8 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), .extracting( "techCommentId", "memberId", - "anonymousMemberId", "techParentCommentId", "techParentCommentMemberId", - "techParentCommentAnonymousMemberId", "techParentCommentAuthor", "techOriginParentCommentId", "author", @@ -740,23 +749,27 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), "isCommentAuthor", "isRecommended", "isModified", - "isDeleted" + "isDeleted", + "anonymousMemberId", + "techParentCommentAnonymousMemberId" ) .containsExactly( Tuple.tuple(parentTechComment1.getId(), - member.getId(), + null, originParentTechComment2.getId(), originParentTechComment2.getCreatedBy().getId(), member.getNicknameAsString(), originParentTechComment2.getId(), - member.getNicknameAsString(), - CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + anonymousMember.getNickname(), + null, parentTechComment1.getContents().getCommentContents(), parentTechComment1.getRecommendTotalCount().getCount(), + true, false, false, false, - false + anonymousMember.getId(), + null ), Tuple.tuple(parentTechComment2.getId(), member.getId(), @@ -771,13 +784,15 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), false, false, false, - false + false, + null, + null ), Tuple.tuple(techcomment1.getId(), member.getId(), parentTechComment1.getId(), - parentTechComment1.getCreatedBy().getId(), - member.getNicknameAsString(), + null, + anonymousMember.getNickname(), originParentTechComment2.getId(), member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), @@ -786,7 +801,9 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), false, false, false, - false + false, + null, + parentTechComment1.getCreatedAnonymousBy().getId() ), Tuple.tuple(techcomment2.getId(), member.getId(), @@ -801,7 +818,9 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), false, false, false, - false + false, + null, + null ) ); @@ -811,10 +830,8 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), .extracting( "techCommentId", "memberId", - "anonymousMemberId", "techParentCommentId", "techParentCommentMemberId", - "techParentCommentAnonymousMemberId", "techParentCommentAuthor", "techOriginParentCommentId", "author", @@ -824,7 +841,9 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), "isCommentAuthor", "isRecommended", "isModified", - "isDeleted" + "isDeleted", + "anonymousMemberId", + "techParentCommentAnonymousMemberId" ) .containsExactly( Tuple.tuple(parentTechComment3.getId(), @@ -840,7 +859,9 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), false, false, false, - false + false, + null, + null ), Tuple.tuple(parentTechComment4.getId(), member.getId(), @@ -855,7 +876,9 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), false, false, false, - false + false, + null, + null ) ); @@ -880,6 +903,10 @@ void getTechCommentsSortByMostRecommended() { 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); @@ -903,7 +930,7 @@ void getTechCommentsSortByMostRecommended() { 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"), member, + TechComment originParentTechComment6 = createMainTechComment(new CommentContents("최상위 댓글6"), anonymousMember, techArticle, new Count(0L), new Count(6L), new Count(0L)); techCommentRepository.saveAll(List.of( @@ -918,7 +945,7 @@ void getTechCommentsSortByMostRecommended() { // when SliceCommentCustom response = guestTechCommentServiceV2.getTechComments(techArticleId, - null, TechCommentSort.MOST_LIKED, pageable, null, authentication); + null, TechCommentSort.MOST_LIKED, pageable, anonymousMember.getAnonymousMemberId(), authentication); // then assertThat(response.getTotalOriginParentComments()).isEqualTo(6L); @@ -926,7 +953,6 @@ void getTechCommentsSortByMostRecommended() { .extracting( "techCommentId", "memberId", - "anonymousMemberId", "author", "maskedEmail", "contents", @@ -935,24 +961,26 @@ void getTechCommentsSortByMostRecommended() { "isCommentAuthor", "isRecommended", "isModified", - "isDeleted" + "isDeleted", + "anonymousMemberId" ) .containsExactly( Tuple.tuple(originParentTechComment6.getId(), - member.getId(), - member.getNickname().getNickname(), - CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + null, + anonymousMember.getNickname(), + null, originParentTechComment6.getContents().getCommentContents(), originParentTechComment6.getReplyTotalCount().getCount(), originParentTechComment6.getRecommendTotalCount().getCount(), + true, false, false, false, - false + anonymousMember.getId() ), Tuple.tuple(originParentTechComment3.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment3.getContents().getCommentContents(), originParentTechComment3.getReplyTotalCount().getCount(), @@ -960,11 +988,12 @@ void getTechCommentsSortByMostRecommended() { false, false, false, - false + false, + null ), Tuple.tuple(originParentTechComment4.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment4.getContents().getCommentContents(), originParentTechComment4.getReplyTotalCount().getCount(), @@ -972,11 +1001,12 @@ void getTechCommentsSortByMostRecommended() { false, false, false, - false + false, + null ), Tuple.tuple(originParentTechComment1.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment1.getContents().getCommentContents(), originParentTechComment1.getReplyTotalCount().getCount(), @@ -984,11 +1014,12 @@ void getTechCommentsSortByMostRecommended() { false, false, false, - false + false, + null ), Tuple.tuple(originParentTechComment5.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment5.getContents().getCommentContents(), originParentTechComment5.getReplyTotalCount().getCount(), @@ -996,7 +1027,8 @@ void getTechCommentsSortByMostRecommended() { false, false, false, - false + false, + null ) ); @@ -1029,6 +1061,10 @@ void getTechCommentsByCursor() { 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); @@ -1041,7 +1077,7 @@ void getTechCommentsByCursor() { techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); - TechComment originParentTechComment1 = createMainTechComment(new CommentContents("최상위 댓글1"), member, + 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)); @@ -1068,7 +1104,7 @@ void getTechCommentsByCursor() { // when SliceCommentCustom response = guestTechCommentServiceV2.getTechComments(techArticleId, - originParentTechComment6.getId(), null, pageable, null, authentication); + originParentTechComment6.getId(), null, pageable, anonymousMember.getAnonymousMemberId(), authentication); // then assertThat(response.getTotalOriginParentComments()).isEqualTo(5L); // 삭제된 댓글은 카운트하지 않는다 @@ -1076,7 +1112,6 @@ void getTechCommentsByCursor() { .extracting( "techCommentId", "memberId", - "anonymousMemberId", "author", "maskedEmail", "contents", @@ -1085,12 +1120,13 @@ void getTechCommentsByCursor() { "isCommentAuthor", "isRecommended", "isModified", - "isDeleted" + "isDeleted", + "anonymousMemberId" ) .containsExactly( Tuple.tuple(originParentTechComment5.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment5.getContents().getCommentContents(), originParentTechComment5.getReplyTotalCount().getCount(), @@ -1098,11 +1134,12 @@ void getTechCommentsByCursor() { false, false, false, - false + false, + null ), Tuple.tuple(originParentTechComment4.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment4.getContents().getCommentContents(), originParentTechComment4.getReplyTotalCount().getCount(), @@ -1110,11 +1147,12 @@ void getTechCommentsByCursor() { false, false, false, - false + false, + null ), Tuple.tuple(originParentTechComment3.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment3.getContents().getCommentContents(), originParentTechComment3.getReplyTotalCount().getCount(), @@ -1122,11 +1160,12 @@ void getTechCommentsByCursor() { false, false, false, - false + false, + null ), Tuple.tuple(originParentTechComment2.getId(), member.getId(), - member.getNickname().getNickname(), + member.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), originParentTechComment2.getContents().getCommentContents(), originParentTechComment2.getReplyTotalCount().getCount(), @@ -1134,19 +1173,21 @@ void getTechCommentsByCursor() { false, false, false, - false + false, + null ), Tuple.tuple(originParentTechComment1.getId(), - member.getId(), - member.getNickname().getNickname(), - CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + null, + anonymousMember.getNickname(), + null, originParentTechComment1.getContents().getCommentContents(), originParentTechComment1.getReplyTotalCount().getCount(), originParentTechComment1.getRecommendTotalCount().getCount(), + true, false, false, false, - false + anonymousMember.getId() ) ); @@ -1179,6 +1220,10 @@ void findTechBestCommentsNotAnonymousMember() { 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(), @@ -1186,7 +1231,8 @@ void findTechBestCommentsNotAnonymousMember() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // when // then - assertThatThrownBy(() -> guestTechCommentServiceV2.findTechBestComments(3, 1L, null, authentication)) + assertThatThrownBy(() -> guestTechCommentServiceV2.findTechBestComments(3, 1L, anonymousMember.getAnonymousMemberId(), + authentication)) .isInstanceOf(IllegalStateException.class) .hasMessage(INVALID_METHODS_CALL_MESSAGE); } @@ -1207,6 +1253,10 @@ void findTechBestComments() { 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"); @@ -1218,7 +1268,7 @@ void findTechBestComments() { 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, @@ -1234,8 +1284,7 @@ void findTechBestComments() { // 답글 생성 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 @@ -1243,15 +1292,14 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), Authentication authentication = mock(Authentication.class); when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); - List response = guestTechCommentServiceV2.findTechBestComments(3, techArticle.getId(), null, - authentication); + List response = guestTechCommentServiceV2.findTechBestComments(3, techArticle.getId(), + anonymousMember.getAnonymousMemberId(), authentication); // then assertThat(response).hasSize(3) .extracting( "techCommentId", "memberId", - "anonymousMemberId", "author", "maskedEmail", "contents", @@ -1260,24 +1308,26 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), "isCommentAuthor", "isRecommended", "isModified", - "isDeleted" + "isDeleted", + "anonymousMemberId" ) .containsExactly( Tuple.tuple(originParentTechComment1.getId(), - member1.getId(), - member1.getNickname().getNickname(), - CommonResponseUtil.sliceAndMaskEmail(member1.getEmailAsString()), + null, + anonymousMember.getNickname(), + null, originParentTechComment1.getContents().getCommentContents(), originParentTechComment1.getReplyTotalCount().getCount(), originParentTechComment1.getRecommendTotalCount().getCount(), - false, + true, false, true, - false + false, + anonymousMember.getId() ), Tuple.tuple(originParentTechComment2.getId(), member2.getId(), - member2.getNickname().getNickname(), + member2.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member2.getEmailAsString()), originParentTechComment2.getContents().getCommentContents(), originParentTechComment2.getReplyTotalCount().getCount(), @@ -1285,11 +1335,12 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), false, false, false, - false + false, + null ), Tuple.tuple(originParentTechComment3.getId(), member3.getId(), - member3.getNickname().getNickname(), + member3.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member3.getEmailAsString()), originParentTechComment3.getContents().getCommentContents(), originParentTechComment3.getReplyTotalCount().getCount(), @@ -1297,7 +1348,8 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), false, false, false, - false + false, + null ) ); @@ -1307,10 +1359,8 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), .extracting( "techCommentId", "memberId", - "anonymousMemberId", "techParentCommentId", "techParentCommentMemberId", - "techParentCommentAnonymousMemberId", "techParentCommentAuthor", "techOriginParentCommentId", "author", @@ -1320,22 +1370,26 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), "isCommentAuthor", "isRecommended", "isModified", - "isDeleted" + "isDeleted", + "anonymousMemberId", + "techParentCommentAnonymousMemberId" ).containsExactly( Tuple.tuple(repliedTechComment.getId(), member3.getId(), - repliedTechComment.getParent().getCreatedBy().getId(), repliedTechComment.getParent().getId(), + null, + repliedTechComment.getParent().getCreatedAnonymousBy().getNickname(), repliedTechComment.getOriginParent().getId(), - repliedTechComment.getOriginParent().getCreatedBy().getNicknameAsString(), - member3.getNickname().getNickname(), + member3.getNicknameAsString(), CommonResponseUtil.sliceAndMaskEmail(member3.getEmailAsString()), repliedTechComment.getContents().getCommentContents(), repliedTechComment.getRecommendTotalCount().getCount(), false, false, false, - false + false, + null, + repliedTechComment.getParent().getCreatedAnonymousBy().getId() ) ); } From 3ca3dc7a797c1f7a81e39f9f7f0381e151ae49c4 Mon Sep 17 00:00:00 2001 From: soyoung Date: Wed, 23 Jul 2025 23:23:17 +0900 Subject: [PATCH 34/66] =?UTF-8?q?fix(nickname):=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EB=8B=89=EB=84=A4=EC=9E=84=20=EB=B3=80=EA=B2=BD=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=20=EC=8B=9C=EA=B0=84=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devdevdev/domain/entity/Member.java | 4 ++-- .../domain/policy/NicknameChangePolicy.java | 14 ++++++++++++++ .../domain/service/member/MemberService.java | 10 +++++++--- .../devdevdev/domain/entity/MemberTest.java | 3 ++- 4 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/dreamypatisiel/devdevdev/domain/policy/NicknameChangePolicy.java 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 7ba25497..ae22b1db 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java @@ -196,8 +196,8 @@ public void changeNickname(String nickname, LocalDateTime now) { this.nicknameUpdatedAt = now; } - public boolean canChangeNickname() { + public boolean canChangeNickname(long restrictionHours) { return nicknameUpdatedAt == null - || ChronoUnit.HOURS.between(nicknameUpdatedAt, LocalDateTime.now()) >= 24; + || ChronoUnit.HOURS.between(nicknameUpdatedAt, LocalDateTime.now()) >= restrictionHours; } } 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..62425cc6 --- /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.hours:24}") + private int nicknameChangeIntervalHours; + + public int getNicknameChangeIntervalHours() { + return nicknameChangeIntervalHours; + } +} 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 622c5439..4053a467 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 @@ -2,6 +2,7 @@ import com.dreamypatisiel.devdevdev.domain.entity.*; 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; @@ -47,6 +48,8 @@ import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.MEMBER_INCOMPLETE_SURVEY_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.NicknameExceptionMessage.NICKNAME_CHANGE_RATE_LIMIT_MESSAGE; +import org.springframework.beans.factory.annotation.Value; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -63,6 +66,7 @@ public class MemberService { private final SurveyAnswerJdbcTemplateRepository surveyAnswerJdbcTemplateRepository; private final CommentRepository commentRepository; private final CompanyRepository companyRepository; + private final NicknameChangePolicy nicknameChangePolicy; /** * 회원 탈퇴 회원의 북마크와 회원 정보를 삭제합니다. @@ -290,7 +294,7 @@ public SliceCustom findMySubscribedCompanies(Pageable } /** - * @Note: 유저의 닉네임을 변경합니다. 최근 24시간 이내에 변경한 이력이 있다면 닉네임 변경이 불가능합니다. + * @Note: 유저의 닉네임을 변경합니다. 설정된 제한 시간 이내에 변경한 이력이 있다면 닉네임 변경이 불가능합니다. * @Author: 유소영 * @Since: 2025.07.03 */ @@ -298,7 +302,7 @@ public SliceCustom findMySubscribedCompanies(Pageable public String changeNickname(String nickname, Authentication authentication) { Member member = memberProvider.getMemberByAuthentication(authentication); - if (!member.canChangeNickname()) { + if (!member.canChangeNickname(nicknameChangePolicy.getNicknameChangeIntervalHours())) { throw new NicknameException(NICKNAME_CHANGE_RATE_LIMIT_MESSAGE); } @@ -313,6 +317,6 @@ public String changeNickname(String nickname, Authentication authentication) { */ public boolean canChangeNickname(Authentication authentication) { Member member = memberProvider.getMemberByAuthentication(authentication); - return member.canChangeNickname(); + return member.canChangeNickname(nicknameChangePolicy.getNicknameChangeIntervalHours()); } } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java index 84f77efe..4b18c549 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java @@ -24,8 +24,9 @@ void canChangeNickname(Long hoursAgo, boolean expected) { if (hoursAgo != null) { member.changeNickname("닉네임", LocalDateTime.now().minusHours(hoursAgo)); } + int nicknameChangeIntervalHours = 24; // when - boolean result = member.canChangeNickname(); + boolean result = member.canChangeNickname(nicknameChangeIntervalHours); // then assertThat(result).isEqualTo(expected); } From 42110031257fb815715dcd916ecf24455dc0e488 Mon Sep 17 00:00:00 2001 From: soyoung Date: Wed, 23 Jul 2025 23:31:06 +0900 Subject: [PATCH 35/66] =?UTF-8?q?fix(nickname):=20long=20->=20int=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EB=B6=88=EC=9D=BC=EC=B9=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/dreamypatisiel/devdevdev/domain/entity/Member.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ae22b1db..7850c731 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java @@ -196,7 +196,7 @@ public void changeNickname(String nickname, LocalDateTime now) { this.nicknameUpdatedAt = now; } - public boolean canChangeNickname(long restrictionHours) { + public boolean canChangeNickname(int restrictionHours) { return nicknameUpdatedAt == null || ChronoUnit.HOURS.between(nicknameUpdatedAt, LocalDateTime.now()) >= restrictionHours; } From 435899dfb66c77053abfcf9ec6416c8bc10e55e9 Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Wed, 23 Jul 2025 23:41:33 +0900 Subject: [PATCH 36/66] =?UTF-8?q?docs(TechArticleCommentControllerDocsTest?= =?UTF-8?q?):=20=EB=8C=93=EA=B8=80=20=EC=A1=B0=ED=9A=8C,=20=EB=B2=A0?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=8C=93=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=EC=97=90=20?= =?UTF-8?q?=EC=9D=B5=EB=AA=85=ED=9A=8C=EC=9B=90=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TechArticleCommentControllerDocsTest.java | 168 ++++++------------ 1 file changed, 50 insertions(+), 118 deletions(-) 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 abab1764..637858ab 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; @@ -68,10 +72,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,6 +91,9 @@ public class TechArticleCommentControllerDocsTest extends SupportControllerDocsT @Autowired TechCommentRecommendRepository techCommentRecommendRepository; + @Autowired + AnonymousMemberRepository anonymousMemberRepository; + @Autowired EntityManager em; @@ -180,7 +183,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("기술블로그 아이디") @@ -385,7 +389,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("기술블로그 아이디"), @@ -553,7 +558,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("기술블로그 아이디"), @@ -670,7 +676,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("기술블로그 아이디"), @@ -754,30 +761,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 +788,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( @@ -844,7 +839,8 @@ techArticle, originParentTechComment2, originParentTechComment2, 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("기술블로그 아이디") @@ -866,7 +862,7 @@ techArticle, originParentTechComment2, originParentTechComment2, new Count(0L), 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("회원의 기술블로그 댓글 작성자 여부"), @@ -901,7 +897,7 @@ techArticle, originParentTechComment2, originParentTechComment2, new Count(0L), 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) @@ -984,7 +980,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("기술블로그 아이디"), @@ -1069,6 +1066,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"); @@ -1081,7 +1082,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, @@ -1116,7 +1117,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("기술블로그 아이디") @@ -1137,16 +1139,12 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), .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("기술블로그 답글 아이디"), @@ -1181,70 +1179,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 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(); - } } From b1a911bb11fc6a182674caffadbe0ca49534b57e Mon Sep 17 00:00:00 2001 From: soyoung Date: Sun, 27 Jul 2025 12:51:59 +0900 Subject: [PATCH 37/66] =?UTF-8?q?fix(nickname):=20=ED=98=84=EC=9E=AC=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=99=B8=EB=B6=80=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=A3=BC=EC=9E=85=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/dreamypatisiel/devdevdev/domain/entity/Member.java | 4 ++-- .../devdevdev/domain/service/member/MemberService.java | 4 ++-- .../dreamypatisiel/devdevdev/domain/entity/MemberTest.java | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) 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 7850c731..88b4b5b7 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java @@ -196,8 +196,8 @@ public void changeNickname(String nickname, LocalDateTime now) { this.nicknameUpdatedAt = now; } - public boolean canChangeNickname(int restrictionHours) { + public boolean canChangeNickname(int restrictionHours, LocalDateTime now) { return nicknameUpdatedAt == null - || ChronoUnit.HOURS.between(nicknameUpdatedAt, LocalDateTime.now()) >= restrictionHours; + || ChronoUnit.HOURS.between(nicknameUpdatedAt, now) >= restrictionHours; } } 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 4053a467..95431c03 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 @@ -302,7 +302,7 @@ public SliceCustom findMySubscribedCompanies(Pageable public String changeNickname(String nickname, Authentication authentication) { Member member = memberProvider.getMemberByAuthentication(authentication); - if (!member.canChangeNickname(nicknameChangePolicy.getNicknameChangeIntervalHours())) { + if (!member.canChangeNickname(nicknameChangePolicy.getNicknameChangeIntervalHours(), timeProvider.getLocalDateTimeNow())) { throw new NicknameException(NICKNAME_CHANGE_RATE_LIMIT_MESSAGE); } @@ -317,6 +317,6 @@ public String changeNickname(String nickname, Authentication authentication) { */ public boolean canChangeNickname(Authentication authentication) { Member member = memberProvider.getMemberByAuthentication(authentication); - return member.canChangeNickname(nicknameChangePolicy.getNicknameChangeIntervalHours()); + return member.canChangeNickname(nicknameChangePolicy.getNicknameChangeIntervalHours(), timeProvider.getLocalDateTimeNow()); } } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java index 4b18c549..504a5433 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java @@ -20,13 +20,14 @@ class MemberTest { @DisplayName("닉네임 변경 가능 여부 파라미터 테스트") void canChangeNickname(Long hoursAgo, boolean expected) { // given + LocalDateTime now = LocalDateTime.now(); Member member = new Member(); if (hoursAgo != null) { - member.changeNickname("닉네임", LocalDateTime.now().minusHours(hoursAgo)); + member.changeNickname("닉네임", now.minusHours(hoursAgo)); } int nicknameChangeIntervalHours = 24; // when - boolean result = member.canChangeNickname(nicknameChangeIntervalHours); + boolean result = member.canChangeNickname(nicknameChangeIntervalHours, now); // then assertThat(result).isEqualTo(expected); } From 60930c9c243c698d53b9b443481b012bc90c2631 Mon Sep 17 00:00:00 2001 From: soyoung Date: Sun, 27 Jul 2025 13:35:30 +0900 Subject: [PATCH 38/66] =?UTF-8?q?fix(nickname):=20DEV=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20=EA=B0=80=EB=8A=A5=20=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=201=EB=B6=84=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devdevdev/domain/entity/Member.java | 4 +- .../domain/policy/NicknameChangePolicy.java | 8 ++-- .../domain/service/member/MemberService.java | 4 +- .../devdevdev/domain/entity/MemberTest.java | 39 +++++++++++++++---- .../service/member/MemberServiceTest.java | 38 +++++++++++------- 5 files changed, 64 insertions(+), 29 deletions(-) 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 88b4b5b7..62e69672 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java @@ -196,8 +196,8 @@ public void changeNickname(String nickname, LocalDateTime now) { this.nicknameUpdatedAt = now; } - public boolean canChangeNickname(int restrictionHours, LocalDateTime now) { + public boolean canChangeNickname(int restrictionMinutes, LocalDateTime now) { return nicknameUpdatedAt == null - || ChronoUnit.HOURS.between(nicknameUpdatedAt, now) >= restrictionHours; + || ChronoUnit.MINUTES.between(nicknameUpdatedAt, now) >= restrictionMinutes; } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/policy/NicknameChangePolicy.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/policy/NicknameChangePolicy.java index 62425cc6..059e0cb5 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/policy/NicknameChangePolicy.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/policy/NicknameChangePolicy.java @@ -5,10 +5,10 @@ @Component public class NicknameChangePolicy { - @Value("${nickname.change.interval.hours:24}") - private int nicknameChangeIntervalHours; + @Value("${nickname.change.interval.minutes:1440}") + private int nicknameChangeIntervalMinutes; - public int getNicknameChangeIntervalHours() { - return nicknameChangeIntervalHours; + public int getNicknameChangeIntervalMinutes() { + return nicknameChangeIntervalMinutes; } } 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 95431c03..8d17f0aa 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 @@ -302,7 +302,7 @@ public SliceCustom findMySubscribedCompanies(Pageable public String changeNickname(String nickname, Authentication authentication) { Member member = memberProvider.getMemberByAuthentication(authentication); - if (!member.canChangeNickname(nicknameChangePolicy.getNicknameChangeIntervalHours(), timeProvider.getLocalDateTimeNow())) { + if (!member.canChangeNickname(nicknameChangePolicy.getNicknameChangeIntervalMinutes(), timeProvider.getLocalDateTimeNow())) { throw new NicknameException(NICKNAME_CHANGE_RATE_LIMIT_MESSAGE); } @@ -317,6 +317,6 @@ public String changeNickname(String nickname, Authentication authentication) { */ public boolean canChangeNickname(Authentication authentication) { Member member = memberProvider.getMemberByAuthentication(authentication); - return member.canChangeNickname(nicknameChangePolicy.getNicknameChangeIntervalHours(), timeProvider.getLocalDateTimeNow()); + return member.canChangeNickname(nicknameChangePolicy.getNicknameChangeIntervalMinutes(), timeProvider.getLocalDateTimeNow()); } } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java index 504a5433..76124f35 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java @@ -9,25 +9,48 @@ 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, false", // 24시간 이내 - "24, true", // 24시간 경과(경계) - "25, true", // 24시간 초과 + "1, true", // 24시간 이내 + "60, true", // 24시간 경과(경계) + "1440, true", // 24시간 초과 }) @DisplayName("닉네임 변경 가능 여부 파라미터 테스트") - void canChangeNickname(Long hoursAgo, boolean expected) { + void canChangeNicknameWhenDev(Long minutesAgo, boolean expected) { // given LocalDateTime now = LocalDateTime.now(); Member member = new Member(); - if (hoursAgo != null) { - member.changeNickname("닉네임", now.minusHours(hoursAgo)); + if (minutesAgo != null) { + member.changeNickname("닉네임", now.minusMinutes(minutesAgo)); } - int nicknameChangeIntervalHours = 24; + int restrictionMinutes = 1; // 1분 // when - boolean result = member.canChangeNickname(nicknameChangeIntervalHours, now); + boolean result = member.canChangeNickname(restrictionMinutes, now); // then assertThat(result).isEqualTo(expected); } 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 47e17764..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 @@ -37,6 +37,7 @@ 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; @@ -62,6 +63,7 @@ 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; @@ -119,6 +121,8 @@ class MemberServiceTest extends ElasticsearchSupportTest { PickCommentRepository pickCommentRepository; @Autowired SubscriptionRepository subscriptionRepository; + @MockBean + TimeProvider timeProvider; @Test @DisplayName("회원이 회원탈퇴 설문조사를 완료하지 않으면 탈퇴가 불가능하다.") @@ -458,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); @@ -1205,24 +1211,27 @@ void changeNickname() { assertThat(changedNickname).isEqualTo(newNickname); } - @DisplayName("회원이 24시간 이내에 닉네임을 변경한 적이 있다면 예외가 발생한다.") + @DisplayName("회원이 1440분(24시간) 이내에 닉네임을 변경한 적이 있다면 예외가 발생한다.") @ParameterizedTest @CsvSource({ "0, true", - "1, true", - "23, true", - "24, false", // 변경 허용 - "25, false" // 변경 허용 + "60, true", // 1시간 + "1439, true", // 23.9시간 + "1440, false", // 24시간, 변경 허용 + "1500, false" // 25시간, 변경 허용 }) - void changeNicknameThrowsExceptionWhenChangedWithin24Hours(long hoursAgo, boolean shouldThrowException) { + 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, LocalDateTime.now().minusHours(hoursAgo)); + member.changeNickname(oldNickname, fixedNow.minusMinutes(minutesAgo)); memberRepository.save(member); UserPrincipal userPrincipal = UserPrincipal.createByMember(member); @@ -1247,20 +1256,23 @@ void changeNicknameThrowsExceptionWhenChangedWithin24Hours(long hoursAgo, boolea @ParameterizedTest @CsvSource({ "0, false", - "1, false", - "23, false", - "24, true", - "25, true" + "60, false", // 1시간 + "1439, false", // 23.9시간 + "1440, true", // 24시간 + "1500, true" // 25시간 }) - void canChangeNickname(long hoursAgo, boolean expected) { + 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, LocalDateTime.now().minusHours(hoursAgo)); + member.changeNickname(newNickname, fixedNow.minusMinutes(minutesAgo)); memberRepository.save(member); UserPrincipal userPrincipal = UserPrincipal.createByMember(member); From 4231e9e44e4fb7000a0fd5b7607fbaa8e1a375a9 Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sun, 27 Jul 2025 16:04:19 +0900 Subject: [PATCH 39/66] feat(GuestTechCommentServiceV2): registerMainTechComment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 익명회원 댓글 작성 서비스 개발 및 테스트 코드 작성 --- .../devdevdev/domain/entity/TechComment.java | 22 +++- .../techArticle/dto/TechCommentDto.java | 18 +++ .../techComment/GuestTechCommentService.java | 3 +- .../GuestTechCommentServiceV2.java | 44 +++++++- .../techComment/MemberTechCommentService.java | 14 +-- .../techComment/TechCommentCommonService.java | 4 +- .../techComment/TechCommentService.java | 3 +- .../TechArticleCommentController.java | 7 +- src/main/resources/application-local.yml | 5 + .../GuestTechCommentServiceTest.java | 8 +- .../MemberTechCommentServiceTest.java | 40 ++++--- .../GuestTechCommentServiceV2Test.java | 105 ++++++++++++++++++ .../TechArticleCommentControllerTest.java | 41 +++---- .../TechArticleCommentControllerDocsTest.java | 20 ++-- 14 files changed, 259 insertions(+), 75 deletions(-) create mode 100644 src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/dto/TechCommentDto.java 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 ede50c29..84121cc8 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechComment.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechComment.java @@ -113,8 +113,8 @@ private TechComment(CommentContents contents, Count blameTotalCount, Count recom 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) @@ -125,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) 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..7b9613e0 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/dto/TechCommentDto.java @@ -0,0 +1,18 @@ +package com.dreamypatisiel.devdevdev.domain.service.techArticle.dto; + +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; + } +} 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 185a4f92..9ce43a5d 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 @@ -6,6 +6,7 @@ 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.global.utils.AuthenticationMemberUtils; import com.dreamypatisiel.devdevdev.web.dto.SliceCommentCustom; import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.ModifyTechCommentRequest; @@ -35,7 +36,7 @@ public GuestTechCommentService(TechCommentRepository techCommentRepository, @Override public TechCommentResponse registerMainTechComment(Long techArticleId, - RegisterTechCommentRequest registerTechCommentRequest, + TechCommentDto techCommentDto, Authentication authentication) { throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); } 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 index 1af1098f..dc874617 100644 --- 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 @@ -3,10 +3,15 @@ import static com.dreamypatisiel.devdevdev.domain.exception.GuestExceptionMessage.INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_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.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.global.utils.AuthenticationMemberUtils; import com.dreamypatisiel.devdevdev.web.dto.SliceCommentCustom; import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.ModifyTechCommentRequest; @@ -26,19 +31,46 @@ public class GuestTechCommentServiceV2 extends TechCommentCommonService implements TechCommentService { private final AnonymousMemberService anonymousMemberService; + private final TechCommentCommonService techCommentCommonService; + private final TechArticleCommonService techArticleCommonService; - public GuestTechCommentServiceV2(TechCommentRepository techCommentRepository, - TechBestCommentsPolicy techBestCommentsPolicy, - AnonymousMemberService anonymousMemberService) { + public GuestTechCommentServiceV2(TechCommentRepository techCommentRepository, TechBestCommentsPolicy techBestCommentsPolicy, + AnonymousMemberService anonymousMemberService, + TechCommentCommonService techCommentCommonService, + TechArticleCommonService techArticleCommonService) { super(techCommentRepository, techBestCommentsPolicy); this.anonymousMemberService = anonymousMemberService; + this.techCommentCommonService = techCommentCommonService; + this.techArticleCommonService = techArticleCommonService; } @Override - public TechCommentResponse registerMainTechComment(Long techArticleId, - RegisterTechCommentRequest registerTechCommentRequest, + @Transactional + public TechCommentResponse registerMainTechComment(Long techArticleId, TechCommentDto techCommentDto, Authentication authentication) { - throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); + + // 익명 회원인지 검증 + AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); + + String anonymousMemberId = techCommentDto.getAnonymousMemberId(); + String contents = techCommentDto.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 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 569b69f4..cb5266f7 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 @@ -15,6 +15,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.techArticle.TechArticleCommonService; import com.dreamypatisiel.devdevdev.exception.NotFoundException; import com.dreamypatisiel.devdevdev.global.common.MemberProvider; @@ -40,15 +41,12 @@ public class MemberTechCommentService extends TechCommentCommonService implement 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); @@ -56,7 +54,6 @@ public MemberTechCommentService(TechCommentRepository techCommentRepository, this.memberProvider = memberProvider; this.timeProvider = timeProvider; this.techArticlePopularScorePolicy = techArticlePopularScorePolicy; - this.techCommentRepository = techCommentRepository1; this.techCommentRecommendRepository = techCommentRecommendRepository; } @@ -66,8 +63,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); @@ -76,8 +74,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); @@ -113,7 +111,7 @@ public TechCommentResponse registerRepliedTechComment(Long techArticleId, TechArticle findTechArticle = findParentTechComment.getTechArticle(); String contents = registerRepliedTechCommentRequest.getContents(); - TechComment repliedTechComment = TechComment.createRepliedTechComment(new CommentContents(contents), findMember, + TechComment repliedTechComment = TechComment.createRepliedTechCommentByMember(new CommentContents(contents), findMember, findTechArticle, findOriginParentTechComment, findParentTechComment); techCommentRepository.save(repliedTechComment); 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 f06d8df5..acfc6caa 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 @@ -29,8 +29,8 @@ @Transactional(readOnly = true) public class TechCommentCommonService { - private final TechCommentRepository techCommentRepository; - private final TechBestCommentsPolicy techBestCommentsPolicy; + protected final TechCommentRepository techCommentRepository; + protected final TechBestCommentsPolicy techBestCommentsPolicy; /** * @Note: 정렬 조건에 따라 커서 방식으로 기술블로그 댓글 목록을 조회한다. 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 a6ac42bf..23980e2c 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,6 +1,7 @@ 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.request.techArticle.ModifyTechCommentRequest; import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.RegisterTechCommentRequest; @@ -14,7 +15,7 @@ public interface TechCommentService { TechCommentResponse registerMainTechComment(Long techArticleId, - RegisterTechCommentRequest registerTechCommentRequest, + TechCommentDto techCommentDto, Authentication authentication); TechCommentResponse registerRepliedTechComment(Long techArticleId, 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 7e5e8a43..c0aac1ff 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 @@ -4,6 +4,7 @@ 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; @@ -48,10 +49,14 @@ public ResponseEntity> registerMainTechCommen @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)); } 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/service/techArticle/GuestTechCommentServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechCommentServiceTest.java index 7aaf1e6f..da5c0118 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 @@ -29,6 +29,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.GuestTechCommentService; import com.dreamypatisiel.devdevdev.global.common.TimeProvider; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; @@ -113,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); } @@ -141,7 +143,7 @@ void registerRepliedTechComment() { 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(); @@ -175,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 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 7bdbed6d..718a0927 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 @@ -34,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; @@ -134,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 @@ -184,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); } @@ -204,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); } @@ -236,7 +240,7 @@ void modifyTechComment() { 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(); @@ -349,7 +353,7 @@ void modifyTechCommentAlreadyDeletedException() { 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(); @@ -391,7 +395,7 @@ void deleteTechComment() { 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(); @@ -438,7 +442,7 @@ void deleteTechCommentAlreadyDeletedException() { 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(); @@ -514,7 +518,7 @@ void deleteTechCommentAdmin() { 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(); @@ -565,7 +569,7 @@ void deleteTechCommentNotByMemberException() { 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(); @@ -620,7 +624,7 @@ void registerRepliedTechComment() { 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(); @@ -680,12 +684,12 @@ 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(); @@ -745,7 +749,7 @@ 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; @@ -783,7 +787,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(); @@ -1923,7 +1927,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 @@ -1961,7 +1965,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); @@ -2002,7 +2006,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; @@ -2037,7 +2041,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(); diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java index b8460ce0..d329be10 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java @@ -1,5 +1,6 @@ package com.dreamypatisiel.devdevdev.domain.service.techArticle.techComment; +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; @@ -8,6 +9,7 @@ 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; @@ -30,11 +32,15 @@ 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.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; @@ -1393,4 +1399,103 @@ void findTechBestComments() { ) ); } + + @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); + } } \ No newline at end of file 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..7655f8a4 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,8 +23,6 @@ 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; @@ -28,7 +37,6 @@ 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; @@ -46,13 +54,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 { @@ -259,7 +260,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 +302,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 +377,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 +420,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 +458,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 @@ -492,12 +493,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(); @@ -541,12 +542,12 @@ void registerRepliedTechCommentContentsIsNullException(String contents) throws E 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(); @@ -708,7 +709,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(); 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 637858ab..d8f2d688 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java @@ -363,7 +363,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(); @@ -428,7 +428,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(); @@ -535,7 +535,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(); @@ -593,7 +593,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 @@ -644,12 +644,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(); @@ -716,12 +716,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(); @@ -958,7 +958,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 @@ -1017,7 +1017,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 From abd8258af0cf79c71da4d18b0483b47ffe2d8ed8 Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sun, 27 Jul 2025 17:26:03 +0900 Subject: [PATCH 40/66] =?UTF-8?q?feat(TechArticleCommentController):=20?= =?UTF-8?q?=EA=B8=B0=EC=88=A0=EB=B8=94=EB=A1=9C=EA=B7=B8=20=EC=9D=B5?= =?UTF-8?q?=EB=AA=85=20=ED=9A=8C=EC=9B=90=20=EB=8C=93=EA=B8=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tech-article-comment-register.adoc | 8 +-- .../TechArticleServiceStrategy.java | 4 +- .../GuestTechCommentServiceV2.java | 1 + .../TechArticleCommentController.java | 5 +- .../TechArticleCommentControllerTest.java | 62 ++++++++++++++----- .../TechArticleCommentControllerDocsTest.java | 17 ++--- 6 files changed, 60 insertions(+), 37 deletions(-) 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..a68bba9f 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 @@ -2,7 +2,6 @@ == 기술블로그 댓글 작성 API(POST: /devdevdev/api/v1/articles/{techArticleId}/comments) * 회원은 기술블로그에 댓글을 작성할 수 있다. -* 익명회원은 댓글을 작성할 수 없다. === 정상 요청/응답 @@ -36,10 +35,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/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/techComment/GuestTechCommentServiceV2.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2.java index dc874617..a243f8df 100644 --- 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 @@ -126,6 +126,7 @@ public TechCommentRecommendResponse recommendTechComment(Long techArticleId, Lon * @Since: 2025.07.20 */ @Override + @Transactional public List findTechBestComments(int size, Long techArticleId, String anonymousMemberId, Authentication authentication) { 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 c0aac1ff..476fb84d 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 @@ -42,7 +42,7 @@ public class TechArticleCommentController { private final TechArticleServiceStrategy techArticleServiceStrategy; - @Operation(summary = "기술블로그 댓글 작성") + @Operation(summary = "기술블로그 댓글 작성", description = "기술블로그 댓글을 작성할 수 있습니다.") @PostMapping("/articles/{techArticleId}/comments") public ResponseEntity> registerMainTechComment( @PathVariable Long techArticleId, @@ -73,8 +73,7 @@ public ResponseEntity> registerRepliedTechCom TechCommentService techCommentService = techArticleServiceStrategy.getTechCommentService(); TechCommentResponse response = techCommentService.registerRepliedTechComment(techArticleId, - originParentTechCommentId, - parentTechCommentId, registerRepliedTechCommentRequest, authentication); + originParentTechCommentId, parentTechCommentId, registerRepliedTechCommentRequest, authentication); return ResponseEntity.ok(BasicResponse.success(response)); } 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 7655f8a4..ff59b145 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 @@ -29,10 +29,12 @@ 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; @@ -41,6 +43,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; @@ -71,7 +74,10 @@ class TechArticleCommentControllerTest extends SupportControllerTest { TimeProvider timeProvider; @Autowired EntityManager em; + @Autowired + TechArticleServiceStrategy techArticleServiceStrategy; + @Disabled("GuestTechCommentServiceV2는 테스트가 불가능하다. 익명 회원은 댓글 작성이 가능 하기 때문") @Test @DisplayName("익명 사용자는 기술블로그 댓글을 작성할 수 없다.") void registerTechCommentByAnonymous() throws Exception { @@ -81,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(); @@ -104,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"); @@ -140,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 { @@ -149,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; @@ -186,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(); @@ -864,8 +896,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 @@ -873,6 +904,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/TechArticleCommentControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java index d8f2d688..674c2e1c 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java @@ -62,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; @@ -97,6 +98,7 @@ public class TechArticleCommentControllerDocsTest extends SupportControllerDocsT @Autowired EntityManager em; + @Disabled("GuestTechCommentServiceV2는 테스트가 불가능하다. 익명 회원은 댓글 작성이 가능 하기 때문") @Test @DisplayName("익명 사용자는 기술블로그 댓글을 작성할 수 없다.") void registerTechCommentByAnonymous() throws Exception { @@ -106,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(); @@ -141,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", @@ -149,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(); @@ -209,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; From 7b4425ee353b8f52c7d7a883f3663598e06f454b Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Wed, 30 Jul 2025 22:39:41 +0900 Subject: [PATCH 41/66] =?UTF-8?q?feat(GuestTechCommentServiceV2):=20?= =?UTF-8?q?=EA=B8=B0=EC=88=A0=EB=B8=94=EB=A1=9C=EA=B7=B8=20=EC=9D=B5?= =?UTF-8?q?=EB=AA=85=20=ED=9A=8C=EC=9B=90=20=EB=8B=B5=EA=B8=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EA=B0=9C=EB=B0=9C=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devdevdev/domain/entity/TechComment.java | 16 ++ .../techComment/GuestTechCommentService.java | 12 +- .../GuestTechCommentServiceV2.java | 52 +++- .../techComment/MemberTechCommentService.java | 40 +-- .../techComment/TechCommentCommonService.java | 33 +++ .../techComment/TechCommentService.java | 5 +- .../TechArticleCommentController.java | 7 +- .../GuestTechCommentServiceTest.java | 6 +- .../MemberTechCommentServiceTest.java | 18 +- .../GuestTechCommentServiceV2Test.java | 259 ++++++++++++++++++ 10 files changed, 379 insertions(+), 69 deletions(-) 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 84121cc8..168e7eb4 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechComment.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechComment.java @@ -152,6 +152,22 @@ public static TechComment createRepliedTechCommentByMember(CommentContents conte .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; 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 9ce43a5d..0cf1b7ce 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,15 +2,14 @@ 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.member.AnonymousMemberService; 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.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; @@ -25,13 +24,10 @@ @Transactional(readOnly = true) public class GuestTechCommentService extends TechCommentCommonService implements TechCommentService { - private final AnonymousMemberService anonymousMemberService; - public GuestTechCommentService(TechCommentRepository techCommentRepository, TechBestCommentsPolicy techBestCommentsPolicy, - AnonymousMemberService anonymousMemberService) { - super(techCommentRepository, techBestCommentsPolicy); - this.anonymousMemberService = anonymousMemberService; + TechArticlePopularScorePolicy techArticlePopularScorePolicy) { + super(techCommentRepository, techBestCommentsPolicy, techArticlePopularScorePolicy); } @Override @@ -44,7 +40,7 @@ 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); } 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 index a243f8df..a5e03b1d 100644 --- 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 @@ -1,21 +1,23 @@ 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.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.TechCommentRecommendResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentsResponse; @@ -31,29 +33,27 @@ public class GuestTechCommentServiceV2 extends TechCommentCommonService implements TechCommentService { private final AnonymousMemberService anonymousMemberService; - private final TechCommentCommonService techCommentCommonService; private final TechArticleCommonService techArticleCommonService; public GuestTechCommentServiceV2(TechCommentRepository techCommentRepository, TechBestCommentsPolicy techBestCommentsPolicy, AnonymousMemberService anonymousMemberService, - TechCommentCommonService techCommentCommonService, + TechArticlePopularScorePolicy techArticlePopularScorePolicy, TechArticleCommonService techArticleCommonService) { - super(techCommentRepository, techBestCommentsPolicy); + super(techCommentRepository, techBestCommentsPolicy, techArticlePopularScorePolicy); this.anonymousMemberService = anonymousMemberService; - this.techCommentCommonService = techCommentCommonService; this.techArticleCommonService = techArticleCommonService; } @Override @Transactional - public TechCommentResponse registerMainTechComment(Long techArticleId, TechCommentDto techCommentDto, + public TechCommentResponse registerMainTechComment(Long techArticleId, TechCommentDto registerTechCommentDto, Authentication authentication) { // 익명 회원인지 검증 AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); - String anonymousMemberId = techCommentDto.getAnonymousMemberId(); - String contents = techCommentDto.getContents(); + String anonymousMemberId = registerTechCommentDto.getAnonymousMemberId(); + String contents = registerTechCommentDto.getContents(); // 회원 조회 또는 생성 AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); @@ -74,11 +74,43 @@ public TechCommentResponse registerMainTechComment(Long techArticleId, TechComme } @Override + @Transactional public TechCommentResponse registerRepliedTechComment(Long techArticleId, Long originParentTechCommentId, Long parentTechCommentId, - RegisterTechCommentRequest registerRepliedTechCommentRequest, + TechCommentDto registerRepliedTechCommentDto, Authentication authentication) { - throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); + // 익명 회원인지 검증 + 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 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 cb5266f7..a738e183 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,9 +1,7 @@ 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_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.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; @@ -22,7 +20,6 @@ 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; @@ -40,7 +37,6 @@ public class MemberTechCommentService extends TechCommentCommonService implement private final TechArticleCommonService techArticleCommonService; private final MemberProvider memberProvider; private final TimeProvider timeProvider; - private final TechArticlePopularScorePolicy techArticlePopularScorePolicy; private final TechCommentRecommendRepository techCommentRecommendRepository; public MemberTechCommentService(TechCommentRepository techCommentRepository, @@ -49,11 +45,10 @@ public MemberTechCommentService(TechCommentRepository techCommentRepository, TechArticlePopularScorePolicy techArticlePopularScorePolicy, TechCommentRecommendRepository techCommentRecommendRepository, TechBestCommentsPolicy techBestCommentsPolicy) { - super(techCommentRepository, techBestCommentsPolicy); + super(techCommentRepository, techBestCommentsPolicy, techArticlePopularScorePolicy); this.techArticleCommonService = techArticleCommonService; this.memberProvider = memberProvider; this.timeProvider = timeProvider; - this.techArticlePopularScorePolicy = techArticlePopularScorePolicy; this.techCommentRecommendRepository = techCommentRecommendRepository; } @@ -95,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); @@ -106,11 +101,11 @@ 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(); + String contents = requestedRepliedTechCommentDto.getContents(); TechComment repliedTechComment = TechComment.createRepliedTechCommentByMember(new CommentContents(contents), findMember, findTechArticle, findOriginParentTechComment, findParentTechComment); techCommentRepository.save(repliedTechComment); @@ -126,33 +121,6 @@ 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: 유소영 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 acfc6caa..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,12 +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; @@ -31,6 +37,7 @@ public class TechCommentCommonService { protected final TechCommentRepository techCommentRepository; protected final TechBestCommentsPolicy techBestCommentsPolicy; + protected final TechArticlePopularScorePolicy techArticlePopularScorePolicy; /** * @Note: 정렬 조건에 따라 커서 방식으로 기술블로그 댓글 목록을 조회한다. @@ -144,4 +151,30 @@ protected List findTechBestComments(int size, Long techArt 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 23980e2c..483b3343 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 @@ -4,7 +4,6 @@ import com.dreamypatisiel.devdevdev.domain.service.techArticle.dto.TechCommentDto; 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; @@ -15,13 +14,13 @@ public interface TechCommentService { TechCommentResponse registerMainTechComment(Long techArticleId, - TechCommentDto techCommentDto, + TechCommentDto registerTechCommentDto, Authentication authentication); TechCommentResponse registerRepliedTechComment(Long techArticleId, Long originParentTechCommentId, Long parentTechCommentId, - RegisterTechCommentRequest registerRepliedTechCommentRequest, + TechCommentDto registerRepliedTechCommentDto, Authentication authentication); TechCommentResponse modifyTechComment(Long techArticleId, Long techCommentId, 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 476fb84d..0215a9a8 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 @@ -61,7 +61,7 @@ public ResponseEntity> registerMainTechCommen return ResponseEntity.ok(BasicResponse.success(response)); } - @Operation(summary = "기술블로그 답글 작성") + @Operation(summary = "기술블로그 답글 작성", description = "기술블로그 답글을 작성할 수 있습니다.") @PostMapping("/articles/{techArticleId}/comments/{originParentTechCommentId}/{parentTechCommentId}") public ResponseEntity> registerRepliedTechComment( @PathVariable Long techArticleId, @@ -70,10 +70,13 @@ 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)); } 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 da5c0118..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 @@ -138,8 +138,7 @@ 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(); @@ -149,10 +148,11 @@ void registerRepliedTechComment() { 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); } 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 718a0927..b0827220 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 @@ -619,8 +619,7 @@ 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(); @@ -630,10 +629,11 @@ void registerRepliedTechComment() { 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 @@ -695,10 +695,11 @@ void registerRepliedTechCommentToRepliedTechComment() { 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(); @@ -754,11 +755,12 @@ void registerRepliedTechCommentNotFoundTechCommentException() { 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); } @@ -798,11 +800,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); } @@ -824,11 +827,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); } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java index d329be10..893fdbdf 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java @@ -1,5 +1,7 @@ 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; @@ -1498,4 +1500,261 @@ void registerTechCommentIllegalStateException() { .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); + } } \ No newline at end of file From 5f0f87d515f97a22d271e2fa04250524bc15df7c Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Wed, 30 Jul 2025 22:45:14 +0900 Subject: [PATCH 42/66] =?UTF-8?q?docs(tech-article-comment):=20=EA=B8=B0?= =?UTF-8?q?=EC=88=A0=EB=B8=94=EB=A1=9C=EA=B7=B8=20=EB=8B=B5=EA=B8=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20API=20=EB=AC=B8=EC=84=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tech-article-reply-register.adoc | 4 +++- .../TechArticleCommentControllerTest.java | 9 +++------ .../TechArticleCommentControllerDocsTest.java | 20 +++++++------------ 3 files changed, 13 insertions(+), 20 deletions(-) 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..7fa986cc 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 @@ -2,7 +2,8 @@ == 기술블로그 답글 작성 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/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleCommentControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleCommentControllerTest.java index ff59b145..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 @@ -514,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(); @@ -569,8 +568,7 @@ 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(); @@ -619,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(); 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 674c2e1c..3d192657 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java @@ -353,8 +353,7 @@ 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(); @@ -418,8 +417,7 @@ 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(); @@ -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,8 +522,7 @@ 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(); @@ -583,8 +579,7 @@ 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(); @@ -620,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", @@ -628,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(); From 48c8ada4efdb55bde1e2972e6a44322b8867635f Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Fri, 1 Aug 2025 00:45:52 +0900 Subject: [PATCH 43/66] =?UTF-8?q?fix(GuestTechCommentServiceV2):=20?= =?UTF-8?q?=EC=9D=B5=EB=AA=85=ED=9A=8C=EC=9B=90=20=EA=B8=B0=EC=88=A0?= =?UTF-8?q?=EB=B8=94=EB=A1=9C=EA=B7=B8=20=EB=8C=93=EA=B8=80/=EB=8B=B5?= =?UTF-8?q?=EA=B8=80=20=EC=88=98=EC=A0=95=20=EA=B0=9C=EB=B0=9C=20=EB=B0=8F?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devdevdev/domain/entity/TechComment.java | 5 + .../techArticle/TechCommentRepository.java | 4 + .../techArticle/dto/TechCommentDto.java | 9 + .../techComment/GuestTechCommentService.java | 4 +- .../GuestTechCommentServiceV2.java | 32 +++- .../techComment/MemberTechCommentService.java | 7 +- .../techComment/TechCommentService.java | 10 +- .../TechArticleCommentController.java | 5 +- .../MemberTechCommentServiceTest.java | 33 ++-- .../GuestTechCommentServiceV2Test.java | 164 +++++++++++++++++- 10 files changed, 232 insertions(+), 41 deletions(-) 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 168e7eb4..ef878e4a 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechComment.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechComment.java @@ -173,6 +173,11 @@ public void changeDeletedAt(LocalDateTime deletedAt, Member deletedBy) { 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; 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 620bd09c..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,6 +17,9 @@ 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", "createdAnonymousBy", "deletedAnonymousBy", "techArticle"}) 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 index 7b9613e0..b8559632 100644 --- 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 @@ -1,5 +1,6 @@ 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; @@ -15,4 +16,12 @@ public static TechCommentDto createRegisterCommentDto(RegisterTechCommentRequest 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/techComment/GuestTechCommentService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentService.java index 0cf1b7ce..55be5e75 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 @@ -9,7 +9,6 @@ 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.request.techArticle.ModifyTechCommentRequest; 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; @@ -46,8 +45,7 @@ public TechCommentResponse registerRepliedTechComment(Long techArticleId, Long o } @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); } 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 index a5e03b1d..ac633fb6 100644 --- 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 @@ -15,9 +15,9 @@ 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.request.techArticle.ModifyTechCommentRequest; 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; @@ -32,14 +32,18 @@ @Transactional(readOnly = true) public class GuestTechCommentServiceV2 extends TechCommentCommonService implements TechCommentService { + private final TimeProvider timeProvider; + private final AnonymousMemberService anonymousMemberService; private final TechArticleCommonService techArticleCommonService; - public GuestTechCommentServiceV2(TechCommentRepository techCommentRepository, TechBestCommentsPolicy techBestCommentsPolicy, + 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; } @@ -114,10 +118,28 @@ public TechCommentResponse registerRepliedTechComment(Long techArticleId, Long o } @Override - public TechCommentResponse modifyTechComment(Long techArticleId, Long techCommentId, - ModifyTechCommentRequest modifyTechCommentRequest, + @Transactional + public TechCommentResponse modifyTechComment(Long techArticleId, Long techCommentId, TechCommentDto modifyTechCommentDto, Authentication authentication) { - throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); + // 익명 회원인지 검증 + 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 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 a738e183..f36318d6 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 @@ -19,7 +19,6 @@ 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.response.techArticle.TechCommentRecommendResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentsResponse; @@ -126,9 +125,9 @@ public TechCommentResponse registerRepliedTechComment(Long techArticleId, * @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); @@ -139,7 +138,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()); // 데이터 가공 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 483b3343..f56ec5b8 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 @@ -3,7 +3,6 @@ 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.request.techArticle.ModifyTechCommentRequest; 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; @@ -23,19 +22,16 @@ TechCommentResponse registerRepliedTechComment(Long techArticleId, 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); SliceCommentCustom getTechComments(Long techArticleId, Long techCommentId, TechCommentSort techCommentSort, Pageable pageable, - String anonymousMemberId, - 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, String anonymousMemberId, Authentication authentication); 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 0215a9a8..eecfdd4a 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 @@ -89,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)); } 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 b0827220..ebff625a 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 @@ -235,8 +235,7 @@ void modifyTechComment() { companyRepository.save(company); TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -245,13 +244,14 @@ void modifyTechComment() { 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 @@ -285,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); @@ -319,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); @@ -348,8 +350,7 @@ 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(); @@ -362,10 +363,11 @@ void modifyTechCommentAlreadyDeletedException() { em.flush(); ModifyTechCommentRequest modifyTechCommentRequest = new ModifyTechCommentRequest("댓글 수정"); + TechCommentDto modifyCommentDto = TechCommentDto.createModifyCommentDto(modifyTechCommentRequest, null); // when // then assertThatThrownBy( - () -> memberTechCommentService.modifyTechComment(techArticleId, techCommentId, modifyTechCommentRequest, + () -> memberTechCommentService.modifyTechComment(techArticleId, techCommentId, modifyCommentDto, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE); @@ -390,8 +392,7 @@ 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(); @@ -437,8 +438,7 @@ 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(); @@ -477,8 +477,7 @@ 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(); @@ -513,8 +512,7 @@ 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(); @@ -564,8 +562,7 @@ 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(); diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java index 893fdbdf..b39f8c6a 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java @@ -41,6 +41,7 @@ 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; @@ -54,6 +55,7 @@ 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; @@ -80,7 +82,7 @@ class GuestTechCommentServiceV2Test { TechCommentRecommendRepository techCommentRecommendRepository; @Autowired AnonymousMemberRepository anonymousMemberRepository; - @Autowired + @MockBean TimeProvider timeProvider; @Autowired EntityManager em; @@ -1703,8 +1705,7 @@ void registerRepliedTechCommentDeletedTechCommentException() { // 기술블로그 생성 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(); @@ -1757,4 +1758,161 @@ void registerRepliedTechCommentIllegalStateException() { .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); + } } \ No newline at end of file From b7b089e9576cde85fb4c737ae43a901952f01402 Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sat, 2 Aug 2025 10:47:37 +0900 Subject: [PATCH 44/66] =?UTF-8?q?fix(PR):=20=EB=A6=AC=EB=B7=B0=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tech-article-comment/tech-article-comment-register.adoc | 4 +++- .../api/tech-article-comment/tech-article-reply-register.adoc | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) 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 a68bba9f..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,7 +1,9 @@ [[Tech-Article-Comments-Register]] == 기술블로그 댓글 작성 API(POST: /devdevdev/api/v1/articles/{techArticleId}/comments) -* 회원은 기술블로그에 댓글을 작성할 수 있다. +* 기술블로그에 댓글을 작성할 수 있다. +** 회원인 경우 토큰을 `Authorization` 헤더에 포함시켜야 한다. +** 익명 회원인 경우 `Anonymous-Member-Id` 헤더에 익명 회원 아이디를 포함시켜야 한다. === 정상 요청/응답 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 7fa986cc..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,7 +1,7 @@ [[Tech-Article-Reply-Register]] == 기술블로그 답글 작성 API(POST: /devdevdev/api/v1/articles/{techArticleId}/comments/{originParentTechCommentId}/{parentTechCommentId} -* 회원은 기술블로그에 댓글에 답글을 작성할 수 있다. +* 기술블로그에 댓글에 답글을 작성할 수 있다. ** 회원인 경우 토큰을 `Authorization` 헤더에 포함시켜야 한다. ** 익명 회원인 경우 `Anonymous-Member-Id` 헤더에 익명 회원 아이디를 포함시켜야 한다. * 삭제된 댓글에는 답글을 작성할 수 없다. From dc19c5966cc70961a2403cfbc4434cc0b08d0a1f Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sun, 3 Aug 2025 14:37:42 +0900 Subject: [PATCH 45/66] =?UTF-8?q?feat(GuestTechCommentServiceV2):=20?= =?UTF-8?q?=EC=9D=B5=EB=AA=85=ED=9A=8C=EC=9B=90=20=EA=B8=B0=EC=88=A0?= =?UTF-8?q?=EB=B8=94=EB=A1=9C=EA=B7=B8=20=EB=8C=93=EA=B8=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tech-article-comment-modify.adoc | 6 +- .../techComment/GuestTechCommentService.java | 3 +- .../GuestTechCommentServiceV2.java | 21 ++- .../techComment/MemberTechCommentService.java | 6 +- .../techComment/TechCommentService.java | 4 +- .../TechArticleCommentController.java | 5 +- .../MemberTechCommentServiceTest.java | 18 ++- .../GuestTechCommentServiceV2Test.java | 130 ++++++++++++++++++ 8 files changed, 172 insertions(+), 21 deletions(-) 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/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 55be5e75..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 @@ -13,6 +13,7 @@ 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; @@ -51,7 +52,7 @@ public TechCommentResponse modifyTechComment(Long techArticleId, Long techCommen } @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); } 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 index ac633fb6..c507b674 100644 --- 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 @@ -143,9 +143,26 @@ public TechCommentResponse modifyTechComment(Long techArticleId, Long techCommen } @Override - public TechCommentResponse deleteTechComment(Long techArticleId, Long techCommentId, + @Transactional + public TechCommentResponse deleteTechComment(Long techArticleId, Long techCommentId, String anonymousMemberId, Authentication authentication) { - throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); + + // 익명 회원인지 검증 + 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()); } /** 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 f36318d6..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 @@ -24,6 +24,7 @@ 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; @@ -150,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); 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 f56ec5b8..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 @@ -7,6 +7,7 @@ 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; @@ -25,7 +26,8 @@ TechCommentResponse registerRepliedTechComment(Long techArticleId, 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, 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 eecfdd4a..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 @@ -81,7 +81,7 @@ public ResponseEntity> registerRepliedTechCom return ResponseEntity.ok(BasicResponse.success(response)); } - @Operation(summary = "기술블로그 댓글/답글 수정") + @Operation(summary = "기술블로그 댓글/답글 수정", description = "기술블로그 댓글/답글을 수정할 수 있습니다.") @PatchMapping("/articles/{techArticleId}/comments/{techCommentId}") public ResponseEntity> modifyTechComment( @PathVariable Long techArticleId, @@ -107,10 +107,11 @@ 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)); } 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 ebff625a..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 @@ -366,9 +366,8 @@ void modifyTechCommentAlreadyDeletedException() { TechCommentDto modifyCommentDto = TechCommentDto.createModifyCommentDto(modifyTechCommentRequest, null); // when // then - assertThatThrownBy( - () -> memberTechCommentService.modifyTechComment(techArticleId, techCommentId, modifyCommentDto, - authentication)) + assertThatThrownBy(() -> memberTechCommentService.modifyTechComment(techArticleId, techCommentId, modifyCommentDto, + authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE); } @@ -406,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(); @@ -452,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); } @@ -483,7 +482,7 @@ void deleteTechCommentNotFoundException() { // when // then assertThatThrownBy( - () -> memberTechCommentService.deleteTechComment(techArticleId, 0L, authentication)) + () -> memberTechCommentService.deleteTechComment(techArticleId, 0L, null, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE); } @@ -526,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(); @@ -572,7 +571,7 @@ void deleteTechCommentNotByMemberException() { // when // then assertThatThrownBy( - () -> memberTechCommentService.deleteTechComment(techArticleId, techCommentId, authentication)) + () -> memberTechCommentService.deleteTechComment(techArticleId, techCommentId, null, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE); } @@ -591,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); } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java index b39f8c6a..c345159d 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java @@ -1915,4 +1915,134 @@ void modifyTechCommentAlreadyDeletedException() { .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 From d0b6c07a060189db44e6573b469ecbf26e02e299 Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sun, 10 Aug 2025 17:38:28 +0900 Subject: [PATCH 46/66] fix(gradle): add dependency mysql --- build.gradle | 2 ++ .../tech-article-comment/tech-article-comment-delete.adoc | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 2b27277c..40baffc0 100644 --- a/build.gradle +++ b/build.gradle @@ -47,6 +47,7 @@ dependencies { implementation 'commons-validator:commons-validator:1.8.0' // https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-ui implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' + testImplementation 'org.testcontainers:mysql' annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' testImplementation 'org.springframework.boot:spring-boot-testcontainers' @@ -96,6 +97,7 @@ dependencies { runtimeOnly 'com.h2database:h2' runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' annotationProcessor 'org.projectlombok:lombok' + runtimeOnly 'com.mysql:mysql-connector-j' testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' 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 From 35563a259c2a7baf134f18970639cddb1c35825c Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Wed, 13 Aug 2025 22:53:20 +0900 Subject: [PATCH 47/66] feat(CommentResponseUtil): getCommentByTechCommentStatus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 익명회원 기술블로그 댓글 작성 고려하여 수정 --- .../devdevdev/domain/entity/TechComment.java | 12 ++++++ .../controller/member/TokenController.java | 2 + .../web/dto/util/CommentResponseUtil.java | 37 ++++++++++++++++--- 3 files changed, 46 insertions(+), 5 deletions(-) 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 ef878e4a..33637cca 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechComment.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechComment.java @@ -222,4 +222,16 @@ public boolean isCreatedAnonymousMember() { 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/web/controller/member/TokenController.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/TokenController.java index 780f2946..029601dd 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/TokenController.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/TokenController.java @@ -1,5 +1,7 @@ package com.dreamypatisiel.devdevdev.web.controller.member; +//import com.dreamypatisiel.devdevdev.LocalInitData; + import com.dreamypatisiel.devdevdev.LocalInitData; import com.dreamypatisiel.devdevdev.global.security.jwt.model.JwtCookieConstant; import com.dreamypatisiel.devdevdev.global.security.jwt.model.Token; 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 16a79668..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 @@ -56,15 +56,42 @@ public static String getCommentByPickCommentStatus(PickComment pickComment) { } public static String getCommentByTechCommentStatus(TechComment techComment) { - if (techComment.isDeleted()) { - // 댓글 작성자에 의해 삭제된 경우 - if (techComment.getDeletedBy().isEqualsId(techComment.getCreatedBy().getId())) { + // 기술블로그 댓글이 삭제되지 않은 경우 + if (!techComment.isDeleted()) { + return techComment.getContents().getCommentContents(); + } + + // 익명회원이 작성한 기술블로그 댓글인 경우 + if (techComment.isCreatedAnonymousMember()) { + // 자기자신이 삭제한 경우 + if (techComment.isDeletedByAnonymousMember()) { return DELETE_COMMENT_MESSAGE; } - return DELETE_INVALID_COMMUNITY_POLICY_COMMENT_MESSAGE; + + // 어드민이 삭제한 경우 + 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) { From 09a372fbcb14bf944046d6f32f44bff8c2b91cb2 Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Wed, 13 Aug 2025 22:55:12 +0900 Subject: [PATCH 48/66] refactor(TokenController): remove redundant import comments --- .../devdevdev/web/controller/member/TokenController.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/TokenController.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/TokenController.java index 029601dd..780f2946 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/TokenController.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/TokenController.java @@ -1,7 +1,5 @@ package com.dreamypatisiel.devdevdev.web.controller.member; -//import com.dreamypatisiel.devdevdev.LocalInitData; - import com.dreamypatisiel.devdevdev.LocalInitData; import com.dreamypatisiel.devdevdev.global.security.jwt.model.JwtCookieConstant; import com.dreamypatisiel.devdevdev.global.security.jwt.model.Token; From 101f66a1f9a1a154a879e8ee1cd697359e7f31d7 Mon Sep 17 00:00:00 2001 From: soyoung Date: Thu, 14 Aug 2025 01:32:12 +0900 Subject: [PATCH 49/66] =?UTF-8?q?fix(keyword):=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=EC=96=B4=20=EC=9E=90=EB=8F=99=EC=99=84=EC=84=B1=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devdevdev/domain/entity/TechKeyword.java | 36 ++++ .../techArticle/TechKeywordRepository.java | 8 + .../custom/TechKeywordRepositoryCustom.java | 10 ++ .../custom/TechKeywordRepositoryImpl.java | 56 ++++++ .../keyword/TechKeywordService.java | 61 +++++++ .../CustomMySQLFunctionContributor.java | 18 ++ .../devdevdev/global/utils/HangulUtils.java | 162 ++++++++++++++++++ .../techArticle/KeywordController.java | 17 +- ...g.hibernate.boot.model.FunctionContributor | 1 + 9 files changed, 360 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechKeyword.java create mode 100644 src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechKeywordRepository.java create mode 100644 src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryCustom.java create mode 100644 src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryImpl.java create mode 100644 src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/keyword/TechKeywordService.java create mode 100644 src/main/java/com/dreamypatisiel/devdevdev/global/config/CustomMySQLFunctionContributor.java create mode 100644 src/main/java/com/dreamypatisiel/devdevdev/global/utils/HangulUtils.java create mode 100644 src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor 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..6bf410bc --- /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__ft__chosung_key", columnList = "chosung_key"), + @Index(name = "idx__ft__jamo_key", 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/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/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..48c158b1 --- /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.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 + var jamoScore = Expressions.numberTemplate(Double.class, + "function('" + MATCH_AGAINST_FUNCTION + "', {0}, {1})", + techKeyword.jamoKey, inputJamo + ); + + var 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/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/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/utils/HangulUtils.java b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/HangulUtils.java new file mode 100644 index 00000000..57595368 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/HangulUtils.java @@ -0,0 +1,162 @@ +package com.dreamypatisiel.devdevdev.global.utils; + +/** + * 한글 처리를 위한 유틸리티 클래스 + */ +public 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/web/controller/techArticle/KeywordController.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/KeywordController.java index bac7cebb..caf1921d 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,11 +1,9 @@ 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.http.ResponseEntity; @@ -14,6 +12,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + @Tag(name = "검색어 자동완성 API", description = "검색어 자동완성, 검색어 추가 API") @Slf4j @RestController @@ -21,15 +21,14 @@ @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/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 From 4ef52bf6b3335c1a60ec38787154f1f97bd93106 Mon Sep 17 00:00:00 2001 From: soyoung Date: Sun, 17 Aug 2025 22:40:53 +0900 Subject: [PATCH 50/66] =?UTF-8?q?test(keyword):=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=EC=96=B4=20=EC=9E=90=EB=8F=99=EC=99=84=EC=84=B1=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devdevdev/global/utils/HangulUtils.java | 2 +- .../techArticle/TechKeywordServiceTest.java | 206 ++++++++++++++++++ .../service/ElasticKeywordServiceTest.java | 2 + .../global/utils/HangulUtilsTest.java | 89 ++++++++ .../techArticle/KeywordControllerTest.java | 48 ++-- .../web/docs/KeywordControllerDocsTest.java | 55 ++--- 6 files changed, 332 insertions(+), 70 deletions(-) create mode 100644 src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechKeywordServiceTest.java create mode 100644 src/test/java/com/dreamypatisiel/devdevdev/global/utils/HangulUtilsTest.java diff --git a/src/main/java/com/dreamypatisiel/devdevdev/global/utils/HangulUtils.java b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/HangulUtils.java index 57595368..c431548d 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/global/utils/HangulUtils.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/HangulUtils.java @@ -3,7 +3,7 @@ /** * 한글 처리를 위한 유틸리티 클래스 */ -public class HangulUtils { +public abstract class HangulUtils { // 한글 유니코드 범위 private static final int HANGUL_START = 0xAC00; // '가' 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..c97cff4e --- /dev/null +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechKeywordServiceTest.java @@ -0,0 +1,206 @@ +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 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.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +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 +@Testcontainers +class TechKeywordServiceTest { + + @Container + @ServiceConnection + static MySQLContainer mysql = new MySQLContainer<>("mysql:8.0") + .withDatabaseName("devdevdev_test") + .withUsername("test") + .withPassword("test") + .withCommand("--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_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"); + } + + @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/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/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/web/controller/techArticle/KeywordControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/KeywordControllerTest.java index f78981e5..fff6df22 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,36 @@ 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/docs/KeywordControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/KeywordControllerDocsTest.java index 5b5b088f..19e42606 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,19 @@ 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 +63,4 @@ void autocompleteKeyword() throws Exception { ) )); } - } \ No newline at end of file From fd5b8783e3177f23f114268933224e27c2a4d228 Mon Sep 17 00:00:00 2001 From: soyoung Date: Sun, 17 Aug 2025 22:46:07 +0900 Subject: [PATCH 51/66] =?UTF-8?q?test(keyword):=20MySQL=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC(=EC=9E=AC=EC=82=AC=EC=9A=A9=20=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../techArticle/TechKeywordServiceTest.java | 28 +------------ .../devdevdev/test/MySQLTestContainer.java | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+), 26 deletions(-) create mode 100644 src/test/java/com/dreamypatisiel/devdevdev/test/MySQLTestContainer.java diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechKeywordServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechKeywordServiceTest.java index c97cff4e..45f05d0d 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechKeywordServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechKeywordServiceTest.java @@ -4,6 +4,7 @@ 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; @@ -11,14 +12,8 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.context.transaction.BeforeTransaction; import org.springframework.transaction.annotation.Transactional; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; import javax.sql.DataSource; import java.sql.Connection; @@ -31,26 +26,7 @@ @SpringBootTest @Transactional -@Testcontainers -class TechKeywordServiceTest { - - @Container - @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>("mysql:8.0") - .withDatabaseName("devdevdev_test") - .withUsername("test") - .withPassword("test") - .withCommand("--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_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"); - } +class TechKeywordServiceTest extends MySQLTestContainer { @Autowired EntityManager em; 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..d6463186 --- /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_unicode_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"); + } +} From bb95ce7fcac4e1333c542f4155d9a3239873fb42 Mon Sep 17 00:00:00 2001 From: soyoung Date: Sun, 17 Aug 2025 22:55:35 +0900 Subject: [PATCH 52/66] =?UTF-8?q?fix(keyword):=20local=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=EC=96=B4=20=EC=9E=90=EB=8F=99=EC=99=84?= =?UTF-8?q?=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=ED=98=B8=EC=B6=9C=20=EB=B6=88?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devdevdev/web/controller/techArticle/KeywordController.java | 2 ++ 1 file changed, 2 insertions(+) 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 caf1921d..15b5846f 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 @@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; 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; @@ -16,6 +17,7 @@ @Tag(name = "검색어 자동완성 API", description = "검색어 자동완성, 검색어 추가 API") @Slf4j +@Profile({"dev", "prod"}) // local 에서는 검색어 자동완성 불가 @RestController @RequestMapping("/devdevdev/api/v1/keywords") @RequiredArgsConstructor From c4aa514df5d5a21d61f36a859e02cac072b52883 Mon Sep 17 00:00:00 2001 From: soyoung Date: Sun, 17 Aug 2025 23:11:23 +0900 Subject: [PATCH 53/66] =?UTF-8?q?fix(keyword):=20test=20=EC=8B=A4=ED=8C=A8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/controller/techArticle/KeywordController.java | 2 +- .../web/controller/techArticle/KeywordControllerTest.java | 3 +-- .../devdevdev/web/docs/KeywordControllerDocsTest.java | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) 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 15b5846f..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 @@ -17,7 +17,7 @@ @Tag(name = "검색어 자동완성 API", description = "검색어 자동완성, 검색어 추가 API") @Slf4j -@Profile({"dev", "prod"}) // local 에서는 검색어 자동완성 불가 +@Profile({"test", "dev", "prod"}) // local 에서는 검색어 자동완성 불가 @RestController @RequestMapping("/devdevdev/api/v1/keywords") @RequiredArgsConstructor 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 fff6df22..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 @@ -29,8 +29,7 @@ void autocompleteKeyword() throws Exception { // given String prefix = "자"; List result = List.of("자바", "자바 스크립트", "자바가 최고야"); - given(techKeywordService.autocompleteKeyword(prefix)) - .willReturn(result); + 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/docs/KeywordControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/KeywordControllerDocsTest.java index 19e42606..560dd455 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/KeywordControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/KeywordControllerDocsTest.java @@ -36,8 +36,7 @@ void autocompleteKeyword() throws Exception { // given String prefix = "자"; List result = List.of("자바", "자바 스크립트", "자바가 최고야"); - given(techKeywordService.autocompleteKeyword(prefix)) - .willReturn(result); + given(techKeywordService.autocompleteKeyword(prefix)).willReturn(result); // when // then ResultActions actions = mockMvc.perform(get(DEFAULT_PATH_V1 + "/keywords/auto-complete") From 8b03c34ecde7ccbc53febb33f68f7c8dcd725bf2 Mon Sep 17 00:00:00 2001 From: soyoung Date: Tue, 19 Aug 2025 17:59:34 +0900 Subject: [PATCH 54/66] =?UTF-8?q?fix(keyword):=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dreamypatisiel/devdevdev/domain/entity/TechKeyword.java | 4 ++-- .../techArticle/custom/TechKeywordRepositoryImpl.java | 6 +++--- .../dreamypatisiel/devdevdev/test/MySQLTestContainer.java | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechKeyword.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechKeyword.java index 6bf410bc..81b2af22 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechKeyword.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechKeyword.java @@ -10,8 +10,8 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(indexes = { - @Index(name = "idx__ft__chosung_key", columnList = "chosung_key"), - @Index(name = "idx__ft__jamo_key", columnList = "jamo_key") + @Index(name = "idx_tech_keyword_01", columnList = "chosung_key"), + @Index(name = "idx_tech_keyword_02", columnList = "jamo_key") }) public class TechKeyword extends BasicTime { @Id diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryImpl.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryImpl.java index 48c158b1..46d782a5 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryImpl.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryImpl.java @@ -3,6 +3,7 @@ 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; @@ -30,12 +31,11 @@ public List searchKeyword(String inputJamo, String inputChosung, Pa ); // 스코어 계산을 위한 expression - var jamoScore = Expressions.numberTemplate(Double.class, + NumberTemplate jamoScore = Expressions.numberTemplate(Double.class, "function('" + MATCH_AGAINST_FUNCTION + "', {0}, {1})", techKeyword.jamoKey, inputJamo ); - - var chosungScore = Expressions.numberTemplate(Double.class, + NumberTemplate chosungScore = Expressions.numberTemplate(Double.class, "function('" + MATCH_AGAINST_FUNCTION + "', {0}, {1})", techKeyword.chosungKey, inputChosung ); diff --git a/src/test/java/com/dreamypatisiel/devdevdev/test/MySQLTestContainer.java b/src/test/java/com/dreamypatisiel/devdevdev/test/MySQLTestContainer.java index d6463186..6884b394 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/test/MySQLTestContainer.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/test/MySQLTestContainer.java @@ -23,7 +23,7 @@ public abstract class MySQLTestContainer { .withPassword("test") .withCommand( "--character-set-server=utf8mb4", - "--collation-server=utf8mb4_unicode_ci", + "--collation-server=utf8mb4_general_ci", "--ngram_token_size=1" ); From 7c28d414aacbe23b68b45f0d1e1d3a2826b94cd0 Mon Sep 17 00:00:00 2001 From: soyoung Date: Tue, 19 Aug 2025 21:24:38 +0900 Subject: [PATCH 55/66] =?UTF-8?q?fix:=20=EA=B8=B0=EC=88=A0=EB=B8=94?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devdevdev/domain/entity/TechArticle.java | 89 ++-- .../techArticle/TechArticleSort.java | 76 ++-- .../custom/TechArticleRepositoryCustom.java | 9 +- .../custom/TechArticleRepositoryImpl.java | 106 +++-- .../domain/service/member/MemberService.java | 36 +- .../notification/NotificationService.java | 47 +-- .../techArticle/GuestTechArticleService.java | 73 +--- .../techArticle/MemberTechArticleService.java | 87 ++-- .../techArticle/TechArticleCommonService.java | 59 --- .../techArticle/TechArticleService.java | 2 +- .../ElasticsearchIndexConfigService.java | 24 +- .../OpenSearchRestClientConfiguration.java | 132 +++--- .../constant/ElasticsearchConstant.java | 22 +- .../data/response/ElasticResponse.java | 8 +- .../domain/document/ElasticKeyword.java | 60 +-- .../domain/document/ElasticTechArticle.java | 152 +++---- .../repository/ElasticKeywordRepository.java | 18 +- .../ElasticTechArticleRepository.java | 22 +- .../domain/service/ElasticKeywordService.java | 112 ++--- .../domain/service/ElasticService.java | 26 +- .../service/ElasticTechArticleService.java | 382 +++++++++--------- .../controller/member/MypageController.java | 4 +- .../techArticle/TechArticleController.java | 20 +- .../TechArticleDetailResponse.java | 77 ++-- .../techArticle/TechArticleMainResponse.java | 156 ++++--- src/main/resources/application.yml | 1 - 26 files changed, 811 insertions(+), 989 deletions(-) diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechArticle.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechArticle.java index a4d0a31b..42dbb4d3 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechArticle.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechArticle.java @@ -4,7 +4,6 @@ import com.dreamypatisiel.devdevdev.domain.entity.embedded.Title; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; import com.dreamypatisiel.devdevdev.domain.policy.TechArticlePopularScorePolicy; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; import jakarta.persistence.AttributeOverride; import jakarta.persistence.Column; import jakarta.persistence.Embedded; @@ -18,6 +17,8 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; + +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import lombok.AccessLevel; @@ -29,7 +30,9 @@ @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(indexes = { - @Index(name = "idx_tech_article_01", columnList = "elasticId") + @Index(name = "idx_tech_article_01", columnList = "title"), + @Index(name = "idx_tech_article_02", columnList = "contents"), + @Index(name = "idx_tech_article_03", columnList = "title, contents") }) public class TechArticle extends BasicTime { @Id @@ -38,6 +41,30 @@ public class TechArticle extends BasicTime { private Title title; + @Column(columnDefinition = "LONGTEXT") + private String contents; + + @Column(length = 255) + private String author; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "company_id", nullable = false) + private Company company; + + private LocalDate regDate; + + @Embedded + @AttributeOverride(name = "url", + column = @Column(name = "tech_article_url") + ) + private Url techArticleUrl; + + @Embedded + @AttributeOverride(name = "url", + column = @Column(name = "thumbnail_url") + ) + private Url thumbnailUrl; + @Embedded @AttributeOverride(name = "count", column = @Column(name = "view_total_count") @@ -62,19 +89,6 @@ public class TechArticle extends BasicTime { ) private Count popularScore; - @Embedded - @AttributeOverride(name = "url", - column = @Column(name = "tech_article_url") - ) - private Url techArticleUrl; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "company_id", nullable = false) - private Company company; - - @Column - private String elasticId; - @OneToMany(mappedBy = "techArticle") private List bookmarks = new ArrayList<>(); @@ -86,48 +100,20 @@ public TechArticle(Long id) { } @Builder - private TechArticle(Title title, Count viewTotalCount, Count recommendTotalCount, Count commentTotalCount, - Count popularScore, - Url techArticleUrl, Company company, String elasticId) { + private TechArticle(Title title, String contents, String author, Company company, + LocalDate regDate, Url techArticleUrl, Url thumbnailUrl, + Count viewTotalCount, Count recommendTotalCount, Count commentTotalCount, Count popularScore) { this.title = title; + this.contents = contents; + this.author = author; + this.company = company; + this.regDate = regDate; this.techArticleUrl = techArticleUrl; + this.thumbnailUrl = thumbnailUrl; this.viewTotalCount = viewTotalCount; this.recommendTotalCount = recommendTotalCount; this.commentTotalCount = commentTotalCount; this.popularScore = popularScore; - this.company = company; - this.elasticId = elasticId; - } - - public static TechArticle createTechArticle(ElasticTechArticle elasticTechArticle, Company company) { - TechArticle techArticle = TechArticle.builder() - .title(new Title(elasticTechArticle.getTitle())) - .techArticleUrl(new Url(elasticTechArticle.getTechArticleUrl())) - .viewTotalCount(new Count(elasticTechArticle.getViewTotalCount())) - .recommendTotalCount(new Count(elasticTechArticle.getRecommendTotalCount())) - .commentTotalCount(new Count(elasticTechArticle.getCommentTotalCount())) - .popularScore(new Count(elasticTechArticle.getPopularScore())) - .elasticId(elasticTechArticle.getId()) - .build(); - - techArticle.changeCompany(company); - - return techArticle; - } - - public static TechArticle createTechArticle(Title title, Url techArticleUrl, Count viewTotalCount, - Count recommendTotalCount, Count commentTotalCount, Count popularScore, - String elasticId, Company company) { - return TechArticle.builder() - .title(title) - .techArticleUrl(techArticleUrl) - .viewTotalCount(viewTotalCount) - .recommendTotalCount(recommendTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(popularScore) - .elasticId(elasticId) - .company(company) - .build(); } public void changeBookmarks(List bookmarks) { @@ -162,7 +148,6 @@ public void decrementCommentCount() { this.commentTotalCount = Count.minusOne(this.commentTotalCount); } - public void incrementRecommendTotalCount() { this.recommendTotalCount = Count.plusOne(this.recommendTotalCount); } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleSort.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleSort.java index 28fee850..2ef88f84 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleSort.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleSort.java @@ -1,17 +1,13 @@ package com.dreamypatisiel.devdevdev.domain.repository.techArticle; -import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant.LATEST_SORT_FIELD_NAME; -import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant.MOST_COMMENTED_SORT_FIELD_NAME; -import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant.MOST_VIEWED_SORT_FIELD_NAME; -import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant.POPULAR_SORT_FIELD_NAME; +import static com.dreamypatisiel.devdevdev.domain.entity.QTechArticle.techArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; +import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.opensearch.search.sort.FieldSortBuilder; -import org.opensearch.search.sort.SortBuilder; -import org.opensearch.search.sort.SortBuilders; -import org.opensearch.search.sort.SortOrder; @Getter @RequiredArgsConstructor @@ -19,68 +15,70 @@ public enum TechArticleSort { LATEST("최신순") { @Override - public SortBuilder getSortCondition() { - return getFieldSortBuilder(LATEST_SORT_FIELD_NAME); + public OrderSpecifier getOrderSpecifierByTechArticleSort() { + return new OrderSpecifier<>(Order.DESC, techArticle.regDate); } @Override - public Object getSearchAfterCondition(ElasticTechArticle elasticTechArticle) { - return elasticTechArticle.getRegDate().toString(); + public BooleanExpression getCursorCondition(TechArticle findTechArticle) { + return techArticle.regDate.lt(findTechArticle.getRegDate()) + .or(techArticle.regDate.eq(findTechArticle.getRegDate()) + .and(techArticle.id.lt(findTechArticle.getId()))); } }, POPULAR("인기순") { - @Override - public SortBuilder getSortCondition() { - return getFieldSortBuilder(POPULAR_SORT_FIELD_NAME); + public OrderSpecifier getOrderSpecifierByTechArticleSort() { + return new OrderSpecifier<>(Order.DESC, techArticle.popularScore.count); } @Override - public Object getSearchAfterCondition(ElasticTechArticle elasticTechArticle) { - return elasticTechArticle.getPopularScore(); + public BooleanExpression getCursorCondition(TechArticle findTechArticle) { + return techArticle.popularScore.count.lt(findTechArticle.getPopularScore().getCount()) + .or(techArticle.popularScore.count.eq(findTechArticle.getPopularScore().getCount()) + .and(techArticle.regDate.eq(findTechArticle.getRegDate()))); } }, MOST_VIEWED("조회순") { - @Override - public SortBuilder getSortCondition() { - return getFieldSortBuilder(MOST_VIEWED_SORT_FIELD_NAME); + public OrderSpecifier getOrderSpecifierByTechArticleSort() { + return new OrderSpecifier<>(Order.DESC, techArticle.viewTotalCount.count); } @Override - public Object getSearchAfterCondition(ElasticTechArticle elasticTechArticle) { - return elasticTechArticle.getViewTotalCount(); + public BooleanExpression getCursorCondition(TechArticle findTechArticle) { + return techArticle.viewTotalCount.count.lt(findTechArticle.getViewTotalCount().getCount()) + .or(techArticle.viewTotalCount.count.eq(findTechArticle.getViewTotalCount().getCount()) + .and(techArticle.regDate.eq(findTechArticle.getRegDate()))); } }, MOST_COMMENTED("댓글순") { - @Override - public SortBuilder getSortCondition() { - return getFieldSortBuilder(MOST_COMMENTED_SORT_FIELD_NAME); + public OrderSpecifier getOrderSpecifierByTechArticleSort() { + return new OrderSpecifier<>(Order.DESC, techArticle.commentTotalCount.count); } @Override - public Object getSearchAfterCondition(ElasticTechArticle elasticTechArticle) { - return elasticTechArticle.getCommentTotalCount(); + public BooleanExpression getCursorCondition(TechArticle findTechArticle) { + return techArticle.commentTotalCount.count.lt(findTechArticle.getCommentTotalCount().getCount()) + .or(techArticle.commentTotalCount.count.eq(findTechArticle.getCommentTotalCount().getCount()) + .and(techArticle.regDate.eq(findTechArticle.getRegDate()))); } }, HIGHEST_SCORE("정확도순") { - @Override - public SortBuilder getSortCondition() { - return SortBuilders.scoreSort().order(SortOrder.DESC); + public OrderSpecifier getOrderSpecifierByTechArticleSort() { + return new OrderSpecifier<>(Order.DESC, techArticle.id); } @Override - public Object getSearchAfterCondition(ElasticTechArticle elasticTechArticle) { - return null; + public BooleanExpression getCursorCondition(TechArticle findTechArticle) { + return techArticle.regDate.lt(findTechArticle.getRegDate()) + .or(techArticle.regDate.eq(findTechArticle.getRegDate()) + .and(techArticle.regDate.eq(findTechArticle.getRegDate()))); } }; - abstract public SortBuilder getSortCondition(); - - abstract public Object getSearchAfterCondition(ElasticTechArticle elasticTechArticle); + abstract public OrderSpecifier getOrderSpecifierByTechArticleSort(); - private static FieldSortBuilder getFieldSortBuilder(String sortFieldName) { - return SortBuilders.fieldSort(sortFieldName).order(SortOrder.DESC); - } + abstract public BooleanExpression getCursorCondition(TechArticle techArticle); private final String description; } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryCustom.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryCustom.java index cbaf6dec..c874cc41 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryCustom.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryCustom.java @@ -3,13 +3,16 @@ import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkSort; -import java.util.List; + +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; +import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; public interface TechArticleRepositoryCustom { - List findAllByElasticIdIn(List elasticIds); - Slice findBookmarkedByMemberAndCursor(Pageable pageable, Long techArticleId, BookmarkSort bookmarkSort, Member member); + + SliceCustom findTechArticlesByCursor(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, + Long companyId, String keyword, Float score); } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java index 7ff6313c..1c7a9054 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java @@ -7,51 +7,36 @@ import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkSort; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; +import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Predicate; import com.querydsl.jpa.JPQLQueryFactory; +import jakarta.persistence.EntityManager; import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; + import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; @RequiredArgsConstructor public class TechArticleRepositoryImpl implements TechArticleRepositoryCustom { private final JPQLQueryFactory query; - @Override - public List findAllByElasticIdIn(List elasticIds) { - - List findTechArticles = query.selectFrom(techArticle) - .where(techArticle.elasticId.in(elasticIds)) - .fetch(); - - // elasticId 목록의 순서를 기반으로 결과 목록 재정렬(h2 database에서는 order by Field() 쿼리를 지원하지 않으므로 재정렬 필요) - Map techArticles = findTechArticles.stream() - .collect(Collectors.toMap(TechArticle::getElasticId, Function.identity())); - - return elasticIds.stream() - .map(techArticles::get) - .collect(Collectors.toList()); - } - @Override public Slice findBookmarkedByMemberAndCursor(Pageable pageable, Long techArticleId, - BookmarkSort bookmarkSort, - Member member) { - + BookmarkSort bookmarkSort, Member member + ) { List contents = query.selectFrom(techArticle) .innerJoin(bookmark) .on(techArticle.eq(bookmark.techArticle)) .where(bookmark.member.eq(member), bookmark.status.isTrue(), - getCursorCondition(bookmarkSort, techArticleId, member)) + getCursorConditionFromBookmarkSort(bookmarkSort, techArticleId, member)) .orderBy(bookmarkSort(bookmarkSort), techArticle.id.desc()) .limit(pageable.getPageSize()) .fetch(); @@ -59,7 +44,55 @@ public Slice findBookmarkedByMemberAndCursor(Pageable pageable, Lon return new SliceImpl<>(contents, pageable, hasNextPage(contents, pageable.getPageSize())); } - private Predicate getCursorCondition(BookmarkSort bookmarkSort, Long techArticleId, Member member) { + @Override + public SliceCustom findTechArticlesByCursor(Pageable pageable, Long techArticleId, + TechArticleSort techArticleSort, Long companyId, + String keyword, Float score + ) { + // 키워드가 있는 경우 FULLTEXT 검색, 없는 경우 일반 조회 + if (StringUtils.hasText(keyword)) { + return findTechArticlesByCursorWithKeyword(pageable, techArticleId, techArticleSort, companyId, keyword, score); + } else { + return findTechArticlesByCursorWithoutKeyword(pageable, techArticleId, techArticleSort, companyId); + } + } + + // 키워드 검색 + private SliceCustom findTechArticlesByCursorWithKeyword(Pageable pageable, Long techArticleId, + TechArticleSort techArticleSort, Long companyId, + String keyword, Float score + ) { + List contents = null; + + // 기술블로그 총 갯수 + long totalElements = query.select(techArticle.count()) + .from(techArticle) + .fetchCount(); + + return new SliceCustom<>(contents, pageable, totalElements); + } + + // 일반 조회 + private SliceCustom findTechArticlesByCursorWithoutKeyword(Pageable pageable, Long techArticleId, + TechArticleSort techArticleSort, Long companyId + ) { + List contents = query.selectFrom(techArticle) + .where(getCursorConditionFromTechArticleSort(techArticleSort, techArticleId)) + .where(companyId != null ? techArticle.company.id.eq(companyId) : null) + .orderBy(techArticleSort(techArticleSort), techArticle.id.desc()) + .limit(pageable.getPageSize()) + .fetch(); + + // 기술블로그 총 갯수 + long totalElements = query.select(techArticle.count()) + .from(techArticle) + .where(companyId != null ? techArticle.company.id.eq(companyId) : null) + .fetchCount(); + + return new SliceCustom<>(contents, pageable, totalElements); + } + + private Predicate getCursorConditionFromBookmarkSort(BookmarkSort bookmarkSort, Long techArticleId, Member member) { if (ObjectUtils.isEmpty(techArticleId)) { return null; } @@ -90,6 +123,31 @@ private OrderSpecifier bookmarkSort(BookmarkSort bookmarkSort) { } + private Predicate getCursorConditionFromTechArticleSort(TechArticleSort techArticleSort, Long techArticleId) { + if (ObjectUtils.isEmpty(techArticleId)) { + return null; + } + + // techArticleId로 기술블로그 조회 + TechArticle findTechArticle = query.selectFrom(techArticle) + .where(techArticle.id.eq(techArticleId)) + .fetchOne(); + + // 일치하는 기술블로그가 없으면 + if (ObjectUtils.isEmpty(findTechArticle)) { + return techArticle.id.loe(techArticleId); + } + + return Optional.ofNullable(techArticleSort) + .orElse(TechArticleSort.LATEST).getCursorCondition(findTechArticle); + } + + private OrderSpecifier techArticleSort(TechArticleSort techArticleSort) { + return Optional.ofNullable(techArticleSort) + .orElse(TechArticleSort.LATEST).getOrderSpecifierByTechArticleSort(); + + } + private boolean hasNextPage(List contents, int pageSize) { return contents.size() >= pageSize; } 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 8d17f0aa..9682a739 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 @@ -13,8 +13,6 @@ import com.dreamypatisiel.devdevdev.domain.repository.survey.custom.SurveyAnswerJdbcTemplateRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkSort; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; -import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.TechArticleCommonService; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; import com.dreamypatisiel.devdevdev.exception.NicknameException; import com.dreamypatisiel.devdevdev.exception.SurveyException; import com.dreamypatisiel.devdevdev.global.common.MemberProvider; @@ -29,7 +27,6 @@ import com.dreamypatisiel.devdevdev.web.dto.response.member.MemberExitSurveyResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.MyPickMainResponse; import com.dreamypatisiel.devdevdev.web.dto.response.subscription.SubscribedCompanyResponse; -import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.CompanyResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleMainResponse; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; @@ -43,13 +40,10 @@ import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; -import java.util.stream.Stream; import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.MEMBER_INCOMPLETE_SURVEY_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.NicknameExceptionMessage.NICKNAME_CHANGE_RATE_LIMIT_MESSAGE; -import org.springframework.beans.factory.annotation.Value; - @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -60,7 +54,6 @@ public class MemberService { private final SurveyVersionQuestionMapperRepository surveyVersionQuestionMapperRepository; private final SurveyAnswerRepository surveyAnswerRepository; private final TechArticleRepository techArticleRepository; - private final TechArticleCommonService techArticleCommonService; private final TimeProvider timeProvider; private final SurveyQuestionOptionRepository surveyQuestionOptionRepository; private final SurveyAnswerJdbcTemplateRepository surveyAnswerJdbcTemplateRepository; @@ -210,33 +203,16 @@ public Slice getBookmarkedTechArticles(Pageable pageabl // 회원 조회 Member findMember = memberProvider.getMemberByAuthentication(authentication); - // 북마크 기술블로그 조회(rds, elasticsearch) - Slice techArticleSlices = techArticleRepository.findBookmarkedByMemberAndCursor(pageable, - techArticleId, bookmarkSort, findMember); - - List techArticles = techArticleSlices.getContent(); - - List elasticTechArticles = techArticleCommonService.findElasticTechArticlesByTechArticles( - techArticles); + // 북마크 기술블로그 조회 + Slice techArticles = techArticleRepository.findBookmarkedByMemberAndCursor( + pageable, techArticleId, bookmarkSort, findMember); // 데이터 가공 - List techArticleMainResponse = techArticles.stream() - .flatMap(techArticle -> mapToTechArticlesResponse(techArticle, elasticTechArticles, findMember)) + List techArticleMainResponse = techArticles.getContent().stream() + .map(techArticle -> TechArticleMainResponse.of(techArticle, findMember)) .toList(); - return new SliceImpl<>(techArticleMainResponse, pageable, techArticleSlices.hasNext()); - } - - /** - * 기술블로그 목록 응답 형태로 가공합니다. - */ - private Stream mapToTechArticlesResponse(TechArticle techArticle, - List elasticTechArticles, - Member member) { - return elasticTechArticles.stream() - .filter(elasticTechArticle -> techArticle.getElasticId().equals(elasticTechArticle.getId())) - .map(elasticTechArticle -> TechArticleMainResponse.of(techArticle, elasticTechArticle, - CompanyResponse.from(techArticle.getCompany()), member)); + return new SliceImpl<>(techArticleMainResponse, techArticles.getPageable(), techArticles.hasNext()); } /** diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/notification/NotificationService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/notification/NotificationService.java index 20345407..2e82d2fe 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/notification/NotificationService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/notification/NotificationService.java @@ -11,7 +11,6 @@ import com.dreamypatisiel.devdevdev.domain.repository.notification.NotificationRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.SubscriptionRepository; import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.TechArticleCommonService; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; import com.dreamypatisiel.devdevdev.exception.NotFoundException; import com.dreamypatisiel.devdevdev.global.common.MemberProvider; import com.dreamypatisiel.devdevdev.global.common.TimeProvider; @@ -26,7 +25,6 @@ import com.dreamypatisiel.devdevdev.web.dto.response.notification.NotificationPopupResponse; import com.dreamypatisiel.devdevdev.web.dto.response.notification.NotificationReadResponse; import com.dreamypatisiel.devdevdev.web.dto.response.notification.NotificationResponse; -import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.CompanyResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleMainResponse; import java.util.LinkedHashSet; import java.util.List; @@ -167,60 +165,25 @@ public SliceCustom getNotifications(Pageable pageable, Lon // 데이터 가공 // NotificationType 에 따라 다른 DTO로 변환 - Map elasticTechArticles = getTechArticleIdToElastic(notifications.getContent()); - List response = notifications.getContent().stream() - .map(notification -> mapToNotificationResponse(notification, elasticTechArticles)) + .map(this::mapToNotificationResponse) .toList(); return new SliceCustom<>(response, pageable, notifications.hasNext(), notifications.getTotalElements()); } - private NotificationResponse mapToNotificationResponse(Notification notification, - Map elasticTechArticles) { + private NotificationResponse mapToNotificationResponse(Notification notification) { // TODO: 현재는 SUBSCRIPTION 타입만 제공, 알림 타입이 추가될 경우 각 타입에 맞는 응답 DTO 변환 매핑 필요 if (notification.getType() == NotificationType.SUBSCRIPTION) { - return NotificationNewArticleResponse.from(notification, - getTechArticleMainResponse(notification, elasticTechArticles)); + return NotificationNewArticleResponse.from(notification, getTechArticleMainResponse(notification)); } throw new NotFoundException(NotificationExceptionMessage.NOT_FOUND_NOTIFICATION_TYPE); } // NotificationType.SUBSCRIPTION 알림의 경우 TechArticleMainResponse 생성 - private TechArticleMainResponse getTechArticleMainResponse(Notification notification, - Map elasticTechArticles) { + private TechArticleMainResponse getTechArticleMainResponse(Notification notification) { TechArticle techArticle = notification.getTechArticle(); - CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); - ElasticTechArticle elasticTechArticle = elasticTechArticles.get(notification.getId()); - - return TechArticleMainResponse.of(techArticle, elasticTechArticle, companyResponse); - } - - // 알림 ID를 키로 하고, ElasticTechArticle 을 값으로 가지는 맵을 반환 - private Map getTechArticleIdToElastic(List notifications) { - // 1. NotificationType.SUBSCRIPTION 알림만 필터링하여 ElasticTechArticle 리스트 생성 - List techArticles = notifications.stream() - .filter(notification -> notification.getType() == NotificationType.SUBSCRIPTION) - .map(Notification::getTechArticle) - .toList(); - - List elasticTechArticles = techArticleCommonService.findElasticTechArticlesByTechArticles( - techArticles); - - // 2. ElasticID → ElasticTechArticle 매핑 - Map elasticIdToElastic = elasticTechArticles.stream() - .collect(Collectors.toMap( - ElasticTechArticle::getId, - elasticTechArticle -> elasticTechArticle - )); - - // 3. Notification ID → ElasticTechArticle 매핑 - return notifications.stream() - .filter(notification -> notification.getType() == NotificationType.SUBSCRIPTION) - .collect(Collectors.toMap( - Notification::getId, - notification -> elasticIdToElastic.get(notification.getTechArticle().getElasticId()) - )); + return TechArticleMainResponse.of(techArticle); } /** diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/GuestTechArticleService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/GuestTechArticleService.java index b593c468..1d312e5c 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/GuestTechArticleService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/GuestTechArticleService.java @@ -8,29 +8,20 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; import com.dreamypatisiel.devdevdev.domain.service.member.AnonymousMemberService; -import com.dreamypatisiel.devdevdev.elastic.data.response.ElasticResponse; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.service.ElasticTechArticleService; import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.*; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; -import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.ObjectUtils; -import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.stream.Stream; - -import static com.dreamypatisiel.devdevdev.web.dto.util.TechArticleResponseUtils.hasNextPage; @Slf4j @Service @@ -39,43 +30,40 @@ public class GuestTechArticleService extends TechArticleCommonService implements public static final String INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE = "비회원은 현재 해당 기능을 이용할 수 없습니다."; - private final ElasticTechArticleService elasticTechArticleService; private final TechArticlePopularScorePolicy techArticlePopularScorePolicy; private final AnonymousMemberService anonymousMemberService; private final TechArticleRecommendRepository techArticleRecommendRepository; + private final TechArticleRepository techArticleRepository; - public GuestTechArticleService(TechArticleRepository techArticleRepository, - ElasticTechArticleRepository elasticTechArticleRepository, - ElasticTechArticleService elasticTechArticleService, - TechArticlePopularScorePolicy techArticlePopularScorePolicy, + public GuestTechArticleService(TechArticlePopularScorePolicy techArticlePopularScorePolicy, AnonymousMemberService anonymousMemberService, + TechArticleRepository techArticleRepository, TechArticleRecommendRepository techArticleRecommendRepository - ) { - super(techArticleRepository, elasticTechArticleRepository); - this.elasticTechArticleService = elasticTechArticleService; + ) { + super(techArticleRepository); this.techArticlePopularScorePolicy = techArticlePopularScorePolicy; this.anonymousMemberService = anonymousMemberService; this.techArticleRecommendRepository = techArticleRecommendRepository; + this.techArticleRepository = techArticleRepository; } @Override - public Slice getTechArticles(Pageable pageable, String elasticId, + public Slice getTechArticles(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, String keyword, Long companyId, Float score, Authentication authentication) { // 익명 사용자 호출인지 확인 AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); - // 엘라스틱서치 기술블로그 조회 - SearchHits elasticTechArticleSearchHits = elasticTechArticleService.getTechArticles( - pageable, elasticId, techArticleSort, keyword, companyId, score); - List> elasticTechArticles = elasticTechArticleService.mapToElasticResponse( - elasticTechArticleSearchHits); + // 기술블로그 조회 + SliceCustom techArticles = techArticleRepository.findTechArticlesByCursor( + pageable, techArticleId, techArticleSort, companyId, keyword, score); // 데이터 가공 - List techArticlesResponse = getTechArticlesResponse(elasticTechArticles); + List techArticlesResponse = techArticles.stream() + .map(TechArticleMainResponse::of) + .toList(); - return new SliceCustom<>(techArticlesResponse, pageable, hasNextPage(techArticlesResponse, pageable), - elasticTechArticleSearchHits.getTotalHits()); + return new SliceCustom<>(techArticlesResponse, pageable, techArticles.hasNext(), techArticles.getTotalElements()); } @Override @@ -86,17 +74,16 @@ public TechArticleDetailResponse getTechArticle(Long techArticleId, String anony // 익명 회원을 조회하거나 생성 AnonymousMember anonymousMember = getAnonymousMemberOrNull(anonymousMemberId); + // 기술블로그 조회 TechArticle techArticle = findTechArticle(techArticleId); - ElasticTechArticle elasticTechArticle = findElasticTechArticle(techArticle); - CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); // 조회수 증가 techArticle.incrementViewTotalCount(); techArticle.changePopularScore(techArticlePopularScorePolicy); // 데이터 가공 - return TechArticleDetailResponse.of(techArticle, elasticTechArticle, companyResponse, anonymousMember); + return TechArticleDetailResponse.of(techArticle, anonymousMember); } @Override @@ -153,34 +140,6 @@ public TechArticleRecommendResponse updateRecommend(Long techArticleId, String a return new TechArticleRecommendResponse(techArticle.getId(), techArticleRecommend.isRecommended(), techArticle.getRecommendTotalCount().getCount()); } - /** - * 엘라스틱서치 검색 결과로 기술블로그 목록 응답을 생성합니다. - */ - private List getTechArticlesResponse( - List> elasticTechArticles) { - // 조회 결과가 없을 경우 빈 리스트 응답 - if (elasticTechArticles.isEmpty()) { - return Collections.emptyList(); - } - - List techArticles = findTechArticlesByElasticTechArticles(elasticTechArticles); - - return techArticles.stream() - .flatMap(techArticle -> mapToTechArticlesResponse(techArticle, elasticTechArticles)) - .toList(); - } - - /** - * 기술블로그 목록을 응답 형태로 가공합니다. - */ - private Stream mapToTechArticlesResponse(TechArticle techArticle, - List> elasticTechArticles) { - return elasticTechArticles.stream() - .filter(elasticTechArticle -> techArticle.getElasticId().equals(elasticTechArticle.content().getId())) - .map(elasticTechArticle -> TechArticleMainResponse.of(techArticle, elasticTechArticle.content(), - CompanyResponse.from(techArticle.getCompany()), elasticTechArticle.score())); - } - /** * anonymousMemberId가 있으면 익명 회원을 조회 또는 생성하고, 없으면 null 반환 */ diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/MemberTechArticleService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/MemberTechArticleService.java index 34ec9e46..ea0b854e 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/MemberTechArticleService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/MemberTechArticleService.java @@ -1,7 +1,5 @@ package com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle; -import static com.dreamypatisiel.devdevdev.web.dto.util.TechArticleResponseUtils.hasNextPage; - import com.dreamypatisiel.devdevdev.domain.entity.Bookmark; import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; @@ -11,69 +9,61 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRecommendRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; -import com.dreamypatisiel.devdevdev.elastic.data.response.ElasticResponse; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.service.ElasticTechArticleService; import com.dreamypatisiel.devdevdev.global.common.MemberProvider; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.*; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; -import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.Optional; + @Slf4j @Service @Transactional(readOnly = true) public class MemberTechArticleService extends TechArticleCommonService implements TechArticleService { - private final ElasticTechArticleService elasticTechArticleService; private final TechArticlePopularScorePolicy techArticlePopularScorePolicy; + private final MemberProvider memberProvider; private final BookmarkRepository bookmarkRepository; private final TechArticleRecommendRepository techArticleRecommendRepository; - private final MemberProvider memberProvider; - - public MemberTechArticleService(TechArticleRepository techArticleRepository, - ElasticTechArticleRepository elasticTechArticleRepository, - ElasticTechArticleService elasticTechArticleService, - TechArticlePopularScorePolicy techArticlePopularScorePolicy, - BookmarkRepository bookmarkRepository, TechArticleRecommendRepository techArticleRecommendRepository, - MemberProvider memberProvider) { - super(techArticleRepository, elasticTechArticleRepository); - this.elasticTechArticleService = elasticTechArticleService; + private final TechArticleRepository techArticleRepository; + + public MemberTechArticleService(TechArticlePopularScorePolicy techArticlePopularScorePolicy, + MemberProvider memberProvider, + BookmarkRepository bookmarkRepository, + TechArticleRecommendRepository techArticleRecommendRepository, + TechArticleRepository techArticleRepository + ) { + super(techArticleRepository); this.techArticlePopularScorePolicy = techArticlePopularScorePolicy; this.bookmarkRepository = bookmarkRepository; this.techArticleRecommendRepository = techArticleRecommendRepository; this.memberProvider = memberProvider; + this.techArticleRepository = techArticleRepository; } @Override - public Slice getTechArticles(Pageable pageable, String elasticId, + public Slice getTechArticles(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, String keyword, Long companyId, Float score, Authentication authentication) { // 회원 조회 Member member = memberProvider.getMemberByAuthentication(authentication); - // 엘라스틱서치 기술블로그 조회 - SearchHits elasticTechArticleSearchHits = elasticTechArticleService.getTechArticles( - pageable, elasticId, techArticleSort, keyword, companyId, score); - List> elasticTechArticles = elasticTechArticleService.mapToElasticResponse( - elasticTechArticleSearchHits); + // 기술블로그 조회 + SliceCustom techArticles = techArticleRepository.findTechArticlesByCursor( + pageable, techArticleId, techArticleSort, companyId, keyword, score); // 데이터 가공 - List techArticlesResponse = getTechArticlesResponse(elasticTechArticles, member); + List techArticlesResponse = techArticles.stream() + .map(techArticle -> TechArticleMainResponse.of(techArticle, member)) + .toList(); - return new SliceCustom<>(techArticlesResponse, pageable, hasNextPage(techArticlesResponse, pageable), - elasticTechArticleSearchHits.getTotalHits()); + return new SliceCustom<>(techArticlesResponse, pageable, techArticles.hasNext(), techArticles.getTotalElements()); } @Override @@ -84,15 +74,13 @@ public TechArticleDetailResponse getTechArticle(Long techArticleId, String anony // 기술블로그 조회 TechArticle techArticle = findTechArticle(techArticleId); - ElasticTechArticle elasticTechArticle = findElasticTechArticle(techArticle); - CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); // 조회수 증가 techArticle.incrementViewTotalCount(); techArticle.changePopularScore(techArticlePopularScorePolicy); // 데이터 가공 - return TechArticleDetailResponse.of(techArticle, elasticTechArticle, companyResponse, member); + return TechArticleDetailResponse.of(techArticle, member); } @Override @@ -174,33 +162,4 @@ public TechArticleRecommendResponse updateRecommend(Long techArticleId, String a return new TechArticleRecommendResponse(techArticle.getId(), techArticleRecommend.isRecommended(), techArticle.getRecommendTotalCount().getCount()); } - - /** - * 엘라스틱서치 검색 결과로 기술블로그 목록 응답을 생성합니다. - */ - private List getTechArticlesResponse( - List> elasticTechArticles, Member member) { - // 조회 결과가 없을 경우 빈 리스트 응답 - if (elasticTechArticles.isEmpty()) { - return Collections.emptyList(); - } - - List techArticles = findTechArticlesByElasticTechArticles(elasticTechArticles); - - return techArticles.stream() - .flatMap(techArticle -> mapToTechArticlesResponse(techArticle, elasticTechArticles, member)) - .toList(); - } - - /** - * 기술블로그 목록을 응답 형태로 가공합니다. - */ - private Stream mapToTechArticlesResponse(TechArticle techArticle, - List> elasticTechArticles, - Member member) { - return elasticTechArticles.stream() - .filter(elasticTechArticle -> techArticle.getElasticId().equals(elasticTechArticle.content().getId())) - .map(elasticTechArticle -> TechArticleMainResponse.of(techArticle, elasticTechArticle.content(), - CompanyResponse.from(techArticle.getCompany()), elasticTechArticle.score(), member)); - } } \ No newline at end of file diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleCommonService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleCommonService.java index 050b89af..68749ce8 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleCommonService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleCommonService.java @@ -1,25 +1,15 @@ package com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_ID_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_TECH_ARTICLE_MESSAGE; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; import com.dreamypatisiel.devdevdev.domain.entity.TechComment; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; -import com.dreamypatisiel.devdevdev.elastic.data.response.ElasticResponse; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; import com.dreamypatisiel.devdevdev.exception.NotFoundException; -import com.dreamypatisiel.devdevdev.exception.TechArticleException; -import java.util.Collections; -import java.util.List; -import java.util.stream.StreamSupport; import javax.annotation.Nullable; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; @Component @RequiredArgsConstructor @@ -27,61 +17,12 @@ public class TechArticleCommonService { private final TechArticleRepository techArticleRepository; - private final ElasticTechArticleRepository elasticTechArticleRepository; - - protected ElasticTechArticle findElasticTechArticle(TechArticle techArticle) { - String elasticId = techArticle.getElasticId(); - - if (!StringUtils.hasText(elasticId)) { - throw new TechArticleException(NOT_FOUND_ELASTIC_ID_MESSAGE); - } - - return elasticTechArticleRepository.findById(elasticId) - .orElseThrow(() -> new NotFoundException(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE)); - } public TechArticle findTechArticle(Long techArticleId) { return techArticleRepository.findById(techArticleId) .orElseThrow(() -> new NotFoundException(NOT_FOUND_TECH_ARTICLE_MESSAGE)); } - protected List findTechArticlesByElasticTechArticles( - List> elasticTechArticlesResponse) { - List elasticIds = getElasticIdsFromElasticTechArticles(elasticTechArticlesResponse); - // 추출한 elasticId가 없다면 빈 리스트 응답 - if (elasticIds.isEmpty()) { - return Collections.emptyList(); - } - - return techArticleRepository.findAllByElasticIdIn(elasticIds); - } - - public List findElasticTechArticlesByTechArticles(List techArticles) { - List elasticIds = getElasticIdsFromTechArticles(techArticles); - // 추출한 elasticId가 없다면 빈 리스트 응답 - if (elasticIds.isEmpty()) { - return Collections.emptyList(); - } - - Iterable elasticTechArticles = elasticTechArticleRepository.findAllById(elasticIds); - - return StreamSupport.stream(elasticTechArticles.spliterator(), false) - .toList(); - } - - private List getElasticIdsFromTechArticles(List techArticles) { - return techArticles.stream() - .map(TechArticle::getElasticId) - .toList(); - } - - private List getElasticIdsFromElasticTechArticles( - List> elasticTechArticlesResponse) { - return elasticTechArticlesResponse.stream() - .map(elasticResponse -> elasticResponse.content().getId()) - .toList(); - } - public static void validateIsDeletedTechComment(TechComment techComment, String message, @Nullable String messageArgs) { if (techComment.isDeleted()) { diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleService.java index 48a9f41d..475b91d1 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleService.java @@ -10,7 +10,7 @@ import org.springframework.security.core.Authentication; public interface TechArticleService { - Slice getTechArticles(Pageable pageable, String elasticId, TechArticleSort techArticleSort, + Slice getTechArticles(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, String keyword, Long companyId, Float score, Authentication authentication); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/config/ElasticsearchIndexConfigService.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/config/ElasticsearchIndexConfigService.java index f9146d7a..ff808863 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/config/ElasticsearchIndexConfigService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/config/ElasticsearchIndexConfigService.java @@ -1,12 +1,12 @@ -package com.dreamypatisiel.devdevdev.elastic.config; - -import lombok.Getter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -@Getter -@Service -public class ElasticsearchIndexConfigService { - @Value("${opensearch.index:}") - private String indexName; -} \ No newline at end of file +//package com.dreamypatisiel.devdevdev.elastic.config; +// +//import lombok.Getter; +//import org.springframework.beans.factory.annotation.Value; +//import org.springframework.stereotype.Service; +// +//@Getter +//@Service +//public class ElasticsearchIndexConfigService { +// @Value("${opensearch.index:}") +// private String indexName; +//} \ No newline at end of file diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/config/OpenSearchRestClientConfiguration.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/config/OpenSearchRestClientConfiguration.java index 5fd90484..072ee308 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/config/OpenSearchRestClientConfiguration.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/config/OpenSearchRestClientConfiguration.java @@ -1,66 +1,66 @@ -package com.dreamypatisiel.devdevdev.elastic.config; - -import java.time.Duration; -import org.apache.http.HttpHost; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.CredentialsProvider; -import org.apache.http.impl.client.BasicCredentialsProvider; -import org.opensearch.client.RestClient; -import org.opensearch.client.RestClientBuilder; -import org.opensearch.client.RestHighLevelClient; -import org.opensearch.data.client.orhlc.AbstractOpenSearchConfiguration; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; - -@Configuration -@EnableElasticsearchRepositories(basePackages = "com.dreamypatisiel.devdevdev.elastic.domain.repository") -public class OpenSearchRestClientConfiguration extends AbstractOpenSearchConfiguration { - - public static final String ENDPOINT_SCHEME = "https"; - public static final int ENDPOINT_PORT = 443; - public static final int CONNECTION_TIMEOUT_IN_SECONDS = 30; - public static final int SOCKET_TIMEOUT_IN_SECONDS = 60; - - @Value("${opensearch.endpoint}") - private String endpoint; - - @Value("${opensearch.credentials.username}") - private String username; - - @Value("${opensearch.credentials.password}") - private String password; - - @Autowired - private ApplicationContext applicationContext; - - @Bean - public CredentialsProvider credentialsProvider() { - CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); - credentialsProvider.setCredentials( - AuthScope.ANY, new UsernamePasswordCredentials(username, password)); - return credentialsProvider; - } - - @Bean - @Override - public RestHighLevelClient opensearchClient() { - CredentialsProvider credentialsProvider = applicationContext.getBean( - CredentialsProvider.class); - - RestClientBuilder builder = RestClient.builder(new HttpHost(endpoint, ENDPOINT_PORT, ENDPOINT_SCHEME)) - .setHttpClientConfigCallback( - httpAsyncClientBuilder -> httpAsyncClientBuilder.setDefaultCredentialsProvider( - credentialsProvider)) - .setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder - .setConnectTimeout((int) Duration.ofSeconds(CONNECTION_TIMEOUT_IN_SECONDS).toMillis()) - .setSocketTimeout((int) Duration.ofSeconds(SOCKET_TIMEOUT_IN_SECONDS).toMillis())); - - RestHighLevelClient client = new RestHighLevelClient(builder); - return client; - } -} \ No newline at end of file +//package com.dreamypatisiel.devdevdev.elastic.config; +// +//import java.time.Duration; +//import org.apache.http.HttpHost; +//import org.apache.http.auth.AuthScope; +//import org.apache.http.auth.UsernamePasswordCredentials; +//import org.apache.http.client.CredentialsProvider; +//import org.apache.http.impl.client.BasicCredentialsProvider; +//import org.opensearch.client.RestClient; +//import org.opensearch.client.RestClientBuilder; +//import org.opensearch.client.RestHighLevelClient; +//import org.opensearch.data.client.orhlc.AbstractOpenSearchConfiguration; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.beans.factory.annotation.Value; +//import org.springframework.context.ApplicationContext; +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; +// +//@Configuration +//@EnableElasticsearchRepositories(basePackages = "com.dreamypatisiel.devdevdev.elastic.domain.repository") +//public class OpenSearchRestClientConfiguration extends AbstractOpenSearchConfiguration { +// +// public static final String ENDPOINT_SCHEME = "https"; +// public static final int ENDPOINT_PORT = 443; +// public static final int CONNECTION_TIMEOUT_IN_SECONDS = 30; +// public static final int SOCKET_TIMEOUT_IN_SECONDS = 60; +// +// @Value("${opensearch.endpoint}") +// private String endpoint; +// +// @Value("${opensearch.credentials.username}") +// private String username; +// +// @Value("${opensearch.credentials.password}") +// private String password; +// +// @Autowired +// private ApplicationContext applicationContext; +// +// @Bean +// public CredentialsProvider credentialsProvider() { +// CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); +// credentialsProvider.setCredentials( +// AuthScope.ANY, new UsernamePasswordCredentials(username, password)); +// return credentialsProvider; +// } +// +// @Bean +// @Override +// public RestHighLevelClient opensearchClient() { +// CredentialsProvider credentialsProvider = applicationContext.getBean( +// CredentialsProvider.class); +// +// RestClientBuilder builder = RestClient.builder(new HttpHost(endpoint, ENDPOINT_PORT, ENDPOINT_SCHEME)) +// .setHttpClientConfigCallback( +// httpAsyncClientBuilder -> httpAsyncClientBuilder.setDefaultCredentialsProvider( +// credentialsProvider)) +// .setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder +// .setConnectTimeout((int) Duration.ofSeconds(CONNECTION_TIMEOUT_IN_SECONDS).toMillis()) +// .setSocketTimeout((int) Duration.ofSeconds(SOCKET_TIMEOUT_IN_SECONDS).toMillis())); +// +// RestHighLevelClient client = new RestHighLevelClient(builder); +// return client; +// } +//} \ No newline at end of file diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/constant/ElasticsearchConstant.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/constant/ElasticsearchConstant.java index 5d5aa336..d1c87644 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/constant/ElasticsearchConstant.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/constant/ElasticsearchConstant.java @@ -1,11 +1,11 @@ -package com.dreamypatisiel.devdevdev.elastic.constant; - -public class ElasticsearchConstant { - public static final String _ID = "_id"; - public static final String _COMPANY_ID = "companyId"; - - public final static String LATEST_SORT_FIELD_NAME = "regDate"; - public final static String POPULAR_SORT_FIELD_NAME = "popularScore"; - public final static String MOST_VIEWED_SORT_FIELD_NAME = "viewTotalCount"; - public final static String MOST_COMMENTED_SORT_FIELD_NAME = "commentTotalCount"; -} +//package com.dreamypatisiel.devdevdev.elastic.constant; +// +//public class ElasticsearchConstant { +// public static final String _ID = "_id"; +// public static final String _COMPANY_ID = "companyId"; +// +// public final static String LATEST_SORT_FIELD_NAME = "regDate"; +// public final static String POPULAR_SORT_FIELD_NAME = "popularScore"; +// public final static String MOST_VIEWED_SORT_FIELD_NAME = "viewTotalCount"; +// public final static String MOST_COMMENTED_SORT_FIELD_NAME = "commentTotalCount"; +//} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/data/response/ElasticResponse.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/data/response/ElasticResponse.java index 71055d63..44480bad 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/data/response/ElasticResponse.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/data/response/ElasticResponse.java @@ -1,4 +1,4 @@ -package com.dreamypatisiel.devdevdev.elastic.data.response; - -public record ElasticResponse(T content, Float score) { -} +//package com.dreamypatisiel.devdevdev.elastic.data.response; +// +//public record ElasticResponse(T content, Float score) { +//} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/document/ElasticKeyword.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/document/ElasticKeyword.java index 0322acad..7c04e04d 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/document/ElasticKeyword.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/document/ElasticKeyword.java @@ -1,30 +1,30 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.document; - -import lombok.Builder; -import lombok.Getter; -import org.springframework.data.annotation.Id; -import org.springframework.data.elasticsearch.annotations.Document; -import org.springframework.data.elasticsearch.annotations.Field; -import org.springframework.data.elasticsearch.annotations.FieldType; - -@Getter -@Document(indexName = "keywords" + "#{@elasticsearchIndexConfigService.getIndexName()}") -public class ElasticKeyword { - @Id - private String id; - - @Field(type = FieldType.Text) - private String keyword; - - @Builder - private ElasticKeyword(String id, String keyword) { - this.id = id; - this.keyword = keyword; - } - - public static ElasticKeyword create(String keyword) { - return ElasticKeyword.builder() - .keyword(keyword) - .build(); - } -} +//package com.dreamypatisiel.devdevdev.elastic.domain.document; +// +//import lombok.Builder; +//import lombok.Getter; +//import org.springframework.data.annotation.Id; +//import org.springframework.data.elasticsearch.annotations.Document; +//import org.springframework.data.elasticsearch.annotations.Field; +//import org.springframework.data.elasticsearch.annotations.FieldType; +// +//@Getter +//@Document(indexName = "keywords" + "#{@elasticsearchIndexConfigService.getIndexName()}") +//public class ElasticKeyword { +// @Id +// private String id; +// +// @Field(type = FieldType.Text) +// private String keyword; +// +// @Builder +// private ElasticKeyword(String id, String keyword) { +// this.id = id; +// this.keyword = keyword; +// } +// +// public static ElasticKeyword create(String keyword) { +// return ElasticKeyword.builder() +// .keyword(keyword) +// .build(); +// } +//} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/document/ElasticTechArticle.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/document/ElasticTechArticle.java index b7bd33e1..9ce5b9b1 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/document/ElasticTechArticle.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/document/ElasticTechArticle.java @@ -1,76 +1,76 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.document; - -import java.time.LocalDate; -import lombok.Builder; -import lombok.Getter; -import org.springframework.data.annotation.Id; -import org.springframework.data.elasticsearch.annotations.Document; -import org.springframework.data.elasticsearch.annotations.Field; -import org.springframework.data.elasticsearch.annotations.FieldType; - -@Getter -@Document(indexName = "articles" + "#{@elasticsearchIndexConfigService.getIndexName()}") -public class ElasticTechArticle { - @Id - private String id; - - @Field(type = FieldType.Text) - private String title; - - @Field(type = FieldType.Date) - private LocalDate regDate; - - @Field(type = FieldType.Text) - private String contents; - - @Field(type = FieldType.Text) - private String techArticleUrl; - - @Field(type = FieldType.Text) - private String description; - - @Field(type = FieldType.Text) - private String thumbnailUrl; - - @Field(type = FieldType.Text) - private String author; - - @Field(type = FieldType.Text) - private String company; - - @Field(type = FieldType.Long) - private Long companyId; - - @Field(type = FieldType.Long) - private Long viewTotalCount; - - @Field(type = FieldType.Long) - private Long recommendTotalCount; - - @Field(type = FieldType.Long) - private Long commentTotalCount; - - @Field(type = FieldType.Long) - private Long popularScore; - - @Builder - public ElasticTechArticle(String id, String title, LocalDate regDate, String contents, String techArticleUrl, - String description, String thumbnailUrl, String author, Long companyId, String company, - Long viewTotalCount, Long recommendTotalCount, Long commentTotalCount, - Long popularScore) { - this.id = id; - this.title = title; - this.regDate = regDate; - this.contents = contents; - this.techArticleUrl = techArticleUrl; - this.description = description; - this.thumbnailUrl = thumbnailUrl; - this.author = author; - this.companyId = companyId; - this.company = company; - this.viewTotalCount = viewTotalCount; - this.recommendTotalCount = recommendTotalCount; - this.commentTotalCount = commentTotalCount; - this.popularScore = popularScore; - } -} +//package com.dreamypatisiel.devdevdev.elastic.domain.document; +// +//import java.time.LocalDate; +//import lombok.Builder; +//import lombok.Getter; +//import org.springframework.data.annotation.Id; +//import org.springframework.data.elasticsearch.annotations.Document; +//import org.springframework.data.elasticsearch.annotations.Field; +//import org.springframework.data.elasticsearch.annotations.FieldType; +// +//@Getter +//@Document(indexName = "articles" + "#{@elasticsearchIndexConfigService.getIndexName()}") +//public class ElasticTechArticle { +// @Id +// private String id; +// +// @Field(type = FieldType.Text) +// private String title; +// +// @Field(type = FieldType.Date) +// private LocalDate regDate; +// +// @Field(type = FieldType.Text) +// private String contents; +// +// @Field(type = FieldType.Text) +// private String techArticleUrl; +// +// @Field(type = FieldType.Text) +// private String description; +// +// @Field(type = FieldType.Text) +// private String thumbnailUrl; +// +// @Field(type = FieldType.Text) +// private String author; +// +// @Field(type = FieldType.Text) +// private String company; +// +// @Field(type = FieldType.Long) +// private Long companyId; +// +// @Field(type = FieldType.Long) +// private Long viewTotalCount; +// +// @Field(type = FieldType.Long) +// private Long recommendTotalCount; +// +// @Field(type = FieldType.Long) +// private Long commentTotalCount; +// +// @Field(type = FieldType.Long) +// private Long popularScore; +// +// @Builder +// public ElasticTechArticle(String id, String title, LocalDate regDate, String contents, String techArticleUrl, +// String description, String thumbnailUrl, String author, Long companyId, String company, +// Long viewTotalCount, Long recommendTotalCount, Long commentTotalCount, +// Long popularScore) { +// this.id = id; +// this.title = title; +// this.regDate = regDate; +// this.contents = contents; +// this.techArticleUrl = techArticleUrl; +// this.description = description; +// this.thumbnailUrl = thumbnailUrl; +// this.author = author; +// this.companyId = companyId; +// this.company = company; +// this.viewTotalCount = viewTotalCount; +// this.recommendTotalCount = recommendTotalCount; +// this.commentTotalCount = commentTotalCount; +// this.popularScore = popularScore; +// } +//} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/repository/ElasticKeywordRepository.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/repository/ElasticKeywordRepository.java index 7c1d59a1..0370bf45 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/repository/ElasticKeywordRepository.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/repository/ElasticKeywordRepository.java @@ -1,9 +1,9 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.repository; - -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticKeyword; -import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; -import org.springframework.data.repository.CrudRepository; - -public interface ElasticKeywordRepository extends ElasticsearchRepository, - CrudRepository { -} +//package com.dreamypatisiel.devdevdev.elastic.domain.repository; +// +//import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticKeyword; +//import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +//import org.springframework.data.repository.CrudRepository; +// +//public interface ElasticKeywordRepository extends ElasticsearchRepository, +// CrudRepository { +//} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/repository/ElasticTechArticleRepository.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/repository/ElasticTechArticleRepository.java index ec644ed5..21f79535 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/repository/ElasticTechArticleRepository.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/repository/ElasticTechArticleRepository.java @@ -1,11 +1,11 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.repository; - -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import java.util.List; -import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; -import org.springframework.data.repository.CrudRepository; - -public interface ElasticTechArticleRepository extends ElasticsearchRepository, - CrudRepository { - List findTop10By(); -} +//package com.dreamypatisiel.devdevdev.elastic.domain.repository; +// +//import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; +//import java.util.List; +//import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +//import org.springframework.data.repository.CrudRepository; +// +//public interface ElasticTechArticleRepository extends ElasticsearchRepository, +// CrudRepository { +// List findTop10By(); +//} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticKeywordService.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticKeywordService.java index b270263e..6ffeae0d 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticKeywordService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticKeywordService.java @@ -1,56 +1,56 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.service; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; -import org.opensearch.client.RequestOptions; -import org.opensearch.client.RestHighLevelClient; -import org.opensearch.common.unit.Fuzziness; -import org.opensearch.index.query.MultiMatchQueryBuilder; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.search.builder.SearchSourceBuilder; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -@Slf4j -@Service -@RequiredArgsConstructor -public class ElasticKeywordService { - - @Value("#{@elasticsearchIndexConfigService.getIndexName()}") - private String INDEX_NAME_POSTFIX; - public static final String INDEX_NAME = "keywords"; - public static final String FIELD_NAME = "keyword"; - public static final String[] MULTI_FIELD_NAMES = {"keyword", "keyword.nfc", "keyword.chosung"}; - public static final int AUTOCOMPLETION_MAX_SIZE = 20; - - - private final RestHighLevelClient elasticsearchClient; - - public List autocompleteKeyword(String prefix) throws IOException { - - MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery( - prefix, // 검색할 쿼리 스트링 - MULTI_FIELD_NAMES); // multi-match 쿼리를 실행할 필드 목록 정의 - multiMatchQueryBuilder.fuzziness(Fuzziness.ZERO); // Fuzziness를 0으로 설정하여 정확히 일치하는 키워드만 검색 - - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder() - .query(multiMatchQueryBuilder) - .size(AUTOCOMPLETION_MAX_SIZE); // 최대 20개 조회 - - // 조회 쿼리 생성 - SearchRequest searchRequest = new SearchRequest(INDEX_NAME + INDEX_NAME_POSTFIX) - .source(searchSourceBuilder); - - // 응답 데이터 가공 - SearchResponse searchResponse = elasticsearchClient.search(searchRequest, RequestOptions.DEFAULT); - return Arrays.stream(searchResponse.getHits().getHits()) - .map(hit -> hit.getSourceAsMap().get(FIELD_NAME).toString()) - .toList(); - } -} - +//package com.dreamypatisiel.devdevdev.elastic.domain.service; +// +//import java.io.IOException; +//import java.util.Arrays; +//import java.util.List; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.opensearch.action.search.SearchRequest; +//import org.opensearch.action.search.SearchResponse; +//import org.opensearch.client.RequestOptions; +//import org.opensearch.client.RestHighLevelClient; +//import org.opensearch.common.unit.Fuzziness; +//import org.opensearch.index.query.MultiMatchQueryBuilder; +//import org.opensearch.index.query.QueryBuilders; +//import org.opensearch.search.builder.SearchSourceBuilder; +//import org.springframework.beans.factory.annotation.Value; +//import org.springframework.stereotype.Service; +// +//@Slf4j +//@Service +//@RequiredArgsConstructor +//public class ElasticKeywordService { +// +// @Value("#{@elasticsearchIndexConfigService.getIndexName()}") +// private String INDEX_NAME_POSTFIX; +// public static final String INDEX_NAME = "keywords"; +// public static final String FIELD_NAME = "keyword"; +// public static final String[] MULTI_FIELD_NAMES = {"keyword", "keyword.nfc", "keyword.chosung"}; +// public static final int AUTOCOMPLETION_MAX_SIZE = 20; +// +// +// private final RestHighLevelClient elasticsearchClient; +// +// public List autocompleteKeyword(String prefix) throws IOException { +// +// MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery( +// prefix, // 검색할 쿼리 스트링 +// MULTI_FIELD_NAMES); // multi-match 쿼리를 실행할 필드 목록 정의 +// multiMatchQueryBuilder.fuzziness(Fuzziness.ZERO); // Fuzziness를 0으로 설정하여 정확히 일치하는 키워드만 검색 +// +// SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder() +// .query(multiMatchQueryBuilder) +// .size(AUTOCOMPLETION_MAX_SIZE); // 최대 20개 조회 +// +// // 조회 쿼리 생성 +// SearchRequest searchRequest = new SearchRequest(INDEX_NAME + INDEX_NAME_POSTFIX) +// .source(searchSourceBuilder); +// +// // 응답 데이터 가공 +// SearchResponse searchResponse = elasticsearchClient.search(searchRequest, RequestOptions.DEFAULT); +// return Arrays.stream(searchResponse.getHits().getHits()) +// .map(hit -> hit.getSourceAsMap().get(FIELD_NAME).toString()) +// .toList(); +// } +//} +// diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticService.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticService.java index b20df68b..26223b97 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticService.java @@ -1,13 +1,13 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.service; - -import com.dreamypatisiel.devdevdev.elastic.data.response.ElasticResponse; -import java.util.List; -import org.springframework.data.elasticsearch.core.SearchHits; - -public interface ElasticService { - default List> mapToElasticResponse(SearchHits searchHits) { - return searchHits.stream() - .map(searchHit -> new ElasticResponse<>(searchHit.getContent(), searchHit.getScore())) - .toList(); - } -} +//package com.dreamypatisiel.devdevdev.elastic.domain.service; +// +//import com.dreamypatisiel.devdevdev.elastic.data.response.ElasticResponse; +//import java.util.List; +//import org.springframework.data.elasticsearch.core.SearchHits; +// +//public interface ElasticService { +// default List> mapToElasticResponse(SearchHits searchHits) { +// return searchHits.stream() +// .map(searchHit -> new ElasticResponse<>(searchHit.getContent(), searchHit.getScore())) +// .toList(); +// } +//} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticTechArticleService.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticTechArticleService.java index bc30edd4..7c212f7e 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticTechArticleService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticTechArticleService.java @@ -1,188 +1,194 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.service; - -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.INVALID_ELASTIC_METHODS_CALL_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_CURSOR_SCORE_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE; -import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant.LATEST_SORT_FIELD_NAME; -import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant._COMPANY_ID; -import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant._ID; - -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; -import com.dreamypatisiel.devdevdev.exception.ElasticTechArticleException; -import com.dreamypatisiel.devdevdev.exception.NotFoundException; -import java.util.List; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.opensearch.action.search.SearchType; -import org.opensearch.data.client.orhlc.NativeSearchQuery; -import org.opensearch.data.client.orhlc.NativeSearchQueryBuilder; -import org.opensearch.index.query.Operator; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.search.sort.FieldSortBuilder; -import org.opensearch.search.sort.SortBuilder; -import org.opensearch.search.sort.SortBuilders; -import org.opensearch.search.sort.SortOrder; -import org.springframework.data.domain.Pageable; -import org.springframework.data.elasticsearch.UncategorizedElasticsearchException; -import org.springframework.data.elasticsearch.core.ElasticsearchOperations; -import org.springframework.data.elasticsearch.core.SearchHits; -import org.springframework.stereotype.Service; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -@Slf4j -@Service -@RequiredArgsConstructor -public class ElasticTechArticleService implements ElasticService { - - private final ElasticsearchOperations elasticsearchOperations; - private final ElasticTechArticleRepository elasticTechArticleRepository; - - public SearchHits getTechArticles(Pageable pageable, String elasticId, - TechArticleSort techArticleSort, String keyword, - Long companyId, Float score) { - if (!StringUtils.hasText(keyword)) { - return findTechArticles(pageable, elasticId, techArticleSort, companyId); - } - - return searchTechArticles(pageable, elasticId, techArticleSort, keyword, companyId, score); - - } - - private SearchHits findTechArticles(Pageable pageable, String elasticId, - TechArticleSort techArticleSort, - Long companyId) { - // 정렬 기준 검증 - TechArticleSort validTechArticleSort = getValidSort(techArticleSort); - - // 쿼리 생성 - NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder() - .withSearchType(SearchType.QUERY_THEN_FETCH) - .withPageable(pageable) - // 정렬 조건 설정 - .withSorts(getSortCondition(validTechArticleSort), - getPrimarySortCondition(LATEST_SORT_FIELD_NAME), - getPrimarySortCondition(_ID)); - - // 회사 필터 설정 - setFilterWithCompanyId(companyId, queryBuilder); - - NativeSearchQuery searchQuery = queryBuilder.build(); - - // searchAfter 설정 - setSearchAfterCondition(elasticId, validTechArticleSort, searchQuery); - - return elasticsearchOperations.search(searchQuery, ElasticTechArticle.class); - } - - private SearchHits searchTechArticles(Pageable pageable, String elasticId, - TechArticleSort techArticleSort, - String keyword, - Long companyId, Float score) - throws UncategorizedElasticsearchException { - - // 검색어 유무 확인 - if (!StringUtils.hasText(keyword)) { - throw new ElasticTechArticleException(INVALID_ELASTIC_METHODS_CALL_MESSAGE); - } - - // 정렬 기준 검증 - TechArticleSort validTechArticleSort = getValidSortWhenSearch(techArticleSort); - - // 쿼리 생성 - NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder() - .withSearchType(SearchType.QUERY_THEN_FETCH) - .withPageable(pageable) - // 쿼리스트링 - .withQuery(QueryBuilders.queryStringQuery(keyword).defaultOperator(Operator.AND)) - // 정렬 조건 설정 - .withSorts(getSortCondition(validTechArticleSort), - getPrimarySortCondition(LATEST_SORT_FIELD_NAME), - getPrimarySortCondition(_ID)); - - // 회사 필터 설정 - setFilterWithCompanyId(companyId, queryBuilder); - - NativeSearchQuery searchQuery = queryBuilder.build(); - - // searchAfter 설정 - setSearchAfterConditionWhenSearch(elasticId, score, validTechArticleSort, searchQuery); - - return elasticsearchOperations.search(searchQuery, ElasticTechArticle.class); - } - - private static void setFilterWithCompanyId(Long companyId, NativeSearchQueryBuilder queryBuilder) { - if (ObjectUtils.isEmpty(companyId)) { - return; - } - queryBuilder.withFilter(QueryBuilders.termQuery(_COMPANY_ID, companyId)); - } - - private FieldSortBuilder getPrimarySortCondition(String fieldName) { - return SortBuilders.fieldSort(fieldName).order(SortOrder.DESC); - } - - private SortBuilder getSortCondition(TechArticleSort techArticleSort) { - return techArticleSort.getSortCondition(); - } - - private static TechArticleSort getValidSort(TechArticleSort techArticleSort) { - return Optional.ofNullable(techArticleSort) - .filter(sort -> sort != TechArticleSort.HIGHEST_SCORE) - .orElse(TechArticleSort.LATEST); - } - - private static TechArticleSort getValidSortWhenSearch(TechArticleSort techArticleSort) { - return Optional.ofNullable(techArticleSort).orElse(TechArticleSort.HIGHEST_SCORE); - } - - private void setSearchAfterCondition(String elasticId, TechArticleSort techArticleSort, - NativeSearchQuery searchQuery) { - if (!StringUtils.hasText(elasticId)) { - return; - } - - ElasticTechArticle elasticTechArticle = elasticTechArticleRepository.findById(elasticId) - .orElseThrow(() -> new NotFoundException(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE)); - - searchQuery.setSearchAfter(getSearchAfter(elasticTechArticle, techArticleSort)); - } - - private void setSearchAfterConditionWhenSearch(String elasticId, Float score, TechArticleSort techArticleSort, - NativeSearchQuery searchQuery) { - if (!StringUtils.hasText(elasticId)) { - return; - } - - ElasticTechArticle elasticTechArticle = elasticTechArticleRepository.findById(elasticId) - .orElseThrow(() -> new NotFoundException(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE)); - - searchQuery.setSearchAfter(getSearchAfterWhenSearch(elasticTechArticle, techArticleSort, score)); - } - - private List getSearchAfter(ElasticTechArticle elasticTechArticle, TechArticleSort techArticleSort) { - return List.of(techArticleSort.getSearchAfterCondition(elasticTechArticle), - TechArticleSort.LATEST.getSearchAfterCondition(elasticTechArticle), - elasticTechArticle.getId()); - } - - private List getSearchAfterWhenSearch(ElasticTechArticle elasticTechArticle, - TechArticleSort techArticleSort, - Float score) { - // 정확도순 정렬이 아닌 경우 - if (!TechArticleSort.HIGHEST_SCORE.equals(techArticleSort)) { - return getSearchAfter(elasticTechArticle, techArticleSort); - } - - if (ObjectUtils.isEmpty(score)) { - throw new ElasticTechArticleException(NOT_FOUND_CURSOR_SCORE_MESSAGE); - } - - return List.of(score, - TechArticleSort.LATEST.getSearchAfterCondition(elasticTechArticle), - elasticTechArticle.getId()); - } -} +//package com.dreamypatisiel.devdevdev.elastic.domain.service; +// +//import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.INVALID_ELASTIC_METHODS_CALL_MESSAGE; +//import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_CURSOR_SCORE_MESSAGE; +//import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE; +//import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant.LATEST_SORT_FIELD_NAME; +//import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant._COMPANY_ID; +//import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant._ID; +// +//import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; +//import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; +//import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; +//import com.dreamypatisiel.devdevdev.exception.ElasticTechArticleException; +//import com.dreamypatisiel.devdevdev.exception.NotFoundException; +//import java.util.List; +//import java.util.Optional; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.opensearch.action.search.SearchType; +//import org.opensearch.data.client.orhlc.NativeSearchQuery; +//import org.opensearch.data.client.orhlc.NativeSearchQueryBuilder; +//import org.opensearch.index.query.Operator; +//import org.opensearch.index.query.QueryBuilders; +//import org.opensearch.search.sort.FieldSortBuilder; +//import org.opensearch.search.sort.SortBuilder; +//import org.opensearch.search.sort.SortBuilders; +//import org.opensearch.search.sort.SortOrder; +//import org.springframework.data.domain.Pageable; +//import org.springframework.data.elasticsearch.UncategorizedElasticsearchException; +//import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +//import org.springframework.data.elasticsearch.core.SearchHits; +//import org.springframework.stereotype.Service; +//import org.springframework.util.ObjectUtils; +//import org.springframework.util.StringUtils; +// +//import java.util.List; +//import java.util.Optional; +// +//import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.*; +//import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant.*; +// +//@Slf4j +//@Service +//@RequiredArgsConstructor +//public class ElasticTechArticleService implements ElasticService { +// +// private final ElasticsearchOperations elasticsearchOperations; +// private final ElasticTechArticleRepository elasticTechArticleRepository; +// +// public SearchHits getTechArticles(Pageable pageable, String elasticId, +// TechArticleSort techArticleSort, String keyword, +// Long companyId, Float score) { +// if (!StringUtils.hasText(keyword)) { +// return findTechArticles(pageable, elasticId, techArticleSort, companyId); +// } +// +// return searchTechArticles(pageable, elasticId, techArticleSort, keyword, companyId, score); +// +// } +// +// private SearchHits findTechArticles(Pageable pageable, String elasticId, +// TechArticleSort techArticleSort, +// Long companyId) { +// // 정렬 기준 검증 +// TechArticleSort validTechArticleSort = getValidSort(techArticleSort); +// +// // 쿼리 생성 +// NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder() +// .withSearchType(SearchType.QUERY_THEN_FETCH) +// .withPageable(pageable) +// // 정렬 조건 설정 +// .withSorts(getSortCondition(validTechArticleSort), +// getPrimarySortCondition(LATEST_SORT_FIELD_NAME), +// getPrimarySortCondition(_ID)); +// +// // 회사 필터 설정 +// setFilterWithCompanyId(companyId, queryBuilder); +// +// NativeSearchQuery searchQuery = queryBuilder.build(); +// +// // searchAfter 설정 +// setSearchAfterCondition(elasticId, validTechArticleSort, searchQuery); +// +// return elasticsearchOperations.search(searchQuery, ElasticTechArticle.class); +// } +// +// private SearchHits searchTechArticles(Pageable pageable, String elasticId, +// TechArticleSort techArticleSort, +// String keyword, +// Long companyId, Float score) +// throws UncategorizedElasticsearchException { +// +// // 검색어 유무 확인 +// if (!StringUtils.hasText(keyword)) { +// throw new ElasticTechArticleException(INVALID_ELASTIC_METHODS_CALL_MESSAGE); +// } +// +// // 정렬 기준 검증 +// TechArticleSort validTechArticleSort = getValidSortWhenSearch(techArticleSort); +// +// // 쿼리 생성 +// NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder() +// .withSearchType(SearchType.QUERY_THEN_FETCH) +// .withPageable(pageable) +// // 쿼리스트링 +// .withQuery(QueryBuilders.queryStringQuery(keyword).defaultOperator(Operator.AND)) +// // 정렬 조건 설정 +// .withSorts(getSortCondition(validTechArticleSort), +// getPrimarySortCondition(LATEST_SORT_FIELD_NAME), +// getPrimarySortCondition(_ID)); +// +// // 회사 필터 설정 +// setFilterWithCompanyId(companyId, queryBuilder); +// +// NativeSearchQuery searchQuery = queryBuilder.build(); +// +// // searchAfter 설정 +// setSearchAfterConditionWhenSearch(elasticId, score, validTechArticleSort, searchQuery); +// +// return elasticsearchOperations.search(searchQuery, ElasticTechArticle.class); +// } +// +// private static void setFilterWithCompanyId(Long companyId, NativeSearchQueryBuilder queryBuilder) { +// if (ObjectUtils.isEmpty(companyId)) { +// return; +// } +// queryBuilder.withFilter(QueryBuilders.termQuery(_COMPANY_ID, companyId)); +// } +// +// private FieldSortBuilder getPrimarySortCondition(String fieldName) { +// return SortBuilders.fieldSort(fieldName).order(SortOrder.DESC); +// } +// +// private SortBuilder getSortCondition(TechArticleSort techArticleSort) { +// return techArticleSort.getSortCondition(); +// } +// +// private static TechArticleSort getValidSort(TechArticleSort techArticleSort) { +// return Optional.ofNullable(techArticleSort) +// .filter(sort -> sort != TechArticleSort.HIGHEST_SCORE) +// .orElse(TechArticleSort.LATEST); +// } +// +// private static TechArticleSort getValidSortWhenSearch(TechArticleSort techArticleSort) { +// return Optional.ofNullable(techArticleSort).orElse(TechArticleSort.HIGHEST_SCORE); +// } +// +// private void setSearchAfterCondition(String elasticId, TechArticleSort techArticleSort, +// NativeSearchQuery searchQuery) { +// if (!StringUtils.hasText(elasticId)) { +// return; +// } +// +// ElasticTechArticle elasticTechArticle = elasticTechArticleRepository.findById(elasticId) +// .orElseThrow(() -> new NotFoundException(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE)); +// +// searchQuery.setSearchAfter(getSearchAfter(elasticTechArticle, techArticleSort)); +// } +// +// private void setSearchAfterConditionWhenSearch(String elasticId, Float score, TechArticleSort techArticleSort, +// NativeSearchQuery searchQuery) { +// if (!StringUtils.hasText(elasticId)) { +// return; +// } +// +// ElasticTechArticle elasticTechArticle = elasticTechArticleRepository.findById(elasticId) +// .orElseThrow(() -> new NotFoundException(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE)); +// +// searchQuery.setSearchAfter(getSearchAfterWhenSearch(elasticTechArticle, techArticleSort, score)); +// } +// +// private List getSearchAfter(ElasticTechArticle elasticTechArticle, TechArticleSort techArticleSort) { +// return List.of(techArticleSort.getSearchAfterCondition(elasticTechArticle), +// TechArticleSort.LATEST.getSearchAfterCondition(elasticTechArticle), +// elasticTechArticle.getId()); +// } +// +// private List getSearchAfterWhenSearch(ElasticTechArticle elasticTechArticle, +// TechArticleSort techArticleSort, +// Float score) { +// // 정확도순 정렬이 아닌 경우 +// if (!TechArticleSort.HIGHEST_SCORE.equals(techArticleSort)) { +// return getSearchAfter(elasticTechArticle, techArticleSort); +// } +// +// if (ObjectUtils.isEmpty(score)) { +// throw new ElasticTechArticleException(NOT_FOUND_CURSOR_SCORE_MESSAGE); +// } +// +// return List.of(score, +// TechArticleSort.LATEST.getSearchAfterCondition(elasticTechArticle), +// elasticTechArticle.getId()); +// } +//} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/MypageController.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/MypageController.java index 50402c69..977c348c 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/MypageController.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/MypageController.java @@ -55,8 +55,8 @@ public ResponseEntity>> getBookmark @RequestParam(required = false) Long techArticleId) { Authentication authentication = AuthenticationMemberUtils.getAuthentication(); - Slice response = memberService.getBookmarkedTechArticles(pageable, techArticleId, - bookmarkSort, authentication); + Slice response = memberService.getBookmarkedTechArticles( + pageable, techArticleId, bookmarkSort, authentication); return ResponseEntity.ok(BasicResponse.success(response)); } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleController.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleController.java index b3749ef3..f7423c9d 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleController.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleController.java @@ -34,15 +34,15 @@ public class TechArticleController { public ResponseEntity>> getTechArticles( @PageableDefault Pageable pageable, @RequestParam(required = false) TechArticleSort techArticleSort, - @RequestParam(required = false) String elasticId, + @RequestParam(required = false) Long techArticleId, @RequestParam(required = false) String keyword, @RequestParam(required = false) Long companyId, - @RequestParam(required = false) Float score) { - + @RequestParam(required = false) Float score + ) { TechArticleService techArticleService = techArticleServiceStrategy.getTechArticleService(); Authentication authentication = AuthenticationMemberUtils.getAuthentication(); - Slice response = techArticleService.getTechArticles(pageable, elasticId, - techArticleSort, keyword, companyId, score, authentication); + Slice response = techArticleService.getTechArticles( + pageable, techArticleId, techArticleSort, keyword, companyId, score, authentication); return ResponseEntity.ok(BasicResponse.success(response)); } @@ -51,8 +51,8 @@ public ResponseEntity>> getTechArti @GetMapping("/articles/{techArticleId}") public ResponseEntity> getTechArticle( @PathVariable Long techArticleId, - @RequestHeader(value = HEADER_ANONYMOUS_MEMBER_ID, required = false) String anonymousMemberId) { - + @RequestHeader(value = HEADER_ANONYMOUS_MEMBER_ID, required = false) String anonymousMemberId + ) { TechArticleService techArticleService = techArticleServiceStrategy.getTechArticleService(); Authentication authentication = AuthenticationMemberUtils.getAuthentication(); TechArticleDetailResponse response = techArticleService.getTechArticle(techArticleId, anonymousMemberId, authentication); @@ -62,8 +62,9 @@ public ResponseEntity> getTechArticle( @Operation(summary = "기술블로그 북마크") @PostMapping("/articles/{techArticleId}/bookmark") - public ResponseEntity> updateBookmark(@PathVariable Long techArticleId) { - + public ResponseEntity> updateBookmark( + @PathVariable Long techArticleId + ) { TechArticleService techArticleService = techArticleServiceStrategy.getTechArticleService(); Authentication authentication = AuthenticationMemberUtils.getAuthentication(); BookmarkResponse response = techArticleService.updateBookmark(techArticleId, authentication); @@ -77,7 +78,6 @@ public ResponseEntity> updateRecomme @PathVariable Long techArticleId, @RequestHeader(value = HEADER_ANONYMOUS_MEMBER_ID, required = false) String anonymousMemberId ) { - TechArticleService techArticleService = techArticleServiceStrategy.getTechArticleService(); Authentication authentication = AuthenticationMemberUtils.getAuthentication(); TechArticleRecommendResponse response = techArticleService.updateRecommend(techArticleId, anonymousMemberId, authentication); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleDetailResponse.java b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleDetailResponse.java index bcf0f84d..ef96a168 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleDetailResponse.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleDetailResponse.java @@ -3,10 +3,10 @@ import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; import java.time.LocalDate; import lombok.Builder; import lombok.Data; +import org.springframework.util.ObjectUtils; import static com.dreamypatisiel.devdevdev.web.dto.util.TechArticleResponseUtils.*; @@ -15,14 +15,13 @@ public class TechArticleDetailResponse { private static final int CONTENTS_MAX_LENGTH = 1000; - public final String elasticId; - public final String thumbnailUrl; - public final String techArticleUrl; public final String title; public final String contents; + public final String author; public final CompanyResponse company; public final LocalDate regDate; - public final String author; + public final String techArticleUrl; + public final String thumbnailUrl; public final Long viewTotalCount; public final Long recommendTotalCount; public final Long commentTotalCount; @@ -31,75 +30,67 @@ public class TechArticleDetailResponse { public final Boolean isRecommended; @Builder - private TechArticleDetailResponse(String elasticId, String thumbnailUrl, String techArticleUrl, String title, - String contents, CompanyResponse company, LocalDate regDate, String author, + private TechArticleDetailResponse(String title, String contents, String author, CompanyResponse company, LocalDate regDate, + String thumbnailUrl, String techArticleUrl, Long viewTotalCount, Long recommendTotalCount, Long commentTotalCount, Long popularScore, Boolean isBookmarked, Boolean isRecommended) { - this.elasticId = elasticId; - this.thumbnailUrl = thumbnailUrl; - this.techArticleUrl = techArticleUrl; this.title = title; this.contents = contents; + this.author = author; this.company = company; this.regDate = regDate; - this.author = author; - this.isBookmarked = isBookmarked; + this.thumbnailUrl = thumbnailUrl; + this.techArticleUrl = techArticleUrl; this.viewTotalCount = viewTotalCount; this.recommendTotalCount = recommendTotalCount; this.commentTotalCount = commentTotalCount; this.popularScore = popularScore; + this.isBookmarked = isBookmarked; this.isRecommended = isRecommended; } - public static TechArticleDetailResponse of(TechArticle techArticle, - ElasticTechArticle elasticTechArticle, - CompanyResponse companyResponse, - AnonymousMember anonymousMember) { + public static TechArticleDetailResponse of(TechArticle techArticle, AnonymousMember anonymousMember) { + CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); return TechArticleDetailResponse.builder() + .title(techArticle.getTitle().getTitle()) + .contents(truncateString(techArticle.getContents(), CONTENTS_MAX_LENGTH)) + .author(techArticle.getAuthor()) + .company(companyResponse) + .regDate(techArticle.getRegDate()) + .thumbnailUrl(techArticle.getThumbnailUrl().getUrl()) + .techArticleUrl(techArticle.getTechArticleUrl().getUrl()) .viewTotalCount(techArticle.getViewTotalCount().getCount()) .recommendTotalCount(techArticle.getRecommendTotalCount().getCount()) .commentTotalCount(techArticle.getCommentTotalCount().getCount()) .popularScore(techArticle.getPopularScore().getCount()) - .elasticId(elasticTechArticle.getId()) - .thumbnailUrl(elasticTechArticle.getThumbnailUrl()) - .techArticleUrl(elasticTechArticle.getTechArticleUrl()) - .title(elasticTechArticle.getTitle()) - .company(companyResponse) - .regDate(elasticTechArticle.getRegDate()) - .author(elasticTechArticle.getAuthor()) - .contents(truncateString(elasticTechArticle.getContents(), CONTENTS_MAX_LENGTH)) - .isBookmarked(false) .isRecommended(isRecommendedByAnonymousMember(techArticle, anonymousMember)) + .isBookmarked(false) .build(); } - public static TechArticleDetailResponse of(TechArticle techArticle, - ElasticTechArticle elasticTechArticle, - CompanyResponse companyResponse, - Member member) { + public static TechArticleDetailResponse of(TechArticle techArticle, Member member) { + CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); return TechArticleDetailResponse.builder() + .title(techArticle.getTitle().getTitle()) + .contents(truncateString(techArticle.getContents(), CONTENTS_MAX_LENGTH)) + .author(techArticle.getAuthor()) + .company(companyResponse) + .regDate(techArticle.getRegDate()) + .thumbnailUrl(techArticle.getThumbnailUrl().getUrl()) + .techArticleUrl(techArticle.getTechArticleUrl().getUrl()) .viewTotalCount(techArticle.getViewTotalCount().getCount()) .recommendTotalCount(techArticle.getRecommendTotalCount().getCount()) .commentTotalCount(techArticle.getCommentTotalCount().getCount()) .popularScore(techArticle.getPopularScore().getCount()) - .elasticId(elasticTechArticle.getId()) - .thumbnailUrl(elasticTechArticle.getThumbnailUrl()) - .techArticleUrl(elasticTechArticle.getTechArticleUrl()) - .title(elasticTechArticle.getTitle()) - .company(companyResponse) - .regDate(elasticTechArticle.getRegDate()) - .author(elasticTechArticle.getAuthor()) - .contents(truncateString(elasticTechArticle.getContents(), CONTENTS_MAX_LENGTH)) - .isBookmarked(isBookmarkedByMember(techArticle, member)) .isRecommended(isRecommendedByMember(techArticle, member)) + .isBookmarked(isBookmarkedByMember(techArticle, member)) .build(); } - private static String truncateString(String elasticTechArticleContents, int maxLength) { - if (elasticTechArticleContents.length() <= maxLength) { - return elasticTechArticleContents; + private static String truncateString(String contents, int maxLength) { + if (ObjectUtils.isEmpty(contents) || contents.length() <= maxLength) { + return contents; } - - return elasticTechArticleContents.substring(0, maxLength); + return contents.substring(0, maxLength); } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java index 10d54dcf..7c7aac65 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java @@ -1,174 +1,158 @@ package com.dreamypatisiel.devdevdev.web.dto.response.techArticle; -import static com.dreamypatisiel.devdevdev.web.dto.util.TechArticleResponseUtils.isBookmarkedByMember; - import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import java.time.LocalDate; -import java.util.Objects; import lombok.Builder; import lombok.Data; import org.springframework.util.ObjectUtils; +import java.time.LocalDate; +import java.util.Objects; + +import static com.dreamypatisiel.devdevdev.web.dto.util.TechArticleResponseUtils.isBookmarkedByMember; + @Data public class TechArticleMainResponse { public static final int CONTENTS_MAX_LENGTH = 1000; public final Long id; - public final String elasticId; - public final String thumbnailUrl; - public final Boolean isLogoImage; - public final String techArticleUrl; public final String title; public final String contents; + public final String author; public final CompanyResponse company; public final LocalDate regDate; - public final String author; + public final String techArticleUrl; + public final String thumbnailUrl; public final Long viewTotalCount; public final Long recommendTotalCount; public final Long commentTotalCount; public final Long popularScore; + public final Boolean isLogoImage; public final Boolean isBookmarked; public final Float score; @Builder - private TechArticleMainResponse(Long id, String elasticId, String thumbnailUrl, Boolean isLogoImage, String techArticleUrl, String title, - String contents, - CompanyResponse company, LocalDate regDate, String author, Long viewTotalCount, - Long recommendTotalCount, Long commentTotalCount, Long popularScore, - Boolean isBookmarked, Float score) { + private TechArticleMainResponse(Long id, String title, String contents, String author, CompanyResponse company, + LocalDate regDate, String thumbnailUrl, String techArticleUrl, + Long viewTotalCount, Long recommendTotalCount, Long commentTotalCount, Long popularScore, + Boolean isLogoImage, Boolean isBookmarked, Float score) { this.id = id; - this.elasticId = elasticId; - this.thumbnailUrl = thumbnailUrl; - this.isLogoImage = isLogoImage; - this.techArticleUrl = techArticleUrl; this.title = title; this.contents = contents; + this.author = author; this.company = company; this.regDate = regDate; - this.author = author; - this.isBookmarked = isBookmarked; + this.techArticleUrl = techArticleUrl; + this.thumbnailUrl = thumbnailUrl; this.viewTotalCount = viewTotalCount; this.recommendTotalCount = recommendTotalCount; this.commentTotalCount = commentTotalCount; this.popularScore = popularScore; + this.isLogoImage = isLogoImage; + this.isBookmarked = isBookmarked; this.score = score; } - public static TechArticleMainResponse of(TechArticle techArticle, - ElasticTechArticle elasticTechArticle, - CompanyResponse companyResponse) { + + public static TechArticleMainResponse of(TechArticle techArticle, Member member) { + CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); return TechArticleMainResponse.builder() .id(techArticle.getId()) + .title(techArticle.getTitle().getTitle()) + .contents(truncateString(techArticle.getContents(), CONTENTS_MAX_LENGTH)) + .author(techArticle.getAuthor()) + .company(companyResponse) + .regDate(techArticle.getRegDate()) + .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl().getUrl(), companyResponse)) + .techArticleUrl(techArticle.getTechArticleUrl().getUrl()) .viewTotalCount(techArticle.getViewTotalCount().getCount()) .recommendTotalCount(techArticle.getRecommendTotalCount().getCount()) .commentTotalCount(techArticle.getCommentTotalCount().getCount()) .popularScore(techArticle.getPopularScore().getCount()) - .elasticId(elasticTechArticle.getId()) - .thumbnailUrl(getThumbnailUrl(elasticTechArticle, companyResponse)) - .isLogoImage(ObjectUtils.isEmpty(elasticTechArticle.getThumbnailUrl())) - .techArticleUrl(elasticTechArticle.getTechArticleUrl()) - .title(elasticTechArticle.getTitle()) - .company(companyResponse) - .regDate(elasticTechArticle.getRegDate()) - .author(elasticTechArticle.getAuthor()) - .contents(truncateString(elasticTechArticle.getContents(), CONTENTS_MAX_LENGTH)) - .isBookmarked(false) + .isLogoImage(ObjectUtils.isEmpty(techArticle.getThumbnailUrl().getUrl())) + .isBookmarked(isBookmarkedByMember(techArticle, member)) .build(); } - public static TechArticleMainResponse of(TechArticle techArticle, - ElasticTechArticle elasticTechArticle, - CompanyResponse companyResponse, - Member member) { + public static TechArticleMainResponse of(TechArticle techArticle) { + CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); return TechArticleMainResponse.builder() .id(techArticle.getId()) + .title(techArticle.getTitle().getTitle()) + .contents(truncateString(techArticle.getContents(), CONTENTS_MAX_LENGTH)) + .author(techArticle.getAuthor()) + .company(companyResponse) + .regDate(techArticle.getRegDate()) + .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl().getUrl(), companyResponse)) + .techArticleUrl(techArticle.getTechArticleUrl().getUrl()) .viewTotalCount(techArticle.getViewTotalCount().getCount()) .recommendTotalCount(techArticle.getRecommendTotalCount().getCount()) .commentTotalCount(techArticle.getCommentTotalCount().getCount()) .popularScore(techArticle.getPopularScore().getCount()) - .elasticId(elasticTechArticle.getId()) - .thumbnailUrl(getThumbnailUrl(elasticTechArticle, companyResponse)) - .isLogoImage(ObjectUtils.isEmpty(elasticTechArticle.getThumbnailUrl())) - .techArticleUrl(elasticTechArticle.getTechArticleUrl()) - .title(elasticTechArticle.getTitle()) - .company(companyResponse) - .regDate(elasticTechArticle.getRegDate()) - .author(elasticTechArticle.getAuthor()) - .contents(truncateString(elasticTechArticle.getContents(), CONTENTS_MAX_LENGTH)) - .isBookmarked(isBookmarkedByMember(techArticle, member)) + .isLogoImage(ObjectUtils.isEmpty(techArticle.getThumbnailUrl().getUrl())) + .isBookmarked(false) .build(); } - public static TechArticleMainResponse of(TechArticle techArticle, - ElasticTechArticle elasticTechArticle, - CompanyResponse companyResponse, - Float score) { + public static TechArticleMainResponse of(TechArticle techArticle, Member member, Float score) { + CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); return TechArticleMainResponse.builder() .id(techArticle.getId()) + .title(techArticle.getTitle().getTitle()) + .contents(truncateString(techArticle.getContents(), CONTENTS_MAX_LENGTH)) + .author(techArticle.getAuthor()) + .company(companyResponse) + .regDate(techArticle.getRegDate()) + .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl().getUrl(), companyResponse)) + .techArticleUrl(techArticle.getTechArticleUrl().getUrl()) .viewTotalCount(techArticle.getViewTotalCount().getCount()) .recommendTotalCount(techArticle.getRecommendTotalCount().getCount()) .commentTotalCount(techArticle.getCommentTotalCount().getCount()) .popularScore(techArticle.getPopularScore().getCount()) - .elasticId(elasticTechArticle.getId()) - .thumbnailUrl(getThumbnailUrl(elasticTechArticle, companyResponse)) - .isLogoImage(ObjectUtils.isEmpty(elasticTechArticle.getThumbnailUrl())) - .techArticleUrl(elasticTechArticle.getTechArticleUrl()) - .title(elasticTechArticle.getTitle()) - .contents(truncateString(elasticTechArticle.getContents(), CONTENTS_MAX_LENGTH)) - .company(companyResponse) - .regDate(elasticTechArticle.getRegDate()) - .author(elasticTechArticle.getAuthor()) - .isBookmarked(false) + .isLogoImage(ObjectUtils.isEmpty(techArticle.getThumbnailUrl().getUrl())) + .isBookmarked(isBookmarkedByMember(techArticle, member)) .score(getValidScore(score)) .build(); } - public static TechArticleMainResponse of(TechArticle techArticle, - ElasticTechArticle elasticTechArticle, - CompanyResponse companyResponse, - Float score, - Member member) { + public static TechArticleMainResponse of(TechArticle techArticle, Float score) { + CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); return TechArticleMainResponse.builder() .id(techArticle.getId()) + .title(techArticle.getTitle().getTitle()) + .contents(truncateString(techArticle.getContents(), CONTENTS_MAX_LENGTH)) + .author(techArticle.getAuthor()) + .company(companyResponse) + .regDate(techArticle.getRegDate()) + .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl().getUrl(), companyResponse)) + .techArticleUrl(techArticle.getTechArticleUrl().getUrl()) .viewTotalCount(techArticle.getViewTotalCount().getCount()) .recommendTotalCount(techArticle.getRecommendTotalCount().getCount()) .commentTotalCount(techArticle.getCommentTotalCount().getCount()) .popularScore(techArticle.getPopularScore().getCount()) - .elasticId(elasticTechArticle.getId()) - .thumbnailUrl(getThumbnailUrl(elasticTechArticle, companyResponse)) - .isLogoImage(ObjectUtils.isEmpty(elasticTechArticle.getThumbnailUrl())) - .techArticleUrl(elasticTechArticle.getTechArticleUrl()) - .title(elasticTechArticle.getTitle()) - .contents(truncateString(elasticTechArticle.getContents(), CONTENTS_MAX_LENGTH)) - .company(companyResponse) - .regDate(elasticTechArticle.getRegDate()) - .author(elasticTechArticle.getAuthor()) + .isLogoImage(ObjectUtils.isEmpty(techArticle.getThumbnailUrl().getUrl())) + .isBookmarked(false) .score(getValidScore(score)) - .isBookmarked(isBookmarkedByMember(techArticle, member)) .build(); } - private static String getThumbnailUrl(ElasticTechArticle elasticTechArticle, CompanyResponse companyResponse) { + private static String getThumbnailUrl(String thumbnailUrl, CompanyResponse companyResponse) { // 썸네일 이미지가 없다면 회사 로고로 내려준다. - if (ObjectUtils.isEmpty(elasticTechArticle.getThumbnailUrl())) { + if (ObjectUtils.isEmpty(thumbnailUrl)) { return companyResponse.getOfficialImageUrl(); } - - return elasticTechArticle.getThumbnailUrl(); + return thumbnailUrl; } private static Float getValidScore(Float score) { return Objects.isNull(score) || Float.isNaN(score) ? null : score; } - private static String truncateString(String elasticTechArticleContents, int maxLength) { - if (elasticTechArticleContents.length() <= maxLength) { - return elasticTechArticleContents; + private static String truncateString(String contents, int maxLength) { + if (ObjectUtils.isEmpty(contents) || contents.length() <= maxLength) { + return contents; } - - return elasticTechArticleContents.substring(0, maxLength); + return contents.substring(0, maxLength); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b4129870..0d4d76a7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -6,5 +6,4 @@ spring: - "oauth2-${spring.profiles.active}" - "jwt-${spring.profiles.active}" - "storage-s3-${spring.profiles.active}" - - "opensearch-${spring.profiles.active}" - open-ai \ No newline at end of file From a0443ef19e91efe18b85cbd3620735bd818efda3 Mon Sep 17 00:00:00 2001 From: soyoung Date: Wed, 20 Aug 2025 22:11:13 +0900 Subject: [PATCH 56/66] =?UTF-8?q?fix:=20=EA=B8=B0=EC=88=A0=EB=B8=94?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devdevdev/LocalInitData.java | 39 ++++++--- .../custom/TechArticleRepositoryImpl.java | 85 ++++++++++++++++++- .../TechArticleDetailResponse.java | 13 ++- .../techArticle/TechArticleMainResponse.java | 23 ++--- 4 files changed, 129 insertions(+), 31 deletions(-) diff --git a/src/main/java/com/dreamypatisiel/devdevdev/LocalInitData.java b/src/main/java/com/dreamypatisiel/devdevdev/LocalInitData.java index 97d552cb..ef812952 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/LocalInitData.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/LocalInitData.java @@ -20,9 +20,9 @@ import com.dreamypatisiel.devdevdev.domain.repository.pick.PickRepository; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickVoteRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; + +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -66,7 +66,6 @@ public class LocalInitData { private final PickPopularScorePolicy pickPopularScorePolicy; private final TechArticleRepository techArticleRepository; private final BookmarkRepository bookmarkRepository; - private final ElasticTechArticleRepository elasticTechArticleRepository; private final CompanyRepository companyRepository; private final MemberNicknameDictionaryRepository memberNicknameDictionaryRepository; private final BlameTypeRepository blameTypeRepository; @@ -93,10 +92,9 @@ public void dataInsert() { pickVoteRepository.saveAll(pickVotes); List companies = createCompanies(); - List savedCompanies = companyRepository.saveAll(companies); + companyRepository.saveAll(companies); - Map companyIdMap = getCompanyIdMap(savedCompanies); - List techArticles = createTechArticles(companyIdMap); + List techArticles = createTechArticles(companies); techArticleRepository.saveAll(techArticles); List bookmarks = createBookmarks(member, techArticles); @@ -205,15 +203,12 @@ private List createBookmarks(Member member, List techArti return bookmarks; } - private List createTechArticles(Map companyIdMap) { + private List createTechArticles(List companies) { List techArticles = new ArrayList<>(); - Iterable elasticTechArticles = elasticTechArticleRepository.findTop10By(); - for (ElasticTechArticle elasticTechArticle : elasticTechArticles) { - Company company = companyIdMap.get(elasticTechArticle.getCompanyId()); - if (company != null) { - TechArticle techArticle = TechArticle.createTechArticle(elasticTechArticle, company); - techArticles.add(techArticle); - } + for (int i = 0; i < companies.size(); i++) { + Company company = companies.get(i); + TechArticle techArticle = createTechArticle(i, company); + techArticles.add(techArticle); } return techArticles; } @@ -363,4 +358,20 @@ private List createBlameTypes() { private BlameType createBlameType(String reason, int sortOrder) { return new BlameType(reason, sortOrder); } + + private TechArticle createTechArticle(int i, Company company) { + return TechArticle.builder() + .title(new Title("타이틀 " + i)) + .contents("내용 " + i) + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(i)) + .recommendTotalCount(new Count(i)) + .viewTotalCount(new Count(i)) + .popularScore(new Count(i)) + .build(); + } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java index 1c7a9054..100fbcad 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java @@ -11,8 +11,10 @@ import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberTemplate; import com.querydsl.jpa.JPQLQueryFactory; -import jakarta.persistence.EntityManager; import java.util.List; import java.util.Optional; @@ -26,6 +28,7 @@ @RequiredArgsConstructor public class TechArticleRepositoryImpl implements TechArticleRepositoryCustom { + public static final String MATCH_AGAINST_FUNCTION = "match_against"; private final JPQLQueryFactory query; @Override @@ -62,11 +65,45 @@ private SliceCustom findTechArticlesByCursorWithKeyword(Pageable pa TechArticleSort techArticleSort, Long companyId, String keyword, Float score ) { - List contents = null; - - // 기술블로그 총 갯수 + // FULLTEXT 검색 조건 생성 + BooleanExpression titleMatch = Expressions.booleanTemplate( + "function('" + MATCH_AGAINST_FUNCTION + "', {0}, {1}) > 0.0", + techArticle.title.title, keyword + ); + + BooleanExpression contentsMatch = Expressions.booleanTemplate( + "function('" + MATCH_AGAINST_FUNCTION + "', {0}, {1}) > 0.0", + techArticle.contents, keyword + ); + + // 스코어 계산을 위한 expression + NumberTemplate titleScore = Expressions.numberTemplate(Double.class, + "function('" + MATCH_AGAINST_FUNCTION + "', {0}, {1})", + techArticle.title.title, keyword + ); + NumberTemplate contentsScore = Expressions.numberTemplate(Double.class, + "function('" + MATCH_AGAINST_FUNCTION + "', {0}, {1})", + techArticle.contents, keyword + ); + + // 전체 스코어 계산 (제목 가중치 2배) + NumberTemplate totalScore = Expressions.numberTemplate(Double.class, + "({0} * 2.0) + {1}", titleScore, contentsScore + ); + + List contents = query.selectFrom(techArticle) + .where(titleMatch.or(contentsMatch)) + .where(companyId != null ? techArticle.company.id.eq(companyId) : null) + .where(getCursorConditionForKeywordSearch(techArticleSort, techArticleId, score, totalScore)) + .orderBy(getOrderSpecifierForKeywordSearch(techArticleSort, totalScore), techArticle.id.desc()) + .limit(pageable.getPageSize()) + .fetch(); + + // 키워드 검색 결과 총 갯수 long totalElements = query.select(techArticle.count()) .from(techArticle) + .where(titleMatch.or(contentsMatch)) + .where(companyId != null ? techArticle.company.id.eq(companyId) : null) .fetchCount(); return new SliceCustom<>(contents, pageable, totalElements); @@ -148,6 +185,46 @@ private OrderSpecifier techArticleSort(TechArticleSort techArticleSort) { } + // 키워드 검색을 위한 커서 조건 생성 + private Predicate getCursorConditionForKeywordSearch(TechArticleSort techArticleSort, Long techArticleId, + Float score, NumberTemplate totalScore) { + if (ObjectUtils.isEmpty(techArticleId) || ObjectUtils.isEmpty(score)) { + return null; + } + + // HIGHEST_SCORE(정확도순)인 경우 스코어 기반 커서 사용 + if (techArticleSort == TechArticleSort.HIGHEST_SCORE) { + return totalScore.lt(score.doubleValue()) + .or(totalScore.eq(score.doubleValue()) + .and(techArticle.id.lt(techArticleId))); + } + + // 다른 정렬 방식인 경우 기존 커서 조건 사용 + TechArticle findTechArticle = query.selectFrom(techArticle) + .where(techArticle.id.eq(techArticleId)) + .fetchOne(); + + if (ObjectUtils.isEmpty(findTechArticle)) { + return techArticle.id.loe(techArticleId); + } + + return Optional.ofNullable(techArticleSort) + .orElse(TechArticleSort.HIGHEST_SCORE).getCursorCondition(findTechArticle); + } + + // 키워드 검색을 위한 정렬 조건 생성 + private OrderSpecifier getOrderSpecifierForKeywordSearch(TechArticleSort techArticleSort, + NumberTemplate totalScore) { + // HIGHEST_SCORE(정확도순)인 경우 스코어 기반 정렬 + if (techArticleSort == TechArticleSort.HIGHEST_SCORE) { + return totalScore.desc(); + } + + // 다른 정렬 방식인 경우 기존 정렬 사용 + return Optional.ofNullable(techArticleSort) + .orElse(TechArticleSort.HIGHEST_SCORE).getOrderSpecifierByTechArticleSort(); + } + private boolean hasNextPage(List contents, int pageSize) { return contents.size() >= pageSize; } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleDetailResponse.java b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleDetailResponse.java index ef96a168..d7f73b61 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleDetailResponse.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleDetailResponse.java @@ -4,6 +4,8 @@ import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; import java.time.LocalDate; + +import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; import lombok.Builder; import lombok.Data; import org.springframework.util.ObjectUtils; @@ -57,7 +59,7 @@ public static TechArticleDetailResponse of(TechArticle techArticle, AnonymousMem .author(techArticle.getAuthor()) .company(companyResponse) .regDate(techArticle.getRegDate()) - .thumbnailUrl(techArticle.getThumbnailUrl().getUrl()) + .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl())) .techArticleUrl(techArticle.getTechArticleUrl().getUrl()) .viewTotalCount(techArticle.getViewTotalCount().getCount()) .recommendTotalCount(techArticle.getRecommendTotalCount().getCount()) @@ -76,7 +78,7 @@ public static TechArticleDetailResponse of(TechArticle techArticle, Member membe .author(techArticle.getAuthor()) .company(companyResponse) .regDate(techArticle.getRegDate()) - .thumbnailUrl(techArticle.getThumbnailUrl().getUrl()) + .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl())) .techArticleUrl(techArticle.getTechArticleUrl().getUrl()) .viewTotalCount(techArticle.getViewTotalCount().getCount()) .recommendTotalCount(techArticle.getRecommendTotalCount().getCount()) @@ -93,4 +95,11 @@ private static String truncateString(String contents, int maxLength) { } return contents.substring(0, maxLength); } + + private static String getThumbnailUrl(Url thumbnailUrl) { + if (ObjectUtils.isEmpty(thumbnailUrl)) { + return null; + } + return thumbnailUrl.getUrl(); + } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java index 7c7aac65..a4eb9d2b 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java @@ -2,6 +2,7 @@ import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; import lombok.Builder; import lombok.Data; import org.springframework.util.ObjectUtils; @@ -64,13 +65,13 @@ public static TechArticleMainResponse of(TechArticle techArticle, Member member) .author(techArticle.getAuthor()) .company(companyResponse) .regDate(techArticle.getRegDate()) - .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl().getUrl(), companyResponse)) + .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl(), companyResponse)) .techArticleUrl(techArticle.getTechArticleUrl().getUrl()) .viewTotalCount(techArticle.getViewTotalCount().getCount()) .recommendTotalCount(techArticle.getRecommendTotalCount().getCount()) .commentTotalCount(techArticle.getCommentTotalCount().getCount()) .popularScore(techArticle.getPopularScore().getCount()) - .isLogoImage(ObjectUtils.isEmpty(techArticle.getThumbnailUrl().getUrl())) + .isLogoImage(ObjectUtils.isEmpty(techArticle.getThumbnailUrl())) .isBookmarked(isBookmarkedByMember(techArticle, member)) .build(); } @@ -84,13 +85,13 @@ public static TechArticleMainResponse of(TechArticle techArticle) { .author(techArticle.getAuthor()) .company(companyResponse) .regDate(techArticle.getRegDate()) - .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl().getUrl(), companyResponse)) + .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl(), companyResponse)) .techArticleUrl(techArticle.getTechArticleUrl().getUrl()) .viewTotalCount(techArticle.getViewTotalCount().getCount()) .recommendTotalCount(techArticle.getRecommendTotalCount().getCount()) .commentTotalCount(techArticle.getCommentTotalCount().getCount()) .popularScore(techArticle.getPopularScore().getCount()) - .isLogoImage(ObjectUtils.isEmpty(techArticle.getThumbnailUrl().getUrl())) + .isLogoImage(ObjectUtils.isEmpty(techArticle.getThumbnailUrl())) .isBookmarked(false) .build(); } @@ -104,13 +105,13 @@ public static TechArticleMainResponse of(TechArticle techArticle, Member member, .author(techArticle.getAuthor()) .company(companyResponse) .regDate(techArticle.getRegDate()) - .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl().getUrl(), companyResponse)) + .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl(), companyResponse)) .techArticleUrl(techArticle.getTechArticleUrl().getUrl()) .viewTotalCount(techArticle.getViewTotalCount().getCount()) .recommendTotalCount(techArticle.getRecommendTotalCount().getCount()) .commentTotalCount(techArticle.getCommentTotalCount().getCount()) .popularScore(techArticle.getPopularScore().getCount()) - .isLogoImage(ObjectUtils.isEmpty(techArticle.getThumbnailUrl().getUrl())) + .isLogoImage(ObjectUtils.isEmpty(techArticle.getThumbnailUrl())) .isBookmarked(isBookmarkedByMember(techArticle, member)) .score(getValidScore(score)) .build(); @@ -125,24 +126,24 @@ public static TechArticleMainResponse of(TechArticle techArticle, Float score) { .author(techArticle.getAuthor()) .company(companyResponse) .regDate(techArticle.getRegDate()) - .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl().getUrl(), companyResponse)) + .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl(), companyResponse)) .techArticleUrl(techArticle.getTechArticleUrl().getUrl()) .viewTotalCount(techArticle.getViewTotalCount().getCount()) .recommendTotalCount(techArticle.getRecommendTotalCount().getCount()) .commentTotalCount(techArticle.getCommentTotalCount().getCount()) .popularScore(techArticle.getPopularScore().getCount()) - .isLogoImage(ObjectUtils.isEmpty(techArticle.getThumbnailUrl().getUrl())) + .isLogoImage(ObjectUtils.isEmpty(techArticle.getThumbnailUrl())) .isBookmarked(false) .score(getValidScore(score)) .build(); } - private static String getThumbnailUrl(String thumbnailUrl, CompanyResponse companyResponse) { + private static String getThumbnailUrl(Url thumbnailUrl, CompanyResponse companyResponse) { // 썸네일 이미지가 없다면 회사 로고로 내려준다. - if (ObjectUtils.isEmpty(thumbnailUrl)) { + if (ObjectUtils.isEmpty(thumbnailUrl) || thumbnailUrl.getUrl() == null) { return companyResponse.getOfficialImageUrl(); } - return thumbnailUrl; + return thumbnailUrl.getUrl(); } private static Float getValidScore(Float score) { From cc3726e6b8e20769661d95cac27cb1223846a759 Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sun, 24 Aug 2025 14:45:52 +0900 Subject: [PATCH 57/66] =?UTF-8?q?feat(MemberService):=20findMyPickMain?= =?UTF-8?q?=EC=97=90=20totalElements=20=ED=95=84=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/pick/PickRepository.java | 2 + .../domain/service/member/MemberService.java | 29 ++++-- .../web/docs/MyPageControllerDocsTest.java | 96 ++++++++++++------- 3 files changed, 85 insertions(+), 42 deletions(-) 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/service/member/MemberService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberService.java index 5aca6bbe..eeb92828 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,6 +1,15 @@ 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.repository.CompanyRepository; import com.dreamypatisiel.devdevdev.domain.repository.comment.CommentRepository; @@ -29,6 +38,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; @@ -37,14 +51,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.MEMBER_INCOMPLETE_SURVEY_MESSAGE; - @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -101,12 +107,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); } /** 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("총 데이터 갯수") ) )); } From bcf6da6da92528fbefede8baf9d9362611e43a04 Mon Sep 17 00:00:00 2001 From: soyoung Date: Mon, 25 Aug 2025 18:54:53 +0900 Subject: [PATCH 58/66] =?UTF-8?q?fix:=20=EA=B8=B0=EC=88=A0=EB=B8=94?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ElasticTechArticleException.java | 14 +- .../TechArticleRepositoryTest.java | 33 +- .../service/member/MemberServiceTest.java | 37 +- .../notification/NotificationServiceTest.java | 88 +- .../GuestTechArticleServiceTest.java | 221 ++- .../GuestTechCommentServiceTest.java | 57 +- .../MemberTechArticleServiceTest.java | 135 +- .../MemberTechCommentServiceTest.java | 128 +- .../TechArticleServiceStrategyTest.java | 8 +- .../GuestTechCommentServiceV2Test.java | 73 +- .../elastic/config/ContainerExtension.java | 86 +- .../service/ElasticKeywordServiceTest.java | 330 ++-- .../ElasticTechArticleServiceTest.java | 1664 ++++++++--------- .../service/ElasticsearchSupportTest.java | 208 +-- .../member/MyPageControllerTest.java | 72 +- .../NotificationControllerTest.java | 6 +- .../TechArticleCommentControllerTest.java | 86 +- .../TechArticleControllerTest.java | 178 +- .../web/docs/MyPageControllerDocsTest.java | 73 +- .../docs/NotificationControllerDocsTest.java | 6 +- .../TechArticleCommentControllerDocsTest.java | 72 +- .../docs/TechArticleControllerDocsTest.java | 187 +- .../TechArticleMainResponseTest.java | 65 +- 23 files changed, 1727 insertions(+), 2100 deletions(-) diff --git a/src/main/java/com/dreamypatisiel/devdevdev/exception/ElasticTechArticleException.java b/src/main/java/com/dreamypatisiel/devdevdev/exception/ElasticTechArticleException.java index b6af9703..8d4a5c31 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/exception/ElasticTechArticleException.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/exception/ElasticTechArticleException.java @@ -1,7 +1,7 @@ -package com.dreamypatisiel.devdevdev.exception; - -public class ElasticTechArticleException extends IllegalArgumentException { - public ElasticTechArticleException(String s) { - super(s); - } -} +//package com.dreamypatisiel.devdevdev.exception; +// +//public class ElasticTechArticleException extends IllegalArgumentException { +// public ElasticTechArticleException(String s) { +// super(s); +// } +//} diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleRepositoryTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleRepositoryTest.java index 7980c0fc..7cd3834c 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleRepositoryTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleRepositoryTest.java @@ -39,31 +39,6 @@ class TechArticleRepositoryTest { @Autowired EntityManager em; - @Test - @DisplayName("elasticIds 리스트의 elasticId에 해당하는 기술블로그 엔티티를 순서대로 가져올 수 있다.") - void findAllByElasticIdIn() { - // given - Company company = Company.builder().name(new CompanyName("회사")).build(); - companyRepository.save(company); - - TechArticle techArticle1 = createTechArticle(company, "elasticId1"); - TechArticle techArticle2 = createTechArticle(company, "elasticId2"); - TechArticle techArticle3 = createTechArticle(company, "elasticId3"); - TechArticle techArticle4 = createTechArticle(company, "elasticId4"); - - techArticleRepository.saveAll(List.of(techArticle1, techArticle2, techArticle3, techArticle4)); - - List elasticIds = List.of("elasticId1", "elasticId3", "elasticId2"); - - // when - List techArticles = techArticleRepository.findAllByElasticIdIn(elasticIds); - - // then - assertThat(techArticles).hasSize(3) - .extracting(TechArticle::getElasticId) - .containsExactly("elasticId1", "elasticId3", "elasticId2"); - } - @Test @DisplayName("기술블로그 북마크 목록을 북마크 등록시간 내림차순으로 가져올 수 있다.") void findBookmarkedByCursorOrderByBookmarkedDesc() { @@ -163,12 +138,8 @@ void findBookmarkedByCursorOrderByCommentDesc() { .containsExactly(techArticle1, techArticle3); } - private static TechArticle createTechArticle(Company company, String elasticId) { - return TechArticle.builder() - .company(company) - .elasticId(elasticId) - .build(); - } + // TODO + // 기술블로그 조회 관련 테스트코드 private static TechArticle createTechArticle(Company company, Count commentTotalCount) { return TechArticle.builder() diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberServiceTest.java index 1df770b8..20378785 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberServiceTest.java @@ -32,7 +32,6 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.SubscriptionRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.service.ElasticsearchSupportTest; import com.dreamypatisiel.devdevdev.exception.MemberException; import com.dreamypatisiel.devdevdev.exception.NicknameException; import com.dreamypatisiel.devdevdev.exception.SurveyException; @@ -53,6 +52,8 @@ import com.dreamypatisiel.devdevdev.web.dto.response.subscription.SubscribedCompanyResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleMainResponse; import jakarta.persistence.EntityManager; + +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -73,9 +74,11 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.transaction.annotation.Transactional; @SpringBootTest -class MemberServiceTest extends ElasticsearchSupportTest { +@Transactional +class MemberServiceTest { String userId = "dreamy5patisiel"; String name = "꿈빛파티시엘"; @@ -422,7 +425,14 @@ void getBookmarkedTechArticles() { userPrincipal.getSocialType().name())); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - Bookmark bookmark = createBookmark(member, firstTechArticle, true); + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + TechArticle techArticle = createTechArticle(company, "기술블로그 제목"); + techArticleRepository.save(techArticle); + + Bookmark bookmark = createBookmark(member, techArticle, true); bookmarkRepository.save(bookmark); em.flush(); @@ -1314,8 +1324,17 @@ private static TechComment createTechComment(TechArticle techArticle, Member mem private static TechArticle createTechArticle(Company company, String title) { return TechArticle.builder() - .company(company) .title(new Title(title)) + .contents("내용 ") + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/article.png")) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(1)) .build(); } @@ -1432,4 +1451,14 @@ private Bookmark createBookmark(Member member, TechArticle techArticle, boolean .status(status) .build(); } + + public static Company createCompany(String companyName, String officialImageUrl, String officialUrl, + String careerUrl) { + return Company.builder() + .name(new CompanyName(companyName)) + .officialUrl(new Url(officialUrl)) + .careerUrl(new Url(careerUrl)) + .officialImageUrl(new Url(officialImageUrl)) + .build(); + } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/notification/NotificationServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/notification/NotificationServiceTest.java index 5ba792d3..c61e236a 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/notification/NotificationServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/notification/NotificationServiceTest.java @@ -15,8 +15,6 @@ import com.dreamypatisiel.devdevdev.domain.repository.notification.NotificationRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.SubscriptionRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; import com.dreamypatisiel.devdevdev.exception.NotFoundException; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; @@ -72,8 +70,6 @@ class NotificationServiceTest { @Autowired TechArticleRepository techArticleRepository; @Autowired - ElasticTechArticleRepository elasticTechArticleRepository; - @Autowired SubscriptionRepository subscriptionRepository; String userId = "dreamy5patisiel"; @@ -85,9 +81,7 @@ class NotificationServiceTest { String role = Role.ROLE_USER.name(); @AfterAll - static void tearDown(@Autowired ElasticTechArticleRepository elasticTechArticleRepository, - @Autowired TechArticleRepository techArticleRepository) { - elasticTechArticleRepository.deleteAll(); + static void tearDown(@Autowired TechArticleRepository techArticleRepository) { techArticleRepository.deleteAllInBatch(); } @@ -253,9 +247,7 @@ void getNotificationPopup() { List notifications = new ArrayList<>(); for (int i = 0; i < 6; i++) { - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목 "+i), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); - + TechArticle techArticle = createTechArticle(i, company); techArticles.add(techArticle); notifications.add(createNotification(member, "알림 메시지 " + i, NotificationType.SUBSCRIPTION, false, techArticle)); } @@ -326,24 +318,11 @@ void getNotifications() { "https://example.com"); companyRepository.save(company); - - List elasticTechArticles = new ArrayList<>(); - for (int i = 0; i < 10; i++) { - ElasticTechArticle elasticTechArticle = createElasticTechArticle("elasticId" + i, "기술블로그 제목 "+i, - LocalDate.now(), "기술블로그 내용", "https://example.com", "기술블로그 설명", - "https://example.com/thumbnail.png", "작성자", "회사명", company.getId(), - 1L, 1L, 1L, 1L); - elasticTechArticles.add(elasticTechArticle); - } - elasticTechArticleRepository.saveAll(elasticTechArticles); - List techArticles = new ArrayList<>(); List notifications = new ArrayList<>(); for (int i = 0; i < 10; i++) { - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목 "+i), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), elasticTechArticles.get(i).getId(), company); - + TechArticle techArticle = createTechArticle(i, company); techArticles.add(techArticle); notifications.add(createNotification(member, "알림 메시지 " + i, NotificationType.SUBSCRIPTION, false, techArticle)); } @@ -371,7 +350,6 @@ void getNotifications() { assertThat(content).allSatisfy(notificationNewArticleResponse -> { assertThat(notificationNewArticleResponse.getNotificationId()).isInstanceOf(Long.class); assertThat(notificationNewArticleResponse.getTechArticle().getId()).isInstanceOf(Long.class); - assertThat(notificationNewArticleResponse.getTechArticle().getElasticId()).isInstanceOf(String.class); assertThat(notificationNewArticleResponse.getTechArticle().getCompany().getId()).isInstanceOf(Long.class); }); @@ -583,8 +561,7 @@ void getUnreadNotificationCount() { List notifications = new ArrayList<>(); for (int i = 0; i < 10; i++) { - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목 "+i), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticles.add(techArticle); notifications.add(createNotification(member, "알림 메시지 " + i, NotificationType.SUBSCRIPTION, false, techArticle)); @@ -635,8 +612,7 @@ void deleteAllByMember() { List notifications = new ArrayList<>(); for (int i = 0; i < 10; i++) { - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목 "+i), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticles.add(techArticle); notifications.add(createNotification(member, "알림 메시지 " + i, NotificationType.SUBSCRIPTION, false, techArticle)); @@ -669,12 +645,6 @@ private static Member createMember() { .build(); } - private static TechArticle createTechArticle(Company company) { - return TechArticle.builder() - .company(company) - .build(); - } - private static Company createCompany(String name) { return Company.builder() .name(new CompanyName(name)) @@ -734,27 +704,35 @@ private static Company createCompany(String companyName, String officialImageUrl .build(); } - private static ElasticTechArticle createElasticTechArticle(String id, String title, LocalDate regDate, - String contents, String techArticleUrl, - String description, String thumbnailUrl, String author, - String company, Long companyId, - Long viewTotalCount, Long recommendTotalCount, - Long commentTotalCount, Long popularScore) { - return ElasticTechArticle.builder() - .id(id) - .title(title) - .regDate(regDate) - .contents(contents) - .techArticleUrl(techArticleUrl) - .description(description) - .thumbnailUrl(thumbnailUrl) - .author(author) + private static TechArticle createTechArticle(Company company) { + return TechArticle.builder() + .title(new Title("타이틀 ")) + .contents("내용 ") + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(1)) + .build(); + } + + private static TechArticle createTechArticle(int i, Company company) { + return TechArticle.builder() + .title(new Title("기술블로그 제목 " + i)) + .contents("기술블로그 내용") .company(company) - .companyId(companyId) - .viewTotalCount(viewTotalCount) - .recommendTotalCount(recommendTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(popularScore) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com")) + .thumbnailUrl(new Url("https://example.com/thumbnail.png")) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(1)) .build(); } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechArticleServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechArticleServiceTest.java index 4d4080ca..14e7ec77 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechArticleServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechArticleServiceTest.java @@ -1,29 +1,21 @@ package com.dreamypatisiel.devdevdev.domain.service.techArticle; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_ID_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_TECH_ARTICLE_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.GuestTechArticleService.INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; +import com.dreamypatisiel.devdevdev.domain.entity.Company; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; import com.dreamypatisiel.devdevdev.domain.entity.TechArticleRecommend; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.CompanyName; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Count; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Title; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; import com.dreamypatisiel.devdevdev.domain.entity.enums.Role; import com.dreamypatisiel.devdevdev.domain.entity.enums.SocialType; +import com.dreamypatisiel.devdevdev.domain.repository.CompanyRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRecommendRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; import com.dreamypatisiel.devdevdev.domain.service.member.AnonymousMemberService; import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.GuestTechArticleService; -import com.dreamypatisiel.devdevdev.elastic.domain.service.ElasticsearchSupportTest; import com.dreamypatisiel.devdevdev.exception.NotFoundException; -import com.dreamypatisiel.devdevdev.exception.TechArticleException; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleDetailResponse; @@ -34,6 +26,7 @@ import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -42,17 +35,27 @@ 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; -import java.util.List; -import java.util.Optional; +import java.time.LocalDate; -class GuestTechArticleServiceTest extends ElasticsearchSupportTest { +import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_TECH_ARTICLE_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.GuestTechArticleService.INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +@SpringBootTest +@Transactional +class GuestTechArticleServiceTest { @Autowired GuestTechArticleService guestTechArticleService; @Autowired TechArticleRepository techArticleRepository; @Autowired + CompanyRepository companyRepository; + @Autowired TechArticleRecommendRepository techArticleRecommendRepository; @Autowired AnonymousMemberService anonymousMemberService; @@ -109,14 +112,20 @@ void getTechArticlesNotAnonymousUserException() { @DisplayName("익명 사용자가 기술블로그 상세를 조회한다. 이때 북마크 값은 false 이다.") void getTechArticle() { // given - Long id = firstTechArticle.getId(); + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); when(authentication.getPrincipal()).thenReturn("anonymousUser"); when(securityContext.getAuthentication()).thenReturn(authentication); SecurityContextHolder.setContext(securityContext); // when - TechArticleDetailResponse techArticleDetailResponse = guestTechArticleService.getTechArticle(id, null, + TechArticleDetailResponse techArticleDetailResponse = guestTechArticleService.getTechArticle(techArticleId, null, authentication); // then @@ -138,10 +147,17 @@ void getTechArticleWithRecommend() { SecurityContextHolder.setContext(securityContext); String anonymousMemberId = "GA1.1.276672604.1715872960"; - Long techArticleId = firstTechArticle.getId(); + + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); - TechArticleRecommend techArticleRecommend = TechArticleRecommend.create(anonymousMember, firstTechArticle); + TechArticleRecommend techArticleRecommend = TechArticleRecommend.create(anonymousMember, techArticle); techArticleRecommendRepository.save(techArticleRecommend); em.flush(); @@ -164,16 +180,23 @@ void getTechArticleWithRecommend() { @DisplayName("익명 사용자가 기술블로그 상세를 조회하면 조회수가 1 증가한다.") void getTechArticleIncrementViewCount() { // given - Long id = firstTechArticle.getId(); - long prevViewTotalCount = firstTechArticle.getViewTotalCount().getCount(); - long prevPopularScore = firstTechArticle.getPopularScore().getCount(); + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); + + long prevViewTotalCount = techArticle.getViewTotalCount().getCount(); + long prevPopularScore = techArticle.getPopularScore().getCount(); when(authentication.getPrincipal()).thenReturn("anonymousUser"); when(securityContext.getAuthentication()).thenReturn(authentication); SecurityContextHolder.setContext(securityContext); // when - TechArticleDetailResponse techArticleDetailResponse = guestTechArticleService.getTechArticle(id, null, + TechArticleDetailResponse techArticleDetailResponse = guestTechArticleService.getTechArticle(techArticleId, null, authentication); // then @@ -188,7 +211,13 @@ void getTechArticleIncrementViewCount() { @DisplayName("익명 사용자가 기술블로그 상세를 조회할 때 익명 사용자가 아니면 예외가 발생한다.") void getTechArticleNotAnonymousUserException() { // given - Long id = firstTechArticle.getId(); + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); UserPrincipal userPrincipal = UserPrincipal.createByEmailAndRoleAndSocialType(email, role, socialType); SecurityContext context = SecurityContextHolder.getContext(); @@ -197,7 +226,7 @@ void getTechArticleNotAnonymousUserException() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // when // then - assertThatThrownBy(() -> guestTechArticleService.getTechArticle(id, null, authentication)) + assertThatThrownBy(() -> guestTechArticleService.getTechArticle(techArticleId, null, authentication)) .isInstanceOf(IllegalStateException.class) .hasMessage(AuthenticationMemberUtils.INVALID_METHODS_CALL_MESSAGE); } @@ -206,60 +235,14 @@ void getTechArticleNotAnonymousUserException() { @DisplayName("익명 사용자가 기술블로그 상세를 조회할 때 기술블로그가 존재하지 않으면 예외가 발생한다.") void getTechArticleNotFoundTechArticleException() { // given - TechArticle techArticle = TechArticle.createTechArticle( - new Title("매일 1,000만 사용자의 데이터를 꿈파는 어떻게 처리할까?"), - new Url("https://example.com"), new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId() + 1; - - when(authentication.getPrincipal()).thenReturn("anonymousUser"); - when(securityContext.getAuthentication()).thenReturn(authentication); - SecurityContextHolder.setContext(securityContext); - - // when // then - assertThatThrownBy(() -> guestTechArticleService.getTechArticle(id, null, authentication)) - .isInstanceOf(NotFoundException.class) - .hasMessage(NOT_FOUND_TECH_ARTICLE_MESSAGE); - } - - @Test - @DisplayName("익명 사용자가 기술블로그 상세를 조회할 때 엘라스틱ID가 존재하지 않으면 예외가 발생한다.") - void getTechArticleNotFoundElasticIdException() { - // given - TechArticle techArticle = TechArticle.createTechArticle( - new Title("매일 1,000만 사용자의 데이터를 꿈파는 어떻게 처리할까?"), - new Url("https://example.com"), new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); - - when(authentication.getPrincipal()).thenReturn("anonymousUser"); - when(securityContext.getAuthentication()).thenReturn(authentication); - SecurityContextHolder.setContext(securityContext); - - // when // then - assertThatThrownBy(() -> guestTechArticleService.getTechArticle(id, null, authentication)) - .isInstanceOf(TechArticleException.class) - .hasMessage(NOT_FOUND_ELASTIC_ID_MESSAGE); - } + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); - @Test - @DisplayName("익명 사용자가 기술블로그 상세를 조회할 때 엘라스틱 기술블로그가 존재하지 않으면 예외가 발생한다.") - void getTechArticleNotFoundElasticTechArticleException() { - // given - TechArticle techArticle = TechArticle.createTechArticle( - new Title("매일 1,000만 사용자의 데이터를 꿈파는 어떻게 처리할까?"), - new Url("https://example.com"), new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), "elasticId", company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); + Long id = techArticleId + 1; when(authentication.getPrincipal()).thenReturn("anonymousUser"); when(securityContext.getAuthentication()).thenReturn(authentication); @@ -268,7 +251,7 @@ void getTechArticleNotFoundElasticTechArticleException() { // when // then assertThatThrownBy(() -> guestTechArticleService.getTechArticle(id, null, authentication)) .isInstanceOf(NotFoundException.class) - .hasMessage(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE); + .hasMessage(NOT_FOUND_TECH_ARTICLE_MESSAGE); } @Test @@ -279,10 +262,16 @@ void updateBookmarkAccessDeniedException() { when(securityContext.getAuthentication()).thenReturn(authentication); SecurityContextHolder.setContext(securityContext); - Long id = firstTechArticle.getId(); + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); // when // then - assertThatThrownBy(() -> guestTechArticleService.updateBookmark(id, authentication)) + assertThatThrownBy(() -> guestTechArticleService.updateBookmark(techArticleId, authentication)) .isInstanceOf(AccessDeniedException.class) .hasMessage(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); } @@ -297,9 +286,17 @@ void createTechArticleRecommend() { SecurityContextHolder.setContext(securityContext); String anonymousMemberId = "GA1.1.276672604.1715872960"; - Long techArticleId = firstTechArticle.getId(); - Count popularScore = firstTechArticle.getPopularScore(); - Count recommendTotalCount = firstTechArticle.getRecommendTotalCount(); + + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); + + Count popularScore = techArticle.getPopularScore(); + Count recommendTotalCount = techArticle.getRecommendTotalCount(); // when TechArticleRecommendResponse techArticleRecommendResponse = guestTechArticleService.updateRecommend(techArticleId, anonymousMemberId, authentication); @@ -313,18 +310,18 @@ void createTechArticleRecommend() { assertThat(response.getRecommendTotalCount()).isEqualTo(recommendTotalCount.getCount() + 1); }); - TechArticle techArticle = techArticleRepository.findById(techArticleId).get(); - assertThat(techArticle) + TechArticle findTechArticle = techArticleRepository.findById(techArticleId).get(); + assertThat(findTechArticle) .satisfies(article -> { assertThat(article.getRecommendTotalCount().getCount()).isEqualTo(recommendTotalCount.getCount() + 1); assertThat(article.getPopularScore().getCount()).isEqualTo(popularScore.getCount() + 4); }); AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); - TechArticleRecommend techArticleRecommend = techArticleRecommendRepository.findByTechArticleAndAnonymousMember(firstTechArticle, anonymousMember).get(); + TechArticleRecommend techArticleRecommend = techArticleRecommendRepository.findByTechArticleAndAnonymousMember(techArticle, anonymousMember).get(); assertThat(techArticleRecommend) .satisfies(recommend -> { - assertThat(recommend.getTechArticle().getId()).isEqualTo(firstTechArticle.getId()); + assertThat(recommend.getTechArticle().getId()).isEqualTo(techArticle.getId()); assertThat(recommend.getAnonymousMember()).isEqualTo(anonymousMember); assertThat(recommend.isRecommended()).isTrue(); }); @@ -340,12 +337,20 @@ void cancelTechArticleRecommend() { SecurityContextHolder.setContext(securityContext); String anonymousMemberId = "GA1.1.276672604.1715872960"; - Long techArticleId = firstTechArticle.getId(); - Count popularScore = firstTechArticle.getPopularScore(); - Count recommendTotalCount = firstTechArticle.getRecommendTotalCount(); + + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); + + Count popularScore = techArticle.getPopularScore(); + Count recommendTotalCount = techArticle.getRecommendTotalCount(); AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); - TechArticleRecommend techArticleRecommend = TechArticleRecommend.create(anonymousMember, firstTechArticle); + TechArticleRecommend techArticleRecommend = TechArticleRecommend.create(anonymousMember, techArticle); techArticleRecommendRepository.save(techArticleRecommend); // when @@ -362,20 +367,46 @@ void cancelTechArticleRecommend() { assertThat(response.getStatus()).isFalse(); }); - TechArticle techArticle = techArticleRepository.findById(techArticleId).get(); - assertThat(techArticle) + TechArticle findTechArticle = techArticleRepository.findById(techArticleId).get(); + assertThat(findTechArticle) .satisfies(article -> { assertThat(article.getRecommendTotalCount().getCount()).isEqualTo(recommendTotalCount.getCount() - 1L); assertThat(article.getPopularScore().getCount()).isEqualTo(popularScore.getCount() - 4L); }); AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); - TechArticleRecommend findTechArticleRecommend = techArticleRecommendRepository.findByTechArticleAndAnonymousMember(firstTechArticle, findAnonymousMember).get(); + TechArticleRecommend findTechArticleRecommend = techArticleRecommendRepository.findByTechArticleAndAnonymousMember(techArticle, findAnonymousMember).get(); assertThat(findTechArticleRecommend) .satisfies(recommend -> { - assertThat(recommend.getTechArticle().getId()).isEqualTo(firstTechArticle.getId()); + assertThat(recommend.getTechArticle().getId()).isEqualTo(techArticle.getId()); assertThat(recommend.getAnonymousMember()).isEqualTo(findAnonymousMember); assertThat(recommend.isRecommended()).isFalse(); }); } + + private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, + String careerUrl) { + return Company.builder() + .name(new CompanyName(companyName)) + .officialImageUrl(new Url(officialImageUrl)) + .careerUrl(new Url(careerUrl)) + .officialUrl(new Url(officialUrl)) + .build(); + } + + private static TechArticle createTechArticle(Company company) { + return TechArticle.builder() + .title(new Title("타이틀 ")) + .contents("내용 ") + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(10)) + .build(); + } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechCommentServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechCommentServiceTest.java index ac0d746c..cf5a9af9 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechCommentServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechCommentServiceTest.java @@ -41,6 +41,8 @@ import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechRepliedCommentsResponse; import com.dreamypatisiel.devdevdev.web.dto.util.CommonResponseUtil; import jakarta.persistence.EntityManager; + +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import org.assertj.core.groups.Tuple; @@ -107,18 +109,16 @@ void registerTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); RegisterTechCommentRequest registerTechCommentRequest = new RegisterTechCommentRequest("댓글입니다."); TechCommentDto registerCommentDto = TechCommentDto.createRegisterCommentDto(registerTechCommentRequest, null); // when // then assertThatThrownBy(() -> guestTechCommentService.registerMainTechComment( - id, registerCommentDto, authentication)) + techArticleId, registerCommentDto, authentication)) .isInstanceOf(AccessDeniedException.class) .hasMessage(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); } @@ -137,8 +137,7 @@ void registerRepliedTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -171,9 +170,7 @@ void recommendTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(2L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -202,9 +199,7 @@ void getTechCommentsSortByOLDEST() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -496,9 +491,7 @@ void getTechCommentsSortByLATEST() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -665,9 +658,7 @@ void getTechCommentsSortByMostCommented() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -959,9 +950,7 @@ void getTechCommentsSortByMostRecommended() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1107,8 +1096,7 @@ void getTechCommentsByCursor() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1283,8 +1271,7 @@ void findTechBestComments() { companyRepository.save(company); // 기술 블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); // 댓글 생성 @@ -1406,4 +1393,20 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), ) ); } + + private TechArticle createTechArticle(Company company) { + return TechArticle.builder() + .title(new Title("타이틀 ")) + .contents("내용 ") + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(1)) + .build(); + } } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechArticleServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechArticleServiceTest.java index ff70ca5f..5db82475 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechArticleServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechArticleServiceTest.java @@ -1,40 +1,37 @@ package com.dreamypatisiel.devdevdev.domain.service.techArticle; import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.INVALID_MEMBER_NOT_FOUND_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_ID_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_TECH_ARTICLE_MESSAGE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import com.dreamypatisiel.devdevdev.domain.entity.*; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.CompanyName; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Count; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Title; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; import com.dreamypatisiel.devdevdev.domain.entity.enums.Role; import com.dreamypatisiel.devdevdev.domain.entity.enums.SocialType; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkRepository; +import com.dreamypatisiel.devdevdev.domain.repository.CompanyRepository; import com.dreamypatisiel.devdevdev.domain.repository.member.MemberRepository; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRecommendRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.MemberTechArticleService; -import com.dreamypatisiel.devdevdev.elastic.domain.service.ElasticsearchSupportTest; import com.dreamypatisiel.devdevdev.exception.MemberException; import com.dreamypatisiel.devdevdev.exception.NotFoundException; -import com.dreamypatisiel.devdevdev.exception.TechArticleException; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; -import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.BookmarkResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleDetailResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleMainResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleRecommendResponse; import jakarta.persistence.EntityManager; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -42,14 +39,22 @@ 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; -class MemberTechArticleServiceTest extends ElasticsearchSupportTest { +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +@SpringBootTest +@Transactional +class MemberTechArticleServiceTest { @Autowired MemberTechArticleService memberTechArticleService; @Autowired TechArticleRepository techArticleRepository; @Autowired + CompanyRepository companyRepository; + @Autowired MemberRepository memberRepository; @Autowired BookmarkRepository bookmarkRepository; @@ -66,6 +71,26 @@ class MemberTechArticleServiceTest extends ElasticsearchSupportTest { String socialType = SocialType.KAKAO.name(); String role = Role.ROLE_USER.name(); + private static final int TEST_ARTICLES_COUNT = 20; + private static Company company; + private static TechArticle firstTechArticle; + private static List techArticles; + + @BeforeAll + static void setup(@Autowired TechArticleRepository techArticleRepository, + @Autowired CompanyRepository companyRepository) { + company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + List techArticles = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + TechArticle techArticle = createTechArticle(i, company); + techArticles.add(techArticle); + } + techArticleRepository.saveAll(techArticles); + } + @Test @DisplayName("회원이 커서 방식으로 기술블로그를 조회하여 응답을 생성한다.") void getTechArticles() { @@ -257,11 +282,11 @@ void getTechArticleNotFoundMemberException() { @DisplayName("회원이 기술블로그 상세를 조회할 때 기술블로그가 존재하지 않으면 예외가 발생한다.") void getTechArticleNotFoundTechArticleException() { // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + TechArticle techArticle = createTechArticle(1, company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long id = savedTechArticle.getId() + 1; @@ -281,62 +306,6 @@ void getTechArticleNotFoundTechArticleException() { .hasMessage(NOT_FOUND_TECH_ARTICLE_MESSAGE); } - @Test - @DisplayName("회원이 기술블로그 상세를 조회할 때 엘라스틱ID가 존재하지 않으면 예외가 발생한다.") - void getTechArticleNotFoundElasticIdException() { - // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); - - SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - memberRepository.save(member); - - UserPrincipal userPrincipal = UserPrincipal.createByMember(member); - SecurityContext context = SecurityContextHolder.getContext(); - context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), - userPrincipal.getSocialType().name())); - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - // when // then - assertThatThrownBy(() -> memberTechArticleService.getTechArticle(id, null, authentication)) - .isInstanceOf(TechArticleException.class) - .hasMessage(NOT_FOUND_ELASTIC_ID_MESSAGE); - } - - @Test - @DisplayName("회원이 기술블로그 상세를 조회할 때 엘라스틱 기술블로그가 존재하지 않으면 예외가 발생한다.") - void getTechArticleNotFoundElasticTechArticleException() { - // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), "elasticId", company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); - - SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - memberRepository.save(member); - - UserPrincipal userPrincipal = UserPrincipal.createByMember(member); - SecurityContext context = SecurityContextHolder.getContext(); - context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), - userPrincipal.getSocialType().name())); - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - // when // then - assertThatThrownBy(() -> memberTechArticleService.getTechArticle(id, null, authentication)) - .isInstanceOf(NotFoundException.class) - .hasMessage(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE); - } - @Test @DisplayName("회원이 북마크 한 적 없는 기술블로그의 북마크를 요청하면 새로운 북마크가 생성된다.") void createBookmark() { @@ -507,4 +476,30 @@ private Bookmark createBookmark(Member member, TechArticle techArticle, boolean .status(status) .build(); } + + private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, + String careerUrl) { + return Company.builder() + .name(new CompanyName(companyName)) + .officialImageUrl(new Url(officialImageUrl)) + .careerUrl(new Url(careerUrl)) + .officialUrl(new Url(officialUrl)) + .build(); + } + + private static TechArticle createTechArticle(int i, Company company) { + return TechArticle.builder() + .title(new Title("타이틀 " + i)) + .contents("내용 " + i) + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(i)) + .recommendTotalCount(new Count(i)) + .viewTotalCount(new Count(i)) + .popularScore(new Count(10L *i)) + .build(); + } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechCommentServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechCommentServiceTest.java index f47eb2c2..9b66f318 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechCommentServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechCommentServiceTest.java @@ -51,6 +51,8 @@ import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechRepliedCommentsResponse; import com.dreamypatisiel.devdevdev.web.dto.util.CommonResponseUtil; import jakarta.persistence.EntityManager; + +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import org.assertj.core.groups.Tuple; @@ -128,18 +130,16 @@ void registerTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); RegisterTechCommentRequest registerTechCommentRequest = new RegisterTechCommentRequest("댓글입니다."); TechCommentDto registerCommentDto = TechCommentDto.createRegisterCommentDto(registerTechCommentRequest, null); // when TechCommentResponse techCommentResponse = memberTechCommentService.registerMainTechComment( - id, registerCommentDto, authentication); + techArticleId, registerCommentDto, authentication); em.flush(); // then @@ -155,7 +155,7 @@ void registerTechComment() { () -> assertThat(findTechComment.getRecommendTotalCount().getCount()).isEqualTo(0L), () -> assertThat(findTechComment.getReplyTotalCount().getCount()).isEqualTo(0L), () -> assertThat(findTechComment.getCreatedBy().getId()).isEqualTo(member.getId()), - () -> assertThat(findTechComment.getTechArticle().getId()).isEqualTo(id), + () -> assertThat(findTechComment.getTechArticle().getId()).isEqualTo(techArticleId), // 기술블로그 댓글 수 증가 확인 () -> assertThat(findTechComment.getTechArticle().getCommentTotalCount().getCount()).isEqualTo(2L) ); @@ -169,11 +169,9 @@ void registerTechCommentNotFoundTechArticleException() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId() + 1; + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId() + 1; SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); Member member = Member.createMemberBy(socialMemberDto); @@ -190,7 +188,7 @@ void registerTechCommentNotFoundTechArticleException() { // when // then assertThatThrownBy( - () -> memberTechCommentService.registerMainTechComment(id, registerCommentDto, authentication)) + () -> memberTechCommentService.registerMainTechComment(techArticleId, registerCommentDto, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(NOT_FOUND_TECH_ARTICLE_MESSAGE); } @@ -234,8 +232,7 @@ void modifyTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -313,9 +310,7 @@ void modifyTechCommentNotFoundTechArticleCommentException() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -349,8 +344,7 @@ void modifyTechCommentAlreadyDeletedException() { userPrincipal.getSocialType().name())); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -390,8 +384,7 @@ void deleteTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -436,8 +429,7 @@ void deleteTechCommentAlreadyDeletedException() { userPrincipal.getSocialType().name())); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -475,8 +467,7 @@ void deleteTechCommentNotFoundException() { userPrincipal.getSocialType().name())); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -510,8 +501,7 @@ void deleteTechCommentAdmin() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -560,8 +550,7 @@ void deleteTechCommentNotByMemberException() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -613,8 +602,7 @@ void registerRepliedTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -673,9 +661,7 @@ void registerRepliedTechCommentToRepliedTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(2L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -717,7 +703,7 @@ void registerRepliedTechCommentToRepliedTechComment() { 1L), // 기술블로그 댓글 수 증가 확인 () -> assertThat(findRepliedTechComment.getTechArticle().getCommentTotalCount().getCount()).isEqualTo( - 3L) + 2L) ); } @@ -739,9 +725,7 @@ void registerRepliedTechCommentNotFoundTechCommentException() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -778,9 +762,7 @@ void registerRepliedTechCommentDeletedTechCommentException() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -850,9 +832,7 @@ void getTechCommentsSortByOLDEST() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1147,9 +1127,7 @@ void getTechCommentsSortByLATEST() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1319,9 +1297,7 @@ void getTechCommentsSortByMostCommented() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1616,9 +1592,7 @@ void getTechCommentsSortByMostRecommended() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1767,9 +1741,7 @@ void getTechCommentsByCursor() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1920,9 +1892,7 @@ void recommendTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(2L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1958,9 +1928,7 @@ void recommendTechCommentCancel() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(2L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1999,9 +1967,7 @@ void recommendTechCommentNotFoundTechCommentException() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -2034,9 +2000,7 @@ void recommendTechCommentDeletedTechCommentException() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -2125,10 +2089,9 @@ void findTechBestComments() { companyRepository.save(company); // 기술 블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); // 댓글 생성 TechComment originParentTechComment1 = createMainTechComment(new CommentContents("최상위 댓글1"), member1, @@ -2275,10 +2238,9 @@ void findTechBestCommentsExcludeLessThanOneRecommend() { companyRepository.save(company); // 기술 블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); // 댓글 생성 TechComment originParentTechComment1 = createMainTechComment(new CommentContents("최상위 댓글1"), member1, @@ -2371,4 +2333,20 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), ) ); } + + private TechArticle createTechArticle(Company company) { + return TechArticle.builder() + .title(new Title("타이틀 ")) + .contents("내용 ") + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(1)) + .build(); + } } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechArticleServiceStrategyTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechArticleServiceStrategyTest.java index ee377deb..e0490c80 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechArticleServiceStrategyTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechArticleServiceStrategyTest.java @@ -7,18 +7,20 @@ import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.GuestTechArticleService; import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.MemberTechArticleService; import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.TechArticleService; -import com.dreamypatisiel.devdevdev.elastic.domain.service.ElasticsearchSupportTest; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.transaction.annotation.Transactional; -class TechArticleServiceStrategyTest extends ElasticsearchSupportTest { - +@SpringBootTest +@Transactional +class TechArticleServiceStrategyTest { @Autowired TechArticleServiceStrategy techArticleServiceStrategy; @Mock diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java index c345159d..1620b719 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java @@ -48,6 +48,8 @@ import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechRepliedCommentsResponse; import com.dreamypatisiel.devdevdev.web.dto.util.CommonResponseUtil; import jakarta.persistence.EntityManager; + +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import org.assertj.core.groups.Tuple; @@ -118,8 +120,7 @@ void getTechCommentsSortByOLDEST() { companyRepository.save(company); // 기술블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -433,8 +434,7 @@ void getTechCommentsSortByLATEST() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -608,9 +608,7 @@ void getTechCommentsSortByMostCommented() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -924,9 +922,7 @@ void getTechCommentsSortByMostRecommended() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1082,8 +1078,7 @@ void getTechCommentsByCursor() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1273,8 +1268,7 @@ void findTechBestComments() { companyRepository.save(company); // 기술 블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); // 댓글 생성 @@ -1422,8 +1416,7 @@ void registerTechComment() { companyRepository.save(company); // 기술블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); // 댓글 등록 요청 생성 @@ -1524,8 +1517,7 @@ void registerRepliedTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1589,8 +1581,7 @@ void registerRepliedTechCommentToRepliedTechComment() { companyRepository.save(company); // 기술블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(2L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1633,7 +1624,7 @@ void registerRepliedTechCommentToRepliedTechComment() { 1L), // 기술블로그 댓글 수 증가 확인 () -> assertThat(findRepliedTechComment.getTechArticle().getCommentTotalCount().getCount()).isEqualTo( - 3L) + 2L) ); } @@ -1659,8 +1650,7 @@ void registerRepliedTechCommentNotFoundTechCommentException() { companyRepository.save(company); // 기술블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1704,8 +1694,7 @@ void registerRepliedTechCommentDeletedTechCommentException() { companyRepository.save(company); // 기술블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1777,8 +1766,7 @@ void modifyTechComment() { companyRepository.save(company); // 기술블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1857,8 +1845,7 @@ void modifyTechCommentNotFoundTechArticleCommentException() { companyRepository.save(company); // 기술블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1889,8 +1876,7 @@ void modifyTechCommentAlreadyDeletedException() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1932,8 +1918,7 @@ void deleteTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1977,8 +1962,7 @@ void deleteTechCommentAlreadyDeletedException() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -2014,8 +1998,7 @@ void deleteTechCommentNotFoundException() { Authentication authentication = mock(Authentication.class); when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -2045,4 +2028,20 @@ void deleteTechCommentIllegalStateException() { .isInstanceOf(IllegalStateException.class) .hasMessage(INVALID_METHODS_CALL_MESSAGE); } + + private TechArticle createTechArticle(Company company) { + return TechArticle.builder() + .title(new Title("타이틀 ")) + .contents("내용 ") + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(1)) + .build(); + } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/elastic/config/ContainerExtension.java b/src/test/java/com/dreamypatisiel/devdevdev/elastic/config/ContainerExtension.java index 5532be63..af162730 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/elastic/config/ContainerExtension.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/elastic/config/ContainerExtension.java @@ -1,43 +1,43 @@ -package com.dreamypatisiel.devdevdev.elastic.config; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.extension.AfterAllCallback; -import org.junit.jupiter.api.extension.BeforeAllCallback; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.testcontainers.containers.BindMode; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.images.builder.ImageFromDockerfile; - -@Disabled -public class ContainerExtension implements BeforeAllCallback, AfterAllCallback { - - static GenericContainer esContainer; - private static boolean initialized = false; - - @Override - public void beforeAll(ExtensionContext extensionContext) throws Exception { - if (!initialized) { - esContainer = new GenericContainer<>( - new ImageFromDockerfile() - .withDockerfileFromBuilder(builder -> builder - .from("docker.elastic.co/elasticsearch/elasticsearch:7.17.10") - .run("bin/elasticsearch-plugin install analysis-nori") - .build())) - .withEnv("discovery.type", "single-node") - .withEnv("http.host", "0.0.0.0") - .withFileSystemBind("./dict", "/usr/share/elasticsearch/config/dict", BindMode.READ_ONLY) - .withExposedPorts(9200) - .withReuse(true); - - esContainer.start(); - System.setProperty("spring.elasticsearch.rest.uris", esContainer.getHost()); - String host = String.format("http://%s:%s", esContainer.getContainerIpAddress(), esContainer.getMappedPort(9200)); - - initialized = true; - } - } - @Override - public void afterAll(ExtensionContext extensionContext) throws Exception { -// esContainer.stop(); - } -} \ No newline at end of file +//package com.dreamypatisiel.devdevdev.elastic.config; +// +//import org.junit.jupiter.api.Disabled; +//import org.junit.jupiter.api.extension.AfterAllCallback; +//import org.junit.jupiter.api.extension.BeforeAllCallback; +//import org.junit.jupiter.api.extension.ExtensionContext; +//import org.testcontainers.containers.BindMode; +//import org.testcontainers.containers.GenericContainer; +//import org.testcontainers.images.builder.ImageFromDockerfile; +// +//@Disabled +//public class ContainerExtension implements BeforeAllCallback, AfterAllCallback { +// +// static GenericContainer esContainer; +// private static boolean initialized = false; +// +// @Override +// public void beforeAll(ExtensionContext extensionContext) throws Exception { +// if (!initialized) { +// esContainer = new GenericContainer<>( +// new ImageFromDockerfile() +// .withDockerfileFromBuilder(builder -> builder +// .from("docker.elastic.co/elasticsearch/elasticsearch:7.17.10") +// .run("bin/elasticsearch-plugin install analysis-nori") +// .build())) +// .withEnv("discovery.type", "single-node") +// .withEnv("http.host", "0.0.0.0") +// .withFileSystemBind("./dict", "/usr/share/elasticsearch/config/dict", BindMode.READ_ONLY) +// .withExposedPorts(9200) +// .withReuse(true); +// +// esContainer.start(); +// System.setProperty("spring.elasticsearch.rest.uris", esContainer.getHost()); +// String host = String.format("http://%s:%s", esContainer.getContainerIpAddress(), esContainer.getMappedPort(9200)); +// +// initialized = true; +// } +// } +// @Override +// public void afterAll(ExtensionContext extensionContext) throws Exception { +//// esContainer.stop(); +// } +//} \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticKeywordServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticKeywordServiceTest.java index 03b9e4f0..bbdbe9f6 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticKeywordServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticKeywordServiceTest.java @@ -1,165 +1,165 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.service; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticKeyword; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticKeywordRepository; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -@Disabled -@SpringBootTest -class ElasticKeywordServiceTest { - - @Autowired - ElasticKeywordService elasticKeywordService; - @Autowired - ElasticKeywordRepository elasticKeywordRepository; - - @AfterEach - void afterEach() { - elasticKeywordRepository.deleteAll(); - } - - @Test - @DisplayName("검색어와 prefix가 일치하는 키워드를 조회한다.") - void autocompleteKeyword() throws IOException { - // given - ElasticKeyword keyword1 = ElasticKeyword.create("자바"); - ElasticKeyword keyword2 = ElasticKeyword.create("자바스크립트"); - ElasticKeyword keyword3 = ElasticKeyword.create("스프링"); - ElasticKeyword keyword4 = ElasticKeyword.create("스프링부트"); - ElasticKeyword keyword5 = ElasticKeyword.create("챗지피티"); - List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); - elasticKeywordRepository.saveAll(elasticKeywords); - - String prefix = "자바"; - - // when - List keywords = elasticKeywordService.autocompleteKeyword(prefix); - - // then - assertThat(keywords) - .hasSize(2) - .contains("자바", "자바스크립트"); - } - - @ParameterizedTest - @ValueSource(strings = {"ㅈ", "자", "잡", "ㅈㅏ", "ㅈㅏㅂ", "ㅈㅏㅂㅏ"}) - @DisplayName("한글 검색어의 경우 자음, 모음을 분리하여 검색할 수 있다.") - void autocompleteKoreanKeywordBySeparatingConsonantsAndVowels(String prefix) throws IOException { - // given - ElasticKeyword keyword1 = ElasticKeyword.create("자바"); - ElasticKeyword keyword2 = ElasticKeyword.create("자바스크립트"); - ElasticKeyword keyword3 = ElasticKeyword.create("스프링"); - ElasticKeyword keyword4 = ElasticKeyword.create("스프링부트"); - ElasticKeyword keyword5 = ElasticKeyword.create("챗지피티"); - List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); - elasticKeywordRepository.saveAll(elasticKeywords); - - // when - List keywords = elasticKeywordService.autocompleteKeyword(prefix); - - // then - assertThat(keywords) - .hasSize(2) - .contains("자바", "자바스크립트"); - } - - @Test - @DisplayName("한글 검색어의 경우 초성검색을 할 수 있다.") - void autocompleteKoreanKeywordByChosung() throws IOException { - // given - ElasticKeyword keyword1 = ElasticKeyword.create("자바"); - ElasticKeyword keyword2 = ElasticKeyword.create("자바스크립트"); - ElasticKeyword keyword3 = ElasticKeyword.create("스프링"); - ElasticKeyword keyword4 = ElasticKeyword.create("스프링부트"); - ElasticKeyword keyword5 = ElasticKeyword.create("챗지피티"); - List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); - elasticKeywordRepository.saveAll(elasticKeywords); - - String prefix = "ㅅㅍㄹ"; - - // when - List keywords = elasticKeywordService.autocompleteKeyword(prefix); - - // then - assertThat(keywords) - .hasSize(2) - .contains("스프링", "스프링부트"); - } - - - @Test - @DisplayName("영어 대소문자 상관없이 키워드를 조회한다.") - void autocompleteKeywordRegardlessOfAlphaCase() throws IOException { - // given - ElasticKeyword keyword1 = ElasticKeyword.create("JAVA"); - ElasticKeyword keyword2 = ElasticKeyword.create("JavaScript"); - ElasticKeyword keyword3 = ElasticKeyword.create("Spring"); - ElasticKeyword keyword4 = ElasticKeyword.create("SpringBoot"); - ElasticKeyword keyword5 = ElasticKeyword.create("ChatGPT"); - List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); - elasticKeywordRepository.saveAll(elasticKeywords); - - String prefix = "spr"; - - // when - List keywords = elasticKeywordService.autocompleteKeyword(prefix); - - // then - assertThat(keywords) - .hasSize(2) - .contains("Spring", "SpringBoot"); - } - - @Test - @DisplayName("일치하는 키워드가 없을 경우 빈 리스트를 반환한다.") - void autocompleteKeywordNotFound() throws IOException { - // given - ElasticKeyword keyword1 = ElasticKeyword.create("자바"); - ElasticKeyword keyword2 = ElasticKeyword.create("자바스크립트"); - ElasticKeyword keyword3 = ElasticKeyword.create("스프링"); - ElasticKeyword keyword4 = ElasticKeyword.create("스프링부트"); - ElasticKeyword keyword5 = ElasticKeyword.create("챗지피티"); - List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); - elasticKeywordRepository.saveAll(elasticKeywords); - - String prefix = "엘라스틱서치"; - - // when - List keywords = elasticKeywordService.autocompleteKeyword(prefix); - - // then - assertThat(keywords).isEmpty(); - } - - @ParameterizedTest - @ValueSource(ints = {19, 20, 21, 22}) - @DisplayName("검색어와 prefix가 일치하는 키워드를 최대 20개 조회한다.") - void autocompleteKeywordWithMax20(int n) throws IOException { - // given - List elasticKeywords = new ArrayList<>(); - for (int i = 0; i < n; i++) { - elasticKeywords.add(ElasticKeyword.create("키워드" + i)); - } - elasticKeywordRepository.saveAll(elasticKeywords); - - String prefix = "키워드"; - - // when - List keywords = elasticKeywordService.autocompleteKeyword(prefix); - - // then - assertThat(keywords).hasSizeLessThanOrEqualTo(20); - } -} \ No newline at end of file +//package com.dreamypatisiel.devdevdev.elastic.domain.service; +// +//import static org.assertj.core.api.Assertions.assertThat; +// +//import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticKeyword; +//import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticKeywordRepository; +//import java.io.IOException; +//import java.util.ArrayList; +//import java.util.List; +//import org.junit.jupiter.api.AfterEach; +//import org.junit.jupiter.api.Disabled; +//import org.junit.jupiter.api.DisplayName; +//import org.junit.jupiter.api.Test; +//import org.junit.jupiter.params.ParameterizedTest; +//import org.junit.jupiter.params.provider.ValueSource; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +// +//@Disabled +//@SpringBootTest +//class ElasticKeywordServiceTest { +// +// @Autowired +// ElasticKeywordService elasticKeywordService; +// @Autowired +// ElasticKeywordRepository elasticKeywordRepository; +// +// @AfterEach +// void afterEach() { +// elasticKeywordRepository.deleteAll(); +// } +// +// @Test +// @DisplayName("검색어와 prefix가 일치하는 키워드를 조회한다.") +// void autocompleteKeyword() throws IOException { +// // given +// ElasticKeyword keyword1 = ElasticKeyword.create("자바"); +// ElasticKeyword keyword2 = ElasticKeyword.create("자바스크립트"); +// ElasticKeyword keyword3 = ElasticKeyword.create("스프링"); +// ElasticKeyword keyword4 = ElasticKeyword.create("스프링부트"); +// ElasticKeyword keyword5 = ElasticKeyword.create("챗지피티"); +// List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); +// elasticKeywordRepository.saveAll(elasticKeywords); +// +// String prefix = "자바"; +// +// // when +// List keywords = elasticKeywordService.autocompleteKeyword(prefix); +// +// // then +// assertThat(keywords) +// .hasSize(2) +// .contains("자바", "자바스크립트"); +// } +// +// @ParameterizedTest +// @ValueSource(strings = {"ㅈ", "자", "잡", "ㅈㅏ", "ㅈㅏㅂ", "ㅈㅏㅂㅏ"}) +// @DisplayName("한글 검색어의 경우 자음, 모음을 분리하여 검색할 수 있다.") +// void autocompleteKoreanKeywordBySeparatingConsonantsAndVowels(String prefix) throws IOException { +// // given +// ElasticKeyword keyword1 = ElasticKeyword.create("자바"); +// ElasticKeyword keyword2 = ElasticKeyword.create("자바스크립트"); +// ElasticKeyword keyword3 = ElasticKeyword.create("스프링"); +// ElasticKeyword keyword4 = ElasticKeyword.create("스프링부트"); +// ElasticKeyword keyword5 = ElasticKeyword.create("챗지피티"); +// List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); +// elasticKeywordRepository.saveAll(elasticKeywords); +// +// // when +// List keywords = elasticKeywordService.autocompleteKeyword(prefix); +// +// // then +// assertThat(keywords) +// .hasSize(2) +// .contains("자바", "자바스크립트"); +// } +// +// @Test +// @DisplayName("한글 검색어의 경우 초성검색을 할 수 있다.") +// void autocompleteKoreanKeywordByChosung() throws IOException { +// // given +// ElasticKeyword keyword1 = ElasticKeyword.create("자바"); +// ElasticKeyword keyword2 = ElasticKeyword.create("자바스크립트"); +// ElasticKeyword keyword3 = ElasticKeyword.create("스프링"); +// ElasticKeyword keyword4 = ElasticKeyword.create("스프링부트"); +// ElasticKeyword keyword5 = ElasticKeyword.create("챗지피티"); +// List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); +// elasticKeywordRepository.saveAll(elasticKeywords); +// +// String prefix = "ㅅㅍㄹ"; +// +// // when +// List keywords = elasticKeywordService.autocompleteKeyword(prefix); +// +// // then +// assertThat(keywords) +// .hasSize(2) +// .contains("스프링", "스프링부트"); +// } +// +// +// @Test +// @DisplayName("영어 대소문자 상관없이 키워드를 조회한다.") +// void autocompleteKeywordRegardlessOfAlphaCase() throws IOException { +// // given +// ElasticKeyword keyword1 = ElasticKeyword.create("JAVA"); +// ElasticKeyword keyword2 = ElasticKeyword.create("JavaScript"); +// ElasticKeyword keyword3 = ElasticKeyword.create("Spring"); +// ElasticKeyword keyword4 = ElasticKeyword.create("SpringBoot"); +// ElasticKeyword keyword5 = ElasticKeyword.create("ChatGPT"); +// List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); +// elasticKeywordRepository.saveAll(elasticKeywords); +// +// String prefix = "spr"; +// +// // when +// List keywords = elasticKeywordService.autocompleteKeyword(prefix); +// +// // then +// assertThat(keywords) +// .hasSize(2) +// .contains("Spring", "SpringBoot"); +// } +// +// @Test +// @DisplayName("일치하는 키워드가 없을 경우 빈 리스트를 반환한다.") +// void autocompleteKeywordNotFound() throws IOException { +// // given +// ElasticKeyword keyword1 = ElasticKeyword.create("자바"); +// ElasticKeyword keyword2 = ElasticKeyword.create("자바스크립트"); +// ElasticKeyword keyword3 = ElasticKeyword.create("스프링"); +// ElasticKeyword keyword4 = ElasticKeyword.create("스프링부트"); +// ElasticKeyword keyword5 = ElasticKeyword.create("챗지피티"); +// List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); +// elasticKeywordRepository.saveAll(elasticKeywords); +// +// String prefix = "엘라스틱서치"; +// +// // when +// List keywords = elasticKeywordService.autocompleteKeyword(prefix); +// +// // then +// assertThat(keywords).isEmpty(); +// } +// +// @ParameterizedTest +// @ValueSource(ints = {19, 20, 21, 22}) +// @DisplayName("검색어와 prefix가 일치하는 키워드를 최대 20개 조회한다.") +// void autocompleteKeywordWithMax20(int n) throws IOException { +// // given +// List elasticKeywords = new ArrayList<>(); +// for (int i = 0; i < n; i++) { +// elasticKeywords.add(ElasticKeyword.create("키워드" + i)); +// } +// elasticKeywordRepository.saveAll(elasticKeywords); +// +// String prefix = "키워드"; +// +// // when +// List keywords = elasticKeywordService.autocompleteKeyword(prefix); +// +// // then +// assertThat(keywords).hasSizeLessThanOrEqualTo(20); +// } +//} \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticTechArticleServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticTechArticleServiceTest.java index f0fa2586..3f4cbfc9 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticTechArticleServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticTechArticleServiceTest.java @@ -1,832 +1,832 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.service; - -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_CURSOR_SCORE_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; -import com.dreamypatisiel.devdevdev.exception.ElasticTechArticleException; -import com.dreamypatisiel.devdevdev.exception.NotFoundException; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.elasticsearch.UncategorizedElasticsearchException; -import org.springframework.data.elasticsearch.core.SearchHit; -import org.springframework.data.elasticsearch.core.SearchHits; - -public class ElasticTechArticleServiceTest extends ElasticsearchSupportTest { - - @Autowired - ElasticTechArticleService elasticTechArticleService; - @Autowired - TechArticleRepository techArticleRepository; - @Autowired - ElasticTechArticleRepository elasticTechArticleRepository; - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 조회한다. (기본정렬은 최신순)") - void getTechArticles() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, null, - null, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getRegDate) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 최신순으로 조회한다.") - void getTechArticlesOrderByLATEST() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.LATEST, null, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getRegDate) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 조회수 내림차순으로 조회한다.") - void getTechArticlesOrderByMOST_VIEWED() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.MOST_VIEWED, null, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getViewTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 댓글수 내림차순으로 조회한다.") - void getTechArticlesOrderByMOST_COMMENTED() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.MOST_COMMENTED, null, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getCommentTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 인기점수 내림차순으로 조회한다.") - void getTechArticlesOrderByPOPULAR_SCORE() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.POPULAR, null, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getPopularScore) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 정확도순으로 조회하면 최신순으로 조회된다.") - void getTechArticlesOrderByHIGHEST_SCORE() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.HIGHEST_SCORE, null, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getRegDate) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 조회할 때, " + - "존재하지 않는 엘라스틱 기술블로그 ID라면 예외가 발생한다.") - void getTechArticlesWithCursorException() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when // then - assertThatThrownBy( - () -> elasticTechArticleService.getTechArticles(pageable, "dontExistElasticId", null, null, null, null)) - .isInstanceOf(NotFoundException.class) - .hasMessage(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 최신순으로 조회한다.") - void getTechArticlesWithCursorOrderByLATEST() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.LATEST, null, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.LATEST, null, null, null); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles2) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getRegDate) - .isSortedAccordingTo(Comparator.reverseOrder()) - .allMatch(date -> !date.isAfter(cursor.getRegDate())); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 조회수 내림차순으로 조회한다.") - void getTechArticlesWithCursorOrderByMOST_VIEWED() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.MOST_VIEWED, null, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.MOST_VIEWED, null, null, null); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles2) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getViewTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()) - .allMatch(viewCount -> viewCount <= cursor.getViewTotalCount()); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 댓글수 내림차순으로 조회한다.") - void getTechArticlesWithCursorOrderByMOST_COMMENTED() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.MOST_COMMENTED, null, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.MOST_COMMENTED, null, null, null); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles2) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getCommentTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()) - .allMatch(commentCount -> commentCount <= cursor.getCommentTotalCount()); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 인기점수 내림차순으로 조회한다.") - void getTechArticlesWithCursorOrderByPOPULAR_SCORE() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.POPULAR, null, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.POPULAR, null, null, null); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles2) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getPopularScore) - .isSortedAccordingTo(Comparator.reverseOrder()) - .allMatch(popularScore -> popularScore <= cursor.getPopularScore()); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 정확도순으로 조회하면 최신순으로 조회된다.") - void getTechArticlesWithCursorOrderByHIGHEST_SCORE() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.HIGHEST_SCORE, null, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.HIGHEST_SCORE, null, null, null); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles2) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getRegDate) - .isSortedAccordingTo(Comparator.reverseOrder()) - .allMatch(date -> !date.isAfter(cursor.getRegDate())); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색한다. (기본정렬은 정확도순)") - void getTechArticlesWithKeyword() { - // given - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, null, - keyword, null, null); - List elasticTechArticlesScores = techArticles.getSearchHits().stream().map(SearchHit::getScore) - .toList(); - - // then - assertThat(techArticles.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticlesScores) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 정확도 내림차순으로 조회한다.") - void getTechArticlesWithKeywordOrderByHIGHEST_SCORE() { - // given - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.HIGHEST_SCORE, keyword, null, null); - List elasticTechArticlesScores = techArticles.getSearchHits().stream().map(SearchHit::getScore) - .toList(); - - // then - assertThat(techArticles.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticlesScores) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 최신순 내림차순으로 조회한다.") - void getTechArticlesWithKeywordOrderByLATEST() { - // given - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.LATEST, keyword, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(techArticles.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticles) - .extracting(ElasticTechArticle::getRegDate) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 조회순 내림차순으로 조회한다.") - void getTechArticlesWithKeywordOrderByMOST_VIEWED() { - // given - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.MOST_VIEWED, keyword, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(techArticles.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticles) - .extracting(ElasticTechArticle::getViewTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 댓글순 내림차순으로 조회한다.") - void getTechArticlesWithKeywordOrderByMOST_COMMENTED() { - // given - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.MOST_COMMENTED, keyword, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(techArticles.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticles) - .extracting(ElasticTechArticle::getCommentTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 인기점수 내림차순으로 조회한다.") - void getTechArticlesWithKeywordOrderByPOPULAR_SCORE() { - // given - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.POPULAR, keyword, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(techArticles.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticles) - .extracting(ElasticTechArticle::getPopularScore) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 정확도 내림차순으로 조회한다.") - void getTechArticlesWithKeywordWithCursorOrderByHIGHEST_SCORE() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.HIGHEST_SCORE, keyword, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - Float cursorScore = elasticTechArticleScores1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.HIGHEST_SCORE, keyword, null, cursorScore); - List elasticTechArticleScores2 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) - .toList(); - - // then - assertThat(techArticles2.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticleScores2) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때," + - "정확도 내림차순으로 조회하기 위한 점수가 없다면 예외가 발생한다.") - void getTechArticlesWithKeywordWithCursorOrderByHIGHEST_SCOREWithoutScoreException() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.HIGHEST_SCORE, keyword, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - - // when // then - assertThatThrownBy( - () -> elasticTechArticleService.getTechArticles(pageable, cursor.getId(), TechArticleSort.HIGHEST_SCORE, - keyword, null, null)) - .isInstanceOf(ElasticTechArticleException.class) - .hasMessage(NOT_FOUND_CURSOR_SCORE_MESSAGE); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 최신순 내림차순으로 조회한다.") - void getTechArticlesWithKeywordWithCursorOrderByLATEST() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.LATEST, keyword, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - Float cursorScore = elasticTechArticleScores1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.LATEST, keyword, null, cursorScore); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(techArticles2.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticles2) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getRegDate) - .isSortedAccordingTo(Comparator.reverseOrder()) - .allMatch(date -> !date.isAfter(cursor.getRegDate())); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 조회순 내림차순으로 조회한다.") - void getTechArticlesWithKeywordWithCursorOrderByMOST_VIEWED() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.MOST_VIEWED, keyword, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - Float cursorScore = elasticTechArticleScores1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.MOST_VIEWED, keyword, null, cursorScore); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(techArticles2.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticles2) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getViewTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 댓글순 내림차순으로 조회한다.") - void getTechArticlesWithKeywordWithCursorOrderByMOST_COMMENTED() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.MOST_COMMENTED, keyword, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - Float cursorScore = elasticTechArticleScores1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.MOST_COMMENTED, keyword, null, cursorScore); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(techArticles2.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticles2) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getCommentTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 인기점수 내림차순으로 조회한다.") - void getTechArticlesWithKeywordWithCursorOrderByPOPULAR_SCORE() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.POPULAR, keyword, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - Float cursorScore = elasticTechArticleScores1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.POPULAR, keyword, null, cursorScore); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(techArticles2.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticles2) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getPopularScore) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 회사로 필터링한 후 최신순으로 조회한다.") - void getTechArticlesFilterByCompanyOrderByLATEST() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.LATEST, null, company.getId(), null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .allSatisfy(article -> { - assertThat(article.getCompanyId()).isEqualTo(company.getId()); - }) - .extracting(ElasticTechArticle::getRegDate) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 회사로 필터링한 후 조회수 내림차순으로 조회한다.") - void getTechArticlesFilterByCompanyOrderByMOST_VIEWED() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.MOST_VIEWED, null, company.getId(), null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .allSatisfy(article -> { - assertThat(article.getCompanyId()).isEqualTo(company.getId()); - }) - .extracting(ElasticTechArticle::getViewTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 회사로 필터링한 후 댓글수 내림차순으로 조회한다.") - void getTechArticlesFilterByCompanyOrderByMOST_COMMENTED() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.MOST_COMMENTED, null, company.getId(), null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .allSatisfy(article -> { - assertThat(article.getCompanyId()).isEqualTo(company.getId()); - }) - .extracting(ElasticTechArticle::getCommentTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 회사로 필터링한 후 인기점수 내림차순으로 조회한다.") - void getTechArticlesFilterByCompanyOrderByPOPULAR_SCORE() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.POPULAR, null, company.getId(), null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .allSatisfy(article -> { - assertThat(article.getCompanyId()).isEqualTo(company.getId()); - }) - .extracting(ElasticTechArticle::getPopularScore) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @ParameterizedTest - @ValueSource(strings = {"!", "^", "(", ")", "-", "+", "/", "[", "]", "{", "}", ":"}) - @DisplayName("엘라스틱서치로 키워드 검색을 할 때, 키워드에 특정 특수문자가 있다면 예외가 발생한다.") - void getTechArticlesWithSpecialSymbolsException(String keyword) { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when // then - assertThatThrownBy( - () -> elasticTechArticleService.getTechArticles(pageable, null, null, keyword, null, null)) - .isInstanceOf(UncategorizedElasticsearchException.class); - } - - @ParameterizedTest - @ValueSource(strings = {"@", "=", "#", "$", "%", "&", "*", "_", "=", "<", ">", ",", ".", "?", ";", "", "'"}) - @DisplayName("엘라스틱서치로 키워드 검색을 할 때, 키워드에 특정 특수문자가 아닌 문자들이 있다면 예외가 발생하지 않는다.") - void getTechArticlesWithSpecialSymbolsDoesNotThrowException(String keyword) { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when // then - assertThatCode(() -> elasticTechArticleService.getTechArticles(pageable, null, null, keyword, null, null)) - .doesNotThrowAnyException(); - } - - @Test - @DisplayName("엘라스틱서치 키워드 검색시 기본 쿼리 옵션은 AND로 동작한다.") - void test() { - // given - List elasticTechArticles = new ArrayList<>(); - elasticTechArticles.add(ElasticTechArticle.builder().title("자바").build()); - elasticTechArticles.add(ElasticTechArticle.builder().title("스프링").build()); - elasticTechArticles.add(ElasticTechArticle.builder().title("자바 스프링").build()); - elasticTechArticleRepository.saveAll(elasticTechArticles); - - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles1 = elasticTechArticleService.getTechArticles(pageable, - null, null, "자바", null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - null, null, "스프링", null, null); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - SearchHits techArticles3 = elasticTechArticleService.getTechArticles(pageable, - null, null, "자바 스프링", null, null); - List elasticTechArticles3 = techArticles3.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - // "자바" 키워드 검색 - assertThat(elasticTechArticles1) - .hasSize(2) - .allSatisfy(article -> - assertThat((article.getContents() != null && article.getContents().contains("자바")) || - (article.getTitle() != null && article.getTitle().contains("자바"))).isTrue()); - // "스프링" 키워드 검색 - assertThat(elasticTechArticles2) - .hasSize(2) - .allSatisfy(article -> - assertThat((article.getContents() != null && article.getContents().contains("스프링")) || - (article.getTitle() != null && article.getTitle().contains("스프링"))).isTrue()); - // "자바 스프링" 키워드 검색 - assertThat(elasticTechArticles3) - .hasSize(1) - .allSatisfy(article -> - assertThat((article.getContents() != null - && article.getContents().contains("자바") && article.getContents().contains("스프링")) - || (article.getTitle() != null - && article.getTitle().contains("자바") && article.getTitle().contains("스프링"))).isTrue()); - } -} \ No newline at end of file +//package com.dreamypatisiel.devdevdev.elastic.domain.service; +// +//import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_CURSOR_SCORE_MESSAGE; +//import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE; +//import static org.assertj.core.api.Assertions.assertThat; +//import static org.assertj.core.api.Assertions.assertThatCode; +//import static org.assertj.core.api.Assertions.assertThatThrownBy; +// +//import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; +//import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; +//import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; +//import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; +//import com.dreamypatisiel.devdevdev.exception.ElasticTechArticleException; +//import com.dreamypatisiel.devdevdev.exception.NotFoundException; +//import java.util.ArrayList; +//import java.util.Comparator; +//import java.util.List; +//import org.junit.jupiter.api.DisplayName; +//import org.junit.jupiter.api.Test; +//import org.junit.jupiter.params.ParameterizedTest; +//import org.junit.jupiter.params.provider.ValueSource; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.data.domain.PageRequest; +//import org.springframework.data.domain.Pageable; +//import org.springframework.data.elasticsearch.UncategorizedElasticsearchException; +//import org.springframework.data.elasticsearch.core.SearchHit; +//import org.springframework.data.elasticsearch.core.SearchHits; +// +//public class ElasticTechArticleServiceTest extends ElasticsearchSupportTest { +// +// @Autowired +// ElasticTechArticleService elasticTechArticleService; +// @Autowired +// TechArticleRepository techArticleRepository; +// @Autowired +// ElasticTechArticleRepository elasticTechArticleRepository; +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 조회한다. (기본정렬은 최신순)") +// void getTechArticles() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, null, +// null, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getRegDate) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 최신순으로 조회한다.") +// void getTechArticlesOrderByLATEST() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.LATEST, null, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getRegDate) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 조회수 내림차순으로 조회한다.") +// void getTechArticlesOrderByMOST_VIEWED() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.MOST_VIEWED, null, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getViewTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 댓글수 내림차순으로 조회한다.") +// void getTechArticlesOrderByMOST_COMMENTED() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.MOST_COMMENTED, null, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getCommentTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 인기점수 내림차순으로 조회한다.") +// void getTechArticlesOrderByPOPULAR_SCORE() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.POPULAR, null, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getPopularScore) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 정확도순으로 조회하면 최신순으로 조회된다.") +// void getTechArticlesOrderByHIGHEST_SCORE() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.HIGHEST_SCORE, null, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getRegDate) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 조회할 때, " + +// "존재하지 않는 엘라스틱 기술블로그 ID라면 예외가 발생한다.") +// void getTechArticlesWithCursorException() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when // then +// assertThatThrownBy( +// () -> elasticTechArticleService.getTechArticles(pageable, "dontExistElasticId", null, null, null, null)) +// .isInstanceOf(NotFoundException.class) +// .hasMessage(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 최신순으로 조회한다.") +// void getTechArticlesWithCursorOrderByLATEST() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.LATEST, null, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.LATEST, null, null, null); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles2) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getRegDate) +// .isSortedAccordingTo(Comparator.reverseOrder()) +// .allMatch(date -> !date.isAfter(cursor.getRegDate())); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 조회수 내림차순으로 조회한다.") +// void getTechArticlesWithCursorOrderByMOST_VIEWED() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.MOST_VIEWED, null, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.MOST_VIEWED, null, null, null); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles2) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getViewTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()) +// .allMatch(viewCount -> viewCount <= cursor.getViewTotalCount()); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 댓글수 내림차순으로 조회한다.") +// void getTechArticlesWithCursorOrderByMOST_COMMENTED() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.MOST_COMMENTED, null, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.MOST_COMMENTED, null, null, null); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles2) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getCommentTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()) +// .allMatch(commentCount -> commentCount <= cursor.getCommentTotalCount()); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 인기점수 내림차순으로 조회한다.") +// void getTechArticlesWithCursorOrderByPOPULAR_SCORE() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.POPULAR, null, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.POPULAR, null, null, null); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles2) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getPopularScore) +// .isSortedAccordingTo(Comparator.reverseOrder()) +// .allMatch(popularScore -> popularScore <= cursor.getPopularScore()); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 정확도순으로 조회하면 최신순으로 조회된다.") +// void getTechArticlesWithCursorOrderByHIGHEST_SCORE() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.HIGHEST_SCORE, null, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.HIGHEST_SCORE, null, null, null); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles2) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getRegDate) +// .isSortedAccordingTo(Comparator.reverseOrder()) +// .allMatch(date -> !date.isAfter(cursor.getRegDate())); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색한다. (기본정렬은 정확도순)") +// void getTechArticlesWithKeyword() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, null, +// keyword, null, null); +// List elasticTechArticlesScores = techArticles.getSearchHits().stream().map(SearchHit::getScore) +// .toList(); +// +// // then +// assertThat(techArticles.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticlesScores) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 정확도 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordOrderByHIGHEST_SCORE() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.HIGHEST_SCORE, keyword, null, null); +// List elasticTechArticlesScores = techArticles.getSearchHits().stream().map(SearchHit::getScore) +// .toList(); +// +// // then +// assertThat(techArticles.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticlesScores) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 최신순 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordOrderByLATEST() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.LATEST, keyword, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(techArticles.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticles) +// .extracting(ElasticTechArticle::getRegDate) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 조회순 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordOrderByMOST_VIEWED() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.MOST_VIEWED, keyword, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(techArticles.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticles) +// .extracting(ElasticTechArticle::getViewTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 댓글순 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordOrderByMOST_COMMENTED() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.MOST_COMMENTED, keyword, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(techArticles.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticles) +// .extracting(ElasticTechArticle::getCommentTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 인기점수 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordOrderByPOPULAR_SCORE() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.POPULAR, keyword, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(techArticles.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticles) +// .extracting(ElasticTechArticle::getPopularScore) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 정확도 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordWithCursorOrderByHIGHEST_SCORE() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.HIGHEST_SCORE, keyword, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// Float cursorScore = elasticTechArticleScores1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.HIGHEST_SCORE, keyword, null, cursorScore); +// List elasticTechArticleScores2 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) +// .toList(); +// +// // then +// assertThat(techArticles2.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticleScores2) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때," + +// "정확도 내림차순으로 조회하기 위한 점수가 없다면 예외가 발생한다.") +// void getTechArticlesWithKeywordWithCursorOrderByHIGHEST_SCOREWithoutScoreException() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.HIGHEST_SCORE, keyword, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// +// // when // then +// assertThatThrownBy( +// () -> elasticTechArticleService.getTechArticles(pageable, cursor.getId(), TechArticleSort.HIGHEST_SCORE, +// keyword, null, null)) +// .isInstanceOf(ElasticTechArticleException.class) +// .hasMessage(NOT_FOUND_CURSOR_SCORE_MESSAGE); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 최신순 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordWithCursorOrderByLATEST() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.LATEST, keyword, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// Float cursorScore = elasticTechArticleScores1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.LATEST, keyword, null, cursorScore); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(techArticles2.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticles2) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getRegDate) +// .isSortedAccordingTo(Comparator.reverseOrder()) +// .allMatch(date -> !date.isAfter(cursor.getRegDate())); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 조회순 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordWithCursorOrderByMOST_VIEWED() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.MOST_VIEWED, keyword, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// Float cursorScore = elasticTechArticleScores1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.MOST_VIEWED, keyword, null, cursorScore); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(techArticles2.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticles2) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getViewTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 댓글순 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordWithCursorOrderByMOST_COMMENTED() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.MOST_COMMENTED, keyword, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// Float cursorScore = elasticTechArticleScores1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.MOST_COMMENTED, keyword, null, cursorScore); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(techArticles2.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticles2) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getCommentTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 인기점수 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordWithCursorOrderByPOPULAR_SCORE() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.POPULAR, keyword, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// Float cursorScore = elasticTechArticleScores1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.POPULAR, keyword, null, cursorScore); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(techArticles2.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticles2) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getPopularScore) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 회사로 필터링한 후 최신순으로 조회한다.") +// void getTechArticlesFilterByCompanyOrderByLATEST() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.LATEST, null, company.getId(), null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .allSatisfy(article -> { +// assertThat(article.getCompanyId()).isEqualTo(company.getId()); +// }) +// .extracting(ElasticTechArticle::getRegDate) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 회사로 필터링한 후 조회수 내림차순으로 조회한다.") +// void getTechArticlesFilterByCompanyOrderByMOST_VIEWED() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.MOST_VIEWED, null, company.getId(), null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .allSatisfy(article -> { +// assertThat(article.getCompanyId()).isEqualTo(company.getId()); +// }) +// .extracting(ElasticTechArticle::getViewTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 회사로 필터링한 후 댓글수 내림차순으로 조회한다.") +// void getTechArticlesFilterByCompanyOrderByMOST_COMMENTED() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.MOST_COMMENTED, null, company.getId(), null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .allSatisfy(article -> { +// assertThat(article.getCompanyId()).isEqualTo(company.getId()); +// }) +// .extracting(ElasticTechArticle::getCommentTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 회사로 필터링한 후 인기점수 내림차순으로 조회한다.") +// void getTechArticlesFilterByCompanyOrderByPOPULAR_SCORE() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.POPULAR, null, company.getId(), null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .allSatisfy(article -> { +// assertThat(article.getCompanyId()).isEqualTo(company.getId()); +// }) +// .extracting(ElasticTechArticle::getPopularScore) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @ParameterizedTest +// @ValueSource(strings = {"!", "^", "(", ")", "-", "+", "/", "[", "]", "{", "}", ":"}) +// @DisplayName("엘라스틱서치로 키워드 검색을 할 때, 키워드에 특정 특수문자가 있다면 예외가 발생한다.") +// void getTechArticlesWithSpecialSymbolsException(String keyword) { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when // then +// assertThatThrownBy( +// () -> elasticTechArticleService.getTechArticles(pageable, null, null, keyword, null, null)) +// .isInstanceOf(UncategorizedElasticsearchException.class); +// } +// +// @ParameterizedTest +// @ValueSource(strings = {"@", "=", "#", "$", "%", "&", "*", "_", "=", "<", ">", ",", ".", "?", ";", "", "'"}) +// @DisplayName("엘라스틱서치로 키워드 검색을 할 때, 키워드에 특정 특수문자가 아닌 문자들이 있다면 예외가 발생하지 않는다.") +// void getTechArticlesWithSpecialSymbolsDoesNotThrowException(String keyword) { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when // then +// assertThatCode(() -> elasticTechArticleService.getTechArticles(pageable, null, null, keyword, null, null)) +// .doesNotThrowAnyException(); +// } +// +// @Test +// @DisplayName("엘라스틱서치 키워드 검색시 기본 쿼리 옵션은 AND로 동작한다.") +// void test() { +// // given +// List elasticTechArticles = new ArrayList<>(); +// elasticTechArticles.add(ElasticTechArticle.builder().title("자바").build()); +// elasticTechArticles.add(ElasticTechArticle.builder().title("스프링").build()); +// elasticTechArticles.add(ElasticTechArticle.builder().title("자바 스프링").build()); +// elasticTechArticleRepository.saveAll(elasticTechArticles); +// +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles(pageable, +// null, null, "자바", null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// null, null, "스프링", null, null); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// SearchHits techArticles3 = elasticTechArticleService.getTechArticles(pageable, +// null, null, "자바 스프링", null, null); +// List elasticTechArticles3 = techArticles3.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// // "자바" 키워드 검색 +// assertThat(elasticTechArticles1) +// .hasSize(2) +// .allSatisfy(article -> +// assertThat((article.getContents() != null && article.getContents().contains("자바")) || +// (article.getTitle() != null && article.getTitle().contains("자바"))).isTrue()); +// // "스프링" 키워드 검색 +// assertThat(elasticTechArticles2) +// .hasSize(2) +// .allSatisfy(article -> +// assertThat((article.getContents() != null && article.getContents().contains("스프링")) || +// (article.getTitle() != null && article.getTitle().contains("스프링"))).isTrue()); +// // "자바 스프링" 키워드 검색 +// assertThat(elasticTechArticles3) +// .hasSize(1) +// .allSatisfy(article -> +// assertThat((article.getContents() != null +// && article.getContents().contains("자바") && article.getContents().contains("스프링")) +// || (article.getTitle() != null +// && article.getTitle().contains("자바") && article.getTitle().contains("스프링"))).isTrue()); +// } +//} \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticsearchSupportTest.java b/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticsearchSupportTest.java index e3554bbd..802828bb 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticsearchSupportTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticsearchSupportTest.java @@ -1,104 +1,104 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.service; - -import com.dreamypatisiel.devdevdev.domain.entity.Company; -import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.CompanyName; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; -import com.dreamypatisiel.devdevdev.domain.repository.CompanyRepository; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -//@ExtendWith(ContainerExtension.class) -//@SpringBootTest(classes = {ElasticsearchTestConfig.class}) -@SpringBootTest -@Transactional -public class ElasticsearchSupportTest { - - public static final int TEST_ARTICLES_COUNT = 20; - public static TechArticle firstTechArticle; - public static Company company; - - @BeforeAll - static void setup(@Autowired TechArticleRepository techArticleRepository, - @Autowired CompanyRepository companyRepository, - @Autowired ElasticTechArticleRepository elasticTechArticleRepository) { - company = createCompany("꿈빛 파티시엘", "https://example.net/image.png", "https://example.com", - "https://example.com"); - companyRepository.save(company); - - // 엘라스틱 기술블로그 데이터를 최신순->오래된순, 조회수많은순->적은순, 댓글많은순->적은순의 순서로 생성한다. - LocalDate baseDate = LocalDate.of(2024, 8, 30); - List elasticTechArticles = new ArrayList<>(); - for (int i = 1; i <= TEST_ARTICLES_COUNT; i++) { - ElasticTechArticle elasticTechArticle = createElasticTechArticle("elasticId_" + i, "타이틀_" + i, - baseDate.minusDays(i), "내용", "http://example.com/" + i, "설명", "http://example.com/", "작성자", - company.getName().getCompanyName(), company.getId(), (long) TEST_ARTICLES_COUNT - i, - (long) TEST_ARTICLES_COUNT - i, (long) TEST_ARTICLES_COUNT - i, - (long) (TEST_ARTICLES_COUNT - i) * 10); - elasticTechArticles.add(elasticTechArticle); - } - Iterable elasticTechArticleIterable = elasticTechArticleRepository.saveAll( - elasticTechArticles); - - // 엘라스틱 기술블로그를 토대로 RDB 기술블로그 데이터를 생성한다. - List techArticles = new ArrayList<>(); - for (ElasticTechArticle elasticTechArticle : elasticTechArticleIterable) { - TechArticle techArticle = TechArticle.createTechArticle(elasticTechArticle, company); - techArticles.add(techArticle); - } - List savedTechArticles = techArticleRepository.saveAll(techArticles); - firstTechArticle = savedTechArticles.getFirst(); - } - - @AfterAll - static void tearDown(@Autowired TechArticleRepository techArticleRepository, - @Autowired ElasticTechArticleRepository elasticTechArticleRepository, - @Autowired CompanyRepository companyRepository) { - elasticTechArticleRepository.deleteAll(); - techArticleRepository.deleteAllInBatch(); - companyRepository.deleteAllInBatch(); - } - - private static ElasticTechArticle createElasticTechArticle(String id, String title, LocalDate regDate, - String contents, String techArticleUrl, - String description, String thumbnailUrl, String author, - String company, Long companyId, - Long viewTotalCount, Long recommendTotalCount, - Long commentTotalCount, Long popularScore) { - return ElasticTechArticle.builder() - .id(id) - .title(title) - .regDate(regDate) - .contents(contents) - .techArticleUrl(techArticleUrl) - .description(description) - .thumbnailUrl(thumbnailUrl) - .author(author) - .company(company) - .companyId(companyId) - .viewTotalCount(viewTotalCount) - .recommendTotalCount(recommendTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(popularScore) - .build(); - } - - private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, - String careerUrl) { - return Company.builder() - .name(new CompanyName(companyName)) - .officialUrl(new Url(officialUrl)) - .careerUrl(new Url(careerUrl)) - .officialImageUrl(new Url(officialImageUrl)) - .build(); - } -} +//package com.dreamypatisiel.devdevdev.elastic.domain.service; +// +//import com.dreamypatisiel.devdevdev.domain.entity.Company; +//import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; +//import com.dreamypatisiel.devdevdev.domain.entity.embedded.CompanyName; +//import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; +//import com.dreamypatisiel.devdevdev.domain.repository.CompanyRepository; +//import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; +//import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; +//import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; +//import java.time.LocalDate; +//import java.util.ArrayList; +//import java.util.List; +//import org.junit.jupiter.api.AfterAll; +//import org.junit.jupiter.api.BeforeAll; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.transaction.annotation.Transactional; +// +////@ExtendWith(ContainerExtension.class) +////@SpringBootTest(classes = {ElasticsearchTestConfig.class}) +//@SpringBootTest +//@Transactional +//public class ElasticsearchSupportTest { +// +// public static final int TEST_ARTICLES_COUNT = 20; +// public static TechArticle firstTechArticle; +// public static Company company; +// +// @BeforeAll +// static void setup(@Autowired TechArticleRepository techArticleRepository, +// @Autowired CompanyRepository companyRepository, +// @Autowired ElasticTechArticleRepository elasticTechArticleRepository) { +// company = createCompany("꿈빛 파티시엘", "https://example.net/image.png", "https://example.com", +// "https://example.com"); +// companyRepository.save(company); +// +// // 엘라스틱 기술블로그 데이터를 최신순->오래된순, 조회수많은순->적은순, 댓글많은순->적은순의 순서로 생성한다. +// LocalDate baseDate = LocalDate.of(2024, 8, 30); +// List elasticTechArticles = new ArrayList<>(); +// for (int i = 1; i <= TEST_ARTICLES_COUNT; i++) { +// ElasticTechArticle elasticTechArticle = createElasticTechArticle("elasticId_" + i, "타이틀_" + i, +// baseDate.minusDays(i), "내용", "http://example.com/" + i, "설명", "http://example.com/", "작성자", +// company.getName().getCompanyName(), company.getId(), (long) TEST_ARTICLES_COUNT - i, +// (long) TEST_ARTICLES_COUNT - i, (long) TEST_ARTICLES_COUNT - i, +// (long) (TEST_ARTICLES_COUNT - i) * 10); +// elasticTechArticles.add(elasticTechArticle); +// } +// Iterable elasticTechArticleIterable = elasticTechArticleRepository.saveAll( +// elasticTechArticles); +// +// // 엘라스틱 기술블로그를 토대로 RDB 기술블로그 데이터를 생성한다. +// List techArticles = new ArrayList<>(); +// for (ElasticTechArticle elasticTechArticle : elasticTechArticleIterable) { +// TechArticle techArticle = createTechArticle(elasticTechArticle, company); +// techArticles.add(techArticle); +// } +// List savedTechArticles = techArticleRepository.saveAll(techArticles); +// firstTechArticle = savedTechArticles.getFirst(); +// } +// +// @AfterAll +// static void tearDown(@Autowired TechArticleRepository techArticleRepository, +// @Autowired ElasticTechArticleRepository elasticTechArticleRepository, +// @Autowired CompanyRepository companyRepository) { +// elasticTechArticleRepository.deleteAll(); +// techArticleRepository.deleteAllInBatch(); +// companyRepository.deleteAllInBatch(); +// } +// +// private static ElasticTechArticle createElasticTechArticle(String id, String title, LocalDate regDate, +// String contents, String techArticleUrl, +// String description, String thumbnailUrl, String author, +// String company, Long companyId, +// Long viewTotalCount, Long recommendTotalCount, +// Long commentTotalCount, Long popularScore) { +// return ElasticTechArticle.builder() +// .id(id) +// .title(title) +// .regDate(regDate) +// .contents(contents) +// .techArticleUrl(techArticleUrl) +// .description(description) +// .thumbnailUrl(thumbnailUrl) +// .author(author) +// .company(company) +// .companyId(companyId) +// .viewTotalCount(viewTotalCount) +// .recommendTotalCount(recommendTotalCount) +// .commentTotalCount(commentTotalCount) +// .popularScore(popularScore) +// .build(); +// } +// +// private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, +// String careerUrl) { +// return Company.builder() +// .name(new CompanyName(companyName)) +// .officialUrl(new Url(officialUrl)) +// .careerUrl(new Url(careerUrl)) +// .officialImageUrl(new Url(officialImageUrl)) +// .build(); +// } +//} diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerTest.java index 8073c50e..cc884906 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerTest.java @@ -16,8 +16,6 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkSort; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.SubscriptionRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; import com.dreamypatisiel.devdevdev.global.common.TimeProvider; import com.dreamypatisiel.devdevdev.global.constant.SecurityConstant; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; @@ -72,7 +70,7 @@ class MyPageControllerTest extends SupportControllerTest { private static final int TEST_ARTICLES_COUNT = 20; - private static List techArticles; + private static final List techArticles = new ArrayList<>(); @Autowired ApplicationContext applicationContext; @@ -109,40 +107,21 @@ class MyPageControllerTest extends SupportControllerTest { @BeforeAll static void setup(@Autowired TechArticleRepository techArticleRepository, - @Autowired CompanyRepository companyRepository, - @Autowired ElasticTechArticleRepository elasticTechArticleRepository) { + @Autowired CompanyRepository companyRepository) { Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", "https://example.com"); companyRepository.save(company); - // 엘라스틱 기술블로그 데이터를 최신순->오래된순, 조회수많은순->적은순, 댓글많은순->적은순의 순서로 생성한다. - LocalDate baseDate = LocalDate.of(2024, 8, 30); - List elasticTechArticles = new ArrayList<>(); - for (int i = 1; i <= TEST_ARTICLES_COUNT; i++) { - ElasticTechArticle elasticTechArticle = createElasticTechArticle("elasticId_" + i, "타이틀_" + i, - baseDate.minusDays(i), "내용", "http://example.com/" + i, "설명", "http://example.com/", "작성자", - company.getName().getCompanyName(), company.getId(), (long) TEST_ARTICLES_COUNT - i, - (long) TEST_ARTICLES_COUNT - i, (long) TEST_ARTICLES_COUNT - i, - (long) (TEST_ARTICLES_COUNT - i) * 10); - elasticTechArticles.add(elasticTechArticle); - } - Iterable elasticTechArticleIterable = elasticTechArticleRepository.saveAll( - elasticTechArticles); - - // 엘라스틱 기술블로그를 토대로 RDB 기술블로그 데이터를 생성한다. - techArticles = new ArrayList<>(); - for (ElasticTechArticle elasticTechArticle : elasticTechArticleIterable) { - TechArticle techArticle = TechArticle.createTechArticle(elasticTechArticle, company); + for (int i = 0; i < 10; i++) { + TechArticle techArticle = createTechArticle(i, company); techArticles.add(techArticle); } techArticleRepository.saveAll(techArticles); } @AfterAll - static void tearDown(@Autowired ElasticTechArticleRepository elasticTechArticleRepository, - @Autowired TechArticleRepository techArticleRepository, + static void tearDown(@Autowired TechArticleRepository techArticleRepository, @Autowired CompanyRepository companyRepository) { - elasticTechArticleRepository.deleteAll(); techArticleRepository.deleteAllInBatch(); companyRepository.deleteAllInBatch(); } @@ -181,7 +160,6 @@ void getBookmarkedTechArticles() throws Exception { .andExpect(jsonPath("$.data").isNotEmpty()) .andExpect(jsonPath("$.data.content").isArray()) .andExpect(jsonPath("$.data.content.[0].id").isNumber()) - .andExpect(jsonPath("$.data.content.[0].elasticId").isString()) .andExpect(jsonPath("$.data.content.[0].thumbnailUrl").isString()) .andExpect(jsonPath("$.data.content.[0].techArticleUrl").isString()) .andExpect(jsonPath("$.data.content.[0].title").isString()) @@ -853,30 +831,6 @@ private static LocalDate createRandomDate() { return startDate.plusDays(randomDays); } - private static ElasticTechArticle createElasticTechArticle(String id, String title, LocalDate regDate, - String contents, String techArticleUrl, - String description, String thumbnailUrl, String author, - String company, Long companyId, - Long viewTotalCount, Long recommendTotalCount, - Long commentTotalCount, Long popularScore) { - return ElasticTechArticle.builder() - .id(id) - .title(title) - .regDate(regDate) - .contents(contents) - .techArticleUrl(techArticleUrl) - .description(description) - .thumbnailUrl(thumbnailUrl) - .author(author) - .company(company) - .companyId(companyId) - .viewTotalCount(viewTotalCount) - .recommendTotalCount(recommendTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(popularScore) - .build(); - } - private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, String careerUrl) { return Company.builder() @@ -898,4 +852,20 @@ private static Company createCompany(String companyName, String officialUrl, Str .industry(industry) .build(); } + + private static TechArticle createTechArticle(int i, Company company) { + return TechArticle.builder() + .title(new Title("타이틀 " + i)) + .contents("내용 " + i) + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(i)) + .recommendTotalCount(new Count(i)) + .viewTotalCount(new Count(i)) + .popularScore(new Count(i)) + .build(); + } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/notification/NotificationControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/notification/NotificationControllerTest.java index 7759f1c8..c540a873 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/notification/NotificationControllerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/notification/NotificationControllerTest.java @@ -172,7 +172,7 @@ void getNotifications() throws Exception { // given PageRequest pageable = PageRequest.of(0, 1); TechArticleMainResponse techArticleMainResponse = createTechArticleMainResponse( - 1L, "elasticId", "http://thumbnailUrl.com", false, + 1L, "http://thumbnailUrl.com", false, "http://techArticleUrl.com", "기술블로그 타이틀", "기술블로그 내용", 1L, "기업명", "http://careerUrl.com","http://officialImage.com", LocalDate.now(), "작성자", 0L, 0L, 0L, false, null @@ -200,7 +200,6 @@ void getNotifications() throws Exception { .andExpect(jsonPath("$.data.content.[0].isRead").isBoolean()) .andExpect(jsonPath("$.data.content.[0].techArticle").isNotEmpty()) .andExpect(jsonPath("$.data.content.[0].techArticle.id").isNumber()) - .andExpect(jsonPath("$.data.content.[0].techArticle.elasticId").isString()) .andExpect(jsonPath("$.data.content.[0].techArticle.thumbnailUrl").isString()) .andExpect(jsonPath("$.data.content.[0].techArticle.isLogoImage").isBoolean()) .andExpect(jsonPath("$.data.content.[0].techArticle.techArticleUrl").isString()) @@ -243,14 +242,13 @@ void getNotifications() throws Exception { verify(notificationService, times(1)).getNotifications(any(), anyLong(), any()); } - private TechArticleMainResponse createTechArticleMainResponse(Long id, String elasticId, String thumbnailUrl, Boolean isLogoImage, + private TechArticleMainResponse createTechArticleMainResponse(Long id, String thumbnailUrl, Boolean isLogoImage, String techArticleUrl, String title, String contents, Long companyId, String companyName, String careerUrl, String officialImageUrl, LocalDate regDate, String author, long recommendCount, long commentCount, long viewCount, Boolean isBookmarked, Float score) { return TechArticleMainResponse.builder() .id(id) - .elasticId(elasticId) .thumbnailUrl(thumbnailUrl) .isLogoImage(isLogoImage) .techArticleUrl(techArticleUrl) diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleCommentControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleCommentControllerTest.java index 8f61b87d..0381306a 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleCommentControllerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleCommentControllerTest.java @@ -41,6 +41,7 @@ import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; import jakarta.persistence.EntityManager; import java.nio.charset.StandardCharsets; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.Disabled; @@ -86,8 +87,7 @@ void registerTechCommentByAnonymous() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long id = techArticle.getId(); @@ -113,11 +113,7 @@ void registerTechCommentByMember() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long id = techArticle.getId(); @@ -151,8 +147,7 @@ void registerTechCommentByAnonymousMember() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long id = techArticle.getId(); @@ -186,8 +181,7 @@ void registerTechCommentNotFoundTechArticleException() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long id = savedTechArticle.getId() + 1; @@ -220,8 +214,7 @@ void registerTechCommentNotFoundMemberException() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long id = savedTechArticle.getId(); @@ -249,11 +242,7 @@ void registerTechCommentContentsIsNullException(String contents) throws Exceptio "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long id = savedTechArticle.getId(); @@ -286,9 +275,7 @@ void modifyTechComment() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -328,9 +315,7 @@ void modifyTechCommentContentsIsNullException(String contents) throws Exception member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -368,9 +353,7 @@ void modifyTechCommentNotFoundException() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -403,9 +386,7 @@ void modifyTechCommentAlreadyDeletedException() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -446,9 +427,7 @@ void deleteTechComment() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -484,9 +463,7 @@ void deleteTechCommentNotFoundException() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -513,8 +490,7 @@ void registerRepliedTechComment() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -567,8 +543,7 @@ void registerRepliedTechCommentContentsIsNullException(String contents) throws E "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long techArticleId = savedTechArticle.getId(); @@ -616,8 +591,7 @@ void getTechComments() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -733,9 +707,7 @@ void recommendTechComment() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); @@ -783,9 +755,7 @@ void getTechBestComments() throws Exception { companyRepository.save(company); // 기술 블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); // 댓글 생성 @@ -871,9 +841,7 @@ void getTechBestCommentsAnonymous() throws Exception { companyRepository.save(company); // 기술 블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); // 댓글 생성 @@ -1003,4 +971,20 @@ private static TechComment createRepliedTechComment(CommentContents contents, Me .parent(parent) .build(); } + + private static TechArticle createTechArticle(Company company) { + return TechArticle.builder() + .title(new Title("타이틀 ")) + .contents("내용 ") + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/article.png")) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(1)) + .build(); + } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java index 8fa978b6..afc18deb 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java @@ -3,8 +3,6 @@ import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.INVALID_MEMBER_NOT_FOUND_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.KEYWORD_WITH_SPECIAL_SYMBOLS_EXCEPTION_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_CURSOR_SCORE_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_ID_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_TECH_ARTICLE_MESSAGE; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -27,8 +25,6 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; import com.dreamypatisiel.devdevdev.global.constant.SecurityConstant; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; import com.dreamypatisiel.devdevdev.web.controller.SupportControllerTest; @@ -60,49 +56,28 @@ class TechArticleControllerTest extends SupportControllerTest { @Autowired TechArticleRepository techArticleRepository; @Autowired - ElasticTechArticleRepository elasticTechArticleRepository; - @Autowired MemberRepository memberRepository; @Autowired BookmarkRepository bookmarkRepository; @BeforeAll static void setup(@Autowired TechArticleRepository techArticleRepository, - @Autowired CompanyRepository companyRepository, - @Autowired ElasticTechArticleRepository elasticTechArticleRepository) { + @Autowired CompanyRepository companyRepository) { company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", "https://example.com"); companyRepository.save(company); - // 엘라스틱 기술블로그 데이터를 최신순->오래된순, 조회수많은순->적은순, 댓글많은순->적은순의 순서로 생성한다. - LocalDate baseDate = LocalDate.of(2024, 8, 30); - List elasticTechArticles = new ArrayList<>(); - for (int i = 1; i <= TEST_ARTICLES_COUNT; i++) { - ElasticTechArticle elasticTechArticle = createElasticTechArticle("elasticId_" + i, "타이틀_" + i, - baseDate.minusDays(i), "내용", "http://example.com/" + i, "설명", "http://example.com/", "작성자", - company.getName().getCompanyName(), company.getId(), (long) TEST_ARTICLES_COUNT - i, - (long) TEST_ARTICLES_COUNT - i, (long) TEST_ARTICLES_COUNT - i, - (long) (TEST_ARTICLES_COUNT - i) * 10); - elasticTechArticles.add(elasticTechArticle); - } - Iterable elasticTechArticleIterable = elasticTechArticleRepository.saveAll( - elasticTechArticles); - - // 엘라스틱 기술블로그를 토대로 RDB 기술블로그 데이터를 생성한다. - techArticles = new ArrayList<>(); - for (ElasticTechArticle elasticTechArticle : elasticTechArticleIterable) { - TechArticle techArticle = TechArticle.createTechArticle(elasticTechArticle, company); + List techArticles = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + TechArticle techArticle = createTechArticle(i, company); techArticles.add(techArticle); } - List savedTechArticles = techArticleRepository.saveAll(techArticles); - firstTechArticle = savedTechArticles.getFirst(); + techArticleRepository.saveAll(techArticles); } @AfterAll static void tearDown(@Autowired TechArticleRepository techArticleRepository, - @Autowired ElasticTechArticleRepository elasticTechArticleRepository, @Autowired CompanyRepository companyRepository) { - elasticTechArticleRepository.deleteAll(); techArticleRepository.deleteAllInBatch(); companyRepository.deleteAllInBatch(); } @@ -112,7 +87,7 @@ static void tearDown(@Autowired TechArticleRepository techArticleRepository, void getTechArticlesByAnonymous() throws Exception { // given Pageable pageable = PageRequest.of(0, 10); - String elasticId = "elasticId_1"; + String techArticleId = "1"; String keyword = "타이틀"; String companyId = company.getId().toString(); @@ -121,7 +96,7 @@ void getTechArticlesByAnonymous() throws Exception { .queryParam("size", String.valueOf(pageable.getPageSize())) .queryParam("techArticleSort", TechArticleSort.LATEST.name()) .queryParam("keyword", keyword) - .queryParam("elasticId", elasticId) + .queryParam("techArticleId", techArticleId) .queryParam("companyId", companyId) .queryParam("score", "10") .contentType(MediaType.APPLICATION_JSON) @@ -132,7 +107,6 @@ void getTechArticlesByAnonymous() throws Exception { .andExpect(jsonPath("$.data").isNotEmpty()) .andExpect(jsonPath("$.data.content").isArray()) .andExpect(jsonPath("$.data.content.[0].id").isNumber()) - .andExpect(jsonPath("$.data.content.[0].elasticId").isString()) .andExpect(jsonPath("$.data.content.[0].thumbnailUrl").isString()) .andExpect(jsonPath("$.data.content.[0].techArticleUrl").isString()) .andExpect(jsonPath("$.data.content.[0].title").isString()) @@ -190,7 +164,7 @@ void getTechArticlesByMember() throws Exception { bookmarkRepository.saveAll(bookmarks); Pageable pageable = PageRequest.of(0, 10); - String elasticId = "elasticId_1"; + String techArticleId = "1"; String keyword = "타이틀"; String companyId = company.getId().toString(); @@ -199,7 +173,7 @@ void getTechArticlesByMember() throws Exception { .queryParam("size", String.valueOf(pageable.getPageSize())) .queryParam("techArticleSort", TechArticleSort.LATEST.name()) .queryParam("keyword", keyword) - .queryParam("elasticId", elasticId) + .queryParam("techArticleId", techArticleId) .queryParam("companyId", companyId) .queryParam("score", "10") .contentType(MediaType.APPLICATION_JSON) @@ -211,7 +185,6 @@ void getTechArticlesByMember() throws Exception { .andExpect(jsonPath("$.data").isNotEmpty()) .andExpect(jsonPath("$.data.content").isArray()) .andExpect(jsonPath("$.data.content.[0].id").isNumber()) - .andExpect(jsonPath("$.data.content.[0].elasticId").isString()) .andExpect(jsonPath("$.data.content.[0].thumbnailUrl").isString()) .andExpect(jsonPath("$.data.content.[0].techArticleUrl").isString()) .andExpect(jsonPath("$.data.content.[0].title").isString()) @@ -251,42 +224,20 @@ void getTechArticlesByMember() throws Exception { } @Test - @DisplayName("기술블로그 메인을 조회할 때 존재하지 않는 엘라스틱서치 ID를 조회하면 에러가 발생한다.") - void getTechArticlesNotFoundElasticIdException() throws Exception { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when // then - mockMvc.perform(get("/devdevdev/api/v1/articles") - .queryParam("size", String.valueOf(pageable.getPageSize())) - .queryParam("techArticleSort", TechArticleSort.LATEST.name()) - .queryParam("elasticId", "elasticId") - .contentType(MediaType.APPLICATION_JSON) - .characterEncoding(StandardCharsets.UTF_8)) - .andDo(print()) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.resultType").value(ResultType.FAIL.name())) - .andExpect(jsonPath("$.message").value(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE)) - .andExpect(jsonPath("$.errorCode").value(HttpStatus.NOT_FOUND.value())); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때," + + @DisplayName("커서 방식으로 다음 페이지의 기술블로그를 검색어로 검색할 때," + "정확도 내림차순으로 조회하기 위한 점수가 없다면 예외가 발생한다.") void getTechArticlesWithKeywordWithCursorOrderByHIGHEST_SCOREWithoutScoreException() throws Exception { // given Pageable prevPageable = PageRequest.of(0, 1); Pageable pageable = PageRequest.of(0, 10); - List elasticTechArticles = elasticTechArticleRepository.findAll(prevPageable).stream() - .toList(); - ElasticTechArticle cursor = elasticTechArticles.getLast(); + String techArticleId = "1"; String keyword = "타이틀"; // when // then mockMvc.perform(get("/devdevdev/api/v1/articles") .queryParam("size", String.valueOf(pageable.getPageSize())) .queryParam("techArticleSort", TechArticleSort.HIGHEST_SCORE.name()) - .queryParam("elasticId", cursor.getId()) + .queryParam("techArticleId", techArticleId) .queryParam("keyword", keyword) .contentType(MediaType.APPLICATION_JSON) .characterEncoding(StandardCharsets.UTF_8)) @@ -336,7 +287,6 @@ void getTechArticleByAnonymous() throws Exception { .andExpect(jsonPath("$.resultType").value(ResultType.SUCCESS.name())) .andExpect(jsonPath("$.data").isNotEmpty()) .andExpect(jsonPath("$.data").isMap()) - .andExpect(jsonPath("$.data.elasticId").isString()) .andExpect(jsonPath("$.data.thumbnailUrl").isString()) .andExpect(jsonPath("$.data.techArticleUrl").isString()) .andExpect(jsonPath("$.data.title").isString()) @@ -376,7 +326,6 @@ void getTechArticleByMember() throws Exception { .andExpect(jsonPath("$.resultType").value(ResultType.SUCCESS.name())) .andExpect(jsonPath("$.data").isNotEmpty()) .andExpect(jsonPath("$.data").isMap()) - .andExpect(jsonPath("$.data.elasticId").isString()) .andExpect(jsonPath("$.data.thumbnailUrl").isString()) .andExpect(jsonPath("$.data.techArticleUrl").isString()) .andExpect(jsonPath("$.data.title").isString()) @@ -417,11 +366,7 @@ void getTechArticleNotFoundMemberException() throws Exception { @DisplayName("기술블로그 상세를 조회할 때 기술블로그가 존재하지 않으면 예외가 발생한다.") void getTechArticleNotFoundTechArticleException() throws Exception { // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); + TechArticle techArticle = createTechArticle(1, company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long id = savedTechArticle.getId() + 1; @@ -436,51 +381,6 @@ void getTechArticleNotFoundTechArticleException() throws Exception { .andExpect(jsonPath("$.errorCode").value(HttpStatus.NOT_FOUND.value())); } - @Test - @DisplayName("기술블로그 상세를 조회할 때 엘라스틱ID가 존재하지 않으면 예외가 발생한다.") - void getTechArticleNotFoundElasticIdException() throws Exception { - // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); - - // when // then - mockMvc.perform(get("/devdevdev/api/v1/articles/{id}", id) - .contentType(MediaType.APPLICATION_JSON) - .characterEncoding(StandardCharsets.UTF_8)) - .andDo(print()) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.resultType").value(ResultType.FAIL.name())) - .andExpect(jsonPath("$.message").value(NOT_FOUND_ELASTIC_ID_MESSAGE)) - .andExpect(jsonPath("$.errorCode").value(HttpStatus.BAD_REQUEST.value())); - } - - @Test - @DisplayName("기술블로그 상세를 조회할 때 엘라스틱 기술블로그가 존재하지 않으면 예외가 발생한다.") - void getTechArticleNotFoundElasticTechArticleException() throws Exception { - // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), "elasticId", company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); - - // when // then - mockMvc.perform(get("/devdevdev/api/v1/articles/{id}", id) - .contentType(MediaType.APPLICATION_JSON) - .characterEncoding(StandardCharsets.UTF_8)) - .andDo(print()) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.resultType").value(ResultType.FAIL.name())) - .andExpect(jsonPath("$.message").value(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE)) - .andExpect(jsonPath("$.errorCode").value(HttpStatus.NOT_FOUND.value())); - } - @Test @DisplayName("회원이 기술블로그 북마크를 요청한다.") void updateBookmark() throws Exception { @@ -510,11 +410,7 @@ void updateBookmark() throws Exception { @DisplayName("회원이 기술블로그 북마크를 요청할 때 존재하지 않는 기술블로그라면 예외가 발생한다.") void updateBookmarkNotFoundTechArticleException() throws Exception { // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); + TechArticle techArticle = createTechArticle(1, company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long id = savedTechArticle.getId() + 1; @@ -585,11 +481,7 @@ void updateRecommend() throws Exception { @DisplayName("회원이 기술블로그 추천을 요청할 때 존재하지 않는 기술블로그라면 예외가 발생한다.") void updateRecommendNotFoundTechArticleException() throws Exception { // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); + TechArticle techArticle = createTechArticle(1, company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long id = savedTechArticle.getId() + 1; @@ -665,30 +557,6 @@ private static LocalDate createRandomDate() { return startDate.plusDays(randomDays); } - private static ElasticTechArticle createElasticTechArticle(String id, String title, LocalDate regDate, - String contents, String techArticleUrl, - String description, String thumbnailUrl, String author, - String company, Long companyId, - Long viewTotalCount, Long recommendTotalCount, - Long commentTotalCount, Long popularScore) { - return ElasticTechArticle.builder() - .id(id) - .title(title) - .regDate(regDate) - .contents(contents) - .techArticleUrl(techArticleUrl) - .description(description) - .thumbnailUrl(thumbnailUrl) - .author(author) - .company(company) - .companyId(companyId) - .viewTotalCount(viewTotalCount) - .recommendTotalCount(recommendTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(popularScore) - .build(); - } - private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, String careerUrl) { return Company.builder() @@ -698,4 +566,20 @@ private static Company createCompany(String companyName, String officialImageUrl .officialUrl(new Url(officialUrl)) .build(); } + + private static TechArticle createTechArticle(int i, Company company) { + return TechArticle.builder() + .title(new Title("타이틀 " + i)) + .contents("내용 " + i) + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(i)) + .recommendTotalCount(new Count(i)) + .viewTotalCount(new Count(i)) + .popularScore(new Count(i)) + .build(); + } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsTest.java index 0473a739..8194ce72 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsTest.java @@ -16,8 +16,6 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkSort; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.SubscriptionRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; import com.dreamypatisiel.devdevdev.global.constant.SecurityConstant; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; @@ -73,15 +71,13 @@ public class MyPageControllerDocsTest extends SupportControllerDocsTest { private static final int TEST_ARTICLES_COUNT = 20; - private static List techArticles; + private static final List techArticles = new ArrayList<>(); @Autowired TechArticleRepository techArticleRepository; @Autowired CompanyRepository companyRepository; @Autowired - ElasticTechArticleRepository elasticTechArticleRepository; - @Autowired MemberRepository memberRepository; @Autowired BookmarkRepository bookmarkRepository; @@ -108,30 +104,13 @@ public class MyPageControllerDocsTest extends SupportControllerDocsTest { @BeforeAll static void setup(@Autowired TechArticleRepository techArticleRepository, - @Autowired CompanyRepository companyRepository, - @Autowired ElasticTechArticleRepository elasticTechArticleRepository) { + @Autowired CompanyRepository companyRepository) { Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", "https://example.com"); companyRepository.save(company); - // 엘라스틱 기술블로그 데이터를 최신순->오래된순, 조회수많은순->적은순, 댓글많은순->적은순의 순서로 생성한다. - LocalDate baseDate = LocalDate.of(2024, 8, 30); - List elasticTechArticles = new ArrayList<>(); - for (int i = 1; i <= TEST_ARTICLES_COUNT; i++) { - ElasticTechArticle elasticTechArticle = createElasticTechArticle("elasticId_" + i, "타이틀_" + i, - baseDate.minusDays(i), "내용", "http://example.com/" + i, "설명", "http://example.com/", "작성자", - company.getName().getCompanyName(), company.getId(), (long) TEST_ARTICLES_COUNT - i, - (long) TEST_ARTICLES_COUNT - i, (long) TEST_ARTICLES_COUNT - i, - (long) (TEST_ARTICLES_COUNT - i) * 10); - elasticTechArticles.add(elasticTechArticle); - } - Iterable elasticTechArticleIterable = elasticTechArticleRepository.saveAll( - elasticTechArticles); - - // 엘라스틱 기술블로그를 토대로 RDB 기술블로그 데이터를 생성한다. - techArticles = new ArrayList<>(); - for (ElasticTechArticle elasticTechArticle : elasticTechArticleIterable) { - TechArticle techArticle = TechArticle.createTechArticle(elasticTechArticle, company); + for (int i = 0; i < 10; i++) { + TechArticle techArticle = createTechArticle(i, company); techArticles.add(techArticle); } techArticleRepository.saveAll(techArticles); @@ -139,9 +118,7 @@ static void setup(@Autowired TechArticleRepository techArticleRepository, @AfterAll static void tearDown(@Autowired TechArticleRepository techArticleRepository, - @Autowired ElasticTechArticleRepository elasticTechArticleRepository, @Autowired CompanyRepository companyRepository) { - elasticTechArticleRepository.deleteAll(); techArticleRepository.deleteAllInBatch(); companyRepository.deleteAllInBatch(); } @@ -195,8 +172,6 @@ void getBookmarkedTechArticles() throws Exception { fieldWithPath("data.content").type(JsonFieldType.ARRAY).description("기술블로그 메인 배열"), fieldWithPath("data.content.[].id").type(JsonFieldType.NUMBER).description("기술블로그 아이디"), - fieldWithPath("data.content.[].elasticId").type(JsonFieldType.STRING) - .description("기술블로그 엘라스틱서치 아이디"), fieldWithPath("data.content.[].techArticleUrl").type(JsonFieldType.STRING) .description("기술블로그 Url"), fieldWithPath("data.content.[].thumbnailUrl").type(JsonFieldType.STRING) @@ -971,30 +946,6 @@ private static LocalDate createRandomDate() { return startDate.plusDays(randomDays); } - private static ElasticTechArticle createElasticTechArticle(String id, String title, LocalDate regDate, - String contents, String techArticleUrl, - String description, String thumbnailUrl, String author, - String company, Long companyId, - Long viewTotalCount, Long recommendTotalCount, - Long commentTotalCount, Long popularScore) { - return ElasticTechArticle.builder() - .id(id) - .title(title) - .regDate(regDate) - .contents(contents) - .techArticleUrl(techArticleUrl) - .description(description) - .thumbnailUrl(thumbnailUrl) - .author(author) - .company(company) - .companyId(companyId) - .viewTotalCount(viewTotalCount) - .recommendTotalCount(recommendTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(popularScore) - .build(); - } - private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, String careerUrl) { return Company.builder() @@ -1016,4 +967,20 @@ private static Company createCompany(String companyName, String officialUrl, Str .industry(industry) .build(); } + + private static TechArticle createTechArticle(int i, Company company) { + return TechArticle.builder() + .title(new Title("타이틀 " + i)) + .contents("내용 " + i) + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(i)) + .recommendTotalCount(new Count(i)) + .viewTotalCount(new Count(i)) + .popularScore(new Count(i)) + .build(); + } } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/NotificationControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/NotificationControllerDocsTest.java index 64bf1939..76ad5b56 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/NotificationControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/NotificationControllerDocsTest.java @@ -349,7 +349,7 @@ void getNotifications() throws Exception { // given PageRequest pageable = PageRequest.of(0, 1); TechArticleMainResponse techArticleMainResponse = createTechArticleMainResponse( - 1L, "elasticId", "http://thumbnailUrl.com", false, + 1L, "http://thumbnailUrl.com", false, "http://techArticleUrl.com", "기술블로그 타이틀", "기술블로그 내용", 1L, "기업명", "http://careerUrl.com", "http://officialImage.com", LocalDate.now(), "작성자", 0L, 0L, 0L, false, null @@ -392,7 +392,6 @@ void getNotifications() throws Exception { fieldWithPath("data.content[].isRead").type(BOOLEAN).description("회원의 알림 읽음 여부"), fieldWithPath("data.content[].techArticle").type(OBJECT).description("기술블로그 정보"), fieldWithPath("data.content[].techArticle.id").type(NUMBER).description("기술블로그 ID"), - fieldWithPath("data.content[].techArticle.elasticId").type(STRING).description("엘라스틱서치 ID"), fieldWithPath("data.content[].techArticle.thumbnailUrl").type(STRING).description("썸네일 URL"), fieldWithPath("data.content[].techArticle.isLogoImage").type(BOOLEAN).description("로고 이미지 여부"), fieldWithPath("data.content[].techArticle.techArticleUrl").type(STRING).description("기술블로그 URL"), @@ -507,14 +506,13 @@ void publishNotificationsNotFoundException() throws Exception { .andExpect(jsonPath("$.errorCode").isNumber()); } - private TechArticleMainResponse createTechArticleMainResponse(Long id, String elasticId, String thumbnailUrl, Boolean isLogoImage, + private TechArticleMainResponse createTechArticleMainResponse(Long id, String thumbnailUrl, Boolean isLogoImage, String techArticleUrl, String title, String contents, Long companyId, String companyName, String careerUrl, String officialImageUrl, LocalDate regDate, String author, long recommendCount, long commentCount, long viewCount, Boolean isBookmarked, Float score) { return TechArticleMainResponse.builder() .id(id) - .elasticId(elasticId) .thumbnailUrl(thumbnailUrl) .isLogoImage(isLogoImage) .techArticleUrl(techArticleUrl) diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java index 3d192657..0f48c41c 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java @@ -60,6 +60,7 @@ import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; import jakarta.persistence.EntityManager; import java.nio.charset.StandardCharsets; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.Disabled; @@ -107,8 +108,7 @@ void registerTechCommentByAnonymous() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long id = techArticle.getId(); @@ -149,8 +149,7 @@ void registerTechComment() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long id = techArticle.getId(); @@ -206,8 +205,7 @@ void registerTechCommentNotFoundTechArticleException() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long id = techArticle.getId() + 1; @@ -255,11 +253,7 @@ void registerTechCommentNotFoundMemberException() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long id = techArticle.getId(); @@ -303,8 +297,7 @@ void registerTechCommentContentsIsNullException(String contents) throws Exceptio "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long techArticleId = savedTechArticle.getId(); @@ -352,8 +345,7 @@ void modifyTechComment() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -416,8 +408,7 @@ void modifyTechCommentContentsIsNullException(String contents) throws Exception member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -471,8 +462,7 @@ void modifyTechCommentNotFoundException() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -521,8 +511,7 @@ void deleteTechComment() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -578,8 +567,7 @@ void deleteTechCommentNotFoundException() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -622,8 +610,7 @@ void registerTechReply() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -699,9 +686,7 @@ void registerTechReplyContentsIsNullException(String contents) throws Exception "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long techArticleId = savedTechArticle.getId(); @@ -762,8 +747,7 @@ void getTechComments() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -942,9 +926,7 @@ void recommendTechComment() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); @@ -1001,9 +983,7 @@ void recommendTechCommentNotFoundTechComment() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); @@ -1065,9 +1045,7 @@ void getTechBestComments() throws Exception { companyRepository.save(company); // 기술 블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); // 댓글 생성 @@ -1168,4 +1146,20 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), ) )); } + + private TechArticle createTechArticle(Company company) { + return TechArticle.builder() + .title(new Title("타이틀 ")) + .contents("내용 ") + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(1)) + .build(); + } } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java index e41092db..1c7b346e 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java @@ -37,8 +37,6 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; import com.dreamypatisiel.devdevdev.global.constant.SecurityConstant; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; @@ -49,6 +47,7 @@ import java.util.List; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; @@ -74,49 +73,28 @@ public class TechArticleControllerDocsTest extends SupportControllerDocsTest { @Autowired CompanyRepository companyRepository; @Autowired - ElasticTechArticleRepository elasticTechArticleRepository; - @Autowired MemberRepository memberRepository; @Autowired BookmarkRepository bookmarkRepository; @BeforeAll static void setup(@Autowired TechArticleRepository techArticleRepository, - @Autowired CompanyRepository companyRepository, - @Autowired ElasticTechArticleRepository elasticTechArticleRepository) { + @Autowired CompanyRepository companyRepository) { company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", "https://example.com"); companyRepository.save(company); - // 엘라스틱 기술블로그 데이터를 최신순->오래된순, 조회수많은순->적은순, 댓글많은순->적은순의 순서로 생성한다. - LocalDate baseDate = LocalDate.of(2024, 8, 30); - List elasticTechArticles = new ArrayList<>(); - for (int i = 1; i <= TEST_ARTICLES_COUNT; i++) { - ElasticTechArticle elasticTechArticle = createElasticTechArticle("elasticId_" + i, "타이틀_" + i, - baseDate.minusDays(i), "내용", "http://example.com/" + i, "설명", "http://example.com/", "작성자", - company.getName().getCompanyName(), company.getId(), (long) TEST_ARTICLES_COUNT - i, - (long) TEST_ARTICLES_COUNT - i, (long) TEST_ARTICLES_COUNT - i, - (long) (TEST_ARTICLES_COUNT - i) * 10); - elasticTechArticles.add(elasticTechArticle); - } - Iterable elasticTechArticleIterable = elasticTechArticleRepository.saveAll( - elasticTechArticles); - - // 엘라스틱 기술블로그를 토대로 RDB 기술블로그 데이터를 생성한다. - techArticles = new ArrayList<>(); - for (ElasticTechArticle elasticTechArticle : elasticTechArticleIterable) { - TechArticle techArticle = TechArticle.createTechArticle(elasticTechArticle, company); + List techArticles = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + TechArticle techArticle = createTechArticle(i, company); techArticles.add(techArticle); } - List savedTechArticles = techArticleRepository.saveAll(techArticles); - firstTechArticle = savedTechArticles.getFirst(); + techArticleRepository.saveAll(techArticles); } @AfterAll static void tearDown(@Autowired TechArticleRepository techArticleRepository, - @Autowired ElasticTechArticleRepository elasticTechArticleRepository, @Autowired CompanyRepository companyRepository) { - elasticTechArticleRepository.deleteAll(); techArticleRepository.deleteAllInBatch(); companyRepository.deleteAllInBatch(); } @@ -141,7 +119,7 @@ void getTechArticlesByMember() throws Exception { bookmarkRepository.saveAll(bookmarks); Pageable pageable = PageRequest.of(0, 1); - String elasticId = "elasticId_1"; + String techArticleId = "1"; String keyword = "타이틀"; String companyId = company.getId().toString(); @@ -150,7 +128,7 @@ void getTechArticlesByMember() throws Exception { .queryParam("size", String.valueOf(pageable.getPageSize())) .queryParam("techArticleSort", TechArticleSort.HIGHEST_SCORE.name()) .queryParam("keyword", keyword) - .queryParam("elasticId", elasticId) + .queryParam("techArticleId", techArticleId) .queryParam("companyId", companyId) .queryParam("score", "10") .contentType(MediaType.APPLICATION_JSON) @@ -172,7 +150,7 @@ void getTechArticlesByMember() throws Exception { .attributes(techArticleSortType()), parameterWithName("keyword").optional().description("검색어"), parameterWithName("companyId").optional().description("회사 아이디"), - parameterWithName("elasticId").optional().description("마지막 데이터의 엘라스틱서치 아이디"), + parameterWithName("techArticleId").optional().description("마지막 데이터의 기술블로그 아이디"), parameterWithName("score").optional().description("마지막 데이터의 정확도 점수(정확도순 검색일 때에만 필수)") ), responseFields( @@ -181,8 +159,6 @@ void getTechArticlesByMember() throws Exception { fieldWithPath("data.content").type(JsonFieldType.ARRAY).description("기술블로그 메인 배열"), fieldWithPath("data.content.[].id").type(JsonFieldType.NUMBER).description("기술블로그 아이디"), - fieldWithPath("data.content.[].elasticId").type(JsonFieldType.STRING) - .description("기술블로그 엘라스틱서치 아이디"), fieldWithPath("data.content.[].techArticleUrl").type(JsonFieldType.STRING) .description("기술블로그 Url"), fieldWithPath("data.content.[].thumbnailUrl").type(JsonFieldType.STRING) @@ -246,46 +222,19 @@ void getTechArticlesByMember() throws Exception { } @Test - @DisplayName("기술블로그 메인을 조회할 때 존재하지 않는 엘라스틱서치 ID를 조회하면 에러가 발생한다.") - void getTechArticlesNotFoundElasticIdException() throws Exception { - // given - Pageable pageable = PageRequest.of(0, 1); - - // when // then - ResultActions actions = mockMvc.perform(get("/devdevdev/api/v1/articles") - .queryParam("size", String.valueOf(pageable.getPageSize())) - .queryParam("techArticleSort", TechArticleSort.LATEST.name()) - .queryParam("elasticId", "elasticId") - .contentType(MediaType.APPLICATION_JSON) - .characterEncoding(StandardCharsets.UTF_8)) - .andDo(print()) - .andExpect(status().isNotFound()); - - // Docs - actions.andDo(document("not-found-elastic-tech-article-cursor-exception", - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath("resultType").type(JsonFieldType.STRING).description("응답 결과"), - fieldWithPath("message").type(JsonFieldType.STRING).description("에러 메시지"), - fieldWithPath("errorCode").type(JsonFieldType.NUMBER).description("에러 코드") - ) - )); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때," + + @DisplayName("커서 방식으로 다음 페이지의 기술블로그를 검색어로 검색할 때," + "정확도 내림차순으로 조회하기 위한 점수가 없다면 예외가 발생한다.") void getTechArticlesWithKeywordWithCursorOrderByHIGHEST_SCOREWithoutScoreException() throws Exception { // given Pageable pageable = PageRequest.of(0, 10); - String elasticId = "elasticId_1"; + String techArticleId = "1"; String keyword = "타이틀"; // when // then ResultActions actions = mockMvc.perform(get("/devdevdev/api/v1/articles") .queryParam("size", String.valueOf(pageable.getPageSize())) .queryParam("techArticleSort", TechArticleSort.HIGHEST_SCORE.name()) - .queryParam("elasticId", elasticId) + .queryParam("techArticleId", techArticleId) .queryParam("keyword", keyword) .contentType(MediaType.APPLICATION_JSON) .characterEncoding(StandardCharsets.UTF_8)) @@ -366,8 +315,6 @@ void getTechArticleByMember() throws Exception { responseFields( fieldWithPath("resultType").type(JsonFieldType.STRING).description("응답 결과"), fieldWithPath("data").type(JsonFieldType.OBJECT).description("응답 데이터"), - - fieldWithPath("data.elasticId").type(JsonFieldType.STRING).description("기술블로그 엘라스틱서치 아이디"), fieldWithPath("data.techArticleUrl").type(JsonFieldType.STRING).description("기술블로그 Url"), fieldWithPath("data.thumbnailUrl").type(JsonFieldType.STRING).description("기술블로그 썸네일 이미지"), fieldWithPath("data.title").type(JsonFieldType.STRING).description("기술블로그 제목"), @@ -431,13 +378,9 @@ void getTechArticleNotFoundMemberException() throws Exception { @DisplayName("기술블로그 상세를 조회할 때 기술블로그가 존재하지 않으면 예외가 발생한다.") void getTechArticleNotFoundTechArticleException() throws Exception { // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId() + 1; + TechArticle techArticle = createTechArticle(1, company); + techArticleRepository.save(techArticle); + Long id = techArticle.getId() + 1; SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", "꿈빛파티시엘", "1234", email, socialType, role); @@ -463,66 +406,6 @@ void getTechArticleNotFoundTechArticleException() throws Exception { )); } - @Test - @DisplayName("기술블로그 상세를 조회할 때 엘라스틱ID가 존재하지 않으면 예외가 발생한다.") - void getTechArticleNotFoundElasticIdException() throws Exception { - // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); - - // when // then - ResultActions actions = mockMvc.perform(get("/devdevdev/api/v1/articles/{techArticleId}", id) - .contentType(MediaType.APPLICATION_JSON) - .characterEncoding(StandardCharsets.UTF_8) - .header(AUTHORIZATION_HEADER, SecurityConstant.BEARER_PREFIX + accessToken)) - .andDo(print()); - - // Docs - actions.andDo(document("not-found-elastic-id-exception", - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath("resultType").type(JsonFieldType.STRING).description("응답 결과"), - fieldWithPath("message").type(JsonFieldType.STRING).description("에러 메시지"), - fieldWithPath("errorCode").type(JsonFieldType.NUMBER).description("에러 코드") - ) - )); - } - - @Test - @DisplayName("기술블로그 상세를 조회할 때 엘라스틱 기술블로그가 존재하지 않으면 예외가 발생한다.") - void getTechArticleNotFoundElasticTechArticleException() throws Exception { - // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), "elasticId", company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); - - // when // then - ResultActions actions = mockMvc.perform(get("/devdevdev/api/v1/articles/{techArticleId}", id) - .contentType(MediaType.APPLICATION_JSON) - .characterEncoding(StandardCharsets.UTF_8) - .header(AUTHORIZATION_HEADER, SecurityConstant.BEARER_PREFIX + accessToken)) - .andDo(print()); - - // Docs - actions.andDo(document("not-found-elastic-tech-article-exception", - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath("resultType").type(JsonFieldType.STRING).description("응답 결과"), - fieldWithPath("message").type(JsonFieldType.STRING).description("에러 메시지"), - fieldWithPath("errorCode").type(JsonFieldType.NUMBER).description("에러 코드") - ) - )); - } - @Test @DisplayName("회원이 기술블로그 북마크를 요청한다.") void updateBookmark() throws Exception { @@ -637,30 +520,6 @@ private static LocalDate createRandomDate() { return startDate.plusDays(randomDays); } - private static ElasticTechArticle createElasticTechArticle(String id, String title, LocalDate regDate, - String contents, String techArticleUrl, - String description, String thumbnailUrl, String author, - String company, Long companyId, - Long viewTotalCount, Long recommendTotalCount, - Long commentTotalCount, Long popularScore) { - return ElasticTechArticle.builder() - .id(id) - .title(title) - .regDate(regDate) - .contents(contents) - .techArticleUrl(techArticleUrl) - .description(description) - .thumbnailUrl(thumbnailUrl) - .author(author) - .company(company) - .companyId(companyId) - .viewTotalCount(viewTotalCount) - .recommendTotalCount(recommendTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(popularScore) - .build(); - } - private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, String careerUrl) { return Company.builder() @@ -670,4 +529,20 @@ private static Company createCompany(String companyName, String officialImageUrl .officialUrl(new Url(officialUrl)) .build(); } + + private static TechArticle createTechArticle(int i, Company company) { + return TechArticle.builder() + .title(new Title("타이틀 " + i)) + .contents("내용 " + i) + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(i)) + .recommendTotalCount(new Count(i)) + .viewTotalCount(new Count(i)) + .popularScore(new Count(i)) + .build(); + } } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponseTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponseTest.java index ae8b1691..ba79b0f5 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponseTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponseTest.java @@ -6,7 +6,6 @@ import com.dreamypatisiel.devdevdev.domain.entity.embedded.Count; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Title; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; import java.time.LocalDate; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -17,25 +16,15 @@ class TechArticleMainResponseTest { @Test - @DisplayName("ElasticTechArticle의 썸네일 이미지가 있다면 썸네일 이미지로 설정되어야 한다.") + @DisplayName("기술블로그의 썸네일 이미지가 있다면 썸네일 이미지로 설정되어야 한다.") public void setThumbnailImageWhenPresent() { // given Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://officialUrl.com", "https://careerUrl.com"); - - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); - - ElasticTechArticle elasticTechArticle = createElasticTechArticle("elasticId", "타이틀", LocalDate.now(), - "내용", "http://example.com/", "설명", "http://thumbnailImage.com/image.png", "작성자", - company.getName().getCompanyName(), company.getId(), 0L, 0L, 0L, 0L); - - CompanyResponse companyResponse = CompanyResponse.from(company); + TechArticle techArticle = createTechArticle(company, new Url("http://thumbnailImage.com/image.png")); // when - TechArticleMainResponse techArticleMainResponse = TechArticleMainResponse - .of(techArticle, elasticTechArticle, companyResponse); + TechArticleMainResponse techArticleMainResponse = TechArticleMainResponse.of(techArticle); // then assertEquals("http://thumbnailImage.com/image.png", techArticleMainResponse.getThumbnailUrl()); @@ -43,25 +32,15 @@ public void setThumbnailImageWhenPresent() { } @Test - @DisplayName("ElasticTechArticle의 썸네일 이미지가 없다면 회사 로고 이미지로 대체하고, isLogoImage가 true로 설정되어야 한다.") + @DisplayName("기술블로그의 썸네일 이미지가 없다면 회사 로고 이미지로 대체하고, isLogoImage가 true로 설정되어야 한다.") public void setLogoImageWhenThumbnailIsAbsent() { // given Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://officialUrl.com", "https://careerUrl.com"); - - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); - - ElasticTechArticle elasticTechArticle = createElasticTechArticle("elasticId", "타이틀", LocalDate.now(), - "내용", "http://example.com/", "설명", null, "작성자", - company.getName().getCompanyName(), company.getId(), 0L, 0L, 0L, 0L); - - CompanyResponse companyResponse = CompanyResponse.from(company); + TechArticle techArticle = createTechArticle(company, null); // when - TechArticleMainResponse techArticleMainResponse = TechArticleMainResponse - .of(techArticle, elasticTechArticle, companyResponse); + TechArticleMainResponse techArticleMainResponse = TechArticleMainResponse.of(techArticle); // then assertEquals(company.getOfficialImageUrl().getUrl(), techArticleMainResponse.getThumbnailUrl()); @@ -78,27 +57,19 @@ private static Company createCompany(String companyName, String officialImageUrl .build(); } - private static ElasticTechArticle createElasticTechArticle(String id, String title, LocalDate regDate, - String contents, String techArticleUrl, - String description, String thumbnailUrl, String author, - String company, Long companyId, - Long viewTotalCount, Long recommendTotalCount, - Long commentTotalCount, Long popularScore) { - return ElasticTechArticle.builder() - .id(id) - .title(title) - .regDate(regDate) - .contents(contents) - .techArticleUrl(techArticleUrl) - .description(description) - .thumbnailUrl(thumbnailUrl) - .author(author) + private TechArticle createTechArticle(Company company, Url thumbnailUrl) { + return TechArticle.builder() + .title(new Title("타이틀 ")) + .contents("내용 ") .company(company) - .companyId(companyId) - .viewTotalCount(viewTotalCount) - .recommendTotalCount(recommendTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(popularScore) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(thumbnailUrl) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(1)) .build(); } } \ No newline at end of file From e2d8b9e6df31742d579890d45802849c20af1d05 Mon Sep 17 00:00:00 2001 From: soyoung Date: Wed, 27 Aug 2025 20:58:01 +0900 Subject: [PATCH 59/66] =?UTF-8?q?fix:=20=EA=B8=B0=EC=88=A0=EB=B8=94?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GuestTechArticleServiceTest.java | 329 +++++++++++++++++- .../MemberTechArticleServiceTest.java | 288 +++++++++++++-- .../techArticle/TechKeywordServiceTest.java | 68 ++-- .../devdevdev/test/MySQLTestContainer.java | 66 +++- 4 files changed, 703 insertions(+), 48 deletions(-) diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechArticleServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechArticleServiceTest.java index 14e7ec77..b073b976 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechArticleServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechArticleServiceTest.java @@ -13,17 +13,25 @@ import com.dreamypatisiel.devdevdev.domain.repository.CompanyRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRecommendRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; import com.dreamypatisiel.devdevdev.domain.service.member.AnonymousMemberService; import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.GuestTechArticleService; import com.dreamypatisiel.devdevdev.exception.NotFoundException; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleDetailResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleMainResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleRecommendResponse; import jakarta.persistence.EntityManager; +import org.springframework.test.context.transaction.BeforeTransaction; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -35,9 +43,18 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.test.context.transaction.BeforeTransaction; import org.springframework.transaction.annotation.Transactional; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_TECH_ARTICLE_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.GuestTechArticleService.INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE; @@ -48,6 +65,7 @@ @SpringBootTest @Transactional +@Testcontainers class GuestTechArticleServiceTest { @Autowired GuestTechArticleService guestTechArticleService; @@ -65,11 +83,89 @@ class GuestTechArticleServiceTest { Authentication authentication; @Mock SecurityContext securityContext; + @Autowired + DataSource dataSource; + + @Container + @ServiceConnection + static MySQLContainer mysql = new MySQLContainer<>("mysql:8.0") + .withDatabaseName("devdevdev_test") + .withUsername("test") + .withPassword("test") + .withCommand( + "--character-set-server=utf8mb4", + "--collation-server=utf8mb4_general_ci", + "--ngram_token_size=1" + ); String email = "dreamy5patisiel@kakao.com"; String socialType = SocialType.KAKAO.name(); String role = Role.ROLE_USER.name(); + private static final int TEST_ARTICLES_COUNT = 20; + private static Company testCompany; + private static List testTechArticles; + private static boolean indexesCreated = false; + + @BeforeTransaction + public void initIndexes() throws SQLException { + if (!indexesCreated) { + // 인덱스 생성 + createFulltextIndexesWithJDBC(); + indexesCreated = true; + + // 데이터 추가 + testCompany = createCompany("꿈빛 파티시엘", "https://example.com/company.png", + "https://example.com", "https://example.com"); + companyRepository.save(testCompany); + + testTechArticles = new ArrayList<>(); + for (int i = 0; i < TEST_ARTICLES_COUNT; i++) { + TechArticle techArticle = createTechArticle(i, testCompany); + testTechArticles.add(techArticle); + } + techArticleRepository.saveAll(testTechArticles); + } + } + + /** + * JDBC를 사용하여 MySQL fulltext 인덱스를 생성 + */ + private void createFulltextIndexesWithJDBC() throws SQLException { + Connection connection = null; + try { + // 현재 테스트 클래스의 컨테이너에 직접 연결 + connection = DriverManager.getConnection( + mysql.getJdbcUrl(), + mysql.getUsername(), + mysql.getPassword() + ); + connection.setAutoCommit(false); // 트랜잭션 시작 + + try (Statement statement = connection.createStatement()) { + try { + // 기존 인덱스가 있다면 삭제 + statement.executeUpdate("DROP INDEX idx__ft__title ON tech_article"); + statement.executeUpdate("DROP INDEX idx__ft__contents ON tech_article"); + statement.executeUpdate("DROP INDEX idx__ft__title_contents ON tech_article"); + } catch (Exception e) { + System.out.println("인덱스 없음 (정상): " + e.getMessage()); + } + + // fulltext 인덱스 생성 (개별 + 복합) + statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__title ON tech_article (title) WITH PARSER ngram"); + statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__contents ON tech_article (contents) WITH PARSER ngram"); + statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__title_contents ON tech_article (title, contents) WITH PARSER ngram"); + + connection.commit(); // 트랜잭션 커밋 + } + } finally { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } + } + @Test @DisplayName("익명 사용자가 커서 방식으로 기술블로그를 조회하여 응답을 생성한다.") void getTechArticles() { @@ -86,7 +182,9 @@ void getTechArticles() { // then assertThat(techArticles) - .hasSize(pageable.getPageSize()); + .hasSize(pageable.getPageSize()) + .extracting(TechArticleMainResponse::getRegDate) + .isSortedAccordingTo(Comparator.reverseOrder()); // 기본 정렬은 최신순 } @Test @@ -346,12 +444,17 @@ void cancelTechArticleRecommend() { techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); - Count popularScore = techArticle.getPopularScore(); - Count recommendTotalCount = techArticle.getRecommendTotalCount(); - AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); TechArticleRecommend techArticleRecommend = TechArticleRecommend.create(anonymousMember, techArticle); techArticleRecommendRepository.save(techArticleRecommend); + + // 추천 후 상태 저장 + em.flush(); + em.clear(); + + TechArticle updatedTechArticle = techArticleRepository.findById(techArticleId).get(); + Count popularScore = updatedTechArticle.getPopularScore(); + Count recommendTotalCount = updatedTechArticle.getRecommendTotalCount(); // when TechArticleRecommendResponse techArticleRecommendResponse = guestTechArticleService.updateRecommend(techArticleId, anonymousMemberId, authentication); @@ -384,6 +487,208 @@ void cancelTechArticleRecommend() { }); } + // ===== ElasticTechArticleServiceTest에서 이관된 정렬 및 커서 기능 테스트들 ===== + + @ParameterizedTest + @EnumSource(value = TechArticleSort.class, names = {"LATEST", "MOST_VIEWED", "MOST_COMMENTED", "POPULAR"}) + @DisplayName("익명 사용자가 다양한 정렬 기준으로 기술블로그를 조회한다.") + void getTechArticlesWithDifferentSorts(TechArticleSort sort) { + // given + Pageable pageable = PageRequest.of(0, 10); + + when(authentication.getPrincipal()).thenReturn("anonymousUser"); + when(securityContext.getAuthentication()).thenReturn(authentication); + SecurityContextHolder.setContext(securityContext); + + // when + Slice techArticles = guestTechArticleService.getTechArticles( + pageable, null, sort, null, null, null, authentication); + + // then + assertThat(techArticles).hasSize(pageable.getPageSize()); + + List articles = techArticles.getContent(); + switch (sort) { + case LATEST -> assertThat(articles) + .extracting(TechArticleMainResponse::getRegDate) + .isSortedAccordingTo(Comparator.reverseOrder()); + case MOST_VIEWED -> assertThat(articles) + .extracting(TechArticleMainResponse::getViewTotalCount) + .isSortedAccordingTo(Comparator.reverseOrder()); + case MOST_COMMENTED -> assertThat(articles) + .extracting(TechArticleMainResponse::getCommentTotalCount) + .isSortedAccordingTo(Comparator.reverseOrder()); + case POPULAR -> assertThat(articles) + .extracting(TechArticleMainResponse::getPopularScore) + .isSortedAccordingTo(Comparator.reverseOrder()); + } + } + + @Test + @DisplayName("익명 사용자가 커서 방식으로 다음 페이지의 기술블로그를 최신순으로 조회한다.") + void getTechArticlesWithCursorOrderByLatest() { + // given + Pageable prevPageable = PageRequest.of(0, 1); + Pageable pageable = PageRequest.of(0, 5); + + when(authentication.getPrincipal()).thenReturn("anonymousUser"); + when(securityContext.getAuthentication()).thenReturn(authentication); + SecurityContextHolder.setContext(securityContext); + + // 첫 번째 페이지 조회 + Slice firstPage = guestTechArticleService.getTechArticles( + prevPageable, null, TechArticleSort.LATEST, null, null, null, authentication); + + TechArticleMainResponse cursor = firstPage.getContent().get(0); + + // when + Slice secondPage = guestTechArticleService.getTechArticles( + pageable, cursor.getId(), TechArticleSort.LATEST, null, null, null, authentication); + + // then + assertThat(secondPage) + .hasSize(pageable.getPageSize()) + .extracting(TechArticleMainResponse::getRegDate) + .isSortedAccordingTo(Comparator.reverseOrder()) + .allMatch(date -> !date.isAfter(cursor.getRegDate())); + } + + @Test + @DisplayName("익명 사용자가 커서 방식으로 다음 페이지의 기술블로그를 조회순으로 조회한다.") + void getTechArticlesWithCursorOrderByMostViewed() { + // given + Pageable prevPageable = PageRequest.of(0, 1); + Pageable pageable = PageRequest.of(0, 5); + + when(authentication.getPrincipal()).thenReturn("anonymousUser"); + when(securityContext.getAuthentication()).thenReturn(authentication); + SecurityContextHolder.setContext(securityContext); + + // 첫 번째 페이지 조회 + Slice firstPage = guestTechArticleService.getTechArticles( + prevPageable, null, TechArticleSort.MOST_VIEWED, null, null, null, authentication); + + TechArticleMainResponse cursor = firstPage.getContent().get(0); + + // when + Slice secondPage = guestTechArticleService.getTechArticles( + pageable, cursor.getId(), TechArticleSort.MOST_VIEWED, null, null, null, authentication); + + // then + assertThat(secondPage) + .hasSize(pageable.getPageSize()) + .extracting(TechArticleMainResponse::getViewTotalCount) + .isSortedAccordingTo(Comparator.reverseOrder()) + .allMatch(viewCount -> viewCount <= cursor.getViewTotalCount()); + } + + @Test + @DisplayName("익명 사용자가 커서 방식으로 다음 페이지의 기술블로그를 댓글순으로 조회한다.") + void getTechArticlesWithCursorOrderByMostCommented() { + // given + Pageable prevPageable = PageRequest.of(0, 1); + Pageable pageable = PageRequest.of(0, 5); + + when(authentication.getPrincipal()).thenReturn("anonymousUser"); + when(securityContext.getAuthentication()).thenReturn(authentication); + SecurityContextHolder.setContext(securityContext); + + // 첫 번째 페이지 조회 + Slice firstPage = guestTechArticleService.getTechArticles( + prevPageable, null, TechArticleSort.MOST_COMMENTED, null, null, null, authentication); + + TechArticleMainResponse cursor = firstPage.getContent().get(0); + + // when + Slice secondPage = guestTechArticleService.getTechArticles( + pageable, cursor.getId(), TechArticleSort.MOST_COMMENTED, null, null, null, authentication); + + // then + assertThat(secondPage) + .hasSize(pageable.getPageSize()) + .extracting(TechArticleMainResponse::getCommentTotalCount) + .isSortedAccordingTo(Comparator.reverseOrder()) + .allMatch(commentCount -> commentCount <= cursor.getCommentTotalCount()); + } + + @Test + @DisplayName("익명 사용자가 커서 방식으로 다음 페이지의 기술블로그를 인기순으로 조회한다.") + void getTechArticlesWithCursorOrderByPopular() { + // given + Pageable prevPageable = PageRequest.of(0, 1); + Pageable pageable = PageRequest.of(0, 5); + + when(authentication.getPrincipal()).thenReturn("anonymousUser"); + when(securityContext.getAuthentication()).thenReturn(authentication); + SecurityContextHolder.setContext(securityContext); + + // 첫 번째 페이지 조회 + Slice firstPage = guestTechArticleService.getTechArticles( + prevPageable, null, TechArticleSort.POPULAR, null, null, null, authentication); + + TechArticleMainResponse cursor = firstPage.getContent().get(0); + + // when + Slice secondPage = guestTechArticleService.getTechArticles( + pageable, cursor.getId(), TechArticleSort.POPULAR, null, null, null, authentication); + + // then + assertThat(secondPage) + .hasSize(pageable.getPageSize()) + .extracting(TechArticleMainResponse::getPopularScore) + .isSortedAccordingTo(Comparator.reverseOrder()) + .allMatch(popularScore -> popularScore <= cursor.getPopularScore()); + } + + @Test + @DisplayName("익명 사용자가 키워드로 기술블로그를 검색한다.") + void getTechArticlesWithKeyword() { + // given + Pageable pageable = PageRequest.of(0, 10); + String keyword = "내용"; + + when(authentication.getPrincipal()).thenReturn("anonymousUser"); + when(securityContext.getAuthentication()).thenReturn(authentication); + SecurityContextHolder.setContext(securityContext); + + // when + Slice techArticles = guestTechArticleService.getTechArticles( + pageable, null, null, keyword, null, null, authentication); + + // then + assertThat(techArticles.getContent()) + .isNotEmpty() + .allSatisfy(article -> { + boolean containsKeyword = article.getTitle().contains(keyword) || + article.getContents().contains(keyword); + assertThat(containsKeyword).isTrue(); + }); + } + + @Test + @DisplayName("익명 사용자가 특정 회사의 기술블로그만 필터링하여 조회한다.") + void getTechArticlesFilterByCompany() { + // given + Pageable pageable = PageRequest.of(0, 10); + + when(authentication.getPrincipal()).thenReturn("anonymousUser"); + when(securityContext.getAuthentication()).thenReturn(authentication); + SecurityContextHolder.setContext(securityContext); + + // when + Slice techArticles = guestTechArticleService.getTechArticles( + pageable, null, TechArticleSort.LATEST, null, testCompany.getId(), null, authentication); + + // then + assertThat(techArticles.getContent()) + .isNotEmpty() + .allSatisfy(article -> + assertThat(article.getCompany().getId()).isEqualTo(testCompany.getId()) + ) + .extracting(TechArticleMainResponse::getRegDate) + .isSortedAccordingTo(Comparator.reverseOrder()); + } + private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, String careerUrl) { return Company.builder() @@ -409,4 +714,20 @@ private static TechArticle createTechArticle(Company company) { .popularScore(new Count(10)) .build(); } + + private static TechArticle createTechArticle(int i, Company company) { + return TechArticle.builder() + .title(new Title("타이틀 " + i)) + .contents("내용 " + i) + .company(company) + .author("작성자") + .regDate(LocalDate.now().minusDays(i)) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(i)) + .recommendTotalCount(new Count(i)) + .viewTotalCount(new Count(i)) + .popularScore(new Count(10L *i)) + .build(); + } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechArticleServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechArticleServiceTest.java index 5db82475..f1f09389 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechArticleServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechArticleServiceTest.java @@ -17,19 +17,26 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRecommendRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.MemberTechArticleService; import com.dreamypatisiel.devdevdev.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 org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.BookmarkResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleDetailResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleMainResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleRecommendResponse; import jakarta.persistence.EntityManager; -import org.junit.jupiter.api.BeforeAll; +import org.springframework.test.context.transaction.BeforeTransaction; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.PageRequest; @@ -41,12 +48,17 @@ import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.transaction.annotation.Transactional; +import javax.sql.DataSource; +import java.sql.*; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; @SpringBootTest @Transactional +@Testcontainers class MemberTechArticleServiceTest { @Autowired MemberTechArticleService memberTechArticleService; @@ -62,6 +74,20 @@ class MemberTechArticleServiceTest { TechArticleRecommendRepository techArticleRecommendRepository; @Autowired EntityManager em; + @Autowired + DataSource dataSource; + + @Container + @ServiceConnection + static MySQLContainer mysql = new MySQLContainer<>("mysql:8.0") + .withDatabaseName("devdevdev_test") + .withUsername("test") + .withPassword("test") + .withCommand( + "--character-set-server=utf8mb4", + "--collation-server=utf8mb4_general_ci", + "--ngram_token_size=1" + ); String userId = "dreamy5patisiel"; String name = "꿈빛파티시엘"; @@ -72,23 +98,69 @@ class MemberTechArticleServiceTest { String role = Role.ROLE_USER.name(); private static final int TEST_ARTICLES_COUNT = 20; - private static Company company; + private static Company testCompany; + private static List testTechArticles; private static TechArticle firstTechArticle; - private static List techArticles; - - @BeforeAll - static void setup(@Autowired TechArticleRepository techArticleRepository, - @Autowired CompanyRepository companyRepository) { - company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", - "https://example.com"); - companyRepository.save(company); + private static boolean indexesCreated = false; + + @BeforeTransaction + public void initIndexes() throws SQLException { + if (!indexesCreated) { + // 인덱스 생성 + createFulltextIndexesWithJDBC(); + indexesCreated = true; + + // 데이터 추가 + testCompany = createCompany("꿈빛 파티시엘", "https://example.com/company.png", + "https://example.com", "https://example.com"); + companyRepository.save(testCompany); + + testTechArticles = new ArrayList<>(); + for (int i = 0; i < TEST_ARTICLES_COUNT; i++) { + TechArticle techArticle = createTechArticle(i, testCompany); + testTechArticles.add(techArticle); + } + techArticleRepository.saveAll(testTechArticles); + firstTechArticle = testTechArticles.get(1); + } + } - List techArticles = new ArrayList<>(); - for (int i = 0; i < 10; i++) { - TechArticle techArticle = createTechArticle(i, company); - techArticles.add(techArticle); + /** + * JDBC를 사용하여 MySQL fulltext 인덱스를 생성 + */ + private void createFulltextIndexesWithJDBC() throws SQLException { + Connection connection = null; + try { + // 현재 테스트 클래스의 컨테이너에 직접 연결 + connection = DriverManager.getConnection( + mysql.getJdbcUrl(), + mysql.getUsername(), + mysql.getPassword() + ); + connection.setAutoCommit(false); // 트랜잭션 시작 + + try (Statement statement = connection.createStatement()) { + try { + // 기존 인덱스가 있다면 삭제 + statement.executeUpdate("DROP INDEX idx__ft__title ON tech_article"); + statement.executeUpdate("DROP INDEX idx__ft__contents ON tech_article"); + statement.executeUpdate("DROP INDEX idx__ft__title_contents ON tech_article"); + } catch (Exception e) { + System.out.println("인덱스 없음 (정상): " + e.getMessage()); + } + + // fulltext 인덱스 생성 (개별 + 복합) + statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__title ON tech_article (title) WITH PARSER ngram"); + statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__contents ON tech_article (contents) WITH PARSER ngram"); + statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__title_contents ON tech_article (title, contents) WITH PARSER ngram"); + + connection.commit(); // 트랜잭션 커밋 + } + } finally { + if (connection != null && !connection.isClosed()) { + connection.close(); + } } - techArticleRepository.saveAll(techArticles); } @Test @@ -113,7 +185,9 @@ void getTechArticles() { // then assertThat(techArticles) - .hasSize(pageable.getPageSize()); + .hasSize(pageable.getPageSize()) + .extracting(TechArticleMainResponse::getRegDate) + .isSortedAccordingTo(Comparator.reverseOrder()); // 기본 정렬은 최신순 } @@ -422,12 +496,12 @@ void cancelTechArticleRecommend() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Long techArticleId = firstTechArticle.getId(); - Count popularScore = firstTechArticle.getPopularScore(); - Count recommendTotalCount = firstTechArticle.getRecommendTotalCount(); - TechArticleRecommend techArticleRecommend = TechArticleRecommend.create(member, firstTechArticle); techArticleRecommendRepository.save(techArticleRecommend); + Count popularScore = firstTechArticle.getPopularScore(); + Count recommendTotalCount = firstTechArticle.getRecommendTotalCount(); + // when TechArticleRecommendResponse techArticleRecommendResponse = memberTechArticleService.updateRecommend(techArticleId, null, authentication); @@ -456,6 +530,180 @@ void cancelTechArticleRecommend() { }); } + // ===== ElasticTechArticleServiceTest에서 이관된 정렬 및 커서 기능 테스트들 ===== + + @ParameterizedTest + @EnumSource(value = TechArticleSort.class, names = {"LATEST", "MOST_VIEWED", "MOST_COMMENTED", "POPULAR"}) + @DisplayName("회원이 다양한 정렬 기준으로 기술블로그를 조회한다.") + void getTechArticlesWithDifferentSorts(TechArticleSort sort) { + // given + Pageable pageable = PageRequest.of(0, 10); + + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + UserPrincipal userPrincipal = UserPrincipal.createByMember(member); + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), + userPrincipal.getSocialType().name())); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + // when + Slice techArticles = memberTechArticleService.getTechArticles( + pageable, null, sort, null, null, null, authentication); + + // then + assertThat(techArticles).hasSize(pageable.getPageSize()); + + List articles = techArticles.getContent(); + switch (sort) { + case LATEST -> assertThat(articles) + .extracting(TechArticleMainResponse::getRegDate) + .isSortedAccordingTo(Comparator.reverseOrder()); + case MOST_VIEWED -> assertThat(articles) + .extracting(TechArticleMainResponse::getViewTotalCount) + .isSortedAccordingTo(Comparator.reverseOrder()); + case MOST_COMMENTED -> assertThat(articles) + .extracting(TechArticleMainResponse::getCommentTotalCount) + .isSortedAccordingTo(Comparator.reverseOrder()); + case POPULAR -> assertThat(articles) + .extracting(TechArticleMainResponse::getPopularScore) + .isSortedAccordingTo(Comparator.reverseOrder()); + } + } + + @Test + @DisplayName("회원이 커서 방식으로 다음 페이지의 기술블로그를 최신순으로 조회한다.") + void getTechArticlesWithCursorOrderByLatest() { + // given + Pageable prevPageable = PageRequest.of(0, 1); + Pageable pageable = PageRequest.of(0, 5); + + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + UserPrincipal userPrincipal = UserPrincipal.createByMember(member); + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), + userPrincipal.getSocialType().name())); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + // 첫 번째 페이지 조회 + Slice firstPage = memberTechArticleService.getTechArticles( + prevPageable, null, TechArticleSort.LATEST, null, null, null, authentication); + + TechArticleMainResponse cursor = firstPage.getContent().get(0); + + // when + Slice secondPage = memberTechArticleService.getTechArticles( + pageable, cursor.getId(), TechArticleSort.LATEST, null, null, null, authentication); + + // then + assertThat(secondPage) + .hasSize(pageable.getPageSize()) + .extracting(TechArticleMainResponse::getRegDate) + .isSortedAccordingTo(Comparator.reverseOrder()) + .allMatch(date -> !date.isAfter(cursor.getRegDate())); + } + + @Test + @DisplayName("회원이 커서 방식으로 다음 페이지의 기술블로그를 조회순으로 조회한다.") + void getTechArticlesWithCursorOrderByMostViewed() { + // given + Pageable prevPageable = PageRequest.of(0, 1); + Pageable pageable = PageRequest.of(0, 5); + + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + UserPrincipal userPrincipal = UserPrincipal.createByMember(member); + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), + userPrincipal.getSocialType().name())); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + // 첫 번째 페이지 조회 + Slice firstPage = memberTechArticleService.getTechArticles( + prevPageable, null, TechArticleSort.MOST_VIEWED, null, null, null, authentication); + + TechArticleMainResponse cursor = firstPage.getContent().get(0); + + // when + Slice secondPage = memberTechArticleService.getTechArticles( + pageable, cursor.getId(), TechArticleSort.MOST_VIEWED, null, null, null, authentication); + + // then + assertThat(secondPage) + .hasSize(pageable.getPageSize()) + .extracting(TechArticleMainResponse::getViewTotalCount) + .isSortedAccordingTo(Comparator.reverseOrder()) + .allMatch(viewCount -> viewCount <= cursor.getViewTotalCount()); + } + + @Test + @DisplayName("회원이 키워드로 기술블로그를 검색한다.") + void getTechArticlesWithKeyword() { + // given + Pageable pageable = PageRequest.of(0, 10); + String keyword = "내용"; + + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + UserPrincipal userPrincipal = UserPrincipal.createByMember(member); + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), + userPrincipal.getSocialType().name())); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + // when + Slice techArticles = memberTechArticleService.getTechArticles( + pageable, null, null, keyword, null, null, authentication); + + // then + assertThat(techArticles.getContent()) + .isNotEmpty() + .allSatisfy(article -> { + boolean containsKeyword = article.getTitle().contains(keyword) || + article.getContents().contains(keyword); + assertThat(containsKeyword).isTrue(); + }); + } + + @Test + @DisplayName("회원이 특정 회사의 기술블로그만 필터링하여 조회한다.") + void getTechArticlesFilterByCompany() { + // given + Pageable pageable = PageRequest.of(0, 10); + + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + UserPrincipal userPrincipal = UserPrincipal.createByMember(member); + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), + userPrincipal.getSocialType().name())); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + // when + Slice techArticles = memberTechArticleService.getTechArticles( + pageable, null, TechArticleSort.LATEST, null, testCompany.getId(), null, authentication); + + // then + assertThat(techArticles.getContent()) + .isNotEmpty() + .allSatisfy(article -> + assertThat(article.getCompany().getId()).isEqualTo(testCompany.getId()) + ) + .extracting(TechArticleMainResponse::getRegDate) + .isSortedAccordingTo(Comparator.reverseOrder()); + } + private SocialMemberDto createSocialDto(String userId, String name, String nickName, String password, String email, String socialType, String role) { return SocialMemberDto.builder() @@ -493,7 +741,7 @@ private static TechArticle createTechArticle(int i, Company company) { .contents("내용 " + i) .company(company) .author("작성자") - .regDate(LocalDate.now()) + .regDate(LocalDate.now().minusDays(i)) .techArticleUrl(new Url("https://example.com/article")) .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) .commentTotalCount(new Count(i)) diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechKeywordServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechKeywordServiceTest.java index 45f05d0d..7245680d 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechKeywordServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechKeywordServiceTest.java @@ -4,7 +4,10 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechKeywordRepository; import com.dreamypatisiel.devdevdev.domain.service.techArticle.keyword.TechKeywordService; import com.dreamypatisiel.devdevdev.global.utils.HangulUtils; -import com.dreamypatisiel.devdevdev.test.MySQLTestContainer; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; import jakarta.persistence.EntityManager; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -16,9 +19,7 @@ import org.springframework.transaction.annotation.Transactional; import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.SQLException; -import java.sql.Statement; +import java.sql.*; import java.util.ArrayList; import java.util.List; @@ -26,7 +27,8 @@ @SpringBootTest @Transactional -class TechKeywordServiceTest extends MySQLTestContainer { +@Testcontainers +class TechKeywordServiceTest { @Autowired EntityManager em; @@ -40,6 +42,18 @@ class TechKeywordServiceTest extends MySQLTestContainer { @Autowired DataSource dataSource; + @Container + @ServiceConnection + static MySQLContainer mysql = new MySQLContainer<>("mysql:8.0") + .withDatabaseName("devdevdev_test") + .withUsername("test") + .withPassword("test") + .withCommand( + "--character-set-server=utf8mb4", + "--collation-server=utf8mb4_general_ci", + "--ngram_token_size=1" + ); + private static boolean indexesCreated = false; @BeforeTransaction @@ -66,24 +80,36 @@ public void initIndexes() throws SQLException { * JDBC를 사용하여 MySQL fulltext 인덱스를 생성 */ private void createFulltextIndexesWithJDBC() throws SQLException { - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - - connection.setAutoCommit(false); // 트랜잭션 시작 - + Connection connection = null; try { - // 기존 인덱스가 있다면 삭제 - statement.executeUpdate("DROP INDEX idx__ft__jamo_key ON tech_keyword"); - statement.executeUpdate("DROP INDEX idx__ft__chosung_key ON tech_keyword"); - } catch (Exception e) { - System.out.println("인덱스 없음 (정상): " + e.getMessage()); + // 현재 테스트 클래스의 컨테이너에 직접 연결 + connection = DriverManager.getConnection( + mysql.getJdbcUrl(), + mysql.getUsername(), + mysql.getPassword() + ); + connection.setAutoCommit(false); // 트랜잭션 시작 + + try (Statement statement = connection.createStatement()) { + try { + // 기존 인덱스가 있다면 삭제 + statement.executeUpdate("DROP INDEX idx__ft__jamo_key ON tech_keyword"); + statement.executeUpdate("DROP INDEX idx__ft__chosung_key ON tech_keyword"); + } catch (Exception e) { + System.out.println("인덱스 없음 (정상): " + e.getMessage()); + } + + // fulltext 인덱스 생성 + statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__jamo_key ON tech_keyword (jamo_key) WITH PARSER ngram"); + statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__chosung_key ON tech_keyword (chosung_key) WITH PARSER ngram"); + + connection.commit(); // 트랜잭션 커밋 + } + } finally { + if (connection != null && !connection.isClosed()) { + connection.close(); + } } - - // fulltext 인덱스 생성 - statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__jamo_key ON tech_keyword (jamo_key) WITH PARSER ngram"); - statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__chosung_key ON tech_keyword (chosung_key) WITH PARSER ngram"); - - connection.commit(); // 트랜잭션 커밋 } @Test diff --git a/src/test/java/com/dreamypatisiel/devdevdev/test/MySQLTestContainer.java b/src/test/java/com/dreamypatisiel/devdevdev/test/MySQLTestContainer.java index 6884b394..8e981639 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/test/MySQLTestContainer.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/test/MySQLTestContainer.java @@ -1,5 +1,6 @@ package com.dreamypatisiel.devdevdev.test; +import org.junit.jupiter.api.AfterAll; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; @@ -7,10 +8,13 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + /** * MySQL 테스트컨테이너를 제공하는 공통 클래스 - * 1. 테스트 클래스에서 이 클래스를 상속받거나 - * 2. @ExtendWith(MySQLTestContainer.class) 어노테이션을 사용 + * 각 테스트 클래스가 독립적인 컨테이너를 사용하도록 변경(현재 사용X) */ @Testcontainers public abstract class MySQLTestContainer { @@ -29,11 +33,67 @@ public abstract class MySQLTestContainer { @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { - registry.add("spring.datasource.url", mysql::getJdbcUrl); + // 컨테이너 시작 확인 + if (!mysql.isRunning()) { + mysql.start(); + } + + // 컨테이너 준비 대기 + waitForContainer(); + + String jdbcUrl = mysql.getJdbcUrl(); + System.out.println("MySQL Container JDBC URL: " + jdbcUrl); + + registry.add("spring.datasource.url", () -> jdbcUrl); registry.add("spring.datasource.username", mysql::getUsername); registry.add("spring.datasource.password", mysql::getPassword); registry.add("spring.jpa.database-platform", () -> "org.hibernate.dialect.MySQLDialect"); registry.add("spring.jpa.hibernate.ddl-auto", () -> "create-drop"); registry.add("spring.jpa.show-sql", () -> "true"); + + // HikariCP 완전 비활성화 + registry.add("spring.datasource.type", () -> "org.springframework.jdbc.datasource.SimpleDriverDataSource"); + + // 로깅 레벨 설정 + registry.add("logging.level.com.zaxxer.hikari", () -> "OFF"); + registry.add("logging.level.com.zaxxer.hikari.pool", () -> "OFF"); + registry.add("logging.level.com.zaxxer.hikari.pool.PoolBase", () -> "OFF"); + registry.add("logging.level.com.zaxxer.hikari.pool.ProxyLeakTask", () -> "OFF"); + } + + private static void waitForContainer() { + int maxRetries = 15; + int retryCount = 0; + + while (retryCount < maxRetries) { + try { + if (mysql.isRunning()) { + // 실제 커넥션 테스트 + try (Connection conn = DriverManager.getConnection( + mysql.getJdbcUrl(), + mysql.getUsername(), + mysql.getPassword())) { + System.out.println("MySQL Container 연결 성공: " + mysql.getJdbcUrl()); + return; + } catch (SQLException e) { + System.out.println("커넥션 대기 중... (" + retryCount + "/" + maxRetries + ")"); + } + } + Thread.sleep(1000); + retryCount++; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + throw new RuntimeException("MySQL 컴테이너 연결 실패"); + } + + @AfterAll + static void tearDownContainer() { + // 컨테이너 종료 + if (mysql != null && mysql.isRunning()) { + mysql.stop(); + } } } From 40b28210e128e0f0e7db3e98d57f6c4f5089e765 Mon Sep 17 00:00:00 2001 From: soyoung Date: Fri, 29 Aug 2025 22:28:54 +0900 Subject: [PATCH 60/66] =?UTF-8?q?fix:=20=EA=B8=B0=EC=88=A0=EB=B8=94?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TechArticleControllerTest.java | 320 ++++++++---------- .../docs/TechArticleControllerDocsTest.java | 282 +++++++-------- 2 files changed, 261 insertions(+), 341 deletions(-) diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java index afc18deb..f7e0f6c7 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java @@ -10,77 +10,38 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.dreamypatisiel.devdevdev.domain.entity.Bookmark; -import com.dreamypatisiel.devdevdev.domain.entity.Company; -import com.dreamypatisiel.devdevdev.domain.entity.Member; -import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.CompanyName; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.Count; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.Title; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; -import com.dreamypatisiel.devdevdev.domain.entity.enums.Role; -import com.dreamypatisiel.devdevdev.domain.entity.enums.SocialType; -import com.dreamypatisiel.devdevdev.domain.repository.CompanyRepository; -import com.dreamypatisiel.devdevdev.domain.repository.member.MemberRepository; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkRepository; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.GuestTechArticleService; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.MemberTechArticleService; +import com.dreamypatisiel.devdevdev.exception.MemberException; +import com.dreamypatisiel.devdevdev.exception.NotFoundException; +import com.dreamypatisiel.devdevdev.exception.TechArticleException; +import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; +import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.*; import com.dreamypatisiel.devdevdev.global.constant.SecurityConstant; -import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; import com.dreamypatisiel.devdevdev.web.controller.SupportControllerTest; import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; import java.nio.charset.StandardCharsets; import java.time.LocalDate; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; import java.util.List; -import java.util.Random; -import java.util.concurrent.ThreadLocalRandom; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -class TechArticleControllerTest extends SupportControllerTest { +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; - private static final int TEST_ARTICLES_COUNT = 20; - private static Company company; - private static TechArticle firstTechArticle; - private static List techArticles; - - @Autowired - TechArticleRepository techArticleRepository; - @Autowired - MemberRepository memberRepository; - @Autowired - BookmarkRepository bookmarkRepository; - - @BeforeAll - static void setup(@Autowired TechArticleRepository techArticleRepository, - @Autowired CompanyRepository companyRepository) { - company = createCompany("꿈빛 파티시엘", - "https://example.com/company.png", "https://example.com", "https://example.com"); - companyRepository.save(company); - - List techArticles = new ArrayList<>(); - for (int i = 0; i < 10; i++) { - TechArticle techArticle = createTechArticle(i, company); - techArticles.add(techArticle); - } - techArticleRepository.saveAll(techArticles); - } +class TechArticleControllerTest extends SupportControllerTest { - @AfterAll - static void tearDown(@Autowired TechArticleRepository techArticleRepository, - @Autowired CompanyRepository companyRepository) { - techArticleRepository.deleteAllInBatch(); - companyRepository.deleteAllInBatch(); - } + @MockBean + GuestTechArticleService guestTechArticleService; + + @MockBean + MemberTechArticleService memberTechArticleService; @Test @DisplayName("익명 사용자가 기술블로그 메인을 조회한다.") @@ -89,7 +50,20 @@ void getTechArticlesByAnonymous() throws Exception { Pageable pageable = PageRequest.of(0, 10); String techArticleId = "1"; String keyword = "타이틀"; - String companyId = company.getId().toString(); + String companyId = "1"; + + TechArticleMainResponse response = createTechArticleMainResponse( + 1L, "http://thumbnail.com", false, "http://article.com", "타이틀 1", "내용 1", + 1L, "회사명", "http://career.com", "http://official.com", LocalDate.now(), "작성자", + 10L, 5L, 100L, null, 10.0f + ); + + SliceCustom mockSlice = new SliceCustom<>( + List.of(response), pageable, false, 1L + ); + + given(guestTechArticleService.getTechArticles(any(), any(), any(), any(), any(), any(), any())) + .willReturn(mockSlice); // when // then mockMvc.perform(get("/devdevdev/api/v1/articles") @@ -148,25 +122,23 @@ void getTechArticlesByAnonymous() throws Exception { @DisplayName("회원이 기술블로그 메인을 조회한다.") void getTechArticlesByMember() throws Exception { // given - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); - - List bookmarks = new ArrayList<>(); - for (TechArticle techArticle : techArticles) { - if (createRandomBoolean()) { - Bookmark bookmark = createBookmark(member, techArticle, true); - bookmarks.add(bookmark); - } - } - bookmarkRepository.saveAll(bookmarks); - Pageable pageable = PageRequest.of(0, 10); String techArticleId = "1"; String keyword = "타이틀"; - String companyId = company.getId().toString(); + String companyId = "1"; + + TechArticleMainResponse response = createTechArticleMainResponse( + 1L, "http://thumbnail.com", false, "http://article.com", "타이틀 1", "내용 1", + 1L, "회사명", "http://career.com", "http://official.com", LocalDate.now(), "작성자", + 10L, 5L, 100L, true, 10.0f + ); + + SliceCustom mockSlice = new SliceCustom<>( + List.of(response), pageable, false, 1L + ); + + given(memberTechArticleService.getTechArticles(any(), any(), any(), any(), any(), any(), any())) + .willReturn(mockSlice); // when // then mockMvc.perform(get("/devdevdev/api/v1/articles") @@ -228,10 +200,12 @@ void getTechArticlesByMember() throws Exception { "정확도 내림차순으로 조회하기 위한 점수가 없다면 예외가 발생한다.") void getTechArticlesWithKeywordWithCursorOrderByHIGHEST_SCOREWithoutScoreException() throws Exception { // given - Pageable prevPageable = PageRequest.of(0, 1); Pageable pageable = PageRequest.of(0, 10); String techArticleId = "1"; String keyword = "타이틀"; + + given(guestTechArticleService.getTechArticles(any(), any(), any(), any(), any(), any(), any())) + .willThrow(new TechArticleException(NOT_FOUND_CURSOR_SCORE_MESSAGE)); // when // then mockMvc.perform(get("/devdevdev/api/v1/articles") @@ -255,6 +229,9 @@ void getTechArticlesSpecialSymbolException() throws Exception { // given Pageable pageable = PageRequest.of(0, 10); String keyword = "!"; + + given(guestTechArticleService.getTechArticles(any(), any(), any(), any(), any(), any(), any())) + .willThrow(new TechArticleException(KEYWORD_WITH_SPECIAL_SYMBOLS_EXCEPTION_MESSAGE)); // when // then mockMvc.perform(get("/devdevdev/api/v1/articles") @@ -274,8 +251,17 @@ void getTechArticlesSpecialSymbolException() throws Exception { @DisplayName("익명 사용자가 기술블로그 상세를 조회한다.") void getTechArticleByAnonymous() throws Exception { // given - Long id = firstTechArticle.getId(); + Long id = 1L; String anonymousMemberId = "GA1.1.276672604.1715872960"; + + TechArticleDetailResponse response = createTechArticleDetailResponse( + "http://thumbnail.com", "http://article.com", "기술블로그 제목", "기술블로그 내용", + 1L, "회사명", "http://career.com", "http://official.com", LocalDate.now(), "작성자", + 100L, 10L, 5L, 50L, false, false + ); + + given(guestTechArticleService.getTechArticle(eq(id), any(), any())) + .willReturn(response); // when // then mockMvc.perform(get("/devdevdev/api/v1/articles/{id}", id) @@ -308,13 +294,16 @@ void getTechArticleByAnonymous() throws Exception { @DisplayName("회원이 기술블로그 상세를 조회한다.") void getTechArticleByMember() throws Exception { // given - Long id = firstTechArticle.getId(); - // given - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); + Long id = 1L; + + TechArticleDetailResponse response = createTechArticleDetailResponse( + "http://thumbnail.com", "http://article.com", "기술블로그 제목", "기술블로그 내용", + 1L, "회사명", "http://career.com", "http://official.com", LocalDate.now(), "작성자", + 100L, 10L, 5L, 50L, false, true + ); + + given(memberTechArticleService.getTechArticle(eq(id), any(), any())) + .willReturn(response); // when // then mockMvc.perform(get("/devdevdev/api/v1/articles/{id}", id) @@ -348,7 +337,10 @@ void getTechArticleByMember() throws Exception { @DisplayName("회원이 기술블로그 상세를 조회할 때 회원이 없으면 예외가 발생한다.") void getTechArticleNotFoundMemberException() throws Exception { // given - Long id = firstTechArticle.getId(); + Long id = 1L; + + given(memberTechArticleService.getTechArticle(eq(id), any(), any())) + .willThrow(new MemberException(INVALID_MEMBER_NOT_FOUND_MESSAGE)); // when // then mockMvc.perform(get("/devdevdev/api/v1/articles/{id}", id) @@ -366,9 +358,10 @@ void getTechArticleNotFoundMemberException() throws Exception { @DisplayName("기술블로그 상세를 조회할 때 기술블로그가 존재하지 않으면 예외가 발생한다.") void getTechArticleNotFoundTechArticleException() throws Exception { // given - TechArticle techArticle = createTechArticle(1, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId() + 1; + Long id = 999L; + + given(guestTechArticleService.getTechArticle(eq(id), any(), any())) + .willThrow(new NotFoundException(NOT_FOUND_TECH_ARTICLE_MESSAGE)); // when // then mockMvc.perform(get("/devdevdev/api/v1/articles/{id}", id) @@ -385,12 +378,11 @@ void getTechArticleNotFoundTechArticleException() throws Exception { @DisplayName("회원이 기술블로그 북마크를 요청한다.") void updateBookmark() throws Exception { // given - Long id = firstTechArticle.getId(); - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); + Long id = 1L; + + BookmarkResponse response = new BookmarkResponse(id, true); + given(memberTechArticleService.updateBookmark(eq(id), any())) + .willReturn(response); // when // then mockMvc.perform(post("/devdevdev/api/v1/articles/{id}/bookmark", id) @@ -410,15 +402,10 @@ void updateBookmark() throws Exception { @DisplayName("회원이 기술블로그 북마크를 요청할 때 존재하지 않는 기술블로그라면 예외가 발생한다.") void updateBookmarkNotFoundTechArticleException() throws Exception { // given - TechArticle techArticle = createTechArticle(1, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId() + 1; - - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); + Long id = 999L; + + given(memberTechArticleService.updateBookmark(eq(id), any())) + .willThrow(new NotFoundException(NOT_FOUND_TECH_ARTICLE_MESSAGE)); // when // then mockMvc.perform(post("/devdevdev/api/v1/articles/{id}/bookmark", id) @@ -436,7 +423,10 @@ void updateBookmarkNotFoundTechArticleException() throws Exception { @DisplayName("회원이 기술블로그 북마크를 요청할 때 존재하지 않는 회원이라면 예외가 발생한다.") void updateBookmarkNotFoundMemberException() throws Exception { // given - Long id = firstTechArticle.getId(); + Long id = 1L; + + given(memberTechArticleService.updateBookmark(eq(id), any())) + .willThrow(new MemberException(INVALID_MEMBER_NOT_FOUND_MESSAGE)); // when // then mockMvc.perform(post("/devdevdev/api/v1/articles/{id}/bookmark", id) @@ -454,12 +444,11 @@ void updateBookmarkNotFoundMemberException() throws Exception { @DisplayName("회원이 기술블로그 추천을 요청한다.") void updateRecommend() throws Exception { // given - Long id = firstTechArticle.getId(); - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); + Long id = 1L; + + TechArticleRecommendResponse response = new TechArticleRecommendResponse(id, true, 11L); + given(memberTechArticleService.updateRecommend(eq(id), any(), any())) + .willReturn(response); // when // then mockMvc.perform(post("/devdevdev/api/v1/articles/{id}/recommend", id) @@ -481,15 +470,10 @@ void updateRecommend() throws Exception { @DisplayName("회원이 기술블로그 추천을 요청할 때 존재하지 않는 기술블로그라면 예외가 발생한다.") void updateRecommendNotFoundTechArticleException() throws Exception { // given - TechArticle techArticle = createTechArticle(1, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId() + 1; - - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); + Long id = 999L; + + given(memberTechArticleService.updateRecommend(eq(id), any(), any())) + .willThrow(new NotFoundException(NOT_FOUND_TECH_ARTICLE_MESSAGE)); // when // then mockMvc.perform(post("/devdevdev/api/v1/articles/{id}/recommend", id) @@ -507,7 +491,10 @@ void updateRecommendNotFoundTechArticleException() throws Exception { @DisplayName("회원이 기술블로그 추천을 요청할 때 존재하지 않는 회원이라면 예외가 발생한다.") void updateRecommendNotFoundMemberException() throws Exception { // given - Long id = firstTechArticle.getId(); + Long id = 1L; + + given(memberTechArticleService.updateRecommend(eq(id), any(), any())) + .willThrow(new MemberException(INVALID_MEMBER_NOT_FOUND_MESSAGE)); // when // then mockMvc.perform(post("/devdevdev/api/v1/articles/{id}/recommend", id) @@ -521,65 +508,50 @@ void updateRecommendNotFoundMemberException() throws Exception { .andExpect(jsonPath("$.errorCode").value(HttpStatus.NOT_FOUND.value())); } - private SocialMemberDto createSocialDto(String userId, String name, String nickName, String password, String email, - String socialType, String role) { - return SocialMemberDto.builder() - .userId(userId) - .name(name) - .nickname(nickName) - .password(password) - .email(email) - .socialType(SocialType.valueOf(socialType)) - .role(Role.valueOf(role)) + private TechArticleMainResponse createTechArticleMainResponse(Long id, String thumbnailUrl, Boolean isLogoImage, + String techArticleUrl, String title, String contents, + Long companyId, String companyName, String careerUrl, String officialImageUrl, + LocalDate regDate, String author, long recommendCount, + long commentCount, long viewCount, Boolean isBookmarked, Float score) { + return TechArticleMainResponse.builder() + .id(id) + .thumbnailUrl(thumbnailUrl) + .isLogoImage(isLogoImage) + .techArticleUrl(techArticleUrl) + .title(title) + .contents(contents) + .company(CompanyResponse.of(companyId, companyName, careerUrl, officialImageUrl)) + .regDate(regDate) + .author(author) + .viewTotalCount(viewCount) + .recommendTotalCount(recommendCount) + .commentTotalCount(commentCount) + .popularScore(0L) + .isBookmarked(isBookmarked) + .score(score) .build(); } - - private Bookmark createBookmark(Member member, TechArticle techArticle, boolean status) { - return Bookmark.builder() - .member(member) - .techArticle(techArticle) - .status(status) - .build(); - } - - private boolean createRandomBoolean() { - return new Random().nextBoolean(); - } - - private static LocalDate createRandomDate() { - LocalDate startDate = LocalDate.of(2024, 1, 1); - LocalDate endDate = LocalDate.of(2024, 3, 10); - - // 시작 날짜와 종료 날짜 사이의 차이 중 랜덤한 일 수 선택 - long daysBetween = ChronoUnit.DAYS.between(startDate, endDate); - long randomDays = ThreadLocalRandom.current().nextLong(daysBetween + 1); - - return startDate.plusDays(randomDays); - } - - private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, - String careerUrl) { - return Company.builder() - .name(new CompanyName(companyName)) - .officialImageUrl(new Url(officialImageUrl)) - .careerUrl(new Url(careerUrl)) - .officialUrl(new Url(officialUrl)) - .build(); - } - - private static TechArticle createTechArticle(int i, Company company) { - return TechArticle.builder() - .title(new Title("타이틀 " + i)) - .contents("내용 " + i) - .company(company) - .author("작성자") - .regDate(LocalDate.now()) - .techArticleUrl(new Url("https://example.com/article")) - .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) - .commentTotalCount(new Count(i)) - .recommendTotalCount(new Count(i)) - .viewTotalCount(new Count(i)) - .popularScore(new Count(i)) + + private TechArticleDetailResponse createTechArticleDetailResponse(String thumbnailUrl, String techArticleUrl, + String title, String contents, Long companyId, + String companyName, String careerUrl, String officialImageUrl, + LocalDate regDate, String author, long viewCount, + long recommendCount, long commentCount, long popularScore, + boolean isRecommended, boolean isBookmarked) { + return TechArticleDetailResponse.builder() + .thumbnailUrl(thumbnailUrl) + .techArticleUrl(techArticleUrl) + .title(title) + .contents(contents) + .company(CompanyResponse.of(companyId, companyName, careerUrl, officialImageUrl)) + .regDate(regDate) + .author(author) + .viewTotalCount(viewCount) + .recommendTotalCount(recommendCount) + .commentTotalCount(commentCount) + .popularScore(popularScore) + .isRecommended(isRecommended) + .isBookmarked(isBookmarked) .build(); } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java index 1c7b346e..4a9776aa 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java @@ -2,9 +2,13 @@ import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.INVALID_MEMBER_NOT_FOUND_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.KEYWORD_WITH_SPECIAL_SYMBOLS_EXCEPTION_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_CURSOR_SCORE_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_TECH_ARTICLE_MESSAGE; import static com.dreamypatisiel.devdevdev.global.constant.SecurityConstant.AUTHORIZATION_HEADER; import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.authenticationType; import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.techArticleSortType; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; @@ -22,37 +26,23 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.dreamypatisiel.devdevdev.domain.entity.Bookmark; -import com.dreamypatisiel.devdevdev.domain.entity.Company; -import com.dreamypatisiel.devdevdev.domain.entity.Member; -import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.CompanyName; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.Count; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.Title; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; -import com.dreamypatisiel.devdevdev.domain.entity.enums.Role; -import com.dreamypatisiel.devdevdev.domain.entity.enums.SocialType; -import com.dreamypatisiel.devdevdev.domain.repository.CompanyRepository; -import com.dreamypatisiel.devdevdev.domain.repository.member.MemberRepository; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkRepository; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.GuestTechArticleService; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.MemberTechArticleService; +import com.dreamypatisiel.devdevdev.exception.MemberException; +import com.dreamypatisiel.devdevdev.exception.NotFoundException; +import com.dreamypatisiel.devdevdev.exception.TechArticleException; import com.dreamypatisiel.devdevdev.global.constant.SecurityConstant; -import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; +import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; +import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.*; import java.nio.charset.StandardCharsets; import java.time.LocalDate; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; import java.util.List; -import java.util.Random; -import java.util.concurrent.ThreadLocalRandom; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; @@ -63,65 +53,34 @@ public class TechArticleControllerDocsTest extends SupportControllerDocsTest { - private static final int TEST_ARTICLES_COUNT = 20; - private static Company company; - private static TechArticle firstTechArticle; - private static List techArticles; - - @Autowired - TechArticleRepository techArticleRepository; - @Autowired - CompanyRepository companyRepository; - @Autowired - MemberRepository memberRepository; - @Autowired - BookmarkRepository bookmarkRepository; - - @BeforeAll - static void setup(@Autowired TechArticleRepository techArticleRepository, - @Autowired CompanyRepository companyRepository) { - company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", - "https://example.com"); - companyRepository.save(company); - - List techArticles = new ArrayList<>(); - for (int i = 0; i < 10; i++) { - TechArticle techArticle = createTechArticle(i, company); - techArticles.add(techArticle); - } - techArticleRepository.saveAll(techArticles); - } + @MockBean + GuestTechArticleService guestTechArticleService; + + @MockBean + MemberTechArticleService memberTechArticleService; - @AfterAll - static void tearDown(@Autowired TechArticleRepository techArticleRepository, - @Autowired CompanyRepository companyRepository) { - techArticleRepository.deleteAllInBatch(); - companyRepository.deleteAllInBatch(); - } @Test @DisplayName("회원이 기술블로그 메인을 조회한다.") void getTechArticlesByMember() throws Exception { // given - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); - - List bookmarks = new ArrayList<>(); - for (TechArticle techArticle : techArticles) { - if (creatRandomBoolean()) { - Bookmark bookmark = createBookmark(member, techArticle, true); - bookmarks.add(bookmark); - } - } - bookmarkRepository.saveAll(bookmarks); - Pageable pageable = PageRequest.of(0, 1); String techArticleId = "1"; String keyword = "타이틀"; - String companyId = company.getId().toString(); + String companyId = "1"; + + TechArticleMainResponse response = createTechArticleMainResponse( + 1L, "http://thumbnail.com", false, "http://article.com", "타이틀 1", "내용 1", + 1L, "회사명", "http://career.com", "http://official.com", LocalDate.now(), "작성자", + 10L, 5L, 100L, true, 10.0f + ); + + SliceCustom mockSlice = new SliceCustom<>( + List.of(response), pageable, false, 1L + ); + + given(memberTechArticleService.getTechArticles(any(), any(), any(), any(), any(), any(), any())) + .willReturn(mockSlice); // when // then ResultActions actions = mockMvc.perform(get("/devdevdev/api/v1/articles") @@ -229,6 +188,9 @@ void getTechArticlesWithKeywordWithCursorOrderByHIGHEST_SCOREWithoutScoreExcepti Pageable pageable = PageRequest.of(0, 10); String techArticleId = "1"; String keyword = "타이틀"; + + given(guestTechArticleService.getTechArticles(any(), any(), any(), any(), any(), any(), any())) + .willThrow(new TechArticleException(NOT_FOUND_CURSOR_SCORE_MESSAGE)); // when // then ResultActions actions = mockMvc.perform(get("/devdevdev/api/v1/articles") @@ -258,6 +220,9 @@ void getTechArticlesSpecialSymbolException() throws Exception { // given Pageable pageable = PageRequest.of(0, 10); String keyword = "!"; + + given(guestTechArticleService.getTechArticles(any(), any(), any(), any(), any(), any(), any())) + .willThrow(new TechArticleException(KEYWORD_WITH_SPECIAL_SYMBOLS_EXCEPTION_MESSAGE)); // when // then ResultActions actions = mockMvc.perform(MockMvcRequestBuilders.get("/devdevdev/api/v1/articles") @@ -286,12 +251,16 @@ void getTechArticlesSpecialSymbolException() throws Exception { @DisplayName("회원이 기술블로그 상세를 조회한다.") void getTechArticleByMember() throws Exception { // given - Long id = firstTechArticle.getId(); - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); + Long id = 1L; + + TechArticleDetailResponse response = createTechArticleDetailResponse( + "http://thumbnail.com", "http://article.com", "기술블로그 제목", "기술블로그 내용", + 1L, "회사명", "http://career.com", "http://official.com", LocalDate.now(), "작성자", + 100L, 10L, 5L, 50L, false, true + ); + + given(memberTechArticleService.getTechArticle(eq(id), any(), any())) + .willReturn(response); // when // then ResultActions actions = mockMvc.perform(get("/devdevdev/api/v1/articles/{techArticleId}", id) @@ -345,12 +314,10 @@ void getTechArticleByMember() throws Exception { @DisplayName("회원이 기술블로그 상세를 조회할 때 회원이 없으면 예외가 발생한다.") void getTechArticleNotFoundMemberException() throws Exception { // given - Long id = firstTechArticle.getId(); - // given - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); + Long id = 1L; + + given(memberTechArticleService.getTechArticle(eq(id), any(), any())) + .willThrow(new MemberException(INVALID_MEMBER_NOT_FOUND_MESSAGE)); // when // then ResultActions actions = mockMvc.perform(get("/devdevdev/api/v1/articles/{techArticleId}", id) @@ -378,22 +345,20 @@ void getTechArticleNotFoundMemberException() throws Exception { @DisplayName("기술블로그 상세를 조회할 때 기술블로그가 존재하지 않으면 예외가 발생한다.") void getTechArticleNotFoundTechArticleException() throws Exception { // given - TechArticle techArticle = createTechArticle(1, company); - techArticleRepository.save(techArticle); - Long id = techArticle.getId() + 1; - - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); + Long id = 999L; + + given(guestTechArticleService.getTechArticle(eq(id), any(), any())) + .willThrow(new NotFoundException(NOT_FOUND_TECH_ARTICLE_MESSAGE)); // when // then ResultActions actions = mockMvc.perform(get("/devdevdev/api/v1/articles/{techArticleId}", id) .contentType(MediaType.APPLICATION_JSON) - .characterEncoding(StandardCharsets.UTF_8) - .header(AUTHORIZATION_HEADER, SecurityConstant.BEARER_PREFIX + accessToken)) - .andDo(print()); + .characterEncoding(StandardCharsets.UTF_8)) + .andDo(print()) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.resultType").value(ResultType.FAIL.name())) + .andExpect(jsonPath("$.message").value(NOT_FOUND_TECH_ARTICLE_MESSAGE)) + .andExpect(jsonPath("$.errorCode").value(HttpStatus.NOT_FOUND.value())); // Docs actions.andDo(document("not-found-tech-article-exception", @@ -410,12 +375,11 @@ void getTechArticleNotFoundTechArticleException() throws Exception { @DisplayName("회원이 기술블로그 북마크를 요청한다.") void updateBookmark() throws Exception { // given - Long id = firstTechArticle.getId(); - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); + Long id = 1L; + + BookmarkResponse response = new BookmarkResponse(id, true); + given(memberTechArticleService.updateBookmark(eq(id), any())) + .willReturn(response); // when // then ResultActions actions = mockMvc.perform(post("/devdevdev/api/v1/articles/{techArticleId}/bookmark", id) @@ -448,12 +412,11 @@ void updateBookmark() throws Exception { @DisplayName("회원이 기술블로그 추천을 요청한다.") void updateRecommend() throws Exception { // given - Long id = firstTechArticle.getId(); - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); + Long id = 1L; + + TechArticleRecommendResponse response = new TechArticleRecommendResponse(id, true, 11L); + given(memberTechArticleService.updateRecommend(eq(id), any(), any())) + .willReturn(response); // when // then ResultActions actions = mockMvc.perform(post("/devdevdev/api/v1/articles/{techArticleId}/recommend", id) @@ -480,69 +443,54 @@ void updateRecommend() throws Exception { fieldWithPath("data.techArticleId").type(JsonFieldType.NUMBER).description("기술블로그 아이디"), fieldWithPath("data.status").type(JsonFieldType.BOOLEAN).description("추천 상태"), fieldWithPath("data.recommendTotalCount").type(JsonFieldType.NUMBER).description("기술블로그 총 추천수") - ) - )); - } - - private SocialMemberDto createSocialDto(String userId, String name, String nickName, String password, String email, - String socialType, String role) { - return SocialMemberDto.builder() - .userId(userId) - .name(name) - .nickname(nickName) - .password(password) - .email(email) - .socialType(SocialType.valueOf(socialType)) - .role(Role.valueOf(role)) - .build(); + )) + ); } - private Bookmark createBookmark(Member member, TechArticle techArticle, boolean status) { - return Bookmark.builder() - .member(member) - .techArticle(techArticle) - .status(status) + private TechArticleMainResponse createTechArticleMainResponse(Long id, String thumbnailUrl, Boolean isLogoImage, + String techArticleUrl, String title, String contents, + Long companyId, String companyName, String careerUrl, String officialImageUrl, + LocalDate regDate, String author, long recommendCount, + long commentCount, long viewCount, Boolean isBookmarked, Float score) { + return TechArticleMainResponse.builder() + .id(id) + .thumbnailUrl(thumbnailUrl) + .isLogoImage(isLogoImage) + .techArticleUrl(techArticleUrl) + .title(title) + .contents(contents) + .company(CompanyResponse.of(companyId, companyName, careerUrl, officialImageUrl)) + .regDate(regDate) + .author(author) + .viewTotalCount(viewCount) + .recommendTotalCount(recommendCount) + .commentTotalCount(commentCount) + .popularScore(0L) + .isBookmarked(isBookmarked) + .score(score) .build(); } - - private boolean creatRandomBoolean() { - return new Random().nextBoolean(); - } - - private static LocalDate createRandomDate() { - LocalDate startDate = LocalDate.of(2024, 1, 1); - LocalDate endDate = LocalDate.of(2024, 3, 10); - - // 시작 날짜와 종료 날짜 사이의 차이 중 랜덤한 일 수 선택 - long daysBetween = ChronoUnit.DAYS.between(startDate, endDate); - long randomDays = ThreadLocalRandom.current().nextLong(daysBetween + 1); - - return startDate.plusDays(randomDays); - } - - private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, - String careerUrl) { - return Company.builder() - .name(new CompanyName(companyName)) - .officialImageUrl(new Url(officialImageUrl)) - .careerUrl(new Url(careerUrl)) - .officialUrl(new Url(officialUrl)) - .build(); - } - - private static TechArticle createTechArticle(int i, Company company) { - return TechArticle.builder() - .title(new Title("타이틀 " + i)) - .contents("내용 " + i) - .company(company) - .author("작성자") - .regDate(LocalDate.now()) - .techArticleUrl(new Url("https://example.com/article")) - .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) - .commentTotalCount(new Count(i)) - .recommendTotalCount(new Count(i)) - .viewTotalCount(new Count(i)) - .popularScore(new Count(i)) + + private TechArticleDetailResponse createTechArticleDetailResponse(String thumbnailUrl, String techArticleUrl, + String title, String contents, Long companyId, + String companyName, String careerUrl, String officialImageUrl, + LocalDate regDate, String author, long viewCount, + long recommendCount, long commentCount, long popularScore, + boolean isRecommended, boolean isBookmarked) { + return TechArticleDetailResponse.builder() + .thumbnailUrl(thumbnailUrl) + .techArticleUrl(techArticleUrl) + .title(title) + .contents(contents) + .company(CompanyResponse.of(companyId, companyName, careerUrl, officialImageUrl)) + .regDate(regDate) + .author(author) + .viewTotalCount(viewCount) + .recommendTotalCount(recommendCount) + .commentTotalCount(commentCount) + .popularScore(popularScore) + .isRecommended(isRecommended) + .isBookmarked(isBookmarked) .build(); } } From 64d7073d2e04a9d2faf3eff48e440d9c0638a50a Mon Sep 17 00:00:00 2001 From: soyoung Date: Fri, 29 Aug 2025 23:10:06 +0900 Subject: [PATCH 61/66] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../techArticle/custom/TechArticleRepositoryImpl.java | 11 ++++++----- .../techArticle/custom/TechKeywordRepositoryImpl.java | 8 +++++--- .../global/config/CustomMySQLFunctionContributor.java | 7 +++++++ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java index 100fbcad..bb829cc3 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java @@ -29,6 +29,8 @@ public class TechArticleRepositoryImpl implements TechArticleRepositoryCustom { public static final String MATCH_AGAINST_FUNCTION = "match_against"; + public static final String MATCH_AGAINST_NL_FUNCTION = "match_against_nl"; + private final JPQLQueryFactory query; @Override @@ -55,9 +57,8 @@ public SliceCustom findTechArticlesByCursor(Pageable pageable, Long // 키워드가 있는 경우 FULLTEXT 검색, 없는 경우 일반 조회 if (StringUtils.hasText(keyword)) { return findTechArticlesByCursorWithKeyword(pageable, techArticleId, techArticleSort, companyId, keyword, score); - } else { - return findTechArticlesByCursorWithoutKeyword(pageable, techArticleId, techArticleSort, companyId); } + return findTechArticlesByCursorWithoutKeyword(pageable, techArticleId, techArticleSort, companyId); } // 키워드 검색 @@ -76,13 +77,13 @@ private SliceCustom findTechArticlesByCursorWithKeyword(Pageable pa techArticle.contents, keyword ); - // 스코어 계산을 위한 expression + // 스코어 계산을 위한 expression (Natural Language Mode) NumberTemplate titleScore = Expressions.numberTemplate(Double.class, - "function('" + MATCH_AGAINST_FUNCTION + "', {0}, {1})", + "function('" + MATCH_AGAINST_NL_FUNCTION + "', {0}, {1})", techArticle.title.title, keyword ); NumberTemplate contentsScore = Expressions.numberTemplate(Double.class, - "function('" + MATCH_AGAINST_FUNCTION + "', {0}, {1})", + "function('" + MATCH_AGAINST_NL_FUNCTION + "', {0}, {1})", techArticle.contents, keyword ); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryImpl.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryImpl.java index 46d782a5..044fe3a3 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryImpl.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryImpl.java @@ -16,6 +16,8 @@ public class TechKeywordRepositoryImpl implements TechKeywordRepositoryCustom { public static final String MATCH_AGAINST_FUNCTION = "match_against"; + public static final String MATCH_AGAINST_NL_FUNCTION = "match_against_nl"; + private final JPQLQueryFactory query; @Override @@ -30,13 +32,13 @@ public List searchKeyword(String inputJamo, String inputChosung, Pa techKeyword.chosungKey, inputChosung ); - // 스코어 계산을 위한 expression + // 스코어 계산을 위한 expression (Natural Language Mode) NumberTemplate jamoScore = Expressions.numberTemplate(Double.class, - "function('" + MATCH_AGAINST_FUNCTION + "', {0}, {1})", + "function('" + MATCH_AGAINST_NL_FUNCTION + "', {0}, {1})", techKeyword.jamoKey, inputJamo ); NumberTemplate chosungScore = Expressions.numberTemplate(Double.class, - "function('" + MATCH_AGAINST_FUNCTION + "', {0}, {1})", + "function('" + MATCH_AGAINST_NL_FUNCTION + "', {0}, {1})", techKeyword.chosungKey, inputChosung ); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/global/config/CustomMySQLFunctionContributor.java b/src/main/java/com/dreamypatisiel/devdevdev/global/config/CustomMySQLFunctionContributor.java index 1328d9a2..6cbd1275 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/global/config/CustomMySQLFunctionContributor.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/config/CustomMySQLFunctionContributor.java @@ -8,11 +8,18 @@ public class CustomMySQLFunctionContributor implements FunctionContributor { private static final String MATCH_AGAINST_FUNCTION = "match_against"; private static final String MATCH_AGAINST_PATTERN = "match (?1) against (?2 in boolean mode)"; + + private static final String MATCH_AGAINST_NL_FUNCTION = "match_against_nl"; + private static final String MATCH_AGAINST_NL_PATTERN = "match (?1) against (?2 in natural language mode)"; @Override public void contributeFunctions(FunctionContributions functionContributions) { functionContributions.getFunctionRegistry() .registerPattern(MATCH_AGAINST_FUNCTION, MATCH_AGAINST_PATTERN, functionContributions.getTypeConfiguration().getBasicTypeRegistry().resolve(DOUBLE)); + + functionContributions.getFunctionRegistry() + .registerPattern(MATCH_AGAINST_NL_FUNCTION, MATCH_AGAINST_NL_PATTERN, + functionContributions.getTypeConfiguration().getBasicTypeRegistry().resolve(DOUBLE)); } } From ee1055364f2c6e22c006c2652aa55daed6f19189 Mon Sep 17 00:00:00 2001 From: soyoung Date: Sat, 30 Aug 2025 00:45:16 +0900 Subject: [PATCH 62/66] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../asciidoc/api/tech-article/detail.adoc | 1 - src/docs/asciidoc/api/tech-article/main.adoc | 1 - .../GuestTechArticleServiceTest.java | 217 +++++++++++++++++- .../MemberTechArticleServiceTest.java | 198 +++++++++++++++- 4 files changed, 402 insertions(+), 15 deletions(-) diff --git a/src/docs/asciidoc/api/tech-article/detail.adoc b/src/docs/asciidoc/api/tech-article/detail.adoc index ce677362..e552ff5b 100644 --- a/src/docs/asciidoc/api/tech-article/detail.adoc +++ b/src/docs/asciidoc/api/tech-article/detail.adoc @@ -19,5 +19,4 @@ include::{snippets}/tech-article-detail/response-fields.adoc[] === 예외 ==== HTTP Response -include::{snippets}/not-found-elastic-id-exception/response-body.adoc[] include::{snippets}/not-found-tech-article-exception/response-body.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/api/tech-article/main.adoc b/src/docs/asciidoc/api/tech-article/main.adoc index 9e9ef62a..941b4f8c 100644 --- a/src/docs/asciidoc/api/tech-article/main.adoc +++ b/src/docs/asciidoc/api/tech-article/main.adoc @@ -30,6 +30,5 @@ include::{snippets}/tech-article-main/response-fields.adoc[] ==== HTTP Response -include::{snippets}/not-found-elastic-tech-article-cursor-exception/response-body.adoc[] include::{snippets}/not-found-score-exception/response-body.adoc[] include::{snippets}/keyword-with-special-symbols-exception/response-body.adoc[] \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechArticleServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechArticleServiceTest.java index b073b976..5abbb131 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechArticleServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechArticleServiceTest.java @@ -183,6 +183,26 @@ void getTechArticles() { // then assertThat(techArticles) .hasSize(pageable.getPageSize()) + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull().isFalse(); + }) .extracting(TechArticleMainResponse::getRegDate) .isSortedAccordingTo(Comparator.reverseOrder()); // 기본 정렬은 최신순 } @@ -231,7 +251,23 @@ void getTechArticle() { .isNotNull() .isInstanceOf(TechArticleDetailResponse.class) .satisfies(article -> { + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); assertThat(article.getIsBookmarked()).isFalse(); + assertThat(article.getIsRecommended()).isNotNull(); }); } @@ -270,6 +306,22 @@ void getTechArticleWithRecommend() { .isNotNull() .isInstanceOf(TechArticleDetailResponse.class) .satisfies(article -> { + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsBookmarked()).isFalse(); assertThat(article.getIsRecommended()).isTrue(); }); } @@ -300,6 +352,22 @@ void getTechArticleIncrementViewCount() { // then assertThat(techArticleDetailResponse) .satisfies(article -> { + // 모든 필드 검증 + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsBookmarked()).isFalse(); + assertThat(article.getIsRecommended()).isNotNull(); assertThat(article.getViewTotalCount()).isEqualTo(prevViewTotalCount + 1); assertThat(article.getPopularScore()).isEqualTo(prevPopularScore + 2); }); @@ -487,8 +555,6 @@ void cancelTechArticleRecommend() { }); } - // ===== ElasticTechArticleServiceTest에서 이관된 정렬 및 커서 기능 테스트들 ===== - @ParameterizedTest @EnumSource(value = TechArticleSort.class, names = {"LATEST", "MOST_VIEWED", "MOST_COMMENTED", "POPULAR"}) @DisplayName("익명 사용자가 다양한 정렬 기준으로 기술블로그를 조회한다.") @@ -508,6 +574,29 @@ void getTechArticlesWithDifferentSorts(TechArticleSort sort) { assertThat(techArticles).hasSize(pageable.getPageSize()); List articles = techArticles.getContent(); + + assertThat(articles).allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull().isFalse(); + }); + + // 정렬 검증 switch (sort) { case LATEST -> assertThat(articles) .extracting(TechArticleMainResponse::getRegDate) @@ -548,6 +637,26 @@ void getTechArticlesWithCursorOrderByLatest() { // then assertThat(secondPage) .hasSize(pageable.getPageSize()) + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull().isFalse(); + }) .extracting(TechArticleMainResponse::getRegDate) .isSortedAccordingTo(Comparator.reverseOrder()) .allMatch(date -> !date.isAfter(cursor.getRegDate())); @@ -577,6 +686,26 @@ void getTechArticlesWithCursorOrderByMostViewed() { // then assertThat(secondPage) .hasSize(pageable.getPageSize()) + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull().isFalse(); + }) .extracting(TechArticleMainResponse::getViewTotalCount) .isSortedAccordingTo(Comparator.reverseOrder()) .allMatch(viewCount -> viewCount <= cursor.getViewTotalCount()); @@ -606,6 +735,26 @@ void getTechArticlesWithCursorOrderByMostCommented() { // then assertThat(secondPage) .hasSize(pageable.getPageSize()) + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull().isFalse(); + }) .extracting(TechArticleMainResponse::getCommentTotalCount) .isSortedAccordingTo(Comparator.reverseOrder()) .allMatch(commentCount -> commentCount <= cursor.getCommentTotalCount()); @@ -635,6 +784,26 @@ void getTechArticlesWithCursorOrderByPopular() { // then assertThat(secondPage) .hasSize(pageable.getPageSize()) + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull().isFalse(); + }) .extracting(TechArticleMainResponse::getPopularScore) .isSortedAccordingTo(Comparator.reverseOrder()) .allMatch(popularScore -> popularScore <= cursor.getPopularScore()); @@ -659,7 +828,25 @@ void getTechArticlesWithKeyword() { assertThat(techArticles.getContent()) .isNotEmpty() .allSatisfy(article -> { - boolean containsKeyword = article.getTitle().contains(keyword) || + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull().isFalse(); + boolean containsKeyword = article.getTitle().contains(keyword) || article.getContents().contains(keyword); assertThat(containsKeyword).isTrue(); }); @@ -682,9 +869,27 @@ void getTechArticlesFilterByCompany() { // then assertThat(techArticles.getContent()) .isNotEmpty() - .allSatisfy(article -> - assertThat(article.getCompany().getId()).isEqualTo(testCompany.getId()) - ) + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull().isFalse(); + assertThat(article.getCompany().getId()).isEqualTo(testCompany.getId()); + }) .extracting(TechArticleMainResponse::getRegDate) .isSortedAccordingTo(Comparator.reverseOrder()); } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechArticleServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechArticleServiceTest.java index f1f09389..eff2bfc7 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechArticleServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechArticleServiceTest.java @@ -186,6 +186,27 @@ void getTechArticles() { // then assertThat(techArticles) .hasSize(pageable.getPageSize()) + .allSatisfy(article -> { + // 모든 필드 검증 + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull(); + }) .extracting(TechArticleMainResponse::getRegDate) .isSortedAccordingTo(Comparator.reverseOrder()); // 기본 정렬은 최신순 } @@ -235,7 +256,24 @@ void getTechArticle() { .isNotNull() .isInstanceOf(TechArticleDetailResponse.class) .satisfies(article -> { + // 모든 필드 검증 + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); assertThat(article.getIsBookmarked()).isNotNull(); + assertThat(article.getIsRecommended()).isNotNull(); }); } @@ -267,6 +305,22 @@ void getTechArticleWithRecommend() { .isNotNull() .isInstanceOf(TechArticleDetailResponse.class) .satisfies(article -> { + // 모든 필드 검증 + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); assertThat(article.getIsBookmarked()).isNotNull(); assertThat(article.getIsRecommended()).isTrue(); }); @@ -297,6 +351,22 @@ void getTechArticleWithoutRecommend() { .isNotNull() .isInstanceOf(TechArticleDetailResponse.class) .satisfies(article -> { + // 모든 필드 검증 + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); assertThat(article.getIsBookmarked()).isNotNull(); assertThat(article.getIsRecommended()).isFalse(); }); @@ -329,6 +399,22 @@ void getTechArticleIncrementViewCount() { .isNotNull() .isInstanceOf(TechArticleDetailResponse.class) .satisfies(article -> { + // 모든 필드 검증 + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsBookmarked()).isNotNull(); + assertThat(article.getIsRecommended()).isNotNull(); assertThat(article.getViewTotalCount()).isEqualTo(prevViewTotalCount + 1); assertThat(article.getPopularScore()).isEqualTo(prevPopularScore + 2); }); @@ -530,8 +616,6 @@ void cancelTechArticleRecommend() { }); } - // ===== ElasticTechArticleServiceTest에서 이관된 정렬 및 커서 기능 테스트들 ===== - @ParameterizedTest @EnumSource(value = TechArticleSort.class, names = {"LATEST", "MOST_VIEWED", "MOST_COMMENTED", "POPULAR"}) @DisplayName("회원이 다양한 정렬 기준으로 기술블로그를 조회한다.") @@ -557,6 +641,30 @@ void getTechArticlesWithDifferentSorts(TechArticleSort sort) { assertThat(techArticles).hasSize(pageable.getPageSize()); List articles = techArticles.getContent(); + + // 모든 응답 객체의 필드 검증 + assertThat(articles).allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull(); + }); + + // 정렬 검증 switch (sort) { case LATEST -> assertThat(articles) .extracting(TechArticleMainResponse::getRegDate) @@ -603,6 +711,26 @@ void getTechArticlesWithCursorOrderByLatest() { // then assertThat(secondPage) .hasSize(pageable.getPageSize()) + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull(); + }) .extracting(TechArticleMainResponse::getRegDate) .isSortedAccordingTo(Comparator.reverseOrder()) .allMatch(date -> !date.isAfter(cursor.getRegDate())); @@ -638,6 +766,26 @@ void getTechArticlesWithCursorOrderByMostViewed() { // then assertThat(secondPage) .hasSize(pageable.getPageSize()) + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull(); + }) .extracting(TechArticleMainResponse::getViewTotalCount) .isSortedAccordingTo(Comparator.reverseOrder()) .allMatch(viewCount -> viewCount <= cursor.getViewTotalCount()); @@ -668,8 +816,26 @@ void getTechArticlesWithKeyword() { assertThat(techArticles.getContent()) .isNotEmpty() .allSatisfy(article -> { - boolean containsKeyword = article.getTitle().contains(keyword) || - article.getContents().contains(keyword); + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull(); + boolean containsKeyword = article.getTitle().contains(keyword) || + article.getContents().contains(keyword); assertThat(containsKeyword).isTrue(); }); } @@ -697,9 +863,27 @@ void getTechArticlesFilterByCompany() { // then assertThat(techArticles.getContent()) .isNotEmpty() - .allSatisfy(article -> - assertThat(article.getCompany().getId()).isEqualTo(testCompany.getId()) - ) + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull(); + assertThat(article.getCompany().getId()).isEqualTo(testCompany.getId()); + }) .extracting(TechArticleMainResponse::getRegDate) .isSortedAccordingTo(Comparator.reverseOrder()); } From cc3fcbd502aa7f496dc5eec91aba662873f29270 Mon Sep 17 00:00:00 2001 From: soyoung Date: Sun, 31 Aug 2025 13:52:08 +0900 Subject: [PATCH 63/66] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81(=EC=A4=91=EB=B3=B5=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../techArticle/custom/TechArticleRepositoryImpl.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java index bb829cc3..e3d18159 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java @@ -94,7 +94,7 @@ private SliceCustom findTechArticlesByCursorWithKeyword(Pageable pa List contents = query.selectFrom(techArticle) .where(titleMatch.or(contentsMatch)) - .where(companyId != null ? techArticle.company.id.eq(companyId) : null) + .where(getCompanyIdCondition(companyId)) .where(getCursorConditionForKeywordSearch(techArticleSort, techArticleId, score, totalScore)) .orderBy(getOrderSpecifierForKeywordSearch(techArticleSort, totalScore), techArticle.id.desc()) .limit(pageable.getPageSize()) @@ -104,7 +104,7 @@ private SliceCustom findTechArticlesByCursorWithKeyword(Pageable pa long totalElements = query.select(techArticle.count()) .from(techArticle) .where(titleMatch.or(contentsMatch)) - .where(companyId != null ? techArticle.company.id.eq(companyId) : null) + .where(getCompanyIdCondition(companyId)) .fetchCount(); return new SliceCustom<>(contents, pageable, totalElements); @@ -226,6 +226,13 @@ private OrderSpecifier getOrderSpecifierForKeywordSearch(TechArticleSort tech .orElse(TechArticleSort.HIGHEST_SCORE).getOrderSpecifierByTechArticleSort(); } + public BooleanExpression getCompanyIdCondition(Long companyId) { + if(companyId == null) { + return null; + } + return techArticle.company.id.eq(companyId); + } + private boolean hasNextPage(List contents, int pageSize) { return contents.size() >= pageSize; } From 59752f183e1b46807c5d496537e76b0577490fdc Mon Sep 17 00:00:00 2001 From: soyoung Date: Sun, 31 Aug 2025 15:25:35 +0900 Subject: [PATCH 64/66] =?UTF-8?q?fix(techArticle):=20=EA=B8=B0=EC=88=A0?= =?UTF-8?q?=EB=B8=94=EB=A1=9C=EA=B7=B8=20=ED=82=A4=EC=9B=8C=EB=93=9C?= =?UTF-8?q?=EA=B2=80=EC=83=89=EC=8B=9C=20score=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../techArticle/TechArticleDto.java | 16 ++++++++ .../custom/TechArticleRepositoryCustom.java | 5 ++- .../custom/TechArticleRepositoryImpl.java | 41 +++++++++++++------ .../techArticle/GuestTechArticleService.java | 9 ++-- .../techArticle/MemberTechArticleService.java | 11 ++--- .../techArticle/TechArticleService.java | 2 +- .../techArticle/TechArticleController.java | 2 +- .../techArticle/TechArticleMainResponse.java | 12 +++--- .../NotificationControllerTest.java | 2 +- .../TechArticleControllerTest.java | 6 +-- .../docs/NotificationControllerDocsTest.java | 2 +- .../docs/TechArticleControllerDocsTest.java | 4 +- 12 files changed, 72 insertions(+), 40 deletions(-) create mode 100644 src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleDto.java diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleDto.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleDto.java new file mode 100644 index 00000000..e2d13a7f --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleDto.java @@ -0,0 +1,16 @@ +package com.dreamypatisiel.devdevdev.domain.repository.techArticle; + +import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class TechArticleDto { + private final TechArticle techArticle; + private final Double score; + + public static TechArticleDto of(TechArticle techArticle, Double score) { + return new TechArticleDto(techArticle, score); + } +} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryCustom.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryCustom.java index c874cc41..e9341776 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryCustom.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryCustom.java @@ -6,6 +6,7 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleDto; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -13,6 +14,6 @@ public interface TechArticleRepositoryCustom { Slice findBookmarkedByMemberAndCursor(Pageable pageable, Long techArticleId, BookmarkSort bookmarkSort, Member member); - SliceCustom findTechArticlesByCursor(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, - Long companyId, String keyword, Float score); + SliceCustom findTechArticlesByCursor(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, + Long companyId, String keyword, Double score); } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java index e3d18159..c3524eb4 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java @@ -9,6 +9,8 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkSort; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleDto; +import com.querydsl.core.Tuple; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Predicate; import com.querydsl.core.types.dsl.BooleanExpression; @@ -50,9 +52,9 @@ public Slice findBookmarkedByMemberAndCursor(Pageable pageable, Lon } @Override - public SliceCustom findTechArticlesByCursor(Pageable pageable, Long techArticleId, + public SliceCustom findTechArticlesByCursor(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, Long companyId, - String keyword, Float score + String keyword, Double score ) { // 키워드가 있는 경우 FULLTEXT 검색, 없는 경우 일반 조회 if (StringUtils.hasText(keyword)) { @@ -62,9 +64,9 @@ public SliceCustom findTechArticlesByCursor(Pageable pageable, Long } // 키워드 검색 - private SliceCustom findTechArticlesByCursorWithKeyword(Pageable pageable, Long techArticleId, + public SliceCustom findTechArticlesByCursorWithKeyword(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, Long companyId, - String keyword, Float score + String keyword, Double score ) { // FULLTEXT 검색 조건 생성 BooleanExpression titleMatch = Expressions.booleanTemplate( @@ -92,7 +94,9 @@ private SliceCustom findTechArticlesByCursorWithKeyword(Pageable pa "({0} * 2.0) + {1}", titleScore, contentsScore ); - List contents = query.selectFrom(techArticle) + // TechArticle과 score를 함께 조회 + List results = query.select(techArticle, totalScore) + .from(techArticle) .where(titleMatch.or(contentsMatch)) .where(getCompanyIdCondition(companyId)) .where(getCursorConditionForKeywordSearch(techArticleSort, techArticleId, score, totalScore)) @@ -100,6 +104,12 @@ private SliceCustom findTechArticlesByCursorWithKeyword(Pageable pa .limit(pageable.getPageSize()) .fetch(); + // Tuple을 TechArticleDto로 변환 + List contents = results.stream() + .map(result -> TechArticleDto.of( + result.get(techArticle), result.get(totalScore))) + .toList(); + // 키워드 검색 결과 총 갯수 long totalElements = query.select(techArticle.count()) .from(techArticle) @@ -111,16 +121,21 @@ private SliceCustom findTechArticlesByCursorWithKeyword(Pageable pa } // 일반 조회 - private SliceCustom findTechArticlesByCursorWithoutKeyword(Pageable pageable, Long techArticleId, + private SliceCustom findTechArticlesByCursorWithoutKeyword(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, Long companyId ) { - List contents = query.selectFrom(techArticle) + List results = query.selectFrom(techArticle) .where(getCursorConditionFromTechArticleSort(techArticleSort, techArticleId)) - .where(companyId != null ? techArticle.company.id.eq(companyId) : null) + .where(getCompanyIdCondition(companyId)) .orderBy(techArticleSort(techArticleSort), techArticle.id.desc()) .limit(pageable.getPageSize()) .fetch(); + // Tuple을 TechArticleDto로 변환 + List contents = results.stream() + .map(result -> TechArticleDto.of(result, null)) + .toList(); + // 기술블로그 총 갯수 long totalElements = query.select(techArticle.count()) .from(techArticle) @@ -188,13 +203,13 @@ private OrderSpecifier techArticleSort(TechArticleSort techArticleSort) { // 키워드 검색을 위한 커서 조건 생성 private Predicate getCursorConditionForKeywordSearch(TechArticleSort techArticleSort, Long techArticleId, - Float score, NumberTemplate totalScore) { - if (ObjectUtils.isEmpty(techArticleId) || ObjectUtils.isEmpty(score)) { + Double score, NumberTemplate totalScore) { + if (ObjectUtils.isEmpty(techArticleId)) { return null; } // HIGHEST_SCORE(정확도순)인 경우 스코어 기반 커서 사용 - if (techArticleSort == TechArticleSort.HIGHEST_SCORE) { + if (techArticleSort == TechArticleSort.HIGHEST_SCORE || ObjectUtils.isEmpty(techArticleSort)) { return totalScore.lt(score.doubleValue()) .or(totalScore.eq(score.doubleValue()) .and(techArticle.id.lt(techArticleId))); @@ -217,13 +232,13 @@ private Predicate getCursorConditionForKeywordSearch(TechArticleSort techArticle private OrderSpecifier getOrderSpecifierForKeywordSearch(TechArticleSort techArticleSort, NumberTemplate totalScore) { // HIGHEST_SCORE(정확도순)인 경우 스코어 기반 정렬 - if (techArticleSort == TechArticleSort.HIGHEST_SCORE) { + if (techArticleSort == TechArticleSort.HIGHEST_SCORE || ObjectUtils.isEmpty(techArticleSort)) { return totalScore.desc(); } // 다른 정렬 방식인 경우 기존 정렬 사용 return Optional.ofNullable(techArticleSort) - .orElse(TechArticleSort.HIGHEST_SCORE).getOrderSpecifierByTechArticleSort(); + .orElse(TechArticleSort.LATEST).getOrderSpecifierByTechArticleSort(); } public BooleanExpression getCompanyIdCondition(Long companyId) { diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/GuestTechArticleService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/GuestTechArticleService.java index 1d312e5c..67ed3597 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/GuestTechArticleService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/GuestTechArticleService.java @@ -11,6 +11,7 @@ import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.*; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleDto; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -50,17 +51,19 @@ public GuestTechArticleService(TechArticlePopularScorePolicy techArticlePopularS @Override public Slice getTechArticles(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, String keyword, - Long companyId, Float score, Authentication authentication) { + Long companyId, Double score, Authentication authentication) { // 익명 사용자 호출인지 확인 AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); // 기술블로그 조회 - SliceCustom techArticles = techArticleRepository.findTechArticlesByCursor( + SliceCustom techArticles = techArticleRepository.findTechArticlesByCursor( pageable, techArticleId, techArticleSort, companyId, keyword, score); // 데이터 가공 List techArticlesResponse = techArticles.stream() - .map(TechArticleMainResponse::of) + .map(techArticle -> TechArticleMainResponse.of( + techArticle.getTechArticle(), techArticle.getScore() + )) .toList(); return new SliceCustom<>(techArticlesResponse, pageable, techArticles.hasNext(), techArticles.getTotalElements()); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/MemberTechArticleService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/MemberTechArticleService.java index ea0b854e..1d3ba278 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/MemberTechArticleService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/MemberTechArticleService.java @@ -5,10 +5,7 @@ import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; import com.dreamypatisiel.devdevdev.domain.entity.TechArticleRecommend; import com.dreamypatisiel.devdevdev.domain.policy.TechArticlePopularScorePolicy; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkRepository; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRecommendRepository; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.*; import com.dreamypatisiel.devdevdev.global.common.MemberProvider; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.*; @@ -50,17 +47,17 @@ public MemberTechArticleService(TechArticlePopularScorePolicy techArticlePopular @Override public Slice getTechArticles(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, String keyword, - Long companyId, Float score, Authentication authentication) { + Long companyId, Double score, Authentication authentication) { // 회원 조회 Member member = memberProvider.getMemberByAuthentication(authentication); // 기술블로그 조회 - SliceCustom techArticles = techArticleRepository.findTechArticlesByCursor( + SliceCustom techArticles = techArticleRepository.findTechArticlesByCursor( pageable, techArticleId, techArticleSort, companyId, keyword, score); // 데이터 가공 List techArticlesResponse = techArticles.stream() - .map(techArticle -> TechArticleMainResponse.of(techArticle, member)) + .map(techArticle -> TechArticleMainResponse.of(techArticle.getTechArticle(), member, techArticle.getScore())) .toList(); return new SliceCustom<>(techArticlesResponse, pageable, techArticles.hasNext(), techArticles.getTotalElements()); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleService.java index 475b91d1..91213745 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleService.java @@ -11,7 +11,7 @@ public interface TechArticleService { Slice getTechArticles(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, - String keyword, Long companyId, Float score, + String keyword, Long companyId, Double score, Authentication authentication); TechArticleDetailResponse getTechArticle(Long techArticleId, String anonymousMemberId, Authentication authentication); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleController.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleController.java index f7423c9d..d554af14 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleController.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleController.java @@ -37,7 +37,7 @@ public ResponseEntity>> getTechArti @RequestParam(required = false) Long techArticleId, @RequestParam(required = false) String keyword, @RequestParam(required = false) Long companyId, - @RequestParam(required = false) Float score + @RequestParam(required = false) Double score ) { TechArticleService techArticleService = techArticleServiceStrategy.getTechArticleService(); Authentication authentication = AuthenticationMemberUtils.getAuthentication(); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java index a4eb9d2b..104f9bcb 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java @@ -31,13 +31,13 @@ public class TechArticleMainResponse { public final Long popularScore; public final Boolean isLogoImage; public final Boolean isBookmarked; - public final Float score; + public final Double score; @Builder private TechArticleMainResponse(Long id, String title, String contents, String author, CompanyResponse company, LocalDate regDate, String thumbnailUrl, String techArticleUrl, Long viewTotalCount, Long recommendTotalCount, Long commentTotalCount, Long popularScore, - Boolean isLogoImage, Boolean isBookmarked, Float score) { + Boolean isLogoImage, Boolean isBookmarked, Double score) { this.id = id; this.title = title; this.contents = contents; @@ -96,7 +96,7 @@ public static TechArticleMainResponse of(TechArticle techArticle) { .build(); } - public static TechArticleMainResponse of(TechArticle techArticle, Member member, Float score) { + public static TechArticleMainResponse of(TechArticle techArticle, Member member, Double score) { CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); return TechArticleMainResponse.builder() .id(techArticle.getId()) @@ -117,7 +117,7 @@ public static TechArticleMainResponse of(TechArticle techArticle, Member member, .build(); } - public static TechArticleMainResponse of(TechArticle techArticle, Float score) { + public static TechArticleMainResponse of(TechArticle techArticle, Double score) { CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); return TechArticleMainResponse.builder() .id(techArticle.getId()) @@ -146,8 +146,8 @@ private static String getThumbnailUrl(Url thumbnailUrl, CompanyResponse companyR return thumbnailUrl.getUrl(); } - private static Float getValidScore(Float score) { - return Objects.isNull(score) || Float.isNaN(score) ? null : score; + private static Double getValidScore(Double score) { + return Objects.isNull(score) || Double.isNaN(score) ? null : score; } private static String truncateString(String contents, int maxLength) { diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/notification/NotificationControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/notification/NotificationControllerTest.java index c540a873..e3fb8a98 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/notification/NotificationControllerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/notification/NotificationControllerTest.java @@ -246,7 +246,7 @@ private TechArticleMainResponse createTechArticleMainResponse(Long id, String th String techArticleUrl, String title, String contents, Long companyId, String companyName, String careerUrl, String officialImageUrl, LocalDate regDate, String author, long recommendCount, - long commentCount, long viewCount, Boolean isBookmarked, Float score) { + long commentCount, long viewCount, Boolean isBookmarked, Double score) { return TechArticleMainResponse.builder() .id(id) .thumbnailUrl(thumbnailUrl) diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java index f7e0f6c7..a1f532a5 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java @@ -55,7 +55,7 @@ void getTechArticlesByAnonymous() throws Exception { TechArticleMainResponse response = createTechArticleMainResponse( 1L, "http://thumbnail.com", false, "http://article.com", "타이틀 1", "내용 1", 1L, "회사명", "http://career.com", "http://official.com", LocalDate.now(), "작성자", - 10L, 5L, 100L, null, 10.0f + 10L, 5L, 100L, null, 10.0 ); SliceCustom mockSlice = new SliceCustom<>( @@ -130,7 +130,7 @@ void getTechArticlesByMember() throws Exception { TechArticleMainResponse response = createTechArticleMainResponse( 1L, "http://thumbnail.com", false, "http://article.com", "타이틀 1", "내용 1", 1L, "회사명", "http://career.com", "http://official.com", LocalDate.now(), "작성자", - 10L, 5L, 100L, true, 10.0f + 10L, 5L, 100L, true, 10.0 ); SliceCustom mockSlice = new SliceCustom<>( @@ -512,7 +512,7 @@ private TechArticleMainResponse createTechArticleMainResponse(Long id, String th String techArticleUrl, String title, String contents, Long companyId, String companyName, String careerUrl, String officialImageUrl, LocalDate regDate, String author, long recommendCount, - long commentCount, long viewCount, Boolean isBookmarked, Float score) { + long commentCount, long viewCount, Boolean isBookmarked, Double score) { return TechArticleMainResponse.builder() .id(id) .thumbnailUrl(thumbnailUrl) diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/NotificationControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/NotificationControllerDocsTest.java index 76ad5b56..183d5e82 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/NotificationControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/NotificationControllerDocsTest.java @@ -510,7 +510,7 @@ private TechArticleMainResponse createTechArticleMainResponse(Long id, String th String techArticleUrl, String title, String contents, Long companyId, String companyName, String careerUrl, String officialImageUrl, LocalDate regDate, String author, long recommendCount, - long commentCount, long viewCount, Boolean isBookmarked, Float score) { + long commentCount, long viewCount, Boolean isBookmarked, Double score) { return TechArticleMainResponse.builder() .id(id) .thumbnailUrl(thumbnailUrl) diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java index 4a9776aa..dd806847 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java @@ -72,7 +72,7 @@ void getTechArticlesByMember() throws Exception { TechArticleMainResponse response = createTechArticleMainResponse( 1L, "http://thumbnail.com", false, "http://article.com", "타이틀 1", "내용 1", 1L, "회사명", "http://career.com", "http://official.com", LocalDate.now(), "작성자", - 10L, 5L, 100L, true, 10.0f + 10L, 5L, 100L, true, 10.0 ); SliceCustom mockSlice = new SliceCustom<>( @@ -451,7 +451,7 @@ private TechArticleMainResponse createTechArticleMainResponse(Long id, String th String techArticleUrl, String title, String contents, Long companyId, String companyName, String careerUrl, String officialImageUrl, LocalDate regDate, String author, long recommendCount, - long commentCount, long viewCount, Boolean isBookmarked, Float score) { + long commentCount, long viewCount, Boolean isBookmarked, Double score) { return TechArticleMainResponse.builder() .id(id) .thumbnailUrl(thumbnailUrl) From 17f7a85afc34c516a318f8a8918631cdf9f8e6a9 Mon Sep 17 00:00:00 2001 From: soyoung Date: Sun, 31 Aug 2025 15:42:43 +0900 Subject: [PATCH 65/66] =?UTF-8?q?fix(techArticle):=20=ED=82=A4=EC=9B=8C?= =?UTF-8?q?=EB=93=9C=20=EA=B2=80=EC=83=89=20score=20Double=20=EB=B2=94?= =?UTF-8?q?=EC=9C=84=20=EC=B4=88=EA=B3=BC=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../techArticle/custom/TechArticleRepositoryImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java index c3524eb4..b5e599a2 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java @@ -89,9 +89,9 @@ public SliceCustom findTechArticlesByCursorWithKeyword(Pageable techArticle.contents, keyword ); - // 전체 스코어 계산 (제목 가중치 2배) + // 전체 스코어 계산 (제목 가중치 2배, 안전한 범위로 제한) NumberTemplate totalScore = Expressions.numberTemplate(Double.class, - "({0} * 2.0) + {1}", titleScore, contentsScore + "(LEAST({0}, 100000) * 2.0) + LEAST({1}, 100000)", titleScore, contentsScore ); // TechArticle과 score를 함께 조회 From 2975e6c1b9950676d3095bc833a12da122b90496 Mon Sep 17 00:00:00 2001 From: soyoung Date: Sun, 31 Aug 2025 16:13:25 +0900 Subject: [PATCH 66/66] =?UTF-8?q?fix(techArticle):=20codex=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../techArticle/custom/TechArticleRepositoryImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java index b5e599a2..2b9439b4 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java @@ -204,7 +204,7 @@ private OrderSpecifier techArticleSort(TechArticleSort techArticleSort) { // 키워드 검색을 위한 커서 조건 생성 private Predicate getCursorConditionForKeywordSearch(TechArticleSort techArticleSort, Long techArticleId, Double score, NumberTemplate totalScore) { - if (ObjectUtils.isEmpty(techArticleId)) { + if (ObjectUtils.isEmpty(techArticleId) || ObjectUtils.isEmpty(score)) { return null; }