Skip to content

Commit 3327722

Browse files
authored
Merge pull request #84 from Konkuk-KUIT/81-collection-fix
fix: collection summarization added on home tab
2 parents c780487 + 73e8177 commit 3327722

4 files changed

Lines changed: 143 additions & 60 deletions

File tree

src/main/java/com/archiveat/server/domain/collection/entity/Collection.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,9 @@ public Collection(User user, Topic topic, String title, String smallCardSummary,
6060
this.perspectiveType = perspectiveType;
6161
this.depthType = depthType;
6262
}
63+
64+
public void updateSummaries(String smallCardSummary, String mediumCardSummary) {
65+
this.smallCardSummary = smallCardSummary;
66+
this.mediumCardSummary = mediumCardSummary;
67+
}
6368
}

src/main/java/com/archiveat/server/domain/collection/service/CollectionGeneratorService.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public class CollectionGeneratorService {
3939
private final TopicRepository topicRepository;
4040
private final UserTopicRepository userTopicRepository;
4141
private final TransactionTemplate transactionTemplate;
42+
private final com.archiveat.server.global.client.PythonClientService pythonClientService;
4243

4344
public void generateCollectionsForTime(LocalTime time) {
4445
log.info("Starting collection generation for time: {}", time);
@@ -167,9 +168,56 @@ private void generateForUser(User user, DepthType targetDepth) {
167168

168169
collection.getCollectionNewsletters().addAll(collectionNewsletters);
169170
collectionRepository.save(collection);
171+
172+
// 9. Generate AI Summary (Async)
173+
// [수정] Python 서버에 요약 요청
174+
generateCollectionSummary(collection, collectionNewsletters);
175+
170176
log.info("Generated collection {} for user {}", collection.getId(), user.getId());
171177
}
172178

179+
private void generateCollectionSummary(Collection collection, List<CollectionNewsletter> collectionNewsletters) {
180+
try {
181+
// 뉴스레터 제목 + (요약 내용이 있다면) 요약 내용 조합
182+
List<String> newsletterSummaries = collectionNewsletters.stream()
183+
.map(cn -> {
184+
String title = cn.getNewsletter().getTitle();
185+
// UserNewsletter를 조회해서 요약 내용을 가져와야 하지만, 현재 구조상 Newsletter 엔티티만 접근 가능
186+
// Newsletter 엔티티에 저장된 요약이 없으므로 제목만 일단 보냄 (추후 개선 필요)
187+
// 또는 UserNewsletter를 통해 이미 조회된 selectedCluster를 활용
188+
return title;
189+
})
190+
.collect(Collectors.toList());
191+
192+
// selectedCluster를 인자로 받아서 처리하도록 리팩토링 필요하지만, 일단 title 리스트만 보냄
193+
// Python 서버 프롬프트가 제목만으로도 어느정도 작문 가능
194+
195+
pythonClientService.requestCollectionSummary(newsletterSummaries)
196+
.thenAccept(response -> {
197+
if (response.getAnalysis() != null) {
198+
transactionTemplate.execute(status -> {
199+
Collection managed = collectionRepository.findById(collection.getId())
200+
.orElse(null);
201+
if (managed != null) {
202+
managed.updateSummaries(
203+
response.getAnalysis().getSmallCardSummary(),
204+
response.getAnalysis().getMediumCardSummary());
205+
}
206+
return null;
207+
});
208+
log.info("Updated summaries for collection {}", collection.getId());
209+
}
210+
})
211+
.exceptionally(ex -> {
212+
log.error("Failed to generate summary for collection {}", collection.getId(), ex);
213+
return null;
214+
});
215+
216+
} catch (Exception e) {
217+
log.error("Error initiating summary generation for collection {}", collection.getId(), e);
218+
}
219+
}
220+
173221
private PerspectiveType calculatePerspectiveType(Long userId, String topicName) {
174222
List<String> nowTopics = userTopicRepository.findTopicNamesByUserIdAndPerspectiveType(userId,
175223
PerspectiveType.NOW);

src/main/java/com/archiveat/server/global/client/PythonClientService.java

Lines changed: 57 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import reactor.core.publisher.Mono;
1212
import reactor.util.retry.Retry;
1313

14+
import java.util.List;
1415
import java.time.Duration;
1516
import java.util.concurrent.CompletableFuture;
1617

@@ -134,39 +135,66 @@ public CompletableFuture<PythonSummaryResponse> requestNaverNewsSummary(String u
134135
.toFuture();
135136
}
136137

137-
/**
138-
* 네이버 뉴스 또는 일반 웹 콘텐츠 요약 요청
139-
*
140-
* @param url 네이버 뉴스 또는 일반 웹 URL
141-
* @param userMemo 사용자 메모 (분류 우선순위에 활용, 선택사항)
142-
* @return CompletableFuture<PythonSummaryResponse> 비동기 응답
143-
*/
144-
public CompletableFuture<PythonSummaryResponse> requestTistorySummary(String url, String userMemo) {
145-
log.info("Requesting Tistory summary from Python server: {}", url);
146-
if (userMemo != null && !userMemo.isEmpty()) {
147-
log.info("User memo provided: {}", userMemo);
138+
/**
139+
* Tistory 블로그 요약 요청
140+
*
141+
* @param url Tistory 블로그 URL
142+
* @param userMemo 사용자 메모 (분류 우선순위에 활용, 선택사항)
143+
* @return CompletableFuture<PythonSummaryResponse> 비동기 응답
144+
*/
145+
public CompletableFuture<PythonSummaryResponse> requestTistorySummary(String url, String userMemo) {
146+
log.info("Requesting Tistory summary from Python server: {}", url);
147+
if (userMemo != null && !userMemo.isEmpty()) {
148+
log.info("User memo provided: {}", userMemo);
149+
}
150+
151+
SummarizeNaverNewsRequest request = new SummarizeNaverNewsRequest(url, userMemo);
152+
153+
return pythonWebClient.post()
154+
.uri("/api/v1/summarize/tistory")
155+
.bodyValue(request)
156+
.retrieve()
157+
.bodyToMono(PythonSummaryResponse.class)
158+
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
159+
.maxBackoff(Duration.ofSeconds(5))
160+
.filter(throwable -> !(throwable instanceof WebClientResponseException.BadRequest)))
161+
.doOnSuccess(response -> log
162+
.info("Successfully received Tistory summary from Python server: {}",
163+
url))
164+
.doOnError(error -> log.error("Failed to get Tistory summary from Python server: {}",
165+
url,
166+
error))
167+
.toFuture();
148168
}
149169

150-
SummarizeNaverNewsRequest request = new SummarizeNaverNewsRequest(url, userMemo);
151-
152-
return pythonWebClient.post()
153-
.uri("/api/v1/summarize/tistory")
154-
.bodyValue(request)
155-
.retrieve()
156-
.bodyToMono(PythonSummaryResponse.class)
157-
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
158-
.maxBackoff(Duration.ofSeconds(5))
159-
.filter(throwable -> !(throwable instanceof WebClientResponseException.BadRequest)))
160-
.doOnSuccess(response -> log
161-
.info("Successfully received Tistory summary from Python server: {}",
162-
url))
163-
.doOnError(error -> log.error("Failed to get Tistory summary from Python server: {}",
164-
url,
165-
error))
166-
.toFuture();
167-
}
170+
/**
171+
* 컬렉션 요약 요청 (Small/Medium Card)
172+
*
173+
* @param newsletters 뉴스레터 제목/요약 목록
174+
* @return CompletableFuture<PythonSummaryResponse> 비동기 응답 (analysis 필드에 요약 포함)
175+
*/
176+
public CompletableFuture<PythonSummaryResponse> requestCollectionSummary(List<String> newsletters) {
177+
log.info("Requesting collection summary from Python server for {} items", newsletters.size());
178+
179+
return pythonWebClient.post()
180+
.uri("/api/v1/summarize/collection")
181+
.bodyValue(new CollectionSummaryRequest(newsletters))
182+
.retrieve()
183+
.bodyToMono(PythonSummaryResponse.class)
184+
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
185+
.maxBackoff(Duration.ofSeconds(5))
186+
.filter(throwable -> !(throwable instanceof WebClientResponseException.BadRequest)))
187+
.doOnSuccess(response -> log
188+
.info("Successfully received collection summary from Python server"))
189+
.doOnError(error -> log.error("Failed to get collection summary from Python server",
190+
error))
191+
.toFuture();
192+
}
168193

169194
// 내부 DTO
170195
private record GenericSummaryRequest(String title, String content) {
171196
}
197+
198+
private record CollectionSummaryRequest(List<String> newsletters) {
199+
}
172200
}

src/test/java/com/archiveat/server/domain/report/service/ReportServiceTest.java

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -44,29 +44,31 @@ void getWeeklyReport() {
4444

4545
// [Insight] 생성자 대신 정의된 @Builder를 사용하여 가독성 확보
4646
UserNewsletter un1 = UserNewsletter.builder()
47-
.depthType(DepthType.LIGHT)
48-
.perspectiveType(PerspectiveType.NOW)
49-
.lastViewedAt(LocalDateTime.now())
50-
.build();
47+
.depthType(DepthType.LIGHT)
48+
.perspectiveType(PerspectiveType.NOW)
49+
.lastViewedAt(LocalDateTime.now())
50+
.build();
5151

5252
UserNewsletter un2 = UserNewsletter.builder()
53-
.depthType(DepthType.DEEP)
54-
.perspectiveType(PerspectiveType.FUTURE)
55-
.lastViewedAt(LocalDateTime.now())
56-
.build();
53+
.depthType(DepthType.DEEP)
54+
.perspectiveType(PerspectiveType.FUTURE)
55+
.lastViewedAt(LocalDateTime.now())
56+
.build();
5757

5858
when(userNewsletterRepository.findByUserIdAndCreatedAtBetween(eq(userId), any(), any()))
59-
.thenReturn(List.of(un1));
59+
.thenReturn(List.of(un1));
60+
// Balance calculation now uses read newsletters, so un1 (Light) must be in this
61+
// list to pass assertThat(lightCount).isEqualTo(1)
6062
when(userNewsletterRepository.findByUserIdAndLastViewedAtBetweenAndIsReadTrue(eq(userId), any(), any()))
61-
.thenReturn(List.of(un2));
63+
.thenReturn(List.of(un1, un2));
6264

6365
// when
6466
WeeklyReportResponse response = reportService.getWeeklyReport(userId);
6567

6668
// then
6769
assertThat(response).isNotNull();
6870
assertThat(response.totalSavedCount()).isEqualTo(1);
69-
assertThat(response.totalReadCount()).isEqualTo(1);
71+
assertThat(response.totalReadCount()).isEqualTo(2);
7072
assertThat(response.lightCount()).isEqualTo(1); // un1 기준
7173
assertThat(response.nowCount()).isEqualTo(1);
7274
}
@@ -78,25 +80,25 @@ void getConsumption() {
7880
Long userId = 1L;
7981

8082
Newsletter newsletter = Newsletter.builder()
81-
.title("Test Title")
82-
.contentUrl("http://test.com")
83-
.build();
83+
.title("Test Title")
84+
.contentUrl("http://test.com")
85+
.build();
8486

8587
// [Reason] Newsletter 빌더에 없는 필드(id, category)만 리플렉션 사용
8688
ReflectionTestUtils.setField(newsletter, "id", 100L);
8789
ReflectionTestUtils.setField(newsletter, "category", "IT/Science");
8890

8991
UserNewsletter un = UserNewsletter.builder()
90-
.newsletter(newsletter)
91-
.lastViewedAt(LocalDateTime.now())
92-
.build();
92+
.newsletter(newsletter)
93+
.lastViewedAt(LocalDateTime.now())
94+
.build();
9395

9496
when(userNewsletterRepository.findByUserIdAndCreatedAtBetween(eq(userId), any(), any()))
95-
.thenReturn(Collections.emptyList());
97+
.thenReturn(Collections.emptyList());
9698
when(userNewsletterRepository.findByUserIdAndLastViewedAtBetweenAndIsReadTrue(eq(userId), any(), any()))
97-
.thenReturn(Collections.emptyList());
99+
.thenReturn(Collections.emptyList());
98100
when(userNewsletterRepository.findByUserIdAndIsReadTrueOrderByLastViewedAtDesc(userId))
99-
.thenReturn(List.of(un));
101+
.thenReturn(List.of(un));
100102

101103
// when
102104
ConsumptionResponse response = reportService.getConsumption(userId);
@@ -118,19 +120,19 @@ void getGapAnalysis_ShouldReturnTopicIds() {
118120
ReflectionTestUtils.setField(topic, "id", topicId); // ID는 수동 주입
119121

120122
UserNewsletter savedUn = UserNewsletter.builder()
121-
.topic(topic)
122-
.lastViewedAt(LocalDateTime.now())
123-
.build();
123+
.topic(topic)
124+
.lastViewedAt(LocalDateTime.now())
125+
.build();
124126

125127
UserNewsletter readUn = UserNewsletter.builder()
126-
.topic(topic)
127-
.lastViewedAt(LocalDateTime.now())
128-
.build();
128+
.topic(topic)
129+
.lastViewedAt(LocalDateTime.now())
130+
.build();
129131

130132
when(userNewsletterRepository.findByUserIdAndCreatedAtBetween(eq(userId), any(), any()))
131-
.thenReturn(List.of(savedUn, savedUn)); // 2개 저장
133+
.thenReturn(List.of(savedUn, savedUn)); // 2개 저장
132134
when(userNewsletterRepository.findByUserIdAndLastViewedAtBetweenAndIsReadTrue(eq(userId), any(), any()))
133-
.thenReturn(List.of(readUn)); // 1개 읽음
135+
.thenReturn(List.of(readUn)); // 1개 읽음
134136

135137
// when
136138
var response = reportService.getGapAnalysis(userId);
@@ -153,7 +155,7 @@ void getBalance_PatternCheck() {
153155
UserNewsletter deep = UserNewsletter.builder().depthType(DepthType.DEEP).build();
154156

155157
when(userNewsletterRepository.findByUserIdAndLastViewedAtBetweenAndIsReadTrue(eq(userId), any(), any()))
156-
.thenReturn(List.of(light, light, deep));
158+
.thenReturn(List.of(light, light, deep));
157159

158160
// when
159161
var response = reportService.getBalance(userId);
@@ -169,9 +171,9 @@ void getBalance_PatternCheck() {
169171
void getWeeklyReport_Empty() {
170172
// given
171173
when(userNewsletterRepository.findByUserIdAndCreatedAtBetween(any(), any(), any()))
172-
.thenReturn(Collections.emptyList());
174+
.thenReturn(Collections.emptyList());
173175
when(userNewsletterRepository.findByUserIdAndLastViewedAtBetweenAndIsReadTrue(any(), any(), any()))
174-
.thenReturn(Collections.emptyList());
176+
.thenReturn(Collections.emptyList());
175177

176178
// when
177179
WeeklyReportResponse response = reportService.getWeeklyReport(1L);

0 commit comments

Comments
 (0)