Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1f1b2e0
feat: 소식지 좋아요 응답 dto 생성
Gyuhyeok99 Jul 7, 2025
e83154a
feat: 소식지 좋아요 Repository 생성
Gyuhyeok99 Jul 7, 2025
a1d0caf
feat: 소식지 좋아요 Service 생성
Gyuhyeok99 Jul 7, 2025
0b11221
feat: 소식지 좋아요 Controller 생성
Gyuhyeok99 Jul 7, 2025
f6a3844
test: 소식지 좋아요 테스트 코드 작성
Gyuhyeok99 Jul 7, 2025
f7bf68f
feat: 소식지 좋아요 취소 Service 생성
Gyuhyeok99 Jul 7, 2025
1ebb78f
feat: 소식지 좋아요 취소 Controller 생성
Gyuhyeok99 Jul 7, 2025
feec4ed
test: 소식지 좋아요 취소 테스트 코드 작성
Gyuhyeok99 Jul 7, 2025
9a06d62
feat: 소식지 좋아요 상태 확인 Service 생성
Gyuhyeok99 Jul 7, 2025
82fd744
feat: 소식지 좋아요 상태 확인 Controller 생성
Gyuhyeok99 Jul 7, 2025
5e57fc6
test: 소식지 좋아요 상태 확인 테스트 코드 작성
Gyuhyeok99 Jul 7, 2025
09e0fe7
refactor: newsId Long -> long으로 변경
Gyuhyeok99 Jul 8, 2025
2458c25
refactor: 좋아요 성공 및 취소 시 200 상태코드만 주도록 변경
Gyuhyeok99 Jul 8, 2025
c9fd16f
refactor: 좋아요 상태 확인 응답 dto에 id 제거
Gyuhyeok99 Jul 8, 2025
6e1aa83
style: 소식지 좋아요 상태 관련 테스트명 변경
Gyuhyeok99 Jul 8, 2025
c988806
refactor: return 타입 ResponseEntity<Void>로 변경
Gyuhyeok99 Jul 8, 2025
fdbc668
style: 테스트명에 공백 추가
Gyuhyeok99 Jul 8, 2025
a635ca3
refactor: 좋아요 상태 확인 함수명 isNewsLiked로 변경
Gyuhyeok99 Jul 8, 2025
e70bdd8
fix: 충돌 해결
Gyuhyeok99 Jul 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -100,20 +100,20 @@ public enum ErrorCode {

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

// mentor
CHANNEL_SEQUENCE_NOT_UNIQUE(HttpStatus.BAD_REQUEST.value(), "채널의 순서가 중복되었습니다."),
CHANNEL_REGISTRATION_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST.value(), "등록 가능한 채널 수를 초과하였습니다."),

// database
DATA_INTEGRITY_VIOLATION(HttpStatus.CONFLICT.value(), "데이터베이스 무결성 제약조건 위반이 발생했습니다."),

// mentor
ALREADY_MENTOR(HttpStatus.BAD_REQUEST.value(), "이미 멘토로 등록된 사용자입니다."),
MENTORING_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 멘토링 신청을 찾을 수 없습니다."),
UNAUTHORIZED_MENTORING(HttpStatus.FORBIDDEN.value(), "멘토링 권한이 없습니다."),
MENTORING_ALREADY_CONFIRMED(HttpStatus.BAD_REQUEST.value(), "이미 승인 또는 거절된 멘토링입니다."),

// database
DATA_INTEGRITY_VIOLATION(HttpStatus.CONFLICT.value(), "데이터베이스 무결성 제약조건 위반이 발생했습니다."),

// general
JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱을 할 수 없습니다."),
JWT_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "JWT 토큰을 처리할 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
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;
import com.example.solidconnection.news.dto.NewsUpdateRequest;
import com.example.solidconnection.news.service.NewsCommandService;
import com.example.solidconnection.news.service.NewsLikeService;
import com.example.solidconnection.news.service.NewsQueryService;
import com.example.solidconnection.security.annotation.RequireRoleAccess;
import com.example.solidconnection.siteuser.domain.Role;
Expand All @@ -31,6 +33,7 @@ public class NewsController {

private final NewsQueryService newsQueryService;
private final NewsCommandService newsCommandService;
private final NewsLikeService newsLikeService;

// todo: 추후 Slice 적용
@GetMapping
Expand Down Expand Up @@ -77,4 +80,31 @@ public ResponseEntity<NewsCommandResponse> deleteNewsById(
NewsCommandResponse newsCommandResponse = newsCommandService.deleteNewsById(siteUser, newsId);
return ResponseEntity.ok(newsCommandResponse);
}

@GetMapping("/{news-id}/like")
public ResponseEntity<LikedNewsResponse> isNewsLiked(
@AuthorizedUser SiteUser siteUser,
@PathVariable("news-id") Long newsId
) {
LikedNewsResponse likedNewsResponse = newsLikeService.isNewsLiked(siteUser.getId(), newsId);
return ResponseEntity.ok(likedNewsResponse);
}

@PostMapping("/{news-id}/like")
public ResponseEntity<Void> addNewsLike(
@AuthorizedUser SiteUser siteUser,
@PathVariable("news-id") Long newsId
) {
newsLikeService.addNewsLike(siteUser.getId(), newsId);
return ResponseEntity.ok().build();
}

@DeleteMapping("/{news-id}/like")
public ResponseEntity<Void> cancelNewsLike(
@AuthorizedUser SiteUser siteUser,
@PathVariable("news-id") Long newsId
) {
newsLikeService.cancelNewsLike(siteUser.getId(), newsId);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,9 @@ public class LikedNews {

@Column(name = "site_user_id")
private long siteUserId;

public LikedNews(long newsId, long siteUserId) {
this.newsId = newsId;
this.siteUserId = siteUserId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.solidconnection.news.dto;

public record LikedNewsResponse(
boolean isLike
) {

public static LikedNewsResponse of(boolean isLike) {
return new LikedNewsResponse(isLike);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.solidconnection.news.repository;

import com.example.solidconnection.news.domain.LikedNews;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface LikedNewsRepository extends JpaRepository<LikedNews, Long> {

boolean existsByNewsIdAndSiteUserId(long newsId, long siteUserId);

Optional<LikedNews> findByNewsIdAndSiteUserId(long newsId, long siteUserId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.example.solidconnection.news.service;

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;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import static com.example.solidconnection.common.exception.ErrorCode.ALREADY_LIKED_NEWS;
import static com.example.solidconnection.common.exception.ErrorCode.NEWS_NOT_FOUND;
import static com.example.solidconnection.common.exception.ErrorCode.NOT_LIKED_NEWS;

@RequiredArgsConstructor
@Service
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)) {
throw new CustomException(NEWS_NOT_FOUND);
}
if (likedNewsRepository.existsByNewsIdAndSiteUserId(newsId, siteUserId)) {
throw new CustomException(ALREADY_LIKED_NEWS);
}
LikedNews likedNews = new LikedNews(newsId, siteUserId);
likedNewsRepository.save(likedNews);
}

@Transactional
public void cancelNewsLike(long siteUserId, long newsId) {
if (!newsRepository.existsById(newsId)) {
throw new CustomException(NEWS_NOT_FOUND);
}
LikedNews likedNews = likedNewsRepository.findByNewsIdAndSiteUserId(newsId, siteUserId)
.orElseThrow(() -> new CustomException(NOT_LIKED_NEWS));
likedNewsRepository.delete(likedNews);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.example.solidconnection.news.service;

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;
import com.example.solidconnection.siteuser.fixture.SiteUserFixture;
import com.example.solidconnection.support.TestContainerSpringBootTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

import static com.example.solidconnection.common.exception.ErrorCode.ALREADY_LIKED_NEWS;
import static com.example.solidconnection.common.exception.ErrorCode.NOT_LIKED_NEWS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode;

@TestContainerSpringBootTest
@DisplayName("소식지 좋아요 서비스 테스트")
class NewsLikeServiceTest {

@Autowired
private NewsLikeService newsLikeService;

@Autowired
private LikedNewsRepository likedNewsRepository;

@Autowired
private SiteUserFixture siteUserFixture;

@Autowired
private NewsFixture newsFixture;

private SiteUser user;
private News news;

@BeforeEach
void setUp() {
user = siteUserFixture.사용자();
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 소식지_좋아요를_등록한다 {

@Test
void 성공적으로_좋아요를_등록한다() {
// when
newsLikeService.addNewsLike(user.getId(), news.getId());

// then
assertThat(likedNewsRepository.existsByNewsIdAndSiteUserId(news.getId(), user.getId())).isTrue();
}

@Test
void 이미_좋아요했으면_예외_응답을_반환한다() {
// given
newsLikeService.addNewsLike(user.getId(), news.getId());

// when & then
assertThatCode(() -> newsLikeService.addNewsLike(user.getId(), news.getId()))
.isInstanceOf(CustomException.class)
.hasMessage(ALREADY_LIKED_NEWS.getMessage());
}
}

@Nested
class 소식지_좋아요를_취소한다 {

@Test
void 성공적으로_좋아요를_취소한다() {
// given
newsLikeService.addNewsLike(user.getId(), news.getId());

// when
newsLikeService.cancelNewsLike(user.getId(), news.getId());

// then
assertThat(likedNewsRepository.existsByNewsIdAndSiteUserId(news.getId(), user.getId())).isFalse();
}

@Test
void 좋아요하지_않았으면_예외_응답을_반환한다() {
// when & then
assertThatCode(() -> newsLikeService.cancelNewsLike(user.getId(), news.getId()))
.isInstanceOf(CustomException.class)
.hasMessage(NOT_LIKED_NEWS.getMessage());
}
}
}
Loading