Skip to content

Commit

Permalink
Merge pull request #39 from olmangjolmang/OMJM-78-home
Browse files Browse the repository at this point in the history
홈 화면 정보 제공
  • Loading branch information
Amepistheo authored Jul 20, 2024
2 parents fe2141c + 9df9fea commit c1c1adc
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.ticle.server.post.dto;
package com.ticle.server.global.config;


import lombok.RequiredArgsConstructor;
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/ticle/server/home/controller/HomeController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.ticle.server.global.dto.ResponseTemplate;
import com.ticle.server.home.dto.request.SubscriptionRequest;
import com.ticle.server.home.dto.response.HomeResponse;
import com.ticle.server.home.service.HomeService;
import com.ticle.server.user.service.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -15,6 +16,9 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

import static com.ticle.server.global.dto.ResponseTemplate.EMPTY_RESPONSE;

Expand All @@ -38,4 +42,15 @@ public ResponseEntity<ResponseTemplate<Object>> uploadSubscription(
.status(HttpStatus.OK)
.body(EMPTY_RESPONSE);
}

@Operation(summary = "홈화면 정보 가져오기", description = "TOP 3와 3개의 소주제를 가져옵니다. (각각 3개의 아티클 포함)")
@GetMapping("")
public ResponseEntity<ResponseTemplate<Object>> getTopicsAndPosts() {

List<HomeResponse> responseList = homeService.generateHomeInfo();

return ResponseEntity
.status(HttpStatus.OK)
.body(ResponseTemplate.from(responseList));
}
}

This file was deleted.

18 changes: 18 additions & 0 deletions src/main/java/com/ticle/server/home/dto/response/HomeResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ticle.server.home.dto.response;

import lombok.Builder;

import java.util.List;

@Builder
public record HomeResponse(
String topic,
List<PostSetsResponse> responseList
) {
public static HomeResponse of(String topic, List<PostSetsResponse> responseList) {
return HomeResponse.builder()
.topic(topic)
.responseList(responseList)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.ticle.server.home.dto.response;

import com.ticle.server.post.domain.Post;
import com.ticle.server.user.domain.type.Category;

import java.time.LocalDate;

public record PostSetsResponse(
String title,
String imageUrl,
Category category,
String author,
LocalDate createdDate
) {
public static PostSetsResponse from(Post post) {
return new PostSetsResponse(
post.getTitle(),
post.getImage().getImageUrl(),
post.getCategory(),
post.getAuthor(),
post.getCreatedDate()
);
}
}
32 changes: 32 additions & 0 deletions src/main/java/com/ticle/server/home/service/HomeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

import com.ticle.server.home.domain.Subscription;
import com.ticle.server.home.dto.request.SubscriptionRequest;
import com.ticle.server.home.dto.response.HomeResponse;
import com.ticle.server.home.dto.response.PostSetsResponse;
import com.ticle.server.home.repository.SubscriptionRepository;
import com.ticle.server.post.repository.PostRepository;
import com.ticle.server.user.domain.User;
import com.ticle.server.user.exception.UserNotFoundException;
import com.ticle.server.user.repository.UserRepository;
Expand All @@ -12,6 +15,9 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

import static com.ticle.server.user.exception.errorcode.UserErrorCode.USER_NOT_FOUND;

@Slf4j
Expand All @@ -22,6 +28,8 @@ public class HomeService {

private final UserRepository userRepository;
private final SubscriptionRepository subscriptionRepository;
private final PostRepository postRepository;
private final PostTopicCache postTopicCache;

@Transactional
public void uploadSubscription(SubscriptionRequest request, CustomUserDetails userDetails) {
Expand All @@ -31,4 +39,28 @@ public void uploadSubscription(SubscriptionRequest request, CustomUserDetails us
Subscription subscription = request.toSubscription(user);
subscriptionRepository.save(subscription);
}

public List<HomeResponse> generateHomeInfo() {
List<HomeResponse> responseList = new ArrayList<>();

// 이번주 TOP 3
responseList.add(findTop3Posts());
// 랜덤 3개의 주제와 포스트
responseList.addAll(find3RandomTopicAndPosts());

return responseList;
}

private HomeResponse findTop3Posts() {
List<PostSetsResponse> topPosts = postRepository.findTop3ByOrderByScrapCountDesc();
return HomeResponse.of("이번주 TOP 3", topPosts);
}


private List<HomeResponse> find3RandomTopicAndPosts() {
return postTopicCache.getRandomPosts(3)
.entrySet().stream()
.map(entry -> HomeResponse.of(entry.getKey(), entry.getValue()))
.toList();
}
}
44 changes: 44 additions & 0 deletions src/main/java/com/ticle/server/home/service/PostTopicCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.ticle.server.home.service;

import com.ticle.server.home.dto.response.PostSetsResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@RequiredArgsConstructor
@Component
public class PostTopicCache {

private static final Map<String, List<PostSetsResponse>> postTopicCache = new ConcurrentHashMap<>();

public void saveToCache(String commonTitle, List<PostSetsResponse> posts) {
postTopicCache.put(commonTitle, posts);
}

public List<PostSetsResponse> getPostsFromCache(String commonTitle) {
return postTopicCache.get(commonTitle);
}

public void clearCache() {
postTopicCache.clear();
}

public Map<String, List<PostSetsResponse>> getRandomPosts(int count) {
List<Map.Entry<String, List<PostSetsResponse>>> allEntries = new ArrayList<>(postTopicCache.entrySet());
Collections.shuffle(allEntries);

Map<String, List<PostSetsResponse>> result = new HashMap<>();

for (int i = 0; i < count; i++) {
Map.Entry<String, List<PostSetsResponse>> entry = allEntries.get(i);

int resultSize = Math.min(count, entry.getValue().size());

result.put(entry.getKey(), new ArrayList<>(entry.getValue().subList(0, resultSize)));
}

return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,51 @@

import com.ticle.server.home.domain.Subscription;
import com.ticle.server.home.domain.type.Day;
import com.ticle.server.home.dto.response.PostSetsResponse;
import com.ticle.server.home.repository.SubscriptionRepository;
import com.ticle.server.post.domain.Post;
import com.ticle.server.post.dto.GeminiRequest;
import com.ticle.server.post.dto.GeminiResponse;
import com.ticle.server.post.exception.PostNotFoundException;
import com.ticle.server.post.repository.PostRepository;
import com.ticle.server.user.service.EmailService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;

import java.time.LocalDate;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;

import static com.ticle.server.post.exception.errorcode.PostErrorCode.POST_NOT_FOUND;

@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class SchedulingService {
public class SchedulingService implements ApplicationRunner {

private final SubscriptionRepository subscriptionRepository;
private final PostRepository postRepository;
private final EmailService emailService;
private final RestTemplate restTemplate;
private final PostTopicCache postTopicCache;

@Value("${gemini.api.url}")
private String apiUrl;

@Value("${gemini.api.key2}")
private String geminiApiKey;

private static final int POST_COUNT = 10;

@Scheduled(cron = "0 0 0 * * *")
public void sendWeeklyPosts() {
Expand All @@ -41,4 +61,67 @@ public void sendWeeklyPosts() {
emailService.sendEmail(subscription.getUser().getEmail(), topPost);
}
}

@Override
public void run(ApplicationArguments args) {
generateCommonTitleMultipleTimes();
}

@Scheduled(cron = "0 0 0 * * *")
public void generateCommonTitleMultipleTimes() {
postTopicCache.clearCache();

int count = (int) postRepository.count();

for (int i = 0; i < POST_COUNT; i++) {
saveCommonTitleAndPostsToCache(count);
}
}

private void saveCommonTitleAndPostsToCache(int count) {
Set<Long> selectedPostIds = getRandomIndices(count);

// 선택된 post_id에 해당하는 Post 객체 가져오기
List<PostSetsResponse> selectedPosts = postRepository.findSelectedPostInfoByIds(selectedPostIds);

// AI에게 3개의 Post 제목을 보내서 공통된 제목 추천받기
String commonTitle = generateCommonTitle(selectedPosts);

// commonTitle과 selectedPosts를 PostTopicCache에 저장
postTopicCache.saveToCache(commonTitle, selectedPosts);
log.info("post: {}", postTopicCache.getPostsFromCache(commonTitle));
}

private String generateCommonTitle(List<PostSetsResponse> selectedPosts) {
// Gemini에 요청 전송
String requestUrl = apiUrl + "?key=" + geminiApiKey;

String prompt = selectedPosts.stream()
.map(PostSetsResponse::title)
.toList() + " 다음은 3개의 기사 제목 리스트야. 해당 제목들에 대한 공통제목을 20자 이내로 1개만 추천해줘 " +
"예: commonTitle = [개발자를 위한 성장 가이드]";

log.info("prompt: {}", prompt);

GeminiRequest request = new GeminiRequest(prompt);
GeminiResponse response = restTemplate.postForObject(requestUrl, request, GeminiResponse.class);

log.info("response: {}", response);

return response.getCommonTitle();
}

private Set<Long> getRandomIndices(int count) {
// 3개의 랜덤한 post_id 선택하기
Set<Long> selectedPostIds = new HashSet<>();
ThreadLocalRandom random = ThreadLocalRandom.current();

while (selectedPostIds.size() < 3) {
long randomPostId = random.nextInt(count) + 1L;
selectedPostIds.add(randomPostId);
}

log.info("selectedPostIds: {}", selectedPostIds);
return selectedPostIds;
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/ticle/server/post/dto/GeminiResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.ticle.server.post.repository.PostRepository;
import lombok.*;
import org.springframework.util.CollectionUtils;

import java.util.*;

Expand Down Expand Up @@ -84,6 +85,7 @@ public List<Map<String, Object>> formatRecommendPost() {
// quiz 생성 데이터 format
public List<QuizResponse> formatQuiz(String postTitle) {
List<QuizResponse> quizzes = new ArrayList<>();

if (candidates != null) {
System.out.println("Candidates found: " + candidates.size());
int quizNo = 1;
Expand Down Expand Up @@ -122,8 +124,24 @@ public List<QuizResponse> formatQuiz(String postTitle) {
}
}
}

System.out.println("Formatted quizzes: " + quizzes);
return quizzes;
}

public String getCommonTitle() {
String text = "";

if (candidates != null) {
for (Candidate candidate : candidates) {
Content content = candidate.getContent();

if (content != null && !CollectionUtils.isEmpty(content.getParts())) {
text = content.getParts().get(0).getText();
}
}
}

return text;
}
}
14 changes: 12 additions & 2 deletions src/main/java/com/ticle/server/post/repository/PostRepository.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ticle.server.post.repository;

import com.ticle.server.home.dto.response.PostSetsResponse;
import com.ticle.server.post.domain.Post;
import com.ticle.server.post.dto.PostIdTitleDto;
import com.ticle.server.user.domain.type.Category;
Expand All @@ -11,11 +12,10 @@

import java.util.List;
import java.util.Optional;
import java.util.Set;


public interface PostRepository extends JpaRepository<Post, Long> {
Post findByPostId(Long postId);

Page<Post> findByCategory(Category category, Pageable pageable);

@Query("SELECT new com.ticle.server.post.dto.PostIdTitleDto(p.postId, p.title) FROM Post p")
Expand All @@ -28,6 +28,16 @@ public interface PostRepository extends JpaRepository<Post, Long> {
"ORDER BY p.createdDate DESC LIMIT 1")
Optional<Post> findTopPostByCategory(@Param("category") Category category);

@Query("SELECT new com.ticle.server.home.dto.response.PostSetsResponse(p.title, p.image.imageUrl, p.category, p.author, p.createdDate) " +
"FROM Post p " +
"WHERE p.postId IN (:postIds)")
List<PostSetsResponse> findSelectedPostInfoByIds(@Param("postIds") Set<Long> postIds);

@Query("SELECT new com.ticle.server.home.dto.response.PostSetsResponse(p.title, p.image.imageUrl, p.category, p.author, p.createdDate) " +
"FROM Post p " +
"ORDER BY p.scrapCount DESC LIMIT 3")
List<PostSetsResponse> findTop3ByOrderByScrapCountDesc();

@Query("SELECT p FROM Post p WHERE " +
"(:keyword IS NULL OR p.title LIKE %:keyword%) AND " +
"(:category IS NULL OR p.category = :category)")
Expand Down

0 comments on commit c1c1adc

Please sign in to comment.