77import com .bootsignal .domain .ai .review .dto .ReviewSummaryContent ;
88import com .bootsignal .domain .ai .review .dto .ReviewSummaryCreateRequest ;
99import com .bootsignal .domain .ai .review .dto .ReviewSummaryResponse ;
10+ import com .bootsignal .domain .ai .review .entity .ReviewSummaryCache ;
11+ import com .bootsignal .domain .ai .review .repository .ReviewSummaryCacheRepository ;
12+ import com .bootsignal .domain .crawled_review .repository .CrawledReviewRepository ;
1013import com .bootsignal .domain .user .entity .User ;
1114import com .bootsignal .domain .user .repository .UserRepository ;
1215import com .bootsignal .global .exception .BootSignalException ;
1316import com .bootsignal .global .exception .ErrorCode ;
1417import com .bootsignal .global .security .SecurityUtil ;
1518import java .math .BigDecimal ;
19+ import java .time .Instant ;
1620import java .util .LinkedHashMap ;
1721import java .util .Map ;
22+ import java .util .Optional ;
23+ import java .util .UUID ;
1824import lombok .RequiredArgsConstructor ;
1925import org .springframework .stereotype .Service ;
26+ import org .springframework .transaction .annotation .Transactional ;
2027
2128@ Service
2229@ RequiredArgsConstructor
2330public class ReviewSummaryService {
2431
25- // 인증된 사용자의 요청을 AI 실행 컨텍스트로 변환해 리뷰 요약 Agent에 위임한다.
2632 private final AgentHarness agentHarness ;
2733 private final UserRepository userRepository ;
34+ private final ReviewSummaryCacheRepository cacheRepository ;
35+ private final CrawledReviewRepository crawledReviewRepository ;
2836
37+ @ Transactional
2938 public ReviewSummaryResponse createSummary (ReviewSummaryCreateRequest request ) {
3039 User user = getAuthenticatedUser ();
40+ Long courseId = request .courseId ();
41+
42+ long currentCount = countReviews (courseId );
43+ Instant currentLatestCrawledAt = latestCrawledAt (courseId );
44+
45+ Optional <ReviewSummaryCache > cached = cacheRepository .findByCourseId (courseId );
46+
47+ if (cached .isPresent ()) {
48+ ReviewSummaryCache cache = cached .get ();
49+ if (!cache .isStale ((int ) currentCount , currentLatestCrawledAt )) {
50+ return toCachedResponse (cache );
51+ }
52+ return regenerateAndUpdate (request , cache , user , (int ) currentCount , currentLatestCrawledAt );
53+ }
54+
55+ return generateAndSave (request , user , (int ) currentCount , currentLatestCrawledAt );
56+ }
57+
58+ private ReviewSummaryResponse generateAndSave (ReviewSummaryCreateRequest request ,
59+ User user , int reviewCount , Instant latestCrawledAt ) {
60+ AgentExecutionResult result = executeAgent (request , user );
61+ ReviewSummaryContent content = extractContent (result );
62+
63+ ReviewSummaryCache cache = ReviewSummaryCache .builder ()
64+ .courseId (asLong (result .output ().get ("courseId" )))
65+ .executionId (result .executionId ().toString ())
66+ .reviewCount (reviewCount )
67+ .latestCrawledAt (latestCrawledAt )
68+ .averageRating (asBigDecimal (result .output ().get ("averageRating" )))
69+ .courseTitle (asString (result .output ().get ("courseTitle" )))
70+ .summary (content .summary ())
71+ .strengths (content .strengths ())
72+ .weaknesses (content .weaknesses ())
73+ .recommendedFor (content .recommendedFor ())
74+ .keywords (content .keywords ())
75+ .build ();
76+ cacheRepository .save (cache );
77+
78+ return toResponse (result , content );
79+ }
80+
81+ private ReviewSummaryResponse regenerateAndUpdate (ReviewSummaryCreateRequest request ,
82+ ReviewSummaryCache cache , User user , int reviewCount , Instant latestCrawledAt ) {
83+ AgentExecutionResult result = executeAgent (request , user );
84+ ReviewSummaryContent content = extractContent (result );
85+
86+ cache .update (
87+ result .executionId ().toString (),
88+ reviewCount ,
89+ latestCrawledAt ,
90+ asBigDecimal (result .output ().get ("averageRating" )),
91+ asString (result .output ().get ("courseTitle" )),
92+ content
93+ );
94+
95+ return toResponse (result , content );
96+ }
97+
98+ private ReviewSummaryResponse toCachedResponse (ReviewSummaryCache cache ) {
99+ return ReviewSummaryResponse .from (
100+ UUID .fromString (cache .getExecutionId ()),
101+ cache .getCourseId (),
102+ cache .getCourseTitle (),
103+ cache .getReviewCount (),
104+ cache .getAverageRating (),
105+ cache .toContent ()
106+ );
107+ }
108+
109+ private AgentExecutionResult executeAgent (ReviewSummaryCreateRequest request , User user ) {
31110 AgentExecutionResult result = agentHarness .execute (AgentExecutionContext .of (
32111 AgentType .REVIEW_SUMMARY ,
33112 user .getId (),
34113 toInputSummary (request ),
35114 toInput (request )
36115 ));
37-
38116 if (!result .isSuccess ()) {
39117 throw new BootSignalException (ErrorCode .AI_EXECUTION_FAILED , result .errorMessage ());
40118 }
119+ return result ;
120+ }
41121
122+ private ReviewSummaryContent extractContent (AgentExecutionResult result ) {
42123 Object summary = result .output ().get ("summary" );
43124 if (!(summary instanceof ReviewSummaryContent content )) {
44125 throw new BootSignalException (ErrorCode .AI_OUTPUT_INVALID , "수강후기 요약 결과를 찾을 수 없습니다." );
45126 }
127+ return content ;
128+ }
129+
130+ private ReviewSummaryResponse toResponse (AgentExecutionResult result , ReviewSummaryContent content ) {
46131 return ReviewSummaryResponse .from (
47132 result .executionId (),
48133 asLong (result .output ().get ("courseId" )),
@@ -53,6 +138,18 @@ public ReviewSummaryResponse createSummary(ReviewSummaryCreateRequest request) {
53138 );
54139 }
55140
141+ private long countReviews (Long courseId ) {
142+ long count = crawledReviewRepository .countReviewsByCourseId (courseId );
143+ if (count == 0 ) {
144+ throw new BootSignalException (ErrorCode .AI_INPUT_INVALID , "요약할 고용24 수강후기가 없습니다." );
145+ }
146+ return count ;
147+ }
148+
149+ private Instant latestCrawledAt (Long courseId ) {
150+ return crawledReviewRepository .findMaxCrawledAtByCourseId (courseId ).orElse (Instant .EPOCH );
151+ }
152+
56153 private Map <String , Object > toInput (ReviewSummaryCreateRequest request ) {
57154 Map <String , Object > input = new LinkedHashMap <>();
58155 input .put ("courseId" , request .courseId ());
0 commit comments