From 817185e9306c24851e21c545dde4ad377d1efcc8 Mon Sep 17 00:00:00 2001 From: soyoung Date: Wed, 5 Nov 2025 22:53:57 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(pick):=20=ED=94=BD=ED=94=BD=ED=94=BD?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20V2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/pick/GuestPickServiceV2.java | 61 +++++-- .../service/pick/MemberPickServiceV2.java | 39 +++++ .../domain/service/pick/PickServiceV2.java | 3 + .../web/controller/pick/PickControllerV2.java | 13 ++ .../response/pick/PickDetailResponseV2.java | 79 +++++++++ .../web/docs/PickControllerV2DocsTest.java | 161 ++++++++++++++++++ 6 files changed, 343 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/pick/PickDetailResponseV2.java diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickServiceV2.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickServiceV2.java index ab240a5b..2e79324e 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickServiceV2.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickServiceV2.java @@ -2,34 +2,35 @@ import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; import com.dreamypatisiel.devdevdev.domain.entity.Pick; +import com.dreamypatisiel.devdevdev.domain.entity.PickOption; 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.PickRepository; -import com.dreamypatisiel.devdevdev.domain.repository.pick.PickSearchDto; -import com.dreamypatisiel.devdevdev.domain.repository.pick.PickSort; +import com.dreamypatisiel.devdevdev.domain.repository.pick.*; import com.dreamypatisiel.devdevdev.domain.repository.pick.mybatis.PickMapper; import com.dreamypatisiel.devdevdev.domain.service.member.AnonymousMemberService; +import com.dreamypatisiel.devdevdev.exception.NotFoundException; import com.dreamypatisiel.devdevdev.global.common.TimeProvider; import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; import com.dreamypatisiel.devdevdev.openai.embeddings.EmbeddingsService; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; -import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainResponseV2; -import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainSearchResponseV2; -import com.dreamypatisiel.devdevdev.web.dto.response.pick.SimilarPickResponseV2; +import com.dreamypatisiel.devdevdev.web.dto.response.pick.*; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.security.core.Authentication; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; + +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_APPROVAL_STATUS_PICK_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_FOUND_PICK_MESSAGE; @Service @Transactional(readOnly = true) @@ -77,6 +78,40 @@ public Slice findPicksMain(Pageable pageable, Long pickId, P return new SliceCustom<>(pickMainResponse, pageable, picks.hasNext(), totalElements); } + /** + * 픽픽픽 상세 조회 V2 + */ + @Transactional + @Override + public PickDetailResponseV2 findPickDetail(Long pickId, String anonymousMemberId, Authentication authentication) { + + // 익명 사용자 호출인지 확인 + AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); + + // 익명 회원 조회 또는 생성 + AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + + // 픽픽픽 상세 조회(pickOption 페치조인) + Pick findPick = pickRepository.findPickWithPickOptionByPickId(pickId) + .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_MESSAGE)); + + // 픽픽픽 게시글의 승인 상태가 아니면 + if (!findPick.isTrueContentStatus(ContentStatus.APPROVAL)) { + throw new IllegalArgumentException(INVALID_NOT_APPROVAL_STATUS_PICK_MESSAGE); + } + + findPick.plusOneViewTotalCount(); // 조회수 증가 + findPick.changePopularScore(pickPopularScorePolicy); // 인기점수 계산 + + // 픽픽픽 옵션 가공 + Map pickDetailOptions = findPick.getPickOptions().stream() + .collect(Collectors.toMap(PickOption::getPickOptionType, + pickOption -> PickDetailOptionResponse.of(pickOption, findPick, anonymousMember))); + + // 픽픽픽 상세 + return PickDetailResponseV2.of(findPick, findPick.getMember(), anonymousMember, pickDetailOptions); + } + /** * 나도 고민했는데 픽픽픽 V2 */ diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickServiceV2.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickServiceV2.java index bab6fc90..18cf80b9 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickServiceV2.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickServiceV2.java @@ -2,7 +2,9 @@ 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.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; @@ -10,11 +12,14 @@ import com.dreamypatisiel.devdevdev.domain.repository.pick.PickRepository; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickSearchDto; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickSort; +import com.dreamypatisiel.devdevdev.exception.NotFoundException; import com.dreamypatisiel.devdevdev.domain.repository.pick.mybatis.PickMapper; 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.SliceCustom; +import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickDetailOptionResponse; +import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickDetailResponseV2; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainResponseV2; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainSearchResponseV2; import com.dreamypatisiel.devdevdev.web.dto.response.pick.SimilarPickResponseV2; @@ -30,6 +35,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_APPROVAL_STATUS_PICK_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_FOUND_PICK_MESSAGE; + @Service @Transactional(readOnly = true) public class MemberPickServiceV2 extends PickCommonService implements PickServiceV2 { @@ -72,6 +80,37 @@ public Slice findPicksMain(Pageable pageable, Long pickId, P return new SliceCustom<>(pickMainResponse, pageable, picks.hasNext(), totalElements); } + /** + * 픽픽픽 상세 조회 V2 + */ + @Transactional + @Override + public PickDetailResponseV2 findPickDetail(Long pickId, String anonymousMemberId, Authentication authentication) { + + // 회원 조회 + Member findMember = memberProvider.getMemberByAuthentication(authentication); + + // 픽픽픽 상세 조회(pickOption 페치조인) + Pick findPick = pickRepository.findPickWithPickOptionByPickId(pickId) + .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_MESSAGE)); + + // 픽픽픽 게시글의 승인 상태가 아니면 + if (!findPick.isTrueContentStatus(ContentStatus.APPROVAL)) { + throw new IllegalArgumentException(INVALID_NOT_APPROVAL_STATUS_PICK_MESSAGE); + } + + findPick.plusOneViewTotalCount(); // 조회수 증가 + findPick.changePopularScore(pickPopularScorePolicy); // 인기점수 계산 + + // 픽픽픽 옵션 가공 + Map pickDetailOptions = findPick.getPickOptions().stream() + .collect(Collectors.toMap(PickOption::getPickOptionType, + pickOption -> PickDetailOptionResponse.of(pickOption, findPick, findMember))); + + // 픽픽픽 상세 + return PickDetailResponseV2.of(findPick, findPick.getMember(), findMember, pickDetailOptions); + } + /** * 나도 고민했는데 픽픽픽 V2 */ diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickServiceV2.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickServiceV2.java index 49964a23..aa531fe8 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickServiceV2.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickServiceV2.java @@ -1,6 +1,7 @@ package com.dreamypatisiel.devdevdev.domain.service.pick; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickSort; +import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickDetailResponseV2; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainResponseV2; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainSearchResponseV2; import com.dreamypatisiel.devdevdev.web.dto.response.pick.SimilarPickResponseV2; @@ -13,6 +14,8 @@ public interface PickServiceV2 extends PickService { Slice findPicksMain(Pageable pageable, Long pickId, PickSort pickSort, String anonymousMemberId, Authentication authentication); + PickDetailResponseV2 findPickDetail(Long pickId, String anonymousMemberId, Authentication authentication); + List findTop3SimilarPicksV2(Long pickId); Slice findPickMainSearch(Pageable pageable, Long pickId, Double searchScore, String keyword, diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/pick/PickControllerV2.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/pick/PickControllerV2.java index e646878a..961a9fd9 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/pick/PickControllerV2.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/pick/PickControllerV2.java @@ -9,6 +9,7 @@ import com.dreamypatisiel.devdevdev.global.utils.HttpRequestUtils; import com.dreamypatisiel.devdevdev.web.controller.ApiVersion; import com.dreamypatisiel.devdevdev.web.dto.response.BasicResponse; +import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickDetailResponseV2; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainResponseV2; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainSearchResponseV2; import com.dreamypatisiel.devdevdev.web.dto.response.pick.SimilarPickResponseV2; @@ -54,6 +55,18 @@ public ResponseEntity>> getPicksMain( return ResponseEntity.ok(BasicResponse.success(response)); } + @Operation(summary = "픽픽픽 상세 조회 V2", description = "픽픽픽 상세 페이지를 조회합니다.") + @GetMapping("/picks/{pickId}") + public ResponseEntity> getPickDetail(@PathVariable Long pickId, + @RequestHeader(value = HEADER_ANONYMOUS_MEMBER_ID, required = false) String anonymousMemberId) { + Authentication authentication = AuthenticationMemberUtils.getAuthentication(); + + PickServiceV2 pickService = (PickServiceV2) pickServiceStrategy.getPickService(ApiVersion.V2); + PickDetailResponseV2 response = pickService.findPickDetail(pickId, anonymousMemberId, authentication); + + return ResponseEntity.ok(BasicResponse.success(response)); + } + @Operation(summary = "나도 고민했는데 픽픽픽 V2", description = "픽픽픽 상세와 유사한 성격의 픽픽픽 3개를 추천합니다.") @GetMapping("picks/{pickId}/similarties") public ResponseEntity> getSimilarPicks(@PathVariable Long pickId) { diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/pick/PickDetailResponseV2.java b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/pick/PickDetailResponseV2.java new file mode 100644 index 00000000..dc072dab --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/pick/PickDetailResponseV2.java @@ -0,0 +1,79 @@ +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.Pick; +import com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType; +import com.dreamypatisiel.devdevdev.web.dto.util.CommonResponseUtil; +import com.dreamypatisiel.devdevdev.web.dto.util.PickResponseUtils; +import com.dreamypatisiel.devdevdev.global.common.TimeProvider; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Shape; +import java.time.LocalDateTime; +import java.util.Map; +import lombok.Builder; +import lombok.Data; + +@Data +public class PickDetailResponseV2 { + private final String userId; + private final String nickname; + + @JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = TimeProvider.DEFAULT_ZONE_ID) + private final LocalDateTime pickCreatedAt; + + private final String pickTitle; + private final long voteTotalCount; + private final long commentTotalCount; + private final Boolean isAuthor; + private final Boolean isVoted; + private final Map pickOptions; + + @Builder + public PickDetailResponseV2(String userId, String nickname, LocalDateTime pickCreatedAt, String pickTitle, + long voteTotalCount, long commentTotalCount, boolean isAuthor, boolean isVoted, + Map pickOptions) { + this.userId = userId; + this.nickname = nickname; + this.pickCreatedAt = pickCreatedAt; + this.pickTitle = pickTitle; + this.voteTotalCount = voteTotalCount; + this.commentTotalCount = commentTotalCount; + this.isAuthor = isAuthor; + this.isVoted = isVoted; + this.pickOptions = pickOptions; + } + + // 회원 전용 + public static PickDetailResponseV2 of(Pick pick, Member pickMember, Member member, + Map pickDetailOptions) { + return PickDetailResponseV2.builder() + .userId(CommonResponseUtil.sliceAndMaskEmail(pickMember.getEmail().getEmail())) + .nickname(pickMember.getNickname().getNickname()) + .pickCreatedAt(pick.getCreatedAt()) + .pickTitle(pick.getTitle().getTitle()) + .voteTotalCount(pick.getVoteTotalCount().getCount()) + .commentTotalCount(pick.getCommentTotalCount().getCount()) + .isAuthor(pick.isEqualMember(member)) + .isVoted(PickResponseUtils.isVotedMember(pick, member)) + .pickOptions(pickDetailOptions) + .build(); + } + + // 익명 회원 전용 + public static PickDetailResponseV2 of(Pick pick, Member pickMember, AnonymousMember anonymousMember, + Map pickDetailOptions) { + return PickDetailResponseV2.builder() + .isAuthor(false) + .pickCreatedAt(pick.getCreatedAt()) + .nickname(pickMember.getNickname().getNickname()) + .userId(CommonResponseUtil.sliceAndMaskEmail(pickMember.getEmail().getEmail())) + .pickTitle(pick.getTitle().getTitle()) + .voteTotalCount(pick.getVoteTotalCount().getCount()) + .commentTotalCount(pick.getCommentTotalCount().getCount()) + .pickOptions(pickDetailOptions) + .isVoted(PickResponseUtils.isVotedAnonymousMember(pick, anonymousMember)) + .build(); + } +} + diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickControllerV2DocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickControllerV2DocsTest.java index 8c4adfb9..18afebc3 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickControllerV2DocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickControllerV2DocsTest.java @@ -28,11 +28,13 @@ import com.dreamypatisiel.devdevdev.domain.entity.embedded.Count; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Title; +import com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickSort; import com.dreamypatisiel.devdevdev.domain.service.pick.GuestPickServiceV2; import com.dreamypatisiel.devdevdev.domain.service.pick.MemberPickServiceV2; import com.dreamypatisiel.devdevdev.global.constant.SecurityConstant; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; +import com.dreamypatisiel.devdevdev.web.dto.response.pick.*; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainOptionResponseV2; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainResponseV2; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainSearchResponseV2; @@ -49,6 +51,30 @@ import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.test.web.servlet.ResultActions; + +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static com.dreamypatisiel.devdevdev.global.constant.SecurityConstant.AUTHORIZATION_HEADER; +import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.authenticationType; +import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.pickSortType; +import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.yearMonthDateTimeType; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; +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.operation.preprocess.Preprocessors.*; +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.request.RequestDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; class PickControllerV2DocsTest extends SupportControllerDocsTest { @@ -255,6 +281,141 @@ void getSimilarPicks() throws Exception { )); } + @Test + @DisplayName("회원이 픽픽픽 상세를 조회한다. V2") + void getPickDetail() throws Exception { + // given + Long pickId = 1L; + + // Mock 데이터 생성 + PickDetailOptionImageResponse image1 = PickDetailOptionImageResponse.builder() + .id(1L) + .imageUrl("http://image1.png") + .build(); + + PickDetailOptionImageResponse image2 = PickDetailOptionImageResponse.builder() + .id(2L) + .imageUrl("http://image2.png") + .build(); + + PickDetailOptionResponse firstOption = PickDetailOptionResponse.builder() + .id(1L) + .title("픽옵션1") + .isPicked(true) + .percent(new BigDecimal("60")) + .content("픽콘텐츠1") + .voteTotalCount(3L) + .pickDetailOptionImagesResponse(List.of(image1)) + .build(); + + PickDetailOptionResponse secondOption = PickDetailOptionResponse.builder() + .id(2L) + .title("픽옵션2") + .isPicked(false) + .percent(new BigDecimal("40")) + .content("픽콘텐츠2") + .voteTotalCount(2L) + .pickDetailOptionImagesResponse(List.of(image2)) + .build(); + + Map pickOptions = Map.of( + PickOptionType.firstPickOption, firstOption, + PickOptionType.secondPickOption, secondOption + ); + + PickDetailResponseV2 response = PickDetailResponseV2.builder() + .userId("dre***@gmail.com") + .nickname("꿈빛파티시엘") + .pickCreatedAt(LocalDateTime.of(2024, 1, 1, 12, 0, 0)) + .pickTitle("픽픽픽 제목") + .voteTotalCount(5L) + .commentTotalCount(10L) + .isAuthor(true) + .isVoted(true) + .pickOptions(pickOptions) + .build(); + + when(memberPickServiceV2.findPickDetail(anyLong(), any(), any(Authentication.class))) + .thenReturn(response); + + // when // then + ResultActions actions = mockMvc.perform(get("/devdevdev/api/v2/picks/{pickId}", pickId) + .contentType(MediaType.APPLICATION_JSON) + .header(AUTHORIZATION_HEADER, SecurityConstant.BEARER_PREFIX + accessToken) + .characterEncoding(StandardCharsets.UTF_8)) + .andDo(print()) + .andExpect(status().isOk()); + + // docs + actions.andDo(document("pick-detail-v2", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰"), + headerWithName("Anonymous-Member-Id").optional().description("익명 회원 아이디") + ), + pathParameters( + parameterWithName("pickId").description("픽픽픽 아이디") + ), + responseFields( + fieldWithPath("resultType").type(STRING).description("응답 결과"), + fieldWithPath("data").type(OBJECT).description("응답 데이터"), + + fieldWithPath("data.nickname").type(STRING).description("픽픽픽 작성자 닉네임"), + fieldWithPath("data.userId").type(STRING).description("픽픽픽 작성자 아이디"), + fieldWithPath("data.pickCreatedAt").type(STRING).description("픽픽픽 생성 일시") + .attributes(yearMonthDateTimeType()), + fieldWithPath("data.pickTitle").type(STRING).description("픽픽픽 제목"), + fieldWithPath("data.voteTotalCount").type(NUMBER).description("픽픽픽 전체 투표 수 (NEW)"), + fieldWithPath("data.commentTotalCount").type(NUMBER).description("픽픽픽 전체 댓글 수 (NEW)"), + fieldWithPath("data.isAuthor").type(BOOLEAN).description("현재 로그인한 회원이 픽픽픽 작성자 여부"), + fieldWithPath("data.isVoted").type(BOOLEAN).description("픽픽픽 투표 여부"), + + fieldWithPath("data.pickOptions").type(OBJECT).description("픽픽픽 옵션 객체"), + + fieldWithPath("data.pickOptions.firstPickOption").type(OBJECT).description("픽픽픽 첫번째 옵션 객체"), + fieldWithPath("data.pickOptions.firstPickOption.id").type(NUMBER) + .description("첫 번째 픽픽픽 옵션 아이디"), + fieldWithPath("data.pickOptions.firstPickOption.title").type(STRING) + .description("첫 번째 픽픽픽 옵션 제목"), + fieldWithPath("data.pickOptions.firstPickOption.isPicked").type(BOOLEAN) + .description("첫 번째 픽픽픽 옵션 투표 여부"), + fieldWithPath("data.pickOptions.firstPickOption.percent").type(NUMBER) + .description("첫 번째 픽픽픽 옵션 득표율(%)"), + fieldWithPath("data.pickOptions.firstPickOption.content").type(STRING) + .description("첫 번째 픽픽픽 옵션 내용"), + fieldWithPath("data.pickOptions.firstPickOption.voteTotalCount").type(NUMBER) + .description("첫 번째 픽픽픽 옵션 득표수"), + fieldWithPath("data.pickOptions.firstPickOption.pickDetailOptionImages").type(ARRAY) + .description("첫 번째 픽픽픽 옵션 이미지 배열"), + fieldWithPath("data.pickOptions.firstPickOption.pickDetailOptionImages.[].id").type(NUMBER) + .description("첫 번째 픽픽픽 옵션 이미지 아이디"), + fieldWithPath("data.pickOptions.firstPickOption.pickDetailOptionImages.[].imageUrl").type( + STRING).description("첫 번째 픽픽픽 옵션 이미지 url"), + + fieldWithPath("data.pickOptions.secondPickOption").type(OBJECT).description("픽픽픽 두번째 옵션 객체"), + fieldWithPath("data.pickOptions.secondPickOption.id").type(NUMBER) + .description("두 번째 픽픽픽 옵션 아이디"), + fieldWithPath("data.pickOptions.secondPickOption.title").type(STRING) + .description("두 번째 픽픽픽 옵션 제목"), + fieldWithPath("data.pickOptions.secondPickOption.isPicked").type(BOOLEAN) + .description("두 번째 픽픽픽 옵션 투표 여부"), + fieldWithPath("data.pickOptions.secondPickOption.percent").type(NUMBER) + .description("두 번째 픽픽픽 옵션 득표율(%)"), + fieldWithPath("data.pickOptions.secondPickOption.content").type(STRING) + .description("두 번째 픽픽픽 옵션 내용"), + fieldWithPath("data.pickOptions.secondPickOption.voteTotalCount").type(NUMBER) + .description("두 번째 픽픽픽 옵션 득표수"), + fieldWithPath("data.pickOptions.secondPickOption.pickDetailOptionImages").type(ARRAY) + .description("두 번째 픽픽픽 옵션 이미지 배열"), + fieldWithPath("data.pickOptions.secondPickOption.pickDetailOptionImages.[].id").type(NUMBER) + .description("두 번째 픽픽픽 옵션 이미지 아이디"), + fieldWithPath("data.pickOptions.secondPickOption.pickDetailOptionImages.[].imageUrl").type( + STRING).description("두 번째 픽픽픽 옵션 이미지 url") + ) + )); + } + @Test @DisplayName("회원이 픽픽픽 검색을 조회한다.") void searchPicksMain() throws Exception { From 051de8af431d479cb7a2470b0fa16f0f07befd50 Mon Sep 17 00:00:00 2001 From: soyoung Date: Wed, 5 Nov 2025 23:07:26 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat(pick):=20=ED=94=BD=ED=94=BD=ED=94=BD?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20V2=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PickDetailResponseV2 DTO 추가 (voteTotalCount, commentTotalCount 포함) - MemberPickServiceV2, GuestPickServiceV2에 findPickDetail 구현 - PickControllerV2에 상세 조회 엔드포인트 추가 - Service 및 Controller 테스트 추가 - API 문서 추가 --- .../asciidoc/api/pick/pick-detail-v2.adoc | 34 +++++ src/docs/asciidoc/api/pick/v1/pick.adoc | 1 + .../service/pick/GuestPickServiceV2Test.java | 110 ++++++++++++++++ .../service/pick/MemberPickServiceV2Test.java | 117 ++++++++++++++++++ 4 files changed, 262 insertions(+) create mode 100644 src/docs/asciidoc/api/pick/pick-detail-v2.adoc diff --git a/src/docs/asciidoc/api/pick/pick-detail-v2.adoc b/src/docs/asciidoc/api/pick/pick-detail-v2.adoc new file mode 100644 index 00000000..b6a68da6 --- /dev/null +++ b/src/docs/asciidoc/api/pick/pick-detail-v2.adoc @@ -0,0 +1,34 @@ +[[Pick-Detail-V2]] +== 픽픽픽 상세 API V2(GET: /devdevdev/api/v2/picks/{pickId}) + +* 픽픽픽 아이디로 상세 화면을 조회할 수 있다. +* 픽픽픽 게시글의 상태가 승인 상태(`APPROVAL`)가 아니면 조회할 수 없다. +* V1과 비교하여 응답에 **투표수(voteTotalCount)**와 **댓글수(commentTotalCount)** 필드가 추가되었다. + +=== 정상 요청/응답 + +==== HTTP Request + +include::{snippets}/pick-detail-v2/http-request.adoc[] + +==== HTTP Request Header Fields + +include::{snippets}/pick-detail-v2/request-headers.adoc[] + +==== HTTP Request Path Parameters Fields + +include::{snippets}/pick-detail-v2/path-parameters.adoc[] + +==== HTTP Response + +include::{snippets}/pick-detail-v2/http-response.adoc[] + +==== HTTP Response Fields + +include::{snippets}/pick-detail-v2/response-fields.adoc[] + +=== V1과의 차이점 + +* `data.voteTotalCount`: 픽픽픽 전체 투표 수 (NEW) +* `data.commentTotalCount`: 픽픽픽 전체 댓글 수 (NEW) + diff --git a/src/docs/asciidoc/api/pick/v1/pick.adoc b/src/docs/asciidoc/api/pick/v1/pick.adoc index 7820c8d0..ccc3e9e3 100644 --- a/src/docs/asciidoc/api/pick/v1/pick.adoc +++ b/src/docs/asciidoc/api/pick/v1/pick.adoc @@ -2,6 +2,7 @@ include::pick-main.adoc[] include::pick-detail.adoc[] +include::pick-detail-v2.adoc[] include::pick-register.adoc[] include::pick-modify.adoc[] include::pick-delete.adoc[] diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickServiceV2Test.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickServiceV2Test.java index c4c208ae..e3608f56 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickServiceV2Test.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickServiceV2Test.java @@ -13,12 +13,18 @@ import com.dreamypatisiel.devdevdev.domain.repository.pick.PickOptionRepository; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickRepository; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickSort; +import com.dreamypatisiel.devdevdev.domain.repository.pick.PickVoteRepository; import com.dreamypatisiel.devdevdev.domain.service.member.AnonymousMemberService; +import com.dreamypatisiel.devdevdev.exception.NotFoundException; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; import com.dreamypatisiel.devdevdev.openai.embeddings.EmbeddingsService; +import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickDetailOptionResponse; +import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickDetailResponseV2; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainResponseV2; +import com.dreamypatisiel.devdevdev.web.dto.util.CommonResponseUtil; +import jakarta.persistence.EntityManager; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -32,6 +38,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Map; import static com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType.firstPickOption; import static com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType.secondPickOption; @@ -39,6 +46,7 @@ import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_FOUND_PICK_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPickOptionImage; import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -57,7 +65,11 @@ class GuestPickServiceV2Test { @Autowired PickOptionImageRepository pickOptionImageRepository; @Autowired + PickVoteRepository pickVoteRepository; + @Autowired MemberRepository memberRepository; + @Autowired + EntityManager em; @MockBean AnonymousMemberService anonymousMemberService; @MockBean @@ -132,6 +144,104 @@ void findPicksMain() { ); } + @Test + @DisplayName("익명 회원이 픽픽픽 상세를 조회한다. V2") + void findPickDetail() { + // given + String anonymousMemberId = "GA1.1.276672604.1715872960"; + AnonymousMember anonymousMember = AnonymousMember.builder() + .anonymousMemberId(anonymousMemberId) + .build(); + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + when(anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId)).thenReturn(anonymousMember); + + // 회원 생성 + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + // 픽픽픽 생성 (투표수 2, 댓글수 7) + Pick pick = createPick(new Title("픽픽픽 제목"), new Count(0), new Count(7), new Count(2), new Count(0), member, + ContentStatus.APPROVAL); + pickRepository.save(pick); + + // 픽픽픽 옵션 생성 + PickOption firstPickOption = createPickOption(pick, new Title("픽픽픽 옵션1"), new PickOptionContents("픽픽픽 옵션1 내용"), + new Count(2), PickOptionType.firstPickOption); + PickOption secondPickOption = createPickOption(pick, new Title("픽픽픽 옵션2"), new PickOptionContents("픽픽픽 옵션2 내용"), + new Count(0), PickOptionType.secondPickOption); + pickOptionRepository.saveAll(List.of(firstPickOption, secondPickOption)); + + // 픽픽픽 옵션 이미지 생성 + PickOptionImage firstPickOptionImage = createPickOptionImage("이미지1", "http://image1.png", firstPickOption); + PickOptionImage secondPickOptionImage = createPickOptionImage("이미지2", "http://image2.png", secondPickOption); + pickOptionImageRepository.saveAll(List.of(firstPickOptionImage, secondPickOptionImage)); + + // 픽픽픽 옵션 투표 여부 + PickVote pickVote = createPickVote(anonymousMember, firstPickOption, pick); + pickVoteRepository.save(pickVote); + + em.flush(); + em.clear(); + + // when + PickDetailResponseV2 pickDetail = guestPickServiceV2.findPickDetail(pick.getId(), anonymousMemberId, authentication); + + // then + assertThat(pickDetail).isNotNull(); + assertAll( + () -> assertThat(pickDetail.getUserId()).isEqualTo( + CommonResponseUtil.sliceAndMaskEmail(member.getEmail().getEmail())), + () -> assertThat(pickDetail.getNickname()).isEqualTo(member.getNickname().getNickname()), + () -> assertThat(pickDetail.getPickTitle()).isEqualTo("픽픽픽 제목"), + () -> assertThat(pickDetail.getVoteTotalCount()).isEqualTo(2L), + () -> assertThat(pickDetail.getCommentTotalCount()).isEqualTo(7L), + () -> assertThat(pickDetail.getIsAuthor()).isEqualTo(false), + () -> assertThat(pickDetail.getIsVoted()).isEqualTo(true) + ); + + Map pickOptions = pickDetail.getPickOptions(); + PickDetailOptionResponse findFirstPickOptionResponse = pickOptions.get(PickOptionType.firstPickOption); + PickDetailOptionResponse findSecondPickOptionResponse = pickOptions.get(PickOptionType.secondPickOption); + + assertThat(findFirstPickOptionResponse).isNotNull(); + assertAll( + () -> assertThat(findFirstPickOptionResponse.getTitle()).isEqualTo("픽픽픽 옵션1"), + () -> assertThat(findFirstPickOptionResponse.getIsPicked()).isEqualTo(true), + () -> assertThat(findFirstPickOptionResponse.getPercent()).isEqualTo(100), + () -> assertThat(findFirstPickOptionResponse.getContent()).isEqualTo("픽픽픽 옵션1 내용"), + () -> assertThat(findFirstPickOptionResponse.getVoteTotalCount()).isEqualTo(2L) + ); + + assertThat(findSecondPickOptionResponse).isNotNull(); + assertAll( + () -> assertThat(findSecondPickOptionResponse.getTitle()).isEqualTo("픽픽픽 옵션2"), + () -> assertThat(findSecondPickOptionResponse.getIsPicked()).isEqualTo(false), + () -> assertThat(findSecondPickOptionResponse.getPercent()).isEqualTo(0), + () -> assertThat(findSecondPickOptionResponse.getContent()).isEqualTo("픽픽픽 옵션2 내용"), + () -> assertThat(findSecondPickOptionResponse.getVoteTotalCount()).isEqualTo(0L) + ); + } + + @Test + @DisplayName("익명 회원이 픽픽픽 상세 조회할 때 픽픽픽이 없으면 예외가 발생한다. V2") + void findPickDetailNotFoundPickDetail() { + // given + String anonymousMemberId = "GA1.1.276672604.1715872960"; + AnonymousMember anonymousMember = AnonymousMember.builder() + .anonymousMemberId(anonymousMemberId) + .build(); + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + when(anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId)).thenReturn(anonymousMember); + + // when // then + assertThatThrownBy(() -> guestPickServiceV2.findPickDetail(0L, anonymousMemberId, authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(INVALID_NOT_FOUND_PICK_MESSAGE); + } + private Pick createPick(Title title, Count viewTotalCount, Count commentTotalCount, Count voteTotalCount, Count poplarScore, Member member, ContentStatus contentStatus) { return Pick.builder() diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickServiceV2Test.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickServiceV2Test.java index d39a0f48..4690faac 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickServiceV2Test.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickServiceV2Test.java @@ -21,10 +21,16 @@ 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.exception.NotFoundException; import com.dreamypatisiel.devdevdev.global.common.MemberProvider; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; +import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; import com.dreamypatisiel.devdevdev.openai.embeddings.EmbeddingsService; +import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickDetailOptionResponse; +import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickDetailResponseV2; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainResponseV2; +import com.dreamypatisiel.devdevdev.web.dto.util.CommonResponseUtil; import jakarta.persistence.EntityManager; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -35,8 +41,19 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; +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; +import java.util.List; +import java.util.Map; + +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_FOUND_PICK_MESSAGE; +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; + @SpringBootTest @Transactional class MemberPickServiceV2Test { @@ -52,6 +69,8 @@ class MemberPickServiceV2Test { @Autowired PickOptionImageRepository pickOptionImageRepository; @Autowired + PickVoteRepository pickVoteRepository; + @Autowired PickPopularScorePolicy pickPopularScorePolicy; @Autowired EntityManager em; @@ -122,6 +141,104 @@ void findPicksMain() { ); } + @Test + @DisplayName("회원이 픽픽픽 상세를 조회한다. V2") + void findPickDetail() { + // 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(); + + // 픽픽픽 생성 (투표수 1, 댓글수 5) + Pick pick = createPick(new Title("픽픽픽 제목"), new Count(0), new Count(5), new Count(1), new Count(0), member, + ContentStatus.APPROVAL); + pickRepository.save(pick); + + // 픽픽픽 옵션 생성 + PickOption firstPickOption = createPickOption(pick, new Title("픽픽픽 옵션1"), new PickOptionContents("픽픽픽 옵션1 내용"), + new Count(1), PickOptionType.firstPickOption); + PickOption secondPickOption = createPickOption(pick, new Title("픽픽픽 옵션2"), new PickOptionContents("픽픽픽 옵션2 내용"), + new Count(0), PickOptionType.secondPickOption); + pickOptionRepository.saveAll(List.of(firstPickOption, secondPickOption)); + + // 픽픽픽 옵션 이미지 생성 + PickOptionImage firstPickOptionImage = createPickOptionImage("이미지1", "http://image1.png", firstPickOption); + PickOptionImage secondPickOptionImage = createPickOptionImage("이미지2", "http://image2.png", secondPickOption); + pickOptionImageRepository.saveAll(List.of(firstPickOptionImage, secondPickOptionImage)); + + // 픽픽픽 옵션 투표 여부 + PickVote pickVote = createPickVote(member, firstPickOption, pick); + pickVoteRepository.save(pickVote); + + em.flush(); + em.clear(); + + // when + PickDetailResponseV2 pickDetail = memberPickServiceV2.findPickDetail(pick.getId(), null, authentication); + + // then + assertThat(pickDetail).isNotNull(); + assertAll( + () -> assertThat(pickDetail.getUserId()).isEqualTo( + CommonResponseUtil.sliceAndMaskEmail(member.getEmail().getEmail())), + () -> assertThat(pickDetail.getNickname()).isEqualTo(member.getNickname().getNickname()), + () -> assertThat(pickDetail.getPickTitle()).isEqualTo("픽픽픽 제목"), + () -> assertThat(pickDetail.getVoteTotalCount()).isEqualTo(1L), + () -> assertThat(pickDetail.getCommentTotalCount()).isEqualTo(5L), + () -> assertThat(pickDetail.getIsAuthor()).isEqualTo(true), + () -> assertThat(pickDetail.getIsVoted()).isEqualTo(true) + ); + + Map pickOptions = pickDetail.getPickOptions(); + PickDetailOptionResponse findFirstPickOptionResponse = pickOptions.get(PickOptionType.firstPickOption); + PickDetailOptionResponse findSecondPickOptionResponse = pickOptions.get(PickOptionType.secondPickOption); + + assertThat(findFirstPickOptionResponse).isNotNull(); + assertAll( + () -> assertThat(findFirstPickOptionResponse.getTitle()).isEqualTo("픽픽픽 옵션1"), + () -> assertThat(findFirstPickOptionResponse.getIsPicked()).isEqualTo(true), + () -> assertThat(findFirstPickOptionResponse.getPercent()).isEqualTo(100), + () -> assertThat(findFirstPickOptionResponse.getContent()).isEqualTo("픽픽픽 옵션1 내용"), + () -> assertThat(findFirstPickOptionResponse.getVoteTotalCount()).isEqualTo(1L) + ); + + assertThat(findSecondPickOptionResponse).isNotNull(); + assertAll( + () -> assertThat(findSecondPickOptionResponse.getTitle()).isEqualTo("픽픽픽 옵션2"), + () -> assertThat(findSecondPickOptionResponse.getIsPicked()).isEqualTo(false), + () -> assertThat(findSecondPickOptionResponse.getPercent()).isEqualTo(0), + () -> assertThat(findSecondPickOptionResponse.getContent()).isEqualTo("픽픽픽 옵션2 내용"), + () -> assertThat(findSecondPickOptionResponse.getVoteTotalCount()).isEqualTo(0L) + ); + } + + @Test + @DisplayName("회원이 픽픽픽 상세 조회할 때 픽픽픽이 없으면 예외가 발생한다. V2") + void findPickDetailNotFoundPickDetail() { + // 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(() -> memberPickServiceV2.findPickDetail(0L, null, authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(INVALID_NOT_FOUND_PICK_MESSAGE); + } + private Pick createPick(Title title, Count viewTotalCount, Count commentTotalCount, Count voteTotalCount, Count poplarScore, Member member, ContentStatus contentStatus) { return Pick.builder() From 58d41c34f485ca279c85e1a4df89c7f6e9015955 Mon Sep 17 00:00:00 2001 From: soyoung Date: Thu, 6 Nov 2025 00:07:08 +0900 Subject: [PATCH 3/4] =?UTF-8?q?test(pick):=20=ED=94=BD=ED=94=BD=ED=94=BD?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20V2=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/pick/GuestPickServiceV2Test.java | 74 +++++++++++-------- .../service/pick/MemberPickServiceV2Test.java | 36 ++++----- 2 files changed, 59 insertions(+), 51 deletions(-) diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickServiceV2Test.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickServiceV2Test.java index e3608f56..1d209fca 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickServiceV2Test.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickServiceV2Test.java @@ -8,20 +8,19 @@ 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.repository.member.AnonymousMemberRepository; import com.dreamypatisiel.devdevdev.domain.repository.member.MemberRepository; 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.PickSort; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickVoteRepository; -import com.dreamypatisiel.devdevdev.domain.service.member.AnonymousMemberService; import com.dreamypatisiel.devdevdev.exception.NotFoundException; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; import com.dreamypatisiel.devdevdev.openai.embeddings.EmbeddingsService; +import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickDetailOptionImageResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickDetailOptionResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickDetailResponseV2; -import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainResponseV2; import com.dreamypatisiel.devdevdev.web.dto.util.CommonResponseUtil; import jakarta.persistence.EntityManager; @@ -33,22 +32,15 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; -import org.springframework.data.domain.SliceImpl; import org.springframework.security.core.Authentication; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Map; -import static com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType.firstPickOption; -import static com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType.secondPickOption; -import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_APPROVAL_STATUS_PICK_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_FOUND_PICK_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPickOptionImage; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertAll; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -69,10 +61,10 @@ class GuestPickServiceV2Test { @Autowired MemberRepository memberRepository; @Autowired + AnonymousMemberRepository anonymousMemberRepository; + @Autowired EntityManager em; @MockBean - AnonymousMemberService anonymousMemberService; - @MockBean EmbeddingsService embeddingsService; String userId = "dreamy5patisiel"; @@ -145,37 +137,39 @@ void findPicksMain() { } @Test - @DisplayName("익명 회원이 픽픽픽 상세를 조회한다. V2") + @DisplayName("익명 회원이 픽픽픽 상세를 조회한다.") void findPickDetail() { // given + // 익명 회원 생성 + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + String anonymousMemberId = "GA1.1.276672604.1715872960"; AnonymousMember anonymousMember = AnonymousMember.builder() .anonymousMemberId(anonymousMemberId) .build(); - Authentication authentication = mock(Authentication.class); - when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); - when(anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId)).thenReturn(anonymousMember); + anonymousMemberRepository.save(anonymousMember); - // 회원 생성 + // 픽픽픽 작성 회원 생성 SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); Member member = Member.createMemberBy(socialMemberDto); memberRepository.save(member); - // 픽픽픽 생성 (투표수 2, 댓글수 7) - Pick pick = createPick(new Title("픽픽픽 제목"), new Count(0), new Count(7), new Count(2), new Count(0), member, + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 제목"), new Count(0), new Count(5), new Count(1), new Count(0), member, ContentStatus.APPROVAL); pickRepository.save(pick); // 픽픽픽 옵션 생성 PickOption firstPickOption = createPickOption(pick, new Title("픽픽픽 옵션1"), new PickOptionContents("픽픽픽 옵션1 내용"), - new Count(2), PickOptionType.firstPickOption); + new Count(1), PickOptionType.firstPickOption); PickOption secondPickOption = createPickOption(pick, new Title("픽픽픽 옵션2"), new PickOptionContents("픽픽픽 옵션2 내용"), new Count(0), PickOptionType.secondPickOption); pickOptionRepository.saveAll(List.of(firstPickOption, secondPickOption)); // 픽픽픽 옵션 이미지 생성 - PickOptionImage firstPickOptionImage = createPickOptionImage("이미지1", "http://image1.png", firstPickOption); - PickOptionImage secondPickOptionImage = createPickOptionImage("이미지2", "http://image2.png", secondPickOption); + PickOptionImage firstPickOptionImage = createPickOptionImage("이미지1", "http://iamge1.png", firstPickOption); + PickOptionImage secondPickOptionImage = createPickOptionImage("이미지2", "http://iamge2.png", secondPickOption); pickOptionImageRepository.saveAll(List.of(firstPickOptionImage, secondPickOptionImage)); // 픽픽픽 옵션 투표 여부 @@ -186,7 +180,8 @@ void findPickDetail() { em.clear(); // when - PickDetailResponseV2 pickDetail = guestPickServiceV2.findPickDetail(pick.getId(), anonymousMemberId, authentication); + PickDetailResponseV2 pickDetail = guestPickServiceV2.findPickDetail(pick.getId(), anonymousMemberId, + authentication); // then assertThat(pickDetail).isNotNull(); @@ -195,35 +190,55 @@ void findPickDetail() { CommonResponseUtil.sliceAndMaskEmail(member.getEmail().getEmail())), () -> assertThat(pickDetail.getNickname()).isEqualTo(member.getNickname().getNickname()), () -> assertThat(pickDetail.getPickTitle()).isEqualTo("픽픽픽 제목"), - () -> assertThat(pickDetail.getVoteTotalCount()).isEqualTo(2L), - () -> assertThat(pickDetail.getCommentTotalCount()).isEqualTo(7L), - () -> assertThat(pickDetail.getIsAuthor()).isEqualTo(false), - () -> assertThat(pickDetail.getIsVoted()).isEqualTo(true) + () -> assertThat(pickDetail.getCommentTotalCount()).isEqualTo(5), + () -> assertThat(pickDetail.getVoteTotalCount()).isEqualTo(1), + () -> assertThat(pickDetail.getIsAuthor()).isEqualTo(false) ); Map pickOptions = pickDetail.getPickOptions(); PickDetailOptionResponse findFirstPickOptionResponse = pickOptions.get(PickOptionType.firstPickOption); PickDetailOptionResponse findSecondPickOptionResponse = pickOptions.get(PickOptionType.secondPickOption); + PickOption findFirstPickOption = pickOptionRepository.findById(findFirstPickOptionResponse.getId()).get(); assertThat(findFirstPickOptionResponse).isNotNull(); assertAll( + () -> assertThat(findFirstPickOptionResponse.getId()).isEqualTo(findFirstPickOption.getId()), () -> assertThat(findFirstPickOptionResponse.getTitle()).isEqualTo("픽픽픽 옵션1"), () -> assertThat(findFirstPickOptionResponse.getIsPicked()).isEqualTo(true), () -> assertThat(findFirstPickOptionResponse.getPercent()).isEqualTo(100), () -> assertThat(findFirstPickOptionResponse.getContent()).isEqualTo("픽픽픽 옵션1 내용"), - () -> assertThat(findFirstPickOptionResponse.getVoteTotalCount()).isEqualTo(2L) + () -> assertThat(findFirstPickOptionResponse.getVoteTotalCount()).isEqualTo(1) ); + List findFirstPickOptionPickOptionImagesResponse = findFirstPickOptionResponse.getPickDetailOptionImages(); + PickOptionImage findFirstPickOptionImage = findFirstPickOption.getPickOptionImages().get(0); + assertThat(findFirstPickOptionPickOptionImagesResponse).hasSize(1) + .extracting("id", "imageUrl") + .containsExactly( + tuple(findFirstPickOptionImage.getId(), "http://iamge1.png") + ); + + PickOption findfirstPickOption = pickOptionRepository.findById(findSecondPickOptionResponse.getId()).get(); assertThat(findSecondPickOptionResponse).isNotNull(); assertAll( + () -> assertThat(findSecondPickOptionResponse.getId()).isEqualTo(findfirstPickOption.getId()), () -> assertThat(findSecondPickOptionResponse.getTitle()).isEqualTo("픽픽픽 옵션2"), () -> assertThat(findSecondPickOptionResponse.getIsPicked()).isEqualTo(false), () -> assertThat(findSecondPickOptionResponse.getPercent()).isEqualTo(0), () -> assertThat(findSecondPickOptionResponse.getContent()).isEqualTo("픽픽픽 옵션2 내용"), - () -> assertThat(findSecondPickOptionResponse.getVoteTotalCount()).isEqualTo(0L) + () -> assertThat(findSecondPickOptionResponse.getVoteTotalCount()).isEqualTo(0) ); + + List findfirstPickOptionPickOptionImagesResponse = findSecondPickOptionResponse.getPickDetailOptionImages(); + PickOptionImage findsecondPickOptionImage = secondPickOption.getPickOptionImages().get(0); + assertThat(findfirstPickOptionPickOptionImagesResponse).hasSize(1) + .extracting("id", "imageUrl") + .containsExactly( + tuple(findsecondPickOptionImage.getId(), "http://iamge2.png") + ); } + @Test @DisplayName("익명 회원이 픽픽픽 상세 조회할 때 픽픽픽이 없으면 예외가 발생한다. V2") void findPickDetailNotFoundPickDetail() { @@ -234,7 +249,6 @@ void findPickDetailNotFoundPickDetail() { .build(); Authentication authentication = mock(Authentication.class); when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); - when(anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId)).thenReturn(anonymousMember); // when // then assertThatThrownBy(() -> guestPickServiceV2.findPickDetail(0L, anonymousMemberId, authentication)) diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickServiceV2Test.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickServiceV2Test.java index 4690faac..86624eba 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickServiceV2Test.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickServiceV2Test.java @@ -1,14 +1,6 @@ package com.dreamypatisiel.devdevdev.domain.service.pick; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.tuple; - -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.PickOption; -import com.dreamypatisiel.devdevdev.domain.entity.PickOptionImage; -import com.dreamypatisiel.devdevdev.domain.entity.PickVote; +import com.dreamypatisiel.devdevdev.domain.entity.*; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Count; import com.dreamypatisiel.devdevdev.domain.entity.embedded.PickOptionContents; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Title; @@ -23,21 +15,17 @@ import com.dreamypatisiel.devdevdev.domain.repository.pick.PickRepository; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickVoteRepository; import com.dreamypatisiel.devdevdev.exception.NotFoundException; -import com.dreamypatisiel.devdevdev.global.common.MemberProvider; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; -import com.dreamypatisiel.devdevdev.openai.embeddings.EmbeddingsService; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickDetailOptionResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickDetailResponseV2; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainResponseV2; import com.dreamypatisiel.devdevdev.web.dto.util.CommonResponseUtil; import jakarta.persistence.EntityManager; -import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -74,10 +62,6 @@ class MemberPickServiceV2Test { PickPopularScorePolicy pickPopularScorePolicy; @Autowired EntityManager em; - @MockBean - MemberProvider memberProvider; - @MockBean - EmbeddingsService embeddingsService; String userId = "dreamy5patisiel"; String name = "꿈빛파티시엘"; @@ -96,6 +80,12 @@ void findPicksMain() { 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(); + // 픽픽픽 생성 Pick pick = createPick(new Title("픽픽픽 제목"), new Count(0), new Count(0), new Count(1), new Count(0), member, ContentStatus.APPROVAL); @@ -116,7 +106,8 @@ void findPicksMain() { Pageable pageable = PageRequest.of(0, 10); // when - Slice picksMain = memberPickServiceV2.findPicksMain(pageable, null, null, null, null); + Slice picksMain = memberPickServiceV2.findPicksMain(pageable, null, null, + null, authentication); // then Pick findPick = pickRepository.findById(pick.getId()).get(); @@ -178,7 +169,6 @@ void findPickDetail() { pickVoteRepository.save(pickVote); em.flush(); - em.clear(); // when PickDetailResponseV2 pickDetail = memberPickServiceV2.findPickDetail(pick.getId(), null, authentication); @@ -200,22 +190,26 @@ void findPickDetail() { PickDetailOptionResponse findFirstPickOptionResponse = pickOptions.get(PickOptionType.firstPickOption); PickDetailOptionResponse findSecondPickOptionResponse = pickOptions.get(PickOptionType.secondPickOption); + PickOption findFirstPickOption = pickOptionRepository.findById(findFirstPickOptionResponse.getId()).get(); assertThat(findFirstPickOptionResponse).isNotNull(); assertAll( () -> assertThat(findFirstPickOptionResponse.getTitle()).isEqualTo("픽픽픽 옵션1"), () -> assertThat(findFirstPickOptionResponse.getIsPicked()).isEqualTo(true), () -> assertThat(findFirstPickOptionResponse.getPercent()).isEqualTo(100), () -> assertThat(findFirstPickOptionResponse.getContent()).isEqualTo("픽픽픽 옵션1 내용"), - () -> assertThat(findFirstPickOptionResponse.getVoteTotalCount()).isEqualTo(1L) + () -> assertThat(findFirstPickOptionResponse.getVoteTotalCount()).isEqualTo(1L), + () -> assertThat(findFirstPickOption.getPickOptionImages()).hasSize(1) ); + PickOption findSecondPickOption = pickOptionRepository.findById(findSecondPickOptionResponse.getId()).get(); assertThat(findSecondPickOptionResponse).isNotNull(); assertAll( () -> assertThat(findSecondPickOptionResponse.getTitle()).isEqualTo("픽픽픽 옵션2"), () -> assertThat(findSecondPickOptionResponse.getIsPicked()).isEqualTo(false), () -> assertThat(findSecondPickOptionResponse.getPercent()).isEqualTo(0), () -> assertThat(findSecondPickOptionResponse.getContent()).isEqualTo("픽픽픽 옵션2 내용"), - () -> assertThat(findSecondPickOptionResponse.getVoteTotalCount()).isEqualTo(0L) + () -> assertThat(findSecondPickOptionResponse.getVoteTotalCount()).isEqualTo(0L), + () -> assertThat(findSecondPickOption.getPickOptionImages()).hasSize(1) ); } From 568d19274d5be83014a070541d1ab60d0eabc5f9 Mon Sep 17 00:00:00 2001 From: daisy Date: Thu, 6 Nov 2025 00:12:22 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix(pick):=20=EC=83=81=EC=84=B8=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20V2=20=EB=AC=B8=EC=84=9C=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/api/pick/v1/pick.adoc | 1 - src/docs/asciidoc/api/pick/{ => v2}/pick-detail-v2.adoc | 0 src/docs/asciidoc/api/pick/v2/pick.adoc | 1 + 3 files changed, 1 insertion(+), 1 deletion(-) rename src/docs/asciidoc/api/pick/{ => v2}/pick-detail-v2.adoc (100%) diff --git a/src/docs/asciidoc/api/pick/v1/pick.adoc b/src/docs/asciidoc/api/pick/v1/pick.adoc index ccc3e9e3..7820c8d0 100644 --- a/src/docs/asciidoc/api/pick/v1/pick.adoc +++ b/src/docs/asciidoc/api/pick/v1/pick.adoc @@ -2,7 +2,6 @@ include::pick-main.adoc[] include::pick-detail.adoc[] -include::pick-detail-v2.adoc[] include::pick-register.adoc[] include::pick-modify.adoc[] include::pick-delete.adoc[] diff --git a/src/docs/asciidoc/api/pick/pick-detail-v2.adoc b/src/docs/asciidoc/api/pick/v2/pick-detail-v2.adoc similarity index 100% rename from src/docs/asciidoc/api/pick/pick-detail-v2.adoc rename to src/docs/asciidoc/api/pick/v2/pick-detail-v2.adoc diff --git a/src/docs/asciidoc/api/pick/v2/pick.adoc b/src/docs/asciidoc/api/pick/v2/pick.adoc index a8925e6a..cbacae15 100644 --- a/src/docs/asciidoc/api/pick/v2/pick.adoc +++ b/src/docs/asciidoc/api/pick/v2/pick.adoc @@ -1,5 +1,6 @@ = 픽픽픽 V2 include::pick-main-v2.adoc[] +include::pick-detail-v2.adoc[] include::pick-search-v2.adoc[] include::pick-similarity-v2.adoc[] \ No newline at end of file