Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ dependencies {
// mybatis
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.4'

// markdown parser
implementation 'org.commonmark:commonmark:0.21.0'

compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
Expand Down
17 changes: 17 additions & 0 deletions src/docs/asciidoc/api/pick/pick-main-v2.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[[Pick-Main-V2]]
== 픽픽픽 메인 API(GET: /devdevdev/api/v2/picks)
* 픽픽픽 메인 화면을 조회할 수 있다.
* 회원/익명 사용자에 따라 API 응답값이 상이하다.

=== 정상 요청/응답
==== HTTP Request
include::{snippets}/pick-main-v2/http-request.adoc[]
==== HTTP Request Header Fields
include::{snippets}/pick-main-v2/request-headers.adoc[]
==== HTTP Request Query Parameters Fields
include::{snippets}/pick-main-v2/query-parameters.adoc[]

==== HTTP Response
include::{snippets}/pick-main-v2/http-response.adoc[]
==== HTTP Response Fields
include::{snippets}/pick-main-v2/response-fields.adoc[]
38 changes: 38 additions & 0 deletions src/docs/asciidoc/api/pick/pick-similarity-v2.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[[Pick-Similarties-V2]]
== 나도 고민 했는데 픽픽픽 API(GET: /devdevdev/api/v2/picks/{pickId}/similarties)

* 픽픽픽 상세와 유사도가 가장 높은 픽픽픽 3개를 조회한다.
* 유사도가 가장 높은 픽픽픽이 존재하지 않으면 빈 배열로 응답된다.
* 유사도 계산에 사용되는 embeddings 값이 없을 경우 503 예외를 발생한다.
** 외부 API(Open AI API) 를 사용하기 때문에 embeddings 값을 저장하지 못할 수 있다.
* 회원/익명 사용자 모두 조회할 수 있다.

=== 정상 요청/응답

==== HTTP Request

include::{snippets}/pick-similarity-v2/http-request.adoc[]

==== HTTP Request Header Fields

include::{snippets}/pick-similarity-v2/request-headers.adoc[]

==== HTTP Request Path Parameters Fields

include::{snippets}/pick-similarity-v2/path-parameters.adoc[]

==== HTTP Response

include::{snippets}/pick-similarity-v2/http-response.adoc[]

==== HTTP Response Fields

include::{snippets}/pick-similarity-v2/response-fields.adoc[]

=== 예외

==== HTTP Response

include::{snippets}/pick-similarity-not-found/response-body.adoc[]
include::{snippets}/pick-similarity-bad-request/response-body.adoc[]
include::{snippets}/pick-similarity-internal-server-exception/response-body.adoc[]
4 changes: 3 additions & 1 deletion src/docs/asciidoc/api/pick/pick.adoc
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
= 픽픽픽

include::pick-main.adoc[]
include::pick-main-v2.adoc[]
include::pick-detail.adoc[]
include::pick-register.adoc[]
include::pick-modify.adoc[]
include::pick-delete.adoc[]
include::pick-vote.adoc[]
include::pick-option-register-image.adoc[]
include::pick-option-delete-image.adoc[]
include::pick-similarity.adoc[]
include::pick-similarity.adoc[]
include::pick-similarity-v2.adoc[]
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ public interface PickRepository extends JpaRepository<Pick, Long>, PickRepositor
List<Pick> findTop1000ByContentStatusAndEmbeddingsIsNotNullOrderByCreatedAtDesc(ContentStatus contentStatus);

Long countByMember(Member member);

Long countByContentStatus(ContentStatus contentStatus);
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package com.dreamypatisiel.devdevdev.domain.service.pick;

import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember;
import com.dreamypatisiel.devdevdev.domain.entity.Pick;
import com.dreamypatisiel.devdevdev.domain.entity.enums.ContentStatus;
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.PickSort;
import com.dreamypatisiel.devdevdev.domain.repository.pick.*;
import com.dreamypatisiel.devdevdev.domain.service.member.AnonymousMemberService;
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.response.pick.PickDetailResponse;
import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainResponse;
import com.dreamypatisiel.devdevdev.web.dto.response.pick.SimilarPickResponse;
import com.dreamypatisiel.devdevdev.web.dto.SliceCustom;
import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainResponseV2;
import com.dreamypatisiel.devdevdev.web.dto.response.pick.SimilarPickResponseV2;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.security.core.Authentication;
Expand All @@ -22,30 +24,52 @@
@Service
@Transactional(readOnly = true)
public class GuestPickServiceV2 extends PickCommonService implements PickServiceV2 {
public GuestPickServiceV2(EmbeddingsService embeddingsService,

private final AnonymousMemberService anonymousMemberService;

public GuestPickServiceV2(PickRepository pickRepository, EmbeddingsService embeddingsService,
PickBestCommentsPolicy pickBestCommentsPolicy,
PickPopularScorePolicy pickPopularScorePolicy,
TimeProvider timeProvider,
PickRepository pickRepository,
PickCommentRepository pickCommentRepository,
PickCommentRecommendRepository pickCommentRecommendRepository) {
PickCommentRecommendRepository pickCommentRecommendRepository,
PickPopularScorePolicy pickPopularScorePolicy,
TimeProvider timeProvider, AnonymousMemberService anonymousMemberService) {
super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, timeProvider, pickRepository,
pickCommentRepository, pickCommentRecommendRepository);
this.anonymousMemberService = anonymousMemberService;
}

/**
* 픽픽픽 메인 조회 V2
*/
@Transactional
@Override
public Slice<PickMainResponse> findPicksMain(Pageable pageable, Long pickId, PickSort pickSort, String anonymousMemberId, Authentication authentication) {
return null;
}
public Slice<PickMainResponseV2> findPicksMain(Pageable pageable, Long pickId, PickSort pickSort,
String anonymousMemberId, Authentication authentication) {
// 익명 사용자 호출인지 확인
AuthenticationMemberUtils.validateAnonymousMethodCall(authentication);

@Override
public PickDetailResponse findPickDetail(Long pickId, String anonymousMemberId, Authentication authentication) {
return null;
// anonymousMemberId 검증
AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId);

// 픽픽픽 조회
Slice<Pick> picks = pickRepository.findPicksByCursor(pageable, pickId, pickSort);

// 데이터 가공
List<PickMainResponseV2> pickMainResponse = picks.stream()
.map(pick -> PickMainResponseV2.of(pick, anonymousMember))
.toList();

// 전체 픽픽픽 개수 조회
long totalElements = pickRepository.countByContentStatus(ContentStatus.APPROVAL);

return new SliceCustom<>(pickMainResponse, pageable, picks.hasNext(), totalElements);
}

/**
* 나도 고민했는데 픽픽픽 V2
*/
@Override
public List<SimilarPickResponse> findTop3SimilarPicks(Long pickId) {
return null;
public List<SimilarPickResponseV2> findTop3SimilarPicksV2(Long pickId) {
return super.findTop3SimilarPicksV2(pickId);
}

}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package com.dreamypatisiel.devdevdev.domain.service.pick;

import com.dreamypatisiel.devdevdev.domain.entity.Member;
import com.dreamypatisiel.devdevdev.domain.entity.Pick;
import com.dreamypatisiel.devdevdev.domain.entity.enums.ContentStatus;
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.PickSort;
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.response.pick.PickDetailResponse;
import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainResponse;
import com.dreamypatisiel.devdevdev.web.dto.response.pick.SimilarPickResponse;
import com.dreamypatisiel.devdevdev.web.dto.SliceCustom;
import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainResponseV2;
import com.dreamypatisiel.devdevdev.web.dto.response.pick.SimilarPickResponseV2;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.security.core.Authentication;
Expand All @@ -23,30 +27,48 @@
@Transactional(readOnly = true)
public class MemberPickServiceV2 extends PickCommonService implements PickServiceV2 {

public MemberPickServiceV2(EmbeddingsService embeddingsService,
PickBestCommentsPolicy pickBestCommentsPolicy,
PickPopularScorePolicy pickPopularScorePolicy,
TimeProvider timeProvider,
PickRepository pickRepository,
private final MemberProvider memberProvider;

public MemberPickServiceV2(EmbeddingsService embeddingsService, PickRepository pickRepository,
MemberProvider memberProvider,
PickCommentRepository pickCommentRepository,
PickCommentRecommendRepository pickCommentRecommendRepository) {
PickCommentRecommendRepository pickCommentRecommendRepository,
PickPopularScorePolicy pickPopularScorePolicy,
PickBestCommentsPolicy pickBestCommentsPolicy, TimeProvider timeProvider) {
super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, timeProvider, pickRepository,
pickCommentRepository, pickCommentRecommendRepository);
pickCommentRepository,
pickCommentRecommendRepository);
this.memberProvider = memberProvider;
}

/**
* 픽픽픽 메인 조회 V2
*/
@Override
public Slice<PickMainResponse> findPicksMain(Pageable pageable, Long pickId, PickSort pickSort, String anonymousMemberId, Authentication authentication) {
return null;
}
public Slice<PickMainResponseV2> findPicksMain(Pageable pageable, Long pickId, PickSort pickSort,
String anonymousMemberId, Authentication authentication) {
// 픽픽픽 조회
Slice<Pick> picks = pickRepository.findPicksByCursor(pageable, pickId, pickSort);

@Override
public PickDetailResponse findPickDetail(Long pickId, String anonymousMemberId, Authentication authentication) {
return null;
// 회원 조회
Member member = memberProvider.getMemberByAuthentication(authentication);

// 데이터 가공
List<PickMainResponseV2> pickMainResponse = picks.stream()
.map(pick -> PickMainResponseV2.of(pick, member))
.toList();

// 전체 픽픽픽 개수 조회
long totalElements = pickRepository.countByContentStatus(ContentStatus.APPROVAL);

return new SliceCustom<>(pickMainResponse, pageable, picks.hasNext(), totalElements);
}

/**
* 나도 고민했는데 픽픽픽 V2
*/
@Override
public List<SimilarPickResponse> findTop3SimilarPicks(Long pickId) {
return null;
public List<SimilarPickResponseV2> findTop3SimilarPicksV2(Long pickId) {
return super.findTop3SimilarPicksV2(pickId);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;

import com.dreamypatisiel.devdevdev.web.dto.response.pick.SimilarPickResponseV2;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
Expand Down Expand Up @@ -86,6 +88,33 @@ public List<SimilarPickResponse> findTop3SimilarPicks(Long pickId) {
.toList();
}

public List<SimilarPickResponseV2> findTop3SimilarPicksV2(Long pickId) {

// 픽픽픽 조회
Pick findPick = pickRepository.findById(pickId)
.orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_MESSAGE));

// 픽픽픽 게시글의 승인 상태가 아니면
if (!findPick.isTrueContentStatus(ContentStatus.APPROVAL)) {
throw new IllegalArgumentException(INVALID_NOT_APPROVAL_STATUS_PICK_MESSAGE);
}

// 임베딩 값이 없으면 500 예외 발생
if (ObjectUtils.isEmpty(findPick.getEmbeddings())) {
throw new InternalServerException();
}

// 유사도를 계산한 픽픽픽 조회
List<PickWithSimilarityDto> pickWithSimilarityDto = embeddingsService.getPicksWithSimilarityDtoExcludeTargetPick(
findPick);

return pickWithSimilarityDto.stream()
.map(SimilarPickResponseV2::from)
.sorted(Comparator.comparingDouble(SimilarPickResponseV2::getSimilarity).reversed()) // 내림차순
.limit(SIMILARITY_PICK_MAX_COUNT)
.toList();
}

// 픽픽픽 게시글의 승인 상태 검증
public static void validateIsApprovalPickContentStatus(Pick pick, String message,
@Nullable String messageArgs) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
package com.dreamypatisiel.devdevdev.domain.service.pick;

import com.dreamypatisiel.devdevdev.domain.repository.pick.PickSort;
import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickDetailResponse;
import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainResponse;
import com.dreamypatisiel.devdevdev.web.dto.response.pick.SimilarPickResponse;
import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickMainResponseV2;
import com.dreamypatisiel.devdevdev.web.dto.response.pick.SimilarPickResponseV2;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.security.core.Authentication;

import java.util.List;

public interface PickServiceV2 extends PickService {
Slice<PickMainResponse> findPicksMain(Pageable pageable, Long pickId, PickSort pickSort, String anonymousMemberId,
Authentication authentication);
Slice<PickMainResponseV2> findPicksMain(Pageable pageable, Long pickId, PickSort pickSort, String anonymousMemberId,
Authentication authentication);

PickDetailResponse findPickDetail(Long pickId, String anonymousMemberId, Authentication authentication);

List<SimilarPickResponse> findTop3SimilarPicks(Long pickId);
List<SimilarPickResponseV2> findTop3SimilarPicksV2(Long pickId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.dreamypatisiel.devdevdev.global.utils;

import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.text.TextContentRenderer;

/**
* 마크다운 처리를 위한 유틸리티 클래스
*/
public abstract class MarkdownUtils {

private static final Parser PARSER = Parser.builder().build();
private static final TextContentRenderer TEXT_RENDERER = TextContentRenderer.builder().build();

/**
* 마크다운 텍스트를 순수 텍스트로 변환
*/
public static String convertMarkdownToText(String markdown) {
if (markdown == null || markdown.isEmpty()) {
return markdown;
}

Node document = PARSER.parse(markdown);
return TEXT_RENDERER.render(document).trim();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.dreamypatisiel.devdevdev.global.utils;

import java.time.LocalDate;
import java.time.LocalDateTime;

/**
* 시간 관련 유틸리티 클래스
*/
public abstract class TimeUtils {

/**
* 주어진 날짜가 현재로부터 일주일 이내인지 확인합니다.
*
* @param createdAt 확인할 시간
* @return 날짜 기준 일주일 이내라면 true, 그렇지 않다면 false
*/
public static boolean isWithinOneWeek(LocalDateTime createdAt, LocalDateTime now) {
LocalDate createdDate = createdAt.toLocalDate();
LocalDate oneWeekAgo = now.toLocalDate().minusWeeks(1);
return !createdDate.isBefore(oneWeekAgo);
}
}
Loading