Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor/#130 게시글 상세 조회 시 이전, 다음 게시글 정보 추가 #131

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
Expand Up @@ -50,10 +50,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 = REQUIRED, nullable = true)
minjo-on marked this conversation as resolved.
Show resolved Hide resolved
PostTitleResponse prevPost,

@Schema(description = "다음 게시글 정보",
example = "{\"id\": 1, "
+ "\"title\": \"이전 게시글 제목\"}",
requiredMode = REQUIRED, nullable = true)
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 +77,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,25 @@
package kgu.developers.api.post.presentation.response;

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", nullable = true)
Long postId,

@Schema(description = "게시글 제목", example = "SW 부트캠프 4기 교육생 모집", nullable = true)
String postTitle
minjo-on marked this conversation as resolved.
Show resolved Hide resolved
) {
public static PostTitleResponse from(Post post) {
if (post == null) {
return null;
}

return PostTitleResponse.builder()
.postId(post.getId())
.postTitle(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);
minjo-on marked this conversation as resolved.
Show resolved Hide resolved
assertEquals(response.nextPost().postTitle(), "테스트용 제목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().postTitle(), "테스트용 제목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
@@ -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));
}
}
Loading