diff --git a/build.gradle b/build.gradle index e98f700..c7bd75f 100644 --- a/build.gradle +++ b/build.gradle @@ -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' @@ -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') { diff --git a/src/main/java/swm/betterlife/antifragile/common/config/S3Config.java b/src/main/java/swm/betterlife/antifragile/common/config/AwsConfig.java similarity index 57% rename from src/main/java/swm/betterlife/antifragile/common/config/S3Config.java rename to src/main/java/swm/betterlife/antifragile/common/config/AwsConfig.java index ac77477..261ee4e 100644 --- a/src/main/java/swm/betterlife/antifragile/common/config/S3Config.java +++ b/src/main/java/swm/betterlife/antifragile/common/config/AwsConfig.java @@ -3,6 +3,8 @@ 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; @@ -10,7 +12,7 @@ import org.springframework.context.annotation.Configuration; @Configuration -public class S3Config { +public class AwsConfig { @Value("${aws.accessKey}") private String accessKey; @@ -18,15 +20,29 @@ public class S3Config { @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(); } diff --git a/src/main/java/swm/betterlife/antifragile/common/config/SchedulingConfig.java b/src/main/java/swm/betterlife/antifragile/common/config/SchedulingConfig.java new file mode 100644 index 0000000..d764ef6 --- /dev/null +++ b/src/main/java/swm/betterlife/antifragile/common/config/SchedulingConfig.java @@ -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(); + } +} diff --git a/src/main/java/swm/betterlife/antifragile/common/config/SecurityConfig.java b/src/main/java/swm/betterlife/antifragile/common/config/SecurityConfig.java index d9478b4..9b5cc45 100644 --- a/src/main/java/swm/betterlife/antifragile/common/config/SecurityConfig.java +++ b/src/main/java/swm/betterlife/antifragile/common/config/SecurityConfig.java @@ -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 = { diff --git a/src/main/java/swm/betterlife/antifragile/domain/content/service/ContentService.java b/src/main/java/swm/betterlife/antifragile/domain/content/service/ContentService.java index 77f51fe..c1d27cb 100644 --- a/src/main/java/swm/betterlife/antifragile/domain/content/service/ContentService.java +++ b/src/main/java/swm/betterlife/antifragile/domain/content/service/ContentService.java @@ -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 @@ -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) { @@ -119,10 +121,13 @@ public void unlikeContent(String memberId, String contentId) { private List getRecommendContentsByAnalysis(DiaryAnalysis analysis, Member member) { - String prompt = recommendService.createPrompt(analysis.getEmotions(), member); + String prompt = recommendService.createPrompt( + analysis.getEmotions(), analysis.getEvent(), member); + + List videoIds = lambdaService.getRecommendations(prompt); try { - YouTubeResponse youTubeResponse = recommendService.youTubeRecommend(prompt); + YouTubeResponse youTubeResponse = recommendService.getYoutubeInfo(videoIds); return youTubeResponse.toContentList(); } catch (IOException e) { throw new YouTubeApiException(); diff --git a/src/main/java/swm/betterlife/antifragile/domain/member/service/MemberService.java b/src/main/java/swm/betterlife/antifragile/domain/member/service/MemberService.java index 563c981..4dc24e5 100644 --- a/src/main/java/swm/betterlife/antifragile/domain/member/service/MemberService.java +++ b/src/main/java/swm/betterlife/antifragile/domain/member/service/MemberService.java @@ -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( diff --git a/src/main/java/swm/betterlife/antifragile/domain/recommend/controller/RecommendController.java b/src/main/java/swm/betterlife/antifragile/domain/recommend/controller/RecommendController.java index d780c1e..6684173 100644 --- a/src/main/java/swm/betterlife/antifragile/domain/recommend/controller/RecommendController.java +++ b/src/main/java/swm/betterlife/antifragile/domain/recommend/controller/RecommendController.java @@ -1,6 +1,7 @@ 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; @@ -8,9 +9,11 @@ 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 @@ -19,6 +22,7 @@ @RequestMapping("/recommends") public class RecommendController { private final RecommendService recommendService; + private final LambdaService lambdaService; @PostMapping("/chat-gpt") public ResponseBody chatGpt( @@ -35,4 +39,12 @@ public ResponseBody youTubeRecommend( return ResponseBody.ok( recommendService.youTubeRecommend(request.prompt())); } + + @PostMapping("/lambda") + public ResponseBody> getRecommendations(@RequestBody LambdaRequest request) { + + return ResponseBody.ok( + lambdaService.getRecommendations(request.prompt()) + ); + } } diff --git a/src/main/java/swm/betterlife/antifragile/domain/recommend/dto/request/LambdaRequest.java b/src/main/java/swm/betterlife/antifragile/domain/recommend/dto/request/LambdaRequest.java new file mode 100644 index 0000000..adc8293 --- /dev/null +++ b/src/main/java/swm/betterlife/antifragile/domain/recommend/dto/request/LambdaRequest.java @@ -0,0 +1,6 @@ +package swm.betterlife.antifragile.domain.recommend.dto.request; + +public record LambdaRequest( + String prompt +) { +} diff --git a/src/main/java/swm/betterlife/antifragile/domain/recommend/service/LambdaService.java b/src/main/java/swm/betterlife/antifragile/domain/recommend/service/LambdaService.java new file mode 100644 index 0000000..f2cba31 --- /dev/null +++ b/src/main/java/swm/betterlife/antifragile/domain/recommend/service/LambdaService.java @@ -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 getRecommendations(String prompt) { + try { + // 요청 데이터 구성 + Map 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 parseLambdaResponse(String response) throws JsonProcessingException { + JsonNode root = objectMapper.readTree(response); + List 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; + } +} diff --git a/src/main/java/swm/betterlife/antifragile/domain/recommend/service/RecommendService.java b/src/main/java/swm/betterlife/antifragile/domain/recommend/service/RecommendService.java index d8d2334..3c86d90 100644 --- a/src/main/java/swm/betterlife/antifragile/domain/recommend/service/RecommendService.java +++ b/src/main/java/swm/betterlife/antifragile/domain/recommend/service/RecommendService.java @@ -42,18 +42,87 @@ public class RecommendService { @Value("${youtube.api.key}") private String apiKey; - public String createPrompt(List emotions, Member member) { + public String createPrompt(List 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 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 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