Skip to content

Commit 163d0a3

Browse files
authored
feat: 소식지 좋아요 기능 구현 (#377)
* feat: 소식지 좋아요 응답 dto 생성 * feat: 소식지 좋아요 Repository 생성 * feat: 소식지 좋아요 Service 생성 * feat: 소식지 좋아요 Controller 생성 * test: 소식지 좋아요 테스트 코드 작성 * feat: 소식지 좋아요 취소 Service 생성 * feat: 소식지 좋아요 취소 Controller 생성 * test: 소식지 좋아요 취소 테스트 코드 작성 * feat: 소식지 좋아요 상태 확인 Service 생성 * feat: 소식지 좋아요 상태 확인 Controller 생성 * test: 소식지 좋아요 상태 확인 테스트 코드 작성 * refactor: newsId Long -> long으로 변경 * refactor: 좋아요 성공 및 취소 시 200 상태코드만 주도록 변경 * refactor: 좋아요 상태 확인 응답 dto에 id 제거 * style: 소식지 좋아요 상태 관련 테스트명 변경 * refactor: return 타입 ResponseEntity<Void>로 변경 * style: 테스트명에 공백 추가 * refactor: 좋아요 상태 확인 함수명 isNewsLiked로 변경
1 parent d673365 commit 163d0a3

7 files changed

Lines changed: 235 additions & 5 deletions

File tree

src/main/java/com/example/solidconnection/common/exception/ErrorCode.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,20 +100,20 @@ public enum ErrorCode {
100100

101101
// news
102102
INVALID_NEWS_ACCESS(HttpStatus.BAD_REQUEST.value(), "자신의 소식지만 제어할 수 있습니다."),
103+
ALREADY_LIKED_NEWS(HttpStatus.BAD_REQUEST.value(), "이미 좋아요한 소식지입니다."),
104+
NOT_LIKED_NEWS(HttpStatus.BAD_REQUEST.value(), "좋아요하지 않은 소식지입니다."),
103105

104106
// mentor
105107
CHANNEL_SEQUENCE_NOT_UNIQUE(HttpStatus.BAD_REQUEST.value(), "채널의 순서가 중복되었습니다."),
106108
CHANNEL_REGISTRATION_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST.value(), "등록 가능한 채널 수를 초과하였습니다."),
107-
108-
// database
109-
DATA_INTEGRITY_VIOLATION(HttpStatus.CONFLICT.value(), "데이터베이스 무결성 제약조건 위반이 발생했습니다."),
110-
111-
// mentor
112109
ALREADY_MENTOR(HttpStatus.BAD_REQUEST.value(), "이미 멘토로 등록된 사용자입니다."),
113110
MENTORING_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 멘토링 신청을 찾을 수 없습니다."),
114111
UNAUTHORIZED_MENTORING(HttpStatus.FORBIDDEN.value(), "멘토링 권한이 없습니다."),
115112
MENTORING_ALREADY_CONFIRMED(HttpStatus.BAD_REQUEST.value(), "이미 승인 또는 거절된 멘토링입니다."),
116113

114+
// database
115+
DATA_INTEGRITY_VIOLATION(HttpStatus.CONFLICT.value(), "데이터베이스 무결성 제약조건 위반이 발생했습니다."),
116+
117117
// general
118118
JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱을 할 수 없습니다."),
119119
JWT_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "JWT 토큰을 처리할 수 없습니다."),

src/main/java/com/example/solidconnection/news/controller/NewsController.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package com.example.solidconnection.news.controller;
22

33
import com.example.solidconnection.common.resolver.AuthorizedUser;
4+
import com.example.solidconnection.news.dto.LikedNewsResponse;
45
import com.example.solidconnection.news.dto.NewsCommandResponse;
56
import com.example.solidconnection.news.dto.NewsCreateRequest;
67
import com.example.solidconnection.news.dto.NewsListResponse;
78
import com.example.solidconnection.news.dto.NewsUpdateRequest;
89
import com.example.solidconnection.news.service.NewsCommandService;
10+
import com.example.solidconnection.news.service.NewsLikeService;
911
import com.example.solidconnection.news.service.NewsQueryService;
1012
import com.example.solidconnection.security.annotation.RequireRoleAccess;
1113
import com.example.solidconnection.siteuser.domain.Role;
@@ -31,6 +33,7 @@ public class NewsController {
3133

3234
private final NewsQueryService newsQueryService;
3335
private final NewsCommandService newsCommandService;
36+
private final NewsLikeService newsLikeService;
3437

3538
// todo: 추후 Slice 적용
3639
@GetMapping
@@ -77,4 +80,31 @@ public ResponseEntity<NewsCommandResponse> deleteNewsById(
7780
NewsCommandResponse newsCommandResponse = newsCommandService.deleteNewsById(siteUser, newsId);
7881
return ResponseEntity.ok(newsCommandResponse);
7982
}
83+
84+
@GetMapping("/{news-id}/like")
85+
public ResponseEntity<LikedNewsResponse> isNewsLiked(
86+
@AuthorizedUser SiteUser siteUser,
87+
@PathVariable("news-id") Long newsId
88+
) {
89+
LikedNewsResponse likedNewsResponse = newsLikeService.isNewsLiked(siteUser.getId(), newsId);
90+
return ResponseEntity.ok(likedNewsResponse);
91+
}
92+
93+
@PostMapping("/{news-id}/like")
94+
public ResponseEntity<Void> addNewsLike(
95+
@AuthorizedUser SiteUser siteUser,
96+
@PathVariable("news-id") Long newsId
97+
) {
98+
newsLikeService.addNewsLike(siteUser.getId(), newsId);
99+
return ResponseEntity.ok().build();
100+
}
101+
102+
@DeleteMapping("/{news-id}/like")
103+
public ResponseEntity<Void> cancelNewsLike(
104+
@AuthorizedUser SiteUser siteUser,
105+
@PathVariable("news-id") Long newsId
106+
) {
107+
newsLikeService.cancelNewsLike(siteUser.getId(), newsId);
108+
return ResponseEntity.ok().build();
109+
}
80110
}

src/main/java/com/example/solidconnection/news/domain/LikedNews.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,9 @@ public class LikedNews {
3333

3434
@Column(name = "site_user_id")
3535
private long siteUserId;
36+
37+
public LikedNews(long newsId, long siteUserId) {
38+
this.newsId = newsId;
39+
this.siteUserId = siteUserId;
40+
}
3641
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.example.solidconnection.news.dto;
2+
3+
public record LikedNewsResponse(
4+
boolean isLike
5+
) {
6+
7+
public static LikedNewsResponse of(boolean isLike) {
8+
return new LikedNewsResponse(isLike);
9+
}
10+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.example.solidconnection.news.repository;
2+
3+
import com.example.solidconnection.news.domain.LikedNews;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
import java.util.Optional;
7+
8+
public interface LikedNewsRepository extends JpaRepository<LikedNews, Long> {
9+
10+
boolean existsByNewsIdAndSiteUserId(long newsId, long siteUserId);
11+
12+
Optional<LikedNews> findByNewsIdAndSiteUserId(long newsId, long siteUserId);
13+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.example.solidconnection.news.service;
2+
3+
import com.example.solidconnection.common.exception.CustomException;
4+
import com.example.solidconnection.news.domain.LikedNews;
5+
import com.example.solidconnection.news.dto.LikedNewsResponse;
6+
import com.example.solidconnection.news.repository.LikedNewsRepository;
7+
import com.example.solidconnection.news.repository.NewsRepository;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.stereotype.Service;
10+
import org.springframework.transaction.annotation.Transactional;
11+
12+
import static com.example.solidconnection.common.exception.ErrorCode.ALREADY_LIKED_NEWS;
13+
import static com.example.solidconnection.common.exception.ErrorCode.NEWS_NOT_FOUND;
14+
import static com.example.solidconnection.common.exception.ErrorCode.NOT_LIKED_NEWS;
15+
16+
@RequiredArgsConstructor
17+
@Service
18+
public class NewsLikeService {
19+
20+
private final NewsRepository newsRepository;
21+
private final LikedNewsRepository likedNewsRepository;
22+
23+
@Transactional(readOnly = true)
24+
public LikedNewsResponse isNewsLiked(long siteUserId, long newsId) {
25+
if (!newsRepository.existsById(newsId)) {
26+
throw new CustomException(NEWS_NOT_FOUND);
27+
}
28+
boolean isLike = likedNewsRepository.existsByNewsIdAndSiteUserId(newsId, siteUserId);
29+
return LikedNewsResponse.of(isLike);
30+
}
31+
32+
@Transactional
33+
public void addNewsLike(long siteUserId, long newsId) {
34+
if (!newsRepository.existsById(newsId)) {
35+
throw new CustomException(NEWS_NOT_FOUND);
36+
}
37+
if (likedNewsRepository.existsByNewsIdAndSiteUserId(newsId, siteUserId)) {
38+
throw new CustomException(ALREADY_LIKED_NEWS);
39+
}
40+
LikedNews likedNews = new LikedNews(newsId, siteUserId);
41+
likedNewsRepository.save(likedNews);
42+
}
43+
44+
@Transactional
45+
public void cancelNewsLike(long siteUserId, long newsId) {
46+
if (!newsRepository.existsById(newsId)) {
47+
throw new CustomException(NEWS_NOT_FOUND);
48+
}
49+
LikedNews likedNews = likedNewsRepository.findByNewsIdAndSiteUserId(newsId, siteUserId)
50+
.orElseThrow(() -> new CustomException(NOT_LIKED_NEWS));
51+
likedNewsRepository.delete(likedNews);
52+
}
53+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.example.solidconnection.news.service;
2+
3+
import com.example.solidconnection.common.exception.CustomException;
4+
import com.example.solidconnection.news.domain.News;
5+
import com.example.solidconnection.news.dto.LikedNewsResponse;
6+
import com.example.solidconnection.news.fixture.NewsFixture;
7+
import com.example.solidconnection.news.repository.LikedNewsRepository;
8+
import com.example.solidconnection.siteuser.domain.SiteUser;
9+
import com.example.solidconnection.siteuser.fixture.SiteUserFixture;
10+
import com.example.solidconnection.support.TestContainerSpringBootTest;
11+
import org.junit.jupiter.api.BeforeEach;
12+
import org.junit.jupiter.api.DisplayName;
13+
import org.junit.jupiter.api.Nested;
14+
import org.junit.jupiter.api.Test;
15+
import org.springframework.beans.factory.annotation.Autowired;
16+
17+
import static com.example.solidconnection.common.exception.ErrorCode.ALREADY_LIKED_NEWS;
18+
import static com.example.solidconnection.common.exception.ErrorCode.NOT_LIKED_NEWS;
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode;
21+
22+
@TestContainerSpringBootTest
23+
@DisplayName("소식지 좋아요 서비스 테스트")
24+
class NewsLikeServiceTest {
25+
26+
@Autowired
27+
private NewsLikeService newsLikeService;
28+
29+
@Autowired
30+
private LikedNewsRepository likedNewsRepository;
31+
32+
@Autowired
33+
private SiteUserFixture siteUserFixture;
34+
35+
@Autowired
36+
private NewsFixture newsFixture;
37+
38+
private SiteUser user;
39+
private News news;
40+
41+
@BeforeEach
42+
void setUp() {
43+
user = siteUserFixture.사용자();
44+
news = newsFixture.소식지(siteUserFixture.멘토(1, "mentor").getId());
45+
}
46+
47+
@Nested
48+
class 소식지_좋아요_상태를_조회한다 {
49+
50+
@Test
51+
void 좋아요한_소식지의_좋아요_상태를_조회한다() {
52+
// given
53+
newsLikeService.addNewsLike(user.getId(), news.getId());
54+
55+
// when
56+
LikedNewsResponse response = newsLikeService.isNewsLiked(user.getId(), news.getId());
57+
58+
// then
59+
assertThat(response.isLike()).isTrue();
60+
}
61+
62+
@Test
63+
void 좋아요하지_않은_소식지의_좋아요_상태를_조회한다() {
64+
// when
65+
LikedNewsResponse response = newsLikeService.isNewsLiked(user.getId(), news.getId());
66+
67+
// then
68+
assertThat(response.isLike()).isFalse();
69+
}
70+
}
71+
72+
@Nested
73+
class 소식지_좋아요를_등록한다 {
74+
75+
@Test
76+
void 성공적으로_좋아요를_등록한다() {
77+
// when
78+
newsLikeService.addNewsLike(user.getId(), news.getId());
79+
80+
// then
81+
assertThat(likedNewsRepository.existsByNewsIdAndSiteUserId(news.getId(), user.getId())).isTrue();
82+
}
83+
84+
@Test
85+
void 이미_좋아요했으면_예외_응답을_반환한다() {
86+
// given
87+
newsLikeService.addNewsLike(user.getId(), news.getId());
88+
89+
// when & then
90+
assertThatCode(() -> newsLikeService.addNewsLike(user.getId(), news.getId()))
91+
.isInstanceOf(CustomException.class)
92+
.hasMessage(ALREADY_LIKED_NEWS.getMessage());
93+
}
94+
}
95+
96+
@Nested
97+
class 소식지_좋아요를_취소한다 {
98+
99+
@Test
100+
void 성공적으로_좋아요를_취소한다() {
101+
// given
102+
newsLikeService.addNewsLike(user.getId(), news.getId());
103+
104+
// when
105+
newsLikeService.cancelNewsLike(user.getId(), news.getId());
106+
107+
// then
108+
assertThat(likedNewsRepository.existsByNewsIdAndSiteUserId(news.getId(), user.getId())).isFalse();
109+
}
110+
111+
@Test
112+
void 좋아요하지_않았으면_예외_응답을_반환한다() {
113+
// when & then
114+
assertThatCode(() -> newsLikeService.cancelNewsLike(user.getId(), news.getId()))
115+
.isInstanceOf(CustomException.class)
116+
.hasMessage(NOT_LIKED_NEWS.getMessage());
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)