From cd45d6d6edc005899c6a11cffe09d4c2f5c1ee72 Mon Sep 17 00:00:00 2001 From: Jansoon Date: Wed, 24 Dec 2025 09:41:20 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EC=B1=85=EC=9E=84=20=EC=A0=9C=EA=B1=B0=20(#167)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sofa/linkiving/domain/link/service/LinkCommandService.java | 2 -- .../sofa/linkiving/domain/link/service/LinkQueryService.java | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/main/java/com/sofa/linkiving/domain/link/service/LinkCommandService.java b/src/main/java/com/sofa/linkiving/domain/link/service/LinkCommandService.java index f6493d43..2b57f523 100644 --- a/src/main/java/com/sofa/linkiving/domain/link/service/LinkCommandService.java +++ b/src/main/java/com/sofa/linkiving/domain/link/service/LinkCommandService.java @@ -1,7 +1,6 @@ package com.sofa.linkiving.domain.link.service; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import com.sofa.linkiving.domain.link.entity.Link; import com.sofa.linkiving.domain.link.repository.LinkRepository; @@ -10,7 +9,6 @@ import lombok.RequiredArgsConstructor; @Service -@Transactional @RequiredArgsConstructor public class LinkCommandService { diff --git a/src/main/java/com/sofa/linkiving/domain/link/service/LinkQueryService.java b/src/main/java/com/sofa/linkiving/domain/link/service/LinkQueryService.java index 3bd36016..01dfa13d 100644 --- a/src/main/java/com/sofa/linkiving/domain/link/service/LinkQueryService.java +++ b/src/main/java/com/sofa/linkiving/domain/link/service/LinkQueryService.java @@ -5,7 +5,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import com.sofa.linkiving.domain.link.entity.Link; import com.sofa.linkiving.domain.link.error.LinkErrorCode; @@ -18,7 +17,6 @@ @Slf4j @Service -@Transactional(readOnly = true) @RequiredArgsConstructor public class LinkQueryService { From 2888c83b35a6e58da66a4d12d75f8c3056dd7489 Mon Sep 17 00:00:00 2001 From: Jansoon Date: Wed, 24 Dec 2025 09:41:46 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20Controller=20&=20API=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?(#167)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/link/controller/LinkApi.java | 17 +++++----- .../link/controller/LinkController.java | 33 +++++++++---------- 2 files changed, 24 insertions(+), 26 deletions(-) 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 006de73d..7ca5cff0 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 @@ -2,7 +2,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.http.ResponseEntity; import com.sofa.linkiving.domain.link.dto.request.LinkCreateReq; import com.sofa.linkiving.domain.link.dto.request.LinkMemoUpdateReq; @@ -31,51 +30,51 @@ BaseResponse scrapeMetadata( ); @Operation(summary = "URL 중복 체크", description = "저장하려는 URL이 이미 존재하는지 확인하고, 존재 시 linkId를 반환합니다") - ResponseEntity> checkDuplicate( + BaseResponse checkDuplicate( String url, Member member ); @Operation(summary = "링크 생성", description = "새로운 링크를 저장합니다") - ResponseEntity> createLink( + BaseResponse createLink( LinkCreateReq request, Member member ); @Operation(summary = "링크 수정", description = "링크 정보를 수정합니다. null이 아닌 필드만 수정됩니다.") - ResponseEntity> updateLink( + BaseResponse updateLink( Long id, LinkUpdateReq request, Member member ); @Operation(summary = "링크 삭제", description = "링크를 삭제합니다 (Soft Delete)") - ResponseEntity> deleteLink( + BaseResponse deleteLink( Long id, Member member ); @Operation(summary = "링크 조회", description = "링크 상세 정보를 조회합니다") - ResponseEntity> getLink( + BaseResponse getLink( Long id, Member member ); @Operation(summary = "링크 목록 조회", description = "저장된 링크 목록을 페이징하여 조회합니다") - ResponseEntity>> getLinkList( + BaseResponse> getLinkList( Pageable pageable, Member member ); @Operation(summary = "링크 제목 수정", description = "링크 제목만 수정합니다") - ResponseEntity> updateTitle( + BaseResponse updateTitle( Long id, LinkTitleUpdateReq request, Member member ); @Operation(summary = "링크 메모 수정", description = "링크 메모만 수정합니다") - ResponseEntity> updateMemo( + BaseResponse updateMemo( Long id, LinkMemoUpdateReq request, 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 7ed0f534..98fbb326 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 @@ -3,7 +3,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; @@ -52,17 +51,17 @@ public BaseResponse scrapeMetadata( @Override @GetMapping("/duplicate") - public ResponseEntity> checkDuplicate( + public BaseResponse checkDuplicate( @RequestParam String url, @AuthMember Member member ) { LinkDuplicateCheckRes response = linkFacade.checkDuplicate(member, url); - return ResponseEntity.ok(BaseResponse.success(response, "URL 중복 체크 완료")); + return BaseResponse.success(response, "URL 중복 체크 완료"); } @Override @PostMapping - public ResponseEntity> createLink( + public BaseResponse createLink( @Valid @RequestBody LinkCreateReq request, @AuthMember Member member ) { @@ -73,12 +72,12 @@ public ResponseEntity> createLink( request.memo(), request.imageUrl() ); - return ResponseEntity.ok(BaseResponse.success(response, "링크 생성 완료")); + return BaseResponse.success(response, "링크 생성 완료"); } @Override @PutMapping("/{id}") - public ResponseEntity> updateLink( + public BaseResponse updateLink( @PathVariable Long id, @Valid @RequestBody LinkUpdateReq request, @AuthMember Member member @@ -89,59 +88,59 @@ public ResponseEntity> updateLink( request.title(), request.memo() ); - return ResponseEntity.ok(BaseResponse.success(response, "링크 수정 완료")); + return BaseResponse.success(response, "링크 수정 완료"); } @Override @DeleteMapping("/{id}") - public ResponseEntity> deleteLink( + public BaseResponse deleteLink( @PathVariable Long id, @AuthMember Member member ) { linkFacade.deleteLink(id, member); - return ResponseEntity.ok(BaseResponse.noContent("링크 삭제 완료")); + return BaseResponse.noContent("링크 삭제 완료"); } @Override @GetMapping("/{id}") - public ResponseEntity> getLink( + public BaseResponse getLink( @PathVariable Long id, @AuthMember Member member ) { LinkRes response = linkFacade.getLink(id, member); - return ResponseEntity.ok(BaseResponse.success(response, "링크 조회 완료")); + return BaseResponse.success(response, "링크 조회 완료"); } @Override @GetMapping - public ResponseEntity>> getLinkList( + public BaseResponse> getLinkList( @PageableDefault(size = 20) Pageable pageable, @AuthMember Member member ) { Page response = linkFacade.getLinkList(member, pageable); - return ResponseEntity.ok(BaseResponse.success(response, "링크 목록 조회 완료")); + return BaseResponse.success(response, "링크 목록 조회 완료"); } @Override @PatchMapping("/{id}/title") - public ResponseEntity> updateTitle( + public BaseResponse updateTitle( @PathVariable Long id, @Valid @RequestBody LinkTitleUpdateReq request, @AuthMember Member member ) { LinkRes response = linkFacade.updateTitle(id, member, request.title()); - return ResponseEntity.ok(BaseResponse.success(response, "제목 수정 완료")); + return BaseResponse.success(response, "제목 수정 완료"); } @Override @PatchMapping("/{id}/memo") - public ResponseEntity> updateMemo( + public BaseResponse updateMemo( @PathVariable Long id, @Valid @RequestBody LinkMemoUpdateReq request, @AuthMember Member member ) { LinkRes response = linkFacade.updateMemo(id, member, request.memo()); - return ResponseEntity.ok(BaseResponse.success(response, "메모 수정 완료")); + return BaseResponse.success(response, "메모 수정 완료"); } @Override From bd65aa6e9035204b4cdbec691d7f140dc733d65d Mon Sep 17 00:00:00 2001 From: Jansoon Date: Wed, 24 Dec 2025 09:43:21 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20Service=EC=97=90=EC=84=9C=20Fac?= =?UTF-8?q?ade=EB=A1=9C=20DTO=20=EB=B3=80=ED=99=98=20=EC=B1=85=EC=9E=84=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20(#167)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/link/facade/LinkFacade.java | 25 +- .../domain/link/service/LinkService.java | 37 ++- .../domain/link/facade/LinkFacadeTest.java | 236 +++++++++++++++--- .../domain/link/service/LinkServiceTest.java | 42 ++-- 4 files changed, 260 insertions(+), 80 deletions(-) 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 ba44d814..794bd2df 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 @@ -10,6 +10,7 @@ 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.entity.Link; import com.sofa.linkiving.domain.link.enums.Format; import com.sofa.linkiving.domain.link.service.LinkService; import com.sofa.linkiving.domain.link.service.SummaryService; @@ -28,19 +29,23 @@ public class LinkFacade { private final SummaryService summaryService; public LinkRes createLink(Member member, String url, String title, String memo, String imageUrl) { - return linkService.createLink(member, url, title, memo, imageUrl); + Link link = linkService.createLink(member, url, title, memo, imageUrl); + return LinkRes.from(link); } public LinkRes updateLink(Long linkId, Member member, String title, String memo) { - return linkService.updateLink(linkId, member, title, memo); + Link link = linkService.updateLink(linkId, member, title, memo); + return LinkRes.from(link); } public LinkRes updateTitle(Long linkId, Member member, String title) { - return linkService.updateTitle(linkId, member, title); + Link link = linkService.updateTitle(linkId, member, title); + return LinkRes.from(link); } public LinkRes updateMemo(Long linkId, Member member, String memo) { - return linkService.updateMemo(linkId, member, memo); + Link link = linkService.updateMemo(linkId, member, memo); + return LinkRes.from(link); } public void deleteLink(Long linkId, Member member) { @@ -49,23 +54,27 @@ public void deleteLink(Long linkId, Member member) { @Transactional(readOnly = true) public LinkRes getLink(Long linkId, Member member) { - return linkService.getLink(linkId, member); + Link link = linkService.getLink(linkId, member); + return LinkRes.from(link); } @Transactional(readOnly = true) public Page getLinkList(Member member, Pageable pageable) { - return linkService.getLinkList(member, pageable); + Page links = linkService.getLinkList(member, pageable); + return links.map(LinkRes::from); } @Transactional(readOnly = true) public LinkDuplicateCheckRes checkDuplicate(Member member, String url) { - return linkService.checkDuplicate(member, url); + return linkService.findLinkIdByUrl(member, url) + .map(LinkDuplicateCheckRes::exists) + .orElse(LinkDuplicateCheckRes.notExists()); } @Transactional(readOnly = true) public RecreateSummaryResponse recreateSummary(Member member, Long linkId, Format format) { - String url = linkService.getLink(linkId, member).url(); + String url = linkService.getLink(linkId, member).getUrl(); String existingSummary = summaryService.getSummary(linkId).getContent(); String newSummary = summaryService.createSummary(linkId, url, format); diff --git a/src/main/java/com/sofa/linkiving/domain/link/service/LinkService.java b/src/main/java/com/sofa/linkiving/domain/link/service/LinkService.java index 198e8f36..f6b2fd87 100644 --- a/src/main/java/com/sofa/linkiving/domain/link/service/LinkService.java +++ b/src/main/java/com/sofa/linkiving/domain/link/service/LinkService.java @@ -1,12 +1,12 @@ package com.sofa.linkiving.domain.link.service; +import java.util.Optional; + import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; -import com.sofa.linkiving.domain.link.dto.response.LinkDuplicateCheckRes; -import com.sofa.linkiving.domain.link.dto.response.LinkRes; import com.sofa.linkiving.domain.link.entity.Link; import com.sofa.linkiving.domain.link.error.LinkErrorCode; import com.sofa.linkiving.domain.link.event.LinkCreatedEvent; @@ -25,7 +25,7 @@ public class LinkService { private final LinkQueryService linkQueryService; private final ApplicationEventPublisher eventPublisher; - public LinkRes createLink(Member member, String url, String title, String memo, String imageUrl) { + public Link createLink(Member member, String url, String title, String memo, String imageUrl) { if (linkQueryService.existsByUrl(member, url)) { throw new BusinessException(LinkErrorCode.DUPLICATE_URL); } @@ -33,37 +33,36 @@ public LinkRes createLink(Member member, String url, String title, String memo, Link link = linkCommandService.saveLink(member, url, title, memo, imageUrl); log.info("Link created - id: {}, memberId: {}, url: {}", link.getId(), member.getId(), url); - // 트랜잭션 커밋 후 요약 대기 큐에 추가되도록 이벤트 발행 eventPublisher.publishEvent(new LinkCreatedEvent(link.getId())); - return LinkRes.from(link); + return link; } - public LinkRes updateLink(Long linkId, Member member, String title, String memo) { + public Link updateLink(Long linkId, Member member, String title, String memo) { Link link = linkQueryService.findById(linkId, member); Link updatedLink = linkCommandService.updateLink(link, title, memo); log.info("Link updated - id: {}, memberId: {}", linkId, member.getId()); - return LinkRes.from(updatedLink); + return updatedLink; } - public LinkRes updateTitle(Long linkId, Member member, String title) { + public Link updateTitle(Long linkId, Member member, String title) { Link link = linkQueryService.findById(linkId, member); Link updatedLink = linkCommandService.updateLink(link, title, link.getMemo()); log.info("Link title updated - id: {}, memberId: {}", linkId, member.getId()); - return LinkRes.from(updatedLink); + return updatedLink; } - public LinkRes updateMemo(Long linkId, Member member, String memo) { + public Link updateMemo(Long linkId, Member member, String memo) { Link link = linkQueryService.findById(linkId, member); Link updatedLink = linkCommandService.updateLink(link, link.getTitle(), memo); log.info("Link memo updated - id: {}, memberId: {}", linkId, member.getId()); - return LinkRes.from(updatedLink); + return updatedLink; } public void deleteLink(Long linkId, Member member) { @@ -73,19 +72,15 @@ public void deleteLink(Long linkId, Member member) { log.info("Link soft deleted - id: {}, memberId: {}", linkId, member.getId()); } - public LinkRes getLink(Long linkId, Member member) { - Link link = linkQueryService.findById(linkId, member); - return LinkRes.from(link); + public Link getLink(Long linkId, Member member) { + return linkQueryService.findById(linkId, member); } - public Page getLinkList(Member member, Pageable pageable) { - Page links = linkQueryService.findAllByMember(member, pageable); - return links.map(LinkRes::from); + public Page getLinkList(Member member, Pageable pageable) { + return linkQueryService.findAllByMember(member, pageable); } - public LinkDuplicateCheckRes checkDuplicate(Member member, String url) { - return linkQueryService.findIdByUrl(member, url) - .map(LinkDuplicateCheckRes::exists) - .orElse(LinkDuplicateCheckRes.notExists()); + public Optional findLinkIdByUrl(Member member, String url) { + return linkQueryService.findIdByUrl(member, url); } } 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 528a350a..e64c161f 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 @@ -3,40 +3,66 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.BDDMockito.*; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; 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 org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import com.sofa.linkiving.domain.link.dto.OgTagDto; -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.entity.Link; import com.sofa.linkiving.domain.link.entity.Summary; import com.sofa.linkiving.domain.link.enums.Format; +import com.sofa.linkiving.domain.link.error.LinkErrorCode; +import com.sofa.linkiving.domain.link.service.LinkCommandService; +import com.sofa.linkiving.domain.link.service.LinkQueryService; import com.sofa.linkiving.domain.link.service.LinkService; import com.sofa.linkiving.domain.link.service.SummaryService; import com.sofa.linkiving.domain.link.util.OgTagCrawler; import com.sofa.linkiving.domain.member.entity.Member; +import com.sofa.linkiving.global.error.exception.BusinessException; @ExtendWith(MockitoExtension.class) @DisplayName("LinkFacade 단위 테스트") public class LinkFacadeTest { - @InjectMocks private LinkFacade linkFacade; - @Mock + @InjectMocks private LinkService linkService; + @Mock + private LinkCommandService linkCommandService; + + @Mock + private LinkQueryService linkQueryService; + + @Mock + private ApplicationEventPublisher eventPublisher; + @Mock private SummaryService summaryService; @Mock private OgTagCrawler ogTagCrawler; + @BeforeEach + void setUp() { + linkFacade = new LinkFacade(linkService, ogTagCrawler, summaryService); + } + @Test @DisplayName("메타데이터 크롤링 성공 시 정상적으로 MetaScrapeRes를 반환한다") void shouldReturnMetaScrapeResWhenCrawlSucceeds() { @@ -68,41 +94,24 @@ void shouldReturnMetaScrapeResWhenCrawlSucceeds() { void shouldReturnRecreateSummaryResponseWhenRecreateSummary() { // given Long linkId = 1L; - Member member = mock(Member.class); // Member 객체 Mock - Format format = Format.DETAILED; + Member member = mock(Member.class); String url = "https://example.com"; - String existingSummaryBody = "기존 요약 내용입니다."; - String newSummaryBody = "새로운 상세 요약 내용입니다."; - String comparisonBody = "기존 대비 상세 내용이 추가되었습니다."; + Link link = mock(Link.class); - // 1. LinkService Mocking (URL 가져오기) - LinkRes mockLinkRes = mock(LinkRes.class); - given(mockLinkRes.url()).willReturn(url); - given(linkService.getLink(linkId, member)).willReturn(mockLinkRes); + given(link.getUrl()).willReturn(url); + given(linkService.getLink(linkId, member)).willReturn(link); // Service Mock 동작 정의 - // 2. SummaryService (기존 요약 가져오기) Summary mockSummary = mock(Summary.class); - given(mockSummary.getContent()).willReturn(existingSummaryBody); + given(mockSummary.getContent()).willReturn("Old"); given(summaryService.getSummary(linkId)).willReturn(mockSummary); - - // 3. SummaryService (새 요약 생성 및 비교) - given(summaryService.createSummary(linkId, url, format)).willReturn(newSummaryBody); - given(summaryService.comparisonSummary(existingSummaryBody, newSummaryBody)).willReturn(comparisonBody); + given(summaryService.createSummary(linkId, url, Format.DETAILED)).willReturn("New"); + given(summaryService.comparisonSummary("Old", "New")).willReturn("Diff"); // when - RecreateSummaryResponse response = linkFacade.recreateSummary(member, linkId, format); + RecreateSummaryResponse res = linkFacade.recreateSummary(member, linkId, Format.DETAILED); // then - assertThat(response).isNotNull(); - assertThat(response.existingSummary()).isEqualTo(existingSummaryBody); - assertThat(response.newSummary()).isEqualTo(newSummaryBody); - assertThat(response.comparison()).isEqualTo(comparisonBody); - - // verify - verify(linkService).getLink(linkId, member); - verify(summaryService).getSummary(linkId); - verify(summaryService).createSummary(linkId, url, format); - verify(summaryService).comparisonSummary(existingSummaryBody, newSummaryBody); + assertThat(res.newSummary()).isEqualTo("New"); } @Test @@ -123,4 +132,173 @@ void shouldReturnEmptyMetaScrapeResWhenCrawlFails() { assertThat(result.url()).isEmpty(); verify(ogTagCrawler, times(1)).crawl(url); } + + @Test + @DisplayName("링크를 생성하고 Entity를 반환한다") + void shouldCreateLink() { + // given + Member member = Member.builder().email("test@example.com").build(); + Link link = Link.builder() + .member(member) + .url("https://example.com") + .title("테스트 링크") + .build(); + + given(linkQueryService.existsByUrl(member, "https://example.com")).willReturn(false); + given(linkCommandService.saveLink(any(), any(), any(), any(), any())).willReturn(link); + + // when + Link createdLink = linkService.createLink( + member, "https://example.com", "테스트 링크", "메모", null + ); + + // then + assertThat(createdLink).isNotNull(); + assertThat(createdLink).isInstanceOf(Link.class); // Entity 반환 확인 + assertThat(createdLink.getUrl()).isEqualTo("https://example.com"); + + verify(linkQueryService, times(1)).existsByUrl(member, "https://example.com"); + verify(linkCommandService, times(1)).saveLink(any(), any(), any(), any(), any()); + } + + @Test + @DisplayName("중복된 URL로 링크 생성 시 예외가 발생한다") + void shouldThrowExceptionWhenDuplicateUrl() { + // given + Member member = Member.builder().email("test@example.com").build(); + given(linkQueryService.existsByUrl(member, "https://example.com")).willReturn(true); + + // when & then + assertThatThrownBy(() -> linkService.createLink( + member, "https://example.com", "테스트 링크", null, null + )) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("errorCode", LinkErrorCode.DUPLICATE_URL); + + verify(linkCommandService, never()).saveLink(any(), any(), any(), any(), any()); + } + + @Test + @DisplayName("링크를 수정하고 Entity를 반환한다") + void shouldUpdateLink() { + // given + Member member = Member.builder().email("test@example.com").build(); + Link originalLink = Link.builder().member(member).url("https://example.com").title("원본").build(); + Link updatedLink = Link.builder().member(member).url("https://example.com").title("수정").build(); + + given(linkQueryService.findById(1L, member)).willReturn(originalLink); + given(linkCommandService.updateLink(any(), any(), any())).willReturn(updatedLink); + + // when + Link result = linkService.updateLink(1L, member, "수정", null); + + // then + assertThat(result).isEqualTo(updatedLink); + assertThat(result.getTitle()).isEqualTo("수정"); + } + + @Test + @DisplayName("링크 제목만 수정할 수 있다") + void shouldUpdateTitle() { + // given + Member member = Member.builder().email("test@example.com").build(); + Link originalLink = Link.builder().member(member).title("원본").memo("메모").build(); + Link updatedLink = Link.builder().member(member).title("수정").memo("메모").build(); + + given(linkQueryService.findById(1L, member)).willReturn(originalLink); + given(linkCommandService.updateLink(any(), eq("수정"), eq("메모"))).willReturn(updatedLink); + + // when + Link result = linkService.updateTitle(1L, member, "수정"); + + // then + assertThat(result.getTitle()).isEqualTo("수정"); + } + + @Test + @DisplayName("링크 메모만 수정할 수 있다") + void shouldUpdateMemo() { + // given + Member member = Member.builder().email("test@example.com").build(); + Link originalLink = Link.builder().member(member).title("제목").memo("원본").build(); + Link updatedLink = Link.builder().member(member).title("제목").memo("수정").build(); + + given(linkQueryService.findById(1L, member)).willReturn(originalLink); + given(linkCommandService.updateLink(any(), eq("제목"), eq("수정"))).willReturn(updatedLink); + + // when + Link result = linkService.updateMemo(1L, member, "수정"); + + // then + assertThat(result.getMemo()).isEqualTo("수정"); + } + + @Test + @DisplayName("링크를 삭제할 수 있다") + void shouldDeleteLink() { + // given + Member member = Member.builder().email("test@example.com").build(); + Link link = Link.builder().member(member).build(); + + given(linkQueryService.findById(1L, member)).willReturn(link); + + // when + linkService.deleteLink(1L, member); + + // then + verify(linkCommandService, times(1)).deleteLink(link); + } + + @Test + @DisplayName("단일 링크 Entity를 조회할 수 있다") + void shouldGetLink() { + // given + Member member = Member.builder().email("test@example.com").build(); + Link link = Link.builder().member(member).url("https://example.com").build(); + + given(linkQueryService.findById(1L, member)).willReturn(link); + + // when + Link result = linkService.getLink(1L, member); + + // then + assertThat(result).isEqualTo(link); + verify(linkQueryService, times(1)).findById(1L, member); + } + + @Test + @DisplayName("링크 Entity 페이지 목록을 조회할 수 있다") + void shouldGetLinkList() { + // given + Member member = Member.builder().email("test@example.com").build(); + Link link1 = Link.builder().member(member).title("1").build(); + Link link2 = Link.builder().member(member).title("2").build(); + Pageable pageable = PageRequest.of(0, 10); + Page expectedPage = new PageImpl<>(List.of(link1, link2)); + + given(linkQueryService.findAllByMember(member, pageable)).willReturn(expectedPage); + + // when + Page result = linkService.getLinkList(member, pageable); + + // then + assertThat(result).hasSize(2); + assertThat(result.getContent().get(0)).isInstanceOf(Link.class); + } + + @Test + @DisplayName("URL로 링크 ID를 찾을 수 있다 (중복 체크용)") + void shouldFindLinkIdByUrl() { + // given + Member member = Member.builder().email("test@example.com").build(); + given(linkQueryService.findIdByUrl(member, "https://example.com")) + .willReturn(Optional.of(123L)); + + // when + Optional result = linkService.findLinkIdByUrl(member, "https://example.com"); + + // then + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo(123L); + } } diff --git a/src/test/java/com/sofa/linkiving/domain/link/service/LinkServiceTest.java b/src/test/java/com/sofa/linkiving/domain/link/service/LinkServiceTest.java index 77e454df..ea5d64ae 100644 --- a/src/test/java/com/sofa/linkiving/domain/link/service/LinkServiceTest.java +++ b/src/test/java/com/sofa/linkiving/domain/link/service/LinkServiceTest.java @@ -5,6 +5,7 @@ import static org.mockito.BDDMockito.*; import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -18,8 +19,6 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import com.sofa.linkiving.domain.link.dto.response.LinkDuplicateCheckRes; -import com.sofa.linkiving.domain.link.dto.response.LinkRes; import com.sofa.linkiving.domain.link.entity.Link; import com.sofa.linkiving.domain.link.error.LinkErrorCode; import com.sofa.linkiving.domain.member.entity.Member; @@ -61,7 +60,7 @@ void shouldCreateLink() { .willReturn(link); // when - LinkRes createdLink = linkService.createLink( + Link save = linkService.createLink( member, "https://example.com", "테스트 링크", @@ -70,8 +69,8 @@ void shouldCreateLink() { ); // then - assertThat(createdLink).isNotNull(); - assertThat(createdLink.url()).isEqualTo("https://example.com"); + assertThat(save).isNotNull(); + assertThat(save.getUrl()).isEqualTo("https://example.com"); verify(linkQueryService, times(1)).existsByUrl(member, "https://example.com"); verify(linkCommandService, times(1)).saveLink(any(), any(), any(), any(), any()); } @@ -127,11 +126,11 @@ void shouldUpdateLink() { given(linkCommandService.updateLink(any(), any(), any())).willReturn(updatedLink); // when - LinkRes result = linkService.updateLink(1L, member, "수정된 제목", null); + Link result = linkService.updateLink(1L, member, "수정된 제목", null); // then assertThat(result).isNotNull(); - assertThat(result.title()).isEqualTo("수정된 제목"); + assertThat(result.getTitle()).isEqualTo("수정된 제목"); verify(linkQueryService, times(1)).findById(1L, member); verify(linkCommandService, times(1)).updateLink(any(), any(), any()); } @@ -164,12 +163,12 @@ void shouldUpdateTitle() { .willReturn(updatedLink); // when - LinkRes result = linkService.updateTitle(1L, member, "수정된 제목"); + Link result = linkService.updateTitle(1L, member, "수정된 제목"); // then assertThat(result).isNotNull(); - assertThat(result.title()).isEqualTo("수정된 제목"); - assertThat(result.memo()).isEqualTo("원본 메모"); + assertThat(result.getTitle()).isEqualTo("수정된 제목"); + assertThat(result.getMemo()).isEqualTo("원본 메모"); verify(linkQueryService, times(1)).findById(1L, member); verify(linkCommandService, times(1)).updateLink(any(), eq("수정된 제목"), eq("원본 메모")); } @@ -202,12 +201,12 @@ void shouldUpdateMemo() { .willReturn(updatedLink); // when - LinkRes result = linkService.updateMemo(1L, member, "수정된 메모"); + Link result = linkService.updateMemo(1L, member, "수정된 메모"); // then assertThat(result).isNotNull(); - assertThat(result.title()).isEqualTo("원본 제목"); - assertThat(result.memo()).isEqualTo("수정된 메모"); + assertThat(result.getTitle()).isEqualTo("원본 제목"); + assertThat(result.getMemo()).isEqualTo("수정된 메모"); verify(linkQueryService, times(1)).findById(1L, member); verify(linkCommandService, times(1)).updateLink(any(), eq("원본 제목"), eq("수정된 메모")); } @@ -255,11 +254,11 @@ void shouldGetLink() { given(linkQueryService.findById(1L, member)).willReturn(link); // when - LinkRes result = linkService.getLink(1L, member); + Link result = linkService.getLink(1L, member); // then assertThat(result).isNotNull(); - assertThat(result.url()).isEqualTo("https://example.com"); + assertThat(result.getUrl()).isEqualTo("https://example.com"); verify(linkQueryService, times(1)).findById(1L, member); } @@ -290,7 +289,7 @@ void shouldGetLinkList() { given(linkQueryService.findAllByMember(member, pageable)).willReturn(expectedPage); // when - Page result = linkService.getLinkList(member, pageable); + Page result = linkService.getLinkList(member, pageable); // then assertThat(result).isNotNull(); @@ -310,11 +309,11 @@ void shouldCheckDuplicate() { given(linkQueryService.findIdByUrl(member, "https://example.com")).willReturn(java.util.Optional.of(123L)); // when - LinkDuplicateCheckRes result = linkService.checkDuplicate(member, "https://example.com"); + Optional result = linkService.findLinkIdByUrl(member, "https://example.com"); // then - assertThat(result.exists()).isTrue(); - assertThat(result.linkId()).isEqualTo(123L); + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo(123L); verify(linkQueryService, times(1)).findIdByUrl(member, "https://example.com"); } @@ -330,11 +329,10 @@ void shouldCheckDuplicateNotExists() { given(linkQueryService.findIdByUrl(member, "https://example.com")).willReturn(java.util.Optional.empty()); // when - LinkDuplicateCheckRes result = linkService.checkDuplicate(member, "https://example.com"); + Optional result = linkService.findLinkIdByUrl(member, "https://example.com"); // then - assertThat(result.exists()).isFalse(); - assertThat(result.linkId()).isNull(); + assertThat(result).isEmpty(); verify(linkQueryService, times(1)).findIdByUrl(member, "https://example.com"); } } From c3d5f8d1b869a99423fb5d6704a261da37f47e72 Mon Sep 17 00:00:00 2001 From: Jansoon Date: Wed, 24 Dec 2025 09:46:17 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20Controller=EC=97=90=EC=84=9C=20?= =?UTF-8?q?Api=EB=A1=9C=20=EA=B2=80=EC=A6=9D=20=EC=B1=85=EC=9E=84=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20(#167)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linkiving/domain/link/controller/LinkApi.java | 13 +++++++------ .../domain/link/controller/LinkController.java | 13 ++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) 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 7ca5cff0..be60ff70 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 @@ -19,13 +19,14 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; @Tag(name = "Link", description = "링크 관리 API") public interface LinkApi { @Operation(summary = "메타 정보 수집", description = "URL의 OG 태그를 크롤링하여 메타 정보를 반환합니다") BaseResponse scrapeMetadata( - MetaScrapeReq request, + @Valid MetaScrapeReq request, Member member ); @@ -37,14 +38,14 @@ BaseResponse checkDuplicate( @Operation(summary = "링크 생성", description = "새로운 링크를 저장합니다") BaseResponse createLink( - LinkCreateReq request, + @Valid LinkCreateReq request, Member member ); @Operation(summary = "링크 수정", description = "링크 정보를 수정합니다. null이 아닌 필드만 수정됩니다.") BaseResponse updateLink( Long id, - LinkUpdateReq request, + @Valid LinkUpdateReq request, Member member ); @@ -69,21 +70,21 @@ BaseResponse> getLinkList( @Operation(summary = "링크 제목 수정", description = "링크 제목만 수정합니다") BaseResponse updateTitle( Long id, - LinkTitleUpdateReq request, + @Valid LinkTitleUpdateReq request, Member member ); @Operation(summary = "링크 메모 수정", description = "링크 메모만 수정합니다") BaseResponse updateMemo( Long id, - LinkMemoUpdateReq request, + @Valid LinkMemoUpdateReq request, Member member ); @Operation(summary = "요약 재생성", description = "요약을 재생성 하고 신규 요약 기존 요약, 기존 및 신규 요약 비교 정보을 제공합니다.") BaseResponse recreateSummary( Long id, - @Schema(description = "요청 형식(CONCISE: 간결하게, DETAILED:자세하게)") Format format, + @Valid @Schema(description = "요청 형식(CONCISE: 간결하게, DETAILED:자세하게)") Format format, 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 98fbb326..ce6c0d36 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 @@ -29,7 +29,6 @@ import com.sofa.linkiving.global.common.BaseResponse; import com.sofa.linkiving.security.annotation.AuthMember; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @RestController @@ -42,7 +41,7 @@ public class LinkController implements LinkApi { @Override @PostMapping("/meta-scrape") public BaseResponse scrapeMetadata( - @Valid @RequestBody MetaScrapeReq request, + @RequestBody MetaScrapeReq request, @AuthMember Member member ) { MetaScrapeRes response = linkFacade.scrapeMetadata(request.url()); @@ -62,7 +61,7 @@ public BaseResponse checkDuplicate( @Override @PostMapping public BaseResponse createLink( - @Valid @RequestBody LinkCreateReq request, + @RequestBody LinkCreateReq request, @AuthMember Member member ) { LinkRes response = linkFacade.createLink( @@ -79,7 +78,7 @@ public BaseResponse createLink( @PutMapping("/{id}") public BaseResponse updateLink( @PathVariable Long id, - @Valid @RequestBody LinkUpdateReq request, + @RequestBody LinkUpdateReq request, @AuthMember Member member ) { LinkRes response = linkFacade.updateLink( @@ -125,7 +124,7 @@ public BaseResponse> getLinkList( @PatchMapping("/{id}/title") public BaseResponse updateTitle( @PathVariable Long id, - @Valid @RequestBody LinkTitleUpdateReq request, + @RequestBody LinkTitleUpdateReq request, @AuthMember Member member ) { LinkRes response = linkFacade.updateTitle(id, member, request.title()); @@ -136,7 +135,7 @@ public BaseResponse updateTitle( @PatchMapping("/{id}/memo") public BaseResponse updateMemo( @PathVariable Long id, - @Valid @RequestBody LinkMemoUpdateReq request, + @RequestBody LinkMemoUpdateReq request, @AuthMember Member member ) { LinkRes response = linkFacade.updateMemo(id, member, request.memo()); @@ -147,7 +146,7 @@ public BaseResponse updateMemo( @GetMapping("/{id}/summary") public BaseResponse recreateSummary( @PathVariable Long id, - @Valid @RequestParam Format format, + @RequestParam Format format, @AuthMember Member member ) { RecreateSummaryResponse response = linkFacade.recreateSummary(member, id, format);