Skip to content

Commit 571c3bb

Browse files
authored
Merge pull request #17 from CodIN-INU/develop
feat: 스케쥴링을 통한 AI 요약 생성기능 추가 enhancement
2 parents ece753a + 2da052e commit 571c3bb

File tree

7 files changed

+155
-20
lines changed

7 files changed

+155
-20
lines changed

src/main/java/inu/codin/codin/domain/lecture/repository/LectureRepository.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package inu.codin.codin.domain.lecture.repository;
22

33
import inu.codin.codin.domain.lecture.entity.Lecture;
4-
import org.springframework.data.domain.Page;
54
import org.springframework.data.domain.Pageable;
65
import org.springframework.data.jpa.repository.JpaRepository;
76
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
88
import org.springframework.stereotype.Repository;
99

10+
import java.time.LocalDateTime;
1011
import java.util.List;
1112
import java.util.Optional;
1213

@@ -53,4 +54,19 @@ public interface LectureRepository extends JpaRepository<Lecture, Long> {
5354
WHERE l.id=:lectureId
5455
""")
5556
Optional<Lecture> findLectureWithTagsAndReviewsById(Long lectureId);
57+
58+
@Query("""
59+
SELECT l.id
60+
FROM Lecture l
61+
WHERE l.aiSummary IS NULL OR l.aiSummary = ''
62+
""")
63+
List<Long> findLectureIdsWithoutAiSummary();
64+
65+
@Query("""
66+
SELECT DISTINCT l.id
67+
FROM Lecture l
68+
JOIN l.reviews r
69+
WHERE r.createdAt > :since
70+
""")
71+
List<Long> findLectureIdsWithRecentReviews(@Param("since") LocalDateTime since);
5672
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package inu.codin.codin.domain.lecture.scheduler;
2+
3+
import inu.codin.codin.domain.lecture.repository.LectureRepository;
4+
import inu.codin.codin.domain.lecture.service.LectureSummarizationService;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.scheduling.annotation.Async;
8+
import org.springframework.scheduling.annotation.Scheduled;
9+
import org.springframework.stereotype.Service;
10+
11+
import java.time.LocalDateTime;
12+
import java.util.List;
13+
14+
@Service
15+
@Slf4j
16+
@RequiredArgsConstructor
17+
public class LectureAiSummaryScheduler {
18+
19+
private final LectureRepository lectureRepository;
20+
private final LectureSummarizationService summarizationService;
21+
22+
/**
23+
* 매시간 AI 요약이 없는 강의들 처리 (00분마다 실행)
24+
*/
25+
@Scheduled(cron = "0 0 * * * *")
26+
@Async("aiSummaryScheduleExecutor")
27+
public void generateMissingAiSummaries() {
28+
log.info("[generateMissingAiSummaries] 매시간 AI 요약 누락 강의 처리 시작");
29+
30+
try {
31+
processLecturesWithoutAiSummary();
32+
log.info("[generateMissingAiSummaries] 매시간 AI 요약 누락 강의 처리 완료");
33+
} catch (Exception e) {
34+
log.error("[generateMissingAiSummaries] 매시간 AI 요약 누락 강의 처리 중 오류 발생", e);
35+
}
36+
}
37+
38+
/**
39+
* 매일 새벽 3시 전체 AI 요약 생성/업데이트
40+
*/
41+
@Scheduled(cron = "0 0 3 * * *")
42+
@Async("aiSummaryScheduleExecutor")
43+
public void generateDailyAiSummaries() {
44+
log.info("[generateDailyAiSummaries] 일일 AI 요약 생성 작업 시작");
45+
46+
try {
47+
// 최근 리뷰가 업데이트된 강의들 처리
48+
processLecturesWithRecentReviews();
49+
50+
log.info("[generateDailyAiSummaries] 일일 AI 요약 생성 작업 완료");
51+
} catch (Exception e) {
52+
log.error("[generateDailyAiSummaries] 일일 AI 요약 생성 작업 중 오류 발생", e);
53+
}
54+
}
55+
56+
/**
57+
* AI 요약이 없는 강의들 처리
58+
*/
59+
private void processLecturesWithoutAiSummary() {
60+
List<Long> lectureIdsWithoutSummary = lectureRepository.findLectureIdsWithoutAiSummary();
61+
log.info("[processLecturesWithoutAiSummary] AI 요약이 없는 강의 수: {}", lectureIdsWithoutSummary.size());
62+
63+
for (Long lectureId : lectureIdsWithoutSummary) {
64+
try {
65+
summarizationService.summarizeLecture(lectureId);
66+
log.debug("[processLecturesWithoutAiSummary] 강의 ID {} AI 요약 생성 완료", lectureId);
67+
Thread.sleep(500); // API 호출 제한 딜레이
68+
} catch (Exception e) {
69+
log.error("[processLecturesWithoutAiSummary] 강의 ID {} AI 요약 생성 실패", lectureId, e);
70+
}
71+
}
72+
}
73+
74+
/**
75+
* 최근 리뷰가 업데이트된 강의들 처리
76+
*/
77+
private void processLecturesWithRecentReviews() {
78+
LocalDateTime yesterday = LocalDateTime.now().minusDays(1);
79+
List<Long> lectureIdsWithRecentReviews = lectureRepository.findLectureIdsWithRecentReviews(yesterday);
80+
log.info("[processLecturesWithRecentReviews] 최근 리뷰가 등록된 강의 수: {}", lectureIdsWithRecentReviews.size());
81+
82+
for (Long lectureId : lectureIdsWithRecentReviews) {
83+
try {
84+
summarizationService.summarizeLecture(lectureId);
85+
log.debug("[processLecturesWithRecentReviews] 강의 ID {} AI 요약 업데이트 완료", lectureId);
86+
Thread.sleep(500); // API 호출 제한 딜레이
87+
} catch (Exception e) {
88+
log.error("[processLecturesWithRecentReviews] 강의 ID {} AI 요약 업데이트 실패", lectureId, e);
89+
}
90+
}
91+
}
92+
}

src/main/java/inu/codin/codin/domain/lecture/service/LectureSummarizationService.java

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,15 @@ public class LectureSummarizationService {
2828

2929
@Transactional
3030
public void summarizeLecture(Long lectureId) {
31+
log.info("[summarizeLecture] 강의 AI 요약 프롬프트 실행, lectureId: {}", lectureId);
3132
Lecture lecture = lectureRepository.findLectureWithTagsAndReviewsById(lectureId)
3233
.orElseThrow(() -> new LectureException(LectureErrorCode.LECTURE_NOT_FOUND));
3334

34-
// 리뷰가 없거나 최소 조건 만족 예외 처리
35-
if (lecture.getReviews().isEmpty()) {
36-
log.info("강의에 리뷰가 없어 AI 요약을 생략, lectureId:{}", lectureId);
37-
return;
38-
}
39-
4035
// 리뷰들을 하나의 문자열화
4136
String reviewsText = lecture.getReviews().stream()
4237
.map(Review::getContent)
4338
.collect(Collectors.joining("\n"));
4439

45-
if (reviewsText.trim().isEmpty()) {
46-
log.info("유효한 리뷰 내용이 없어 AI 요약을 생략, lectureId:{}", lectureId);
47-
return;
48-
}
49-
5040
// 강의 태그 메타데이터 문자열화
5141
String tags = lecture.getTags().stream()
5242
.map(t -> t.getTag().getTagName())

src/main/java/inu/codin/codin/global/config/AsyncConfig.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,12 @@ public class AsyncConfig {
1414
@Bean("aiSummaryExecutor")
1515
public Executor aiSummaryExecutor() {
1616
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
17-
executor.setCorePoolSize(2);
18-
executor.setMaxPoolSize(4);
19-
executor.setQueueCapacity(100);
20-
executor.setThreadNamePrefix("ai-summary-");
21-
17+
executor.setCorePoolSize(1);
18+
executor.setMaxPoolSize(2);
19+
executor.setQueueCapacity(50);
20+
executor.setThreadNamePrefix("ai-summary-event");
2221
executor.setWaitForTasksToCompleteOnShutdown(true);
2322
executor.setAwaitTerminationSeconds(30);
24-
2523
executor.initialize();
2624
return executor;
2725
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package inu.codin.codin.global.config;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.scheduling.annotation.EnableScheduling;
6+
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
7+
8+
import java.util.concurrent.Executor;
9+
10+
@Configuration
11+
@EnableScheduling
12+
public class SchedulingConfig {
13+
14+
@Bean("aiSummaryScheduleExecutor")
15+
public Executor aiSummaryScheduleExecutor() {
16+
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
17+
executor.setCorePoolSize(2);
18+
executor.setMaxPoolSize(4);
19+
executor.setQueueCapacity(100);
20+
executor.setThreadNamePrefix("ai-summary-schedule-");
21+
executor.setWaitForTasksToCompleteOnShutdown(true);
22+
executor.setAwaitTerminationSeconds(30);
23+
executor.initialize();
24+
return executor;
25+
}
26+
}

src/main/java/inu/codin/codin/global/config/SwaggerConfig.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,17 @@ public class SwaggerConfig {
2323
@Value("${server.domain}")
2424
private String BASE_DOMAIN_URL;
2525

26+
@Value(("${server.port}"))
27+
private String BASE_PORT;
28+
2629
@Bean
2730
public OpenAPI customOpenAPI() {
2831
Info info = new Info()
2932
.title("CODIN Lecture API Documentation")
3033
.description("CODIN Lecture API 명세서")
3134
.version("v1.0.0");
3235

33-
// Bearer Token Auth 설정 (백업용)
36+
// Bearer Token Auth 설정
3437
SecurityScheme bearerAuth = new SecurityScheme()
3538
.type(SecurityScheme.Type.HTTP)
3639
.scheme("bearer")
@@ -47,7 +50,7 @@ public OpenAPI customOpenAPI() {
4750
.addSecuritySchemes("bearerAuth", bearerAuth)
4851
)
4952
.servers(List.of(
50-
new Server().url("http://localhost:8085").description("Local Server"),
53+
new Server().url("http://localhost:" + BASE_PORT).description("Local Server"),
5154
new Server().url(BASE_DOMAIN_URL + "/api/lectures").description("Production Server")
5255
));
5356
}

src/main/resources/application.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ spring:
2828
options:
2929
model: gpt-4.1-mini
3030
temperature: 0.7
31+
task:
32+
scheduling:
33+
pool:
34+
size: 2
35+
thread-name-prefix: scheduled-task-
36+
execution:
37+
pool:
38+
core-size: 2
39+
max-size: 4
40+
queue-capacity: 100
3141

3242
elasticsearch:
3343
uris: ${ELASTICSEARCH_URL:http://localhost:9200}

0 commit comments

Comments
 (0)