diff --git a/src/main/java/com/example/solidconnection/news/controller/NewsController.java b/src/main/java/com/example/solidconnection/news/controller/NewsController.java index 6ba7b2eda..263124a18 100644 --- a/src/main/java/com/example/solidconnection/news/controller/NewsController.java +++ b/src/main/java/com/example/solidconnection/news/controller/NewsController.java @@ -1,7 +1,6 @@ package com.example.solidconnection.news.controller; import com.example.solidconnection.common.resolver.AuthorizedUser; -import com.example.solidconnection.news.dto.LikedNewsResponse; import com.example.solidconnection.news.dto.NewsCommandResponse; import com.example.solidconnection.news.dto.NewsCreateRequest; import com.example.solidconnection.news.dto.NewsListResponse; @@ -37,9 +36,10 @@ public class NewsController { // todo: 추후 Slice 적용 @GetMapping public ResponseEntity findNewsBySiteUserId( - @RequestParam(value = "site-user-id") Long siteUserId + @AuthorizedUser(required = false) Long siteUserId, + @RequestParam(value = "author-id") Long authorId ) { - NewsListResponse newsListResponse = newsQueryService.findNewsBySiteUserId(siteUserId); + NewsListResponse newsListResponse = newsQueryService.findNewsByAuthorId(siteUserId, authorId); return ResponseEntity.ok(newsListResponse); } @@ -80,15 +80,6 @@ public ResponseEntity deleteNewsById( return ResponseEntity.ok(newsCommandResponse); } - @GetMapping("/{news-id}/like") - public ResponseEntity isNewsLiked( - @AuthorizedUser long siteUserId, - @PathVariable("news-id") Long newsId - ) { - LikedNewsResponse likedNewsResponse = newsLikeService.isNewsLiked(siteUserId, newsId); - return ResponseEntity.ok(likedNewsResponse); - } - @PostMapping("/{news-id}/like") public ResponseEntity addNewsLike( @AuthorizedUser long siteUserId, diff --git a/src/main/java/com/example/solidconnection/news/dto/LikedNewsResponse.java b/src/main/java/com/example/solidconnection/news/dto/LikedNewsResponse.java deleted file mode 100644 index b854b9bf0..000000000 --- a/src/main/java/com/example/solidconnection/news/dto/LikedNewsResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.example.solidconnection.news.dto; - -public record LikedNewsResponse( - boolean isLike -) { - - public static LikedNewsResponse of(boolean isLike) { - return new LikedNewsResponse(isLike); - } -} diff --git a/src/main/java/com/example/solidconnection/news/dto/NewsResponse.java b/src/main/java/com/example/solidconnection/news/dto/NewsResponse.java index 77d8cd3a3..d344080ba 100644 --- a/src/main/java/com/example/solidconnection/news/dto/NewsResponse.java +++ b/src/main/java/com/example/solidconnection/news/dto/NewsResponse.java @@ -1,6 +1,9 @@ package com.example.solidconnection.news.dto; +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; + import com.example.solidconnection.news.domain.News; +import com.fasterxml.jackson.annotation.JsonInclude; import java.time.ZonedDateTime; public record NewsResponse( @@ -9,16 +12,21 @@ public record NewsResponse( String description, String thumbnailUrl, String url, + + @JsonInclude(NON_NULL) + Boolean isLiked, + ZonedDateTime updatedAt ) { - public static NewsResponse from(News news) { + public static NewsResponse of(News news, Boolean isLiked) { return new NewsResponse( news.getId(), news.getTitle(), news.getDescription(), news.getThumbnailUrl(), news.getUrl(), + isLiked, news.getUpdatedAt() ); } diff --git a/src/main/java/com/example/solidconnection/news/repository/NewsRepository.java b/src/main/java/com/example/solidconnection/news/repository/NewsRepository.java index 4ec1798df..0d3ccf3e9 100644 --- a/src/main/java/com/example/solidconnection/news/repository/NewsRepository.java +++ b/src/main/java/com/example/solidconnection/news/repository/NewsRepository.java @@ -1,10 +1,11 @@ package com.example.solidconnection.news.repository; import com.example.solidconnection.news.domain.News; +import com.example.solidconnection.news.repository.custom.NewsCustomRepository; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; -public interface NewsRepository extends JpaRepository { +public interface NewsRepository extends JpaRepository, NewsCustomRepository { List findAllBySiteUserIdOrderByUpdatedAtDesc(long siteUserId); } diff --git a/src/main/java/com/example/solidconnection/news/repository/custom/NewsCustomRepository.java b/src/main/java/com/example/solidconnection/news/repository/custom/NewsCustomRepository.java new file mode 100644 index 000000000..ebba659e0 --- /dev/null +++ b/src/main/java/com/example/solidconnection/news/repository/custom/NewsCustomRepository.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.news.repository.custom; + +import com.example.solidconnection.news.dto.NewsResponse; +import java.util.List; + +public interface NewsCustomRepository { + + List findNewsByAuthorIdWithLikeStatus(long authorId, Long siteUserId); +} diff --git a/src/main/java/com/example/solidconnection/news/repository/custom/NewsCustomRepositoryImpl.java b/src/main/java/com/example/solidconnection/news/repository/custom/NewsCustomRepositoryImpl.java new file mode 100644 index 000000000..949d188bc --- /dev/null +++ b/src/main/java/com/example/solidconnection/news/repository/custom/NewsCustomRepositoryImpl.java @@ -0,0 +1,38 @@ +package com.example.solidconnection.news.repository.custom; + +import com.example.solidconnection.news.dto.NewsResponse; +import jakarta.persistence.EntityManager; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class NewsCustomRepositoryImpl implements NewsCustomRepository { + + private final EntityManager entityManager; + + @Override + public List findNewsByAuthorIdWithLikeStatus(long authorId, Long siteUserId) { + String jpql = """ + SELECT new com.example.solidconnection.news.dto.NewsResponse( + n.id, + n.title, + n.description, + n.thumbnailUrl, + n.url, + CASE WHEN ln.id IS NOT NULL THEN true ELSE false END, + n.updatedAt + ) + FROM News n + LEFT JOIN LikedNews ln ON n.id = ln.newsId AND ln.siteUserId = :siteUserId + WHERE n.siteUserId = :authorId + ORDER BY n.updatedAt DESC + """; + + return entityManager.createQuery(jpql, NewsResponse.class) + .setParameter("authorId", authorId) + .setParameter("siteUserId", siteUserId) + .getResultList(); + } +} diff --git a/src/main/java/com/example/solidconnection/news/service/NewsLikeService.java b/src/main/java/com/example/solidconnection/news/service/NewsLikeService.java index e0e2e5114..4b9435ab6 100644 --- a/src/main/java/com/example/solidconnection/news/service/NewsLikeService.java +++ b/src/main/java/com/example/solidconnection/news/service/NewsLikeService.java @@ -6,7 +6,6 @@ import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.news.domain.LikedNews; -import com.example.solidconnection.news.dto.LikedNewsResponse; import com.example.solidconnection.news.repository.LikedNewsRepository; import com.example.solidconnection.news.repository.NewsRepository; import lombok.RequiredArgsConstructor; @@ -20,15 +19,6 @@ public class NewsLikeService { private final NewsRepository newsRepository; private final LikedNewsRepository likedNewsRepository; - @Transactional(readOnly = true) - public LikedNewsResponse isNewsLiked(long siteUserId, long newsId) { - if (!newsRepository.existsById(newsId)) { - throw new CustomException(NEWS_NOT_FOUND); - } - boolean isLike = likedNewsRepository.existsByNewsIdAndSiteUserId(newsId, siteUserId); - return LikedNewsResponse.of(isLike); - } - @Transactional public void addNewsLike(long siteUserId, long newsId) { if (!newsRepository.existsById(newsId)) { diff --git a/src/main/java/com/example/solidconnection/news/service/NewsQueryService.java b/src/main/java/com/example/solidconnection/news/service/NewsQueryService.java index cda55c1dd..e0050643b 100644 --- a/src/main/java/com/example/solidconnection/news/service/NewsQueryService.java +++ b/src/main/java/com/example/solidconnection/news/service/NewsQueryService.java @@ -1,6 +1,5 @@ package com.example.solidconnection.news.service; -import com.example.solidconnection.news.domain.News; import com.example.solidconnection.news.dto.NewsListResponse; import com.example.solidconnection.news.dto.NewsResponse; import com.example.solidconnection.news.repository.NewsRepository; @@ -16,11 +15,19 @@ public class NewsQueryService { private final NewsRepository newsRepository; @Transactional(readOnly = true) - public NewsListResponse findNewsBySiteUserId(long siteUserId) { - List newsList = newsRepository.findAllBySiteUserIdOrderByUpdatedAtDesc(siteUserId); - List newsResponseList = newsList.stream() - .map(NewsResponse::from) - .toList(); + public NewsListResponse findNewsByAuthorId(Long siteUserId, long authorId) { + // 로그인하지 않은 경우 + if (siteUserId == null) { + List newsResponseList = newsRepository.findAllBySiteUserIdOrderByUpdatedAtDesc(authorId) + .stream() + .map(news -> NewsResponse.of(news, null)) + .toList(); + return NewsListResponse.from(newsResponseList); + } + + // 로그인한 경우 + List newsResponseList = newsRepository.findNewsByAuthorIdWithLikeStatus(authorId, siteUserId); + return NewsListResponse.from(newsResponseList); } } diff --git a/src/test/java/com/example/solidconnection/news/fixture/LikedNewsFixture.java b/src/test/java/com/example/solidconnection/news/fixture/LikedNewsFixture.java new file mode 100644 index 000000000..acacb25b0 --- /dev/null +++ b/src/test/java/com/example/solidconnection/news/fixture/LikedNewsFixture.java @@ -0,0 +1,19 @@ +package com.example.solidconnection.news.fixture; + +import com.example.solidconnection.news.domain.LikedNews; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class LikedNewsFixture { + + private final LikedNewsFixtureBuilder likedNewsFixtureBuilder; + + public LikedNews 소식지_좋아요(long newsId, long siteUserId) { + return likedNewsFixtureBuilder.likedNews() + .newsId(newsId) + .siteUserId(siteUserId) + .create(); + } +} diff --git a/src/test/java/com/example/solidconnection/news/fixture/LikedNewsFixtureBuilder.java b/src/test/java/com/example/solidconnection/news/fixture/LikedNewsFixtureBuilder.java new file mode 100644 index 000000000..8554dc6b9 --- /dev/null +++ b/src/test/java/com/example/solidconnection/news/fixture/LikedNewsFixtureBuilder.java @@ -0,0 +1,36 @@ +package com.example.solidconnection.news.fixture; + +import com.example.solidconnection.news.domain.LikedNews; +import com.example.solidconnection.news.repository.LikedNewsRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class LikedNewsFixtureBuilder { + + private final LikedNewsRepository likedNewsRepository; + + private long newsId; + + private long siteUserId; + + public LikedNewsFixtureBuilder likedNews() { + return new LikedNewsFixtureBuilder(likedNewsRepository); + } + + public LikedNewsFixtureBuilder newsId(long newsId) { + this.newsId = newsId; + return this; + } + + public LikedNewsFixtureBuilder siteUserId(long siteUserId) { + this.siteUserId = siteUserId; + return this; + } + + public LikedNews create() { + LikedNews likedNews = new LikedNews(newsId, siteUserId); + return likedNewsRepository.save(likedNews); + } +} diff --git a/src/test/java/com/example/solidconnection/news/service/NewsLikeServiceTest.java b/src/test/java/com/example/solidconnection/news/service/NewsLikeServiceTest.java index e620fa206..2600c2891 100644 --- a/src/test/java/com/example/solidconnection/news/service/NewsLikeServiceTest.java +++ b/src/test/java/com/example/solidconnection/news/service/NewsLikeServiceTest.java @@ -7,7 +7,6 @@ import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.news.domain.News; -import com.example.solidconnection.news.dto.LikedNewsResponse; import com.example.solidconnection.news.fixture.NewsFixture; import com.example.solidconnection.news.repository.LikedNewsRepository; import com.example.solidconnection.siteuser.domain.SiteUser; @@ -44,31 +43,6 @@ void setUp() { news = newsFixture.소식지(siteUserFixture.멘토(1, "mentor").getId()); } - @Nested - class 소식지_좋아요_상태를_조회한다 { - - @Test - void 좋아요한_소식지의_좋아요_상태를_조회한다() { - // given - newsLikeService.addNewsLike(user.getId(), news.getId()); - - // when - LikedNewsResponse response = newsLikeService.isNewsLiked(user.getId(), news.getId()); - - // then - assertThat(response.isLike()).isTrue(); - } - - @Test - void 좋아요하지_않은_소식지의_좋아요_상태를_조회한다() { - // when - LikedNewsResponse response = newsLikeService.isNewsLiked(user.getId(), news.getId()); - - // then - assertThat(response.isLike()).isFalse(); - } - } - @Nested class 소식지_좋아요를_등록한다 { diff --git a/src/test/java/com/example/solidconnection/news/service/NewsQueryServiceTest.java b/src/test/java/com/example/solidconnection/news/service/NewsQueryServiceTest.java index 12bfe2c59..6c69db0cf 100644 --- a/src/test/java/com/example/solidconnection/news/service/NewsQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/news/service/NewsQueryServiceTest.java @@ -6,12 +6,15 @@ import com.example.solidconnection.news.domain.News; import com.example.solidconnection.news.dto.NewsListResponse; import com.example.solidconnection.news.dto.NewsResponse; +import com.example.solidconnection.news.fixture.LikedNewsFixture; import com.example.solidconnection.news.fixture.NewsFixture; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -29,25 +32,70 @@ class NewsQueryServiceTest { @Autowired private NewsFixture newsFixture; + @Autowired + private LikedNewsFixture likedNewsFixture; + @Test - void 특정_사용자의_소식지_목록을_성공적으로_조회한다() { + void 로그인하지_않은_사용자가_특정_사용자의_소식지_목록을_성공적으로_조회한다() { // given - SiteUser user1 = siteUserFixture.멘토(1, "mentor1"); - SiteUser user2 = siteUserFixture.멘토(2, "mentor2"); - News news1 = newsFixture.소식지(user1.getId()); - News news2 = newsFixture.소식지(user1.getId()); - newsFixture.소식지(user2.getId()); + SiteUser author = siteUserFixture.멘토(1, "author"); + SiteUser otherUser = siteUserFixture.멘토(2, "other"); + + News news1 = newsFixture.소식지(author.getId()); + News news2 = newsFixture.소식지(author.getId()); + newsFixture.소식지(otherUser.getId()); List newsList = List.of(news1, news2); // when - NewsListResponse response = newsQueryService.findNewsBySiteUserId(user1.getId()); + NewsListResponse response = newsQueryService.findNewsByAuthorId(null, author.getId()); // then assertAll( - () -> assertThat(response.newsResponseList()).hasSize(newsList.size()), + () -> assertThat(response.newsResponseList()) + .extracting(NewsResponse::id) + .containsExactlyInAnyOrder(news1.getId(), news2.getId()), + () -> assertThat(response.newsResponseList()) + .extracting(NewsResponse::updatedAt) + .isSortedAccordingTo(Comparator.reverseOrder()), + () -> assertThat(response.newsResponseList()) + .extracting(NewsResponse::isLiked) + .containsOnly((Boolean) null) + ); + } + + @Test + void 로그인한_사용자가_특정_사용자의_소식지_목록을_성공적으로_조회한다() { + // given + SiteUser author = siteUserFixture.멘토(1, "author"); + SiteUser loginUser = siteUserFixture.멘토(2, "loginUser"); + + News news1 = newsFixture.소식지(author.getId()); + News news2 = newsFixture.소식지(author.getId()); + News news3 = newsFixture.소식지(author.getId()); + + likedNewsFixture.소식지_좋아요(news1.getId(), loginUser.getId()); + likedNewsFixture.소식지_좋아요(news3.getId(), loginUser.getId()); + + List newsList = List.of(news1, news2, news3); + + // when + NewsListResponse response = newsQueryService.findNewsByAuthorId(loginUser.getId(), author.getId()); + + // then + assertAll( + () -> assertThat(response.newsResponseList()) + .extracting(NewsResponse::id) + .containsExactlyInAnyOrder(news1.getId(), news2.getId(), news3.getId()), () -> assertThat(response.newsResponseList()) .extracting(NewsResponse::updatedAt) - .isSortedAccordingTo(Comparator.reverseOrder()) + .isSortedAccordingTo(Comparator.reverseOrder()), + () -> { + Map likeStatusMap = response.newsResponseList().stream() + .collect(Collectors.toMap(NewsResponse::id, NewsResponse::isLiked)); + assertThat(likeStatusMap.get(news1.getId())).isTrue(); + assertThat(likeStatusMap.get(news2.getId())).isFalse(); + assertThat(likeStatusMap.get(news3.getId())).isTrue(); + } ); } }