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
10 changes: 8 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// mongodb
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
Expand All @@ -49,12 +49,18 @@ dependencies {

// aws
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.261'
implementation 'com.amazonaws:aws-java-sdk-lambda:1.12.586'
implementation 'com.amazonaws:aws-java-sdk-core:1.12.586'


// Youtube API
implementation 'com.google.api-client:google-api-client:1.33.0'
implementation 'com.google.oauth-client:google-oauth-client-jetty:1.23.0'
implementation 'com.google.apis:google-api-services-youtube:v3-rev20230816-2.0.0'
implementation 'com.google.http-client:google-http-client-jackson2:1.39.2'

// jackson
implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.0'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,46 @@
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.lambda.AWSLambda;
import com.amazonaws.services.lambda.AWSLambdaClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class S3Config {
public class AwsConfig {

@Value("${aws.accessKey}")
private String accessKey;

@Value("${aws.secretKey}")
private String secretKey;

@Value("${aws.region}")
private String region;
@Value("${aws.s3.region}")
private String s3Region;

@Value("${aws.lambda.region}")
private String lambdaRegion;

@Bean
public AmazonS3 amazonS3() {
BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);

return AmazonS3ClientBuilder.standard()
.withRegion(Regions.fromName(region))
.withRegion(Regions.fromName(s3Region))
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}


@Bean
public AWSLambda awsLambda() {
BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);

return AWSLambdaClientBuilder.standard()
.withRegion(Regions.fromName(lambdaRegion))
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package swm.betterlife.antifragile.common.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import swm.betterlife.antifragile.domain.member.service.MemberService;

@Configuration
@EnableScheduling
@RequiredArgsConstructor
public class SchedulingConfig {
private final MemberService memberService;

@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
public void resetDailyRecommendCount() {
memberService.resetRemainRecommendNumber();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class SecurityConfig {
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

private static final String[] PERMIT_PATHS = {
"/auth", "/auth/**", "token/**", "/health-check",
"/auth", "/auth/**", "token/**", "/health-check", "/recommends/lambda"
};

private static final String[] PERMIT_QUERY_PARAM_PATHS = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import swm.betterlife.antifragile.domain.member.entity.Member;
import swm.betterlife.antifragile.domain.member.service.MemberService;
import swm.betterlife.antifragile.domain.recommend.dto.response.YouTubeResponse;
import swm.betterlife.antifragile.domain.recommend.service.LambdaService;
import swm.betterlife.antifragile.domain.recommend.service.RecommendService;

@Service
Expand All @@ -40,6 +41,7 @@ public class ContentService {
private final MemberService memberService;
private final DiaryAnalysisService diaryAnalysisService;
private final RecommendService recommendService;
private final LambdaService lambdaService;

@Transactional
public ContentListResponse saveRecommendContents(String memberId, LocalDate date) {
Expand Down Expand Up @@ -119,10 +121,13 @@ public void unlikeContent(String memberId, String contentId) {

private List<Content> getRecommendContentsByAnalysis(DiaryAnalysis analysis, Member member) {

String prompt = recommendService.createPrompt(analysis.getEmotions(), member);
String prompt = recommendService.createPrompt(
analysis.getEmotions(), analysis.getEvent(), member);

List<String> videoIds = lambdaService.getRecommendations(prompt);

try {
YouTubeResponse youTubeResponse = recommendService.youTubeRecommend(prompt);
YouTubeResponse youTubeResponse = recommendService.getYoutubeInfo(videoIds);
return youTubeResponse.toContentList();
} catch (IOException e) {
throw new YouTubeApiException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,11 @@ public void modifyPassword(
}
}

@Scheduled(cron = "0 0 0 * * *")
public void resetRemainRecommendNumber() {
Query query = new Query();
Update update = new Update().set("remainRecommendNumber", 3);

mongoTemplate.updateMulti(query, update, Member.class);
UpdateResult result = mongoTemplate.updateMulti(query, update, Member.class);
}

private Authentication getAuthenticate(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package swm.betterlife.antifragile.domain.recommend.controller;

import java.io.IOException;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import swm.betterlife.antifragile.common.response.ResponseBody;
import swm.betterlife.antifragile.domain.recommend.dto.request.LambdaRequest;
import swm.betterlife.antifragile.domain.recommend.dto.request.RecommendPromptRequest;
import swm.betterlife.antifragile.domain.recommend.dto.response.OpenAiResponse;
import swm.betterlife.antifragile.domain.recommend.dto.response.YouTubeResponse;
import swm.betterlife.antifragile.domain.recommend.service.LambdaService;
import swm.betterlife.antifragile.domain.recommend.service.RecommendService;

@Slf4j
Expand All @@ -19,6 +22,7 @@
@RequestMapping("/recommends")
public class RecommendController {
private final RecommendService recommendService;
private final LambdaService lambdaService;

@PostMapping("/chat-gpt")
public ResponseBody<OpenAiResponse> chatGpt(
Expand All @@ -35,4 +39,12 @@ public ResponseBody<YouTubeResponse> youTubeRecommend(
return ResponseBody.ok(
recommendService.youTubeRecommend(request.prompt()));
}

@PostMapping("/lambda")
public ResponseBody<List<String>> getRecommendations(@RequestBody LambdaRequest request) {

return ResponseBody.ok(
lambdaService.getRecommendations(request.prompt())
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package swm.betterlife.antifragile.domain.recommend.dto.request;

public record LambdaRequest(
String prompt
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package swm.betterlife.antifragile.domain.recommend.service;

import com.amazonaws.services.lambda.AWSLambda;
import com.amazonaws.services.lambda.model.InvokeRequest;
import com.amazonaws.services.lambda.model.InvokeResult;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class LambdaService {

private final ObjectMapper objectMapper;
private final AWSLambda awsLambda;
private static final String FUNCTION_NAME = "bedrock_api";

public List<String> getRecommendations(String prompt) {
try {
// 요청 데이터 구성
Map<String, String> payload = new HashMap<>();
payload.put("prompt", prompt);

// Lambda 호출 요청 생성
InvokeRequest request = new InvokeRequest()
.withFunctionName(FUNCTION_NAME)
.withPayload(new ObjectMapper().writeValueAsString(payload));

// Lambda 호출
log.info("Invoking Lambda function with payload: {}", payload);
InvokeResult result = awsLambda.invoke(request);

// 응답 처리
if (result.getFunctionError() != null) {
log.error("Lambda function error: {}", result.getFunctionError());
throw new RuntimeException("Lambda function error: " + result.getFunctionError());
}

String response = new String(result.getPayload().array(), StandardCharsets.UTF_8);
log.info("Lambda response: {}", response);

// 응답 파싱
return parseLambdaResponse(response);

} catch (Exception e) {
log.error("Failed to invoke Lambda function", e);
throw new RuntimeException("Failed to get recommendations", e);
}
}

private List<String> parseLambdaResponse(String response) throws JsonProcessingException {
JsonNode root = objectMapper.readTree(response);
List<String> recommendations = new ArrayList<>();

// API Gateway 형식의 응답을 처리
if (root.has("body")) {
String body = root.get("body").asText();
// body가 문자열로 된 JSON이라면 다시 파싱
JsonNode bodyNode = objectMapper.readTree(body);

if (bodyNode.has("video_ids")) {
JsonNode videoIdsNode = bodyNode.get("video_ids");
if (videoIdsNode.isArray()) {
for (JsonNode item : videoIdsNode) {
recommendations.add(item.asText());
}
}
}
}

return recommendations;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,87 @@ public class RecommendService {
@Value("${youtube.api.key}")
private String apiKey;

public String createPrompt(List<String> emotions, Member member) {
public String createPrompt(List<String> emotions, String event, Member member) {

String emotionString = String.join(", ", emotions);

return String.format(
"%s 감정을 가진 나이가 %d인 %s에게 추천할만한 영상의 키워드를 하나만 반환해줘.",
"%s 감정을 가진 나이가 %d인 %s이 쓴 일기 내용은 %s야. 이 일기의 감정에 정신적으로 도움이 되는 메타데이터를 10개 추천해줘",
emotionString,
AgeConverter.convertDateToAge(member.getBirthDate()),
member.getJob()
member.getJob(),
event
);
}

public YouTubeResponse getYoutubeInfo(List<String> videoIds) throws IOException {
log.info("Received Video IDs: {}", videoIds);

JsonFactory jsonFactory = new JacksonFactory();

// YouTube 객체를 빌드하여 API에 접근할 수 있는 YouTube 클라이언트 생성
YouTube youtube = new YouTube.Builder(
new com.google.api.client.http.javanet.NetHttpTransport(),
jsonFactory,
request -> {})
.setApplicationName("Antifragile")
.build();

List<YouTubeResponse.YouTubeApiInfo> youTubeApiInfos = new ArrayList<>();

// 동영상 정보를 가져오기 위한 요청 생성
YouTube.Videos.List videoRequest = youtube.videos()
.list(Collections.singletonList("snippet,statistics,status"));
videoRequest.setKey(apiKey);
videoRequest.setId(videoIds);

// API 호출
VideoListResponse videoResponse = videoRequest.execute();
List<Video> videos = videoResponse.getItems();

// 각 동영상에 대해 정보 추출
for (Video video : videos) {
VideoStatus status = video.getStatus();
if (status.getEmbeddable() == null || !status.getEmbeddable()) {
continue; // 임베딩이 불가능한 동영상 건너뛰기
}

String videoId = video.getId();
String videoTitle = video.getSnippet().getTitle();
String videoDescription = video.getSnippet().getDescription();
String thumbnailUrl = "https://img.youtube.com/vi/" + videoId + "/sddefault.jpg";
String channelId = video.getSnippet().getChannelId();
String channelTitle = video.getSnippet().getChannelTitle();

// 채널 정보 요청 생성 및 실행
YouTube.Channels.List channelRequest = youtube.channels()
.list(Collections.singletonList("snippet,statistics"));
channelRequest.setKey(apiKey);
channelRequest.setId(Collections.singletonList(channelId));
ChannelListResponse channelResponse = channelRequest.execute();
Channel channel = channelResponse.getItems().get(0);
ChannelStatistics statistics = channel.getStatistics();
Long subscriberCount = Optional.ofNullable(statistics)
.map(ChannelStatistics::getSubscriberCount)
.map(Number::longValue)
.orElse(null);
String channelImageUrl = channel.getSnippet()
.getThumbnails().getDefault().getUrl();

youTubeApiInfos.add(new YouTubeResponse.YouTubeApiInfo(
videoTitle,
videoDescription,
thumbnailUrl,
subscriberCount,
channelTitle,
channelImageUrl,
"https://www.youtube.com/watch?v=" + videoId
));
}

return new YouTubeResponse(youTubeApiInfos);
}

public OpenAiResponse chatGpt(String prompt) {
OpenAiRequest openAiRequest = new OpenAiRequest(
model, prompt, 1);
Expand Down
2 changes: 1 addition & 1 deletion submodule-config