-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] 좋아요 댓글 API 개발 #528
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
[FEAT] 좋아요 댓글 API 개발 #528
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
5d5bfab
[FEAT] 댓글 좋아요 API 개발
dlchdaud123 f2a5f84
[FEAT] 댓글 좋아요 테스트 코드 개발
dlchdaud123 97daccd
[ADD] 댓글 좋아요 명세서 수정
dlchdaud123 be3169f
Merge branch 'develop' into feat/comment/post-like
dlchdaud123 8a9ffeb
Merge branch 'develop' into feat/comment/post-like
dlchdaud123 de072c2
댓글 좋아요 Response DTO 수정
dlchdaud123 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| == 특정 댓글의 좋아요 작성 API | ||
|
|
||
| === 설명 | ||
|
|
||
| - #POST# `/api/v1/comments/{commentId}/like` | ||
| - 특정 댓글에 좋아요를 하거나 취소하는 API 입니다. | ||
| - `status` 필드가 `true`이면 좋아요, `false`이면 좋아요 취소입니다. | ||
|
|
||
| === 개발 이력 | ||
|
|
||
| - Sprint 24 (2026-02-04): 기능 개발이 완료되었습니다. | ||
|
|
||
| === 개발 참고 사항 | ||
|
|
||
| - <<공통-개발-참고-사항,공통 개발 참고 사항>>을 참고하세요. | ||
|
|
||
| === 코드 샘플 | ||
|
|
||
| operation::create-comment-like/create-comment-like_success[snippets="curl-request,http-request,http-response"] | ||
|
|
||
| === 매개 변수 | ||
|
|
||
| operation::create-comment-like/create-comment-like_success[snippets="request-headers,path-parameters,request-fields,response-fields"] | ||
|
|
||
| ==== 응답 상태 코드 | ||
|
|
||
| |=== | ||
| |상태 코드|설명 | ||
| |201|리소스가 성공적으로 생성되었습니다. (좋아요 성공/취소 모두 성공 응답) | ||
| |401|인증되지 않은 요청입니다. Access Token이 없거나 유효하지 않습니다. | ||
| |404|존재하지 않는 댓글입니다. | ||
| |=== |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| = 좋아요 API | ||
| :doctype: book | ||
| :source-highlighter: highlightjs | ||
| :toc: left | ||
| :toclevels: 3 | ||
| :seclinks: | ||
|
|
||
| :operation-request-headers-title: 요청 헤더 | ||
| :operation-path-parameters-title: 경로 매개 변수 | ||
| :operation-request-parts-title: 요청 파트(Form Data) | ||
| :operation-request-fields-title: 요청 필드 | ||
| :operation-response-fields-title: 응답 필드 | ||
| :operation-curl-request-title: Curl 요청 예시 | ||
| :operation-http-request-title: HTTP 요청 예시 | ||
| :operation-http-response-title: HTTP 응답 예시 | ||
|
|
||
| [[공통-개발-참고-사항]] | ||
| == 공통 개발 참고 사항 | ||
|
|
||
| include::overview/공통-개발-참고-사항.adoc[] | ||
|
|
||
| include::like/특정-댓글의-좋아요-작성-API.adoc[] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
src/main/java/com/project200/undabang/like/controller/CommentLikeCommandController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package com.project200.undabang.like.controller; | ||
|
|
||
| import com.project200.undabang.common.web.response.CommonResponse; | ||
| import com.project200.undabang.like.dto.request.CreateCommentLikeRequest; | ||
| import com.project200.undabang.like.dto.response.CreateCommentLikeResponse; | ||
| import com.project200.undabang.like.service.impl.CommentCommandLikeServiceImpl; | ||
| import jakarta.validation.Valid; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api") | ||
| public class CommentLikeCommandController { | ||
|
|
||
| private final CommentCommandLikeServiceImpl commentCommandLikeService; | ||
|
|
||
| @PostMapping("/v1/comments/{commentId}/like") | ||
| public ResponseEntity<CommonResponse<CreateCommentLikeResponse>> createCommentLike(@PathVariable Long commentId, | ||
| @Valid @RequestBody CreateCommentLikeRequest request) { | ||
| return ResponseEntity.status(HttpStatus.CREATED) | ||
| .body(CommonResponse.create(commentCommandLikeService.createCommentLike(commentId, request))); | ||
|
|
||
| } | ||
| } |
8 changes: 8 additions & 0 deletions
8
src/main/java/com/project200/undabang/like/dto/request/CreateCommentLikeRequest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.project200.undabang.like.dto.request; | ||
|
|
||
| import jakarta.validation.constraints.NotNull; | ||
|
|
||
| public record CreateCommentLikeRequest( | ||
| @NotNull(message = "좋아요 여부는 필수 값입니다.") | ||
| Boolean liked) { | ||
| } | ||
6 changes: 6 additions & 0 deletions
6
src/main/java/com/project200/undabang/like/dto/response/CreateCommentLikeResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package com.project200.undabang.like.dto.response; | ||
|
|
||
| public record CreateCommentLikeResponse( | ||
| Boolean liked, | ||
| Integer likesCount) { | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
src/main/java/com/project200/undabang/like/repository/CommentLikeRepository.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,13 @@ | ||
| package com.project200.undabang.like.repository; | ||
|
|
||
| import com.project200.undabang.comment.entity.Comment; | ||
| import com.project200.undabang.like.entity.CommentLike; | ||
| import com.project200.undabang.member.entity.Member; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| public interface CommentLikeRepository extends JpaRepository<CommentLike, Long> { | ||
| Optional<CommentLike> findByCommentAndMember(Comment comment, Member member); | ||
| } | ||
|
|
9 changes: 9 additions & 0 deletions
9
src/main/java/com/project200/undabang/like/service/CommentCommandLikeService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.project200.undabang.like.service; | ||
|
|
||
| import com.project200.undabang.like.dto.request.CreateCommentLikeRequest; | ||
| import com.project200.undabang.like.dto.response.CreateCommentLikeResponse; | ||
|
|
||
| public interface CommentCommandLikeService { | ||
|
|
||
| CreateCommentLikeResponse createCommentLike(Long CommentId, CreateCommentLikeRequest request); | ||
| } |
61 changes: 61 additions & 0 deletions
61
src/main/java/com/project200/undabang/like/service/impl/CommentCommandLikeServiceImpl.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package com.project200.undabang.like.service.impl; | ||
|
|
||
| import com.project200.undabang.comment.entity.Comment; | ||
| import com.project200.undabang.comment.repository.CommentRepository; | ||
| import com.project200.undabang.common.context.UserContextHolder; | ||
| import com.project200.undabang.common.web.exception.CustomException; | ||
| import com.project200.undabang.common.web.exception.ErrorCode; | ||
| import com.project200.undabang.like.dto.request.CreateCommentLikeRequest; | ||
| import com.project200.undabang.like.dto.response.CreateCommentLikeResponse; | ||
| import com.project200.undabang.like.entity.CommentLike; | ||
| import com.project200.undabang.like.repository.CommentLikeRepository; | ||
| import com.project200.undabang.like.service.CommentCommandLikeService; | ||
| import com.project200.undabang.member.entity.Member; | ||
| import com.project200.undabang.member.repository.MemberRepository; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.util.Optional; | ||
| import java.util.UUID; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| @Transactional | ||
| public class CommentCommandLikeServiceImpl implements CommentCommandLikeService { | ||
|
|
||
| private final CommentLikeRepository commentLikeRepository; | ||
| private final MemberRepository memberRepository; | ||
| private final CommentRepository commentRepository; | ||
|
|
||
| @Override | ||
| public CreateCommentLikeResponse createCommentLike(Long commentId, CreateCommentLikeRequest request) { | ||
| UUID currentUserId = UserContextHolder.getUserId(); | ||
|
|
||
| // 댓글 존재 여부 검증 | ||
| Comment comment = commentRepository.findByIdAndDeletedAtIsNull(commentId) | ||
| .orElseThrow(() -> new CustomException(ErrorCode.COMMENT_NOT_FOUND)); | ||
|
|
||
| // 현재 사용자 조회 | ||
| Member member = memberRepository.findByMemberIdAndMemberDeletedAtNull(currentUserId) | ||
| .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND)); | ||
|
|
||
| // 댓글 좋아요 생성 | ||
| Optional<CommentLike> existingLike = commentLikeRepository.findByCommentAndMember(comment, member); | ||
|
|
||
| if (Boolean.TRUE.equals(request.liked())) { | ||
| // 좋아요 생성: 이미 존재하면 아무것도 안 함 | ||
| if (existingLike.isEmpty()) { | ||
| commentLikeRepository.save(CommentLike.create(comment, member)); | ||
| comment.incrementLikesCount(); | ||
| } | ||
| } else { | ||
| // 좋아요 취소: 존재하면 삭제 | ||
| existingLike.ifPresent(like -> { | ||
| commentLikeRepository.delete(like); | ||
| comment.decrementLikesCount(); | ||
| }); | ||
| } | ||
|
dlchdaud123 marked this conversation as resolved.
|
||
| return new CreateCommentLikeResponse(request.liked(), comment.getLikesCount()); | ||
| } | ||
| } | ||
113 changes: 113 additions & 0 deletions
113
src/test/java/com/project200/undabang/like/controller/CommentLikeCommandControllerTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| package com.project200.undabang.like.controller; | ||
|
|
||
| import com.project200.undabang.common.web.exception.CustomException; | ||
| import com.project200.undabang.common.web.exception.ErrorCode; | ||
| import com.project200.undabang.common.web.response.CommonResponse; | ||
| import com.project200.undabang.configuration.AbstractRestDocSupport; | ||
| import com.project200.undabang.like.dto.request.CreateCommentLikeRequest; | ||
| import com.project200.undabang.like.dto.response.CreateCommentLikeResponse; | ||
| import com.project200.undabang.like.service.impl.CommentCommandLikeServiceImpl; | ||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Nested; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.mockito.BDDMockito; | ||
| import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.restdocs.payload.JsonFieldType; | ||
| import org.springframework.test.context.bean.override.mockito.MockitoBean; | ||
| import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; | ||
|
|
||
| import java.util.UUID; | ||
|
|
||
| import static com.project200.undabang.configuration.DocumentFormatGenerator.getTypeFormat; | ||
| import static com.project200.undabang.configuration.HeadersGenerator.getCommonApiHeaders; | ||
| import static com.project200.undabang.configuration.RestDocsUtils.*; | ||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; | ||
| import static org.springframework.restdocs.payload.PayloadDocumentation.*; | ||
| import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; | ||
| import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; | ||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | ||
|
|
||
| @WebMvcTest(CommentLikeCommandController.class) | ||
| @DisplayName("CommentCommandLikeController 테스트") | ||
| class CommentLikeCommandControllerTest extends AbstractRestDocSupport { | ||
|
|
||
| @MockitoBean | ||
| private CommentCommandLikeServiceImpl commentCommandLikeService; | ||
|
|
||
| @Nested | ||
| @DisplayName("createCommentLike 메소드는") | ||
| class CreateCommentLike { | ||
|
|
||
| @Test | ||
| @DisplayName("댓글 좋아요를 성공한다") | ||
| void createCommentLike_success() throws Exception { | ||
| // given | ||
| UUID testMemberId = UUID.randomUUID(); | ||
| Long commentId = 5L; | ||
| CreateCommentLikeRequest request = new CreateCommentLikeRequest(true); | ||
| CreateCommentLikeResponse responseDto = new CreateCommentLikeResponse(true, 1); | ||
|
|
||
| BDDMockito | ||
| .given(commentCommandLikeService.createCommentLike(BDDMockito.eq(commentId), | ||
| BDDMockito.any(CreateCommentLikeRequest.class))) | ||
| .willReturn(responseDto); | ||
|
|
||
| // when | ||
| String response = mockMvc | ||
| .perform(MockMvcRequestBuilders | ||
| .post("/api/v1/comments/{commentId}/like", commentId) | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .accept(MediaType.APPLICATION_JSON) | ||
| .headers(getCommonApiHeaders(testMemberId)) | ||
| .content(objectMapper.writeValueAsString(request))) | ||
| .andExpect(status().isCreated()) | ||
| .andDo(document.document( | ||
| requestHeaders(HEADER_ACCESS_TOKEN), | ||
| pathParameters( | ||
| parameterWithName("commentId").attributes( | ||
| getTypeFormat(JsonFieldType.NUMBER)) | ||
| .description("좋아요할 댓글의 ID")), | ||
| requestFields( | ||
| fieldWithPath("liked") | ||
| .type(JsonFieldType.BOOLEAN) | ||
| .description("좋아요 상태 (true: 좋아요, false: 좋아요 취소)")), | ||
| responseFields(commonResponseFields( | ||
| fieldWithPath("data.liked") | ||
| .type(JsonFieldType.BOOLEAN) | ||
| .description("좋아요 여부"), | ||
| fieldWithPath("data.likesCount") | ||
| .type(JsonFieldType.NUMBER) | ||
| .description("댓글 좋아요 수"))))) | ||
| .andReturn().getResponse().getContentAsString(); | ||
|
|
||
| // then | ||
| CommonResponse<CreateCommentLikeResponse> expectedData = CommonResponse.create(responseDto); | ||
| String expected = objectMapper.writeValueAsString(expectedData); | ||
| assertThat(response).as("응답 본문 검증").isEqualTo(expected); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("존재하지 않는 댓글 ID로 좋아요 시도 시 실패한다") | ||
| void createCommentLike_Failed_CommentNotFound() throws Exception { | ||
| // given | ||
| UUID testMemberId = UUID.randomUUID(); | ||
| Long commentId = 999L; | ||
| CreateCommentLikeRequest request = new CreateCommentLikeRequest(true); | ||
|
|
||
| BDDMockito | ||
| .given(commentCommandLikeService.createCommentLike(BDDMockito.eq(commentId), | ||
| BDDMockito.any(CreateCommentLikeRequest.class))) | ||
| .willThrow(new CustomException(ErrorCode.COMMENT_NOT_FOUND)); | ||
|
|
||
| // when | ||
| mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/comments/{commentId}/like", commentId) | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .accept(MediaType.APPLICATION_JSON) | ||
| .headers(getCommonApiHeaders(testMemberId)) | ||
| .content(objectMapper.writeValueAsString(request))) | ||
| .andExpect(status().isNotFound()); | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.