From a8236a430f1fa086c8684542000ded3ff509292a Mon Sep 17 00:00:00 2001 From: Jansoon Date: Tue, 6 Jan 2026 07:41:28 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9A=94=EC=95=BD=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20AI=20=EC=84=9C=EB=B2=84=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#166)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sofa/linkiving/LinkivingApplication.java | 2 +- .../domain/link/ai/AiSummaryClient.java | 22 ---- .../domain/link/ai/MockAiSummaryClient.java | 41 ------ .../domain/link/ai/MockSummaryClient.java | 22 ++++ .../domain/link/ai/RagSummaryClient.java | 61 +++++++++ .../domain/link/ai/RagSummaryFeign.java | 22 ++++ .../domain/link/ai/SummaryClient.java | 26 ++++ .../domain/link/controller/LinkApi.java | 12 +- .../link/controller/LinkController.java | 16 +-- .../dto/request/RagInitialSummaryReq.java | 10 ++ .../dto/request/RagRegenerateSummaryReq.java | 9 ++ .../dto/request/RegenerateSummaryReq.java | 13 ++ .../dto/response/RagInitialSummaryRes.java | 6 + .../dto/response/RagRegenerateSummaryRes.java | 7 + ...esponse.java => RegenerateSummaryRes.java} | 4 +- .../domain/link/facade/LinkFacade.java | 27 ++-- .../link/service/SummaryCommandService.java | 26 ++++ .../domain/link/service/SummaryService.java | 19 ++- .../link/ai/MockAiSummaryClientTest.java | 44 ------- .../domain/link/ai/MockSummaryClientTest.java | 34 +++++ .../domain/link/ai/RagSummaryClientTest.java | 124 ++++++++++++++++++ .../domain/link/facade/LinkFacadeTest.java | 73 ++++++++--- .../integration/LinkApiIntegrationTest.java | 30 ++--- .../service/SummaryCommandServiceTest.java | 67 ++++++++++ .../link/service/SummaryServiceTest.java | 56 ++++---- 25 files changed, 563 insertions(+), 210 deletions(-) delete mode 100644 src/main/java/com/sofa/linkiving/domain/link/ai/AiSummaryClient.java delete mode 100644 src/main/java/com/sofa/linkiving/domain/link/ai/MockAiSummaryClient.java create mode 100644 src/main/java/com/sofa/linkiving/domain/link/ai/MockSummaryClient.java create mode 100644 src/main/java/com/sofa/linkiving/domain/link/ai/RagSummaryClient.java create mode 100644 src/main/java/com/sofa/linkiving/domain/link/ai/RagSummaryFeign.java create mode 100644 src/main/java/com/sofa/linkiving/domain/link/ai/SummaryClient.java create mode 100644 src/main/java/com/sofa/linkiving/domain/link/dto/request/RagInitialSummaryReq.java create mode 100644 src/main/java/com/sofa/linkiving/domain/link/dto/request/RagRegenerateSummaryReq.java create mode 100644 src/main/java/com/sofa/linkiving/domain/link/dto/request/RegenerateSummaryReq.java create mode 100644 src/main/java/com/sofa/linkiving/domain/link/dto/response/RagInitialSummaryRes.java create mode 100644 src/main/java/com/sofa/linkiving/domain/link/dto/response/RagRegenerateSummaryRes.java rename src/main/java/com/sofa/linkiving/domain/link/dto/response/{RecreateSummaryResponse.java => RegenerateSummaryRes.java} (84%) create mode 100644 src/main/java/com/sofa/linkiving/domain/link/service/SummaryCommandService.java delete mode 100644 src/test/java/com/sofa/linkiving/domain/link/ai/MockAiSummaryClientTest.java create mode 100644 src/test/java/com/sofa/linkiving/domain/link/ai/MockSummaryClientTest.java create mode 100644 src/test/java/com/sofa/linkiving/domain/link/ai/RagSummaryClientTest.java create mode 100644 src/test/java/com/sofa/linkiving/domain/link/service/SummaryCommandServiceTest.java diff --git a/src/main/java/com/sofa/linkiving/LinkivingApplication.java b/src/main/java/com/sofa/linkiving/LinkivingApplication.java index b7319694..12c429ce 100644 --- a/src/main/java/com/sofa/linkiving/LinkivingApplication.java +++ b/src/main/java/com/sofa/linkiving/LinkivingApplication.java @@ -7,7 +7,7 @@ @SpringBootApplication @EnableJpaAuditing -@EnableFeignClients(basePackages = "com.sofa.linkiving.infra.feign") +@EnableFeignClients public class LinkivingApplication { public static void main(String[] args) { diff --git a/src/main/java/com/sofa/linkiving/domain/link/ai/AiSummaryClient.java b/src/main/java/com/sofa/linkiving/domain/link/ai/AiSummaryClient.java deleted file mode 100644 index afd3449a..00000000 --- a/src/main/java/com/sofa/linkiving/domain/link/ai/AiSummaryClient.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.sofa.linkiving.domain.link.ai; - -import com.sofa.linkiving.domain.link.enums.Format; - -public interface AiSummaryClient { - /** - * AI 서버에 요약 요청을 보냅니다. - * @param linkId 링크 ID - * @param url 요약할 URL - * @param format 요약 모드 - * @return 요약된 텍스트 - */ - String generateSummary(Long linkId, String url, Format format); - - /** - * 기존 요약과 신규 요약 내용을 비교합니다. - * @param existingSummary 기존 요약 - * @param newSummary 신규 요약 - * @return 요약 비교 정보 - */ - String comparisonSummary(String existingSummary, String newSummary); -} diff --git a/src/main/java/com/sofa/linkiving/domain/link/ai/MockAiSummaryClient.java b/src/main/java/com/sofa/linkiving/domain/link/ai/MockAiSummaryClient.java deleted file mode 100644 index 55960d4d..00000000 --- a/src/main/java/com/sofa/linkiving/domain/link/ai/MockAiSummaryClient.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.sofa.linkiving.domain.link.ai; - -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Component; - -import com.sofa.linkiving.domain.link.enums.Format; - -@Component -@Primary -public class MockAiSummaryClient implements AiSummaryClient { - - @Override - public String generateSummary(Long linkId, String url, Format format) { - - if (format == Format.DETAILED) { - return """ - [자세한 요약 (Mock)] - OpenFeign 도입을 대비하여 Interface 기반 설계를 적용했습니다. - 1. AiSummaryClient 인터페이스를 정의하여 의존성을 역전시켰습니다. - 2. 현재는 MockAiSummaryClient가 동작하지만, 추후 실제 구현체로 교체하기 쉽습니다. - 3. 비즈니스 로직은 AI 서버의 통신 방식(HTTP, gRPC 등)에 영향을 받지 않습니다. - """; - } else { - return """ - [간결한 요약 (Mock)] - OpenFeign 도입을 위해 인터페이스 패턴을 적용하여, 코드 수정 없이 구현체 교체가 가능한 확장성 있는 구조를 만들었습니다. - """; - } - } - - @Override - public String comparisonSummary(String existingSummary, String newSummary) { - return """ - [변경 사항 분석] - 기존 요약 대비 다음 내용이 보강되었습니다: - - AI 아키텍처 설계 방식에 대한 구체적인 설명 추가 - - OpenFeign 도입의 이점 명시 - (이 내용은 Mock 데이터이며, 실제 AI는 두 텍스트의 차이를 분석하여 제공합니다.) - """; - } -} diff --git a/src/main/java/com/sofa/linkiving/domain/link/ai/MockSummaryClient.java b/src/main/java/com/sofa/linkiving/domain/link/ai/MockSummaryClient.java new file mode 100644 index 00000000..300ddbcb --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/link/ai/MockSummaryClient.java @@ -0,0 +1,22 @@ +package com.sofa.linkiving.domain.link.ai; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import com.sofa.linkiving.domain.link.dto.response.RagInitialSummaryRes; +import com.sofa.linkiving.domain.link.dto.response.RagRegenerateSummaryRes; + +@Component +@Profile("test") +public class MockSummaryClient implements SummaryClient { + + @Override + public RagInitialSummaryRes initialSummary(Long linkId, Long userId, String title, String url, String memo) { + return new RagInitialSummaryRes("최초 요약"); + } + + @Override + public RagRegenerateSummaryRes regenerateSummary(Long linkId, Long userId, String url, String existingSummary) { + return new RagRegenerateSummaryRes("신규 요약", "비교 사항"); + } +} diff --git a/src/main/java/com/sofa/linkiving/domain/link/ai/RagSummaryClient.java b/src/main/java/com/sofa/linkiving/domain/link/ai/RagSummaryClient.java new file mode 100644 index 00000000..4ebb24ae --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/link/ai/RagSummaryClient.java @@ -0,0 +1,61 @@ +package com.sofa.linkiving.domain.link.ai; + +import java.util.List; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import com.sofa.linkiving.domain.link.dto.request.RagInitialSummaryReq; +import com.sofa.linkiving.domain.link.dto.request.RagRegenerateSummaryReq; +import com.sofa.linkiving.domain.link.dto.response.RagInitialSummaryRes; +import com.sofa.linkiving.domain.link.dto.response.RagRegenerateSummaryRes; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@Profile("!test") +@RequiredArgsConstructor +public class RagSummaryClient implements SummaryClient { + + private final RagSummaryFeign ragSummaryFeign; + + @Override + public RagInitialSummaryRes initialSummary(Long linkId, Long userId, String title, String url, String memo) { + try { + RagInitialSummaryReq req = new RagInitialSummaryReq(linkId, userId, title, url, memo); + List response = ragSummaryFeign.requestInitialSummary(req); + + if (response != null && !response.isEmpty()) { + log.info("[AI Server] Initial Summary Requested Success. LinkId: {}", linkId); + return response.get(0); + } + return null; + + } catch (Exception e) { + log.error("[AI Server Error] Failed to request initial summary for LinkId: {}. Error: {}", linkId, + e.getMessage()); + return null; + } + } + + @Override + public RagRegenerateSummaryRes regenerateSummary(Long linkId, Long userId, String url, String existingSummary) { + try { + RagRegenerateSummaryReq req = new RagRegenerateSummaryReq(linkId, userId, url, existingSummary); + List response = ragSummaryFeign.requestRegenerateSummary(req); + + if (response != null && !response.isEmpty()) { + log.info("[AI Server] Regenerate Summary Success. LinkId: {}", linkId); + return response.get(0); + } + return null; + + } catch (Exception e) { + log.error("[AI Server Error] Failed to regenerate summary for LinkId: {}. Error: {}", linkId, + e.getMessage()); + return null; + } + } +} diff --git a/src/main/java/com/sofa/linkiving/domain/link/ai/RagSummaryFeign.java b/src/main/java/com/sofa/linkiving/domain/link/ai/RagSummaryFeign.java new file mode 100644 index 00000000..93c389f1 --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/link/ai/RagSummaryFeign.java @@ -0,0 +1,22 @@ +package com.sofa.linkiving.domain.link.ai; + +import java.util.List; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import com.sofa.linkiving.domain.link.dto.request.RagInitialSummaryReq; +import com.sofa.linkiving.domain.link.dto.request.RagRegenerateSummaryReq; +import com.sofa.linkiving.domain.link.dto.response.RagInitialSummaryRes; +import com.sofa.linkiving.domain.link.dto.response.RagRegenerateSummaryRes; +import com.sofa.linkiving.infra.feign.GlobalFeignConfig; + +@FeignClient(name = "ai-summary-client", url = "${ai.server.url}", configuration = GlobalFeignConfig.class) +public interface RagSummaryFeign { + @PostMapping("/webhook/summary-initial") + List requestInitialSummary(@RequestBody RagInitialSummaryReq req); + + @PostMapping("/webhook/summary-resummarize") + List requestRegenerateSummary(@RequestBody RagRegenerateSummaryReq request); +} diff --git a/src/main/java/com/sofa/linkiving/domain/link/ai/SummaryClient.java b/src/main/java/com/sofa/linkiving/domain/link/ai/SummaryClient.java new file mode 100644 index 00000000..a96268ee --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/link/ai/SummaryClient.java @@ -0,0 +1,26 @@ +package com.sofa.linkiving.domain.link.ai; + +import com.sofa.linkiving.domain.link.dto.response.RagInitialSummaryRes; +import com.sofa.linkiving.domain.link.dto.response.RagRegenerateSummaryRes; + +public interface SummaryClient { + /** + * AI 서버에 최초 요약 요청을 보냅니다. + * @param linkId 링크 ID + * @param userId 유저 ID + * @param url 요약할 URL + * @param title 제목 + * @param memo 메모 + * @return 요약된 텍스트 + */ + RagInitialSummaryRes initialSummary(Long linkId, Long userId, String title, String url, String memo); + + /** + * 요약 재생성 및 기존 요약과의 차이점 요청을 보냅니다. + * @param linkId 링크 ID + * @param userId 유저 ID + * @param url 요약할 URL + * @return 요약 비교 정보 + */ + RagRegenerateSummaryRes regenerateSummary(Long linkId, Long userId, String url, String existingSummary); +} diff --git a/src/main/java/com/sofa/linkiving/domain/link/controller/LinkApi.java b/src/main/java/com/sofa/linkiving/domain/link/controller/LinkApi.java index d7213448..51b89de7 100644 --- a/src/main/java/com/sofa/linkiving/domain/link/controller/LinkApi.java +++ b/src/main/java/com/sofa/linkiving/domain/link/controller/LinkApi.java @@ -7,19 +7,18 @@ import com.sofa.linkiving.domain.link.dto.request.LinkTitleUpdateReq; import com.sofa.linkiving.domain.link.dto.request.LinkUpdateReq; import com.sofa.linkiving.domain.link.dto.request.MetaScrapeReq; +import com.sofa.linkiving.domain.link.dto.request.RegenerateSummaryReq; import com.sofa.linkiving.domain.link.dto.response.LinkCardsRes; import com.sofa.linkiving.domain.link.dto.response.LinkDetailRes; import com.sofa.linkiving.domain.link.dto.response.LinkDuplicateCheckRes; import com.sofa.linkiving.domain.link.dto.response.LinkRes; import com.sofa.linkiving.domain.link.dto.response.MetaScrapeRes; -import com.sofa.linkiving.domain.link.dto.response.RecreateSummaryResponse; -import com.sofa.linkiving.domain.link.enums.Format; +import com.sofa.linkiving.domain.link.dto.response.RegenerateSummaryRes; import com.sofa.linkiving.domain.member.entity.Member; import com.sofa.linkiving.global.common.BaseResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; @@ -42,7 +41,7 @@ BaseResponse checkDuplicate( ); @Operation(summary = "링크 생성", description = "새로운 링크를 저장합니다") - BaseResponse createLink( + BaseResponse createLink( @Valid LinkCreateReq request, Member member ); @@ -92,10 +91,9 @@ BaseResponse updateMemo( ); @Operation(summary = "요약 재생성", description = "요약을 재생성 하고 신규 요약 기존 요약, 기존 및 신규 요약 비교 정보을 제공합니다.") - BaseResponse recreateSummary( + BaseResponse recreateSummary( Long id, - @Schema(description = "요청 형식(CONCISE: 간결하게, DETAILED:자세하게)") - @Valid Format format, + @Valid RegenerateSummaryReq req, Member member ); } diff --git a/src/main/java/com/sofa/linkiving/domain/link/controller/LinkController.java b/src/main/java/com/sofa/linkiving/domain/link/controller/LinkController.java index 408180ae..c166e0a2 100644 --- a/src/main/java/com/sofa/linkiving/domain/link/controller/LinkController.java +++ b/src/main/java/com/sofa/linkiving/domain/link/controller/LinkController.java @@ -16,13 +16,13 @@ import com.sofa.linkiving.domain.link.dto.request.LinkTitleUpdateReq; import com.sofa.linkiving.domain.link.dto.request.LinkUpdateReq; import com.sofa.linkiving.domain.link.dto.request.MetaScrapeReq; +import com.sofa.linkiving.domain.link.dto.request.RegenerateSummaryReq; import com.sofa.linkiving.domain.link.dto.response.LinkCardsRes; import com.sofa.linkiving.domain.link.dto.response.LinkDetailRes; import com.sofa.linkiving.domain.link.dto.response.LinkDuplicateCheckRes; import com.sofa.linkiving.domain.link.dto.response.LinkRes; import com.sofa.linkiving.domain.link.dto.response.MetaScrapeRes; -import com.sofa.linkiving.domain.link.dto.response.RecreateSummaryResponse; -import com.sofa.linkiving.domain.link.enums.Format; +import com.sofa.linkiving.domain.link.dto.response.RegenerateSummaryRes; import com.sofa.linkiving.domain.link.facade.LinkFacade; import com.sofa.linkiving.domain.member.entity.Member; import com.sofa.linkiving.global.common.BaseResponse; @@ -59,11 +59,11 @@ public BaseResponse checkDuplicate( @Override @PostMapping - public BaseResponse createLink( + public BaseResponse createLink( @RequestBody LinkCreateReq request, @AuthMember Member member ) { - LinkRes response = linkFacade.createLink( + LinkDetailRes response = linkFacade.createLink( member, request.url(), request.title(), @@ -143,13 +143,13 @@ public BaseResponse updateMemo( } @Override - @GetMapping("/{id}/summary") - public BaseResponse recreateSummary( + @PostMapping("/{id}/summary") + public BaseResponse recreateSummary( @PathVariable Long id, - @RequestParam Format format, + @RequestBody RegenerateSummaryReq req, @AuthMember Member member ) { - RecreateSummaryResponse response = linkFacade.recreateSummary(member, id, format); + RegenerateSummaryRes response = linkFacade.recreateSummary(member, id, req.format()); return BaseResponse.success(response, "요약 재성성 완료"); } } diff --git a/src/main/java/com/sofa/linkiving/domain/link/dto/request/RagInitialSummaryReq.java b/src/main/java/com/sofa/linkiving/domain/link/dto/request/RagInitialSummaryReq.java new file mode 100644 index 00000000..b3472819 --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/link/dto/request/RagInitialSummaryReq.java @@ -0,0 +1,10 @@ +package com.sofa.linkiving.domain.link.dto.request; + +public record RagInitialSummaryReq( + Long linkId, + Long userId, + String title, + String url, + String memo +) { +} diff --git a/src/main/java/com/sofa/linkiving/domain/link/dto/request/RagRegenerateSummaryReq.java b/src/main/java/com/sofa/linkiving/domain/link/dto/request/RagRegenerateSummaryReq.java new file mode 100644 index 00000000..70c9e609 --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/link/dto/request/RagRegenerateSummaryReq.java @@ -0,0 +1,9 @@ +package com.sofa.linkiving.domain.link.dto.request; + +public record RagRegenerateSummaryReq( + Long linkId, + Long userId, + String url, + String summary +) { +} diff --git a/src/main/java/com/sofa/linkiving/domain/link/dto/request/RegenerateSummaryReq.java b/src/main/java/com/sofa/linkiving/domain/link/dto/request/RegenerateSummaryReq.java new file mode 100644 index 00000000..67f83e4c --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/link/dto/request/RegenerateSummaryReq.java @@ -0,0 +1,13 @@ +package com.sofa.linkiving.domain.link.dto.request; + +import com.sofa.linkiving.domain.link.enums.Format; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Builder +public record RegenerateSummaryReq( + @Schema(description = "요청 형식(CONCISE: 간결하게, DETAILED:자세하게)") + Format format +) { +} diff --git a/src/main/java/com/sofa/linkiving/domain/link/dto/response/RagInitialSummaryRes.java b/src/main/java/com/sofa/linkiving/domain/link/dto/response/RagInitialSummaryRes.java new file mode 100644 index 00000000..38138156 --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/link/dto/response/RagInitialSummaryRes.java @@ -0,0 +1,6 @@ +package com.sofa.linkiving.domain.link.dto.response; + +public record RagInitialSummaryRes( + String summary +) { +} diff --git a/src/main/java/com/sofa/linkiving/domain/link/dto/response/RagRegenerateSummaryRes.java b/src/main/java/com/sofa/linkiving/domain/link/dto/response/RagRegenerateSummaryRes.java new file mode 100644 index 00000000..fbd7dd2e --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/link/dto/response/RagRegenerateSummaryRes.java @@ -0,0 +1,7 @@ +package com.sofa.linkiving.domain.link.dto.response; + +public record RagRegenerateSummaryRes( + String summary, + String difference +) { +} diff --git a/src/main/java/com/sofa/linkiving/domain/link/dto/response/RecreateSummaryResponse.java b/src/main/java/com/sofa/linkiving/domain/link/dto/response/RegenerateSummaryRes.java similarity index 84% rename from src/main/java/com/sofa/linkiving/domain/link/dto/response/RecreateSummaryResponse.java rename to src/main/java/com/sofa/linkiving/domain/link/dto/response/RegenerateSummaryRes.java index 807b662b..6ef0bbb7 100644 --- a/src/main/java/com/sofa/linkiving/domain/link/dto/response/RecreateSummaryResponse.java +++ b/src/main/java/com/sofa/linkiving/domain/link/dto/response/RegenerateSummaryRes.java @@ -4,12 +4,12 @@ import lombok.Builder; @Builder -public record RecreateSummaryResponse( +public record RegenerateSummaryRes( @Schema(description = "기존 요약") String existingSummary, @Schema(description = "신규 요약") String newSummary, @Schema(description = "비교 정보") - String comparison + String difference ) { } diff --git a/src/main/java/com/sofa/linkiving/domain/link/facade/LinkFacade.java b/src/main/java/com/sofa/linkiving/domain/link/facade/LinkFacade.java index 2bd06075..ab6b6ccb 100644 --- a/src/main/java/com/sofa/linkiving/domain/link/facade/LinkFacade.java +++ b/src/main/java/com/sofa/linkiving/domain/link/facade/LinkFacade.java @@ -4,6 +4,7 @@ import org.springframework.transaction.annotation.Transactional; import com.sofa.linkiving.domain.link.abstraction.ImageUploader; +import com.sofa.linkiving.domain.link.ai.SummaryClient; import com.sofa.linkiving.domain.link.dto.internal.LinkDto; import com.sofa.linkiving.domain.link.dto.internal.LinksDto; import com.sofa.linkiving.domain.link.dto.internal.OgTagDto; @@ -12,8 +13,11 @@ import com.sofa.linkiving.domain.link.dto.response.LinkDuplicateCheckRes; import com.sofa.linkiving.domain.link.dto.response.LinkRes; import com.sofa.linkiving.domain.link.dto.response.MetaScrapeRes; -import com.sofa.linkiving.domain.link.dto.response.RecreateSummaryResponse; +import com.sofa.linkiving.domain.link.dto.response.RagInitialSummaryRes; +import com.sofa.linkiving.domain.link.dto.response.RagRegenerateSummaryRes; +import com.sofa.linkiving.domain.link.dto.response.RegenerateSummaryRes; import com.sofa.linkiving.domain.link.entity.Link; +import com.sofa.linkiving.domain.link.entity.Summary; import com.sofa.linkiving.domain.link.enums.Format; import com.sofa.linkiving.domain.link.service.LinkService; import com.sofa.linkiving.domain.link.service.SummaryService; @@ -31,11 +35,16 @@ public class LinkFacade { private final OgTagCrawler ogTagCrawler; private final SummaryService summaryService; private final ImageUploader imageUploader; + private final SummaryClient summaryClient; - public LinkRes createLink(Member member, String url, String title, String memo, String imageUrl) { + public LinkDetailRes createLink(Member member, String url, String title, String memo, String imageUrl) { String storedImageUrl = imageUploader.uploadFromUrl(imageUrl); Link link = linkService.createLink(member, url, title, memo, storedImageUrl); - return LinkRes.from(link); + RagInitialSummaryRes res = summaryClient.initialSummary(link.getId(), member.getId(), + link.getTitle(), link.getUrl(), link.getMemo()); + Summary summary = summaryService.createSummary(link, Format.CONCISE, res.summary()); + + return LinkDetailRes.of(link, summary); } public LinkRes updateLink(Long linkId, Member member, String title, String memo) { @@ -77,19 +86,17 @@ public LinkDuplicateCheckRes checkDuplicate(Member member, String url) { } @Transactional(readOnly = true) - public RecreateSummaryResponse recreateSummary(Member member, Long linkId, Format format) { + public RegenerateSummaryRes recreateSummary(Member member, Long linkId, Format format) { String url = linkService.getLink(linkId, member).getUrl(); - String existingSummary = summaryService.getSummary(linkId).getContent(); - String newSummary = summaryService.createSummary(linkId, url, format); - String comparison = summaryService.comparisonSummary(existingSummary, newSummary); + RagRegenerateSummaryRes res = summaryClient.regenerateSummary(linkId, member.getId(), url, existingSummary); - return RecreateSummaryResponse.builder() + return RegenerateSummaryRes.builder() .existingSummary(existingSummary) - .newSummary(newSummary) - .comparison(comparison) + .newSummary(res.summary()) + .difference(res.difference()) .build(); } diff --git a/src/main/java/com/sofa/linkiving/domain/link/service/SummaryCommandService.java b/src/main/java/com/sofa/linkiving/domain/link/service/SummaryCommandService.java new file mode 100644 index 00000000..4b295958 --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/link/service/SummaryCommandService.java @@ -0,0 +1,26 @@ +package com.sofa.linkiving.domain.link.service; + +import org.springframework.stereotype.Service; + +import com.sofa.linkiving.domain.link.entity.Link; +import com.sofa.linkiving.domain.link.entity.Summary; +import com.sofa.linkiving.domain.link.enums.Format; +import com.sofa.linkiving.domain.link.repository.SummaryRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class SummaryCommandService { + private final SummaryRepository summaryRepository; + + public Summary save(Link link, Format format, String content) { + return summaryRepository.save( + Summary.builder() + .link(link) + .format(format) + .content(content) + .build() + ); + } +} diff --git a/src/main/java/com/sofa/linkiving/domain/link/service/SummaryService.java b/src/main/java/com/sofa/linkiving/domain/link/service/SummaryService.java index f867945a..e9aa40be 100644 --- a/src/main/java/com/sofa/linkiving/domain/link/service/SummaryService.java +++ b/src/main/java/com/sofa/linkiving/domain/link/service/SummaryService.java @@ -1,28 +1,27 @@ package com.sofa.linkiving.domain.link.service; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import com.sofa.linkiving.domain.link.ai.AiSummaryClient; +import com.sofa.linkiving.domain.link.entity.Link; import com.sofa.linkiving.domain.link.entity.Summary; import com.sofa.linkiving.domain.link.enums.Format; import lombok.RequiredArgsConstructor; @Service +@Transactional @RequiredArgsConstructor public class SummaryService { private final SummaryQueryService summaryQueryService; - private final AiSummaryClient aiSummaryClient; - - public String createSummary(Long linkId, String url, Format format) { - return aiSummaryClient.generateSummary(linkId, url, format); - } - - public String comparisonSummary(String existingSummary, String newSummary) { - return aiSummaryClient.comparisonSummary(existingSummary, newSummary); - } + private final SummaryCommandService summaryCommandService; + @Transactional(readOnly = true) public Summary getSummary(Long linkId) { return summaryQueryService.getSummary(linkId); } + + public Summary createSummary(Link link, Format format, String summary) { + return summaryCommandService.save(link, format, summary); + } } diff --git a/src/test/java/com/sofa/linkiving/domain/link/ai/MockAiSummaryClientTest.java b/src/test/java/com/sofa/linkiving/domain/link/ai/MockAiSummaryClientTest.java deleted file mode 100644 index 1bc5ce71..00000000 --- a/src/test/java/com/sofa/linkiving/domain/link/ai/MockAiSummaryClientTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.sofa.linkiving.domain.link.ai; - -import static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import com.sofa.linkiving.domain.link.enums.Format; - -public class MockAiSummaryClientTest { - private final MockAiSummaryClient client = new MockAiSummaryClient(); - - @Test - @DisplayName("DETAILED 포맷 요청 시 상세 요약 텍스트 반환") - void generateSummary_Detailed() { - // when - String result = client.generateSummary(1L, "url", Format.DETAILED); - - // then - assertThat(result).contains("[자세한 요약 (Mock)]"); - assertThat(result).contains("OpenFeign 도입"); - } - - @Test - @DisplayName("SIMPLE 포맷 요청 시 간결한 요약 텍스트 반환") - void generateSummary_Simple() { - // when - String result = client.generateSummary(1L, "url", Format.CONCISE); - - // then - assertThat(result).contains("[간결한 요약 (Mock)]"); - } - - @Test - @DisplayName("comparisonSummary 호출 시 변경 사항 분석 텍스트 반환") - void comparisonSummary() { - // when - String result = client.comparisonSummary("old", "new"); - - // then - assertThat(result).contains("[변경 사항 분석]"); - assertThat(result).contains("보강되었습니다"); - } -} diff --git a/src/test/java/com/sofa/linkiving/domain/link/ai/MockSummaryClientTest.java b/src/test/java/com/sofa/linkiving/domain/link/ai/MockSummaryClientTest.java new file mode 100644 index 00000000..8b10726d --- /dev/null +++ b/src/test/java/com/sofa/linkiving/domain/link/ai/MockSummaryClientTest.java @@ -0,0 +1,34 @@ +package com.sofa.linkiving.domain.link.ai; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.sofa.linkiving.domain.link.dto.response.RagInitialSummaryRes; +import com.sofa.linkiving.domain.link.dto.response.RagRegenerateSummaryRes; + +public class MockSummaryClientTest { + private final MockSummaryClient client = new MockSummaryClient(); + + @Test + @DisplayName("generateSummary 호출 시 최초 요약 진행") + void initialSummary_Detailed() { + // when + RagInitialSummaryRes result = client.initialSummary(1L, 1L, "title", "url", "memo"); + + // then + assertThat(result.summary()).isEqualTo("최초 요약"); + } + + @Test + @DisplayName("comparisonSummary 호출 시 변경 사항 분석 텍스트 반환") + void comparisonSummary() { + // when + RagRegenerateSummaryRes result = client.regenerateSummary(1L, 1L, "url", "old"); + + // then + assertThat(result.summary()).isEqualTo("신규 요약"); + assertThat(result.difference()).contains("비교 사항"); + } +} diff --git a/src/test/java/com/sofa/linkiving/domain/link/ai/RagSummaryClientTest.java b/src/test/java/com/sofa/linkiving/domain/link/ai/RagSummaryClientTest.java new file mode 100644 index 00000000..71f9d22a --- /dev/null +++ b/src/test/java/com/sofa/linkiving/domain/link/ai/RagSummaryClientTest.java @@ -0,0 +1,124 @@ +package com.sofa.linkiving.domain.link.ai; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.*; + +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.sofa.linkiving.domain.link.dto.request.RagInitialSummaryReq; +import com.sofa.linkiving.domain.link.dto.request.RagRegenerateSummaryReq; +import com.sofa.linkiving.domain.link.dto.response.RagInitialSummaryRes; +import com.sofa.linkiving.domain.link.dto.response.RagRegenerateSummaryRes; + +@ExtendWith(MockitoExtension.class) +@DisplayName("RagSummaryClient 단위 테스트") +public class RagSummaryClientTest { + + @InjectMocks + private RagSummaryClient ragSummaryClient; + + @Mock + private RagSummaryFeign ragSummaryFeign; + + @Test + @DisplayName("최초 요약 요청 성공 시 응답 객체를 반환한다") + void shouldReturnInitialSummaryResWhenSuccess() { + // given + Long linkId = 1L; + Long userId = 100L; + String title = "Test Title"; + String url = "https://test.com"; + String memo = "Test Memo"; + + RagInitialSummaryRes expectedRes = new RagInitialSummaryRes("요약 내용"); + List responseList = List.of(expectedRes); + + given(ragSummaryFeign.requestInitialSummary(any(RagInitialSummaryReq.class))) + .willReturn(responseList); + + // when + RagInitialSummaryRes result = ragSummaryClient.initialSummary(linkId, userId, title, url, memo); + + // then + assertThat(result).isNotNull(); + assertThat(result).isEqualTo(expectedRes); + + verify(ragSummaryFeign, times(1)).requestInitialSummary(any(RagInitialSummaryReq.class)); + } + + @Test + @DisplayName("최초 요약 요청 시 응답이 비어있으면 null을 반환한다") + void shouldReturnNullWhenInitialSummaryResponseIsEmpty() { + // given + given(ragSummaryFeign.requestInitialSummary(any(RagInitialSummaryReq.class))) + .willReturn(Collections.emptyList()); + + // when + RagInitialSummaryRes result = ragSummaryClient.initialSummary(1L, 100L, "Title", "URL", "Memo"); + + // then + assertThat(result).isNull(); + } + + @Test + @DisplayName("최초 요약 요청 중 예외 발생 시 로그를 남기고 null을 반환한다") + void shouldReturnNullWhenInitialSummaryThrowsException() { + // given + given(ragSummaryFeign.requestInitialSummary(any(RagInitialSummaryReq.class))) + .willThrow(new RuntimeException("AI Server Error")); + + // when + RagInitialSummaryRes result = ragSummaryClient.initialSummary(1L, 100L, "Title", "URL", "Memo"); + + // then + assertThat(result).isNull(); + } + + @Test + @DisplayName("요약 재생성 요청 성공 시 응답 객체를 반환한다") + void shouldReturnRegenerateSummaryResWhenSuccess() { + // given + Long linkId = 1L; + Long userId = 100L; + String url = "https://test.com"; + String existingSummary = "Old Summary"; + + RagRegenerateSummaryRes expectedRes = new RagRegenerateSummaryRes("New Summary", "Difference"); + List responseList = List.of(expectedRes); + + given(ragSummaryFeign.requestRegenerateSummary(any(RagRegenerateSummaryReq.class))) + .willReturn(responseList); + + // when + RagRegenerateSummaryRes result = ragSummaryClient.regenerateSummary(linkId, userId, url, existingSummary); + + // then + assertThat(result).isNotNull(); + assertThat(result).isEqualTo(expectedRes); + + verify(ragSummaryFeign, times(1)).requestRegenerateSummary(any(RagRegenerateSummaryReq.class)); + } + + @Test + @DisplayName("요약 재생성 요청 중 예외 발생 시 null을 반환한다") + void shouldReturnNullWhenRegenerateSummaryThrowsException() { + // given + given(ragSummaryFeign.requestRegenerateSummary(any(RagRegenerateSummaryReq.class))) + .willThrow(new RuntimeException("Connection Timeout")); + + // when + RagRegenerateSummaryRes result = ragSummaryClient.regenerateSummary(1L, 100L, "URL", "Old"); + + // then + assertThat(result).isNull(); + } +} diff --git a/src/test/java/com/sofa/linkiving/domain/link/facade/LinkFacadeTest.java b/src/test/java/com/sofa/linkiving/domain/link/facade/LinkFacadeTest.java index 9280a7ba..f637781a 100644 --- a/src/test/java/com/sofa/linkiving/domain/link/facade/LinkFacadeTest.java +++ b/src/test/java/com/sofa/linkiving/domain/link/facade/LinkFacadeTest.java @@ -14,15 +14,19 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.test.util.ReflectionTestUtils; import com.sofa.linkiving.domain.link.abstraction.ImageUploader; +import com.sofa.linkiving.domain.link.ai.SummaryClient; import com.sofa.linkiving.domain.link.dto.internal.LinkDto; import com.sofa.linkiving.domain.link.dto.internal.LinksDto; import com.sofa.linkiving.domain.link.dto.internal.OgTagDto; import com.sofa.linkiving.domain.link.dto.response.LinkCardsRes; -import com.sofa.linkiving.domain.link.dto.response.LinkRes; +import com.sofa.linkiving.domain.link.dto.response.LinkDetailRes; import com.sofa.linkiving.domain.link.dto.response.MetaScrapeRes; -import com.sofa.linkiving.domain.link.dto.response.RecreateSummaryResponse; +import com.sofa.linkiving.domain.link.dto.response.RagInitialSummaryRes; +import com.sofa.linkiving.domain.link.dto.response.RagRegenerateSummaryRes; +import com.sofa.linkiving.domain.link.dto.response.RegenerateSummaryRes; import com.sofa.linkiving.domain.link.entity.Link; import com.sofa.linkiving.domain.link.entity.Summary; import com.sofa.linkiving.domain.link.enums.Format; @@ -59,12 +63,15 @@ public class LinkFacadeTest { @Mock private ImageUploader imageUploader; + @Mock + private SummaryClient summaryClient; + @Mock private ApplicationEventPublisher eventPublisher; @BeforeEach void setUp() { - linkFacade = new LinkFacade(linkService, ogTagCrawler, summaryService, imageUploader); + linkFacade = new LinkFacade(linkService, ogTagCrawler, summaryService, imageUploader, summaryClient); } @Test @@ -98,12 +105,16 @@ void shouldReturnMetaScrapeResWhenCrawlSucceeds() { void shouldReturnRecreateSummaryResponseWhenRecreateSummary() { // given Long linkId = 1L; + Long memberId = 1L; + Member member = mock(Member.class); + given(member.getId()).willReturn(memberId); + Format format = Format.DETAILED; String url = "https://example.com"; - String existingSummaryBody = "기존 요약 내용입니다."; - String newSummaryBody = "새로운 상세 요약 내용입니다."; - String comparisonBody = "기존 대비 상세 내용이 추가되었습니다."; + String existingSummary = "기존 요약 내용입니다."; + String newSummary = "새로운 상세 요약 내용입니다."; + String difference = "기존 대비 상세 내용이 추가되었습니다."; // 1. LinkService Mocking (URL 가져오기) Link mockLink = mock(Link.class); @@ -112,26 +123,25 @@ void shouldReturnRecreateSummaryResponseWhenRecreateSummary() { // 2. SummaryService (기존 요약 가져오기) Summary mockSummary = mock(Summary.class); - given(mockSummary.getContent()).willReturn(existingSummaryBody); + given(mockSummary.getContent()).willReturn(existingSummary); given(summaryService.getSummary(linkId)).willReturn(mockSummary); // 3. SummaryService (새 요약 생성 및 비교) - given(summaryService.createSummary(linkId, url, format)).willReturn(newSummaryBody); - given(summaryService.comparisonSummary(existingSummaryBody, newSummaryBody)).willReturn(comparisonBody); + RagRegenerateSummaryRes ragRes = new RagRegenerateSummaryRes(newSummary, difference); + given(summaryClient.regenerateSummary(linkId, member.getId(), url, existingSummary)).willReturn(ragRes); // when - RecreateSummaryResponse response = linkFacade.recreateSummary(member, linkId, format); + RegenerateSummaryRes response = linkFacade.recreateSummary(member, linkId, format); // then assertThat(response).isNotNull(); - assertThat(response.existingSummary()).isEqualTo(existingSummaryBody); - assertThat(response.newSummary()).isEqualTo(newSummaryBody); - assertThat(response.comparison()).isEqualTo(comparisonBody); + assertThat(response.existingSummary()).isEqualTo(existingSummary); + assertThat(response.newSummary()).isEqualTo(newSummary); + assertThat(response.difference()).isEqualTo(difference); // verify verify(summaryService).getSummary(linkId); - verify(summaryService).createSummary(linkId, url, format); - verify(summaryService).comparisonSummary(existingSummaryBody, newSummaryBody); + verify(summaryClient).regenerateSummary(linkId, member.getId(), url, existingSummary); } @Test @@ -154,15 +164,20 @@ void shouldReturnEmptyMetaScrapeResWhenCrawlFails() { } @Test - @DisplayName("이미지 URL을 업로드하고 반환된 저장 경로로 링크를 생성한다") + @DisplayName("링크 생성 시 이미지 업로드, 링크 저장, AI 요약 요청, 요약 저장이 순차적으로 수행된다") void shouldCreateLink() { // given - Member member = mock(Member.class); + Long linkId = 1L; + Long memberId = 1L; String url = "https://example.com"; String title = "테스트 제목"; String memo = "테스트 메모"; String originalImageUrl = "https://original.com/image.jpg"; String storedImageUrl = "https://s3-bucket.com/stored-image.jpg"; + String aiSummaryContent = "AI가 요약한 내용입니다."; + + Member member = mock(Member.class); + when(member.getId()).thenReturn(memberId); given(imageUploader.uploadFromUrl(originalImageUrl)).willReturn(storedImageUrl); @@ -173,21 +188,37 @@ void shouldCreateLink() { .imageUrl(storedImageUrl) .build(); + ReflectionTestUtils.setField(savedLink, "id", linkId); given(linkCommandService.saveLink(member, url, title, memo, storedImageUrl)) .willReturn(savedLink); + RagInitialSummaryRes ragRes = new RagInitialSummaryRes(aiSummaryContent); + given(summaryClient.initialSummary(eq(linkId), eq(memberId), eq(title), eq(url), eq(memo))) + .willReturn(ragRes); + + Summary savedSummary = Summary.builder() + .link(savedLink) + .content(aiSummaryContent) + .format(Format.CONCISE) + .build(); + + given(summaryService.createSummary(savedLink, Format.CONCISE, aiSummaryContent)) + .willReturn(savedSummary); + // when - LinkRes result = linkFacade.createLink(member, url, title, memo, originalImageUrl); + LinkDetailRes result = linkFacade.createLink(member, url, title, memo, originalImageUrl); // then assertThat(result).isNotNull(); assertThat(result.url()).isEqualTo(url); assertThat(result.imageUrl()).isEqualTo(storedImageUrl); + assertThat(result.summary().content()).isEqualTo(aiSummaryContent); // Verify - verify(imageUploader, times(1)).uploadFromUrl(originalImageUrl); - verify(linkQueryService, times(1)).existsByUrl(member, url); - verify(linkCommandService, times(1)).saveLink(member, url, title, memo, storedImageUrl); + verify(imageUploader).uploadFromUrl(originalImageUrl); + verify(linkCommandService).saveLink(member, url, title, memo, storedImageUrl); + verify(summaryClient).initialSummary(eq(linkId), eq(memberId), eq(title), eq(url), eq(memo)); + verify(summaryService).createSummary(savedLink, Format.CONCISE, aiSummaryContent); } @Test diff --git a/src/test/java/com/sofa/linkiving/domain/link/integration/LinkApiIntegrationTest.java b/src/test/java/com/sofa/linkiving/domain/link/integration/LinkApiIntegrationTest.java index 159d3472..8f5e9d63 100644 --- a/src/test/java/com/sofa/linkiving/domain/link/integration/LinkApiIntegrationTest.java +++ b/src/test/java/com/sofa/linkiving/domain/link/integration/LinkApiIntegrationTest.java @@ -22,12 +22,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.sofa.linkiving.domain.link.abstraction.ImageUploader; -import com.sofa.linkiving.domain.link.ai.AiSummaryClient; +import com.sofa.linkiving.domain.link.ai.SummaryClient; import com.sofa.linkiving.domain.link.dto.request.LinkCreateReq; import com.sofa.linkiving.domain.link.dto.request.LinkMemoUpdateReq; import com.sofa.linkiving.domain.link.dto.request.LinkTitleUpdateReq; import com.sofa.linkiving.domain.link.dto.request.LinkUpdateReq; import com.sofa.linkiving.domain.link.dto.request.MetaScrapeReq; +import com.sofa.linkiving.domain.link.dto.request.RegenerateSummaryReq; import com.sofa.linkiving.domain.link.entity.Link; import com.sofa.linkiving.domain.link.entity.Summary; import com.sofa.linkiving.domain.link.enums.Format; @@ -62,8 +63,8 @@ public class LinkApiIntegrationTest { @Autowired private SummaryRepository summaryRepository; - @MockitoBean - private AiSummaryClient aiSummaryClient; + @Autowired + private SummaryClient summaryClient; @MockitoBean private ImageUploader imageUploader; @@ -120,7 +121,9 @@ void shouldCreateLinkSuccessfully() throws Exception { .andExpect(jsonPath("$.message").value("링크 생성 완료")) .andExpect(jsonPath("$.data.url").value(req.url())) .andExpect(jsonPath("$.data.title").value(req.title())) - .andExpect(jsonPath("$.data.memo").value(req.memo())); + .andExpect(jsonPath("$.data.memo").value(req.memo())) + .andExpect(jsonPath("$.data.imageUrl").value(uploadedS3Url)) + .andExpect(jsonPath("$.data.summary.content").value("최초 요약")); // DB 검증 boolean exists = linkRepository.existsByMemberAndUrlAndIsDeleteFalse(testMember, req.url()); @@ -622,7 +625,6 @@ void shouldRecreateSummarySuccessfully() throws Exception { .title("테스트 링크") .build()); - Format format = Format.DETAILED; Long linkId = savedLink.getId(); summaryRepository.save(Summary.builder() @@ -630,28 +632,24 @@ void shouldRecreateSummarySuccessfully() throws Exception { .content("기존 요약입니다.") .build()); - String newSummaryText = "새로 생성된 상세 요약입니다."; - String comparisonText = "내용이 더 보강되었습니다."; - - given(aiSummaryClient.generateSummary(eq(linkId), anyString(), eq(format))) - .willReturn(newSummaryText); + Format format = Format.DETAILED; - given(aiSummaryClient.comparisonSummary(anyString(), eq(newSummaryText))) - .willReturn(comparisonText); + RegenerateSummaryReq req = new RegenerateSummaryReq(format); // when & then - mockMvc.perform(get(BASE_URL + "/{id}/summary", linkId) + mockMvc.perform(post(BASE_URL + "/{id}/summary", linkId) .param("format", "DETAILED") .with(csrf()) .with(user(testUserDetails)) - .contentType(MediaType.APPLICATION_JSON)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.message").value("요약 재성성 완료")) .andExpect(jsonPath("$.data.existingSummary").value("기존 요약입니다.")) - .andExpect(jsonPath("$.data.newSummary").value(newSummaryText)) - .andExpect(jsonPath("$.data.comparison").value(comparisonText)); + .andExpect(jsonPath("$.data.newSummary").value("신규 요약")) + .andExpect(jsonPath("$.data.difference").value("비교 사항")); } @Test diff --git a/src/test/java/com/sofa/linkiving/domain/link/service/SummaryCommandServiceTest.java b/src/test/java/com/sofa/linkiving/domain/link/service/SummaryCommandServiceTest.java new file mode 100644 index 00000000..77f343a2 --- /dev/null +++ b/src/test/java/com/sofa/linkiving/domain/link/service/SummaryCommandServiceTest.java @@ -0,0 +1,67 @@ +package com.sofa.linkiving.domain.link.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.sofa.linkiving.domain.link.entity.Link; +import com.sofa.linkiving.domain.link.entity.Summary; +import com.sofa.linkiving.domain.link.enums.Format; +import com.sofa.linkiving.domain.link.repository.SummaryRepository; +import com.sofa.linkiving.domain.member.entity.Member; + +@ExtendWith(MockitoExtension.class) +@DisplayName("SummaryCommandService 단위 테스트") +public class SummaryCommandServiceTest { + + @InjectMocks + private SummaryCommandService summaryCommandService; + + @Mock + private SummaryRepository summaryRepository; + + @Test + @DisplayName("요약를 저장할 수 있다") + void shouldSaveLink() { + // given + Member member = Member.builder() + .email("test@example.com") + .password("password") + .build(); + + Link link = Link.builder() + .member(member) + .url("https://example.com") + .title("테스트 링크") + .memo("메모") + .imageUrl("https://example.com/image.jpg") + .build(); + + Summary summary = Summary.builder() + .format(Format.CONCISE) + .link(link) + .content("요약") + .build(); + + given(summaryRepository.save(any(Summary.class))).willReturn(summary); + + // when + Summary save = summaryCommandService.save( + link, + Format.CONCISE, + "요약" + ); + + // then + assertThat(save).isNotNull(); + assertThat(save.getContent()).isEqualTo("요약"); + verify(summaryRepository, times(1)).save(any(Summary.class)); + } +} diff --git a/src/test/java/com/sofa/linkiving/domain/link/service/SummaryServiceTest.java b/src/test/java/com/sofa/linkiving/domain/link/service/SummaryServiceTest.java index 0ef8695f..5f4d2524 100644 --- a/src/test/java/com/sofa/linkiving/domain/link/service/SummaryServiceTest.java +++ b/src/test/java/com/sofa/linkiving/domain/link/service/SummaryServiceTest.java @@ -10,9 +10,10 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import com.sofa.linkiving.domain.link.ai.AiSummaryClient; +import com.sofa.linkiving.domain.link.entity.Link; import com.sofa.linkiving.domain.link.entity.Summary; import com.sofa.linkiving.domain.link.enums.Format; +import com.sofa.linkiving.domain.member.entity.Member; @ExtendWith(MockitoExtension.class) public class SummaryServiceTest { @@ -23,43 +24,42 @@ public class SummaryServiceTest { private SummaryQueryService summaryQueryService; @Mock - private AiSummaryClient aiSummaryClient; + private SummaryCommandService summaryCommandService; @Test - @DisplayName("createSummary 호출 시 AiSummaryClient에게 위임한다") - void shouldCallGenerateSummaryWhenCreateSummary() { + @DisplayName("요약를 생성할 수 있다") + void shouldCreateLink() { // given - Long linkId = 1L; - String url = "https://example.com"; - Format format = Format.CONCISE; - String expectedResult = "Generated Summary"; - - given(aiSummaryClient.generateSummary(linkId, url, format)).willReturn(expectedResult); - - // when - String result = summaryService.createSummary(linkId, url, format); + Member member = Member.builder() + .email("test@example.com") + .password("password") + .build(); - // then - assertThat(result).isEqualTo(expectedResult); - verify(aiSummaryClient).generateSummary(linkId, url, format); - } + Link link = Link.builder() + .member(member) + .url("https://example.com") + .title("테스트 링크") + .build(); - @Test - @DisplayName("comparisonSummary 호출 시 AiSummaryClient에게 위임한다") - void shouldCallComparisonSummaryWhenComparisonSummary() { - // given - String oldSummary = "old"; - String newSummary = "new"; - String expectedResult = "Comparison Result"; + Summary summary = Summary.builder() + .format(Format.CONCISE) + .link(link) + .content("요약") + .build(); - given(aiSummaryClient.comparisonSummary(oldSummary, newSummary)).willReturn(expectedResult); + given(summaryCommandService.save(any(), any(), any())).willReturn(summary); // when - String result = summaryService.comparisonSummary(oldSummary, newSummary); + Summary save = summaryCommandService.save( + link, + Format.CONCISE, + "요약" + ); // then - assertThat(result).isEqualTo(expectedResult); - verify(aiSummaryClient).comparisonSummary(oldSummary, newSummary); + assertThat(save).isNotNull(); + assertThat(save.getContent()).isEqualTo("요약"); + verify(summaryCommandService, times(1)).save(any(), any(), any()); } @Test