Skip to content

Commit

Permalink
refactor/#130 게시글 상세 조회 시 이전, 다음 게시글 정보 추가 (#131)
Browse files Browse the repository at this point in the history
* feat: PostTitleResponse 구현

* refactor: 게시글 상세조회 이전, 다음 게시글 정보 추가

* feat: 이전, 다음 게시글 조회 query 작성

* refactor: 게시글 상세 조회 메소드 이름, 로직 수정

* refactor: 게시글 상세조회 테스트 코드 수정

* refactor: schema example 수정

* feat: 마지막 게시글 조회 테스트케이스 추가

* refactor: schema example 수정

* Update PostDetailResponse.java

* refactor: deprecated된 메소드 수정

* refactor: 게시글 제목 컬럼 수정

* refactor: nullable 수정
  • Loading branch information
minjo-on authored Dec 10, 2024
1 parent 7a97598 commit 99b83e4
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
package kgu.developers.api.post.application;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import org.springframework.data.domain.PageRequest;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import kgu.developers.api.post.presentation.exception.PostNotFoundException;
import kgu.developers.api.post.presentation.request.PostRequest;
import kgu.developers.api.post.presentation.response.PostDetailResponse;
import kgu.developers.api.post.presentation.response.PostPersistResponse;
import kgu.developers.api.post.presentation.response.PostSummaryPageResponse;
import kgu.developers.api.post.presentation.response.PostTitleResponse;
import kgu.developers.api.user.application.UserService;
import kgu.developers.common.response.PaginatedListResponse;
import kgu.developers.domain.post.domain.Category;
import kgu.developers.domain.post.domain.Post;
import kgu.developers.domain.post.domain.PostRepository;
import kgu.developers.domain.user.domain.User;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Service
@RequiredArgsConstructor
Expand All @@ -39,17 +41,26 @@ public PostPersistResponse createPost(PostRequest request, Category category) {
}

public PostSummaryPageResponse getPostsByKeywordAndCategory(PageRequest request, String keyword,
Category category) {
Category category) {
PaginatedListResponse<Post> paginatedListResponse = postRepository.findAllByTitleContainingAndCategoryOrderByCreatedAtDesc(
keyword, category, request);
return PostSummaryPageResponse.of(paginatedListResponse.contents(), paginatedListResponse.pageable());
}

@Transactional(readOnly = true)
public PostDetailResponse getPostById(Long postId) {
@Transactional
public PostDetailResponse getPostByIdWithPrevAndNext(Long postId) {
Post post = getById(postId);
post.increaseViews();
return PostDetailResponse.from(post);

LocalDateTime timestamp = post.getCreatedAt();
Category category = post.getCategory();

Post prevPost = postRepository.findByPrevPost(timestamp, category).orElse(null);
Post nextPost = postRepository.findByNextPost(timestamp, category).orElse(null);

PostTitleResponse prevPostResponse = PostTitleResponse.from(prevPost);
PostTitleResponse nextPostResponse = PostTitleResponse.from(nextPost);
return PostDetailResponse.from(post, prevPostResponse, nextPostResponse);
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public ResponseEntity<PostSummaryPageResponse> getPostsByKeywordAndCategory(
public ResponseEntity<PostDetailResponse> getPostById(
@Parameter(description = "조회할 게시글의 id", example = "1", required = true) @PathVariable @Positive Long postId
) {
PostDetailResponse response = postService.getPostById(postId);
PostDetailResponse response = postService.getPostByIdWithPrevAndNext(postId);
return ResponseEntity.ok(response);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kgu.developers.api.post.presentation.response;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import java.time.format.DateTimeFormatter;
Expand All @@ -13,7 +14,7 @@

@Builder
public record PostDetailResponse(
@Schema(description = "게시글 id", example = "1", requiredMode = REQUIRED)
@Schema(description = "게시글 id", example = "2", requiredMode = REQUIRED)
Long postId,

@Schema(description = "게시글 카테고리", example = "학과공지", requiredMode = REQUIRED)
Expand Down Expand Up @@ -50,10 +51,22 @@ public record PostDetailResponse(

@Schema(description = "작성일", example = "2024-11-11 15:45", requiredMode = REQUIRED)
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm")
String createdAt
String createdAt,

@Schema(description = "이전 게시글 정보",
example = "{\"id\": 1,"
+ " \"title\": \"이전 게시글 제목\"}",
requiredMode = NOT_REQUIRED)
PostTitleResponse prevPost,

@Schema(description = "다음 게시글 정보",
example = "{\"id\": 3, "
+ "\"title\": \"다음 게시글 제목\"}",
requiredMode = NOT_REQUIRED)
PostTitleResponse nextPost

) {
public static PostDetailResponse from(Post post) {
public static PostDetailResponse from(Post post, PostTitleResponse prevPost, PostTitleResponse nextPost) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
return PostDetailResponse.builder()
.postId(post.getId())
Expand All @@ -65,6 +78,8 @@ public static PostDetailResponse from(Post post) {
.isPinned(post.isPinned())
.file(post.getFile() != null ? FileResponse.from(post.getFile()) : null)
.createdAt(post.getCreatedAt().format(formatter))
.prevPost(prevPost)
.nextPost(nextPost)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package kgu.developers.api.post.presentation.response;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import io.swagger.v3.oas.annotations.media.Schema;
import kgu.developers.domain.post.domain.Post;
import lombok.Builder;

@Builder
public record PostTitleResponse(
@Schema(description = "게시글 id", example = "1", requiredMode = REQUIRED)
Long postId,

@Schema(description = "게시글 제목", example = "SW 부트캠프 4기 교육생 모집", requiredMode = REQUIRED)
String title
) {
public static PostTitleResponse from(Post post) {
if (post == null) {
return null;
}

return PostTitleResponse.builder()
.postId(post.getId())
.title(post.getTitle())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.PageRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import kgu.developers.api.post.application.PostService;
import kgu.developers.api.post.presentation.exception.PostNotFoundException;
Expand All @@ -17,15 +28,6 @@
import kgu.developers.domain.user.domain.User;
import mock.FakePostRepository;
import mock.FakeUserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.PageRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class PostServiceTest {
private PostService postService;
Expand Down Expand Up @@ -82,25 +84,44 @@ public void createPost_Success() {
assertEquals(3, response.postId());

// when
PostDetailResponse created = postService.getPostById(response.postId());
Post created = postService.getById(response.postId());

// then
assertEquals(request.title(), created.title());
assertEquals(request.content(), created.content());
assertEquals(category.getDescription(), created.category());
assertEquals(request.title(), created.getTitle());
assertEquals(request.content(), created.getContent());
assertEquals(category.getDescription(), created.getCategory().getDescription());
}

@Test
@DisplayName("getPostById는 게시글을 조회할 수 있다")
@DisplayName("getPostById는 해당 게시글과 이전, 다음 게시글을 조회할 수 있다")
public void getPostById_Success() {
// given
Long postId = 1L;

// when
PostDetailResponse response = postService.getPostById(postId);
PostDetailResponse response = postService.getPostByIdWithPrevAndNext(postId);

// then
assertEquals(postId, response.postId());
assertNull(response.prevPost());
assertEquals(response.nextPost().postId(), 2L);
assertEquals(response.nextPost().title(), "테스트용 제목2");
}

@Test
@DisplayName("getPostById는 마지막 게시글 조회 시 다음 게시글은 null이어야 한다")
public void getPostById_LastPost_Success() {
// given
Long lastPostId = 2L;

// when
PostDetailResponse response = postService.getPostByIdWithPrevAndNext(lastPostId);

// then
assertEquals(lastPostId, response.postId());
assertNull(response.nextPost());
assertEquals(response.prevPost().postId(), 1L);
assertEquals(response.prevPost().title(), "테스트용 제목1");
}

@Test
Expand All @@ -112,7 +133,7 @@ public void getPostById_Throws_PostNotFoundException() {
// when
// then
assertThatThrownBy(
() -> postService.getPostById(postId)
() -> postService.getPostByIdWithPrevAndNext(postId)
).isInstanceOf(PostNotFoundException.class);
}

Expand All @@ -139,13 +160,13 @@ public void getPostsByKeywordAndCategory_Success() {
public void togglePostPinStatus_Success() {
// given
Long postId = 1L;
PostDetailResponse before = postService.getPostById(postId);
PostDetailResponse before = postService.getPostByIdWithPrevAndNext(postId);

// when
postService.togglePostPinStatus(postId);

// then
PostDetailResponse after = postService.getPostById(postId);
PostDetailResponse after = postService.getPostByIdWithPrevAndNext(postId);
assertNotEquals(before.isPinned(), after.isPinned());
}

Expand All @@ -160,7 +181,7 @@ public void deletePost_Throws_PostNotFoundException() {

// then
assertThatThrownBy(
() -> postService.getPostById(postId)
() -> postService.getPostByIdWithPrevAndNext(postId)
).isInstanceOf(PostNotFoundException.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ protected ResponseEntity<ExceptionResponse> handleException(Exception exception)
protected ResponseEntity<Object> handleHandlerMethodValidationException(HandlerMethodValidationException exception,
HttpHeaders headers, HttpStatusCode status, WebRequest request) {

String message = exception.getAllValidationResults().stream()
String message = exception.getParameterValidationResults().stream()
.map(ParameterValidationResult::getResolvableErrors)
.flatMap(List::stream)
.map(MessageSourceResolvable::getDefaultMessage)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kgu.developers.domain.post.domain;

import java.time.LocalDateTime;
import java.util.Optional;

import org.springframework.data.domain.Pageable;
Expand All @@ -15,4 +16,8 @@ PaginatedListResponse<Post> findAllByTitleContainingAndCategoryOrderByCreatedAtD
Optional<Post> findById(Long postId);

void deleteAllByDeletedAtBefore(int retentionDays);

Optional<Post> findByPrevPost(LocalDateTime createdAt, Category category);

Optional<Post> findByNextPost(LocalDateTime createdAt, Category category);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
package kgu.developers.domain.post.infrastructure;

import kgu.developers.domain.post.domain.Post;
import java.time.LocalDateTime;
import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

import kgu.developers.domain.post.domain.Category;
import kgu.developers.domain.post.domain.Post;

public interface JpaPostRepository extends JpaRepository<Post, Long> {
Optional<Post> findFirstByCreatedAtLessThanAndDeletedAtIsNullAndCategoryOrderByCreatedAtDesc(
LocalDateTime createdAt, Category category);

Optional<Post> findFirstByCreatedAtGreaterThanAndDeletedAtIsNullAndCategoryOrderByCreatedAtAsc(
LocalDateTime createdAt, Category category);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kgu.developers.domain.post.infrastructure;

import java.time.LocalDateTime;
import java.util.Optional;

import org.springframework.data.domain.Pageable;
Expand Down Expand Up @@ -38,4 +39,16 @@ public void deleteAllByDeletedAtBefore(int retentionDays) {
queryPostRepository.deleteAllByDeletedAtBefore(retentionDays);
}

@Override
public Optional<Post> findByPrevPost(LocalDateTime createdAt, Category category) {
return jpaPostRepository.findFirstByCreatedAtLessThanAndDeletedAtIsNullAndCategoryOrderByCreatedAtDesc(
createdAt, category);
}

@Override
public Optional<Post> findByNextPost(LocalDateTime createdAt, Category category) {
return jpaPostRepository.findFirstByCreatedAtGreaterThanAndDeletedAtIsNullAndCategoryOrderByCreatedAtAsc(
createdAt, category);
}

}
33 changes: 25 additions & 8 deletions aics-domain/src/testFixtures/java/mock/FakePostRepository.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
package mock;

import kgu.developers.common.response.PageableResponse;
import kgu.developers.common.response.PaginatedListResponse;
import kgu.developers.domain.post.domain.Category;
import kgu.developers.domain.post.domain.Post;
import kgu.developers.domain.post.domain.PostRepository;
import org.springframework.data.domain.Pageable;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
Expand All @@ -16,6 +9,14 @@
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;

import org.springframework.data.domain.Pageable;

import kgu.developers.common.response.PageableResponse;
import kgu.developers.common.response.PaginatedListResponse;
import kgu.developers.domain.post.domain.Category;
import kgu.developers.domain.post.domain.Post;
import kgu.developers.domain.post.domain.PostRepository;

public class FakePostRepository implements PostRepository {
private final List<Post> data = Collections.synchronizedList(new ArrayList<>());
private final AtomicLong autoGeneratedId = new AtomicLong(0);
Expand Down Expand Up @@ -58,7 +59,7 @@ public PaginatedListResponse<Post> findAllByTitleContainingAndCategoryOrderByCre
.sorted(Comparator.comparing(Post::getCreatedAt).reversed())
.collect(Collectors.toList());

int start = (int) pageable.getOffset();
int start = (int)pageable.getOffset();
int end = Math.min(start + pageable.getPageSize(), filteredPosts.size());

List<Post> paginatedPosts = start > filteredPosts.size() ?
Expand All @@ -84,4 +85,20 @@ public void deleteAllByDeletedAtBefore(int retentionDays) {
LocalDateTime threshold = LocalDateTime.now().minusDays(retentionDays);
data.removeIf(post -> post.getDeletedAt() != null && post.getDeletedAt().isBefore(threshold));
}

@Override
public Optional<Post> findByPrevPost(LocalDateTime createdAt, Category category) {
return data.stream()
.filter(post -> post.getCreatedAt().isBefore(createdAt) && post.getCategory().equals(category)
&& post.getDeletedAt() == null)
.max(Comparator.comparing(Post::getCreatedAt));
}

@Override
public Optional<Post> findByNextPost(LocalDateTime createdAt, Category category) {
return data.stream()
.filter(post -> post.getCreatedAt().isAfter(createdAt) && post.getCategory().equals(category)
&& post.getDeletedAt() == null)
.min(Comparator.comparing(Post::getCreatedAt));
}
}

0 comments on commit 99b83e4

Please sign in to comment.