Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -37,9 +36,10 @@ public class NewsController {
// todo: 추후 Slice 적용
@GetMapping
public ResponseEntity<NewsListResponse> 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);
}

Expand Down Expand Up @@ -80,15 +80,6 @@ public ResponseEntity<NewsCommandResponse> deleteNewsById(
return ResponseEntity.ok(newsCommandResponse);
}

@GetMapping("/{news-id}/like")
public ResponseEntity<LikedNewsResponse> 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<Void> addNewsLike(
@AuthorizedUser long siteUserId,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -9,16 +12,21 @@ public record NewsResponse(
String description,
String thumbnailUrl,
String url,

@JsonInclude(NON_NULL)
Boolean isLike,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

흠.. 지금 전체적인 코드에 '좋아요 했음'이 isLiked와 isLike가 혼용되고 있네요🤔
통일하는게 좋아보이는데, 둘 중 뭐가 더 좋다 생각하시나요?
(디스코드에도 질문 올려두겠습니다.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isLiked로 결정된 거 같아서 반영하였습니다~ 452b4c7


ZonedDateTime updatedAt
) {

public static NewsResponse from(News news) {
public static NewsResponse from(News news, Boolean isLike) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인자가 2개 이상이 되었으므로, of 로 정적 팩터리 메서드 이름을 변경하는게 좋겠습니다~

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했습니다! 214be6b

return new NewsResponse(
news.getId(),
news.getTitle(),
news.getDescription(),
news.getThumbnailUrl(),
news.getUrl(),
isLike,
news.getUpdatedAt()
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
package com.example.solidconnection.news.repository;

import com.example.solidconnection.news.domain.LikedNews;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface LikedNewsRepository extends JpaRepository<LikedNews, Long> {

boolean existsByNewsIdAndSiteUserId(long newsId, long siteUserId);

Optional<LikedNews> findByNewsIdAndSiteUserId(long newsId, long siteUserId);

@Query("""
SELECT l.newsId
FROM LikedNews l
WHERE l.newsId IN :newsIds AND l.siteUserId = :siteUserId
""")
Set<Long> findLikedNewsIdsByNewsIdsAndSiteUserId(@Param("newsIds") List<Long> newsIds, @Param("siteUserId") Long siteUserId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
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.LikedNewsRepository;
import com.example.solidconnection.news.repository.NewsRepository;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -14,13 +16,33 @@
public class NewsQueryService {

private final NewsRepository newsRepository;
private final LikedNewsRepository likedNewsRepository;

@Transactional(readOnly = true)
public NewsListResponse findNewsBySiteUserId(long siteUserId) {
List<News> newsList = newsRepository.findAllBySiteUserIdOrderByUpdatedAtDesc(siteUserId);
public NewsListResponse findNewsByAuthorId(Long siteUserId, long authorId) {
List<News> newsList = newsRepository.findAllBySiteUserIdOrderByUpdatedAtDesc(authorId);
Comment thread
Gyuhyeok99 marked this conversation as resolved.
Outdated

// 로그인하지 않은 경우
if (siteUserId == null) {
List<NewsResponse> newsResponseList = newsList.stream()
.map(news -> NewsResponse.from(news, null))
.toList();
return NewsListResponse.from(newsResponseList);
}

// 로그인한 경우
List<Long> newsIds = newsList.stream()
.map(News::getId)
.toList();

Set<Long> likedNewsIds = likedNewsRepository.findLikedNewsIdsByNewsIdsAndSiteUserId(newsIds, siteUserId);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

쿼리가 두 번만 발생하긴 하지만, JPQL을 사용하면 한 번으로 줄일 수 있을 것 같습니다. 어차피 SELECT 쿼리니까 DB - 영속성 컨텍스트 불일치 문제도 신경 안 써도 되구요 !

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DTO Projection을 사용해야할까요!? LikedNews랑 News를 한 번에 가져오는 걸 말씀하신거죠??

@whqtker whqtker Aug 16, 2025 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 맞습니다 ! 아 그럼 QueryDSL을 사용해야겠네요

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음 jpql이나 QueryDSL로 둘 다 가능할 거 같긴한데 쿼리 2개를 1개로 줄이려고 join과 함께 DTO Projection을 쓰는 게 맞을까란 생각도 들긴하네요

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우선은 그렇게 바꿔보겠습니다~

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음 jpql이나 QueryDSL로 둘 다 가능할 거 같긴한데 쿼리 2개를 1개로 줄이려고 join과 함께 DTO Projection을 쓰는 게 맞을까란 생각도 들긴하네요

저도 딱 이 생각입니다 ... ㅋㅋㅋ 한 번 구현해보신다면 해 보시고 배보다 배꼽이 더 큰 것 같다 하시면 그대로 가도 좋을 것 같습니다 ..!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 성혁님과 마찬가지로 규혁님의 판단에 맡기겠습니다 ㅎㅎ

참고로 '나라면 어떻게 했을까?'에 대해 생각해보자면,..
저라면 커스텀 레포지토리를 만들어서 querydsl은 사용하지 않고 jpql 프로젝션으로만 구현했을 것 같습니다!
querydsl을 사용할 정도의 복잡도는 아닌 것 같고,
join 해와서 나쁠게 없다고 생각하고,
"조회용"이라는 특정한 목적을 가졌으므로 별도의 클래스로 분리하는게 좋을 것 같다는 생각이 드네요😊

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했습니다 ~ dd1873e

List<NewsResponse> newsResponseList = newsList.stream()
.map(NewsResponse::from)
.map(news -> {
Boolean isLike = likedNewsIds.contains(news.getId());
return NewsResponse.from(news, isLike);
})
.toList();

return NewsListResponse.from(newsResponseList);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 소식지_좋아요를_등록한다 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,25 +32,66 @@ 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<News> 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()),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then 절에서 size 가 일치하는지 검증하는 것보다,
news1.getId() 와 news2.getId()가 포함되는지 검증하는게
더 정확도가 높지 않을까요?
규혁님은 어떻게 생각하시나요? ㅎㅎ

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 영서님과 같은 생각입니다!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했습니다! c86ebac

() -> assertThat(response.newsResponseList())
.extracting(NewsResponse::updatedAt)
.isSortedAccordingTo(Comparator.reverseOrder()),
() -> assertThat(response.newsResponseList())
.extracting(NewsResponse::isLike)
.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<News> newsList = List.of(news1, news2, news3);

// when
NewsListResponse response = newsQueryService.findNewsByAuthorId(loginUser.getId(), author.getId());

// then
assertAll(
() -> assertThat(response.newsResponseList()).hasSize(newsList.size()),
() -> assertThat(response.newsResponseList())
.extracting(NewsResponse::updatedAt)
.isSortedAccordingTo(Comparator.reverseOrder())
.isSortedAccordingTo(Comparator.reverseOrder()),
() -> {
Map<Long, Boolean> likeStatusMap = response.newsResponseList().stream()
.collect(Collectors.toMap(NewsResponse::id, NewsResponse::isLike));
assertThat(likeStatusMap.get(news1.getId())).isTrue();
assertThat(likeStatusMap.get(news2.getId())).isFalse();
assertThat(likeStatusMap.get(news3.getId())).isTrue();
}
);
}
}