From 475d5995bb6b85b712b159e1827cb460ab4676b2 Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sun, 29 Jun 2025 17:07:55 +0900 Subject: [PATCH 01/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] 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/55] =?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/55] =?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/55] =?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/55] =?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/55] 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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] 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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] 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/55] 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/55] 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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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/55] =?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 cc3726e6b8e20769661d95cac27cb1223846a759 Mon Sep 17 00:00:00 2001 From: howisitgoing Date: Sun, 24 Aug 2025 14:45:52 +0900 Subject: [PATCH 55/55] =?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("총 데이터 갯수") ) )); }