diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index b031374..49ac425 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -30,16 +30,6 @@ jobs: with: arguments: build - - name: 테스트 커버리지를 PR에 코멘트로 등록합니다 - id: jacoco - uses: madrapps/jacoco-report@v1.2 - with: - title: 📝 테스트 커버리지 리포트입니다 - paths: ${{ github.workspace }}/build/reports/jacoco/test/jacocoTestReport.xml - token: ${{ secrets.GITHUB_TOKEN }} - min-coverage-overall: 50 - min-coverage-changed-files: 50 - # 3. Docker 이미지 빌드 - name: docker image build run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/honeycourses-backend-spring-prod . diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b19f4c1..2f41cd2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,3 +31,13 @@ jobs: if: ${{ always() }} # 테스트가 실패하여도 Report를 보기 위해 `always`로 설정 with: files: build/test-results/**/*.xml + + - name: 테스트 커버리지를 PR에 코멘트로 등록합니다 + id: jacoco + uses: madrapps/jacoco-report@v1.2 + with: + title: 📝 테스트 커버리지 리포트입니다 + paths: ${{ github.workspace }}/build/reports/jacoco/test/jacocoTestReport.xml + token: ${{ secrets.GITHUB_TOKEN }} + min-coverage-overall: 50 + min-coverage-changed-files: 50 diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index 786f792..7ea6678 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -27,5 +27,19 @@ $ProjectFileDir$ + + mysql.8 + true + true + $PROJECT_DIR$/src/main/resources/application.properties + com.mysql.cj.jdbc.Driver + jdbc:mysql://honeycourses-db.cxpckfpx9yoc.ap-northeast-2.rds.amazonaws.com/railgunT + + + + + + $ProjectFileDir$ + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 2473420..645a82f 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,9 +4,9 @@ - - - - - - - - - - - - - - - - - @@ -674,8 +838,25 @@ + - @@ -689,6 +870,8 @@ + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 74487ff..3066ef2 100644 --- a/build.gradle +++ b/build.gradle @@ -104,7 +104,7 @@ jacocoTestCoverageVerification { minimum = 0.75 } - excludes = ['*.*Controller', '*.advice.*', '*.dto.*', '*.config.*', '*.domain.*', '*.support.*'] + excludes = ['*.*Controller', '*.advice.*', '*.dto.*', '*.global.*', '*.domain.*'] } } } \ No newline at end of file diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 84f3c83..c6416eb 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -29,22 +29,20 @@ operation::reviews/find/success[snippets='http-request,http-response'] ==== 실패 ===== 최근 작성한 리뷰가 6개월 이상이거나, 없을경우 operation::reviews/find/fail/denied[snippets='http-request,http-response'] -===== 수업이 존재하지 않을 경우 -operation::reviews/find/fail/noCourse[snippets='http-request,http-response'] === 특정 강의의 리뷰 작성하기 (POST /courses/{id}/reviews) ==== 성공 operation::reviews/create/success[snippets='http-request,http-response'] -=== 리뷰 좋아요 (PUT /courses/reviews/{rid}) +=== 리뷰 좋아요 (PUT /reviews/{id}/like) ==== 성공 operation::reviews/like/success[snippets='http-request,http-response'] -=== 내 리뷰 보기 (GET /courses/reviews/me) +=== 내 리뷰 보기 (GET /reviews/me) ==== 성공 operation::reviews/find/me/success[snippets='http-request,http-response'] -=== 리뷰 수정 (PUT /courses/reviews/{rid}) +=== 리뷰 수정 (PUT /reviews/{id}) ==== 성공 operation::reviews/update/success[snippets='http-request,http-response'] @@ -52,7 +50,7 @@ operation::reviews/update/success[snippets='http-request,http-response'] ===== 권한이 없는 경우 operation::reviews/update/fail/noAuth[snippets='http-request,http-response'] -=== 리뷰 삭제 (DELETE /courses/reviews/{rid}) +=== 리뷰 삭제 (DELETE /reviews/{id}) ==== 성공 operation::reviews/delete/success[snippets='http-request,http-response'] @@ -62,36 +60,36 @@ operation::reviews/delete/fail/noAuth[snippets='http-request,http-response'] == 게시글 관리 -=== 모든 게시글 보기 (GET /community) +=== 모든 게시글 보기 (GET /posts) ==== 성공 operation::post/find/all/success[snippets='http-request,http-response'] -=== 특정 카테고리 게시글 보기 (GET /community/category/{category}) -유효한 category 값: free(자유), question(질문), trade(중고거래), offer(구인) +=== 특정 카테고리 게시글 보기 (GET /posts?page=1&category=free) +유효한 category 값: free(자유), question(질문), trade(중고거래), offer(구인), notice(공지) ==== 성공 operation::post/find/category/success[snippets='http-request,http-response'] -=== 특정 게시글 보기 (GET /community/{id}) +=== 특정 게시글 보기 (GET /posts/{id}) ==== 성공 operation::post/find/one/success[snippets='http-request,http-response'] -=== 게시글 작성하기 (POST /community) +=== 게시글 작성하기 (POST /posts) ==== 성공 operation::post/create/success[snippets='http-request,http-response'] ==== 실패 ===== 제목에 내용이 없는 경우 operation::post/create/fail/noTitle[snippets='http-request,http-response'] -=== 내 게시글 보기 (GET /community/me) +=== 내 게시글 보기 (GET /posts/me) ==== 성공 operation::post/find/me/success[snippets='http-request,http-response'] -=== 게시글 좋아요 (PUT /community/{id}/like) +=== 게시글 좋아요 (PUT /posts/{id}/like) ==== 성공 operation::post/like/success[snippets='http-request,http-response'] -=== 게시글 수정 (PUT /community/{id}) +=== 게시글 수정 (PUT /posts/{id}) ==== 성공 operation::post/update/success[snippets='http-request,http-response'] ==== 실패 @@ -100,7 +98,7 @@ operation::post/update/fail/noAuth[snippets='http-request,http-response'] ===== 제목에 내용이 없는 경우 operation::post/update/fail/noTitle[snippets='http-request,http-response'] -=== 게시글 삭제 (DELETE /community/{id}) +=== 게시글 삭제 (DELETE /posts/{id}) ==== 성공 operation::post/delete/success[snippets='http-request,http-response'] ==== 실패 diff --git a/src/main/java/org/wooriverygood/api/SampleApi.java b/src/main/java/org/wooriverygood/api/SampleApi.java new file mode 100644 index 0000000..ab55589 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/SampleApi.java @@ -0,0 +1,15 @@ +package org.wooriverygood.api; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class SampleApi { + + @GetMapping("/") + public ResponseEntity sample() { + return ResponseEntity.ok("api is online"); + } + +} diff --git a/src/main/java/org/wooriverygood/api/advice/ControllerAdvice.java b/src/main/java/org/wooriverygood/api/advice/ControllerAdvice.java deleted file mode 100644 index 628a008..0000000 --- a/src/main/java/org/wooriverygood/api/advice/ControllerAdvice.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.wooriverygood.api.advice; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.wooriverygood.api.advice.exception.general.BadRequestException; -import org.wooriverygood.api.advice.exception.general.ForbiddenException; -import org.wooriverygood.api.advice.exception.general.NotFoundException; - -@RestControllerAdvice -public class ControllerAdvice { - - @ExceptionHandler(ForbiddenException.class) - public ResponseEntity handleForbiddenException(ForbiddenException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN) - .body(new ErrorResponse(e.getMessage())); - } - - @ExceptionHandler(NotFoundException.class) - public ResponseEntity handleNotFoundException(NotFoundException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(new ErrorResponse(e.getMessage())); - } - - @ExceptionHandler(BadRequestException.class) - public ResponseEntity handleBadRequestException(BadRequestException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(new ErrorResponse(e.getMessage())); - } - - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleMethodArgumentNotValidException(BindingResult bindingResult) { - String message = bindingResult.getFieldErrors() - .get(0). - getDefaultMessage(); - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(new ErrorResponse(message)); - } -} diff --git a/src/main/java/org/wooriverygood/api/advice/ErrorResponse.java b/src/main/java/org/wooriverygood/api/advice/ErrorResponse.java deleted file mode 100644 index a37edb6..0000000 --- a/src/main/java/org/wooriverygood/api/advice/ErrorResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.wooriverygood.api.advice; - -import lombok.Getter; - -@Getter -public class ErrorResponse { - - private final String message; - - public ErrorResponse(String message) { - this.message = message; - } -} diff --git a/src/main/java/org/wooriverygood/api/comment/api/CommentApi.java b/src/main/java/org/wooriverygood/api/comment/api/CommentApi.java new file mode 100644 index 0000000..ddcdee2 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/comment/api/CommentApi.java @@ -0,0 +1,73 @@ +package org.wooriverygood.api.comment.api; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.wooriverygood.api.comment.application.*; +import org.wooriverygood.api.comment.dto.*; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.global.auth.Login; + +@RestController +@RequiredArgsConstructor +public class CommentApi { + + private final CommentLikeToggleService commentLikeToggleService; + + private final CommentFindService commentFindService; + + private final CommentCreateService commentCreateService; + + private final CommentDeleteService commentDeleteService; + + private final CommentUpdateService commentUpdateService; + + + @GetMapping("/posts/{id}/comments") + public ResponseEntity findAllCommentsByPostId(@PathVariable("id") Long postId, + @Login AuthInfo authInfo) { + CommentsResponse response = commentFindService.findAllCommentsByPostId(postId, authInfo); + return ResponseEntity.ok(response); + } + + @PostMapping("/posts/{id}/comments") + public ResponseEntity addComment(@PathVariable("id") Long postId, + @Login AuthInfo authInfo, + @Valid @RequestBody NewCommentRequest newCommentRequest) { + commentCreateService.addComment(authInfo, postId, newCommentRequest); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @PutMapping("/comments/{id}/like") + public ResponseEntity toggleCommentLike(@PathVariable("id") Long commentId, + @Login AuthInfo authInfo) { + CommentLikeResponse response = commentLikeToggleService.likeComment(commentId, authInfo); + return ResponseEntity.ok(response); + } + + @PutMapping("/comments/{id}") + public ResponseEntity updateComment(@PathVariable("id") Long commentId, + @Valid @RequestBody CommentUpdateRequest request, + @Login AuthInfo authInfo) { + commentUpdateService.updateComment(commentId, request, authInfo); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/comments/{id}") + public ResponseEntity deleteComment(@PathVariable("id") Long commentId, + @Login AuthInfo authInfo) { + commentDeleteService.deleteComment(commentId, authInfo); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/comments/{id}/reply") + public ResponseEntity addReply(@PathVariable("id") Long commentId, + @RequestBody NewReplyRequest request, + @Login AuthInfo authInfo) { + commentCreateService.addReply(commentId, request, authInfo); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + +} diff --git a/src/main/java/org/wooriverygood/api/comment/application/CommentCreateService.java b/src/main/java/org/wooriverygood/api/comment/application/CommentCreateService.java new file mode 100644 index 0000000..60c338e --- /dev/null +++ b/src/main/java/org/wooriverygood/api/comment/application/CommentCreateService.java @@ -0,0 +1,68 @@ +package org.wooriverygood.api.comment.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.comment.exception.CommentNotFoundException; +import org.wooriverygood.api.comment.exception.ReplyDepthException; +import org.wooriverygood.api.comment.domain.Comment; +import org.wooriverygood.api.comment.dto.NewCommentRequest; +import org.wooriverygood.api.comment.dto.NewReplyRequest; +import org.wooriverygood.api.comment.repository.CommentRepository; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.exception.MemberNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.post.domain.Post; +import org.wooriverygood.api.post.exception.PostNotFoundException; +import org.wooriverygood.api.post.repository.PostRepository; + +@Service +@Transactional +@RequiredArgsConstructor +public class CommentCreateService { + + private final PostRepository postRepository; + + private final CommentRepository commentRepository; + + private final MemberRepository memberRepository; + + + public void addComment(AuthInfo authInfo, Long postId, NewCommentRequest request) { + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + Post post = postRepository.findById(postId) + .orElseThrow(PostNotFoundException::new); + + Comment comment = Comment.builder() + .content(request.getContent()) + .post(post) + .member(member) + .build(); + + commentRepository.save(comment); + } + + public void addReply(Long commentId, NewReplyRequest request, AuthInfo authInfo) { + Comment parent = commentRepository.findById(commentId) + .orElseThrow(CommentNotFoundException::new); + + if (!parent.isParent()) + throw new ReplyDepthException(); + + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + + Comment child = Comment.builder() + .content(request.getContent()) + .post(parent.getPost()) + .parent(parent) + .member(member) + .build(); + parent.addReply(child); + + commentRepository.save(child); + } + +} diff --git a/src/main/java/org/wooriverygood/api/comment/application/CommentDeleteService.java b/src/main/java/org/wooriverygood/api/comment/application/CommentDeleteService.java new file mode 100644 index 0000000..4bab334 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/comment/application/CommentDeleteService.java @@ -0,0 +1,64 @@ +package org.wooriverygood.api.comment.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.comment.exception.CommentNotFoundException; +import org.wooriverygood.api.comment.domain.Comment; +import org.wooriverygood.api.comment.repository.CommentLikeRepository; +import org.wooriverygood.api.comment.repository.CommentRepository; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.exception.MemberNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; + +@Service +@Transactional +@RequiredArgsConstructor +public class CommentDeleteService { + + private final CommentRepository commentRepository; + + private final CommentLikeRepository commentLikeRepository; + + private final MemberRepository memberRepository; + + + public void deleteComment(Long commentId, AuthInfo authInfo) { + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + Comment comment = commentRepository.findById(commentId) + .orElseThrow(CommentNotFoundException::new); + + comment.validateAuthor(member); + + commentLikeRepository.deleteAllByComment(comment); + deleteCommentOrReply(comment); + } + + private void deleteCommentOrReply(Comment comment) { + if (comment.isParent()) { + deleteParent(comment); + return; + } + deleteReply(comment); + } + + private void deleteParent(Comment parent) { + if (parent.hasNoReply()) { + commentRepository.delete(parent); + return; + } + parent.willBeDeleted(); + } + + private void deleteReply(Comment reply) { + Comment parent = reply.getParent(); + parent.deleteReply(reply); + commentRepository.delete(reply); + + if (parent.canDelete()) + commentRepository.delete(parent); + } + +} diff --git a/src/main/java/org/wooriverygood/api/comment/application/CommentFindService.java b/src/main/java/org/wooriverygood/api/comment/application/CommentFindService.java new file mode 100644 index 0000000..599a19a --- /dev/null +++ b/src/main/java/org/wooriverygood/api/comment/application/CommentFindService.java @@ -0,0 +1,63 @@ +package org.wooriverygood.api.comment.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.comment.domain.Comment; +import org.wooriverygood.api.comment.dto.CommentResponse; +import org.wooriverygood.api.comment.dto.CommentsResponse; +import org.wooriverygood.api.comment.dto.ReplyResponse; +import org.wooriverygood.api.comment.repository.CommentLikeRepository; +import org.wooriverygood.api.comment.repository.CommentRepository; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.exception.MemberNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; + +import java.util.List; +import java.util.Objects; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CommentFindService { + + private final CommentRepository commentRepository; + + private final CommentLikeRepository commentLikeRepository; + + private final MemberRepository memberRepository; + + + public CommentsResponse findAllCommentsByPostId(Long postId, AuthInfo authInfo) { + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + List comments = commentRepository.findAllByPostId(postId); + List responses = comments.stream() + .map(comment -> convertToCommentResponse(comment, member)) + .filter(response -> !Objects.isNull(response)) + .toList(); + return new CommentsResponse(responses); + } + + private CommentResponse convertToCommentResponse(Comment comment, Member member) { + if (comment.isReply()) + return null; + if (comment.isSoftRemoved()) + return CommentResponse.softRemovedOf(comment, convertToReplyResponses(comment, member), comment.sameAuthor(member)); + + boolean liked = commentLikeRepository.existsByCommentAndMember(comment, member); + return CommentResponse.of(comment, convertToReplyResponses(comment, member), liked, comment.sameAuthor(member)); + } + + private List convertToReplyResponses(Comment parent, Member member) { + return parent.getReplies() + .stream() + .map(reply -> { + boolean liked = commentLikeRepository.existsByCommentAndMember(reply, member); + return ReplyResponse.of(reply, liked, reply.sameAuthor(member)); + }) + .toList(); + } + +} diff --git a/src/main/java/org/wooriverygood/api/comment/application/CommentLikeToggleService.java b/src/main/java/org/wooriverygood/api/comment/application/CommentLikeToggleService.java new file mode 100644 index 0000000..e05aef3 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/comment/application/CommentLikeToggleService.java @@ -0,0 +1,72 @@ +package org.wooriverygood.api.comment.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.comment.domain.Comment; +import org.wooriverygood.api.comment.domain.CommentLike; +import org.wooriverygood.api.comment.dto.*; +import org.wooriverygood.api.comment.repository.CommentLikeRepository; +import org.wooriverygood.api.comment.repository.CommentRepository; +import org.wooriverygood.api.comment.exception.CommentNotFoundException; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.exception.MemberNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; + +import java.util.Optional; + +@Service +@Transactional +@RequiredArgsConstructor +public class CommentLikeToggleService { + + private final CommentRepository commentRepository; + + private final CommentLikeRepository commentLikeRepository; + + private final MemberRepository memberRepository; + + + public CommentLikeResponse likeComment(Long commentId, AuthInfo authInfo) { + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + Comment comment = commentRepository.findById(commentId) + .orElseThrow(CommentNotFoundException::new); + + Optional commentLike = commentLikeRepository.findByCommentAndMember(comment, member); + + if (commentLike.isEmpty()) { + addCommentLike(comment, member); + return createCommentLikeResponse(comment, true); + } + + deleteCommentLike(comment, commentLike.get()); + return createCommentLikeResponse(comment, false); + } + + private void addCommentLike(Comment comment, Member member) { + CommentLike commentLike = CommentLike.builder() + .comment(comment) + .member(member) + .build(); + + comment.addCommentLike(commentLike); + commentLikeRepository.save(commentLike); + commentRepository.increaseLikeCount(comment.getId()); + } + + private void deleteCommentLike(Comment comment, CommentLike commentLike) { + comment.deleteCommentLike(commentLike); + commentRepository.decreaseLikeCount(comment.getId()); + } + + private CommentLikeResponse createCommentLikeResponse(Comment comment, boolean liked) { + int likeCount = comment.getLikeCount() + (liked ? 1 : -1); + return CommentLikeResponse.builder() + .likeCount(likeCount) + .liked(liked) + .build(); + } + +} diff --git a/src/main/java/org/wooriverygood/api/comment/application/CommentUpdateService.java b/src/main/java/org/wooriverygood/api/comment/application/CommentUpdateService.java new file mode 100644 index 0000000..20e8e4a --- /dev/null +++ b/src/main/java/org/wooriverygood/api/comment/application/CommentUpdateService.java @@ -0,0 +1,36 @@ +package org.wooriverygood.api.comment.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.comment.exception.CommentNotFoundException; +import org.wooriverygood.api.comment.domain.Comment; +import org.wooriverygood.api.comment.dto.CommentUpdateRequest; +import org.wooriverygood.api.comment.repository.CommentRepository; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.exception.MemberNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; + +@Service +@Transactional +@RequiredArgsConstructor +public class CommentUpdateService { + + private final CommentRepository commentRepository; + + private final MemberRepository memberRepository; + + + public void updateComment(Long commentId, CommentUpdateRequest request, AuthInfo authInfo) { + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + Comment comment = commentRepository.findById(commentId) + .orElseThrow(CommentNotFoundException::new); + + comment.validateAuthor(member); + + comment.updateContent(request.getContent()); + } + +} diff --git a/src/main/java/org/wooriverygood/api/comment/controller/CommentController.java b/src/main/java/org/wooriverygood/api/comment/controller/CommentController.java deleted file mode 100644 index b85c74c..0000000 --- a/src/main/java/org/wooriverygood/api/comment/controller/CommentController.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.wooriverygood.api.comment.controller; - -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.wooriverygood.api.comment.dto.*; -import org.wooriverygood.api.comment.service.CommentService; -import org.wooriverygood.api.support.AuthInfo; -import org.wooriverygood.api.support.Login; - -import java.util.List; - -@RestController -public class CommentController { - - private final CommentService commentService; - - - public CommentController(CommentService commentService) { - this.commentService = commentService; - } - - @GetMapping("/community/{id}/comments") - public ResponseEntity> findAllCommentsByPostId(@PathVariable("id") Long postId, - @Login AuthInfo authInfo) { - List responses = commentService.findAllComments(postId, authInfo); - return ResponseEntity.ok(responses); - } - - @PostMapping("/community/{id}/comments") - public ResponseEntity addComment(@PathVariable("id") Long postId, - @Login AuthInfo authInfo, - @Valid @RequestBody NewCommentRequest newCommentRequest) { - NewCommentResponse response = commentService.addComment(authInfo, postId, newCommentRequest); - return ResponseEntity.status(HttpStatus.CREATED).body(response); - } - - @PutMapping("/comments/{id}/like") - public ResponseEntity likeComment(@PathVariable("id") Long commentId, - @Login AuthInfo authInfo) { - CommentLikeResponse response = commentService.likeComment(commentId, authInfo); - return ResponseEntity.ok(response); - } - - @PutMapping("/comments/{id}") - public ResponseEntity updateComment(@PathVariable("id") Long commentId, - @Valid @RequestBody CommentUpdateRequest request, - @Login AuthInfo authInfo) { - CommentUpdateResponse response = commentService.updateComment(commentId, request, authInfo); - return ResponseEntity.ok(response); - } - - @DeleteMapping("/comments/{id}") - public ResponseEntity deleteComment(@PathVariable("id") Long commentId, - @Login AuthInfo authInfo) { - CommentDeleteResponse response = commentService.deleteComment(commentId, authInfo); - return ResponseEntity.ok(response); - } - - @PostMapping("/comments/{id}/reply") - public ResponseEntity addReply(@PathVariable("id") Long commentId, - @RequestBody NewReplyRequest request, - @Login AuthInfo authInfo) { - commentService.addReply(commentId, request, authInfo); - return ResponseEntity.status(HttpStatus.CREATED).build(); - } - -} diff --git a/src/main/java/org/wooriverygood/api/comment/domain/Comment.java b/src/main/java/org/wooriverygood/api/comment/domain/Comment.java index 0d43326..d5a4b0f 100644 --- a/src/main/java/org/wooriverygood/api/comment/domain/Comment.java +++ b/src/main/java/org/wooriverygood/api/comment/domain/Comment.java @@ -1,13 +1,12 @@ package org.wooriverygood.api.comment.domain; import jakarta.persistence.*; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import org.hibernate.annotations.ColumnDefault; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import org.wooriverygood.api.advice.exception.AuthorizationException; +import org.wooriverygood.api.global.error.exception.AuthorizationException; +import org.wooriverygood.api.member.domain.Member; import org.wooriverygood.api.post.domain.Post; import org.wooriverygood.api.report.domain.CommentReport; @@ -17,10 +16,10 @@ import java.util.Objects; @Entity -@Table(name = "comments") @Getter +@Table(name = "comments") @EntityListeners(AuditingEntityListener.class) -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Comment { @Id @@ -33,27 +32,27 @@ public class Comment { private Comment parent; @OneToMany(mappedBy = "parent") - private List children = new ArrayList<>(); + private List replies = new ArrayList<>(); @Column(name = "comment_content", length = 200, nullable = false) private String content; - @Column(name = "comment_author", length = 1000) - private String author; + @ManyToOne(fetch = FetchType.LAZY) + private Member member; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id", referencedColumnName = "post_id") private Post post; @OneToMany(mappedBy = "comment", cascade = CascadeType.ALL, orphanRemoval = true) - private List commentLikes; + private List commentLikes = new ArrayList<>(); @Column(name = "like_count") @ColumnDefault("0") private int likeCount; @OneToMany(mappedBy = "comment") - private List reports; + private List reports = new ArrayList<>(); @Column(name = "report_count") @ColumnDefault("0") @@ -71,16 +70,18 @@ public class Comment { private boolean updated; @Builder - public Comment(Long id, String content, String author, Post post, Comment parent, List commentLikes, List reports, boolean softRemoved, boolean updated) { + public Comment(Long id, String content, Post post, Member member, + Comment parent, List commentLikes, + List reports, boolean softRemoved, boolean updated) { this.id = id; this.content = content; - this.author = author; this.post = post; this.parent = parent; this.commentLikes = commentLikes; this.reports = reports; this.softRemoved = softRemoved; this.updated = updated; + this.member = member; } public void addCommentLike(CommentLike commentLike) { @@ -96,15 +97,19 @@ public void addReport(CommentReport report) { reports.add(report); } - public boolean hasReportByUser(String username) { + public boolean hasReportByMember(Member member) { for (CommentReport report: reports) - if (report.isOwner(username)) + if (report.isOwner(member)) return true; return false; } - public void validateAuthor(String author) { - if (!this.author.equals(author)) throw new AuthorizationException(); + public boolean sameAuthor(Member member) { + return this.member.equals(member); + } + + public void validateAuthor(Member member) { + if (!sameAuthor(member)) throw new AuthorizationException(); } public void updateContent(String content) { @@ -112,12 +117,12 @@ public void updateContent(String content) { updated = true; } - public void addChildren(Comment reply) { - children.add(reply); + public void addReply(Comment reply) { + replies.add(reply); } - public void deleteChild(Comment reply) { - children.remove(reply); + public void deleteReply(Comment reply) { + replies.remove(reply); reply.delete(); } @@ -134,7 +139,7 @@ public boolean isReply() { } public boolean hasNoReply() { - return children.isEmpty(); + return replies.isEmpty(); } public void willBeDeleted() { @@ -145,4 +150,12 @@ public boolean canDelete() { return hasNoReply() && softRemoved; } + public boolean isReportedTooMuch() { + return reportCount >= 5; + } + + public String getContent() { + return isReportedTooMuch() ? null : content; + } + } diff --git a/src/main/java/org/wooriverygood/api/comment/domain/CommentLike.java b/src/main/java/org/wooriverygood/api/comment/domain/CommentLike.java index 320e161..361b595 100644 --- a/src/main/java/org/wooriverygood/api/comment/domain/CommentLike.java +++ b/src/main/java/org/wooriverygood/api/comment/domain/CommentLike.java @@ -4,9 +4,10 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.wooriverygood.api.member.domain.Member; @Entity -@Table(name = "commentLikes") +@Table(name = "comment_likes") @Getter @NoArgsConstructor public class CommentLike { @@ -19,15 +20,15 @@ public class CommentLike { @JoinColumn(name = "comment_id", referencedColumnName = "comment_id") private Comment comment; - @Column - private String username; + @ManyToOne(fetch = FetchType.LAZY) + private Member member; @Builder - public CommentLike(Long id, Comment comment, String username) { + public CommentLike(Long id, Comment comment, Member member) { this.id = id; this.comment = comment; - this.username = username; + this.member = member; } public void delete() { diff --git a/src/main/java/org/wooriverygood/api/comment/dto/CommentDeleteResponse.java b/src/main/java/org/wooriverygood/api/comment/dto/CommentDeleteResponse.java deleted file mode 100644 index 9fb453e..0000000 --- a/src/main/java/org/wooriverygood/api/comment/dto/CommentDeleteResponse.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.wooriverygood.api.comment.dto; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class CommentDeleteResponse { - - private final Long comment_id; - - @Builder - public CommentDeleteResponse(Long comment_id) { - this.comment_id = comment_id; - } -} diff --git a/src/main/java/org/wooriverygood/api/comment/dto/CommentLikeResponse.java b/src/main/java/org/wooriverygood/api/comment/dto/CommentLikeResponse.java index dad11f8..872cdf3 100644 --- a/src/main/java/org/wooriverygood/api/comment/dto/CommentLikeResponse.java +++ b/src/main/java/org/wooriverygood/api/comment/dto/CommentLikeResponse.java @@ -1,18 +1,22 @@ package org.wooriverygood.api.comment.dto; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Builder; import lombok.Getter; @Getter +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class CommentLikeResponse { - private final int like_count; + private final int likeCount; private final boolean liked; + @Builder - public CommentLikeResponse(int like_count, boolean liked) { - this.like_count = like_count; + public CommentLikeResponse(int likeCount, boolean liked) { + this.likeCount = likeCount; this.liked = liked; } diff --git a/src/main/java/org/wooriverygood/api/comment/dto/CommentResponse.java b/src/main/java/org/wooriverygood/api/comment/dto/CommentResponse.java index d8bb30b..d610aa6 100644 --- a/src/main/java/org/wooriverygood/api/comment/dto/CommentResponse.java +++ b/src/main/java/org/wooriverygood/api/comment/dto/CommentResponse.java @@ -1,5 +1,7 @@ package org.wooriverygood.api.comment.dto; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Builder; import lombok.Getter; import org.wooriverygood.api.comment.domain.Comment; @@ -8,19 +10,18 @@ import java.util.List; @Getter +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class CommentResponse { - private final Long comment_id; + private final Long commentId; - private final String comment_content; + private final String commentContent; - private final String comment_author; + private final Long postId; - private final Long post_id; + private final int commentLikeCount; - private final int comment_likes; - - private final LocalDateTime comment_time; + private final LocalDateTime commentTime; private final boolean liked; @@ -30,50 +31,61 @@ public class CommentResponse { private final boolean reported; + private final boolean isMine; + + private final long memberId; + @Builder - public CommentResponse(Long comment_id, String comment_content, String comment_author, Long post_id, int comment_likes, LocalDateTime comment_time, boolean liked, List replies, boolean updated, boolean reported) { - this.comment_id = comment_id; - this.comment_content = comment_content; - this.comment_author = comment_author; - this.post_id = post_id; - this.comment_likes = comment_likes; - this.comment_time = comment_time; + public CommentResponse( + Long commentId, String commentContent, Long postId, + int commentLikeCount, LocalDateTime commentTime, boolean liked, + List replies, boolean updated, boolean reported, + boolean isMine, long memberId + ) { + this.commentId = commentId; + this.commentContent = commentContent; + this.postId = postId; + this.commentLikeCount = commentLikeCount; + this.commentTime = commentTime; this.liked = liked; this.replies = replies; this.updated = updated; this.reported = reported; + this.isMine = isMine; + this.memberId = memberId; } - public static CommentResponse from(Comment comment, List replies, boolean liked) { - boolean reported = comment.getReportCount() >= 5; + public static CommentResponse of(Comment comment, List replies, boolean liked, boolean isMine) { return CommentResponse.builder() - .comment_id(comment.getId()) - .comment_content(reported ? null : comment.getContent()) - .comment_author(comment.getAuthor()) - .post_id(comment.getPost().getId()) - .comment_likes(comment.getLikeCount()) - .comment_time(comment.getCreatedAt()) + .commentId(comment.getId()) + .commentContent(comment.getContent()) + .memberId(comment.getMember().getId()) + .postId(comment.getPost().getId()) + .commentLikeCount(comment.getLikeCount()) + .commentTime(comment.getCreatedAt()) .liked(liked) .replies(replies) .updated(comment.isUpdated()) - .reported(reported) + .reported(comment.isReportedTooMuch()) + .isMine(isMine) .build(); } - public static CommentResponse softRemovedFrom(Comment comment, List replies) { - boolean reported = comment.getReportCount() >= 5; + public static CommentResponse softRemovedOf(Comment comment, List replies, boolean isMine) { return CommentResponse.builder() - .comment_id(comment.getId()) - .comment_content(null) - .comment_author(comment.getAuthor()) - .post_id(comment.getPost().getId()) - .comment_likes(comment.getLikeCount()) - .comment_time(comment.getCreatedAt()) + .commentId(comment.getId()) + .commentContent(null) + .memberId(comment.getMember().getId()) + .postId(comment.getPost().getId()) + .commentLikeCount(comment.getLikeCount()) + .commentTime(comment.getCreatedAt()) .replies(replies) .updated(comment.isUpdated()) - .reported(reported) + .reported(comment.isReportedTooMuch()) + .isMine(isMine) .build(); } + } diff --git a/src/main/java/org/wooriverygood/api/comment/dto/CommentUpdateResponse.java b/src/main/java/org/wooriverygood/api/comment/dto/CommentUpdateResponse.java deleted file mode 100644 index bd85f6f..0000000 --- a/src/main/java/org/wooriverygood/api/comment/dto/CommentUpdateResponse.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.wooriverygood.api.comment.dto; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class CommentUpdateResponse { - - private final Long comment_id; - - @Builder - public CommentUpdateResponse(Long comment_id) { - this.comment_id = comment_id; - } -} diff --git a/src/main/java/org/wooriverygood/api/comment/dto/CommentsResponse.java b/src/main/java/org/wooriverygood/api/comment/dto/CommentsResponse.java new file mode 100644 index 0000000..fa05254 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/comment/dto/CommentsResponse.java @@ -0,0 +1,17 @@ +package org.wooriverygood.api.comment.dto; + +import lombok.Getter; + +import java.util.List; + +@Getter +public class CommentsResponse { + + private final List comments; + + + public CommentsResponse(List comments) { + this.comments = comments; + } + +} diff --git a/src/main/java/org/wooriverygood/api/comment/dto/NewCommentResponse.java b/src/main/java/org/wooriverygood/api/comment/dto/NewCommentResponse.java deleted file mode 100644 index 04c07b1..0000000 --- a/src/main/java/org/wooriverygood/api/comment/dto/NewCommentResponse.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.wooriverygood.api.comment.dto; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class NewCommentResponse { - - private Long comment_id; - private String content; - private String author; - - @Builder - public NewCommentResponse(Long comment_id, String content, String author) { - this.comment_id = comment_id; - this.content = content; - this.author = author; - } -} diff --git a/src/main/java/org/wooriverygood/api/comment/dto/ReplyResponse.java b/src/main/java/org/wooriverygood/api/comment/dto/ReplyResponse.java index eda61ed..7556f1a 100644 --- a/src/main/java/org/wooriverygood/api/comment/dto/ReplyResponse.java +++ b/src/main/java/org/wooriverygood/api/comment/dto/ReplyResponse.java @@ -1,5 +1,7 @@ package org.wooriverygood.api.comment.dto; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Builder; import lombok.Getter; import org.wooriverygood.api.comment.domain.Comment; @@ -7,16 +9,16 @@ import java.time.LocalDateTime; @Getter +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class ReplyResponse { - private final Long reply_id; - private final String reply_content; + private final Long replyId; - private final String reply_author; + private final String replyContent; - private final int reply_likes; + private final int replyLikeCount; - private final LocalDateTime reply_time; + private final LocalDateTime replyTime; private final boolean liked; @@ -24,31 +26,39 @@ public class ReplyResponse { private final boolean reported; + private final boolean isMine; + + private final long memberId; - @Builder - public ReplyResponse(Long reply_id, String reply_content, String reply_author, int reply_likes, LocalDateTime reply_time, boolean liked, boolean updated, boolean reported) { - this.reply_id = reply_id; - this.reply_content = reply_content; - this.reply_author = reply_author; - this.reply_likes = reply_likes; - this.reply_time = reply_time; + @Builder + public ReplyResponse( + Long replyId, String replyContent, int replyLikeCount, + LocalDateTime replyTime, boolean liked, boolean updated, + boolean reported, boolean isMine, long memberId + ) { + this.replyId = replyId; + this.replyContent = replyContent; + this.isMine = isMine; + this.memberId = memberId; + this.replyLikeCount = replyLikeCount; + this.replyTime = replyTime; this.liked = liked; this.updated = updated; this.reported = reported; } - public static ReplyResponse from(Comment reply, boolean liked) { - boolean reported = reply.getReportCount() >= 5; + public static ReplyResponse of(Comment reply, boolean liked, boolean isMine) { return ReplyResponse.builder() - .reply_id(reply.getId()) - .reply_content(reported ? null : reply.getContent()) - .reply_author(reply.getAuthor()) - .reply_likes(reply.getLikeCount()) - .reply_time(reply.getCreatedAt()) + .replyId(reply.getId()) + .replyContent(reply.getContent()) + .memberId(reply.getMember().getId()) + .replyLikeCount(reply.getLikeCount()) + .replyTime(reply.getCreatedAt()) .liked(liked) .updated(reply.isUpdated()) - .reported(reported) + .reported(reply.isReportedTooMuch()) + .isMine(isMine) .build(); } diff --git a/src/main/java/org/wooriverygood/api/advice/exception/CommentNotFoundException.java b/src/main/java/org/wooriverygood/api/comment/exception/CommentNotFoundException.java similarity index 64% rename from src/main/java/org/wooriverygood/api/advice/exception/CommentNotFoundException.java rename to src/main/java/org/wooriverygood/api/comment/exception/CommentNotFoundException.java index 83e284a..e45e533 100644 --- a/src/main/java/org/wooriverygood/api/advice/exception/CommentNotFoundException.java +++ b/src/main/java/org/wooriverygood/api/comment/exception/CommentNotFoundException.java @@ -1,6 +1,6 @@ -package org.wooriverygood.api.advice.exception; +package org.wooriverygood.api.comment.exception; -import org.wooriverygood.api.advice.exception.general.NotFoundException; +import org.wooriverygood.api.global.error.exception.NotFoundException; public class CommentNotFoundException extends NotFoundException { diff --git a/src/main/java/org/wooriverygood/api/advice/exception/ReplyDepthException.java b/src/main/java/org/wooriverygood/api/comment/exception/ReplyDepthException.java similarity index 64% rename from src/main/java/org/wooriverygood/api/advice/exception/ReplyDepthException.java rename to src/main/java/org/wooriverygood/api/comment/exception/ReplyDepthException.java index af7438a..90aee51 100644 --- a/src/main/java/org/wooriverygood/api/advice/exception/ReplyDepthException.java +++ b/src/main/java/org/wooriverygood/api/comment/exception/ReplyDepthException.java @@ -1,6 +1,6 @@ -package org.wooriverygood.api.advice.exception; +package org.wooriverygood.api.comment.exception; -import org.wooriverygood.api.advice.exception.general.BadRequestException; +import org.wooriverygood.api.global.error.exception.BadRequestException; public class ReplyDepthException extends BadRequestException { diff --git a/src/main/java/org/wooriverygood/api/comment/repository/CommentLikeRepository.java b/src/main/java/org/wooriverygood/api/comment/repository/CommentLikeRepository.java index 28c33f5..00e475d 100644 --- a/src/main/java/org/wooriverygood/api/comment/repository/CommentLikeRepository.java +++ b/src/main/java/org/wooriverygood/api/comment/repository/CommentLikeRepository.java @@ -3,14 +3,15 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.wooriverygood.api.comment.domain.Comment; import org.wooriverygood.api.comment.domain.CommentLike; +import org.wooriverygood.api.member.domain.Member; import java.util.Optional; public interface CommentLikeRepository extends JpaRepository { - Optional findByCommentAndUsername(Comment comment, String username); + Optional findByCommentAndMember(Comment comment, Member member); - boolean existsByCommentAndUsername(Comment comment, String username); + boolean existsByCommentAndMember(Comment comment, Member member); void deleteAllByComment(Comment comment); } diff --git a/src/main/java/org/wooriverygood/api/comment/service/CommentService.java b/src/main/java/org/wooriverygood/api/comment/service/CommentService.java deleted file mode 100644 index 7848b25..0000000 --- a/src/main/java/org/wooriverygood/api/comment/service/CommentService.java +++ /dev/null @@ -1,190 +0,0 @@ -package org.wooriverygood.api.comment.service; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.wooriverygood.api.advice.exception.ReplyDepthException; -import org.wooriverygood.api.comment.domain.Comment; -import org.wooriverygood.api.comment.domain.CommentLike; -import org.wooriverygood.api.comment.dto.*; -import org.wooriverygood.api.comment.repository.CommentLikeRepository; -import org.wooriverygood.api.comment.repository.CommentRepository; -import org.wooriverygood.api.advice.exception.CommentNotFoundException; -import org.wooriverygood.api.advice.exception.PostNotFoundException; -import org.wooriverygood.api.post.domain.Post; -import org.wooriverygood.api.post.repository.PostRepository; -import org.wooriverygood.api.support.AuthInfo; - -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -@Service -@Transactional(readOnly = true) -public class CommentService { - - private final CommentRepository commentRepository; - private final PostRepository postRepository; - private final CommentLikeRepository commentLikeRepository; - - - public CommentService(CommentRepository commentRepository, PostRepository postRepository, CommentLikeRepository commentLikeRepository) { - this.commentRepository = commentRepository; - this.postRepository = postRepository; - this.commentLikeRepository = commentLikeRepository; - } - - public List findAllComments(Long postId, AuthInfo authInfo) { - List comments = commentRepository.findAllByPostId(postId); - return comments.stream().map(comment -> convertToCommentResponse(comment, authInfo)) - .filter(response -> !Objects.isNull(response)) - .toList(); - } - - private CommentResponse convertToCommentResponse(Comment comment, AuthInfo authInfo) { - if (comment.isReply()) - return null; - if (comment.isSoftRemoved()) - return CommentResponse.softRemovedFrom(comment, convertToReplyResponses(comment, authInfo)); - - boolean liked = commentLikeRepository.existsByCommentAndUsername(comment, authInfo.getUsername()); - return CommentResponse.from(comment, convertToReplyResponses(comment, authInfo), liked); - } - - private List convertToReplyResponses(Comment parent, AuthInfo authInfo) { - return parent.getChildren().stream().map(reply -> { - boolean liked = commentLikeRepository.existsByCommentAndUsername(reply, authInfo.getUsername()); - return ReplyResponse.from(reply, liked); - }).toList(); - } - - @Transactional - public NewCommentResponse addComment(AuthInfo authInfo, Long postId, NewCommentRequest newCommentRequest) { - Post post = postRepository.findById(postId) - .orElseThrow(PostNotFoundException::new); - - Comment comment = Comment.builder() - .content(newCommentRequest.getContent()) - .author(authInfo.getUsername()) - .post(post) - .build(); - Comment saved = commentRepository.save(comment); - - return NewCommentResponse.builder() - .comment_id(saved.getId()) - .content(saved.getContent()) - .author(saved.getAuthor()) - .build(); - } - - @Transactional - public CommentLikeResponse likeComment(Long commentId, AuthInfo authInfo) { - Comment comment = commentRepository.findById(commentId) - .orElseThrow(CommentNotFoundException::new); - - Optional commentLike = commentLikeRepository.findByCommentAndUsername(comment, authInfo.getUsername()); - - if (commentLike.isEmpty()) { - addCommentLike(comment, authInfo.getUsername()); - return createCommentLikeResponse(comment, true); - } - - deleteCommentLike(comment, commentLike.get()); - return createCommentLikeResponse(comment, false); - } - - private void addCommentLike(Comment comment, String username) { - CommentLike commentLike = CommentLike.builder() - .comment(comment) - .username(username) - .build(); - - comment.addCommentLike(commentLike); - commentLikeRepository.save(commentLike); - commentRepository.increaseLikeCount(comment.getId()); - } - - private void deleteCommentLike(Comment comment, CommentLike commentLike) { - comment.deleteCommentLike(commentLike); - commentRepository.decreaseLikeCount(comment.getId()); - } - - private CommentLikeResponse createCommentLikeResponse(Comment comment, boolean liked) { - int likeCount = comment.getLikeCount() + (liked ? 1 : -1); - return CommentLikeResponse.builder() - .like_count(likeCount) - .liked(liked) - .build(); - } - - @Transactional - public CommentUpdateResponse updateComment(Long commentId, CommentUpdateRequest request, AuthInfo authInfo) { - Comment comment = commentRepository.findById(commentId) - .orElseThrow(CommentNotFoundException::new); - comment.validateAuthor(authInfo.getUsername()); - - comment.updateContent(request.getContent()); - - return CommentUpdateResponse.builder() - .comment_id(comment.getId()) - .build(); - } - - @Transactional - public CommentDeleteResponse deleteComment(Long commentId, AuthInfo authInfo) { - Comment comment = commentRepository.findById(commentId) - .orElseThrow(CommentNotFoundException::new); - - comment.validateAuthor(authInfo.getUsername()); - - commentLikeRepository.deleteAllByComment(comment); - deleteCommentOrReply(comment); - - return CommentDeleteResponse.builder() - .comment_id(commentId) - .build(); - } - - private void deleteCommentOrReply(Comment comment) { - if (comment.isParent()) { - deleteParent(comment); - return; - } - deleteChild(comment); - } - - private void deleteParent(Comment parent) { - if (parent.hasNoReply()) { - commentRepository.delete(parent); - return; - } - parent.willBeDeleted(); - } - - private void deleteChild(Comment reply) { - Comment parent = reply.getParent(); - parent.deleteChild(reply); - commentRepository.delete(reply); - - if (parent.canDelete()) - commentRepository.delete(parent); - } - - @Transactional - public void addReply(Long commentId, NewReplyRequest request, AuthInfo authInfo) { - Comment parent = commentRepository.findById(commentId) - .orElseThrow(CommentNotFoundException::new); - - if (!parent.isParent()) - throw new ReplyDepthException(); - - Comment child = Comment.builder() - .content(request.getContent()) - .author(authInfo.getUsername()) - .post(parent.getPost()) - .parent(parent) - .build(); - parent.addChildren(child); - - commentRepository.save(child); - } -} diff --git a/src/main/java/org/wooriverygood/api/course/api/CourseApi.java b/src/main/java/org/wooriverygood/api/course/api/CourseApi.java new file mode 100644 index 0000000..f69b9d2 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/course/api/CourseApi.java @@ -0,0 +1,40 @@ +package org.wooriverygood.api.course.api; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.wooriverygood.api.course.application.CourseFindService; +import org.wooriverygood.api.course.dto.*; +import org.wooriverygood.api.course.application.CourseCreateService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/courses") +public class CourseApi { + + private final CourseCreateService courseCreateService; + + private final CourseFindService courseFindService; + + + @GetMapping + public ResponseEntity findAllCourses() { + CoursesResponse response = courseFindService.findAll(); + return ResponseEntity.ok(response); + } + + @PostMapping + public ResponseEntity addCourse(@Valid @RequestBody NewCourseRequest newCourseRequest) { + courseCreateService.addCourse(newCourseRequest); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @GetMapping("/{id}/name") + public ResponseEntity findCourseName(@PathVariable("id") Long courseId) { + CourseNameResponse response = courseFindService.findCourseName(courseId); + return ResponseEntity.ok(response); + } + +} diff --git a/src/main/java/org/wooriverygood/api/course/application/CourseCreateService.java b/src/main/java/org/wooriverygood/api/course/application/CourseCreateService.java new file mode 100644 index 0000000..98dbd0a --- /dev/null +++ b/src/main/java/org/wooriverygood/api/course/application/CourseCreateService.java @@ -0,0 +1,34 @@ +package org.wooriverygood.api.course.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.course.domain.Course; +import org.wooriverygood.api.course.dto.NewCourseRequest; +import org.wooriverygood.api.course.repository.CourseRepository; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class CourseCreateService { + + private final CourseRepository courseRepository; + + + @Transactional + public void addCourse(NewCourseRequest request) { + Course course = createCourse(request); + courseRepository.save(course); + } + + private Course createCourse(NewCourseRequest request) { + return Course.builder() + .name(request.getCourseName()) + .category(request.getCourseCategory()) + .credit(request.getCourseCredit()) + .isYouguan(request.getIsYouguan()) + .kaikeYuanxi(request.getKaikeYuanxi()) + .build(); + } + +} diff --git a/src/main/java/org/wooriverygood/api/course/application/CourseFindService.java b/src/main/java/org/wooriverygood/api/course/application/CourseFindService.java new file mode 100644 index 0000000..b9875ae --- /dev/null +++ b/src/main/java/org/wooriverygood/api/course/application/CourseFindService.java @@ -0,0 +1,38 @@ +package org.wooriverygood.api.course.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.course.exception.CourseNotFoundException; +import org.wooriverygood.api.course.domain.Course; +import org.wooriverygood.api.course.dto.CourseNameResponse; +import org.wooriverygood.api.course.dto.CourseResponse; +import org.wooriverygood.api.course.dto.CoursesResponse; +import org.wooriverygood.api.course.repository.CourseRepository; + +import java.util.List; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class CourseFindService { + + private final CourseRepository courseRepository; + + public CoursesResponse findAll() { + List responses = courseRepository.findAll() + .stream() + .map(CourseResponse::of) + .toList(); + + return new CoursesResponse(responses); + } + + public CourseNameResponse findCourseName(Long courseId) { + Course course = courseRepository.findById(courseId) + .orElseThrow(CourseNotFoundException::new); + + return new CourseNameResponse(course.getName()); + } + +} diff --git a/src/main/java/org/wooriverygood/api/course/controller/CourseController.java b/src/main/java/org/wooriverygood/api/course/controller/CourseController.java deleted file mode 100644 index 01f2baa..0000000 --- a/src/main/java/org/wooriverygood/api/course/controller/CourseController.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.wooriverygood.api.course.controller; - -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.wooriverygood.api.course.dto.CourseNameResponse; -import org.wooriverygood.api.course.dto.CourseResponse; -import org.wooriverygood.api.course.dto.NewCourseRequest; -import org.wooriverygood.api.course.dto.NewCourseResponse; -import org.wooriverygood.api.course.service.CourseService; - -import java.util.List; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/courses") -public class CourseController { - private final CourseService courseService; - - @GetMapping - public ResponseEntity> findAllCourses() { - Listresponses = courseService.findAll(); - return ResponseEntity.ok(responses); - } - - @PostMapping - public ResponseEntity addCourse(@Valid @RequestBody NewCourseRequest newCourseRequest) { - NewCourseResponse response = courseService.addCourse(newCourseRequest); - return ResponseEntity.status(HttpStatus.CREATED).body(response); - } - - @GetMapping("/{id}/name") - public ResponseEntity getCourseName(@PathVariable("id") Long courseId) { - CourseNameResponse response = courseService.getCourseName(courseId); - return ResponseEntity.ok(response); - } -} diff --git a/src/main/java/org/wooriverygood/api/course/domain/Courses.java b/src/main/java/org/wooriverygood/api/course/domain/Course.java similarity index 69% rename from src/main/java/org/wooriverygood/api/course/domain/Courses.java rename to src/main/java/org/wooriverygood/api/course/domain/Course.java index 77d1f31..bda5b32 100644 --- a/src/main/java/org/wooriverygood/api/course/domain/Courses.java +++ b/src/main/java/org/wooriverygood/api/course/domain/Course.java @@ -1,6 +1,7 @@ package org.wooriverygood.api.course.domain; import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -8,22 +9,23 @@ @Entity @Getter -@NoArgsConstructor @Table(name = "courses") -public class Courses { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Course { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "course_id") private Long id; @Column(name = "course_category", nullable = false) - private String course_category; + private String category; @Column(name = "course_credit", nullable = false) - private double course_credit; + private double credit; @Column(name = "course_name", nullable = false) - private String course_name; + private String name; @Column(name = "isYouguan", nullable = false) private int isYouguan; @@ -35,17 +37,16 @@ public class Courses { @ColumnDefault("0") private int reviewCount; + @Builder - public Courses(Long id, String course_name, String course_category, double course_credit, int isYouguan, String kaikeYuanxi, int reviewCount) { + public Course(Long id, String name, String category, double credit, int isYouguan, String kaikeYuanxi, int reviewCount) { this.id = id; - this.course_name = course_name; - this.course_category = course_category; - this.course_credit = course_credit; + this.name = name; + this.category = category; + this.credit = credit; this.isYouguan = isYouguan; this.kaikeYuanxi = kaikeYuanxi; this.reviewCount = reviewCount; } - - } diff --git a/src/main/java/org/wooriverygood/api/course/dto/CourseNameResponse.java b/src/main/java/org/wooriverygood/api/course/dto/CourseNameResponse.java index 80d1001..efb808d 100644 --- a/src/main/java/org/wooriverygood/api/course/dto/CourseNameResponse.java +++ b/src/main/java/org/wooriverygood/api/course/dto/CourseNameResponse.java @@ -1,14 +1,17 @@ package org.wooriverygood.api.course.dto; -import lombok.Builder; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Getter; @Getter +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class CourseNameResponse { - private final String course_name; - @Builder - public CourseNameResponse(String course_name) { - this.course_name = course_name; + private final String courseName; + + public CourseNameResponse(String courseName) { + this.courseName = courseName; } + } diff --git a/src/main/java/org/wooriverygood/api/course/dto/CourseResponse.java b/src/main/java/org/wooriverygood/api/course/dto/CourseResponse.java index 1281328..cec3cc6 100644 --- a/src/main/java/org/wooriverygood/api/course/dto/CourseResponse.java +++ b/src/main/java/org/wooriverygood/api/course/dto/CourseResponse.java @@ -1,39 +1,51 @@ package org.wooriverygood.api.course.dto; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Builder; import lombok.Getter; -import org.wooriverygood.api.course.domain.Courses; +import org.wooriverygood.api.course.domain.Course; @Getter +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class CourseResponse { - private final Long course_id; - private final String course_category; - private final double course_credit; - private final String course_name; + + private final Long courseId; + + private final String courseCategory; + + private final double courseCredit; + + private final String courseName; + private final int isYouguan; + private final String kaikeYuanxi; + private final int reviewCount; + @Builder - public CourseResponse(Long course_id, String course_category, double course_credit, String course_name, int isYouguan, String kaikeYuanxi, int reviewCount) { - this.course_id = course_id; - this.course_category = course_category; - this.course_credit = course_credit; - this.course_name = course_name; + public CourseResponse(Long courseId, String courseCategory, double courseCredit, String courseName, int isYouguan, String kaikeYuanxi, int reviewCount) { + this.courseId = courseId; + this.courseCategory = courseCategory; + this.courseCredit = courseCredit; + this.courseName = courseName; this.isYouguan = isYouguan; this.kaikeYuanxi = kaikeYuanxi; this.reviewCount = reviewCount; } - public static CourseResponse from(Courses course) { + public static CourseResponse of(Course course) { return CourseResponse.builder() - .course_id(course.getId()) - .course_category(course.getCourse_category()) - .course_credit(course.getCourse_credit()) - .course_name(course.getCourse_name()) + .courseId(course.getId()) + .courseCategory(course.getCategory()) + .courseCredit(course.getCredit()) + .courseName(course.getName()) .isYouguan(course.getIsYouguan()) .kaikeYuanxi(course.getKaikeYuanxi()) .reviewCount(course.getReviewCount()) .build(); } + } diff --git a/src/main/java/org/wooriverygood/api/course/dto/CoursesResponse.java b/src/main/java/org/wooriverygood/api/course/dto/CoursesResponse.java new file mode 100644 index 0000000..c1164be --- /dev/null +++ b/src/main/java/org/wooriverygood/api/course/dto/CoursesResponse.java @@ -0,0 +1,17 @@ +package org.wooriverygood.api.course.dto; + +import lombok.Getter; + +import java.util.List; + +@Getter +public class CoursesResponse { + + private final List courses; + + + public CoursesResponse(List courses) { + this.courses = courses; + } + +} diff --git a/src/main/java/org/wooriverygood/api/course/dto/NewCourseRequest.java b/src/main/java/org/wooriverygood/api/course/dto/NewCourseRequest.java index 9706cf3..e86dbae 100644 --- a/src/main/java/org/wooriverygood/api/course/dto/NewCourseRequest.java +++ b/src/main/java/org/wooriverygood/api/course/dto/NewCourseRequest.java @@ -1,22 +1,32 @@ package org.wooriverygood.api.course.dto; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Builder; import lombok.Getter; @Getter +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class NewCourseRequest { - private final String course_name; - private final double course_credit; - private final String course_category; + + private final String courseName; + + private final double courseCredit; + + private final String courseCategory; + private final String kaikeYuanxi; + private final int isYouguan; + @Builder - public NewCourseRequest(String course_name, double course_credit, String course_category, String kaikeYuanxi, int isYouguan) { - this.course_name = course_name; - this.course_credit = course_credit; - this.course_category = course_category; + public NewCourseRequest(String courseName, double courseCredit, String courseCategory, String kaikeYuanxi, int isYouguan) { + this.courseName = courseName; + this.courseCredit = courseCredit; + this.courseCategory = courseCategory; this.kaikeYuanxi = kaikeYuanxi; this.isYouguan = isYouguan; } + } diff --git a/src/main/java/org/wooriverygood/api/course/dto/NewCourseResponse.java b/src/main/java/org/wooriverygood/api/course/dto/NewCourseResponse.java deleted file mode 100644 index 5671bcb..0000000 --- a/src/main/java/org/wooriverygood/api/course/dto/NewCourseResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.wooriverygood.api.course.dto; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class NewCourseResponse { - private final Long course_id; - private final String course_name; - private final double course_credit; - private final String course_category; - private final String kaikeYuanxi; - private final int isYouguan; - - @Builder - public NewCourseResponse(Long course_id, String course_name, double course_credit, String course_category, String kaikeYuanxi, int isYouguan) { - this.course_id = course_id; - this.course_name = course_name; - this.course_credit = course_credit; - this.course_category = course_category; - this.kaikeYuanxi = kaikeYuanxi; - this.isYouguan = isYouguan; - } -} diff --git a/src/main/java/org/wooriverygood/api/advice/exception/CourseNotFoundException.java b/src/main/java/org/wooriverygood/api/course/exception/CourseNotFoundException.java similarity index 63% rename from src/main/java/org/wooriverygood/api/advice/exception/CourseNotFoundException.java rename to src/main/java/org/wooriverygood/api/course/exception/CourseNotFoundException.java index a594c6f..9ef3403 100644 --- a/src/main/java/org/wooriverygood/api/advice/exception/CourseNotFoundException.java +++ b/src/main/java/org/wooriverygood/api/course/exception/CourseNotFoundException.java @@ -1,6 +1,6 @@ -package org.wooriverygood.api.advice.exception; +package org.wooriverygood.api.course.exception; -import org.wooriverygood.api.advice.exception.general.NotFoundException; +import org.wooriverygood.api.global.error.exception.NotFoundException; public class CourseNotFoundException extends NotFoundException { private static final String MESSAGE = "강의를 찾을 수 없습니다."; diff --git a/src/main/java/org/wooriverygood/api/course/repository/CourseRepository.java b/src/main/java/org/wooriverygood/api/course/repository/CourseRepository.java index 29d009e..5853702 100644 --- a/src/main/java/org/wooriverygood/api/course/repository/CourseRepository.java +++ b/src/main/java/org/wooriverygood/api/course/repository/CourseRepository.java @@ -1,7 +1,21 @@ package org.wooriverygood.api.course.repository; import org.springframework.data.jpa.repository.JpaRepository; -import org.wooriverygood.api.course.domain.Courses; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.course.domain.Course; + +public interface CourseRepository extends JpaRepository { + + @Transactional + @Modifying(clearAutomatically = true) + @Query(value = "UPDATE courses SET review_count = review_count + 1 WHERE course_id = :courseId", nativeQuery = true) + void increaseReviewCount(Long courseId); + + @Transactional + @Modifying(clearAutomatically = true) + @Query(value = "UPDATE courses SET review_count = review_count - 1 WHERE course_id = :courseId", nativeQuery = true) + void decreaseReviewCount(Long courseId); -public interface CourseRepository extends JpaRepository { } diff --git a/src/main/java/org/wooriverygood/api/course/service/CourseService.java b/src/main/java/org/wooriverygood/api/course/service/CourseService.java deleted file mode 100644 index 74341ec..0000000 --- a/src/main/java/org/wooriverygood/api/course/service/CourseService.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.wooriverygood.api.course.service; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.wooriverygood.api.advice.exception.CourseNotFoundException; -import org.wooriverygood.api.course.domain.Courses; -import org.wooriverygood.api.course.dto.CourseNameResponse; -import org.wooriverygood.api.course.dto.CourseResponse; -import org.wooriverygood.api.course.dto.NewCourseRequest; -import org.wooriverygood.api.course.dto.NewCourseResponse; -import org.wooriverygood.api.course.repository.CourseRepository; - -import java.util.List; - -@RequiredArgsConstructor -@Service -@Transactional(readOnly = true) -public class CourseService { - private final CourseRepository courseRepository; - - public List findAll() { - return courseRepository.findAll().stream().map(CourseResponse::from).toList(); - } - - @Transactional - public NewCourseResponse addCourse(NewCourseRequest newCourseRequest) { - Courses course = createCourse(newCourseRequest); - Courses saved = courseRepository.save(course); - return createResponse(saved); - } - - public CourseNameResponse getCourseName(Long courseId) { - Courses courses = courseRepository.findById(courseId) - .orElseThrow(CourseNotFoundException::new); - - return CourseNameResponse.builder() - .course_name(courses.getCourse_name()) - .build(); - - } - - private Courses createCourse(NewCourseRequest newCourseRequest) { - return Courses.builder() - .course_name(newCourseRequest.getCourse_name()) - .course_category(newCourseRequest.getCourse_category()) - .course_credit(newCourseRequest.getCourse_credit()) - .isYouguan(newCourseRequest.getIsYouguan()) - .kaikeYuanxi(newCourseRequest.getKaikeYuanxi()) - .build(); - } - - private NewCourseResponse createResponse(Courses course) { - return NewCourseResponse.builder() - .course_id(course.getId()) - .course_name(course.getCourse_name()) - .course_category(course.getCourse_category()) - .course_credit(course.getCourse_credit()) - .isYouguan(course.getIsYouguan()) - .kaikeYuanxi(course.getKaikeYuanxi()) - .build(); - } - -} diff --git a/src/main/java/org/wooriverygood/api/global/ErrorResponse.java b/src/main/java/org/wooriverygood/api/global/ErrorResponse.java new file mode 100644 index 0000000..b469f51 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/global/ErrorResponse.java @@ -0,0 +1,44 @@ +package org.wooriverygood.api.global; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ErrorResponse { + + private String message; + + private List errors; + + @Builder + public ErrorResponse(String message, List errors) { + this.message = message; + this.errors = errors; + } + + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + public static class FieldError { + + private String field; + + private String value; + + private String reason; + + + @Builder + public FieldError(String field, String value, String reason) { + this.field = field; + this.value = value; + this.reason = reason; + } + + } + +} diff --git a/src/main/java/org/wooriverygood/api/support/AuthInfo.java b/src/main/java/org/wooriverygood/api/global/auth/AuthInfo.java similarity index 56% rename from src/main/java/org/wooriverygood/api/support/AuthInfo.java rename to src/main/java/org/wooriverygood/api/global/auth/AuthInfo.java index ac5aa9d..4aa4f01 100644 --- a/src/main/java/org/wooriverygood/api/support/AuthInfo.java +++ b/src/main/java/org/wooriverygood/api/global/auth/AuthInfo.java @@ -1,4 +1,4 @@ -package org.wooriverygood.api.support; +package org.wooriverygood.api.global.auth; import lombok.Builder; import lombok.Getter; @@ -6,11 +6,15 @@ @Getter public class AuthInfo { + private Long memberId; + private final String sub; + private final String username; @Builder - public AuthInfo(String sub, String username) { + public AuthInfo(Long memberId, String sub, String username) { + this.memberId = memberId; this.sub = sub; this.username = username; } diff --git a/src/main/java/org/wooriverygood/api/support/AuthenticationPrincipalArgumentResolver.java b/src/main/java/org/wooriverygood/api/global/auth/AuthenticationPrincipalArgumentResolver.java similarity index 57% rename from src/main/java/org/wooriverygood/api/support/AuthenticationPrincipalArgumentResolver.java rename to src/main/java/org/wooriverygood/api/global/auth/AuthenticationPrincipalArgumentResolver.java index bfb1408..c0d17da 100644 --- a/src/main/java/org/wooriverygood/api/support/AuthenticationPrincipalArgumentResolver.java +++ b/src/main/java/org/wooriverygood/api/global/auth/AuthenticationPrincipalArgumentResolver.java @@ -1,5 +1,6 @@ -package org.wooriverygood.api.support; +package org.wooriverygood.api.global.auth; +import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -8,19 +9,31 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.repository.MemberRepository; +@RequiredArgsConstructor public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver { + private final MemberRepository memberRepository; + + @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(Login.class); } @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication != null && authentication.getPrincipal() instanceof Jwt jwt) return createAuthInfo(jwt); + if (authentication != null && authentication.getPrincipal() instanceof Jwt jwt) { + return createAuthInfo(jwt); + } + return AuthInfo.builder().build(); } @@ -28,7 +41,14 @@ private AuthInfo createAuthInfo(Jwt jwt) { String sub = jwt.getClaim("sub"); String username = jwt.getClaim("username"); + Member member = memberRepository.findByUsername(username) + .orElseGet(() -> { + Member newMember = Member.builder().username(username).build(); + return memberRepository.save(newMember); + }); + return AuthInfo.builder() + .memberId(member.getId()) .sub(sub) .username(username) .build(); diff --git a/src/main/java/org/wooriverygood/api/support/Login.java b/src/main/java/org/wooriverygood/api/global/auth/Login.java similarity index 85% rename from src/main/java/org/wooriverygood/api/support/Login.java rename to src/main/java/org/wooriverygood/api/global/auth/Login.java index c577a3c..6560507 100644 --- a/src/main/java/org/wooriverygood/api/support/Login.java +++ b/src/main/java/org/wooriverygood/api/global/auth/Login.java @@ -1,4 +1,4 @@ -package org.wooriverygood.api.support; +package org.wooriverygood.api.global.auth; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/src/main/java/org/wooriverygood/api/support/TokenProvider.java b/src/main/java/org/wooriverygood/api/global/auth/TokenProvider.java similarity index 91% rename from src/main/java/org/wooriverygood/api/support/TokenProvider.java rename to src/main/java/org/wooriverygood/api/global/auth/TokenProvider.java index 1121370..afcbba6 100644 --- a/src/main/java/org/wooriverygood/api/support/TokenProvider.java +++ b/src/main/java/org/wooriverygood/api/global/auth/TokenProvider.java @@ -1,4 +1,4 @@ -package org.wooriverygood.api.support; +package org.wooriverygood.api.global.auth; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.oauth2.jwt.JwtDecoder; diff --git a/src/main/java/org/wooriverygood/api/config/JpaConfig.java b/src/main/java/org/wooriverygood/api/global/config/JpaConfig.java similarity index 81% rename from src/main/java/org/wooriverygood/api/config/JpaConfig.java rename to src/main/java/org/wooriverygood/api/global/config/JpaConfig.java index c451e9d..68da2a6 100644 --- a/src/main/java/org/wooriverygood/api/config/JpaConfig.java +++ b/src/main/java/org/wooriverygood/api/global/config/JpaConfig.java @@ -1,4 +1,4 @@ -package org.wooriverygood.api.config; +package org.wooriverygood.api.global.config; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; diff --git a/src/main/java/org/wooriverygood/api/config/SecurityConfig.java b/src/main/java/org/wooriverygood/api/global/config/SecurityConfig.java similarity index 88% rename from src/main/java/org/wooriverygood/api/config/SecurityConfig.java rename to src/main/java/org/wooriverygood/api/global/config/SecurityConfig.java index 7157027..c1118a4 100644 --- a/src/main/java/org/wooriverygood/api/config/SecurityConfig.java +++ b/src/main/java/org/wooriverygood/api/global/config/SecurityConfig.java @@ -1,4 +1,4 @@ -package org.wooriverygood.api.config; +package org.wooriverygood.api.global.config; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -12,7 +12,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.SecurityFilterChain; -import org.wooriverygood.api.support.TokenProvider; +import org.wooriverygood.api.global.auth.TokenProvider; @Configuration @EnableWebSecurity @@ -36,7 +36,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> authorizationManagerRequestMatcherRegistry .requestMatchers(HttpMethod.OPTIONS).permitAll() - .requestMatchers("/", "/docs/**", "/docs/index.html").permitAll() // 권한이 필요없는 엔드 포인트, 루트와 API 명세 엔드포인트 열어둠 + .requestMatchers("/*", "/docs/**", "/docs/index.html").permitAll() .anyRequest().authenticated()) .oauth2ResourceServer(httpSecurityOAuth2ResourceServerConfigurer -> httpSecurityOAuth2ResourceServerConfigurer.jwt(jwtConfigurer -> diff --git a/src/main/java/org/wooriverygood/api/config/WebConfig.java b/src/main/java/org/wooriverygood/api/global/config/WebConfig.java similarity index 72% rename from src/main/java/org/wooriverygood/api/config/WebConfig.java rename to src/main/java/org/wooriverygood/api/global/config/WebConfig.java index 0525104..9278e3c 100644 --- a/src/main/java/org/wooriverygood/api/config/WebConfig.java +++ b/src/main/java/org/wooriverygood/api/global/config/WebConfig.java @@ -1,20 +1,25 @@ -package org.wooriverygood.api.config; +package org.wooriverygood.api.global.config; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import org.wooriverygood.api.support.AuthenticationPrincipalArgumentResolver; +import org.wooriverygood.api.global.auth.AuthenticationPrincipalArgumentResolver; +import org.wooriverygood.api.member.repository.MemberRepository; import java.util.List; @Configuration +@RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { + private final MemberRepository memberRepository; + @Bean public AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver() { - return new AuthenticationPrincipalArgumentResolver(); + return new AuthenticationPrincipalArgumentResolver(memberRepository); } @Override diff --git a/src/main/java/org/wooriverygood/api/global/error/ControllerAdvice.java b/src/main/java/org/wooriverygood/api/global/error/ControllerAdvice.java new file mode 100644 index 0000000..ef6b35c --- /dev/null +++ b/src/main/java/org/wooriverygood/api/global/error/ControllerAdvice.java @@ -0,0 +1,63 @@ +package org.wooriverygood.api.global.error; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.wooriverygood.api.global.ErrorResponse; +import org.wooriverygood.api.global.error.exception.BadRequestException; +import org.wooriverygood.api.global.error.exception.ForbiddenException; +import org.wooriverygood.api.global.error.exception.NotFoundException; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@RestControllerAdvice +public class ControllerAdvice { + + @ExceptionHandler(ForbiddenException.class) + public ResponseEntity handleForbiddenException(ForbiddenException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(new ErrorResponse(e.getMessage(), new ArrayList<>())); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFoundException(NotFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse(e.getMessage(), new ArrayList<>())); + } + + @ExceptionHandler(BadRequestException.class) + public ResponseEntity handleBadRequestException(BadRequestException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ErrorResponse(e.getMessage(), new ArrayList<>())); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + log.error(e.getMessage()); + final BindingResult bindingResult = e.getBindingResult(); + final List errors = bindingResult.getFieldErrors(); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(buildFieldErrors("Invalid input value", errors.stream() + .map(error -> ErrorResponse.FieldError.builder() + .reason(error.getDefaultMessage()) + .field(error.getField()) + .value((String) error.getRejectedValue()) + .build()) + .toList())); + } + + private ErrorResponse buildFieldErrors(String message, List errors) { + return ErrorResponse.builder() + .message(message) + .errors(errors) + .build(); + } + +} diff --git a/src/main/java/org/wooriverygood/api/advice/exception/AuthorizationException.java b/src/main/java/org/wooriverygood/api/global/error/exception/AuthorizationException.java similarity index 62% rename from src/main/java/org/wooriverygood/api/advice/exception/AuthorizationException.java rename to src/main/java/org/wooriverygood/api/global/error/exception/AuthorizationException.java index e3dcb1c..4fa6e29 100644 --- a/src/main/java/org/wooriverygood/api/advice/exception/AuthorizationException.java +++ b/src/main/java/org/wooriverygood/api/global/error/exception/AuthorizationException.java @@ -1,6 +1,6 @@ -package org.wooriverygood.api.advice.exception; +package org.wooriverygood.api.global.error.exception; -import org.wooriverygood.api.advice.exception.general.ForbiddenException; +import org.wooriverygood.api.global.error.exception.ForbiddenException; public class AuthorizationException extends ForbiddenException { diff --git a/src/main/java/org/wooriverygood/api/advice/exception/general/BadRequestException.java b/src/main/java/org/wooriverygood/api/global/error/exception/BadRequestException.java similarity index 71% rename from src/main/java/org/wooriverygood/api/advice/exception/general/BadRequestException.java rename to src/main/java/org/wooriverygood/api/global/error/exception/BadRequestException.java index cc56612..f9ac968 100644 --- a/src/main/java/org/wooriverygood/api/advice/exception/general/BadRequestException.java +++ b/src/main/java/org/wooriverygood/api/global/error/exception/BadRequestException.java @@ -1,4 +1,4 @@ -package org.wooriverygood.api.advice.exception.general; +package org.wooriverygood.api.global.error.exception; public class BadRequestException extends BusinessException { diff --git a/src/main/java/org/wooriverygood/api/advice/exception/general/BusinessException.java b/src/main/java/org/wooriverygood/api/global/error/exception/BusinessException.java similarity index 71% rename from src/main/java/org/wooriverygood/api/advice/exception/general/BusinessException.java rename to src/main/java/org/wooriverygood/api/global/error/exception/BusinessException.java index 550a84a..5ed0b7a 100644 --- a/src/main/java/org/wooriverygood/api/advice/exception/general/BusinessException.java +++ b/src/main/java/org/wooriverygood/api/global/error/exception/BusinessException.java @@ -1,4 +1,4 @@ -package org.wooriverygood.api.advice.exception.general; +package org.wooriverygood.api.global.error.exception; public class BusinessException extends RuntimeException { diff --git a/src/main/java/org/wooriverygood/api/advice/exception/general/ForbiddenException.java b/src/main/java/org/wooriverygood/api/global/error/exception/ForbiddenException.java similarity index 71% rename from src/main/java/org/wooriverygood/api/advice/exception/general/ForbiddenException.java rename to src/main/java/org/wooriverygood/api/global/error/exception/ForbiddenException.java index ca44c23..2dd36c7 100644 --- a/src/main/java/org/wooriverygood/api/advice/exception/general/ForbiddenException.java +++ b/src/main/java/org/wooriverygood/api/global/error/exception/ForbiddenException.java @@ -1,4 +1,4 @@ -package org.wooriverygood.api.advice.exception.general; +package org.wooriverygood.api.global.error.exception; public class ForbiddenException extends BusinessException { diff --git a/src/main/java/org/wooriverygood/api/advice/exception/general/NotFoundException.java b/src/main/java/org/wooriverygood/api/global/error/exception/NotFoundException.java similarity index 71% rename from src/main/java/org/wooriverygood/api/advice/exception/general/NotFoundException.java rename to src/main/java/org/wooriverygood/api/global/error/exception/NotFoundException.java index 12ddff6..89547a5 100644 --- a/src/main/java/org/wooriverygood/api/advice/exception/general/NotFoundException.java +++ b/src/main/java/org/wooriverygood/api/global/error/exception/NotFoundException.java @@ -1,4 +1,4 @@ -package org.wooriverygood.api.advice.exception.general; +package org.wooriverygood.api.global.error.exception; public class NotFoundException extends BusinessException { diff --git a/src/main/java/org/wooriverygood/api/member/domain/Member.java b/src/main/java/org/wooriverygood/api/member/domain/Member.java new file mode 100644 index 0000000..6dbee95 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/member/domain/Member.java @@ -0,0 +1,56 @@ +package org.wooriverygood.api.member.domain; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.wooriverygood.api.global.error.exception.AuthorizationException; +import org.wooriverygood.api.review.domain.Review; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Entity +@Getter +@Table(name = "members") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Member { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String username; + + @Column(name = "old_username") + private String oldUsername; + + private String email; + + @OneToMany(mappedBy = "member") + private List reviews = new ArrayList<>(); + + + @Builder + public Member(Long id, String username) { + this.id = id; + this.username = username; + } + + public boolean isSame(Member member) { + return Objects.equals(this.id, member.getId()); + } + + public void verify(Member member) { + if (!isSame(member)) { + throw new AuthorizationException(); + } + } + + public void addReview(Review review) { + reviews.add(review); + } + +} \ No newline at end of file diff --git a/src/main/java/org/wooriverygood/api/member/exception/MemberNotFoundException.java b/src/main/java/org/wooriverygood/api/member/exception/MemberNotFoundException.java new file mode 100644 index 0000000..7581fc4 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/member/exception/MemberNotFoundException.java @@ -0,0 +1,12 @@ +package org.wooriverygood.api.member.exception; + +import org.wooriverygood.api.global.error.exception.NotFoundException; + +public class MemberNotFoundException extends NotFoundException { + private static final String MESSAGE = "유저 정보를 찾을 수 없습니다."; + + + public MemberNotFoundException() { + super(MESSAGE); + } +} diff --git a/src/main/java/org/wooriverygood/api/member/repository/MemberRepository.java b/src/main/java/org/wooriverygood/api/member/repository/MemberRepository.java new file mode 100644 index 0000000..d10e1df --- /dev/null +++ b/src/main/java/org/wooriverygood/api/member/repository/MemberRepository.java @@ -0,0 +1,12 @@ +package org.wooriverygood.api.member.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.wooriverygood.api.member.domain.Member; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + + Optional findByUsername(String username); + +} diff --git a/src/main/java/org/wooriverygood/api/post/api/PostApi.java b/src/main/java/org/wooriverygood/api/post/api/PostApi.java new file mode 100644 index 0000000..9bce494 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/post/api/PostApi.java @@ -0,0 +1,87 @@ +package org.wooriverygood.api.post.api; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.wooriverygood.api.post.application.*; +import org.wooriverygood.api.post.dto.*; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.global.auth.Login; + + +@RestController +@RequestMapping("/posts") +@RequiredArgsConstructor +public class PostApi { + + private final PostLikeToggleService postLikeToggleService; + + private final PostFindService postFindService; + + private final PostCreateService postCreateService; + + private final PostDeleteService postDeleteService; + + private final PostUpdateService postUpdateService; + + private final int PAGE_SIZE = 10; + + + @GetMapping + public ResponseEntity findPosts(@Login AuthInfo authInfo, + @RequestParam(required = false, defaultValue = "0", value = "page") int pageNo, + @RequestParam(required = false, defaultValue = "", value = "category") String category) { + Pageable pageable = PageRequest.of(pageNo, PAGE_SIZE); + PostsResponse response = postFindService.findPosts(authInfo, pageable, category); + return ResponseEntity.ok(response); + } + + @GetMapping("/{id}") + public ResponseEntity findPostById(@PathVariable("id") Long postId, + @Login AuthInfo authInfo) { + PostDetailResponse postDetailResponse = postFindService.findPostById(postId, authInfo); + return ResponseEntity.ok(postDetailResponse); + } + + @PostMapping + public ResponseEntity addPost(@Login AuthInfo authInfo, + @Valid @RequestBody NewPostRequest newPostRequest) { + postCreateService.addPost(authInfo, newPostRequest); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @GetMapping("/me") + public ResponseEntity findMyPosts(@Login AuthInfo authInfo, + @RequestParam(required = false, defaultValue = "0", value = "page") int pageNo) { + Pageable pageable = PageRequest.of(pageNo, PAGE_SIZE); + PostsResponse response = postFindService.findMyPosts(authInfo, pageable); + return ResponseEntity.ok(response); + } + + @PutMapping("/{id}/like") + public ResponseEntity togglePostLike(@PathVariable("id") Long postId, + @Login AuthInfo authInfo) { + PostLikeResponse response = postLikeToggleService.togglePostLike(postId, authInfo); + return ResponseEntity.ok(response); + } + + @PutMapping("/{id}") + public ResponseEntity updatePost(@PathVariable("id") Long postId, + @Valid @RequestBody PostUpdateRequest postUpdateRequest, + @Login AuthInfo authInfo) { + postUpdateService.updatePost(postId, postUpdateRequest, authInfo); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/{id}") + public ResponseEntity deletePost(@PathVariable("id") Long postId, + @Login AuthInfo authInfo) { + postDeleteService.deletePost(authInfo, postId); + return ResponseEntity.noContent().build(); + } + +} diff --git a/src/main/java/org/wooriverygood/api/post/application/PostCreateService.java b/src/main/java/org/wooriverygood/api/post/application/PostCreateService.java new file mode 100644 index 0000000..403b65a --- /dev/null +++ b/src/main/java/org/wooriverygood/api/post/application/PostCreateService.java @@ -0,0 +1,41 @@ +package org.wooriverygood.api.post.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.exception.MemberNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.post.domain.Post; +import org.wooriverygood.api.post.domain.PostCategory; +import org.wooriverygood.api.post.dto.NewPostRequest; +import org.wooriverygood.api.post.repository.PostRepository; +import org.wooriverygood.api.global.auth.AuthInfo; + +@Service +@Transactional +@RequiredArgsConstructor +public class PostCreateService { + + private final PostRepository postRepository; + + private final MemberRepository memberRepository; + + public void addPost(AuthInfo authInfo, NewPostRequest newPostRequest) { + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + PostCategory.parse(newPostRequest.getPostCategory()); + Post post = createPost(member, newPostRequest); + postRepository.save(post); + } + + private Post createPost(Member member, NewPostRequest newPostRequest) { + return Post.builder() + .title(newPostRequest.getPostTitle()) + .content(newPostRequest.getPostContent()) + .category(PostCategory.parse(newPostRequest.getPostCategory())) + .member(member) + .build(); + } + +} \ No newline at end of file diff --git a/src/main/java/org/wooriverygood/api/post/application/PostDeleteService.java b/src/main/java/org/wooriverygood/api/post/application/PostDeleteService.java new file mode 100644 index 0000000..feeed2c --- /dev/null +++ b/src/main/java/org/wooriverygood/api/post/application/PostDeleteService.java @@ -0,0 +1,46 @@ +package org.wooriverygood.api.post.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.comment.repository.CommentRepository; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.exception.MemberNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.post.domain.Post; +import org.wooriverygood.api.post.exception.PostNotFoundException; +import org.wooriverygood.api.post.repository.PostLikeRepository; +import org.wooriverygood.api.post.repository.PostRepository; +import org.wooriverygood.api.global.auth.AuthInfo; + +@Service +@Transactional +@RequiredArgsConstructor +public class PostDeleteService { + + private final PostRepository postRepository; + + private final CommentRepository commentRepository; + + private final PostLikeRepository postLikeRepository; + + private final MemberRepository memberRepository; + + + public void deletePost(AuthInfo authInfo, long postId) { + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + Post post = postRepository.findById(postId) + .orElseThrow(PostNotFoundException::new); + post.validateAuthor(member); + + deleteCommentAndPostLike(post); + postRepository.delete(post); + } + + private void deleteCommentAndPostLike(Post post) { + commentRepository.deleteAllByPost(post); + postLikeRepository.deleteAllByPost(post); + } + +} diff --git a/src/main/java/org/wooriverygood/api/post/application/PostFindService.java b/src/main/java/org/wooriverygood/api/post/application/PostFindService.java new file mode 100644 index 0000000..76e5d92 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/post/application/PostFindService.java @@ -0,0 +1,83 @@ +package org.wooriverygood.api.post.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.exception.MemberNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.post.domain.Post; +import org.wooriverygood.api.post.domain.PostCategory; +import org.wooriverygood.api.post.dto.PostDetailResponse; +import org.wooriverygood.api.post.dto.PostResponse; +import org.wooriverygood.api.post.dto.PostsResponse; +import org.wooriverygood.api.post.exception.PostNotFoundException; +import org.wooriverygood.api.post.repository.PostLikeRepository; +import org.wooriverygood.api.post.repository.PostRepository; +import org.wooriverygood.api.global.auth.AuthInfo; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class PostFindService { + + private final PostRepository postRepository; + + private final PostLikeRepository postLikeRepository; + + private final MemberRepository memberRepository; + + + public PostsResponse findPosts(AuthInfo authInfo, Pageable pageable, String postCategory) { + Page page = findPostsPage(pageable, postCategory); + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + return convertToPostsResponse(member, page); + } + + private Page findPostsPage(Pageable pageable, String postCategory) { + if (postCategory.isEmpty()) + return postRepository.findAllByOrderByIdDesc(pageable); + PostCategory category = PostCategory.parse(postCategory); + return postRepository.findAllByCategoryOrderByIdDesc(category, pageable); + } + + @Transactional + public PostDetailResponse findPostById(long postId, AuthInfo authInfo) { + Post post = postRepository.findById(postId) + .orElseThrow(PostNotFoundException::new); + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + boolean liked = postLikeRepository.existsByPostAndMember(post, member); + boolean isMine = post.getMember().equals(member); + postRepository.increaseViewCount(postId); + return PostDetailResponse.of(post, member.getId(), liked, isMine); + } + + public PostsResponse findMyPosts(AuthInfo authInfo, Pageable pageable) { + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + Page page = postRepository.findByMemberOrderByIdDesc(member, pageable); + return convertToPostsResponse(member, page); + } + + private PostsResponse convertToPostsResponse(Member member, Page page) { + List posts = page.getContent().stream() + .map(post -> { + boolean liked = postLikeRepository.existsByPostAndMember(post, member); + return PostResponse.of(post, liked, post.getMember().equals(member)); + }) + .toList(); + + return PostsResponse.builder() + .posts(posts) + .totalPageCount(page.getTotalPages()) + .totalPostCount(page.getTotalElements()) + .build(); + } + +} \ No newline at end of file diff --git a/src/main/java/org/wooriverygood/api/post/application/PostLikeToggleService.java b/src/main/java/org/wooriverygood/api/post/application/PostLikeToggleService.java new file mode 100644 index 0000000..29508ce --- /dev/null +++ b/src/main/java/org/wooriverygood/api/post/application/PostLikeToggleService.java @@ -0,0 +1,72 @@ +package org.wooriverygood.api.post.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.exception.MemberNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.post.domain.Post; +import org.wooriverygood.api.post.domain.PostLike; +import org.wooriverygood.api.post.dto.PostLikeResponse; +import org.wooriverygood.api.post.exception.PostNotFoundException; +import org.wooriverygood.api.post.repository.PostLikeRepository; +import org.wooriverygood.api.post.repository.PostRepository; +import org.wooriverygood.api.global.auth.AuthInfo; + +import java.util.Optional; + +@Service +@Transactional +@RequiredArgsConstructor +public class PostLikeToggleService { + + private final PostRepository postRepository; + + private final PostLikeRepository postLikeRepository; + + private final MemberRepository memberRepository; + + + public PostLikeResponse togglePostLike(long postId, AuthInfo authInfo) { + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + Post post = postRepository.findById(postId) + .orElseThrow(PostNotFoundException::new); + + Optional postLike = postLikeRepository.findByPostAndMember(post, member); + + if (postLike.isEmpty()) { + addPostLike(post, member); + return createPostLikeResponse(post, true); + } + + deletePostLike(post, postLike.get()); + return createPostLikeResponse(post, false); + } + + private void addPostLike(Post post, Member member) { + PostLike newPostLike = PostLike.builder() + .post(post) + .member(member) + .build(); + + post.addPostLike(newPostLike); + postLikeRepository.save(newPostLike); + postRepository.increaseLikeCount(post.getId()); + } + + private void deletePostLike(Post post, PostLike postLike) { + post.deletePostLike(postLike); + postRepository.decreaseLikeCount(post.getId()); + } + + private PostLikeResponse createPostLikeResponse(Post post, boolean liked) { + int likeCount = post.getLikeCount() + (liked ? 1 : -1); + return PostLikeResponse.builder() + .likeCount(likeCount) + .liked(liked) + .build(); + } + +} \ No newline at end of file diff --git a/src/main/java/org/wooriverygood/api/post/application/PostUpdateService.java b/src/main/java/org/wooriverygood/api/post/application/PostUpdateService.java new file mode 100644 index 0000000..0978f36 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/post/application/PostUpdateService.java @@ -0,0 +1,36 @@ +package org.wooriverygood.api.post.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.exception.MemberNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.post.domain.Post; +import org.wooriverygood.api.post.dto.PostUpdateRequest; +import org.wooriverygood.api.post.exception.PostNotFoundException; +import org.wooriverygood.api.post.repository.PostRepository; +import org.wooriverygood.api.global.auth.AuthInfo; + +@Service +@Transactional +@RequiredArgsConstructor +public class PostUpdateService { + + private final PostRepository postRepository; + + private final MemberRepository memberRepository; + + + public void updatePost(long postId, PostUpdateRequest postUpdateRequest, AuthInfo authInfo) { + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + Post post = postRepository.findById(postId) + .orElseThrow(PostNotFoundException::new); + post.validateAuthor(member); + + post.updateTitle(postUpdateRequest.getPostTitle()); + post.updateContent(postUpdateRequest.getPostContent()); + } + +} diff --git a/src/main/java/org/wooriverygood/api/post/controller/PostController.java b/src/main/java/org/wooriverygood/api/post/controller/PostController.java deleted file mode 100644 index 54b5381..0000000 --- a/src/main/java/org/wooriverygood/api/post/controller/PostController.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.wooriverygood.api.post.controller; - -import jakarta.validation.Valid; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.wooriverygood.api.post.dto.*; -import org.wooriverygood.api.post.service.PostService; -import org.wooriverygood.api.support.AuthInfo; -import org.wooriverygood.api.support.Login; - - -@RestController -@RequestMapping("/community") -public class PostController { - - private final PostService postService; - - private final int PAGE_SIZE = 10; - - - public PostController(PostService postService) { - this.postService = postService; - } - - @GetMapping - public ResponseEntity findPosts(@Login AuthInfo authInfo, - @RequestParam(required = false, defaultValue = "0", value = "page") int pageNo) { - Pageable pageable = PageRequest.of(pageNo, PAGE_SIZE); - PostsResponse response = postService.findPosts(authInfo, pageable); - return ResponseEntity.ok(response); - } - - @GetMapping("/category/{category}") - public ResponseEntity findPostsByCategory(@Login AuthInfo authInfo, - @RequestParam(required = false, defaultValue = "0", value = "page") int pageNo, - @PathVariable("category") String postCategory) { - Pageable pageable = PageRequest.of(pageNo, PAGE_SIZE); - PostsResponse response = postService.findPostsByCategory(authInfo, pageable, postCategory); - return ResponseEntity.ok(response); - } - - @GetMapping("/{id}") - public ResponseEntity findPostById(@PathVariable("id") Long postId, - @Login AuthInfo authInfo) { - PostResponse postResponse = postService.findPostById(postId, authInfo); - return ResponseEntity.ok(postResponse); - } - - @PostMapping - public ResponseEntity addPost(@Login AuthInfo authInfo, - @Valid @RequestBody NewPostRequest newPostRequest) { - NewPostResponse response = postService.addPost(authInfo, newPostRequest); - return ResponseEntity.status(HttpStatus.CREATED).body(response); - } - - @GetMapping("/me") - public ResponseEntity findMyPosts(@Login AuthInfo authInfo, - @RequestParam(required = false, defaultValue = "0", value = "page") int pageNo) { - Pageable pageable = PageRequest.of(pageNo, PAGE_SIZE); - PostsResponse response = postService.findMyPosts(authInfo, pageable); - return ResponseEntity.ok(response); - } - - @PutMapping("/{id}/like") - public ResponseEntity likePost(@PathVariable("id") Long postId, - @Login AuthInfo authInfo) { - PostLikeResponse response = postService.likePost(postId, authInfo); - return ResponseEntity.ok(response); - } - - @PutMapping("/{id}") - public ResponseEntity updatePost(@PathVariable("id") Long postId, - @Valid @RequestBody PostUpdateRequest postUpdateRequest, - @Login AuthInfo authInfo) { - PostUpdateResponse response = postService.updatePost(postId, postUpdateRequest, authInfo); - return ResponseEntity.ok(response); - } - - @DeleteMapping("/{id}") - public ResponseEntity deletePost(@PathVariable("id") Long postId, - @Login AuthInfo authInfo) { - PostDeleteResponse response = postService.deletePost(postId, authInfo); - return ResponseEntity.ok(response); - } -} diff --git a/src/main/java/org/wooriverygood/api/post/domain/Content.java b/src/main/java/org/wooriverygood/api/post/domain/Content.java new file mode 100644 index 0000000..0d4dc38 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/post/domain/Content.java @@ -0,0 +1,32 @@ +package org.wooriverygood.api.post.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.Getter; +import org.wooriverygood.api.post.exception.InvalidPostContentException; + +@Embeddable +@Getter +public class Content { + + private static final int MAX_CONTENT_LENGTH = 2000; + + @Column(name = "post_content", length = MAX_CONTENT_LENGTH) + private String value; + + + protected Content() { + } + + public Content(String value) { + validate(value); + this.value = value; + } + + private void validate(String value) { + if (value.length() > MAX_CONTENT_LENGTH) { + throw new InvalidPostContentException(); + } + } + +} diff --git a/src/main/java/org/wooriverygood/api/post/domain/Post.java b/src/main/java/org/wooriverygood/api/post/domain/Post.java index 20ae39c..ae1b951 100644 --- a/src/main/java/org/wooriverygood/api/post/domain/Post.java +++ b/src/main/java/org/wooriverygood/api/post/domain/Post.java @@ -1,24 +1,24 @@ package org.wooriverygood.api.post.domain; import jakarta.persistence.*; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import org.hibernate.annotations.ColumnDefault; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import org.wooriverygood.api.advice.exception.AuthorizationException; +import org.wooriverygood.api.global.error.exception.AuthorizationException; import org.wooriverygood.api.comment.domain.Comment; +import org.wooriverygood.api.member.domain.Member; import org.wooriverygood.api.report.domain.PostReport; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; @Entity -@Table(name = "posts") @Getter +@Table(name = "posts") @EntityListeners(AuditingEntityListener.class) -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Post { @Id @@ -30,32 +30,36 @@ public class Post { @Column(name = "post_category") private PostCategory category; - @Column(name = "post_title", length = 45, nullable = false) - private String title; + @Embedded + private Title title; - @Column(name = "post_content", length = 2000) - private String content; + @Embedded + private Content content; - @Column(name = "post_author", length = 1000) - private String author; + @ManyToOne(fetch = FetchType.LAZY) + private Member member; - @OneToMany(mappedBy = "post") - private List comments; + @OneToMany(mappedBy = "post", fetch = FetchType.EAGER) + private List comments = new ArrayList<>(); @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) - private List postLikes; + private List postLikes = new ArrayList<>(); @Column(name = "like_count") @ColumnDefault("0") private int likeCount; @OneToMany(mappedBy = "post") - private List reports; + private List reports = new ArrayList<>(); @Column(name = "report_count") @ColumnDefault("0") private int reportCount; + @Column(name = "view_count") + @ColumnDefault("0") + private int viewCount; + @ColumnDefault("false") private boolean updated; @@ -65,20 +69,22 @@ public class Post { @Builder - public Post(Long id, PostCategory category, String title, String content, String author, List comments, List postLikes, List reports, boolean updated) { + public Post(Long id, PostCategory category, String title, String content, + Member member, List comments, List postLikes, + List reports, boolean updated) { this.id = id; this.category = category; - this.title = title; - this.content = content; - this.author = author; + this.title = new Title(title); + this.content = new Content(content); + this.member = member; this.comments = comments; this.postLikes = postLikes; this.reports = reports; this.updated = updated; } - public void validateAuthor(String author) { - if (!this.author.equals(author)) throw new AuthorizationException(); + public void validateAuthor(Member member) { + this.member.verify(member); } public void addPostLike(PostLike postLike) { @@ -91,7 +97,7 @@ public void deletePostLike(PostLike postLike) { } public void updateTitle(String title) { - this.title = title; + this.title = new Title(title); updated = true; } @@ -100,7 +106,7 @@ public void addReport(PostReport report) { } public void updateContent(String content) { - this.content = content; + this.content = new Content(content); updated = true; } @@ -110,11 +116,23 @@ public int getCommentCount() { return comments.size(); } - public boolean hasReportByUser(String username) { + public boolean hasReportByMember(Member member) { for (PostReport report: reports) - if (report.isOwner(username)) + if (report.isOwner(member)) return true; return false; } + public String getTitle() { + return isReportedTooMuch() ? null : title.getValue(); + } + + public String getContent() { + return isReportedTooMuch() ? null : content.getValue(); + } + + public boolean isReportedTooMuch() { + return reportCount >= 5; + } + } diff --git a/src/main/java/org/wooriverygood/api/post/domain/PostCategory.java b/src/main/java/org/wooriverygood/api/post/domain/PostCategory.java index 925a096..e2df158 100644 --- a/src/main/java/org/wooriverygood/api/post/domain/PostCategory.java +++ b/src/main/java/org/wooriverygood/api/post/domain/PostCategory.java @@ -1,7 +1,7 @@ package org.wooriverygood.api.post.domain; import lombok.Getter; -import org.wooriverygood.api.advice.exception.InvalidPostCategoryException; +import org.wooriverygood.api.post.exception.InvalidPostCategoryException; @Getter public enum PostCategory { diff --git a/src/main/java/org/wooriverygood/api/post/domain/PostLike.java b/src/main/java/org/wooriverygood/api/post/domain/PostLike.java index 728decc..fd74461 100644 --- a/src/main/java/org/wooriverygood/api/post/domain/PostLike.java +++ b/src/main/java/org/wooriverygood/api/post/domain/PostLike.java @@ -1,14 +1,13 @@ package org.wooriverygood.api.post.domain; import jakarta.persistence.*; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; +import org.wooriverygood.api.member.domain.Member; @Entity -@Table(name = "postLikes") @Getter -@NoArgsConstructor +@Table(name = "post_likes") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class PostLike { @Id @@ -19,16 +18,17 @@ public class PostLike { @JoinColumn(name = "post_id", referencedColumnName = "post_id") private Post post; - @Column - private String username; + @ManyToOne(fetch = FetchType.LAZY) + private Member member; @Builder - public PostLike(Long id, Post post, String username) { + public PostLike(Long id, Post post, Member member) { this.id = id; this.post = post; - this.username = username; + this.member = member; } + public void delete() { this.post = null; } diff --git a/src/main/java/org/wooriverygood/api/post/domain/Title.java b/src/main/java/org/wooriverygood/api/post/domain/Title.java new file mode 100644 index 0000000..77070e6 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/post/domain/Title.java @@ -0,0 +1,35 @@ +package org.wooriverygood.api.post.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.wooriverygood.api.post.exception.InvalidPostContentException; +import org.wooriverygood.api.post.exception.InvalidPostTitleException; + +@Getter +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Title { + + private static final int MAX_TITLE_LENGTH = 45; + + @Column(name = "post_title", length = MAX_TITLE_LENGTH, nullable = false) + private String value; + + + public Title(String value) { + validate(value); + this.value = value; + } + + private void validate(String value) { + if (value == null || value.isBlank()) + throw new InvalidPostTitleException(); + + if (value.length() > MAX_TITLE_LENGTH) + throw new InvalidPostContentException(); + } + +} diff --git a/src/main/java/org/wooriverygood/api/post/dto/NewPostRequest.java b/src/main/java/org/wooriverygood/api/post/dto/NewPostRequest.java index 7e9c03c..af57cbb 100644 --- a/src/main/java/org/wooriverygood/api/post/dto/NewPostRequest.java +++ b/src/main/java/org/wooriverygood/api/post/dto/NewPostRequest.java @@ -1,21 +1,28 @@ package org.wooriverygood.api.post.dto; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; import jakarta.validation.constraints.NotBlank; import lombok.Builder; import lombok.Getter; @Getter +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class NewPostRequest { @NotBlank(message = "제목을 비우면 안됩니다.") - private final String post_title; - private final String post_category; - private final String post_content; + private final String postTitle; + + private final String postCategory; + + private final String postContent; + @Builder - public NewPostRequest(String post_title, String post_category, String post_content) { - this.post_title = post_title; - this.post_category = post_category; - this.post_content = post_content; + public NewPostRequest(String postTitle, String postCategory, String postContent) { + this.postTitle = postTitle; + this.postCategory = postCategory; + this.postContent = postContent; } + } diff --git a/src/main/java/org/wooriverygood/api/post/dto/NewPostResponse.java b/src/main/java/org/wooriverygood/api/post/dto/NewPostResponse.java deleted file mode 100644 index 34f97ea..0000000 --- a/src/main/java/org/wooriverygood/api/post/dto/NewPostResponse.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.wooriverygood.api.post.dto; - -import lombok.Builder; -import lombok.Getter; -import org.wooriverygood.api.post.domain.PostCategory; - -@Getter -public class NewPostResponse { - - private final Long post_id; - private final String title; - private final String category; - private final String author; - - @Builder - public NewPostResponse(Long post_id, String title, String category, String author) { - this.post_id = post_id; - this.title = title; - this.category = category; - this.author = author; - } -} diff --git a/src/main/java/org/wooriverygood/api/post/dto/PostDeleteResponse.java b/src/main/java/org/wooriverygood/api/post/dto/PostDeleteResponse.java deleted file mode 100644 index ebeb733..0000000 --- a/src/main/java/org/wooriverygood/api/post/dto/PostDeleteResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.wooriverygood.api.post.dto; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class PostDeleteResponse { - - private final Long post_id; - - @Builder - public PostDeleteResponse(Long post_id) { - this.post_id = post_id; - } - -} diff --git a/src/main/java/org/wooriverygood/api/post/dto/PostDetailResponse.java b/src/main/java/org/wooriverygood/api/post/dto/PostDetailResponse.java new file mode 100644 index 0000000..d6c4af1 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/post/dto/PostDetailResponse.java @@ -0,0 +1,80 @@ +package org.wooriverygood.api.post.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Builder; +import lombok.Getter; +import org.wooriverygood.api.post.domain.Post; + +import java.time.LocalDateTime; + +@Getter +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class PostDetailResponse { + + private final Long postId; + + private final String postTitle; + + private final String postContent; + + private final String postCategory; + + private final long memberId; + + private final boolean isMine; + + private final int postComments; + + private final int postLikes; + + private final LocalDateTime postTime; + + private final boolean liked; + + private final boolean updated; + + private final boolean reported; + + private final int viewCount; + + + @Builder + public PostDetailResponse(Long postId, String postTitle, String postContent, + String postCategory, long memberId, int postComments, + int postLikes, LocalDateTime postTime, boolean liked, + boolean isMine, boolean updated, boolean reported, int viewCount) { + this.postId = postId; + this.postTitle = postTitle; + this.postContent = postContent; + this.postCategory = postCategory; + this.memberId = memberId; + this.isMine = isMine; + this.postComments = postComments; + this.postLikes = postLikes; + this.postTime = postTime; + this.liked = liked; + this.updated = updated; + this.reported = reported; + this.viewCount = viewCount; + } + + public static PostDetailResponse of(Post post, long memberId, boolean liked, boolean isMine) { + return PostDetailResponse.builder() + .postId(post.getId()) + .postTitle(post.getTitle()) + .postContent(post.getContent()) + .postCategory(post.getCategory().getValue()) + .isMine(isMine) + .memberId(memberId) + .postComments(post.getCommentCount()) + .postLikes(post.getLikeCount()) + .postTime(post.getCreatedAt()) + .liked(liked) + .updated(post.isUpdated()) + .reported(post.isReportedTooMuch()) + .viewCount(post.getViewCount()) + .build(); + } + +} diff --git a/src/main/java/org/wooriverygood/api/post/dto/PostLikeResponse.java b/src/main/java/org/wooriverygood/api/post/dto/PostLikeResponse.java index 1108ab7..fd5b3c1 100644 --- a/src/main/java/org/wooriverygood/api/post/dto/PostLikeResponse.java +++ b/src/main/java/org/wooriverygood/api/post/dto/PostLikeResponse.java @@ -1,19 +1,22 @@ package org.wooriverygood.api.post.dto; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Builder; import lombok.Getter; @Getter +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class PostLikeResponse { - private final int like_count; + private final int likeCount; private final boolean liked; @Builder - public PostLikeResponse(int like_count, boolean liked) { - this.like_count = like_count; + public PostLikeResponse(int likeCount, boolean liked) { + this.likeCount = likeCount; this.liked = liked; } } diff --git a/src/main/java/org/wooriverygood/api/post/dto/PostResponse.java b/src/main/java/org/wooriverygood/api/post/dto/PostResponse.java index 6c45604..7b4c21d 100644 --- a/src/main/java/org/wooriverygood/api/post/dto/PostResponse.java +++ b/src/main/java/org/wooriverygood/api/post/dto/PostResponse.java @@ -1,5 +1,7 @@ package org.wooriverygood.api.post.dto; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Builder; import lombok.Getter; import org.wooriverygood.api.post.domain.Post; @@ -7,23 +9,20 @@ import java.time.LocalDateTime; @Getter +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class PostResponse { - private final Long post_id; + private final Long postId; - private final String post_title; + private final String postTitle; - private final String post_content; + private final String postCategory; - private final String post_category; + private final int postComments; - private final String post_author; + private final int postLikes; - private final int post_comments; - - private final int post_likes; - - private final LocalDateTime post_time; + private final LocalDateTime postTime; private final boolean liked; @@ -31,36 +30,43 @@ public class PostResponse { private final boolean reported; + private final int viewCount; + + private final boolean isMine; + + private final long memberId; + @Builder - public PostResponse(Long post_id, String post_title, String post_content, String post_category, String post_author, int post_comments, int post_likes, LocalDateTime post_time, boolean liked, boolean updated, boolean reported) { - this.post_id = post_id; - this.post_title = post_title; - this.post_content = post_content; - this.post_category = post_category; - this.post_author = post_author; - this.post_comments = post_comments; - this.post_likes = post_likes; - this.post_time = post_time; + public PostResponse(Long postId, String postTitle, String postCategory, int postComments, int postLikes, LocalDateTime postTime, boolean liked, boolean updated, boolean reported, int viewCount, boolean isMine, long memberId) { + this.postId = postId; + this.postTitle = postTitle; + this.postCategory = postCategory; + this.postComments = postComments; + this.postLikes = postLikes; + this.postTime = postTime; this.liked = liked; this.updated = updated; this.reported = reported; + this.viewCount = viewCount; + this.isMine = isMine; + this.memberId = memberId; } - public static PostResponse from(Post post, boolean liked) { - boolean reported = post.getReportCount() >= 5; + public static PostResponse of(Post post, boolean liked, boolean isMine) { return PostResponse.builder() - .post_id(post.getId()) - .post_title(reported ? null : post.getTitle()) - .post_content(reported ? null : post.getContent()) - .post_category(post.getCategory().getValue()) - .post_author(post.getAuthor()) - .post_comments(post.getCommentCount()) - .post_likes(post.getLikeCount()) - .post_time(post.getCreatedAt()) + .postId(post.getId()) + .postTitle(post.getTitle()) + .postCategory(post.getCategory().getValue()) + .postComments(post.getCommentCount()) + .postLikes(post.getLikeCount()) + .postTime(post.getCreatedAt()) .liked(liked) .updated(post.isUpdated()) - .reported(reported) + .reported(post.isReportedTooMuch()) + .viewCount(post.getViewCount()) + .memberId(post.getMember().getId()) + .isMine(isMine) .build(); } diff --git a/src/main/java/org/wooriverygood/api/post/dto/PostUpdateRequest.java b/src/main/java/org/wooriverygood/api/post/dto/PostUpdateRequest.java index eba4da9..e0c8865 100644 --- a/src/main/java/org/wooriverygood/api/post/dto/PostUpdateRequest.java +++ b/src/main/java/org/wooriverygood/api/post/dto/PostUpdateRequest.java @@ -1,22 +1,25 @@ package org.wooriverygood.api.post.dto; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; import jakarta.validation.constraints.NotBlank; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; @Getter -@NoArgsConstructor +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class PostUpdateRequest { @NotBlank(message = "제목이 없습니다.") - private String post_title; + private String postTitle; + + private String postContent; - private String post_content; @Builder - public PostUpdateRequest(String post_title, String post_content) { - this.post_title = post_title; - this.post_content = post_content; + public PostUpdateRequest(String postTitle, String postContent) { + this.postTitle = postTitle; + this.postContent = postContent; } + } diff --git a/src/main/java/org/wooriverygood/api/post/dto/PostUpdateResponse.java b/src/main/java/org/wooriverygood/api/post/dto/PostUpdateResponse.java deleted file mode 100644 index 8556b4d..0000000 --- a/src/main/java/org/wooriverygood/api/post/dto/PostUpdateResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.wooriverygood.api.post.dto; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class PostUpdateResponse { - - private final Long post_id; - - - @Builder - public PostUpdateResponse(Long post_id) { - this.post_id = post_id; - } -} diff --git a/src/main/java/org/wooriverygood/api/advice/exception/InvalidPostCategoryException.java b/src/main/java/org/wooriverygood/api/post/exception/InvalidPostCategoryException.java similarity index 65% rename from src/main/java/org/wooriverygood/api/advice/exception/InvalidPostCategoryException.java rename to src/main/java/org/wooriverygood/api/post/exception/InvalidPostCategoryException.java index 69a474e..7a8939a 100644 --- a/src/main/java/org/wooriverygood/api/advice/exception/InvalidPostCategoryException.java +++ b/src/main/java/org/wooriverygood/api/post/exception/InvalidPostCategoryException.java @@ -1,6 +1,6 @@ -package org.wooriverygood.api.advice.exception; +package org.wooriverygood.api.post.exception; -import org.wooriverygood.api.advice.exception.general.BadRequestException; +import org.wooriverygood.api.global.error.exception.BadRequestException; public class InvalidPostCategoryException extends BadRequestException { diff --git a/src/main/java/org/wooriverygood/api/post/exception/InvalidPostContentException.java b/src/main/java/org/wooriverygood/api/post/exception/InvalidPostContentException.java new file mode 100644 index 0000000..c5eb1f3 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/post/exception/InvalidPostContentException.java @@ -0,0 +1,12 @@ +package org.wooriverygood.api.post.exception; + +import org.wooriverygood.api.global.error.exception.BadRequestException; + +public class InvalidPostContentException extends BadRequestException { + + private static final String MESSAGE = "게시글 내용이 유효하지 않습니다."; + + public InvalidPostContentException() { + super(MESSAGE); + } +} diff --git a/src/main/java/org/wooriverygood/api/post/exception/InvalidPostTitleException.java b/src/main/java/org/wooriverygood/api/post/exception/InvalidPostTitleException.java new file mode 100644 index 0000000..dcd787d --- /dev/null +++ b/src/main/java/org/wooriverygood/api/post/exception/InvalidPostTitleException.java @@ -0,0 +1,12 @@ +package org.wooriverygood.api.post.exception; + +import org.wooriverygood.api.global.error.exception.BadRequestException; + +public class InvalidPostTitleException extends BadRequestException { + + private static final String MESSAGE = "게시글 제목이 유효하지 않습니다."; + + public InvalidPostTitleException() { + super(MESSAGE); + } +} diff --git a/src/main/java/org/wooriverygood/api/advice/exception/PostNotFoundException.java b/src/main/java/org/wooriverygood/api/post/exception/PostNotFoundException.java similarity index 64% rename from src/main/java/org/wooriverygood/api/advice/exception/PostNotFoundException.java rename to src/main/java/org/wooriverygood/api/post/exception/PostNotFoundException.java index 16e945b..d4073b6 100644 --- a/src/main/java/org/wooriverygood/api/advice/exception/PostNotFoundException.java +++ b/src/main/java/org/wooriverygood/api/post/exception/PostNotFoundException.java @@ -1,6 +1,6 @@ -package org.wooriverygood.api.advice.exception; +package org.wooriverygood.api.post.exception; -import org.wooriverygood.api.advice.exception.general.NotFoundException; +import org.wooriverygood.api.global.error.exception.NotFoundException; public class PostNotFoundException extends NotFoundException { diff --git a/src/main/java/org/wooriverygood/api/post/repository/PostLikeRepository.java b/src/main/java/org/wooriverygood/api/post/repository/PostLikeRepository.java index b8f9a0a..f1ff129 100644 --- a/src/main/java/org/wooriverygood/api/post/repository/PostLikeRepository.java +++ b/src/main/java/org/wooriverygood/api/post/repository/PostLikeRepository.java @@ -1,6 +1,7 @@ package org.wooriverygood.api.post.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.wooriverygood.api.member.domain.Member; import org.wooriverygood.api.post.domain.Post; import org.wooriverygood.api.post.domain.PostLike; @@ -8,9 +9,9 @@ public interface PostLikeRepository extends JpaRepository { - Optional findByPostAndUsername(Post post, String username); + Optional findByPostAndMember(Post post, Member member); - boolean existsByPostAndUsername(Post post, String username); + boolean existsByPostAndMember(Post post, Member member); void deleteAllByPost(Post post); } diff --git a/src/main/java/org/wooriverygood/api/post/repository/PostRepository.java b/src/main/java/org/wooriverygood/api/post/repository/PostRepository.java index 7f7f04c..12aa857 100644 --- a/src/main/java/org/wooriverygood/api/post/repository/PostRepository.java +++ b/src/main/java/org/wooriverygood/api/post/repository/PostRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.member.domain.Member; import org.wooriverygood.api.post.domain.Post; import org.wooriverygood.api.post.domain.PostCategory; @@ -16,7 +17,7 @@ public interface PostRepository extends JpaRepository { Page findAllByCategoryOrderByIdDesc(PostCategory category, Pageable pageable); - Page findByAuthorOrderByIdDesc(String author, Pageable pageable); + Page findByMemberOrderByIdDesc(Member member, Pageable pageable); @Transactional @Modifying(clearAutomatically = true) @@ -32,4 +33,10 @@ public interface PostRepository extends JpaRepository { @Modifying(clearAutomatically = true) @Query(value = "UPDATE posts SET report_count = report_count + 1 WHERE post_id = :postId", nativeQuery = true) void increaseReportCount(Long postId); + + @Transactional + @Modifying(clearAutomatically = true) + @Query(value = "UPDATE posts SET view_count = view_count + 1 WHERE post_id = :postId", nativeQuery = true) + void increaseViewCount(Long postId); + } diff --git a/src/main/java/org/wooriverygood/api/post/service/PostService.java b/src/main/java/org/wooriverygood/api/post/service/PostService.java deleted file mode 100644 index 1032e39..0000000 --- a/src/main/java/org/wooriverygood/api/post/service/PostService.java +++ /dev/null @@ -1,172 +0,0 @@ -package org.wooriverygood.api.post.service; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.wooriverygood.api.advice.exception.InvalidPostCategoryException; -import org.wooriverygood.api.advice.exception.PostNotFoundException; -import org.wooriverygood.api.comment.repository.CommentRepository; -import org.wooriverygood.api.post.domain.Post; -import org.wooriverygood.api.post.domain.PostCategory; -import org.wooriverygood.api.post.domain.PostLike; -import org.wooriverygood.api.post.dto.*; -import org.wooriverygood.api.post.repository.PostLikeRepository; -import org.wooriverygood.api.post.repository.PostRepository; -import org.wooriverygood.api.support.AuthInfo; - -import java.util.List; -import java.util.Optional; - -@Service -@Transactional(readOnly = true) -public class PostService { - - private final PostRepository postRepository; - - private final PostLikeRepository postLikeRepository; - - private final CommentRepository commentRepository; - - - - public PostService(PostRepository postRepository, PostLikeRepository postLikeRepository, CommentRepository commentRepository) { - this.postRepository = postRepository; - this.postLikeRepository = postLikeRepository; - this.commentRepository = commentRepository; - } - - public PostsResponse findPosts(AuthInfo authInfo, Pageable pageable) { - Page page = postRepository.findAllByOrderByIdDesc(pageable); - return convertToPostsResponse(authInfo, page); - } - - public PostResponse findPostById(Long postId, AuthInfo authInfo) { - Post post = postRepository.findById(postId) - .orElseThrow(PostNotFoundException::new); - boolean liked = postLikeRepository.existsByPostAndUsername(post, authInfo.getUsername()); - return PostResponse.from(post, liked); - } - - @Transactional - public NewPostResponse addPost(AuthInfo authInfo, NewPostRequest newPostRequest) { - PostCategory.parse(newPostRequest.getPost_category()); - Post post = createPost(authInfo, newPostRequest); - Post saved = postRepository.save(post); - return createResponse(saved); - } - - private Post createPost(AuthInfo authInfo, NewPostRequest newPostRequest) { - return Post.builder() - .title(newPostRequest.getPost_title()) - .content(newPostRequest.getPost_content()) - .category(PostCategory.parse(newPostRequest.getPost_category())) - .author(authInfo.getUsername()) - .build(); - } - - private NewPostResponse createResponse(Post post) { - return NewPostResponse.builder() - .post_id(post.getId()) - .title(post.getTitle()) - .category(post.getCategory().getValue()) - .author(post.getAuthor()) - .build(); - } - - public PostsResponse findMyPosts(AuthInfo authInfo, Pageable pageable) { - Page page = postRepository.findByAuthorOrderByIdDesc(authInfo.getUsername(), pageable); - return convertToPostsResponse(authInfo, page); - } - - private PostsResponse convertToPostsResponse(AuthInfo authInfo, Page page) { - List posts = page.getContent().stream() - .map(post -> { - boolean liked = postLikeRepository.existsByPostAndUsername(post, authInfo.getUsername()); - return PostResponse.from(post, liked); - }) - .toList(); - - return PostsResponse.builder() - .posts(posts) - .totalPageCount(page.getTotalPages()) - .totalPostCount(page.getTotalElements()) - .build(); - } - - @Transactional - public PostLikeResponse likePost(Long postId, AuthInfo authInfo) { - Post post = postRepository.findById(postId) - .orElseThrow(PostNotFoundException::new); - - Optional postLike = postLikeRepository.findByPostAndUsername(post, authInfo.getUsername()); - - if (postLike.isEmpty()) { - addPostLike(post, authInfo.getUsername()); - return createPostLikeResponse(post, true); - } - - deletePostLike(post, postLike.get()); - return createPostLikeResponse(post, false); - } - - private void addPostLike(Post post, String username) { - PostLike newPostLike = PostLike.builder() - .post(post) - .username(username) - .build(); - - post.addPostLike(newPostLike); - postLikeRepository.save(newPostLike); - postRepository.increaseLikeCount(post.getId()); - } - - private void deletePostLike(Post post, PostLike postLike) { - post.deletePostLike(postLike); - postRepository.decreaseLikeCount(post.getId()); - } - - private PostLikeResponse createPostLikeResponse(Post post, boolean liked) { - int likeCount = post.getLikeCount() + (liked ? 1 : -1); - return PostLikeResponse.builder() - .like_count(likeCount) - .liked(liked) - .build(); - } - - @Transactional - public PostUpdateResponse updatePost(Long postId, PostUpdateRequest postUpdateRequest, AuthInfo authInfo) { - Post post = postRepository.findById(postId) - .orElseThrow(PostNotFoundException::new); - post.validateAuthor(authInfo.getUsername()); - - post.updateTitle(postUpdateRequest.getPost_title()); - post.updateContent(postUpdateRequest.getPost_content()); - - return PostUpdateResponse.builder() - .post_id(post.getId()) - .build(); - } - - @Transactional - public PostDeleteResponse deletePost(Long postId, AuthInfo authInfo) { - Post post = postRepository.findById(postId) - .orElseThrow(PostNotFoundException::new); - post.validateAuthor(authInfo.getUsername()); - - commentRepository.deleteAllByPost(post); - postLikeRepository.deleteAllByPost(post); - - postRepository.delete(post); - - return PostDeleteResponse.builder() - .post_id(postId) - .build(); - } - - public PostsResponse findPostsByCategory(AuthInfo authInfo, Pageable pageable, String postCategory) { - PostCategory category = PostCategory.parse(postCategory); - Page page = postRepository.findAllByCategoryOrderByIdDesc(category, pageable); - return convertToPostsResponse(authInfo, page); - } -} diff --git a/src/main/java/org/wooriverygood/api/report/controller/ReportController.java b/src/main/java/org/wooriverygood/api/report/api/ReportApi.java similarity index 64% rename from src/main/java/org/wooriverygood/api/report/controller/ReportController.java rename to src/main/java/org/wooriverygood/api/report/api/ReportApi.java index 5c3a927..4ad5b14 100644 --- a/src/main/java/org/wooriverygood/api/report/controller/ReportController.java +++ b/src/main/java/org/wooriverygood/api/report/api/ReportApi.java @@ -1,33 +1,33 @@ -package org.wooriverygood.api.report.controller; +package org.wooriverygood.api.report.api; import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import org.wooriverygood.api.report.application.PostReportService; import org.wooriverygood.api.report.dto.ReportRequest; -import org.wooriverygood.api.report.service.ReportService; -import org.wooriverygood.api.support.AuthInfo; -import org.wooriverygood.api.support.Login; +import org.wooriverygood.api.report.application.CommentReportService; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.global.auth.Login; @RestController -public class ReportController { +@RequiredArgsConstructor +public class ReportApi { - private final ReportService reportService; + private final CommentReportService commentReportService; - - public ReportController(ReportService reportService) { - this.reportService = reportService; - } + private final PostReportService postReportService; @PostMapping("/posts/{id}/report") public ResponseEntity reportPost(@PathVariable("id") Long postId, @Valid @RequestBody ReportRequest request, @Login AuthInfo authInfo) { - reportService.reportPost(postId, request, authInfo); + postReportService.reportPost(postId, request, authInfo); return ResponseEntity.status(HttpStatus.CREATED).build(); } @@ -35,7 +35,7 @@ public ResponseEntity reportPost(@PathVariable("id") Long postId, public ResponseEntity reportComment(@PathVariable("id") Long commentId, @Valid @RequestBody ReportRequest request, @Login AuthInfo authInfo) { - reportService.reportComment(commentId, request, authInfo); + commentReportService.reportComment(commentId, request, authInfo); return ResponseEntity.status(HttpStatus.CREATED).build(); } } diff --git a/src/main/java/org/wooriverygood/api/report/application/CommentReportService.java b/src/main/java/org/wooriverygood/api/report/application/CommentReportService.java new file mode 100644 index 0000000..0984bcb --- /dev/null +++ b/src/main/java/org/wooriverygood/api/report/application/CommentReportService.java @@ -0,0 +1,53 @@ +package org.wooriverygood.api.report.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.comment.exception.CommentNotFoundException; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.exception.MemberNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.report.exception.DuplicatedCommentReportException; +import org.wooriverygood.api.comment.domain.Comment; +import org.wooriverygood.api.comment.repository.CommentRepository; +import org.wooriverygood.api.report.domain.CommentReport; +import org.wooriverygood.api.report.dto.ReportRequest; +import org.wooriverygood.api.report.repository.CommentReportRepository; +import org.wooriverygood.api.global.auth.AuthInfo; + +@Service +@Transactional +@RequiredArgsConstructor +public class CommentReportService { + + private final CommentRepository commentRepository; + + private final CommentReportRepository commentReportRepository; + + private final MemberRepository memberRepository; + + + public void reportComment(Long commentId, ReportRequest request, AuthInfo authInfo) { + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + Comment comment = commentRepository.findById(commentId) + .orElseThrow(CommentNotFoundException::new); + + CommentReport report = CommentReport.builder() + .comment(comment) + .message(request.getMessage()) + .member(member) + .build(); + + checkIfAlreadyReport(comment, member); + comment.addReport(report); + commentRepository.increaseReportCount(commentId); + + commentReportRepository.save(report); + } + + private void checkIfAlreadyReport(Comment comment, Member member) { + if (comment.hasReportByMember(member)) + throw new DuplicatedCommentReportException(); + } +} diff --git a/src/main/java/org/wooriverygood/api/report/application/PostReportService.java b/src/main/java/org/wooriverygood/api/report/application/PostReportService.java new file mode 100644 index 0000000..20d64bf --- /dev/null +++ b/src/main/java/org/wooriverygood/api/report/application/PostReportService.java @@ -0,0 +1,54 @@ +package org.wooriverygood.api.report.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.exception.MemberNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.report.exception.DuplicatedPostReportException; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.post.domain.Post; +import org.wooriverygood.api.post.exception.PostNotFoundException; +import org.wooriverygood.api.post.repository.PostRepository; +import org.wooriverygood.api.report.domain.PostReport; +import org.wooriverygood.api.report.dto.ReportRequest; +import org.wooriverygood.api.report.repository.PostReportRepository; + +@Service +@Transactional +@RequiredArgsConstructor +public class PostReportService { + + private final PostRepository postRepository; + + private final PostReportRepository postReportRepository; + + private final MemberRepository memberRepository; + + + public void reportPost(Long postId, ReportRequest request, AuthInfo authInfo) { + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + Post post = postRepository.findById(postId) + .orElseThrow(PostNotFoundException::new); + + PostReport report = PostReport.builder() + .post(post) + .message(request.getMessage()) + .member(member) + .build(); + + checkIfAlreadyReport(post, member); + post.addReport(report); + postRepository.increaseReportCount(postId); + + postReportRepository.save(report); + } + + private void checkIfAlreadyReport(Post post, Member member) { + if (post.hasReportByMember(member)) + throw new DuplicatedPostReportException(); + } + +} diff --git a/src/main/java/org/wooriverygood/api/report/domain/CommentReport.java b/src/main/java/org/wooriverygood/api/report/domain/CommentReport.java index 336b0a6..917a59f 100644 --- a/src/main/java/org/wooriverygood/api/report/domain/CommentReport.java +++ b/src/main/java/org/wooriverygood/api/report/domain/CommentReport.java @@ -1,14 +1,17 @@ package org.wooriverygood.api.report.domain; import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.wooriverygood.api.comment.domain.Comment; +import org.wooriverygood.api.member.domain.Member; @Entity @Getter -@NoArgsConstructor +@Table(name = "comment_reports") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class CommentReport { @Id @@ -19,21 +22,21 @@ public class CommentReport { @JoinColumn(name = "comment_id", referencedColumnName = "comment_id") private Comment comment; - @Column - private String username; + @ManyToOne(fetch = FetchType.LAZY) + private Member member; private String message; @Builder - public CommentReport(Long id, Comment comment, String username, String message) { + public CommentReport(Long id, Comment comment, Member member, String message) { this.id = id; this.comment = comment; - this.username = username; + this.member = member; this.message = message; } - public boolean isOwner(String username) { - return this.username.equals(username); + public boolean isOwner(Member member) { + return this.member.equals(member); } } diff --git a/src/main/java/org/wooriverygood/api/report/domain/PostReport.java b/src/main/java/org/wooriverygood/api/report/domain/PostReport.java index 77197b9..e34ae25 100644 --- a/src/main/java/org/wooriverygood/api/report/domain/PostReport.java +++ b/src/main/java/org/wooriverygood/api/report/domain/PostReport.java @@ -1,14 +1,17 @@ package org.wooriverygood.api.report.domain; import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.wooriverygood.api.member.domain.Member; import org.wooriverygood.api.post.domain.Post; @Entity @Getter -@NoArgsConstructor +@Table(name = "post_reports") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class PostReport { @Id @@ -19,22 +22,22 @@ public class PostReport { @JoinColumn(name = "post_id", referencedColumnName = "post_id") private Post post; - @Column - private String username; + @ManyToOne(fetch = FetchType.LAZY) + private Member member; private String message; @Builder - public PostReport(Long id, Post post, String username, String message) { + public PostReport(Long id, Post post, Member member, String message) { this.id = id; this.post = post; - this.username = username; + this.member = member; this.message = message; } - public boolean isOwner(String username) { - return this.username.equals(username); + public boolean isOwner(Member member) { + return this.member.equals(member); } } diff --git a/src/main/java/org/wooriverygood/api/report/dto/ReportRequest.java b/src/main/java/org/wooriverygood/api/report/dto/ReportRequest.java index ec447b2..b95509a 100644 --- a/src/main/java/org/wooriverygood/api/report/dto/ReportRequest.java +++ b/src/main/java/org/wooriverygood/api/report/dto/ReportRequest.java @@ -1,7 +1,6 @@ package org.wooriverygood.api.report.dto; import jakarta.validation.constraints.NotBlank; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -12,8 +11,8 @@ public class ReportRequest { @NotBlank(message = "신고내용은 1자 이상 255자 이하여야 합니다.") private String message; - @Builder public ReportRequest(String message) { this.message = message; } + } diff --git a/src/main/java/org/wooriverygood/api/advice/exception/DuplicatedCommentReportException.java b/src/main/java/org/wooriverygood/api/report/exception/DuplicatedCommentReportException.java similarity index 66% rename from src/main/java/org/wooriverygood/api/advice/exception/DuplicatedCommentReportException.java rename to src/main/java/org/wooriverygood/api/report/exception/DuplicatedCommentReportException.java index 8d29d16..bcd3acc 100644 --- a/src/main/java/org/wooriverygood/api/advice/exception/DuplicatedCommentReportException.java +++ b/src/main/java/org/wooriverygood/api/report/exception/DuplicatedCommentReportException.java @@ -1,6 +1,6 @@ -package org.wooriverygood.api.advice.exception; +package org.wooriverygood.api.report.exception; -import org.wooriverygood.api.advice.exception.general.BadRequestException; +import org.wooriverygood.api.global.error.exception.BadRequestException; public class DuplicatedCommentReportException extends BadRequestException { diff --git a/src/main/java/org/wooriverygood/api/advice/exception/DuplicatedPostReportException.java b/src/main/java/org/wooriverygood/api/report/exception/DuplicatedPostReportException.java similarity index 65% rename from src/main/java/org/wooriverygood/api/advice/exception/DuplicatedPostReportException.java rename to src/main/java/org/wooriverygood/api/report/exception/DuplicatedPostReportException.java index 074091c..3ec45d8 100644 --- a/src/main/java/org/wooriverygood/api/advice/exception/DuplicatedPostReportException.java +++ b/src/main/java/org/wooriverygood/api/report/exception/DuplicatedPostReportException.java @@ -1,6 +1,6 @@ -package org.wooriverygood.api.advice.exception; +package org.wooriverygood.api.report.exception; -import org.wooriverygood.api.advice.exception.general.BadRequestException; +import org.wooriverygood.api.global.error.exception.BadRequestException; public class DuplicatedPostReportException extends BadRequestException { diff --git a/src/main/java/org/wooriverygood/api/report/service/ReportService.java b/src/main/java/org/wooriverygood/api/report/service/ReportService.java deleted file mode 100644 index 8757453..0000000 --- a/src/main/java/org/wooriverygood/api/report/service/ReportService.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.wooriverygood.api.report.service; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.wooriverygood.api.advice.exception.CommentNotFoundException; -import org.wooriverygood.api.advice.exception.DuplicatedCommentReportException; -import org.wooriverygood.api.advice.exception.DuplicatedPostReportException; -import org.wooriverygood.api.advice.exception.PostNotFoundException; -import org.wooriverygood.api.comment.domain.Comment; -import org.wooriverygood.api.comment.repository.CommentRepository; -import org.wooriverygood.api.post.domain.Post; -import org.wooriverygood.api.post.repository.PostRepository; -import org.wooriverygood.api.report.domain.CommentReport; -import org.wooriverygood.api.report.domain.PostReport; -import org.wooriverygood.api.report.dto.ReportRequest; -import org.wooriverygood.api.report.repository.CommentReportRepository; -import org.wooriverygood.api.report.repository.PostReportRepository; -import org.wooriverygood.api.support.AuthInfo; - -@Service -@Transactional -public class ReportService { - - private final CommentRepository commentRepository; - - private final CommentReportRepository commentReportRepository; - - private final PostRepository postRepository; - - private final PostReportRepository postReportRepository; - - - public ReportService(CommentRepository commentRepository, CommentReportRepository commentReportRepository, PostRepository postRepository, PostReportRepository postReportRepository) { - this.commentRepository = commentRepository; - this.commentReportRepository = commentReportRepository; - this.postRepository = postRepository; - this.postReportRepository = postReportRepository; - } - - public void reportPost(Long postId, ReportRequest request, AuthInfo authInfo) { - Post post = postRepository.findById(postId) - .orElseThrow(PostNotFoundException::new); - - PostReport report = PostReport.builder() - .post(post) - .message(request.getMessage()) - .username(authInfo.getUsername()) - .build(); - - checkAlreadyReport(post, authInfo); - post.addReport(report); - postRepository.increaseReportCount(postId); - - postReportRepository.save(report); - } - - private void checkAlreadyReport(Post post, AuthInfo authInfo) { - if (post.hasReportByUser(authInfo.getUsername())) - throw new DuplicatedPostReportException(); - } - - public void reportComment(Long commentId, ReportRequest request, AuthInfo authInfo) { - Comment comment = commentRepository.findById(commentId) - .orElseThrow(CommentNotFoundException::new); - - CommentReport report = CommentReport.builder() - .comment(comment) - .message(request.getMessage()) - .username(authInfo.getUsername()) - .build(); - - checkAlreadyReport(comment, authInfo); - comment.addReport(report); - commentRepository.increaseReportCount(commentId); - - commentReportRepository.save(report); - } - - private void checkAlreadyReport(Comment comment, AuthInfo authInfo) { - if (comment.hasReportByUser(authInfo.getUsername())) - throw new DuplicatedCommentReportException(); - } -} diff --git a/src/main/java/org/wooriverygood/api/review/api/ReviewApi.java b/src/main/java/org/wooriverygood/api/review/api/ReviewApi.java new file mode 100644 index 0000000..0fbdd99 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/review/api/ReviewApi.java @@ -0,0 +1,73 @@ +package org.wooriverygood.api.review.api; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.wooriverygood.api.review.application.*; +import org.wooriverygood.api.review.dto.*; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.global.auth.Login; + +@RestController +@RequiredArgsConstructor +public class ReviewApi { + + private final ReviewLikeToggleService reviewLikeToggleService; + + private final ReviewCreateService reviewCreateService; + + private final ReviewDeleteService reviewDeleteService; + + private final ReviewFindService reviewFindService; + + private final ReviewUpdateService reviewUpdateService; + + private final ReviewValidateAccessService reviewValidateAccessService; + + + @GetMapping("/courses/{id}/reviews") + public ResponseEntity findAllReviewsByCourseId(@PathVariable("id") Long courseId, + @Login AuthInfo authInfo) { + reviewValidateAccessService.validateReviewAccess(authInfo); + ReviewsResponse response = reviewFindService.findAllReviewsByCourseId(courseId, authInfo); + return ResponseEntity.ok(response); + } + + @PostMapping("/courses/{id}/reviews") + public ResponseEntity addReview(@PathVariable("id") Long courseId, @Login AuthInfo authInfo, + @Valid @RequestBody NewReviewRequest request) { + reviewCreateService.addReview(authInfo, courseId, request); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @PutMapping("/reviews/{rid}/like") + public ResponseEntity likeReview(@PathVariable("rid") Long reviewId, + @Login AuthInfo authInfo) { + ReviewLikeResponse response= reviewLikeToggleService.toggleReviewLike(reviewId, authInfo); + return ResponseEntity.ok(response); + } + + @GetMapping("/reviews/me") + public ResponseEntity findMyReviews(@Login AuthInfo authInfo) { + ReviewsResponse response = reviewFindService.findMyReviews(authInfo); + return ResponseEntity.ok(response); + } + + @PutMapping("/reviews/{id}") + public ResponseEntity updateReview(@PathVariable("id") Long reviewId, + @Valid @RequestBody ReviewUpdateRequest request, + @Login AuthInfo authInfo) { + reviewUpdateService.updateReview(reviewId, request, authInfo); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/reviews/{id}") + public ResponseEntity deleteReview(@PathVariable("id") Long reviewId, + @Login AuthInfo authInfo) { + reviewDeleteService.deleteReview(reviewId, authInfo); + return ResponseEntity.noContent().build(); + } + +} diff --git a/src/main/java/org/wooriverygood/api/review/application/ReviewCreateService.java b/src/main/java/org/wooriverygood/api/review/application/ReviewCreateService.java new file mode 100644 index 0000000..b059183 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/review/application/ReviewCreateService.java @@ -0,0 +1,53 @@ +package org.wooriverygood.api.review.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.course.exception.CourseNotFoundException; +import org.wooriverygood.api.course.domain.Course; +import org.wooriverygood.api.course.repository.CourseRepository; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.exception.MemberNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.review.domain.Review; +import org.wooriverygood.api.review.dto.NewReviewRequest; +import org.wooriverygood.api.review.repository.ReviewRepository; + +@Service +@RequiredArgsConstructor +public class ReviewCreateService { + + private final CourseRepository courseRepository; + + private final ReviewRepository reviewRepository; + + private final MemberRepository memberRepository; + + + @Transactional + public void addReview(AuthInfo authInfo, Long courseId, NewReviewRequest request) { + Course course = courseRepository.findById(courseId) + .orElseThrow(CourseNotFoundException::new); + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + Review review = createReview(course, request, member); + member.addReview(review); + + reviewRepository.save(review); + courseRepository.increaseReviewCount(review.getCourse().getId()); + } + + private Review createReview(Course course, NewReviewRequest request, Member member) { + return Review.builder() + .reviewTitle(request.getReviewTitle()) + .course(course) + .instructorName(request.getInstructorName()) + .member(member) + .takenSemyr(request.getTakenSemyr()) + .reviewContent(request.getReviewContent()) + .grade(request.getGrade()) + .build(); + } + +} diff --git a/src/main/java/org/wooriverygood/api/review/application/ReviewDeleteService.java b/src/main/java/org/wooriverygood/api/review/application/ReviewDeleteService.java new file mode 100644 index 0000000..8e1f122 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/review/application/ReviewDeleteService.java @@ -0,0 +1,42 @@ +package org.wooriverygood.api.review.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.course.repository.CourseRepository; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.exception.MemberNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.review.domain.Review; +import org.wooriverygood.api.review.exception.ReviewNotFoundException; +import org.wooriverygood.api.review.repository.ReviewLikeRepository; +import org.wooriverygood.api.review.repository.ReviewRepository; + +@Service +@RequiredArgsConstructor +public class ReviewDeleteService { + + private final CourseRepository courseRepository; + + private final ReviewRepository reviewRepository; + + private final ReviewLikeRepository reviewLikeRepository; + + private final MemberRepository memberRepository; + + + @Transactional + public void deleteReview(Long reviewId, AuthInfo authInfo) { + Review review = reviewRepository.findById(reviewId) + .orElseThrow(ReviewNotFoundException::new); + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + review.validateAuthor(member); + + reviewLikeRepository.deleteAllByReview(review); + reviewRepository.delete(review); + courseRepository.decreaseReviewCount(review.getCourse().getId()); + } + +} diff --git a/src/main/java/org/wooriverygood/api/review/application/ReviewFindService.java b/src/main/java/org/wooriverygood/api/review/application/ReviewFindService.java new file mode 100644 index 0000000..3e51951 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/review/application/ReviewFindService.java @@ -0,0 +1,57 @@ +package org.wooriverygood.api.review.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.exception.MemberNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.review.domain.Review; +import org.wooriverygood.api.review.dto.ReviewResponse; +import org.wooriverygood.api.review.dto.ReviewsResponse; +import org.wooriverygood.api.review.repository.ReviewLikeRepository; +import org.wooriverygood.api.review.repository.ReviewRepository; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ReviewFindService { + + private final ReviewRepository reviewRepository; + + private final ReviewLikeRepository reviewLikeRepository; + + private final MemberRepository memberRepository; + + + public ReviewsResponse findAllReviewsByCourseId(Long courseId, AuthInfo authInfo) { + List reviews = reviewRepository.findAllByCourseId(courseId); + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + + return new ReviewsResponse(reviews.stream() + .map(review -> { + boolean liked = reviewLikeRepository.existsByReviewAndMember(review, member); + return ReviewResponse.of(review, + review.isSameAuthor(member), + liked); + }) + .toList()); + } + + public ReviewsResponse findMyReviews(AuthInfo authInfo) { + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + List reviews= member.getReviews(); + return new ReviewsResponse(reviews.stream() + .map(review -> { + boolean liked = reviewLikeRepository.existsByReviewAndMember(review, member); + return ReviewResponse.of(review, true, liked); + }) + .toList()); + } + +} diff --git a/src/main/java/org/wooriverygood/api/review/application/ReviewLikeToggleService.java b/src/main/java/org/wooriverygood/api/review/application/ReviewLikeToggleService.java new file mode 100644 index 0000000..44925c0 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/review/application/ReviewLikeToggleService.java @@ -0,0 +1,73 @@ +package org.wooriverygood.api.review.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.exception.MemberNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.review.exception.ReviewNotFoundException; +import org.wooriverygood.api.review.domain.Review; +import org.wooriverygood.api.review.domain.ReviewLike; +import org.wooriverygood.api.review.dto.*; +import org.wooriverygood.api.review.repository.ReviewLikeRepository; +import org.wooriverygood.api.review.repository.ReviewRepository; +import org.wooriverygood.api.global.auth.AuthInfo; + +import java.util.Optional; + +@Service +@Transactional +@RequiredArgsConstructor +public class ReviewLikeToggleService { + + private final ReviewRepository reviewRepository; + + private final ReviewLikeRepository reviewLikeRepository; + + private final MemberRepository memberRepository; + + + public ReviewLikeResponse toggleReviewLike(Long reviewId, AuthInfo authInfo) { + Review review = reviewRepository.findById(reviewId) + .orElseThrow(ReviewNotFoundException::new); + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + + Optional reviewLike = reviewLikeRepository.findByReviewAndMember(review, member); + + if (reviewLike.isEmpty()) { + addReviewLike(review, member); + return createReviewLikeResponse(review, true); + } + + deleteReviewLike(review, reviewLike.get()); + return createReviewLikeResponse(review, false); + + } + + private void addReviewLike(Review review, Member member) { + ReviewLike reviewLike = ReviewLike.builder() + .review(review) + .member(member) + .build(); + + review.addReviewLike(reviewLike); + reviewLikeRepository.save(reviewLike); + reviewRepository.increaseLikeCount(review.getId()); + } + + private void deleteReviewLike(Review review, ReviewLike reviewLike) { + review.deleteReviewLike(reviewLike); + reviewRepository.decreaseLikeCount(review.getId()); + } + + private ReviewLikeResponse createReviewLikeResponse(Review review, boolean liked) { + int likeCount = review.getLikeCount() + (liked ? 1 : -1); + return ReviewLikeResponse.builder() + .likeCount(likeCount) + .liked(liked) + .build(); + } + +} diff --git a/src/main/java/org/wooriverygood/api/review/application/ReviewUpdateService.java b/src/main/java/org/wooriverygood/api/review/application/ReviewUpdateService.java new file mode 100644 index 0000000..d838810 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/review/application/ReviewUpdateService.java @@ -0,0 +1,39 @@ +package org.wooriverygood.api.review.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.exception.MemberNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.review.domain.Review; +import org.wooriverygood.api.review.dto.ReviewUpdateRequest; +import org.wooriverygood.api.review.exception.ReviewNotFoundException; +import org.wooriverygood.api.review.repository.ReviewRepository; + +@Service +@RequiredArgsConstructor +public class ReviewUpdateService { + + private final ReviewRepository reviewRepository; + + private final MemberRepository memberRepository; + + + @Transactional + public void updateReview(Long reviewId, ReviewUpdateRequest request, AuthInfo authInfo) { + Review review = reviewRepository.findById(reviewId) + .orElseThrow(ReviewNotFoundException::new); + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + review.validateAuthor(member); + + review.updateTitle(request.getReviewTitle()); + review.updateInstructorName(request.getInstructorName()); + review.updateTakenSemyr(request.getTakenSemyr()); + review.updateContent(request.getReviewContent()); + review.updateGrade(request.getGrade()); + } + +} diff --git a/src/main/java/org/wooriverygood/api/review/application/ReviewValidateAccessService.java b/src/main/java/org/wooriverygood/api/review/application/ReviewValidateAccessService.java new file mode 100644 index 0000000..625f790 --- /dev/null +++ b/src/main/java/org/wooriverygood/api/review/application/ReviewValidateAccessService.java @@ -0,0 +1,39 @@ +package org.wooriverygood.api.review.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.exception.MemberNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.review.exception.ReviewAccessDeniedException; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.review.domain.Review; +import org.wooriverygood.api.review.repository.ReviewRepository; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ReviewValidateAccessService { + + private final ReviewRepository reviewRepository; + + private final MemberRepository memberRepository; + + + public void validateReviewAccess(AuthInfo authInfo) { + Member member = memberRepository.findById(authInfo.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + Review review = reviewRepository.findTopByMemberOrderByCreatedAtDesc(member) + .orElseThrow(ReviewAccessDeniedException::new); + + LocalDateTime now = LocalDateTime.now(); + long distance = ChronoUnit.MONTHS.between(review.getCreatedAt(), now); + if (distance > 6) + throw new ReviewAccessDeniedException(); + } + +} diff --git a/src/main/java/org/wooriverygood/api/review/controller/ReviewController.java b/src/main/java/org/wooriverygood/api/review/controller/ReviewController.java deleted file mode 100644 index 173143e..0000000 --- a/src/main/java/org/wooriverygood/api/review/controller/ReviewController.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.wooriverygood.api.review.controller; - -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.wooriverygood.api.advice.exception.ReviewAccessDeniedException; -import org.wooriverygood.api.review.dto.*; -import org.wooriverygood.api.review.service.ReviewService; -import org.wooriverygood.api.support.AuthInfo; -import org.wooriverygood.api.support.Login; - -import java.util.List; - -@RestController -public class ReviewController { - private final ReviewService reviewService; - - public ReviewController(ReviewService reviewService) { - this.reviewService = reviewService; - } - - @GetMapping("/") - public ResponseEntity apiOnline() { - return ResponseEntity.ok("API Online!"); - } - - @GetMapping("/courses/{id}/reviews") - public ResponseEntity> findAllReviewsByCourseId(@PathVariable("id") Long courseId, @Login AuthInfo authInfo) { - if (!reviewService.canAccessReviews(authInfo)) { - throw new ReviewAccessDeniedException(); - } - List reviews = reviewService.findAllReviewsByCourseId(courseId, authInfo); - return ResponseEntity.ok(reviews); - } - - @PostMapping("/courses/{id}/reviews") - public ResponseEntity addReview(@PathVariable("id") Long courseId, @Login AuthInfo authInfo, @Valid @RequestBody NewReviewRequest newReviewRequest) { - NewReviewResponse response = reviewService.addReview(authInfo, courseId, newReviewRequest); - return ResponseEntity.status(HttpStatus.CREATED).body(response); - } - - @PutMapping("/courses/reviews/{rid}/like") - public ResponseEntity likeReview(@PathVariable("rid") Long reviewId, @Login AuthInfo authInfo) { - ReviewLikeResponse response= reviewService.likeReview(reviewId, authInfo); - return ResponseEntity.ok(response); - } - - @GetMapping("/courses/reviews/me") - public ResponseEntity> findMyPosts(@Login AuthInfo authInfo) { - List reviewResponses = reviewService.findMyReviews(authInfo); - return ResponseEntity.ok(reviewResponses); - } - - @PutMapping("/courses/reviews/{rid}") - public ResponseEntity updateReview(@PathVariable("rid") Long reviewId, - @Valid @RequestBody ReviewUpdateRequest reviewUpdateRequest, - @Login AuthInfo authInfo) { - ReviewUpdateResponse response = reviewService.updateReview(reviewId, reviewUpdateRequest, authInfo); - return ResponseEntity.ok(response); - } - - @DeleteMapping("/courses/reviews/{rid}") - public ResponseEntity deleteReview(@PathVariable("rid") Long reviewId, - @Login AuthInfo authInfo) { - ReviewDeleteResponse response = reviewService.deleteReview(reviewId, authInfo); - return ResponseEntity.ok(response); - } - - -} diff --git a/src/main/java/org/wooriverygood/api/review/domain/Review.java b/src/main/java/org/wooriverygood/api/review/domain/Review.java index ee41882..2e339a0 100644 --- a/src/main/java/org/wooriverygood/api/review/domain/Review.java +++ b/src/main/java/org/wooriverygood/api/review/domain/Review.java @@ -1,23 +1,25 @@ package org.wooriverygood.api.review.domain; import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.ColumnDefault; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import org.wooriverygood.api.advice.exception.AuthorizationException; -import org.wooriverygood.api.course.domain.Courses; +import org.wooriverygood.api.global.error.exception.AuthorizationException; +import org.wooriverygood.api.course.domain.Course; +import org.wooriverygood.api.member.domain.Member; import java.time.LocalDateTime; import java.util.List; @Entity @Getter -@NoArgsConstructor -@EntityListeners(AuditingEntityListener.class) @Table(name = "reviews") +@EntityListeners(AuditingEntityListener.class) +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Review { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -26,7 +28,7 @@ public class Review { @ManyToOne @JoinColumn(name = "course_id", referencedColumnName = "course_id") - private Courses course; + private Course course; @Column(name = "review_content", length = 1000) private String reviewContent; @@ -43,8 +45,8 @@ public class Review { @Column(name = "grade", length = 45, nullable = false) private String grade; - @Column(name = "author_email", length = 300) - private String authorEmail; + @ManyToOne(fetch = FetchType.LAZY) + private Member member; @Column(name = "like_count") @ColumnDefault("0") @@ -61,7 +63,9 @@ public class Review { private boolean updated; @Builder - public Review(Long id, Courses course, String reviewContent, String reviewTitle, String instructorName, String takenSemyr, String grade, String authorEmail, LocalDateTime createdAt, List reviewLikes, boolean updated) { + public Review(Long id, Course course, String reviewContent, String reviewTitle, String instructorName, + String takenSemyr, String grade, LocalDateTime createdAt, Member member, + List reviewLikes, boolean updated) { this.id = id; this.course = course; this.reviewContent = reviewContent; @@ -69,20 +73,18 @@ public Review(Long id, Courses course, String reviewContent, String reviewTitle, this.instructorName = instructorName; this.takenSemyr = takenSemyr; this.grade = grade; - this.authorEmail = authorEmail; this.createdAt = createdAt; this.reviewLikes = reviewLikes; this.updated = updated; + this.member = member; } - public boolean isSameAuthor(String author) { - return this.authorEmail.equals(author); + public boolean isSameAuthor(Member member) { + return this.member.isSame(member); } - public void validateAuthor(String author) { - if (!this.authorEmail.equals(author)) { - throw new AuthorizationException(); - } + public void validateAuthor(Member member) { + this.member.verify(member); } public void addReviewLike(ReviewLike reviewLike) { @@ -94,19 +96,29 @@ public void deleteReviewLike(ReviewLike reviewLike) { reviewLike.delete(); } - public void updateReview(String title, String instructorName, String takenSemyr, String content, String grade, String author) { - this.reviewTitle = title; - this.reviewContent = content; - this.instructorName = instructorName; + public void updateTitle(String title) { + reviewTitle = title; + updated = true; + } + + public void updateContent(String content) { + reviewContent = content; + updated = true; + } + + public void updateTakenSemyr(String takenSemyr) { this.takenSemyr = takenSemyr; + updated = true; + } + + public void updateInstructorName(String instructorName) { + this.instructorName = instructorName; + updated = true; + } + + public void updateGrade(String grade) { this.grade = grade; updated = true; - if (id <= 234) { - authorEmail = author; - } - if (createdAt == null) { - createdAt = LocalDateTime.now(); - } } } diff --git a/src/main/java/org/wooriverygood/api/review/domain/ReviewLike.java b/src/main/java/org/wooriverygood/api/review/domain/ReviewLike.java index 96e0960..bbf5398 100644 --- a/src/main/java/org/wooriverygood/api/review/domain/ReviewLike.java +++ b/src/main/java/org/wooriverygood/api/review/domain/ReviewLike.java @@ -1,30 +1,34 @@ package org.wooriverygood.api.review.domain; import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.wooriverygood.api.member.domain.Member; @Entity -@Table(name = "reviewLikes") @Getter -@NoArgsConstructor +@Table(name = "review_likes") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class ReviewLike { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "review_id", referencedColumnName = "review_id") private Review review; - @Column - private String username; + @ManyToOne(fetch = FetchType.LAZY) + private Member member; + + @Builder - public ReviewLike(Long id, Review review, String username) { + public ReviewLike(Long id, Review review, Member member) { this.id = id; this.review = review; - this.username = username; + this.member = member; } public void delete() { review = null; } diff --git a/src/main/java/org/wooriverygood/api/review/dto/NewReviewRequest.java b/src/main/java/org/wooriverygood/api/review/dto/NewReviewRequest.java index 9069bbf..c036373 100644 --- a/src/main/java/org/wooriverygood/api/review/dto/NewReviewRequest.java +++ b/src/main/java/org/wooriverygood/api/review/dto/NewReviewRequest.java @@ -1,22 +1,35 @@ package org.wooriverygood.api.review.dto; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import jakarta.validation.constraints.NotBlank; import lombok.Builder; import lombok.Getter; @Getter +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class NewReviewRequest { - private final String review_title; - private final String instructor_name; - private final String taken_semyr; - private final String review_content; + + @NotBlank(message = "제목이 없습니다.") + private final String reviewTitle; + + private final String instructorName; + + private final String takenSemyr; + + private final String reviewContent; + private final String grade; + @Builder - public NewReviewRequest(String review_title, String instructor_name, String taken_semyr, String review_content, String grade) { - this.review_title = review_title; - this.instructor_name = instructor_name; - this.taken_semyr = taken_semyr; - this.review_content = review_content; + public NewReviewRequest(String reviewTitle, String instructorName, String takenSemyr, + String reviewContent, String grade) { + this.reviewTitle = reviewTitle; + this.instructorName = instructorName; + this.takenSemyr = takenSemyr; + this.reviewContent = reviewContent; this.grade = grade; } + } diff --git a/src/main/java/org/wooriverygood/api/review/dto/NewReviewResponse.java b/src/main/java/org/wooriverygood/api/review/dto/NewReviewResponse.java deleted file mode 100644 index 2ddc3f8..0000000 --- a/src/main/java/org/wooriverygood/api/review/dto/NewReviewResponse.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.wooriverygood.api.review.dto; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class NewReviewResponse { - private final Long review_id; - private final String review_title; - private final String instructor_name; - private final String taken_semyr; - private final String review_content; - private final String grade; - private final String author_email; - - - @Builder - public NewReviewResponse(Long review_id, String review_title, String instructor_name, String taken_semyr, String review_content, String grade, String author_email) { - this.review_id = review_id; - this.review_title = review_title; - this.instructor_name = instructor_name; - this.taken_semyr = taken_semyr; - this.review_content = review_content; - this.grade = grade; - this.author_email = author_email; - } -} diff --git a/src/main/java/org/wooriverygood/api/review/dto/ReviewDeleteResponse.java b/src/main/java/org/wooriverygood/api/review/dto/ReviewDeleteResponse.java deleted file mode 100644 index db9ffe4..0000000 --- a/src/main/java/org/wooriverygood/api/review/dto/ReviewDeleteResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.wooriverygood.api.review.dto; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class ReviewDeleteResponse { - private final Long review_id; - - @Builder - public ReviewDeleteResponse(Long review_id) { - this.review_id = review_id; - } -} diff --git a/src/main/java/org/wooriverygood/api/review/dto/ReviewLikeResponse.java b/src/main/java/org/wooriverygood/api/review/dto/ReviewLikeResponse.java index 644a8c4..9af6e66 100644 --- a/src/main/java/org/wooriverygood/api/review/dto/ReviewLikeResponse.java +++ b/src/main/java/org/wooriverygood/api/review/dto/ReviewLikeResponse.java @@ -1,16 +1,23 @@ package org.wooriverygood.api.review.dto; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Builder; import lombok.Getter; @Getter +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class ReviewLikeResponse { - private final int like_count; + + private final int likeCount; + private final boolean liked; + @Builder - public ReviewLikeResponse(int like_count, boolean liked) { - this.like_count = like_count; + public ReviewLikeResponse(int likeCount, boolean liked) { + this.likeCount = likeCount; this.liked =liked; } + } diff --git a/src/main/java/org/wooriverygood/api/review/dto/ReviewResponse.java b/src/main/java/org/wooriverygood/api/review/dto/ReviewResponse.java index 88cd695..c7dfd6e 100644 --- a/src/main/java/org/wooriverygood/api/review/dto/ReviewResponse.java +++ b/src/main/java/org/wooriverygood/api/review/dto/ReviewResponse.java @@ -1,5 +1,7 @@ package org.wooriverygood.api.review.dto; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Builder; import lombok.Getter; import org.wooriverygood.api.review.domain.Review; @@ -7,47 +9,48 @@ import java.time.LocalDateTime; @Getter +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class ReviewResponse { - private final Long review_id; - private final Long course_id; - private final String review_content; - private final String review_title; - private final String instructor_name; - private final String taken_semyr; + private final Long reviewId; + private final Long courseId; + private final String reviewContent; + private final String reviewTitle; + private final String instructorName; + private final String takenSemyr; private final String grade; - private final int like_count; - private final LocalDateTime review_time; + private final int likeCount; + private final LocalDateTime reviewTime; private final boolean isMine; private final boolean liked; private final boolean updated; @Builder - public ReviewResponse(Long review_id, Long course_id, String review_content, String review_title, String instructor_name, String taken_semyr, String grade, int like_count, LocalDateTime review_time, boolean isMine, boolean liked, boolean updated) { - this.review_id = review_id; - this.course_id = course_id; - this.review_content = review_content; - this.review_title = review_title; - this.instructor_name = instructor_name; - this.taken_semyr = taken_semyr; + public ReviewResponse(Long reviewId, Long courseId, String reviewContent, String reviewTitle, String instructorName, String takenSemyr, String grade, int likeCount, LocalDateTime reviewTime, boolean isMine, boolean liked, boolean updated) { + this.reviewId = reviewId; + this.courseId = courseId; + this.reviewContent = reviewContent; + this.reviewTitle = reviewTitle; + this.instructorName = instructorName; + this.takenSemyr = takenSemyr; this.grade = grade; - this.like_count = like_count; - this.review_time = review_time; + this.likeCount = likeCount; + this.reviewTime = reviewTime; this.isMine = isMine; this.liked = liked; this.updated = updated; } - public static ReviewResponse from(Review review, boolean isMine, boolean liked) { + public static ReviewResponse of(Review review, boolean isMine, boolean liked) { return ReviewResponse.builder() - .review_id(review.getId()) - .course_id(review.getCourse().getId()) - .review_content(review.getReviewContent()) - .review_title(review.getReviewTitle()) - .instructor_name(review.getInstructorName()) - .taken_semyr(review.getTakenSemyr()) + .reviewId(review.getId()) + .courseId(review.getCourse().getId()) + .reviewContent(review.getReviewContent()) + .reviewTitle(review.getReviewTitle()) + .instructorName(review.getInstructorName()) + .takenSemyr(review.getTakenSemyr()) .grade(review.getGrade()) - .like_count(review.getLikeCount()) - .review_time(review.getCreatedAt()) + .likeCount(review.getLikeCount()) + .reviewTime(review.getCreatedAt()) .isMine(isMine) .liked(liked) .updated(review.isUpdated()) diff --git a/src/main/java/org/wooriverygood/api/review/dto/ReviewUpdateRequest.java b/src/main/java/org/wooriverygood/api/review/dto/ReviewUpdateRequest.java index 7590046..8d2de4a 100644 --- a/src/main/java/org/wooriverygood/api/review/dto/ReviewUpdateRequest.java +++ b/src/main/java/org/wooriverygood/api/review/dto/ReviewUpdateRequest.java @@ -1,26 +1,35 @@ package org.wooriverygood.api.review.dto; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; import jakarta.validation.constraints.NotBlank; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; @Getter -@NoArgsConstructor +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class ReviewUpdateRequest { + @NotBlank(message = "제목이 없습니다.") - private String review_title; - private String instructor_name; - private String taken_semyr; - private String review_content; + private String reviewTitle; + + private String instructorName; + + private String takenSemyr; + + private String reviewContent; + private String grade; + @Builder - public ReviewUpdateRequest(String review_title, String instructor_name, String taken_semyr, String review_content, String grade) { - this.review_title = review_title; - this.instructor_name = instructor_name; - this.taken_semyr = taken_semyr; - this.review_content = review_content; + public ReviewUpdateRequest(String reviewTitle, String instructorName, String takenSemyr, + String reviewContent, String grade) { + this.reviewTitle = reviewTitle; + this.instructorName = instructorName; + this.takenSemyr = takenSemyr; + this.reviewContent = reviewContent; this.grade = grade; } + } diff --git a/src/main/java/org/wooriverygood/api/review/dto/ReviewUpdateResponse.java b/src/main/java/org/wooriverygood/api/review/dto/ReviewUpdateResponse.java deleted file mode 100644 index fb0399c..0000000 --- a/src/main/java/org/wooriverygood/api/review/dto/ReviewUpdateResponse.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.wooriverygood.api.review.dto; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class ReviewUpdateResponse { - - private final Long review_id; - - @Builder - public ReviewUpdateResponse(Long review_id) { - this.review_id = review_id; - } -} diff --git a/src/main/java/org/wooriverygood/api/review/dto/ReviewsResponse.java b/src/main/java/org/wooriverygood/api/review/dto/ReviewsResponse.java new file mode 100644 index 0000000..fa04cae --- /dev/null +++ b/src/main/java/org/wooriverygood/api/review/dto/ReviewsResponse.java @@ -0,0 +1,17 @@ +package org.wooriverygood.api.review.dto; + +import lombok.Getter; + +import java.util.List; + +@Getter +public class ReviewsResponse { + + private final List reviews; + + + public ReviewsResponse(List reviews) { + this.reviews = reviews; + } + +} diff --git a/src/main/java/org/wooriverygood/api/advice/exception/ReviewAccessDeniedException.java b/src/main/java/org/wooriverygood/api/review/exception/ReviewAccessDeniedException.java similarity index 67% rename from src/main/java/org/wooriverygood/api/advice/exception/ReviewAccessDeniedException.java rename to src/main/java/org/wooriverygood/api/review/exception/ReviewAccessDeniedException.java index 9e09222..288c210 100644 --- a/src/main/java/org/wooriverygood/api/advice/exception/ReviewAccessDeniedException.java +++ b/src/main/java/org/wooriverygood/api/review/exception/ReviewAccessDeniedException.java @@ -1,6 +1,6 @@ -package org.wooriverygood.api.advice.exception; +package org.wooriverygood.api.review.exception; -import org.wooriverygood.api.advice.exception.general.BadRequestException; +import org.wooriverygood.api.global.error.exception.BadRequestException; public class ReviewAccessDeniedException extends BadRequestException { diff --git a/src/main/java/org/wooriverygood/api/advice/exception/general/ReviewNotFoundException.java b/src/main/java/org/wooriverygood/api/review/exception/ReviewNotFoundException.java similarity index 64% rename from src/main/java/org/wooriverygood/api/advice/exception/general/ReviewNotFoundException.java rename to src/main/java/org/wooriverygood/api/review/exception/ReviewNotFoundException.java index 598d5af..2ae8736 100644 --- a/src/main/java/org/wooriverygood/api/advice/exception/general/ReviewNotFoundException.java +++ b/src/main/java/org/wooriverygood/api/review/exception/ReviewNotFoundException.java @@ -1,4 +1,6 @@ -package org.wooriverygood.api.advice.exception.general; +package org.wooriverygood.api.review.exception; + +import org.wooriverygood.api.global.error.exception.NotFoundException; public class ReviewNotFoundException extends NotFoundException { private static final String MESSAGE = "댓글을 찾을 수 없습니다."; diff --git a/src/main/java/org/wooriverygood/api/review/repository/ReviewLikeRepository.java b/src/main/java/org/wooriverygood/api/review/repository/ReviewLikeRepository.java index da7077e..f9f5817 100644 --- a/src/main/java/org/wooriverygood/api/review/repository/ReviewLikeRepository.java +++ b/src/main/java/org/wooriverygood/api/review/repository/ReviewLikeRepository.java @@ -1,6 +1,7 @@ package org.wooriverygood.api.review.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.wooriverygood.api.member.domain.Member; import org.wooriverygood.api.review.domain.Review; import org.wooriverygood.api.review.domain.ReviewLike; @@ -8,7 +9,7 @@ import java.util.Optional; public interface ReviewLikeRepository extends JpaRepository { - Optional findByReviewAndUsername(Review review, String username); - boolean existsByReviewAndUsername(Review review, String username); + Optional findByReviewAndMember(Review review, Member member); + boolean existsByReviewAndMember(Review review, Member member); void deleteAllByReview(Review review); } diff --git a/src/main/java/org/wooriverygood/api/review/repository/ReviewRepository.java b/src/main/java/org/wooriverygood/api/review/repository/ReviewRepository.java index 99b5bd7..b2957a1 100644 --- a/src/main/java/org/wooriverygood/api/review/repository/ReviewRepository.java +++ b/src/main/java/org/wooriverygood/api/review/repository/ReviewRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.transaction.annotation.Transactional; +import org.wooriverygood.api.member.domain.Member; import org.wooriverygood.api.review.domain.Review; import java.util.List; @@ -13,9 +14,9 @@ public interface ReviewRepository extends JpaRepository { List findAllByCourseId(Long courseId); - List findByAuthorEmail(String author); + Optional findTopByMemberOrderByCreatedAtDesc(Member member); - Optional findTopByAuthorEmailOrderByCreatedAtDesc(String author); + void deleteAllInBatch(); @Transactional @Modifying(clearAutomatically = true) @@ -27,13 +28,5 @@ public interface ReviewRepository extends JpaRepository { @Query(value = "UPDATE reviews SET like_count = like_count - 1 WHERE review_id = :reviewId", nativeQuery = true) void decreaseLikeCount(Long reviewId); - @Transactional - @Modifying(clearAutomatically = true) - @Query(value = "UPDATE courses SET review_count = review_count + 1 WHERE course_id = :courseId", nativeQuery = true) - void increaseReviewCount(Long courseId); - @Transactional - @Modifying(clearAutomatically = true) - @Query(value = "UPDATE courses SET review_count = review_count - 1 WHERE course_id = :courseId", nativeQuery = true) - void decreaseReviewCount(Long courseId); } diff --git a/src/main/java/org/wooriverygood/api/review/service/ReviewService.java b/src/main/java/org/wooriverygood/api/review/service/ReviewService.java deleted file mode 100644 index 7fc1eb4..0000000 --- a/src/main/java/org/wooriverygood/api/review/service/ReviewService.java +++ /dev/null @@ -1,160 +0,0 @@ -package org.wooriverygood.api.review.service; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.wooriverygood.api.advice.exception.CourseNotFoundException; -import org.wooriverygood.api.advice.exception.general.ReviewNotFoundException; -import org.wooriverygood.api.course.domain.Courses; -import org.wooriverygood.api.course.repository.CourseRepository; -import org.wooriverygood.api.review.domain.Review; -import org.wooriverygood.api.review.domain.ReviewLike; -import org.wooriverygood.api.review.dto.*; -import org.wooriverygood.api.review.repository.ReviewLikeRepository; -import org.wooriverygood.api.review.repository.ReviewRepository; -import org.wooriverygood.api.support.AuthInfo; - -import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; -import java.util.List; -import java.util.Optional; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class ReviewService { - private final ReviewRepository reviewRepository; - private final CourseRepository courseRepository; - private final ReviewLikeRepository reviewLikeRepository; - - public List findAllReviewsByCourseId(Long courseId, AuthInfo authInfo) { - Courses course = courseRepository.findById(courseId) - .orElseThrow(CourseNotFoundException::new); - List reviews = reviewRepository.findAllByCourseId(courseId); - - if(authInfo.getUsername() == null) { - return reviews.stream() - .map(review -> ReviewResponse.from(review, false, reviewLikeRepository.existsByReviewAndUsername(review, authInfo.getUsername()))) - .toList(); - } - return reviews.stream() - .map(review -> ReviewResponse.from(review, review.isSameAuthor(authInfo.getUsername()), reviewLikeRepository.existsByReviewAndUsername(review, authInfo.getUsername()))) - .toList(); - } - - @Transactional - public NewReviewResponse addReview(AuthInfo authInfo, Long courseId, NewReviewRequest newReviewRequest) { - Courses course = courseRepository.findById(courseId) - .orElseThrow(CourseNotFoundException::new); - Review review = Review.builder() - .reviewTitle(newReviewRequest.getReview_title()) - .course(course) - .instructorName(newReviewRequest.getInstructor_name()) - .takenSemyr(newReviewRequest.getTaken_semyr()) - .reviewContent(newReviewRequest.getReview_content()) - .grade(newReviewRequest.getGrade()) - .authorEmail(authInfo.getUsername()) - .build(); - Review saved = reviewRepository.save(review); - reviewRepository.increaseReviewCount(review.getCourse().getId()); - return createResponse(saved); - } - - @Transactional - public ReviewLikeResponse likeReview(Long reviewId, AuthInfo authInfo) { - Review review = reviewRepository.findById(reviewId) - .orElseThrow(ReviewNotFoundException::new); - - Optional reviewLike = reviewLikeRepository.findByReviewAndUsername(review, authInfo.getUsername()); - - if(reviewLike.isEmpty()) { - addReviewLike(review, authInfo.getUsername()); - return createReviewLikeResponse(review, true); - } - - deleteReviewLike(review, reviewLike.get()); - return createReviewLikeResponse(review, false); - - } - - private void addReviewLike(Review review, String username) { - ReviewLike reviewLike = ReviewLike.builder() - .review(review) - .username(username) - .build(); - - review.addReviewLike(reviewLike); - reviewLikeRepository.save(reviewLike); - reviewRepository.increaseLikeCount(review.getId()); - } - - private void deleteReviewLike(Review review, ReviewLike reviewLike) { - review.deleteReviewLike(reviewLike); - reviewRepository.decreaseLikeCount(review.getId()); - } - - public List findMyReviews(AuthInfo authInfo) { - List reviews= reviewRepository.findByAuthorEmail(authInfo.getUsername()); - return reviews.stream().map(review -> ReviewResponse.from(review, true, reviewLikeRepository.existsByReviewAndUsername(review, authInfo.getUsername()))).toList(); - } - - - private NewReviewResponse createResponse(Review review) { - return NewReviewResponse.builder() - .review_id(review.getId()) - .review_title(review.getReviewTitle()) - .instructor_name(review.getInstructorName()) - .taken_semyr(review.getTakenSemyr()) - .review_content(review.getReviewContent()) - .grade(review.getGrade()) - .author_email(review.getAuthorEmail()) - .build(); - } - - private ReviewLikeResponse createReviewLikeResponse(Review review, boolean liked) { - int likeCount = review.getLikeCount() + (liked ? 1 : -1); - return ReviewLikeResponse.builder() - .like_count(likeCount) - .liked(liked) - .build(); - } - - - @Transactional - public ReviewUpdateResponse updateReview(Long reviewId, ReviewUpdateRequest reviewUpdateRequest, AuthInfo authInfo) { - Review review = reviewRepository.findById(reviewId) - .orElseThrow(ReviewNotFoundException::new); -// review.validateAuthor(authInfo.getUsername()); - - review.updateReview(reviewUpdateRequest.getReview_title(), reviewUpdateRequest.getInstructor_name(), reviewUpdateRequest.getTaken_semyr(), reviewUpdateRequest.getReview_content(), reviewUpdateRequest.getGrade(), authInfo.getUsername()); - - return ReviewUpdateResponse.builder() - .review_id(review.getId()) - .build(); - } - - @Transactional - public ReviewDeleteResponse deleteReview(Long reviewId, AuthInfo authInfo) { - Review review = reviewRepository.findById(reviewId) - .orElseThrow(ReviewNotFoundException::new); - review.validateAuthor(authInfo.getUsername()); - - reviewLikeRepository.deleteAllByReview(review); - reviewRepository.delete(review); - reviewRepository.decreaseReviewCount(review.getCourse().getId()); - - return ReviewDeleteResponse.builder() - .review_id(reviewId) - .build(); - } - - public boolean canAccessReviews(AuthInfo authInfo) { - Optional review = reviewRepository.findTopByAuthorEmailOrderByCreatedAtDesc(authInfo.getUsername()); - if (review.isEmpty()) { - return false; - } - LocalDateTime now = LocalDateTime.now(); - long distance = ChronoUnit.MONTHS.between(review.get().getCreatedAt(), now); - return distance <= 6; - } -} diff --git a/src/test/java/org/wooriverygood/api/comment/controller/CommentControllerTest.java b/src/test/java/org/wooriverygood/api/comment/api/CommentApiTest.java similarity index 70% rename from src/test/java/org/wooriverygood/api/comment/controller/CommentControllerTest.java rename to src/test/java/org/wooriverygood/api/comment/api/CommentApiTest.java index e34c000..b364b5b 100644 --- a/src/test/java/org/wooriverygood/api/comment/controller/CommentControllerTest.java +++ b/src/test/java/org/wooriverygood/api/comment/api/CommentApiTest.java @@ -1,67 +1,67 @@ -package org.wooriverygood.api.comment.controller; +package org.wooriverygood.api.comment.api; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.wooriverygood.api.advice.exception.AuthorizationException; -import org.wooriverygood.api.advice.exception.ReplyDepthException; +import org.wooriverygood.api.global.error.exception.AuthorizationException; +import org.wooriverygood.api.comment.exception.ReplyDepthException; import org.wooriverygood.api.comment.dto.*; +import org.wooriverygood.api.member.domain.Member; import org.wooriverygood.api.post.domain.Post; import org.wooriverygood.api.post.domain.PostCategory; -import org.wooriverygood.api.support.AuthInfo; -import org.wooriverygood.api.util.ControllerTest; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.util.ApiTest; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.*; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +class CommentApiTest extends ApiTest { -class CommentControllerTest extends ControllerTest { + private List responses = new ArrayList<>(); - List responses = new ArrayList<>(); - - Post post = Post.builder() + private Post post = Post.builder() .id(1L) .category(PostCategory.OFFER) .title("title6") .content("content6") - .author("user-3333") + .member(new Member(1L, "username")) .comments(new ArrayList<>()) .postLikes(new ArrayList<>()) .build(); @BeforeEach void setUp() { - for (int i = 1; i <= 4; i++) { + for (long i = 1; i <= 4; i++) { responses.add(CommentResponse.builder() - .comment_id((long) i) - .comment_content("content" + i) - .comment_author("user-"+(i % 5)) - .post_id(post.getId()) - .comment_likes(i + 8) - .comment_time(LocalDateTime.now()) + .commentId(i) + .commentContent("content" + i) + .postId(post.getId()) + .commentLikeCount((int) i + 8) + .commentTime(LocalDateTime.now()) .liked(i % 3 == 0) .replies(new ArrayList<>()) .updated(i % 2 == 0) .reported(false) + .memberId(1L) + .isMine(true) .build()); } for (int i = 12; i <= 15; i++) { responses.get(2).getReplies() .add(ReplyResponse.builder() - .reply_id((long) i) - .reply_content("reply content " + i) - .reply_author("user-" + (i % 2)) - .reply_likes(i - 6) - .reply_time(LocalDateTime.now()) + .replyId((long) i) + .replyContent("reply content " + i) + .memberId(1L) + .isMine(true) + .replyLikeCount(i - 6) + .replyTime(LocalDateTime.now()) .liked(false) .updated(i % 2 == 0) .reported(false) @@ -72,13 +72,13 @@ void setUp() { @Test @DisplayName("특정 게시글의 댓글 조회 요청을 받으면 댓글들을 반환한다.") void findAllCommentsByPostId() { - Mockito.when(commentService.findAllComments(any(Long.class), any(AuthInfo.class))) - .thenReturn(responses); + when(commentFindService.findAllCommentsByPostId(anyLong(), any(AuthInfo.class))) + .thenReturn(new CommentsResponse(responses)); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") - .when().get("/community/1/comments") + .when().get("/posts/1/comments") .then().log().all() .assertThat() .apply(document("comments/find/success")) @@ -92,20 +92,11 @@ void addComment() { .content("content51") .build(); - NewCommentResponse response = NewCommentResponse.builder() - .comment_id(51L) - .content("content51") - .author(testAuthInfo.getUsername()) - .build(); - - Mockito.when(commentService.addComment(any(AuthInfo.class), any(Long.class), any(NewCommentRequest.class))) - .thenReturn(response); - restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") .body(request) - .when().post("/community/51/comments") + .when().post("/posts/51/comments") .then().log().all() .assertThat() .apply(document("comments/create/success")) @@ -115,9 +106,9 @@ void addComment() { @Test @DisplayName("특정 댓글의 좋아요를 1 올리거나 내린다.") void likeComment() { - Mockito.when(commentService.likeComment(any(Long.class), any(AuthInfo.class))) + when(commentLikeToggleService.likeComment(any(Long.class), any(AuthInfo.class))) .thenReturn(CommentLikeResponse.builder() - .like_count(5) + .likeCount(5) .liked(false) .build()); @@ -138,11 +129,6 @@ void updateComment() { .content("new comment content") .build(); - Mockito.when(commentService.updateComment(any(Long.class), any(CommentUpdateRequest.class), any(AuthInfo.class))) - .thenReturn(CommentUpdateResponse.builder() - .comment_id(2L) - .build()); - restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") @@ -151,7 +137,7 @@ void updateComment() { .then().log().all() .assertThat() .apply(document("comments/update/success")) - .statusCode(HttpStatus.OK.value()); + .statusCode(HttpStatus.NO_CONTENT.value()); } @Test @@ -178,8 +164,9 @@ void updateComment_exception_noAuth() { .content("new comment content") .build(); - Mockito.when(commentService.updateComment(any(Long.class), any(CommentUpdateRequest.class), any(AuthInfo.class))) - .thenThrow(new AuthorizationException()); + doThrow(new AuthorizationException()) + .when(commentUpdateService) + .updateComment(anyLong(), any(CommentUpdateRequest.class), any(AuthInfo.class)); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) @@ -195,11 +182,6 @@ void updateComment_exception_noAuth() { @Test @DisplayName("권한이 있는 댓글을 삭제한다.") void deleteComment() { - Mockito.when(commentService.deleteComment(any(Long.class), any(AuthInfo.class))) - .thenReturn(CommentDeleteResponse.builder() - .comment_id(3L) - .build()); - restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") @@ -207,14 +189,15 @@ void deleteComment() { .then().log().all() .assertThat() .apply(document("comments/delete/success")) - .statusCode(HttpStatus.OK.value()); + .statusCode(HttpStatus.NO_CONTENT.value()); } @Test @DisplayName("권한이 없는 댓글을 삭제하면 404를 반환한다.") void deleteComment_exception_noAuth() { - Mockito.when(commentService.deleteComment(any(Long.class), any(AuthInfo.class))) - .thenThrow(new AuthorizationException()); + doThrow(new AuthorizationException()) + .when(commentDeleteService) + .deleteComment(anyLong(), any(AuthInfo.class)); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) @@ -233,9 +216,6 @@ void addReply() { .content("reply content") .build(); - doNothing().when(commentService) - .addReply(any(Long.class), any(NewReplyRequest.class), any(AuthInfo.class)); - restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") @@ -248,14 +228,15 @@ void addReply() { } @Test - @DisplayName("특정 댓글의 대댓글을 작성한다.") + @DisplayName("특정 대댓글의 대댓글을 작성하려고 하면, 400 에러를 반환한다.") void addReply_exception_depth() { NewReplyRequest request = NewReplyRequest.builder() .content("reply content") .build(); - doThrow(new ReplyDepthException()).when(commentService) - .addReply(any(Long.class), any(NewReplyRequest.class), any(AuthInfo.class)); + doThrow(new ReplyDepthException()) + .when(commentCreateService) + .addReply(anyLong(), any(NewReplyRequest.class), any(AuthInfo.class)); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) diff --git a/src/test/java/org/wooriverygood/api/comment/application/CommentCreateServiceTest.java b/src/test/java/org/wooriverygood/api/comment/application/CommentCreateServiceTest.java new file mode 100644 index 0000000..8826a3b --- /dev/null +++ b/src/test/java/org/wooriverygood/api/comment/application/CommentCreateServiceTest.java @@ -0,0 +1,89 @@ +package org.wooriverygood.api.comment.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.comment.exception.ReplyDepthException; +import org.wooriverygood.api.comment.domain.Comment; +import org.wooriverygood.api.comment.dto.NewCommentRequest; +import org.wooriverygood.api.comment.dto.NewReplyRequest; +import org.wooriverygood.api.comment.repository.CommentRepository; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.post.repository.PostRepository; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class CommentCreateServiceTest extends CommentServiceTest { + + @InjectMocks + private CommentCreateService commentCreateService; + + @Mock + private PostRepository postRepository; + + @Mock + private CommentRepository commentRepository; + + @Mock + private MemberRepository memberRepository; + + + @Test + @DisplayName("특정 게시글의 댓글을 작성한다.") + void addComment() { + NewCommentRequest request = NewCommentRequest.builder() + .content("comment content") + .build(); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(postRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(post)); + + commentCreateService.addComment(authInfo, post.getId(), request); + + verify(commentRepository).save(any(Comment.class)); + } + + @Test + @DisplayName("특정 댓글의 대댓글을 작성한다.") + void addReply() { + NewReplyRequest request = NewReplyRequest.builder() + .content("reply content") + .build(); + when(commentRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(commentWithoutReply)); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + + commentCreateService.addReply(commentWithoutReply.getId(), request, authInfo); + + assertAll( + () -> assertThat(commentWithoutReply.getReplies().size()).isEqualTo(1), + () -> assertThat(commentWithoutReply.getReplies().get(0).getContent()).isEqualTo(request.getContent()), + () -> assertThat(commentWithoutReply.getReplies().get(0).getParent()).isEqualTo(commentWithoutReply) + ); + } + + @Test + @DisplayName("대댓글의 대댓글을 작성할 수 없다.") + void addReply_exception_depth() { + NewReplyRequest request = NewReplyRequest.builder() + .content("reply content") + .build(); + when(commentRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(reply)); + + assertThatThrownBy(() -> commentCreateService.addReply(reply.getId(), request, authInfo)) + .isInstanceOf(ReplyDepthException.class); + } + +} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/comment/application/CommentDeleteServiceTest.java b/src/test/java/org/wooriverygood/api/comment/application/CommentDeleteServiceTest.java new file mode 100644 index 0000000..e7075a1 --- /dev/null +++ b/src/test/java/org/wooriverygood/api/comment/application/CommentDeleteServiceTest.java @@ -0,0 +1,126 @@ +package org.wooriverygood.api.comment.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.comment.repository.CommentLikeRepository; +import org.wooriverygood.api.comment.repository.CommentRepository; +import org.wooriverygood.api.global.error.exception.AuthorizationException; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.repository.MemberRepository; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; + +class CommentDeleteServiceTest extends CommentServiceTest { + + @InjectMocks + private CommentDeleteService commentDeleteService; + + @Mock + private CommentRepository commentRepository; + + @Mock + private CommentLikeRepository commentLikeRepository; + + @Mock + private MemberRepository memberRepository; + + + @Test + @DisplayName("권한이 있는 댓글을 삭제한다.") + void deleteComment() { + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(commentRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(comment)); + comment.deleteReply(reply); + + commentDeleteService.deleteComment(comment.getId(), authInfo); + + assertAll( + () -> verify(commentLikeRepository).deleteAllByComment(comment), + () -> verify(commentRepository).delete(comment) + ); + } + + @Test + @DisplayName("권한이 있는 대댓글을 삭제한다.") + void deleteReply() { + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(commentRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(reply)); + + commentDeleteService.deleteComment(reply.getId(), authInfo); + + assertAll( + () -> verify(commentLikeRepository).deleteAllByComment(reply), + () -> verify(commentRepository).delete(reply) + ); + } + + @Test + @DisplayName("권한이 없는 대댓글을 삭제할 수 없다.") + void deleteReply_exception_noAuth() { + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.of(new Member(5L, "username"))); + when(commentRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(reply)); + + assertThatThrownBy(() -> commentDeleteService.deleteComment(reply.getId(), authInfo)) + .isInstanceOf(AuthorizationException.class); + } + + @Test + @DisplayName("부모 댓글을 삭제해도 대댓글은 남아있다.") + void deleteComment_keepChildren() { + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(commentRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(comment)); + + commentDeleteService.deleteComment(comment.getId(), authInfo); + + assertAll( + () -> assertThat(comment.isSoftRemoved()).isEqualTo(true), + () -> assertThat(comment.canDelete()).isEqualTo(false) + ); + } + + @Test + @DisplayName("특정 대댓글 삭제 후, 삭제 예정으로 처리되고 대댓글이 없는 부모 댓글을 삭제한다.") + void deletePrentAndReply() { + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(commentRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(reply)); + comment.willBeDeleted(); + + commentDeleteService.deleteComment(reply.getId(), authInfo); + + assertAll( + () -> verify(commentRepository).delete(reply), + () -> verify(commentRepository).delete(comment) + ); + } + + @Test + @DisplayName("권한이 없는 댓글은 삭제할 수 없다") + void deleteComment_exception_noAuth() { + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.of(new Member(5L, "username"))); + when(commentRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(comment)); + + assertThatThrownBy(() -> commentDeleteService.deleteComment(comment.getId(), authInfo)) + .isInstanceOf(AuthorizationException.class); + } + +} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/comment/application/CommentFindServiceTest.java b/src/test/java/org/wooriverygood/api/comment/application/CommentFindServiceTest.java new file mode 100644 index 0000000..2fe1f11 --- /dev/null +++ b/src/test/java/org/wooriverygood/api/comment/application/CommentFindServiceTest.java @@ -0,0 +1,122 @@ +package org.wooriverygood.api.comment.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.comment.domain.Comment; +import org.wooriverygood.api.comment.dto.CommentsResponse; +import org.wooriverygood.api.comment.repository.CommentLikeRepository; +import org.wooriverygood.api.comment.repository.CommentRepository; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.repository.MemberRepository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +class CommentFindServiceTest extends CommentServiceTest { + + @InjectMocks + private CommentFindService commentFindService; + + @Mock + private CommentRepository commentRepository; + + @Mock + private CommentLikeRepository commentLikeRepository; + + @Mock + private MemberRepository memberRepository; + + private final int COMMENT_COUNT = 10; + + private List comments = new ArrayList<>(); + + + @BeforeEach + void setUp() { + for (long i = 0; i < COMMENT_COUNT; i++) { + Comment comment = Comment.builder() + .id(i) + .content("comment" + i) + .post(post) + .member(member) + .build(); + comments.add(comment); + } + } + + @Test + @DisplayName("유효한 id를 통해 특정 게시글의 댓글들을 불러온다.") + void findAllCommentsByPostId() { + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(commentRepository.findAllByPostId(anyLong())) + .thenReturn(comments); + when(commentLikeRepository.existsByCommentAndMember(any(Comment.class), any(Member.class))) + .thenReturn(true); + + CommentsResponse response = commentFindService.findAllCommentsByPostId(2L, authInfo); + + assertThat(response.getComments().size()).isEqualTo(COMMENT_COUNT); + } + + @Test + @DisplayName("삭제 처리될 댓글의 내용은 비어있다.") + void findAllCommentsByPostId_softRemoved() { + comments = new ArrayList<>(); + comments.add(Comment.builder() + .id(1L) + .content("Content") + .post(post) + .member(member) + .softRemoved(true) + .build()); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(commentRepository.findAllByPostId(anyLong())) + .thenReturn(comments); + + CommentsResponse response = commentFindService.findAllCommentsByPostId(2L, authInfo); + + assertThat(response.getComments().get(0).getCommentContent()).isEqualTo(null); + } + + @Test + @DisplayName("대댓글만 불러올 수 없다.") + void findAllCommentsByPostId_cannot_only_reply() { + comments = new ArrayList<>(); + comments.add(reply); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(commentRepository.findAllByPostId(anyLong())) + .thenReturn(comments); + + CommentsResponse response = commentFindService.findAllCommentsByPostId(2L, authInfo); + + assertThat(response.getComments().size()).isEqualTo(0); + } + + @Test + @DisplayName("댓글과 대댓글들을 모두 불러온다.") + void findAllCommentsByPostId_withReplies() { + comments = new ArrayList<>(); + comments.add(comment); + comments.add(reply); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(commentRepository.findAllByPostId(anyLong())) + .thenReturn(comments); + + CommentsResponse response = commentFindService.findAllCommentsByPostId(2L, authInfo); + + assertThat(response.getComments().get(0).getReplies().size()).isEqualTo(1); + } + +} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/comment/application/CommentLikeToggleServiceTest.java b/src/test/java/org/wooriverygood/api/comment/application/CommentLikeToggleServiceTest.java new file mode 100644 index 0000000..da1da3f --- /dev/null +++ b/src/test/java/org/wooriverygood/api/comment/application/CommentLikeToggleServiceTest.java @@ -0,0 +1,78 @@ +package org.wooriverygood.api.comment.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.comment.domain.Comment; +import org.wooriverygood.api.comment.domain.CommentLike; +import org.wooriverygood.api.comment.dto.*; +import org.wooriverygood.api.comment.repository.CommentLikeRepository; +import org.wooriverygood.api.comment.repository.CommentRepository; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.repository.MemberRepository; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +class CommentLikeToggleServiceTest extends CommentServiceTest { + + @InjectMocks + private CommentLikeToggleService commentLikeToggleService; + + @Mock + private CommentRepository commentRepository; + + @Mock + private CommentLikeRepository commentLikeRepository; + + @Mock + private MemberRepository memberRepository; + + + @Test + @DisplayName("특정 댓글의 좋아요를 1 올린다.") + void likeComment_up() { + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(commentRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(comment)); + when(commentLikeRepository.findByCommentAndMember(any(Comment.class), any(Member.class))) + .thenReturn(Optional.empty()); + + CommentLikeResponse response = commentLikeToggleService.likeComment(comment.getId(), authInfo); + + assertAll( + () -> assertThat(response.getLikeCount()).isEqualTo(comment.getLikeCount() + 1), + () -> assertThat(response.isLiked()).isEqualTo(true) + ); + } + + @Test + @DisplayName("특정 댓글의 좋아요를 1 내린다.") + void likeComment_down() { + CommentLike commentLike = CommentLike.builder() + .id(1L) + .comment(comment) + .member(member) + .build(); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(commentRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(comment)); + when(commentLikeRepository.findByCommentAndMember(any(Comment.class), any(Member.class))) + .thenReturn(Optional.ofNullable(commentLike)); + + CommentLikeResponse response = commentLikeToggleService.likeComment(comment.getId(), authInfo); + + assertAll( + () -> assertThat(response.getLikeCount()).isEqualTo(comment.getLikeCount() - 1), + () -> assertThat(response.isLiked()).isEqualTo(false) + ); + } + +} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/comment/application/CommentServiceTest.java b/src/test/java/org/wooriverygood/api/comment/application/CommentServiceTest.java new file mode 100644 index 0000000..b436d21 --- /dev/null +++ b/src/test/java/org/wooriverygood/api/comment/application/CommentServiceTest.java @@ -0,0 +1,57 @@ +package org.wooriverygood.api.comment.application; + +import org.junit.jupiter.api.BeforeEach; +import org.wooriverygood.api.comment.domain.Comment; +import org.wooriverygood.api.post.domain.Post; +import org.wooriverygood.api.post.domain.PostCategory; +import org.wooriverygood.api.util.MockTest; + +import java.util.ArrayList; + +public class CommentServiceTest extends MockTest { + + protected Post post; + + protected Comment comment; + + protected Comment commentWithoutReply; + + protected Comment reply; + + + @BeforeEach + void dateSetUp() { + post = Post.builder() + .id(1L) + .category(PostCategory.OFFER) + .title("simple title") + .content("simple content") + .member(member) + .comments(new ArrayList<>()) + .postLikes(new ArrayList<>()) + .build(); + comment = Comment.builder() + .id(1L) + .content("content") + .post(post) + .member(member) + .commentLikes(new ArrayList<>()) + .reports(new ArrayList<>()) + .build(); + reply = Comment.builder() + .id(2L) + .content("content") + .post(post) + .parent(comment) + .member(member) + .build(); + comment.addReply(reply); + commentWithoutReply = Comment.builder() + .id(3L) + .content("content") + .post(post) + .member(member) + .build(); + } + +} diff --git a/src/test/java/org/wooriverygood/api/comment/application/CommentUpdateServiceTest.java b/src/test/java/org/wooriverygood/api/comment/application/CommentUpdateServiceTest.java new file mode 100644 index 0000000..3baea56 --- /dev/null +++ b/src/test/java/org/wooriverygood/api/comment/application/CommentUpdateServiceTest.java @@ -0,0 +1,67 @@ +package org.wooriverygood.api.comment.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.comment.dto.CommentUpdateRequest; +import org.wooriverygood.api.comment.repository.CommentRepository; +import org.wooriverygood.api.global.error.exception.AuthorizationException; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.repository.MemberRepository; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +class CommentUpdateServiceTest extends CommentServiceTest { + + @InjectMocks + private CommentUpdateService commentUpdateService; + + @Mock + private CommentRepository commentRepository; + + @Mock + private MemberRepository memberRepository; + + + @Test + @DisplayName("권한이 있는 댓글을 수정한다.") + void updateComment() { + CommentUpdateRequest request = CommentUpdateRequest.builder() + .content("new comment content") + .build(); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(commentRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(comment)); + + commentUpdateService.updateComment(comment.getId(), request, authInfo); + + assertAll( + () -> assertThat(comment.isUpdated()).isEqualTo(true), + () -> assertThat(comment.getContent()).isEqualTo(request.getContent()) + ); + } + + @Test + @DisplayName("권한이 없는 댓글을 수정할 수 없다.") + void updateComment_exception_noAuth() { + CommentUpdateRequest request = CommentUpdateRequest.builder() + .content("new comment content") + .build(); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(new Member(5L, "username"))); + when(commentRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(comment)); + + assertThatThrownBy(() -> commentUpdateService.updateComment(comment.getId(), request, authInfo)) + .isInstanceOf(AuthorizationException.class); + } + +} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/comment/service/CommentServiceTest.java b/src/test/java/org/wooriverygood/api/comment/service/CommentServiceTest.java deleted file mode 100644 index f372ae6..0000000 --- a/src/test/java/org/wooriverygood/api/comment/service/CommentServiceTest.java +++ /dev/null @@ -1,303 +0,0 @@ -package org.wooriverygood.api.comment.service; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; -import org.wooriverygood.api.advice.exception.AuthorizationException; -import org.wooriverygood.api.advice.exception.ReplyDepthException; -import org.wooriverygood.api.comment.domain.Comment; -import org.wooriverygood.api.comment.domain.CommentLike; -import org.wooriverygood.api.comment.dto.*; -import org.wooriverygood.api.comment.repository.CommentLikeRepository; -import org.wooriverygood.api.comment.repository.CommentRepository; -import org.wooriverygood.api.post.domain.Post; -import org.wooriverygood.api.post.domain.PostCategory; -import org.wooriverygood.api.post.repository.PostRepository; -import org.wooriverygood.api.support.AuthInfo; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import static org.mockito.ArgumentMatchers.any; - -@ExtendWith(MockitoExtension.class) -class CommentServiceTest { - - @InjectMocks - private CommentService commentService; - - @Mock - private CommentRepository commentRepository; - - @Mock - private PostRepository postRepository; - - @Mock - private CommentLikeRepository commentLikeRepository; - - private final int COMMENT_COUNT = 10; - - List comments = new ArrayList<>(); - - Post singlePost = Post.builder() - .id(6L) - .category(PostCategory.OFFER) - .title("title6") - .content("content6") - .author("author6") - .comments(new ArrayList<>()) - .postLikes(new ArrayList<>()) - .build(); - - AuthInfo authInfo = AuthInfo.builder() - .sub("22222-34534-123") - .username("22222-34534-123") - .build(); - - Comment singleComment = Comment.builder() - .id(2L) - .post(singlePost) - .content("comment content") - .author(authInfo.getUsername()) - .commentLikes(new ArrayList<>()) - .build(); - - Comment reply = Comment.builder() - .id(3L) - .post(singlePost) - .content("reply content") - .author(authInfo.getUsername()) - .commentLikes(new ArrayList<>()) - .parent(singleComment) - .build(); - - - @BeforeEach - void setUpPosts() { - singleComment.getChildren().add(reply); - - for (int i = 0; i < COMMENT_COUNT; i++) { - Comment comment = Comment.builder() - .id((long) i) - .content("comment" + i) - .author("author" + i) - .post(singlePost) - .build(); - comments.add(comment); - } - } - - @Test - @DisplayName("유효한 id를 통해 특정 게시글의 댓글들을 불러온다.") - void findAllCommentsByPostId() { - Mockito.when(commentRepository.findAllByPostId(any())) - .thenReturn(comments); - - List responses = commentService.findAllComments(2L, authInfo); - - Assertions.assertThat(responses.size()).isEqualTo(COMMENT_COUNT); - } - - @Test - @DisplayName("특정 게시글의 댓글을 작성한다.") - void addComment() { - NewCommentRequest newCommentRequest = NewCommentRequest.builder() - .content("comment content") - .build(); - - Mockito.when(commentRepository.save(any(Comment.class))) - .thenReturn(Comment.builder() - .author(authInfo.getUsername()) - .content(newCommentRequest.getContent()) - .build()); - - Mockito.when(postRepository.findById(any())) - .thenReturn(Optional.ofNullable(singlePost)); - - NewCommentResponse response = commentService.addComment(authInfo, 1L, newCommentRequest); - - Assertions.assertThat(response.getAuthor()).isEqualTo(authInfo.getUsername()); - Assertions.assertThat(response.getContent()).isEqualTo(newCommentRequest.getContent()); - } - - @Test - @DisplayName("특정 댓글의 좋아요를 1 올린다.") - void likeComment_up() { - Mockito.when(commentRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(singleComment)); - - CommentLikeResponse response = commentService.likeComment(singleComment.getId(), authInfo); - - Assertions.assertThat(response.getLike_count()).isEqualTo(singleComment.getLikeCount() + 1); - Assertions.assertThat(response.isLiked()).isEqualTo(true); - } - - @Test - @DisplayName("특정 댓글의 좋아요를 1 내린다.") - void likeComment_down() { - CommentLike commentLike = CommentLike.builder() - .id(2L) - .comment(singleComment) - .username(authInfo.getUsername()) - .build(); - Mockito.when(commentRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(singleComment)); - Mockito.when(commentLikeRepository.findByCommentAndUsername(any(Comment.class), any(String.class))) - .thenReturn(Optional.ofNullable(commentLike)); - - CommentLikeResponse response = commentService.likeComment(singleComment.getId(), authInfo); - - Assertions.assertThat(response.getLike_count()).isEqualTo(singleComment.getLikeCount() - 1); - Assertions.assertThat(response.isLiked()).isEqualTo(false); - } - - @Test - @DisplayName("권한이 있는 댓글을 수정한다.") - void updateComment() { - CommentUpdateRequest request = CommentUpdateRequest.builder() - .content("new comment content") - .build(); - - Mockito.when(commentRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(singleComment)); - - CommentUpdateResponse response = commentService.updateComment(singleComment.getId(), request, authInfo); - - Assertions.assertThat(response.getComment_id()).isEqualTo(singleComment.getId()); - Assertions.assertThat(singleComment.isUpdated()).isEqualTo(true); - } - - @Test - @DisplayName("권한이 없는 댓글을 수정할 수 없다.") - void updateComment_exception_noAuth() { - CommentUpdateRequest request = CommentUpdateRequest.builder() - .content("new comment content") - .build(); - AuthInfo noAuthInfo = AuthInfo.builder() - .sub("no") - .username("no") - .build(); - - Mockito.when(commentRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(singleComment)); - - Assertions.assertThatThrownBy(() -> commentService.updateComment(singleComment.getId(), request, noAuthInfo)) - .isInstanceOf(AuthorizationException.class); - } - - @Test - @DisplayName("권한이 있는 댓글을 삭제한다.") - void deleteComment() { - Mockito.when(commentRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(singleComment)); - - CommentDeleteResponse response = commentService.deleteComment(singleComment.getId(), authInfo); - - Assertions.assertThat(response.getComment_id()).isEqualTo(singleComment.getId()); - } - - @Test - @DisplayName("권한이 없는 댓글은 삭제할 수 없다") - void deleteComment_exception_noAuth() { - Mockito.when(commentRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(singleComment)); - - AuthInfo noAuthInfo = AuthInfo.builder() - .sub("no") - .username("no") - .build(); - - Assertions.assertThatThrownBy(() -> commentService.deleteComment(singleComment.getId(), noAuthInfo)) - .isInstanceOf(AuthorizationException.class); - } - - @Test - @DisplayName("특정 댓글의 대댓글을 작성한다.") - void addReply() { - NewReplyRequest request = NewReplyRequest.builder() - .content("reply content") - .build(); - - Mockito.when(commentRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(singleComment)); - - commentService.addReply(singleComment.getId(), request, authInfo); - Comment reply = singleComment.getChildren().get(0); - - Assertions.assertThat(reply.getContent()).isEqualTo(request.getContent()); - Assertions.assertThat(reply.getParent()).isEqualTo(singleComment); - } - - @Test - @DisplayName("대댓글의 대댓글을 작성할 수 없다.") - void addReply_exception_depth() { - NewReplyRequest request = NewReplyRequest.builder() - .content("reply content") - .build(); - - Mockito.when(commentRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(reply)); - - Assertions.assertThatThrownBy(() -> commentService.addReply(reply.getId(), request, authInfo)) - .isInstanceOf(ReplyDepthException.class); - } - - @Test - @DisplayName("권한이 있는 대댓글을 삭제한다.") - void deleteReply() { - Mockito.when(commentRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(reply)); - - CommentDeleteResponse response = commentService.deleteComment(reply.getId(), authInfo); - - Assertions.assertThat(response.getComment_id()).isEqualTo(reply.getId()); - Assertions.assertThat(singleComment.getChildren().size()).isEqualTo(0); - } - - @Test - @DisplayName("권한이 없는 대댓글을 삭제할 수 없다.") - void deleteReply_exception_noAuth() { - Mockito.when(commentRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(reply)); - - AuthInfo noAuthInfo = AuthInfo.builder() - .sub("no") - .username("no") - .build(); - - Assertions.assertThatThrownBy(() -> commentService.deleteComment(reply.getId(), noAuthInfo)) - .isInstanceOf(AuthorizationException.class); - } - - @Test - @DisplayName("부모 댓글을 삭제해도 대댓글은 남아있다.") - void deleteComment_keepChildren() { - Mockito.when(commentRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(singleComment)); - - commentService.deleteComment(singleComment.getId(), authInfo); - - Assertions.assertThat(singleComment.isSoftRemoved()).isEqualTo(true); - Assertions.assertThat(singleComment.getChildren().size()).isEqualTo(1); - } - - @Test - @DisplayName("특정 대댓글 삭제 후, 삭제 예정으로 처리되고 대댓글이 없는 부모 댓글을 삭제한다.") - void deletePrentAndReply() { - singleComment.willBeDeleted(); - Mockito.when(commentRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(reply)); - - commentService.deleteComment(reply.getId(), authInfo); - - Assertions.assertThat(singleComment.canDelete()).isEqualTo(true); - } - -} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/course/controller/CourseControllerTest.java b/src/test/java/org/wooriverygood/api/course/api/CourseApiTest.java similarity index 72% rename from src/test/java/org/wooriverygood/api/course/controller/CourseControllerTest.java rename to src/test/java/org/wooriverygood/api/course/api/CourseApiTest.java index 2d114cd..310b165 100644 --- a/src/test/java/org/wooriverygood/api/course/controller/CourseControllerTest.java +++ b/src/test/java/org/wooriverygood/api/course/api/CourseApiTest.java @@ -1,33 +1,35 @@ -package org.wooriverygood.api.course.controller; +package org.wooriverygood.api.course.api; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.wooriverygood.api.course.dto.CourseNameResponse; import org.wooriverygood.api.course.dto.CourseResponse; +import org.wooriverygood.api.course.dto.CoursesResponse; import org.wooriverygood.api.course.dto.NewCourseRequest; -import org.wooriverygood.api.util.ControllerTest; +import org.wooriverygood.api.util.ApiTest; import java.util.ArrayList; import java.util.List; -import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -public class CourseControllerTest extends ControllerTest { - List responses = new ArrayList<>(); +public class CourseApiTest extends ApiTest { + + private List responses = new ArrayList<>(); @BeforeEach void setUp() { - for (int i = 0; i < 2; i++) { + for (long i = 0; i < 2; i++) { responses.add(CourseResponse.builder() - .course_id((long)i) - .course_category("Zhuanye") - .course_credit(5) - .course_name("Gaoshu"+i) + .courseId(i) + .courseCategory("Zhuanye") + .courseCredit(5) + .courseName("Gaoshu"+i) .isYouguan(0) .kaikeYuanxi("Xinke") .reviewCount(0) @@ -38,7 +40,7 @@ void setUp() { @Test @DisplayName("모든 강의 조회 요청을 받으면 강의를 반환한다.") void getCourses() { - Mockito.when(courseService.findAll()).thenReturn(responses); + when(courseFindService.findAll()).thenReturn(new CoursesResponse(responses)); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) @@ -53,9 +55,9 @@ void getCourses() { @DisplayName("새로운 수업을 성공적으로 등록한다.") public void addCourse() { NewCourseRequest request = NewCourseRequest.builder() - .course_name("테스트 강의") - .course_category("전공") - .course_credit(5) + .courseName("테스트 강의") + .courseCategory("전공") + .courseCredit(5) .kaikeYuanxi("씬커") .isYouguan(0) .build(); @@ -73,11 +75,9 @@ public void addCourse() { @Test @DisplayName("특정 강의의 이름을 반환한다.") void getCourseName() { - CourseNameResponse response = CourseNameResponse.builder() - .course_name("테스트 강의") - .build(); + CourseNameResponse response = new CourseNameResponse("테스트 강의"); - Mockito.when(courseService.getCourseName(any(Long.class))) + when(courseFindService.findCourseName(anyLong())) .thenReturn(response); restDocs @@ -89,4 +89,5 @@ void getCourseName() { .apply(document("course/get/name/success")) .statusCode(HttpStatus.OK.value()); } + } diff --git a/src/test/java/org/wooriverygood/api/course/application/CourseCreateServiceTest.java b/src/test/java/org/wooriverygood/api/course/application/CourseCreateServiceTest.java new file mode 100644 index 0000000..f8c0b2e --- /dev/null +++ b/src/test/java/org/wooriverygood/api/course/application/CourseCreateServiceTest.java @@ -0,0 +1,40 @@ +package org.wooriverygood.api.course.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.course.domain.Course; +import org.wooriverygood.api.course.dto.NewCourseRequest; +import org.wooriverygood.api.course.repository.CourseRepository; +import org.wooriverygood.api.util.MockTest; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; + +public class CourseCreateServiceTest extends MockTest { + + @InjectMocks + private CourseCreateService courseCreateService; + + @Mock + private CourseRepository courseRepository; + + + @Test + @DisplayName("새로운 강의를 추가한다.") + void addCourse() { + NewCourseRequest request = NewCourseRequest.builder() + .courseName("테스트 강의") + .courseCategory("전공") + .courseCredit(5) + .kaikeYuanxi("씬커") + .isYouguan(0) + .build(); + + courseCreateService.addCourse(request); + + verify(courseRepository).save(any(Course.class)); + } + +} diff --git a/src/test/java/org/wooriverygood/api/course/application/CourseFindServiceTest.java b/src/test/java/org/wooriverygood/api/course/application/CourseFindServiceTest.java new file mode 100644 index 0000000..625fc07 --- /dev/null +++ b/src/test/java/org/wooriverygood/api/course/application/CourseFindServiceTest.java @@ -0,0 +1,81 @@ +package org.wooriverygood.api.course.application; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.course.domain.Course; +import org.wooriverygood.api.course.dto.CourseNameResponse; +import org.wooriverygood.api.course.dto.CoursesResponse; +import org.wooriverygood.api.course.repository.CourseRepository; +import org.wooriverygood.api.util.MockTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +class CourseFindServiceTest extends MockTest { + + @InjectMocks + private CourseFindService courseFindService; + + @Mock + private CourseRepository courseRepository; + + private Course course = Course.builder() + .id(3L) + .name("Gaoshu Test") + .category("Zhuanye") + .credit(5) + .isYouguan(0) + .kaikeYuanxi("Xinke") + .reviewCount(0) + .build(); + + private List courses = new ArrayList<>(); + + private final int COURSES_COUNT = 10; + + + @BeforeEach + void setup() { + for (long i = 1; i <= COURSES_COUNT; i++) { + courses.add(Course.builder() + .id(i) + .name("Course" + i) + .category("Zhuanye") + .credit(5) + .isYouguan(0) + .kaikeYuanxi("Xinke") + .reviewCount(0) + .build()); + } + } + + @Test + @DisplayName("강의 전체 조회") + void getCourses() { + when(courseRepository.findAll()).thenReturn(courses); + + CoursesResponse response = courseFindService.findAll(); + + Assertions.assertThat(response.getCourses().size()).isEqualTo(COURSES_COUNT); + } + + @Test + @DisplayName("특정 강의의 이름을 조회온다.") + void getCourseName() { + when(courseRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(course)); + + CourseNameResponse response = courseFindService.findCourseName(3L); + + Assertions.assertThat(response.getCourseName()).isEqualTo(course.getName()); + } + +} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/course/service/CourseServiceTest.java b/src/test/java/org/wooriverygood/api/course/service/CourseServiceTest.java deleted file mode 100644 index 6987151..0000000 --- a/src/test/java/org/wooriverygood/api/course/service/CourseServiceTest.java +++ /dev/null @@ -1,122 +0,0 @@ -package org.wooriverygood.api.course.service; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.transaction.annotation.Transactional; -import org.wooriverygood.api.course.domain.Courses; -import org.wooriverygood.api.course.dto.CourseNameResponse; -import org.wooriverygood.api.course.dto.CourseResponse; -import org.wooriverygood.api.course.dto.NewCourseRequest; -import org.wooriverygood.api.course.dto.NewCourseResponse; -import org.wooriverygood.api.course.repository.CourseRepository; - -import javax.swing.text.html.Option; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import static org.mockito.Mockito.when; -import static org.mockito.ArgumentMatchers.any; - -@ExtendWith(MockitoExtension.class) -public class CourseServiceTest { - @Mock - private CourseRepository courseRepository; - @InjectMocks - private CourseService courseService; - - private List testCourses = new ArrayList<>(); - - private final int COURSES_COUNT = 10; - - @BeforeEach - void setup() { - for(int i = 1; i<=COURSES_COUNT; i++) { - testCourses.add(Courses.builder() - .id((long)i) - .course_name("Gaoshu" + i) - .course_category("Zhuanye") - .course_credit(5) - .isYouguan(0) - .kaikeYuanxi("Xinke") - .reviewCount(0) - .build()); - } - } - - Courses singleCourse = Courses.builder() - .id(3L) - .course_name("Gaoshu Test") - .course_category("Zhuanye") - .course_credit(5) - .isYouguan(0) - .kaikeYuanxi("Xinke") - .reviewCount(0) - .build(); - - - @Test - @DisplayName("강의 목록 조회") - void getCourses() { - Mockito.when(courseRepository.findAll()).thenReturn(testCourses); - - List responses = courseService.findAll(); - - // Then - Assertions.assertThat(responses.size()).isEqualTo(COURSES_COUNT); - - } - - @Test - @DisplayName("새로운 강의를 추가한다.") - void addCourse() { - NewCourseRequest newCourseRequest = NewCourseRequest.builder() - .course_name("테스트 강의") - .course_category("전공") - .course_credit(5) - .kaikeYuanxi("씬커") - .isYouguan(0) - .build(); - - Mockito.when(courseRepository.save(any(Courses.class))) - .thenReturn(Courses.builder() - .course_name(newCourseRequest.getCourse_name()) - .course_category(newCourseRequest.getCourse_category()) - .course_credit(newCourseRequest.getCourse_credit()) - .kaikeYuanxi(newCourseRequest.getKaikeYuanxi()) - .isYouguan(newCourseRequest.getIsYouguan()) - .reviewCount(0) - .build()); - - NewCourseResponse response = courseService.addCourse(newCourseRequest); - - Assertions.assertThat(response.getCourse_name()).isEqualTo(newCourseRequest.getCourse_name()); - Assertions.assertThat(response.getCourse_credit()).isEqualTo(newCourseRequest.getCourse_credit()); - Assertions.assertThat(response.getCourse_category()).isEqualTo(newCourseRequest.getCourse_category()); - Assertions.assertThat(response.getKaikeYuanxi()).isEqualTo(newCourseRequest.getKaikeYuanxi()); - Assertions.assertThat(response.getIsYouguan()).isEqualTo(newCourseRequest.getIsYouguan()); - } - - @Test - @DisplayName("특정 강의의 이름을 받아온다.") - void getCourseName() { - Mockito.when(courseRepository.findById(any())) - .thenReturn(Optional.ofNullable(singleCourse)); - - CourseNameResponse response = courseService.getCourseName(3L); - - Assertions.assertThat(response.getCourse_name()).isEqualTo(singleCourse.getCourse_name()); - - } - -} diff --git a/src/test/java/org/wooriverygood/api/post/controller/PostControllerTest.java b/src/test/java/org/wooriverygood/api/post/api/PostApiTest.java similarity index 64% rename from src/test/java/org/wooriverygood/api/post/controller/PostControllerTest.java rename to src/test/java/org/wooriverygood/api/post/api/PostApiTest.java index 33d5942..a86351c 100644 --- a/src/test/java/org/wooriverygood/api/post/controller/PostControllerTest.java +++ b/src/test/java/org/wooriverygood/api/post/api/PostApiTest.java @@ -1,35 +1,38 @@ -package org.wooriverygood.api.post.controller; +package org.wooriverygood.api.post.api; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.wooriverygood.api.advice.exception.AuthorizationException; -import org.wooriverygood.api.advice.exception.PostNotFoundException; +import org.wooriverygood.api.global.error.exception.AuthorizationException; +import org.wooriverygood.api.post.exception.PostNotFoundException; import org.wooriverygood.api.post.dto.*; -import org.wooriverygood.api.support.AuthInfo; -import org.wooriverygood.api.util.ControllerTest; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.util.ApiTest; import org.wooriverygood.api.util.ResponseFixture; -import static org.mockito.ArgumentMatchers.any; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; -class PostControllerTest extends ControllerTest { +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +class PostApiTest extends ApiTest { @Test @DisplayName("전체 게시글의 1번째 페이지를 반환한다.") void findPosts() { - Mockito.when(postService.findPosts(any(AuthInfo.class), any(Pageable.class))) + when(postFindService.findPosts(any(AuthInfo.class), any(Pageable.class), anyString())) .thenReturn(ResponseFixture.postsResponse(1, 39, testAuthInfo)); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") - .when().get("/community?page=1") + .when().get("/posts?page=1") .then().log().all() .assertThat() .apply(document("post/find/all/success")) @@ -39,13 +42,35 @@ void findPosts() { @Test @DisplayName("자유 카테고리의 2번째 페이지를 반환한다.") void findPostsByCategory() { - Mockito.when(postService.findPostsByCategory(any(AuthInfo.class), any(Pageable.class), any(String.class))) - .thenReturn(ResponseFixture.postsResponse(2, 39, "자유", testAuthInfo)); + List responses = new ArrayList<>(); + int bound = Math.min(39 - 2 * 10 + 1, 10); + int start = 2 * 10 + 1; + for (long i = start; i < start + bound; i++) + responses.add(PostResponse.builder() + .postId(i) + .postTitle("title_" + i) + .postCategory("자유") + .postComments((int) (Math.random() * 100)) + .postLikes((int) (Math.random() * 100)) + .postTime(LocalDateTime.now()) + .liked(i % 6 == 0) + .updated(i % 9 == 0) + .reported(false) + .memberId(1L) + .isMine(false) + .build()); + + when(postFindService.findPosts(any(AuthInfo.class), any(Pageable.class), anyString())) + .thenReturn(PostsResponse.builder() + .posts(responses) + .totalPostCount(39) + .totalPageCount((int) Math.ceil((double) 39 / 10)) + .build()); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") - .when().get("/community/category/free") + .when().get("/posts?page=2&category=free") .then().log().all() .assertThat() .apply(document("post/find/category/success")) @@ -55,13 +80,25 @@ void findPostsByCategory() { @Test @DisplayName("특정 게시글 조회 요청을 받으면 게시글을 반환한다.") void findPost() { - Mockito.when(postService.findPostById(any(Long.class), any(AuthInfo.class))) - .thenReturn(ResponseFixture.postResponse(4L, testAuthInfo)); + when(postFindService.findPostById(anyLong(), any(AuthInfo.class))) + .thenReturn(PostDetailResponse.builder() + .postId(1L) + .postTitle("title_1") + .postCategory("자유") + .postComments((int) (Math.random() * 100)) + .postLikes((int) (Math.random() * 100)) + .postTime(LocalDateTime.now()) + .liked(false) + .updated(false) + .reported(false) + .memberId(1L) + .isMine(false) + .build()); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") - .when().get("/community/4") + .when().get("/posts/4") .then().log().all() .assertThat() .apply(document("post/find/one/success")) @@ -71,13 +108,13 @@ void findPost() { @Test @DisplayName("유효하지 않은 id로 게시글을 조회하면 404를 반환한다.") void findPost_exception_invalidId() { - Mockito.when(postService.findPostById(any(Long.class), any(AuthInfo.class))) + when(postFindService.findPostById(anyLong(), any(AuthInfo.class))) .thenThrow(new PostNotFoundException()); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") - .when().get("/community/1") + .when().get("/posts/1") .then().log().all() .assertThat() .apply(document("post/find/one/fail")) @@ -88,24 +125,16 @@ void findPost_exception_invalidId() { @DisplayName("새로운 게시글을 성공적으로 등록한다.") public void addPost() { NewPostRequest request = NewPostRequest.builder() - .post_title("title") - .post_category("자유") - .post_content("content") + .postTitle("title") + .postCategory("자유") + .postContent("content") .build(); - Mockito.when(postService.addPost(any(AuthInfo.class), any(NewPostRequest.class))) - .thenReturn(NewPostResponse.builder() - .post_id(6L) - .title(request.getPost_title()) - .category(request.getPost_category()) - .author(testAuthInfo.getUsername()) - .build()); - restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") .body(request) - .when().post("/community") + .when().post("/posts") .then().log().all() .assertThat() .apply(document("post/create/success")) @@ -116,15 +145,15 @@ public void addPost() { @DisplayName("새로운 게시글의 제목이 없으면 400을 반환한다.") public void addPost_exception_noTitle() { NewPostRequest request = NewPostRequest.builder() - .post_category("자유") - .post_content("content") + .postCategory("자유") + .postContent("content") .build(); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") .body(request) - .when().post("/community") + .when().post("/posts") .then().log().all() .assertThat() .apply(document("post/create/fail/noTitle")) @@ -134,13 +163,13 @@ public void addPost_exception_noTitle() { @Test @DisplayName("사용자 본인이 작성한 게시글을 불러온다.") void findMyPosts() { - Mockito.when(postService.findMyPosts(any(AuthInfo.class), any(PageRequest.class))) + when(postFindService.findMyPosts(any(AuthInfo.class), any(PageRequest.class))) .thenReturn(ResponseFixture.postsResponse(1, 14, testAuthInfo)); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") - .when().get("/community/me?page=1") + .when().get("/posts/me?page=1") .then().log().all() .assertThat() .apply(document("post/find/me/success")) @@ -150,16 +179,16 @@ void findMyPosts() { @Test @DisplayName("특정 게시글의 좋아요를 1 올리거나 내린다.") void likePost() { - Mockito.when(postService.likePost(any(Long.class), any(AuthInfo.class))) + when(postLikeToggleService.togglePostLike(anyLong(), any(AuthInfo.class))) .thenReturn(PostLikeResponse.builder() - .like_count(5) + .likeCount(5) .liked(true) .build()); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") - .when().put("/community/1/like") + .when().put("/posts/1/like") .then().log().all() .assertThat() .apply(document("post/like/success")) @@ -170,42 +199,38 @@ void likePost() { @DisplayName("권한이 있는 게시글을 수정한다.") void updatePost() { PostUpdateRequest request = PostUpdateRequest.builder() - .post_title("new title") - .post_content("new content") + .postTitle("new title") + .postContent("new content") .build(); - Mockito.when(postService.updatePost(any(Long.class), any(PostUpdateRequest.class), any(AuthInfo.class))) - .thenReturn(PostUpdateResponse.builder() - .post_id((long) 1) - .build()); - restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") .body(request) - .when().put("/community/1") + .when().put("/posts/1") .then().log().all() .assertThat() .apply(document("post/update/success")) - .statusCode(HttpStatus.OK.value()); + .statusCode(HttpStatus.NO_CONTENT.value()); } @Test @DisplayName("권한이 없는 게시글을 수정하면 403을 반환한다.") void updatePost_exception_noAuth() { PostUpdateRequest request = PostUpdateRequest.builder() - .post_title("new title") - .post_content("new content") + .postTitle("new title") + .postContent("new content") .build(); - Mockito.when(postService.updatePost(any(Long.class), any(PostUpdateRequest.class), any(AuthInfo.class))) - .thenThrow(new AuthorizationException()); + doThrow(new AuthorizationException()) + .when(postUpdateService) + .updatePost(anyLong(), any(PostUpdateRequest.class), any(AuthInfo.class)); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") .body(request) - .when().put("/community/1") + .when().put("/posts/1") .then().log().all() .assertThat() .apply(document("post/update/fail/noAuth")) @@ -216,14 +241,14 @@ void updatePost_exception_noAuth() { @Test void updatePost_exception_noTitle() { PostUpdateRequest postUpdateRequest = PostUpdateRequest.builder() - .post_content("content") + .postContent("content") .build(); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .body(postUpdateRequest) .header("Authorization", "any") - .when().put("/community/1") + .when().put("/posts/1") .then().log().all() .assertThat() .apply(document("post/update/fail/noTitle")) @@ -233,32 +258,29 @@ void updatePost_exception_noTitle() { @Test @DisplayName("권한이 있는 게시글을 삭제한다.") void deletePost() { - Mockito.when(postService.deletePost(any(Long.class), any(AuthInfo.class))) - .thenReturn(PostDeleteResponse.builder() - .post_id(7L) - .build()); - restDocs .header("Authorization", "any") - .when().delete("/community/7") + .when().delete("/posts/7") .then().log().all() .assertThat() .apply(document("post/delete/success")) - .statusCode(HttpStatus.OK.value()); + .statusCode(HttpStatus.NO_CONTENT.value()); } @Test @DisplayName("권한이 없는 게시글을 삭제하면 403을 반환한다.") void deletePost_exception_forbidden() { - Mockito.when(postService.deletePost(any(Long.class), any(AuthInfo.class))) - .thenThrow(new AuthorizationException()); + doThrow(new AuthorizationException()) + .when(postDeleteService) + .deletePost(any(AuthInfo.class), anyLong()); restDocs .header("Authorization", "any") - .when().delete("/community/7") + .when().delete("/posts/7") .then().log().all() .assertThat() .apply(document("post/delete/fail/noAuth")) .statusCode(HttpStatus.FORBIDDEN.value()); } + } \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/post/application/PostCreateServiceTest.java b/src/test/java/org/wooriverygood/api/post/application/PostCreateServiceTest.java new file mode 100644 index 0000000..ca0e67e --- /dev/null +++ b/src/test/java/org/wooriverygood/api/post/application/PostCreateServiceTest.java @@ -0,0 +1,67 @@ +package org.wooriverygood.api.post.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.post.domain.Post; +import org.wooriverygood.api.post.dto.NewPostRequest; +import org.wooriverygood.api.post.exception.InvalidPostCategoryException; +import org.wooriverygood.api.post.repository.PostRepository; +import org.wooriverygood.api.util.MockTest; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class PostCreateServiceTest extends MockTest { + + @InjectMocks + private PostCreateService postCreateService; + + @Mock + private PostRepository postRepository; + + @Mock + private MemberRepository memberRepository; + + + @Test + @DisplayName("새로운 게시글을 작성한다.") + void addPost() { + NewPostRequest request = NewPostRequest.builder() + .postTitle("title") + .postCategory("자유") + .postContent("content") + .build(); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + + postCreateService.addPost(authInfo, request); + + verify(postRepository).save(any(Post.class)); + } + + @Test + @DisplayName("새로운 게시글의 카테고리가 유효하지 않으면 등록에 실패한다.") + void addPost_exception_invalid_category() { + NewPostRequest newPostRequest = NewPostRequest.builder() + .postTitle("title") + .postCategory("자유유") + .postContent("content") + .build(); + + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + + assertThatThrownBy(() -> postCreateService.addPost(authInfo, newPostRequest)) + .isInstanceOf(InvalidPostCategoryException.class); + } + +} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/post/application/PostDeleteServiceTest.java b/src/test/java/org/wooriverygood/api/post/application/PostDeleteServiceTest.java new file mode 100644 index 0000000..bb3a8c0 --- /dev/null +++ b/src/test/java/org/wooriverygood/api/post/application/PostDeleteServiceTest.java @@ -0,0 +1,76 @@ +package org.wooriverygood.api.post.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.global.error.exception.AuthorizationException; +import org.wooriverygood.api.comment.repository.CommentRepository; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.post.domain.Post; +import org.wooriverygood.api.post.repository.PostLikeRepository; +import org.wooriverygood.api.post.repository.PostRepository; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class PostDeleteServiceTest extends PostServiceTest { + + @InjectMocks + private PostDeleteService postDeleteService; + + @Mock + private PostRepository postRepository; + + @Mock + private CommentRepository commentRepository; + + @Mock + private PostLikeRepository postLikeRepository; + + @Mock + private MemberRepository memberRepository; + + + @Test + @DisplayName("권한이 있는 게시글을 삭제한다.") + void deletePost() { + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(postRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(post)); + + postDeleteService.deletePost(authInfo, post.getId()); + + assertAll( + () -> verify(commentRepository).deleteAllByPost(post), + () -> verify(postLikeRepository).deleteAllByPost(post) + ); + } + + @Test + @DisplayName("권한이 없는 게시글을 삭제하면 예외가 발생한다.") + void deletePost_exception_noAuth() { + Post noAuthPost = Post.builder() + .id(9L) + .title("title") + .content("content") + .member(new Member(5L, "username")) + .build(); + + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(postRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(noAuthPost)); + + assertThatThrownBy(() -> postDeleteService.deletePost(authInfo, post.getId())) + .isInstanceOf(AuthorizationException.class); + } + +} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/post/application/PostFindServiceTest.java b/src/test/java/org/wooriverygood/api/post/application/PostFindServiceTest.java new file mode 100644 index 0000000..d557dcc --- /dev/null +++ b/src/test/java/org/wooriverygood/api/post/application/PostFindServiceTest.java @@ -0,0 +1,171 @@ +package org.wooriverygood.api.post.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.post.domain.Post; +import org.wooriverygood.api.post.domain.PostCategory; +import org.wooriverygood.api.post.dto.PostDetailResponse; +import org.wooriverygood.api.post.dto.PostsResponse; +import org.wooriverygood.api.post.exception.PostNotFoundException; +import org.wooriverygood.api.post.repository.PostLikeRepository; +import org.wooriverygood.api.post.repository.PostRepository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +class PostFindServiceTest extends PostServiceTest { + + @InjectMocks + private PostFindService postFindService; + + @Mock + private PostRepository postRepository; + + @Mock + private PostLikeRepository postLikeRepository; + + @Mock + private MemberRepository memberRepository; + + private List posts = new ArrayList<>(); + + private List myPosts = new ArrayList<>(); + + private List freePosts = new ArrayList<>(); + + private final int POST_COUNT = 23; + + + @BeforeEach + void setUp() { + posts.clear(); + myPosts.clear(); + freePosts.clear(); + for (long i = 1; i <= POST_COUNT; i++) { + Post post = Post.builder() + .id(i) + .category(PostCategory.FREE) + .title("title" + i) + .content("content" + i) + .member(member) + .comments(new ArrayList<>()) + .postLikes(new ArrayList<>()) + .build(); + posts.add(post); + if (i > 9) { + myPosts.add(post); + freePosts.add(post); + } + } + } + + @Test + @DisplayName("로그인 한 상황에서 모든 카테고리의 게시글을 불러온다.") + void findPosts_login() { + Pageable pageable = PageRequest.of(0, 10); + + int start = (int) pageable.getOffset(); + int end = Math.min(start + pageable.getPageSize(), this.posts.size()); + List posts = this.posts.subList(start, end); + PageImpl page = new PageImpl<>(posts, pageable, this.posts.size()); + when(postRepository.findAllByOrderByIdDesc(any(PageRequest.class))) + .thenReturn(page); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(postLikeRepository.existsByPostAndMember(any(Post.class), any(Member.class))) + .thenReturn(true); + + PostsResponse response = postFindService.findPosts(authInfo, pageable, ""); + + assertAll( + () -> assertThat(response.getPosts().size()).isEqualTo(10), + () -> assertThat(response.getTotalPageCount()).isEqualTo(3), + () -> assertThat(response.getTotalPostCount()).isEqualTo(POST_COUNT) + ); + } + + @Test + @DisplayName("자유 카테고리의 게시글을 불러온다.") + void findPosts_category_free() { + Pageable pageable = PageRequest.of(0, 10); + int start = (int) pageable.getOffset(); + int end = Math.min(start + pageable.getPageSize(), this.freePosts.size()); + List posts = this.freePosts.subList(start, end); + PageImpl page = new PageImpl<>(posts, pageable, this.freePosts.size()); + when(postRepository.findAllByCategoryOrderByIdDesc(any(PostCategory.class), any(PageRequest.class))) + .thenReturn(page); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + + PostsResponse response = postFindService.findPosts(authInfo, pageable, "자유"); + + assertAll( + () -> assertThat(response.getPosts().size()).isEqualTo(10), + () -> assertThat(response.getTotalPageCount()).isEqualTo(2), + () -> assertThat(response.getTotalPostCount()).isEqualTo(14) + ); + } + + @Test + @DisplayName("유효한 id를 이용하여 특정 게시글을 불러온다.") + void findPostById() { + when(postRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(post)); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + + PostDetailResponse response = postFindService.findPostById(6L, authInfo); + + assertThat(response.getPostId()).isEqualTo(post.getId()); + } + + @Test + @DisplayName("유효하지 않은 id를 이용하여 특정 게시글을 불러오면 에러를 반환한다.") + void findPostById_exception_invalidId() { + when(postRepository.findById(any())) + .thenThrow(PostNotFoundException.class); + + assertThatThrownBy(() -> postFindService.findPostById(6L, authInfo)) + .isInstanceOf(PostNotFoundException.class); + } + + + @Test + @DisplayName("사용자 본인이 작성한 게시글을 불러온다.") + void findMyPosts() { + Pageable pageable = PageRequest.of(0, 10); + int start = (int) pageable.getOffset(); + int end = Math.min(start + pageable.getPageSize(), this.myPosts.size()); + List posts = this.myPosts.subList(start, end); + PageImpl page = new PageImpl<>(posts, pageable, this.myPosts.size()); + when(postRepository.findByMemberOrderByIdDesc(any(Member.class), any(PageRequest.class))) + .thenReturn(page); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + + PostsResponse response = postFindService.findMyPosts(authInfo, pageable); + + assertAll( + () -> assertThat(response.getPosts().size()).isEqualTo(10), + () -> assertThat(response.getTotalPageCount()).isEqualTo(2), + () -> assertThat(response.getTotalPostCount()).isEqualTo(14) + ); + } + +} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/post/application/PostLikeToggleServiceTest.java b/src/test/java/org/wooriverygood/api/post/application/PostLikeToggleServiceTest.java new file mode 100644 index 0000000..c4722e6 --- /dev/null +++ b/src/test/java/org/wooriverygood/api/post/application/PostLikeToggleServiceTest.java @@ -0,0 +1,78 @@ +package org.wooriverygood.api.post.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.post.domain.Post; +import org.wooriverygood.api.post.domain.PostLike; +import org.wooriverygood.api.post.dto.PostLikeResponse; +import org.wooriverygood.api.post.repository.PostLikeRepository; +import org.wooriverygood.api.post.repository.PostRepository; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +class PostLikeToggleServiceTest extends PostServiceTest { + + @InjectMocks + private PostLikeToggleService postLikeToggleService; + + @Mock + private PostRepository postRepository; + + @Mock + private PostLikeRepository postLikeRepository; + + @Mock + private MemberRepository memberRepository; + + + @Test + @DisplayName("특정 게시글에 좋아요를 1 올린다.") + void likePost_up() { + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(postRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(post)); + when(postLikeRepository.findByPostAndMember(any(Post.class), any(Member.class))) + .thenReturn(Optional.empty()); + + PostLikeResponse response = postLikeToggleService.togglePostLike(post.getId(), authInfo); + + assertAll( + () -> assertThat(response.getLikeCount()).isEqualTo(post.getLikeCount() + 1), + () -> assertThat(response.isLiked()).isEqualTo(true) + ); + } + + @Test + @DisplayName("특정 게시글에 좋아요를 1 내린다.") + void likePost_down() { + PostLike postLike = PostLike.builder() + .id(1L) + .post(post) + .member(member) + .build(); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(postRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(post)); + when(postLikeRepository.findByPostAndMember(any(Post.class), any(Member.class))) + .thenReturn(Optional.ofNullable(postLike)); + + PostLikeResponse response = postLikeToggleService.togglePostLike(postLike.getId(), authInfo); + + assertAll( + () -> assertThat(response.getLikeCount()).isEqualTo(post.getLikeCount() - 1), + () -> assertThat(response.isLiked()).isEqualTo(false) + ); + } + +} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/post/application/PostServiceTest.java b/src/test/java/org/wooriverygood/api/post/application/PostServiceTest.java new file mode 100644 index 0000000..6b944df --- /dev/null +++ b/src/test/java/org/wooriverygood/api/post/application/PostServiceTest.java @@ -0,0 +1,28 @@ +package org.wooriverygood.api.post.application; + +import org.junit.jupiter.api.BeforeEach; +import org.wooriverygood.api.post.domain.Post; +import org.wooriverygood.api.post.domain.PostCategory; +import org.wooriverygood.api.util.MockTest; + +import java.util.ArrayList; + +public class PostServiceTest extends MockTest { + + protected Post post; + + @BeforeEach + void dateSetUp() { + post = Post.builder() + .id(1L) + .category(PostCategory.OFFER) + .title("simple title") + .content("simple content") + .member(member) + .comments(new ArrayList<>()) + .postLikes(new ArrayList<>()) + .reports(new ArrayList<>()) + .build(); + } + +} diff --git a/src/test/java/org/wooriverygood/api/post/application/PostUpdateServiceTest.java b/src/test/java/org/wooriverygood/api/post/application/PostUpdateServiceTest.java new file mode 100644 index 0000000..22a5b59 --- /dev/null +++ b/src/test/java/org/wooriverygood/api/post/application/PostUpdateServiceTest.java @@ -0,0 +1,82 @@ +package org.wooriverygood.api.post.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.global.error.exception.AuthorizationException; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.post.domain.Post; +import org.wooriverygood.api.post.domain.PostCategory; +import org.wooriverygood.api.post.dto.PostUpdateRequest; +import org.wooriverygood.api.post.repository.PostRepository; + +import java.util.ArrayList; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +class PostUpdateServiceTest extends PostServiceTest { + + @InjectMocks + private PostUpdateService postUpdateService; + + @Mock + private PostRepository postRepository; + + @Mock + private MemberRepository memberRepository; + + private Post noAuthPost = Post.builder() + .id(99L) + .category(PostCategory.OFFER) + .title("title99") + .content("content99") + .member(new Member(5L, "username")) + .comments(new ArrayList<>()) + .postLikes(new ArrayList<>()) + .build(); + + + @Test + @DisplayName("권한이 있는 게시글을 수정한다.") + void updatePost() { + PostUpdateRequest request = PostUpdateRequest.builder() + .postTitle("new title") + .postContent("new content") + .build(); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(postRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(post)); + + postUpdateService.updatePost(post.getId(), request, authInfo); + + assertAll( + () -> assertThat(post.getTitle()).isEqualTo(request.getPostTitle()), + () -> assertThat(post.getContent()).isEqualTo(request.getPostContent()) + ); + } + + @Test + @DisplayName("권한이 없는 게시글을 수정할 수 없다.") + void updatePost_exception_noAuth() { + PostUpdateRequest request = PostUpdateRequest.builder() + .postTitle("new title") + .postContent("new content") + .build(); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(postRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(noAuthPost)); + + assertThatThrownBy(() -> postUpdateService.updatePost(noAuthPost.getId(), request, authInfo)) + .isInstanceOf(AuthorizationException.class); + } + +} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/post/service/PostServiceTest.java b/src/test/java/org/wooriverygood/api/post/service/PostServiceTest.java deleted file mode 100644 index 4a3144f..0000000 --- a/src/test/java/org/wooriverygood/api/post/service/PostServiceTest.java +++ /dev/null @@ -1,297 +0,0 @@ -package org.wooriverygood.api.post.service; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.wooriverygood.api.advice.exception.AuthorizationException; -import org.wooriverygood.api.advice.exception.InvalidPostCategoryException; -import org.wooriverygood.api.advice.exception.PostNotFoundException; -import org.wooriverygood.api.comment.repository.CommentRepository; -import org.wooriverygood.api.post.domain.Post; -import org.wooriverygood.api.post.domain.PostCategory; -import org.wooriverygood.api.post.domain.PostLike; -import org.wooriverygood.api.post.dto.*; -import org.wooriverygood.api.post.repository.PostLikeRepository; -import org.wooriverygood.api.post.repository.PostRepository; -import org.wooriverygood.api.support.AuthInfo; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import static org.mockito.ArgumentMatchers.any; - -@ExtendWith(MockitoExtension.class) -class PostServiceTest { - - @InjectMocks - private PostService postService; - - @Mock - private PostRepository postRepository; - - @Mock - private PostLikeRepository postLikeRepository; - - @Mock - private CommentRepository commentRepository; - - private final int POST_COUNT = 23; - - List posts = new ArrayList<>(); - - List myPosts = new ArrayList<>(); - - List freePosts = new ArrayList<>(); - - AuthInfo authInfo = AuthInfo.builder() - .sub("22222-34534-123") - .username("22222-34534-123") - .build(); - - Post singlePost = Post.builder() - .id(6L) - .category(PostCategory.OFFER) - .title("title6") - .content("content6") - .author(authInfo.getUsername()) - .comments(new ArrayList<>()) - .postLikes(new ArrayList<>()) - .build(); - - Post noAuthPost = Post.builder() - .id(99L) - .category(PostCategory.OFFER) - .title("title99") - .content("content99") - .author("43434-45654-234") - .comments(new ArrayList<>()) - .postLikes(new ArrayList<>()) - .build(); - - - @BeforeEach - void setUpPosts() { - for (int i = 1; i <= POST_COUNT; i++) { - Post post = Post.builder() - .id((long) i) - .category(PostCategory.FREE) - .title("title" + i) - .content("content" + i) - .author(authInfo.getUsername()) - .comments(new ArrayList<>()) - .postLikes(new ArrayList<>()) - .build(); - posts.add(post); - if (post.getId() > 9) myPosts.add(post); - if (post.getId() > 9) freePosts.add(post); - } - } - - @Test - @DisplayName("로그인 한 상황에서 게시글을 불러온다.") - void findPosts_login() { - Pageable pageable = PageRequest.of(0, 10); - - int start = (int) pageable.getOffset(); - int end = Math.min(start + pageable.getPageSize(), this.posts.size()); - List posts = this.posts.subList(start, end); - PageImpl page = new PageImpl<>(posts, pageable, this.posts.size()); - Mockito.when(postRepository.findAllByOrderByIdDesc(any(PageRequest.class))) - .thenReturn(page); - - PostsResponse response = postService.findPosts(authInfo, pageable); - - Assertions.assertThat(response.getPosts().size()).isEqualTo(10); - Assertions.assertThat(response.getTotalPageCount()).isEqualTo(3); - Assertions.assertThat(response.getTotalPostCount()).isEqualTo(POST_COUNT); - } - - @Test - @DisplayName("자유 카테고리의 게시글을 불러온다.") - void findPosts_category_free() { - Pageable pageable = PageRequest.of(0, 10); - int start = (int) pageable.getOffset(); - int end = Math.min(start + pageable.getPageSize(), this.freePosts.size()); - List posts = this.freePosts.subList(start, end); - PageImpl page = new PageImpl<>(posts, pageable, this.freePosts.size()); - Mockito.when(postRepository.findAllByCategoryOrderByIdDesc(any(PostCategory.class), any(PageRequest.class))) - .thenReturn(page); - - PostsResponse response = postService.findPostsByCategory(authInfo, pageable, "자유"); - - Assertions.assertThat(response.getPosts().size()).isEqualTo(10); - Assertions.assertThat(response.getTotalPageCount()).isEqualTo(2); - Assertions.assertThat(response.getTotalPostCount()).isEqualTo(14); - } - - @Test - @DisplayName("유효한 id를 이용하여 특정 게시글을 불러온다.") - void findPostById() { - Mockito.when(postRepository.findById(any())) - .thenReturn(Optional.ofNullable(singlePost)); - - PostResponse response = postService.findPostById(6L, authInfo); - - Assertions.assertThat(response).isNotNull(); - } - - @Test - @DisplayName("유효하지 않은 id를 이용하여 특정 게시글을 불러오면 에러를 반환한다.") - void findPostById_exception_invalidId() { - Mockito.when(postRepository.findById(any())) - .thenThrow(PostNotFoundException.class); - - Assertions.assertThatThrownBy(() -> postService.findPostById(6L, authInfo)) - .isInstanceOf(PostNotFoundException.class); - } - - @Test - @DisplayName("새로운 게시글을 작성한다.") - void addPost() { - NewPostRequest newPostRequest = NewPostRequest.builder() - .post_title("title") - .post_category("자유") - .post_content("content") - .build(); - - Mockito.when(postRepository.save(any(Post.class))) - .thenReturn(Post.builder() - .title(newPostRequest.getPost_title()) - .category(PostCategory.parse(newPostRequest.getPost_category())) - .content(newPostRequest.getPost_content()) - .author(authInfo.getUsername()) - .build()); - - NewPostResponse response = postService.addPost(authInfo, newPostRequest); - - Assertions.assertThat(response.getTitle()).isEqualTo(newPostRequest.getPost_title()); - Assertions.assertThat(response.getCategory()).isEqualTo(newPostRequest.getPost_category()); - Assertions.assertThat(response.getAuthor()).isEqualTo(authInfo.getUsername()); - } - - @Test - @DisplayName("새로운 게시글의 카테고리가 유효하지 않으면 등록에 실패한다.") - void addPost_exception_invalid_category() { - NewPostRequest newPostRequest = NewPostRequest.builder() - .post_title("title") - .post_category("자유유") - .post_content("content") - .build(); - - Assertions.assertThatThrownBy(() -> postService.addPost(authInfo, newPostRequest)) - .isInstanceOf(InvalidPostCategoryException.class); - } - - @Test - @DisplayName("사용자 본인이 작성한 게시글을 불러온다.") - void findMyPosts() { - Pageable pageable = PageRequest.of(0, 10); - int start = (int) pageable.getOffset(); - int end = Math.min(start + pageable.getPageSize(), this.myPosts.size()); - List posts = this.myPosts.subList(start, end); - PageImpl page = new PageImpl<>(posts, pageable, this.myPosts.size()); - Mockito.when(postRepository.findByAuthorOrderByIdDesc(any(String.class), any(PageRequest.class))) - .thenReturn(page); - - PostsResponse response = postService.findMyPosts(authInfo, pageable); - - Assertions.assertThat(response.getPosts().size()).isEqualTo(10); - Assertions.assertThat(response.getTotalPageCount()).isEqualTo(2); - Assertions.assertThat(response.getTotalPostCount()).isEqualTo(14); - } - - @Test - @DisplayName("특정 게시글에 좋아요를 1 올린다.") - void likePost_up() { - Mockito.when(postRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(singlePost)); - - PostLikeResponse response = postService.likePost(singlePost.getId(), authInfo); - - Assertions.assertThat(response.getLike_count()).isEqualTo(singlePost.getLikeCount() + 1); - Assertions.assertThat(response.isLiked()).isEqualTo(true); - } - - @Test - @DisplayName("특정 게시글에 좋아요를 1 내린다.") - void likePost_down() { - PostLike postLike = PostLike.builder() - .id(1L) - .post(singlePost) - .username(authInfo.getUsername()) - .build(); - Mockito.when(postRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(singlePost)); - Mockito.when(postLikeRepository.findByPostAndUsername(any(Post.class), any(String.class))) - .thenReturn(Optional.ofNullable(postLike)); - - PostLikeResponse response = postService.likePost(singlePost.getId(), authInfo); - - Assertions.assertThat(response.getLike_count()).isEqualTo(singlePost.getLikeCount() - 1); - Assertions.assertThat(response.isLiked()).isEqualTo(false); - } - - @Test - @DisplayName("권한이 있는 게시글을 수정한다.") - void updatePost() { - PostUpdateRequest request = PostUpdateRequest.builder() - .post_title("new title") - .post_content("new content") - .build(); - - Mockito.when(postRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(singlePost)); - - PostUpdateResponse response = postService.updatePost(singlePost.getId(), request, authInfo); - - Assertions.assertThat(response.getPost_id()).isEqualTo(singlePost.getId()); - Assertions.assertThat(singlePost.isUpdated()).isEqualTo(true); - } - - @Test - @DisplayName("권한이 없는 게시글을 수정할 수 없다.") - void updatePost_exception_noAuth() { - PostUpdateRequest request = PostUpdateRequest.builder() - .post_title("new title") - .post_content("new content") - .build(); - - Mockito.when(postRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(noAuthPost)); - - Assertions.assertThatThrownBy(() -> postService.updatePost(noAuthPost.getId(), request, authInfo)) - .isInstanceOf(AuthorizationException.class); - } - - @Test - @DisplayName("권한이 있는 게시글을 삭제한다.") - void deletePost() { - Mockito.when(postRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(singlePost)); - - PostDeleteResponse response = postService.deletePost(singlePost.getId(), authInfo); - - Assertions.assertThat(response.getPost_id()).isEqualTo(singlePost.getId()); - } - - @Test - @DisplayName("권한이 없는 게시글을 삭제한다.") - void deletePost_exception_noAuth() { - Mockito.when(postRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(noAuthPost)); - - Assertions.assertThatThrownBy(() -> postService.deletePost(noAuthPost.getId(), authInfo)) - .isInstanceOf(AuthorizationException.class); - } - -} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/report/controller/ReportControllerTest.java b/src/test/java/org/wooriverygood/api/report/api/ReportApiTest.java similarity index 62% rename from src/test/java/org/wooriverygood/api/report/controller/ReportControllerTest.java rename to src/test/java/org/wooriverygood/api/report/api/ReportApiTest.java index a70db4b..9e1067d 100644 --- a/src/test/java/org/wooriverygood/api/report/controller/ReportControllerTest.java +++ b/src/test/java/org/wooriverygood/api/report/api/ReportApiTest.java @@ -1,28 +1,20 @@ -package org.wooriverygood.api.report.controller; +package org.wooriverygood.api.report.api; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.wooriverygood.api.report.dto.ReportRequest; -import org.wooriverygood.api.support.AuthInfo; -import org.wooriverygood.api.util.ControllerTest; +import org.wooriverygood.api.util.ApiTest; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doNothing; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -public class ReportControllerTest extends ControllerTest { +public class ReportApiTest extends ApiTest { @Test @DisplayName("특정 게시글을 신고하면 201을 반환한다.") void reportPost() { - ReportRequest request = ReportRequest.builder() - .message("신고 내용") - .build(); - - doNothing().when(reportService) - .reportPost(any(Long.class), any(ReportRequest.class), any(AuthInfo.class)); + ReportRequest request = new ReportRequest("신고 내용"); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) @@ -38,12 +30,7 @@ void reportPost() { @Test @DisplayName("특정 댓글을 신고하면 201을 반환한다.") void reportComment() { - ReportRequest request = ReportRequest.builder() - .message("신고 내용") - .build(); - - doNothing().when(reportService) - .reportComment(any(Long.class), any(ReportRequest.class), any(AuthInfo.class)); + ReportRequest request = new ReportRequest("신고 내용"); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) @@ -55,5 +42,5 @@ void reportComment() { .apply(document("comments/report/success")) .statusCode(HttpStatus.CREATED.value()); } -} +} diff --git a/src/test/java/org/wooriverygood/api/report/application/CommentReportServiceTest.java b/src/test/java/org/wooriverygood/api/report/application/CommentReportServiceTest.java new file mode 100644 index 0000000..55961e3 --- /dev/null +++ b/src/test/java/org/wooriverygood/api/report/application/CommentReportServiceTest.java @@ -0,0 +1,95 @@ +package org.wooriverygood.api.report.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.comment.application.CommentServiceTest; +import org.wooriverygood.api.comment.exception.CommentNotFoundException; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.post.domain.Post; +import org.wooriverygood.api.post.domain.PostCategory; +import org.wooriverygood.api.report.domain.CommentReport; +import org.wooriverygood.api.report.exception.DuplicatedCommentReportException; +import org.wooriverygood.api.comment.domain.Comment; +import org.wooriverygood.api.comment.repository.CommentRepository; +import org.wooriverygood.api.report.dto.ReportRequest; +import org.wooriverygood.api.report.repository.CommentReportRepository; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.util.MockTest; + +import java.util.ArrayList; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class CommentReportServiceTest extends CommentServiceTest { + + @InjectMocks + private CommentReportService commentReportService; + + @Mock + private CommentRepository commentRepository; + + @Mock + private CommentReportRepository commentReportRepository; + + @Mock + private MemberRepository memberRepository; + + private Post post; + + + @BeforeEach + void setUp() { + post = Post.builder() + .id(6L) + .category(PostCategory.OFFER) + .title("title6") + .content("content6") + .member(member) + .comments(new ArrayList<>()) + .postLikes(new ArrayList<>()) + .build(); + } + + @Test + @DisplayName("유효한 id를 통해 특정 댓글을 신고한다.") + void reportComment() { + ReportRequest request = new ReportRequest("report message"); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(commentRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(comment)); + + commentReportService.reportComment(comment.getId(), request, authInfo); + + assertAll( + () -> assertThat(comment.getReports().size()).isEqualTo(1), + () -> verify(commentRepository).increaseReportCount(comment.getId()), + () -> verify(commentReportRepository).save(any(CommentReport.class)) + ); + } + + @Test + @DisplayName("동일한 댓글을 한 번 이상 신고하면 예외를 발생한다.") + void reportComment_exception_duplicated() { + ReportRequest request = new ReportRequest("report message"); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(commentRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(comment)); + + commentReportService.reportComment(comment.getId(), request, authInfo); + + assertThatThrownBy(() -> commentReportService.reportComment(1L, request, authInfo)) + .isInstanceOf(DuplicatedCommentReportException.class); + } + +} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/report/application/PostReportServiceTest.java b/src/test/java/org/wooriverygood/api/report/application/PostReportServiceTest.java new file mode 100644 index 0000000..f5fe642 --- /dev/null +++ b/src/test/java/org/wooriverygood/api/report/application/PostReportServiceTest.java @@ -0,0 +1,84 @@ +package org.wooriverygood.api.report.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.post.application.PostServiceTest; +import org.wooriverygood.api.post.exception.PostNotFoundException; +import org.wooriverygood.api.post.repository.PostRepository; +import org.wooriverygood.api.report.domain.PostReport; +import org.wooriverygood.api.report.dto.ReportRequest; +import org.wooriverygood.api.report.exception.DuplicatedPostReportException; +import org.wooriverygood.api.report.repository.PostReportRepository; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class PostReportServiceTest extends PostServiceTest { + + @InjectMocks + private PostReportService postReportService; + + @Mock + private PostRepository postRepository; + + @Mock + private PostReportRepository postReportRepository; + + @Mock + private MemberRepository memberRepository; + + + @Test + @DisplayName("유효한 id를 통해 특정 게시글을 신고한다.") + void reportPost() { + ReportRequest request = new ReportRequest("report message"); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(postRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(post)); + + postReportService.reportPost(1L, request, authInfo); + + assertAll( + () -> verify(postRepository).increaseReportCount(post.getId()), + () -> verify(postReportRepository).save(any(PostReport.class)) + ); + } + + @Test + @DisplayName("신고하려는 게시글 id가 유효하지 않으면 예외를 발생한다.") + void reportPost_exception_invalidId() { + ReportRequest request = new ReportRequest("report message"); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(postRepository.findById(anyLong())) + .thenThrow(new PostNotFoundException()); + + assertThatThrownBy(() -> postReportService.reportPost(post.getId(), request, authInfo)) + .isInstanceOf(PostNotFoundException.class); + } + + @Test + @DisplayName("동일한 게시글을 한 번 이상 신고하면 예외를 발생한다.") + void reportPost_exception_duplicated() { + ReportRequest request = new ReportRequest("report message"); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(postRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(post)); + postReportService.reportPost(post.getId(), request, authInfo); + + assertThatThrownBy(() -> postReportService.reportPost(post.getId(), request, authInfo)) + .isInstanceOf(DuplicatedPostReportException.class); + } + +} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/report/service/ReportServiceTest.java b/src/test/java/org/wooriverygood/api/report/service/ReportServiceTest.java deleted file mode 100644 index cf41495..0000000 --- a/src/test/java/org/wooriverygood/api/report/service/ReportServiceTest.java +++ /dev/null @@ -1,172 +0,0 @@ -package org.wooriverygood.api.report.service; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.wooriverygood.api.advice.exception.CommentNotFoundException; -import org.wooriverygood.api.advice.exception.DuplicatedCommentReportException; -import org.wooriverygood.api.advice.exception.DuplicatedPostReportException; -import org.wooriverygood.api.advice.exception.PostNotFoundException; -import org.wooriverygood.api.comment.domain.Comment; -import org.wooriverygood.api.comment.repository.CommentRepository; -import org.wooriverygood.api.post.domain.Post; -import org.wooriverygood.api.post.domain.PostCategory; -import org.wooriverygood.api.post.repository.PostRepository; -import org.wooriverygood.api.report.dto.ReportRequest; -import org.wooriverygood.api.report.repository.CommentReportRepository; -import org.wooriverygood.api.report.repository.PostReportRepository; -import org.wooriverygood.api.support.AuthInfo; - -import java.util.ArrayList; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class ReportServiceTest { - - @InjectMocks - private ReportService reportService; - - @Mock - private CommentRepository commentRepository; - - @Mock - private CommentReportRepository commentReportRepository; - - @Mock - private PostRepository postRepository; - - @Mock - private PostReportRepository postReportRepository; - - private AuthInfo testAuthInfo; - - private Post testPost; - - private Comment testComment; - - - @BeforeEach - void setUp() { - testAuthInfo = AuthInfo.builder() - .sub("22432-12312-3531") - .username("22432-12312-3531") - .build(); - - testPost = Post.builder() - .id(1L) - .category(PostCategory.OFFER) - .title("post title") - .content("post content") - .author(testAuthInfo.getUsername()) - .comments(new ArrayList<>()) - .postLikes(new ArrayList<>()) - .reports(new ArrayList<>()) - .build(); - - testComment = Comment.builder() - .id(1L) - .content("comment content") - .author(testAuthInfo.getUsername()) - .commentLikes(new ArrayList<>()) - .reports(new ArrayList<>()) - .build(); - } - - @Test - @DisplayName("유효한 id를 통해 특정 게시글을 신고한다.") - void reportPost() { - ReportRequest request = ReportRequest.builder() - .message("report message") - .build(); - - when(postRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(testPost)); - - reportService.reportPost(1L, request, testAuthInfo); - - assertThat(testPost.getReports().size()).isEqualTo(1); - } - - @Test - @DisplayName("신고하려는 게시글 id가 유효하지 않으면 예외를 발생한다.") - void reportPost_exception_invalidId() { - ReportRequest request = ReportRequest.builder() - .message("report message") - .build(); - - when(postRepository.findById(any(Long.class))) - .thenThrow(new PostNotFoundException()); - - assertThatThrownBy(() -> reportService.reportPost(1L, request, testAuthInfo)) - .isInstanceOf(PostNotFoundException.class); - } - - @Test - @DisplayName("동일한 게시글을 한 번 이상 신고하면 예외를 발생한다.") - void reportPost_exception_duplicated() { - ReportRequest request = ReportRequest.builder() - .message("report message") - .build(); - - when(postRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(testPost)); - reportService.reportPost(1L, request, testAuthInfo); - - assertThatThrownBy(() -> reportService.reportPost(1L, request, testAuthInfo)) - .isInstanceOf(DuplicatedPostReportException.class); - } - - @Test - @DisplayName("유효한 id를 통해 특정 댓글을 신고한다.") - void reportComment() { - ReportRequest request = ReportRequest.builder() - .message("report message") - .build(); - - when(commentRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(testComment)); - - reportService.reportComment(1L, request, testAuthInfo); - - assertThat(testComment.getReports().size()).isEqualTo(1); - } - - @Test - @DisplayName("신고하려는 댓글의 id가 유효하지 않으면 예외를 발생한다.") - void reportComment_exception_invalidId() { - ReportRequest request = ReportRequest.builder() - .message("report message") - .build(); - - when(commentRepository.findById(any(Long.class))) - .thenThrow(new CommentNotFoundException()); - - assertThatThrownBy(() -> reportService.reportComment(1L, request, testAuthInfo)) - .isInstanceOf(CommentNotFoundException.class); - } - - @Test - @DisplayName("동일한 댓글을 한 번 이상 신고하면 예외를 발생한다.") - void reportComment_exception_duplicated() { - ReportRequest request = ReportRequest.builder() - .message("report message") - .build(); - - when(commentRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(testComment)); - reportService.reportComment(1L, request, testAuthInfo); - - assertThatThrownBy(() -> reportService.reportComment(1L, request, testAuthInfo)) - .isInstanceOf(DuplicatedCommentReportException.class); - } - -} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/review/controller/ReviewControllerTest.java b/src/test/java/org/wooriverygood/api/review/api/ReviewApiTest.java similarity index 54% rename from src/test/java/org/wooriverygood/api/review/controller/ReviewControllerTest.java rename to src/test/java/org/wooriverygood/api/review/api/ReviewApiTest.java index d6fc435..45ad5bf 100644 --- a/src/test/java/org/wooriverygood/api/review/controller/ReviewControllerTest.java +++ b/src/test/java/org/wooriverygood/api/review/api/ReviewApiTest.java @@ -1,55 +1,57 @@ -package org.wooriverygood.api.review.controller; +package org.wooriverygood.api.review.api; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.wooriverygood.api.advice.exception.AuthorizationException; -import org.wooriverygood.api.advice.exception.CourseNotFoundException; -import org.wooriverygood.api.advice.exception.ReviewAccessDeniedException; -import org.wooriverygood.api.course.domain.Courses; +import org.wooriverygood.api.global.error.exception.AuthorizationException; +import org.wooriverygood.api.review.exception.ReviewAccessDeniedException; +import org.wooriverygood.api.course.domain.Course; import org.wooriverygood.api.review.dto.*; -import org.wooriverygood.api.support.AuthInfo; -import org.wooriverygood.api.util.ControllerTest; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.util.ApiTest; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -public class ReviewControllerTest extends ControllerTest { - List responses = new ArrayList<>(); +public class ReviewApiTest extends ApiTest { - Courses course= Courses.builder() + private List responses = new ArrayList<>(); + + private Course course = Course.builder() .id(1L) - .course_name("Gaoshu") - .course_category("Zhuanye") - .course_credit(5) + .name("Gaoshu") + .category("Zhuanye") + .credit(5) .isYouguan(0) .kaikeYuanxi("Xinke") .build(); - AuthInfo authInfo = AuthInfo.builder() + private AuthInfo authInfo = AuthInfo.builder() .sub("22222-34534-123") .username("22222-34534-123") .build(); + @BeforeEach void setUp() { for(int i = 1; i <= 2; i++) { responses.add(ReviewResponse.builder() - .review_id((long)i) - .course_id(1L) - .review_content("Test Review " + i) - .review_title("Title" + i) - .instructor_name("Jiaoshou") - .taken_semyr("22-23") + .reviewId((long)i) + .courseId(1L) + .reviewContent("Test Review " + i) + .reviewTitle("Title" + i) + .instructorName("Jiaoshou") + .takenSemyr("22-23") .grade("60") - .review_time(LocalDateTime.now()) + .reviewTime(LocalDateTime.now()) .isMine(false) .updated(false) .build()); @@ -59,10 +61,11 @@ void setUp() { @Test @DisplayName("최근 작성한 리뷰가 6개월 미만이라면, 특정 강의의 리뷰 조회 요청을 받으면 리뷰들을 반환한다.") void findAllReviewsByCourseId_success() { - Mockito.when(reviewService.canAccessReviews(any(AuthInfo.class))) - .thenReturn(true); - Mockito.when(reviewService.findAllReviewsByCourseId(any(), any(AuthInfo.class))) - .thenReturn(responses); + doNothing() + .when(reviewValidateAccessService) + .validateReviewAccess(any(AuthInfo.class)); + when(reviewFindService.findAllReviewsByCourseId(anyLong(), any(AuthInfo.class))) + .thenReturn(new ReviewsResponse(responses)); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) @@ -76,8 +79,9 @@ void findAllReviewsByCourseId_success() { @Test @DisplayName("최근 작성한 리뷰가 6개월 이상이거나 없다면, 리뷰 요청에 대한 BadRequest 예외를 반환한다.") void findAllReviewsByCourseId_exception_accessDenied() { - Mockito.when(reviewService.canAccessReviews(any(AuthInfo.class))) - .thenThrow(new ReviewAccessDeniedException()); + doThrow(new ReviewAccessDeniedException()) + .when(reviewValidateAccessService) + .validateReviewAccess(any(AuthInfo.class)); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) @@ -88,48 +92,17 @@ void findAllReviewsByCourseId_exception_accessDenied() { .statusCode(HttpStatus.BAD_REQUEST.value()); } - @Test - @DisplayName("유효하지 않은 수업의 리뷰를 조회하면 404를 반환한다.") - void findReviews_exception_invalidId() { - Mockito.when(reviewService.canAccessReviews(any(AuthInfo.class))) - .thenReturn(true); - Mockito.when(reviewService.findAllReviewsByCourseId(any(Long.class), any(AuthInfo.class))) - .thenThrow(new CourseNotFoundException()); - - restDocs - .contentType(MediaType.APPLICATION_JSON_VALUE) - .header("Authorization", "Bearer aws-cognito-access-token") - .when().get("/courses/1/reviews") - .then().log().all() - .assertThat() - .apply(document("reviews/find/fail/noCourse")) - .statusCode(HttpStatus.NOT_FOUND.value()); - } - @Test @DisplayName("특정 강의의 리뷰를 작성한다.") void addReview() { NewReviewRequest request = NewReviewRequest.builder() - .review_title("Test Review from TestCode") - .instructor_name("Jiaoshou") - .taken_semyr("1stSem") - .review_content("Good!") + .reviewTitle("Test Review from TestCode") + .instructorName("Jiaoshou") + .takenSemyr("1stSem") + .reviewContent("Good!") .grade("100") .build(); - NewReviewResponse response = NewReviewResponse.builder() - .review_id(50L) - .author_email(authInfo.getUsername()) - .review_title("Test Review from TestCode") - .instructor_name("Jiaoshou") - .taken_semyr("1stSem") - .review_content("Good!") - .grade("100") - .build(); - - Mockito.when(reviewService.addReview(any(AuthInfo.class), any(Long.class), any(NewReviewRequest.class))) - .thenReturn(response); - restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") @@ -144,16 +117,16 @@ void addReview() { @Test @DisplayName("특정 리뷰의 좋아요를 1 올리거나 내린다.") void likeReview() { - Mockito.when(reviewService.likeReview(any(Long.class), any(AuthInfo.class))) + when(reviewLikeToggleService.toggleReviewLike(any(Long.class), any(AuthInfo.class))) .thenReturn(ReviewLikeResponse.builder() - .like_count(5) + .likeCount(5) .liked(false) .build()); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") - .when().put("/courses/reviews/1/like") + .when().put("/reviews/1/like") .then().log().all() .assertThat() .apply(document("reviews/like/success")) @@ -166,26 +139,26 @@ void findMyReviews() { List responses = new ArrayList<>(); for (int i = 1; i < 5; i++) { responses.add(ReviewResponse.builder() - .review_id((long)i) - .course_id(1L) - .review_content("Test Review " + i) - .review_title("Title" + i) - .instructor_name("Jiaoshou") - .taken_semyr("22-23") + .reviewId((long)i) + .courseId(1L) + .reviewContent("Test Review " + i) + .reviewTitle("Title" + i) + .instructorName("Jiaoshou") + .takenSemyr("22-23") .grade("60") - .review_time(LocalDateTime.now()) + .reviewTime(LocalDateTime.now()) .isMine(true) .updated(false) .build()); } - Mockito.when(reviewService.findMyReviews(any(AuthInfo.class))) - .thenReturn(responses); + when(reviewFindService.findMyReviews(any(AuthInfo.class))) + .thenReturn(new ReviewsResponse(responses)); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") - .when().get("/courses/reviews/me") + .when().get("/reviews/me") .then().log().all() .assertThat() .apply(document("reviews/find/me/success")) @@ -196,44 +169,41 @@ void findMyReviews() { @DisplayName("권한이 있는 리뷰를 수정한다.") void updateReview() { ReviewUpdateRequest request = ReviewUpdateRequest.builder() - .review_title("new title") - .review_content("new content") - .instructor_name("jiaoshou") - .taken_semyr("18-19") + .reviewTitle("new title") + .reviewContent("new content") + .instructorName("jiaoshou") + .takenSemyr("18-19") .grade("100") .build(); - Mockito.when(reviewService.updateReview(any(Long.class), any(ReviewUpdateRequest.class), any(AuthInfo.class))) - .thenReturn(ReviewUpdateResponse.builder() - .review_id((long)1) - .build()); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") .body(request) - .when().put("/courses/reviews/1") + .when().put("/reviews/1") .then().log().all() .assertThat() .apply(document("reviews/update/success")) - .statusCode(HttpStatus.OK.value()); + .statusCode(HttpStatus.NO_CONTENT.value()); } @Test @DisplayName("권한이 없는 리뷰를 수정하면 403을 반환한다.") void updateReview_noAuth() { ReviewUpdateRequest request = ReviewUpdateRequest.builder() - .review_title("new title") - .review_content("new content") + .reviewTitle("new title") + .reviewContent("new content") .build(); - Mockito.when(reviewService.updateReview(any(Long.class), any(ReviewUpdateRequest.class), any(AuthInfo.class))) - .thenThrow(new AuthorizationException()); + doThrow(new AuthorizationException()) + .when(reviewUpdateService) + .updateReview(anyLong(), any(ReviewUpdateRequest.class), any(AuthInfo.class)); restDocs .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Authorization", "Bearer aws-cognito-access-token") .body(request) - .when().put("/courses/reviews/1") + .when().put("/reviews/1") .then().log().all() .assertThat() .apply(document("reviews/update/fail/noAuth")) @@ -243,35 +213,29 @@ void updateReview_noAuth() { @Test @DisplayName("권한이 있는 리뷰를 삭제한다.") void deleteReview() { - Mockito.when(reviewService.deleteReview(any(Long.class), any(AuthInfo.class))) - .thenReturn(ReviewDeleteResponse.builder() - .review_id(8L) - .build()); - restDocs .header("Authorization", "any") - .when().delete("/courses/reviews/8") + .when().delete("/reviews/8") .then().log().all() .assertThat() .apply(document("reviews/delete/success")) - .statusCode(HttpStatus.OK.value()); + .statusCode(HttpStatus.NO_CONTENT.value()); } @Test @DisplayName("권한이 없는 리뷰를 삭제하면 403을 반환한다.") void deleteReview_noAuth() { - Mockito.when(reviewService.deleteReview(any(Long.class), any(AuthInfo.class))) - .thenThrow(new AuthorizationException()); + doThrow(new AuthorizationException()) + .when(reviewDeleteService) + .deleteReview(anyLong(), any(AuthInfo.class)); restDocs .header("Authorization", "any") - .when().delete("/courses/reviews/8") + .when().delete("/reviews/8") .then().log().all() .assertThat() .apply(document("reviews/delete/fail/noAuth")) .statusCode(HttpStatus.FORBIDDEN.value()); } - - } diff --git a/src/test/java/org/wooriverygood/api/review/application/ReviewCreateServiceTest.java b/src/test/java/org/wooriverygood/api/review/application/ReviewCreateServiceTest.java new file mode 100644 index 0000000..a5ac22d --- /dev/null +++ b/src/test/java/org/wooriverygood/api/review/application/ReviewCreateServiceTest.java @@ -0,0 +1,71 @@ +package org.wooriverygood.api.review.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.course.domain.Course; +import org.wooriverygood.api.course.repository.CourseRepository; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.review.domain.Review; +import org.wooriverygood.api.review.dto.NewReviewRequest; +import org.wooriverygood.api.review.repository.ReviewRepository; +import org.wooriverygood.api.util.MockTest; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; + +class ReviewCreateServiceTest extends MockTest { + + @InjectMocks + private ReviewCreateService reviewCreateService; + + @Mock + private CourseRepository courseRepository; + + @Mock + private ReviewRepository reviewRepository; + + @Mock + private MemberRepository memberRepository; + + private Course course = Course.builder() + .id(1L) + .name("Gaoshu") + .category("Zhuanye") + .credit(5) + .isYouguan(0) + .kaikeYuanxi("Xinke") + .build(); + + + @Test + @DisplayName("특정 강의의 리뷰를 작성한다.") + void addReview() { + NewReviewRequest request = NewReviewRequest.builder() + .reviewTitle("Test Review from TestCode") + .instructorName("Jiaoshou") + .takenSemyr("1stSem") + .reviewContent("Good!") + .grade("100") + .build(); + + when(courseRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(course)); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + + reviewCreateService.addReview(authInfo, 1L, request); + + assertAll( + () -> verify(reviewRepository).save(any(Review.class)), + () -> verify(courseRepository).increaseReviewCount(course.getId()) + ); + } + +} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/review/application/ReviewDeleteServiceTest.java b/src/test/java/org/wooriverygood/api/review/application/ReviewDeleteServiceTest.java new file mode 100644 index 0000000..0099a3b --- /dev/null +++ b/src/test/java/org/wooriverygood/api/review/application/ReviewDeleteServiceTest.java @@ -0,0 +1,101 @@ +package org.wooriverygood.api.review.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.course.domain.Course; +import org.wooriverygood.api.course.repository.CourseRepository; +import org.wooriverygood.api.global.error.exception.AuthorizationException; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.review.domain.Review; +import org.wooriverygood.api.review.exception.ReviewNotFoundException; +import org.wooriverygood.api.review.repository.ReviewLikeRepository; +import org.wooriverygood.api.review.repository.ReviewRepository; +import org.wooriverygood.api.util.MockTest; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; + +class ReviewDeleteServiceTest extends MockTest { + + @InjectMocks + private ReviewDeleteService reviewDeleteService; + + @Mock + private CourseRepository courseRepository; + + @Mock + private ReviewRepository reviewRepository; + + @Mock + private ReviewLikeRepository reviewLikeRepository; + + @Mock + private MemberRepository memberRepository; + + private Course course; + + private Review review; + + @BeforeEach + void setUp() { + course = Course.builder() + .id(1L) + .build(); + review = Review.builder() + .member(member) + .course(course) + .build(); + } + + @Test + @DisplayName("권한이 있는 리뷰를 삭제한다.") + void deleteReview() { + when(reviewRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(review)); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + + reviewDeleteService.deleteReview(1L, authInfo); + + assertAll( + () -> verify(reviewLikeRepository).deleteAllByReview(review), + () -> verify(reviewRepository).delete(review), + () -> verify(courseRepository).decreaseReviewCount(course.getId()) + ); + } + + @Test + @DisplayName("권한이 없는 리뷰는 삭제가 불가능하다.") + void deleteReview_exception_noAuth() { + Review review = Review.builder() + .id(2L) + .member(new Member(5L, "noAuth")) + .build(); + when(reviewRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(review)); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + + assertThatThrownBy(() -> reviewDeleteService.deleteReview(1L, authInfo)) + .isInstanceOf(AuthorizationException.class); + } + + @Test + @DisplayName("리뷰를 찾을 수 없으면, 예외를 발생시킨다.") + void deleteReview_exception_reviewNotFound() { + when(reviewRepository.findById(anyLong())) + .thenThrow(new ReviewNotFoundException()); + + assertThatThrownBy(() -> reviewDeleteService.deleteReview(1L, authInfo)) + .isInstanceOf(ReviewNotFoundException.class); + } + +} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/review/application/ReviewFindServiceTest.java b/src/test/java/org/wooriverygood/api/review/application/ReviewFindServiceTest.java new file mode 100644 index 0000000..b3fa693 --- /dev/null +++ b/src/test/java/org/wooriverygood/api/review/application/ReviewFindServiceTest.java @@ -0,0 +1,102 @@ +package org.wooriverygood.api.review.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.course.domain.Course; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.review.domain.Review; +import org.wooriverygood.api.review.dto.ReviewsResponse; +import org.wooriverygood.api.review.repository.ReviewLikeRepository; +import org.wooriverygood.api.review.repository.ReviewRepository; +import org.wooriverygood.api.util.MockTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +class ReviewFindServiceTest extends MockTest { + + @InjectMocks + private ReviewFindService reviewFindService; + + @Mock + private ReviewRepository reviewRepository; + + @Mock + private ReviewLikeRepository reviewLikeRepository; + + @Mock + private MemberRepository memberRepository; + + private List reviews = new ArrayList<>(); + + private Course course = Course.builder() + .id(1L) + .name("Gaoshu") + .category("Zhuanye") + .credit(5) + .isYouguan(0) + .kaikeYuanxi("Xinke") + .build(); + + private final int REVIEW_COUNT = 10; + + + @BeforeEach + void setUp() { + for(long i = 0; i < REVIEW_COUNT; i++) { + Review review = Review.builder() + .id(i) + .course(course) + .member(member) + .reviewContent("review" + i) + .reviewTitle("review" + i) + .instructorName("jiaoshou") + .takenSemyr("22-23") + .grade("60") + .reviewLikes(new ArrayList<>()) + .updated(false) + .build(); + reviews.add(review); + } + } + + @Test + @DisplayName("유효한 id를 통해 특정 수업의 리뷰들을 불러온다.") + void findAllReviewsByCourseId() { + when(reviewRepository.findAllByCourseId(any())) + .thenReturn(reviews); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(reviewLikeRepository.existsByReviewAndMember(any(Review.class), any(Member.class))) + .thenReturn(true); + + ReviewsResponse response = reviewFindService.findAllReviewsByCourseId(1L, authInfo); + + assertThat(response.getReviews()).hasSize(REVIEW_COUNT); + assertThat(response.getReviews().get(0).getReviewTitle()).isEqualTo("review0"); + } + + @Test + @DisplayName("사용자 본인이 작성한 리뷰들을 불러온다.") + void findMyReviews() { + for (Review review : reviews) { + member.addReview(review); + } + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + + ReviewsResponse response = reviewFindService.findMyReviews(authInfo); + + assertThat(response.getReviews().get(0).isMine()).isEqualTo(true); + } + +} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/review/application/ReviewLikeToggleServiceTest.java b/src/test/java/org/wooriverygood/api/review/application/ReviewLikeToggleServiceTest.java new file mode 100644 index 0000000..c9f114e --- /dev/null +++ b/src/test/java/org/wooriverygood/api/review/application/ReviewLikeToggleServiceTest.java @@ -0,0 +1,91 @@ +package org.wooriverygood.api.review.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.review.domain.Review; +import org.wooriverygood.api.review.domain.ReviewLike; +import org.wooriverygood.api.review.dto.*; +import org.wooriverygood.api.review.repository.ReviewLikeRepository; +import org.wooriverygood.api.review.repository.ReviewRepository; +import org.wooriverygood.api.util.MockTest; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +public class ReviewLikeToggleServiceTest extends MockTest { + + @InjectMocks + private ReviewLikeToggleService reviewLikeToggleService; + + @Mock + private ReviewRepository reviewRepository; + + @Mock + private ReviewLikeRepository reviewLikeRepository; + + @Mock + private MemberRepository memberRepository; + + private final Review review = Review.builder() + .id(1L) + .reviewContent("test review") + .reviewTitle("test review title") + .instructorName("jiaoshou") + .takenSemyr("22-23") + .grade("100") + .member(member) + .reviewLikes(new ArrayList<>()) + .updated(false) + .createdAt(LocalDateTime.of(2022, 6, 13, 12, 00)) + .build(); + + + @Test + @DisplayName("특정 리뷰의 좋아요를 1 올린다.") + void likeReview_up() { + when(reviewRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(review)); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + + ReviewLikeResponse response = reviewLikeToggleService.toggleReviewLike(review.getId(), authInfo); + + assertAll( + () -> assertThat(response.getLikeCount()).isEqualTo(review.getLikeCount() + 1), + () -> assertThat(response.isLiked()).isEqualTo(true) + ); + } + + @Test + @DisplayName("특정 리뷰의 좋아요를 1 내린다.") + void likeReview_down() { + ReviewLike reviewLike = ReviewLike.builder() + .id(3L) + .review(review) + .member(member) + .build(); + + when(reviewRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(review)); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(reviewLikeRepository.findByReviewAndMember(any(Review.class), any(Member.class))) + .thenReturn(Optional.ofNullable(reviewLike)); + + ReviewLikeResponse response = reviewLikeToggleService.toggleReviewLike(review.getId(), authInfo); + + assertThat(response.getLikeCount()).isEqualTo(review.getLikeCount() - 1); + assertThat(response.isLiked()).isEqualTo(false); + } + +} diff --git a/src/test/java/org/wooriverygood/api/review/application/ReviewUpdateServiceTest.java b/src/test/java/org/wooriverygood/api/review/application/ReviewUpdateServiceTest.java new file mode 100644 index 0000000..5944d58 --- /dev/null +++ b/src/test/java/org/wooriverygood/api/review/application/ReviewUpdateServiceTest.java @@ -0,0 +1,85 @@ +package org.wooriverygood.api.review.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.global.error.exception.AuthorizationException; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.review.domain.Review; +import org.wooriverygood.api.review.dto.ReviewUpdateRequest; +import org.wooriverygood.api.review.repository.ReviewRepository; +import org.wooriverygood.api.util.MockTest; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.*; + +class ReviewUpdateServiceTest extends MockTest { + + @InjectMocks + private ReviewUpdateService reviewUpdateService; + + @Mock + private ReviewRepository reviewRepository; + + @Mock + private MemberRepository memberRepository; + + private Review review; + + @BeforeEach + void setUp() { + review = Review.builder() + .id(1L) + .member(member) + .build(); + } + + @Test + @DisplayName("권한이 있는 리뷰를 수정한다.") + void updateReview() { + ReviewUpdateRequest request = ReviewUpdateRequest.builder() + .reviewTitle("new title") + .reviewContent("new content") + .instructorName("jiaoshou") + .takenSemyr("18-19") + .grade("100") + .build(); + when(reviewRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(review)); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + + reviewUpdateService.updateReview(review.getId(), request, authInfo); + + assertAll( + () -> assertThat(review.getReviewTitle()).isEqualTo(request.getReviewTitle()), + () -> assertThat(review.getInstructorName()).isEqualTo(request.getInstructorName()), + () -> assertThat(review.getTakenSemyr()).isEqualTo(request.getTakenSemyr()), + () -> assertThat(review.getReviewContent()).isEqualTo(request.getReviewContent()), + () -> assertThat(review.getGrade()).isEqualTo(request.getGrade()) + ); + } + + @Test + @DisplayName("권한이 없는 리뷰는 수정이 불가능하다.") + void updateReview_noAuth() { + ReviewUpdateRequest request = ReviewUpdateRequest.builder() + .build(); + + when(reviewRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(review)); + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.of(new Member(5L, "noAuth"))); + + assertThatThrownBy(() -> reviewUpdateService.updateReview(review.getId(), request, authInfo)) + .isInstanceOf(AuthorizationException.class); + } + +} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/review/application/ReviewValidateAccessServiceTest.java b/src/test/java/org/wooriverygood/api/review/application/ReviewValidateAccessServiceTest.java new file mode 100644 index 0000000..e389a88 --- /dev/null +++ b/src/test/java/org/wooriverygood/api/review/application/ReviewValidateAccessServiceTest.java @@ -0,0 +1,80 @@ +package org.wooriverygood.api.review.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.wooriverygood.api.course.domain.Course; +import org.wooriverygood.api.member.domain.Member; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.review.exception.ReviewAccessDeniedException; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.review.domain.Review; +import org.wooriverygood.api.review.repository.ReviewRepository; +import org.wooriverygood.api.util.MockTest; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +class ReviewValidateAccessServiceTest extends MockTest { + + @InjectMocks + private ReviewValidateAccessService reviewValidateAccessService; + + @Mock + private ReviewRepository reviewRepository; + + @Mock + private MemberRepository memberRepository; + + + @Test + @DisplayName("마지막으로 작성한 리뷰가 현재 기준으로 6개월보다 가깝다면, 예외를 발생시키지 않는다.") + void canAccessReviews_true() { + Review review = Review.builder() + .createdAt(LocalDateTime.now().minusMonths(6)) + .build(); + + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(reviewRepository.findTopByMemberOrderByCreatedAtDesc(any(Member.class))) + .thenReturn(Optional.ofNullable(review)); + + reviewValidateAccessService.validateReviewAccess(authInfo); + } + + @Test + @DisplayName("리뷰를 작성하지 않았다면, 예외를 발생시킨다.") + void canAccessReviews_false_noReview() { + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(reviewRepository.findTopByMemberOrderByCreatedAtDesc(any(Member.class))) + .thenReturn(Optional.empty()); + + assertThatThrownBy(() -> reviewValidateAccessService.validateReviewAccess(authInfo)) + .isInstanceOf(ReviewAccessDeniedException.class); + } + + @Test + @DisplayName("마지막으로 작성한 리뷰가 현재 기준으로 6개월보다 멀다면, 예외를 발생시킨다.") + void canAccessReviews_false_sixMonths() { + Review review = Review.builder() + .createdAt(LocalDateTime.of(2022, 6, 13, 12, 00)) + .build(); + + when(memberRepository.findById(anyLong())) + .thenReturn(Optional.ofNullable(member)); + when(reviewRepository.findTopByMemberOrderByCreatedAtDesc(any(Member.class))) + .thenReturn(Optional.ofNullable(review)); + + assertThatThrownBy(() -> reviewValidateAccessService.validateReviewAccess(authInfo)) + .isInstanceOf(ReviewAccessDeniedException.class); + } + +} \ No newline at end of file diff --git a/src/test/java/org/wooriverygood/api/review/service/ReviewServiceTest.java b/src/test/java/org/wooriverygood/api/review/service/ReviewServiceTest.java deleted file mode 100644 index 4930a18..0000000 --- a/src/test/java/org/wooriverygood/api/review/service/ReviewServiceTest.java +++ /dev/null @@ -1,288 +0,0 @@ -package org.wooriverygood.api.review.service; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; -import org.wooriverygood.api.advice.exception.AuthorizationException; -import org.wooriverygood.api.advice.exception.CourseNotFoundException; -import org.wooriverygood.api.course.domain.Courses; -import org.wooriverygood.api.course.repository.CourseRepository; -import org.wooriverygood.api.review.domain.Review; -import org.wooriverygood.api.review.domain.ReviewLike; -import org.wooriverygood.api.review.dto.*; -import org.wooriverygood.api.review.repository.ReviewLikeRepository; -import org.wooriverygood.api.review.repository.ReviewRepository; -import org.wooriverygood.api.support.AuthInfo; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import static org.mockito.ArgumentMatchers.any; - -@ExtendWith(MockitoExtension.class) -public class ReviewServiceTest { - @InjectMocks - private ReviewService reviewService; - - @Mock - private ReviewRepository reviewRepository; - - @Mock - private CourseRepository courseRepository; - - @Mock - private ReviewLikeRepository reviewLikeRepository; - private final int REVIEW_COUNT = 10; - - List reviews = new ArrayList<>(); - - Courses singleCourse= Courses.builder() - .id(1L) - .course_name("Gaoshu") - .course_category("Zhuanye") - .course_credit(5) - .isYouguan(0) - .kaikeYuanxi("Xinke") - .build(); - - AuthInfo authInfo = AuthInfo.builder() - .sub("22222-34534-123") - .username("22222-34534-123") - .build(); - - @BeforeEach - void setUpReviews() { - for(int i = 0; i()) - .updated(false) - .build(); - reviews.add(review); - } - } - - Review singleReview = Review.builder() - .id(1L) - .course(singleCourse) - .reviewContent("test review") - .reviewTitle("test review title") - .instructorName("jiaoshou") - .takenSemyr("22-23") - .grade("100") - .authorEmail(authInfo.getUsername()) - .reviewLikes(new ArrayList<>()) - .updated(false) - .createdAt(LocalDateTime.of(2022, 6, 13, 12, 00)) - .build(); - - Review noAuthReview = Review.builder() - .id(1L) - .course(singleCourse) - .reviewContent("test review") - .reviewTitle("test review title") - .instructorName("jiaoshou") - .takenSemyr("22-23") - .grade("100") - .authorEmail("somerandom-username") - .reviewLikes(new ArrayList<>()) - .updated(false) - .createdAt(LocalDateTime.of(2024, 1, 13, 12, 00)) - .build(); - - @Test - @DisplayName("유효한 id를 통해 특정 수업의 리뷰들을 불러온다.") - void findAllReviewsByCourseId() { - Mockito.when(courseRepository.findById(any())).thenReturn(Optional.ofNullable(singleCourse)); - Mockito.when(reviewRepository.findAllByCourseId(any())) - .thenReturn(reviews); - - List responses = reviewService.findAllReviewsByCourseId(1L, authInfo); - - Assertions.assertThat(responses).hasSize(REVIEW_COUNT); - Assertions.assertThat(responses.get(0).getReview_title()).isEqualTo("review0"); - - } - - @Test - @DisplayName("존재하지 않는 강의의 리뷰는 불러올 수 없다.") - void findAllReviewsByCourseId_noCourse() { - Mockito.when(courseRepository.findById(any())).thenThrow(CourseNotFoundException.class); - - Assertions.assertThatThrownBy(() -> reviewService.findAllReviewsByCourseId(99L, authInfo)) - .isInstanceOf(CourseNotFoundException.class); - - } - - - @Test - @DisplayName("특정 강의의 리뷰를 작성한다.") - void addReview() { - NewReviewRequest newReviewRequest = NewReviewRequest.builder() - .review_title("Test Review from TestCode") - .instructor_name("Jiaoshou") - .taken_semyr("1stSem") - .review_content("Good!") - .grade("100") - .build(); - - Mockito.when(reviewRepository.save(any(Review.class))) - .thenReturn(Review.builder() - .authorEmail(authInfo.getUsername()) - .reviewTitle(newReviewRequest.getReview_title()) - .instructorName(newReviewRequest.getInstructor_name()) - .takenSemyr(newReviewRequest.getTaken_semyr()) - .reviewContent(newReviewRequest.getReview_content()) - .grade(newReviewRequest.getGrade()) - .build()); - - Mockito.when(courseRepository.findById(any())) - .thenReturn(Optional.ofNullable(singleCourse)); - - NewReviewResponse response = reviewService.addReview(authInfo, 1L, newReviewRequest); - - Assertions.assertThat(response.getAuthor_email()).isEqualTo(authInfo.getUsername()); - Assertions.assertThat(response.getReview_content()).isEqualTo(newReviewRequest.getReview_content()); - } - - @Test - @DisplayName("특정 리뷰의 좋아요를 1 올린다.") - void likeReview_up() { - Mockito.when(reviewRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(singleReview)); - - ReviewLikeResponse response = reviewService.likeReview(singleReview.getId(), authInfo); - - Assertions.assertThat(response.getLike_count()).isEqualTo(singleReview.getLikeCount() + 1); - Assertions.assertThat(response.isLiked()).isEqualTo(true); - } - - @Test - @DisplayName("특정 리뷰의 좋아요를 1 내린다.") - void likeReview_down() { - ReviewLike reviewLike = ReviewLike.builder() - .id(3L) - .review(singleReview) - .username(authInfo.getUsername()) - .build(); - - Mockito.when(reviewRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(singleReview)); - Mockito.when(reviewLikeRepository.findByReviewAndUsername(any(Review.class), any(String.class))) - .thenReturn(Optional.ofNullable(reviewLike)); - - ReviewLikeResponse response = reviewService.likeReview(singleReview.getId(), authInfo); - - Assertions.assertThat(response.getLike_count()).isEqualTo(singleReview.getLikeCount() - 1); - Assertions.assertThat(response.isLiked()).isEqualTo(false); - } - - @Test - @DisplayName("사용자 본인이 작성한 리뷰들을 불러온다.") - void findMyReviews() { - Mockito.when(reviewRepository.findByAuthorEmail(any(String.class))) - .thenReturn(reviews); - - List responses = reviewService.findMyReviews(authInfo); - - Assertions.assertThat(responses.get(0).isMine()).isEqualTo(true); - } - - @Test - @DisplayName("권한이 있는 리뷰를 수정한다.") - void updateReview() { - ReviewUpdateRequest request = ReviewUpdateRequest.builder() - .review_title("new title") - .review_content("new content") - .instructor_name("jiaoshou") - .taken_semyr("18-19") - .grade("100") - .build(); - - Mockito.when(reviewRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(singleReview)); - - ReviewUpdateResponse response = reviewService.updateReview(singleReview.getId(), request, authInfo); - - Assertions.assertThat(response.getReview_id()).isEqualTo(singleReview.getId()); - Assertions.assertThat(singleReview.isUpdated()).isEqualTo(true); - } - -// @Test -// @DisplayName("권한이 없는 리뷰는 수정이 불가능하다.") -// void updateReview_noAuth() { -// ReviewUpdateRequest request = ReviewUpdateRequest.builder() -// .review_title("new title") -// .review_content("new content") -// .build(); -// -// Mockito.when(reviewRepository.findById(any(Long.class))) -// .thenReturn(Optional.ofNullable(noAuthReview)); -// -// Assertions.assertThatThrownBy(() -> reviewService.updateReview(noAuthReview.getId(), request, authInfo)) -// .isInstanceOf(AuthorizationException.class); -// } - - @Test - @DisplayName("권한이 있는 리뷰를 삭제한다.") - void deleteReview() { - Mockito.when(reviewRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(singleReview)); - - ReviewDeleteResponse response = reviewService.deleteReview(singleReview.getId(), authInfo); - Assertions.assertThat(response.getReview_id()).isEqualTo(singleReview.getId()); - } - - @Test - @DisplayName("권한이 없는 리뷰는 삭제가 불가능하다.") - void deleteReview_noAuth() { - Mockito.when(reviewRepository.findById(any(Long.class))) - .thenReturn(Optional.ofNullable(noAuthReview)); - - Assertions.assertThatThrownBy(() -> reviewService.deleteReview(noAuthReview.getId(), authInfo)) - .isInstanceOf(AuthorizationException.class); - } - - @Test - @DisplayName("리뷰를 작성하지 않았다면, false를 반환한다.") - void canAccessReviews_false_noReview() { - Mockito.when(reviewRepository.findTopByAuthorEmailOrderByCreatedAtDesc(any(String.class))) - .thenReturn(Optional.empty()); - - Assertions.assertThat(reviewService.canAccessReviews(authInfo)).isEqualTo(false); - } - - @Test - @DisplayName("마지막으로 작성한 리뷰가 현재 기준으로 6개월보다 멀다면, false를 반환한다.") - void canAccessReviews_false_sixMonths() { - Mockito.when(reviewRepository.findTopByAuthorEmailOrderByCreatedAtDesc(any(String.class))) - .thenReturn(Optional.ofNullable(singleReview)); - - Assertions.assertThat(reviewService.canAccessReviews(authInfo)).isEqualTo(false); - } - - @Test - @DisplayName("마지막으로 작성한 리뷰가 현재 기준으로 6개월보다 가깝다면, true를 반환한다.") - void canAccessReviews_true() { - Mockito.when(reviewRepository.findTopByAuthorEmailOrderByCreatedAtDesc(any(String.class))) - .thenReturn(Optional.ofNullable(noAuthReview)); - - Assertions.assertThat(reviewService.canAccessReviews(authInfo)).isEqualTo(true); - } - -} diff --git a/src/test/java/org/wooriverygood/api/util/ControllerTest.java b/src/test/java/org/wooriverygood/api/util/ApiTest.java similarity index 55% rename from src/test/java/org/wooriverygood/api/util/ControllerTest.java rename to src/test/java/org/wooriverygood/api/util/ApiTest.java index 58ecb23..1ea00cc 100644 --- a/src/test/java/org/wooriverygood/api/util/ControllerTest.java +++ b/src/test/java/org/wooriverygood/api/util/ApiTest.java @@ -11,18 +11,21 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -import org.wooriverygood.api.comment.controller.CommentController; -import org.wooriverygood.api.comment.service.CommentService; -import org.wooriverygood.api.report.controller.ReportController; -import org.wooriverygood.api.report.service.ReportService; -import org.wooriverygood.api.course.controller.CourseController; -import org.wooriverygood.api.course.service.CourseService; -import org.wooriverygood.api.post.controller.PostController; -import org.wooriverygood.api.post.service.PostService; -import org.wooriverygood.api.review.controller.ReviewController; -import org.wooriverygood.api.review.service.ReviewService; -import org.wooriverygood.api.support.AuthInfo; -import org.wooriverygood.api.support.AuthenticationPrincipalArgumentResolver; +import org.wooriverygood.api.comment.api.CommentApi; +import org.wooriverygood.api.comment.application.*; +import org.wooriverygood.api.course.application.CourseFindService; +import org.wooriverygood.api.member.repository.MemberRepository; +import org.wooriverygood.api.post.application.*; +import org.wooriverygood.api.report.api.ReportApi; +import org.wooriverygood.api.report.application.CommentReportService; +import org.wooriverygood.api.course.api.CourseApi; +import org.wooriverygood.api.course.application.CourseCreateService; +import org.wooriverygood.api.post.api.PostApi; +import org.wooriverygood.api.report.application.PostReportService; +import org.wooriverygood.api.review.api.ReviewApi; +import org.wooriverygood.api.review.application.*; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.global.auth.AuthenticationPrincipalArgumentResolver; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; @@ -32,32 +35,80 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; @WebMvcTest({ - CommentController.class, - PostController.class, - CourseController.class, - ReviewController.class, - ReportController.class + CommentApi.class, + PostApi.class, + CourseApi.class, + ReviewApi.class, + ReportApi.class }) @WithMockUser @ExtendWith(RestDocumentationExtension.class) -public class ControllerTest { +public class ApiTest { protected MockMvcRequestSpecification restDocs; @MockBean - protected CourseService courseService; + protected MemberRepository memberRepository; @MockBean - protected ReviewService reviewService; + protected CourseCreateService courseCreateService; @MockBean - protected CommentService commentService; + protected CourseFindService courseFindService; @MockBean - protected PostService postService; + protected ReviewLikeToggleService reviewLikeToggleService; @MockBean - protected ReportService reportService; + protected ReviewFindService reviewFindService; + + @MockBean + protected ReviewCreateService reviewCreateService; + + @MockBean + protected ReviewDeleteService reviewDeleteService; + + @MockBean + protected ReviewUpdateService reviewUpdateService; + + @MockBean + protected ReviewValidateAccessService reviewValidateAccessService; + + @MockBean + protected CommentCreateService commentCreateService; + + @MockBean + protected CommentDeleteService commentDeleteService; + + @MockBean + protected CommentFindService commentFindService; + + @MockBean + protected CommentUpdateService commentUpdateService; + + @MockBean + protected CommentLikeToggleService commentLikeToggleService; + + @MockBean + protected PostLikeToggleService postLikeToggleService; + + @MockBean + protected PostFindService postFindService; + + @MockBean + protected PostCreateService postCreateService; + + @MockBean + protected PostUpdateService postUpdateService; + + @MockBean + protected PostDeleteService postDeleteService; + + @MockBean + protected CommentReportService commentReportService; + + @MockBean + protected PostReportService postReportService; @MockBean protected AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver; diff --git a/src/test/java/org/wooriverygood/api/util/MockTest.java b/src/test/java/org/wooriverygood/api/util/MockTest.java new file mode 100644 index 0000000..a4aae7c --- /dev/null +++ b/src/test/java/org/wooriverygood/api/util/MockTest.java @@ -0,0 +1,26 @@ +package org.wooriverygood.api.util; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.wooriverygood.api.global.auth.AuthInfo; +import org.wooriverygood.api.member.domain.Member; + +@ExtendWith(MockitoExtension.class) +public class MockTest { + + protected AuthInfo authInfo = AuthInfo.builder() + .memberId(1L) + .sub("22222-34534-123") + .username("22222-34534-123") + .build(); + + protected Member member; + + + @BeforeEach + void mockData() { + member = new Member(1L, authInfo.getUsername()); + } + +} diff --git a/src/test/java/org/wooriverygood/api/util/ResponseFixture.java b/src/test/java/org/wooriverygood/api/util/ResponseFixture.java index ffd921e..af0f2f8 100644 --- a/src/test/java/org/wooriverygood/api/util/ResponseFixture.java +++ b/src/test/java/org/wooriverygood/api/util/ResponseFixture.java @@ -2,7 +2,7 @@ import org.wooriverygood.api.post.dto.PostResponse; import org.wooriverygood.api.post.dto.PostsResponse; -import org.wooriverygood.api.support.AuthInfo; +import org.wooriverygood.api.global.auth.AuthInfo; import java.time.LocalDateTime; import java.util.ArrayList; @@ -21,17 +21,17 @@ public static PostResponse postResponse(long id, AuthInfo authInfo) { public static PostResponse postResponse(long id, String category, AuthInfo authInfo) { return PostResponse.builder() - .post_id(id) - .post_title("title_" + id) - .post_category(category) - .post_content("content_" + id) - .post_author(authInfo.getUsername()) - .post_comments((int) (Math.random() * 100)) - .post_likes((int) (Math.random() * 100)) - .post_time(LocalDateTime.now()) + .postId(id) + .postTitle("title_" + id) + .postCategory(category) + .postComments((int) (Math.random() * 100)) + .postLikes((int) (Math.random() * 100)) + .postTime(LocalDateTime.now()) .liked(id % 6 == 0) .updated(id % 9 == 0) .reported(false) + .memberId(1L) + .isMine(false) .build(); } @@ -45,17 +45,17 @@ public static PostResponse reportedPostResponse(long id, AuthInfo authInfo) { public static PostResponse reportedPostResponse(long id, String category, AuthInfo authInfo) { return PostResponse.builder() - .post_id(id) - .post_title(null) - .post_category(category) - .post_content(null) - .post_author(authInfo.getUsername()) - .post_comments((int) (Math.random() * 100)) - .post_likes((int) (Math.random() * 100)) - .post_time(LocalDateTime.now()) + .postId(id) + .postTitle(null) + .postCategory(category) + .postComments((int) (Math.random() * 100)) + .postLikes((int) (Math.random() * 100)) + .postTime(LocalDateTime.now()) .liked(id % 6 == 0) .updated(id % 9 == 0) .reported(true) + .memberId(1L) + .isMine(false) .build(); } @@ -74,19 +74,5 @@ public static PostsResponse postsResponse(int page, int totalCount, AuthInfo aut .build(); } - public static PostsResponse postsResponse(int page, int totalCount, String category, AuthInfo authInfo) { - List responses = new ArrayList<>(); - int bound = Math.min(totalCount - page * 10 + 1, 10); - int start = page * 10 + 1; - for (int i = start; i < start + bound; i++) { - if (i % 5 == 0) responses.add(reportedPostResponse(i, authInfo)); - else responses.add(postResponse(i, authInfo)); - } - return PostsResponse.builder() - .posts(responses) - .totalPostCount(totalCount) - .totalPageCount((int) Math.ceil((double) totalCount / 10)) - .build(); - } }