diff --git a/src/main/java/com/sofa/linkiving/domain/link/entity/Summary.java b/src/main/java/com/sofa/linkiving/domain/link/entity/Summary.java index 144b475e..98619fc4 100644 --- a/src/main/java/com/sofa/linkiving/domain/link/entity/Summary.java +++ b/src/main/java/com/sofa/linkiving/domain/link/entity/Summary.java @@ -28,14 +28,14 @@ public class Summary extends BaseEntity { @Column(columnDefinition = "TEXT", nullable = false) private String content; - @Column(name = "selected") + @Column(nullable = false) private boolean selected; @Builder - public Summary(Link link, Format format, String content, boolean select) { + public Summary(Link link, Format format, String content, Boolean selected) { this.link = link; this.format = format; this.content = content; - this.selected = select; + this.selected = selected != null && selected; } } diff --git a/src/main/java/com/sofa/linkiving/domain/link/repository/SummaryRepository.java b/src/main/java/com/sofa/linkiving/domain/link/repository/SummaryRepository.java index e893282a..6048961a 100644 --- a/src/main/java/com/sofa/linkiving/domain/link/repository/SummaryRepository.java +++ b/src/main/java/com/sofa/linkiving/domain/link/repository/SummaryRepository.java @@ -1,8 +1,10 @@ package com.sofa.linkiving.domain.link.repository; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @@ -12,6 +14,19 @@ @Repository public interface SummaryRepository extends JpaRepository { + + Optional findByLinkIdAndSelectedTrue(Long linkId); + + boolean existsByLinkIdAndSelectedTrue(Long linkId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("update Summary s set s.selected = false where s.link.id = :linkId and s.selected = true") + int clearSelectedByLinkId(@Param("linkId") Long linkId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("update Summary s set s.selected = true where s.id = :summaryId and s.link.id = :linkId") + int selectByIdAndLinkId(@Param("summaryId") Long summaryId, @Param("linkId") Long linkId); + @Query("SELECT s FROM Summary s WHERE s.link IN :links AND s.selected = true") List findAllByLinkInAndSelectedTrue(@Param("links") List links); } 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..9af98652 --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/link/service/SummaryCommandService.java @@ -0,0 +1,28 @@ +package com.sofa.linkiving.domain.link.service; + +import org.springframework.stereotype.Service; + +import com.sofa.linkiving.domain.link.error.LinkErrorCode; +import com.sofa.linkiving.domain.link.repository.SummaryRepository; +import com.sofa.linkiving.global.error.exception.BusinessException; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class SummaryCommandService { + + private final SummaryRepository summaryRepository; + + /** + * 특정 링크에서 선택된 요약을 변경한다. (링크당 selected=true는 최대 1개) + */ + public void selectSummary(Long linkId, Long summaryId) { + summaryRepository.clearSelectedByLinkId(linkId); + int updated = summaryRepository.selectByIdAndLinkId(summaryId, linkId); + if (updated == 0) { + throw new BusinessException(LinkErrorCode.SUMMARY_NOT_FOUND); + } + } +} + diff --git a/src/main/java/com/sofa/linkiving/domain/link/service/SummaryQueryService.java b/src/main/java/com/sofa/linkiving/domain/link/service/SummaryQueryService.java index 6952a4e7..ba0c4657 100644 --- a/src/main/java/com/sofa/linkiving/domain/link/service/SummaryQueryService.java +++ b/src/main/java/com/sofa/linkiving/domain/link/service/SummaryQueryService.java @@ -21,7 +21,7 @@ public class SummaryQueryService { private final SummaryRepository summaryRepository; public Summary getSummary(Long linkId) { - return summaryRepository.findById(linkId).orElseThrow( + return summaryRepository.findByLinkIdAndSelectedTrue(linkId).orElseThrow( () -> new BusinessException(LinkErrorCode.SUMMARY_NOT_FOUND) ); } diff --git a/src/main/java/com/sofa/linkiving/domain/link/worker/SummaryWorker.java b/src/main/java/com/sofa/linkiving/domain/link/worker/SummaryWorker.java index 18afee09..cd6f8176 100644 --- a/src/main/java/com/sofa/linkiving/domain/link/worker/SummaryWorker.java +++ b/src/main/java/com/sofa/linkiving/domain/link/worker/SummaryWorker.java @@ -116,10 +116,12 @@ public void generateAndSaveSummary(Long linkId) { log.info("Summary generated for linkId: {}", linkId); // 3. Summary 엔티티 생성 및 저장 + boolean isFirstSummary = !summaryRepository.existsByLinkIdAndSelectedTrue(linkId); Summary summary = Summary.builder() .link(link) .format(Format.CONCISE) .content(response.summary()) + .selected(isFirstSummary) .build(); summaryRepository.save(summary); diff --git a/src/test/java/com/sofa/linkiving/domain/chat/integration/ChatApiIntegrationTest.java b/src/test/java/com/sofa/linkiving/domain/chat/integration/ChatApiIntegrationTest.java index 766de9eb..4ecad532 100644 --- a/src/test/java/com/sofa/linkiving/domain/chat/integration/ChatApiIntegrationTest.java +++ b/src/test/java/com/sofa/linkiving/domain/chat/integration/ChatApiIntegrationTest.java @@ -113,7 +113,7 @@ void shouldGetMessagesWithLinksAndSummaries() { summaryRepository.save(Summary.builder() .link(link1) .content("링크1의 핵심 요약입니다.") - .select(true) + .selected(true) .build()); Chat chat = chatRepository.save(com.sofa.linkiving.domain.chat.entity.Chat.builder() diff --git a/src/test/java/com/sofa/linkiving/domain/link/entity/SummaryTest.java b/src/test/java/com/sofa/linkiving/domain/link/entity/SummaryTest.java index 624145d9..3fbcd4e3 100644 --- a/src/test/java/com/sofa/linkiving/domain/link/entity/SummaryTest.java +++ b/src/test/java/com/sofa/linkiving/domain/link/entity/SummaryTest.java @@ -55,17 +55,18 @@ void shouldCreateSummaryWithAllFields() { Format format = Format.DETAILED; String content = "This is a detailed summary"; - boolean select = true; + boolean selected = true; Summary summary = Summary.builder() .link(link) .format(format) .content(content) - .select(select) + .selected(selected) .build(); assertThat(summary.getLink()).isEqualTo(link); assertThat(summary.getContent()).isEqualTo(content); assertThat(summary.getFormat()).isEqualTo(format); + assertThat(summary.isSelected()).isTrue(); } } 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..5cd644f7 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 @@ -170,7 +170,7 @@ void shouldGetLinkSuccessfully() throws Exception { .link(link) .content("이것은 요약 내용입니다.") .format(Format.CONCISE) - .select(true) + .selected(true) .build()); // when & then @@ -335,7 +335,7 @@ void shouldGetLinkListSuccessfully() throws Exception { .link(link2) .content("링크 2 요약") .format(Format.CONCISE) - .select(true) + .selected(true) .build()); // when & then @@ -628,6 +628,7 @@ void shouldRecreateSummarySuccessfully() throws Exception { summaryRepository.save(Summary.builder() .link(savedLink) .content("기존 요약입니다.") + .selected(true) .build()); String newSummaryText = "새로 생성된 상세 요약입니다."; diff --git a/src/test/java/com/sofa/linkiving/domain/link/repository/LinkRepositoryTest.java b/src/test/java/com/sofa/linkiving/domain/link/repository/LinkRepositoryTest.java index c1c7824f..dc72ff66 100644 --- a/src/test/java/com/sofa/linkiving/domain/link/repository/LinkRepositoryTest.java +++ b/src/test/java/com/sofa/linkiving/domain/link/repository/LinkRepositoryTest.java @@ -123,7 +123,7 @@ void shouldFindByIdAndMemberWithSummary() { .link(link) .content("선택된 요약 내용") .format(Format.CONCISE) - .select(true) + .selected(true) .build(); entityManager.persist(selectedSummary); @@ -131,7 +131,7 @@ void shouldFindByIdAndMemberWithSummary() { .link(link) .content("다른 요약") .format(Format.CONCISE) - .select(false) + .selected(false) .build(); entityManager.persist(otherSummary); @@ -167,7 +167,7 @@ void shouldFindAllByMemberWithSummaryAndCursor() { .link(link) .content("요약 " + i) .format(Format.CONCISE) - .select(true) + .selected(true) .build(); entityManager.persist(summary); } diff --git a/src/test/java/com/sofa/linkiving/domain/link/repository/SummaryRepositoryTest.java b/src/test/java/com/sofa/linkiving/domain/link/repository/SummaryRepositoryTest.java index 9b8dd20a..b827d515 100644 --- a/src/test/java/com/sofa/linkiving/domain/link/repository/SummaryRepositoryTest.java +++ b/src/test/java/com/sofa/linkiving/domain/link/repository/SummaryRepositoryTest.java @@ -59,18 +59,18 @@ void shouldFindAllByLinkInAndSelectedTrue() { Summary summary1 = Summary.builder() .link(link1) .content("s1") - .select(true) + .selected(true) .build(); Summary summary2 = Summary.builder() .link(link2) .content("s2") - .select(false) + .selected(false) .build(); Summary summary3 = Summary .builder() .link(link3) .content("s3") - .select(true) + .selected(true) .build(); em.persist(summary1); @@ -109,7 +109,7 @@ void shouldReturnEmptyWhenLinkListIsEmpty() { Summary summary = Summary.builder() .link(link) .content("s1") - .select(true) + .selected(true) .build(); em.persist(summary); 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..c6fd749c --- /dev/null +++ b/src/test/java/com/sofa/linkiving/domain/link/service/SummaryCommandServiceTest.java @@ -0,0 +1,61 @@ +package com.sofa.linkiving.domain.link.service; + +import static org.assertj.core.api.Assertions.*; +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.error.LinkErrorCode; +import com.sofa.linkiving.domain.link.repository.SummaryRepository; +import com.sofa.linkiving.global.error.exception.BusinessException; + +@ExtendWith(MockitoExtension.class) +@DisplayName("SummaryCommandService 단위 테스트") +public class SummaryCommandServiceTest { + + @Mock + private SummaryRepository summaryRepository; + + @InjectMocks + private SummaryCommandService summaryCommandService; + + @Test + @DisplayName("요약 선택 변경 성공") + void shouldSelectSummarySuccessfully() { + // given + Long linkId = 1L; + Long summaryId = 2L; + given(summaryRepository.clearSelectedByLinkId(linkId)).willReturn(1); + given(summaryRepository.selectByIdAndLinkId(summaryId, linkId)).willReturn(1); + + // when + summaryCommandService.selectSummary(linkId, summaryId); + + // then + verify(summaryRepository).clearSelectedByLinkId(linkId); + verify(summaryRepository).selectByIdAndLinkId(summaryId, linkId); + } + + @Test + @DisplayName("존재하지 않는 요약 선택 시 예외 발생") + void shouldThrowExceptionWhenSummaryNotFound() { + // given + Long linkId = 1L; + Long summaryId = 999L; + given(summaryRepository.clearSelectedByLinkId(linkId)).willReturn(1); + given(summaryRepository.selectByIdAndLinkId(summaryId, linkId)).willReturn(0); + + // when & then + assertThatThrownBy(() -> summaryCommandService.selectSummary(linkId, summaryId)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("errorCode", LinkErrorCode.SUMMARY_NOT_FOUND); + + verify(summaryRepository).clearSelectedByLinkId(linkId); + verify(summaryRepository).selectByIdAndLinkId(summaryId, linkId); + } +} diff --git a/src/test/java/com/sofa/linkiving/domain/link/service/SummaryQueryServiceTest.java b/src/test/java/com/sofa/linkiving/domain/link/service/SummaryQueryServiceTest.java index b65b1630..ff595fec 100644 --- a/src/test/java/com/sofa/linkiving/domain/link/service/SummaryQueryServiceTest.java +++ b/src/test/java/com/sofa/linkiving/domain/link/service/SummaryQueryServiceTest.java @@ -36,7 +36,7 @@ void shouldReturnSummaryWhenSummaryExists() { // given Long linkId = 1L; Summary mockSummary = mock(Summary.class); // Summary 엔티티 Mock - given(summaryRepository.findById(linkId)).willReturn(Optional.of(mockSummary)); + given(summaryRepository.findByLinkIdAndSelectedTrue(linkId)).willReturn(Optional.of(mockSummary)); // when Summary result = summaryQueryService.getSummary(linkId); @@ -44,7 +44,7 @@ void shouldReturnSummaryWhenSummaryExists() { // then assertThat(result).isNotNull(); assertThat(result).isEqualTo(mockSummary); - verify(summaryRepository).findById(linkId); + verify(summaryRepository).findByLinkIdAndSelectedTrue(linkId); } @Test @@ -52,14 +52,14 @@ void shouldReturnSummaryWhenSummaryExists() { void shouldThrowBusinessExceptionWhenSummaryNotFound() { // given Long linkId = 999L; - given(summaryRepository.findById(linkId)).willReturn(Optional.empty()); + given(summaryRepository.findByLinkIdAndSelectedTrue(linkId)).willReturn(Optional.empty()); // when & then assertThatThrownBy(() -> summaryQueryService.getSummary(linkId)) .isInstanceOf(BusinessException.class) .hasFieldOrPropertyWithValue("errorCode", LinkErrorCode.SUMMARY_NOT_FOUND); - verify(summaryRepository).findById(linkId); + verify(summaryRepository).findByLinkIdAndSelectedTrue(linkId); } @Test