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

[Feat] 게시글 목록 조회 수정 #141

Merged
merged 5 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.palbang.unsemawang.community.constant;

public enum Sortingtype {
LATEST,
MOST_VIEWED;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.palbang.unsemawang.common.util.pagination.CursorRequest;
import com.palbang.unsemawang.common.util.pagination.LongCursorResponse;
import com.palbang.unsemawang.community.constant.CommunityCategory;
import com.palbang.unsemawang.community.constant.Sortingtype;
import com.palbang.unsemawang.community.dto.response.PostListResponse;
import com.palbang.unsemawang.community.service.PostListService;

Expand All @@ -22,15 +23,27 @@ public class PostListController {

private final PostListService postListService;

@Operation(summary = "게시글 목록 조회")
@Operation(
description = """
sort(정렬 분류) : latest || mostViewed <br>
인기 게시글 조회 시 sort = null
""",
summary = "게시글 목록 조회")
@GetMapping("/posts")
public ResponseEntity<LongCursorResponse<PostListResponse>> getPostList(
@RequestParam CommunityCategory category,
@RequestParam(required = false) Long cursorId,
@RequestParam(required = false, defaultValue = "10") Integer size
@RequestParam(required = false, defaultValue = "10") Integer size,
@RequestParam(required = false, defaultValue = "LATEST") Sortingtype sort
) {
CursorRequest<Long> cursorRequest = new CursorRequest<>(cursorId, size);
LongCursorResponse<PostListResponse> response = postListService.getPostList(category, cursorRequest);

LongCursorResponse<PostListResponse> response = postListService.getPostList(
category,
sort,
cursorRequest
);

return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.time.LocalDateTime;

import com.palbang.unsemawang.common.util.file.service.FileService;
import com.palbang.unsemawang.community.constant.CommunityCategory;
import com.palbang.unsemawang.community.entity.Post;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.palbang.unsemawang.community.repository;

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

Expand Down Expand Up @@ -34,24 +35,41 @@ public interface PostRepository extends JpaRepository<Post, Long> {
AND p.communityCategory = :category
AND p.isVisible = true
AND p.isDeleted = false
ORDER BY p.id DESC
ORDER BY p.registeredAt DESC
""")
List<Post> findLatestPostsByCategory(
@Param("category") CommunityCategory category,
@Param("cursorId") Long cursorId,
Pageable pageable // Spring Data JPA Pageable 사용
);

// 인기 게시글 가져오기
// 카테고리별 조회수 순 게시글 가져오기
@Query("""
SELECT p FROM Post p
WHERE (p.id < :cursorId OR :cursorId IS NULL)
AND p.communityCategory = :category
AND p.isVisible = true
AND p.isDeleted = false
ORDER BY p.viewCount DESC, p.id DESC
""")
List<Post> findMostViewedPostsByCategory(
@Param("category") CommunityCategory category,
@Param("cursorId") Long cursorId,
Pageable pageable
);

// 인기 게시글 가져오기(최근 30일)
@Query("""
SELECT p FROM Post p
WHERE p.isVisible = true
AND p.isDeleted = false
AND (p.id < :cursorId OR :cursorId IS NULL)
AND p.registeredAt >= :thirtyDaysAgo
ORDER BY (p.viewCount * 7) + (p.likeCount * 3) DESC, p.id DESC
""")
List<Post> findPopularPosts(
@Param("cursorId") Long cursorId,
@Param("thirtyDaysAgo") LocalDateTime thirtyDaysAgo,
Pageable pageable
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pageable->커서기반으로 리팩토링 부탁합니다!

);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ public PostDetailResponse getPostDetail(String memberId, Long postId) {

// 비공개 접근 권한 확인
if (!post.getIsVisible() && !post.getMember().getId().equals(memberId)) {
throw new GeneralException(ResponseCode.FORBIDDEN); // 적절한 응답 코드 사용
throw new GeneralException(ResponseCode.FORBIDDEN);
}

// 삭제 처리된 글 조회 제한
if (post.getIsDeleted()) {
throw new GeneralException(ResponseCode.NOT_EXIST_POST);
}

// 게시글에 포함된 이미지 리스트 조회
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package com.palbang.unsemawang.community.service;

import java.time.LocalDateTime;
import java.util.List;

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

import com.palbang.unsemawang.common.util.file.service.FileService;
import com.palbang.unsemawang.common.util.pagination.CursorRequest;
import com.palbang.unsemawang.common.util.pagination.LongCursorResponse;
import com.palbang.unsemawang.community.constant.CommunityCategory;
import com.palbang.unsemawang.community.constant.Sortingtype;
import com.palbang.unsemawang.community.dto.response.PostListResponse;
import com.palbang.unsemawang.community.entity.Post;
import com.palbang.unsemawang.community.repository.PostRepository;
Expand All @@ -24,21 +27,18 @@ public class PostListService {
private final PostRepository postRepository;
private final FileService fileService;

public LongCursorResponse<PostListResponse> getPostList(CommunityCategory category,
public LongCursorResponse<PostListResponse> getPostList(
CommunityCategory category,
Sortingtype sort,
CursorRequest<Long> cursorRequest) {
List<Post> posts;

// 요청 크기 기반 페이징 설정
var pageable = PageRequest.of(0, cursorRequest.size());

// 카테고리별 게시글 조회
if (category == CommunityCategory.POPULAR_BOARD) {
posts = postRepository.findPopularPosts(cursorRequest.key(), pageable);
} else {
posts = postRepository.findLatestPostsByCategory(category, cursorRequest.key(), pageable);
}
// 게시글 조회 분기 처리 (로직 메서드 분리)
List<Post> posts = fetchPosts(category, sort, cursorRequest.key(), pageable);

// 응답 데이터 매핑 (FileService를 통해 썸네일 URL 포함)
// 응답 데이터 매핑
List<PostListResponse> data = posts.stream()
.map(post -> PostListResponse.fromEntity(
post,
Expand All @@ -49,7 +49,22 @@ public LongCursorResponse<PostListResponse> getPostList(CommunityCategory catego
// 다음 커서 생성
Long nextCursor = !data.isEmpty() ? data.get(data.size() - 1).getCursorId() : null;

// LongCursorResponse를 반환
// LongCursorResponse 반환
return LongCursorResponse.of(cursorRequest.next(nextCursor), data);
}

// Helper 메서드: 게시글 조회 분기 처리
private List<Post> fetchPosts(CommunityCategory category, Sortingtype sort, Long cursorId, Pageable pageable) {
if (Sortingtype.MOST_VIEWED.equals(sort)) {
return postRepository.findMostViewedPostsByCategory(category, cursorId, pageable);
}
if (CommunityCategory.POPULAR_BOARD.equals(category)) {
LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30);
return postRepository.findPopularPosts(cursorId, thirtyDaysAgo, pageable);
}
if (Sortingtype.LATEST.equals(sort)) {
return postRepository.findLatestPostsByCategory(category, cursorId, pageable);
}
throw new IllegalArgumentException("지원하지 않는 정렬 옵션입니다. 'latest' 또는 'mostViewed' 만 가능합니다.");
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

대박 엄청 단순해졌네요👍

Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
package com.palbang.unsemawang.fortune.dto.result;

import java.util.List;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -19,54 +13,9 @@
public class GunghapApiRequest {
@Schema(description = "본인 정보", required = true)
@Valid
private UserInfoDto me;
private FortuneApiRequest me;

@Schema(description = "상대 정보", required = true)
@Schema(description = "상대 정보", required = false)
@Valid
private UserInfoDto other;

@Getter
@AllArgsConstructor
@NoArgsConstructor
public static class UserInfoDto {

@Schema(description = "사용자 이름", example = "이몽룡", required = false)
private String name;

@Schema(description = "성별", example = "남", required = true)
@Pattern(regexp = "남|여", message = "sex must be one of the following values: 남, 여")
private String sex;

@Schema(description = "출생 연도", example = "1990", required = true)
@Min(value = 1930, message = "year must not be less than 1930")
@Max(value = 2025, message = "year must not be less than 2025")
private int year;

@Schema(description = "출생 월", example = "3", required = true)
@Min(value = 1, message = "month must not be less than 1")
@Max(value = 12, message = "month must not be greater than 12")
private int month;

@Schema(description = "출생 일", example = "15", required = true)
@Min(value = 1, message = "day must not be less than 1")
@Max(value = 31, message = "day must not be greater than 31")
private int day;

@Schema(description = "출생 시", example = "10", required = true)
private int hour;

@Schema(description = "양력(solar)/음력(lunar)", example = "solar", required = true)
@Pattern(regexp = "solar|lunar", message = "solunar must be one of the following values: solar, lunar")
private String solunar;

@Schema(description = "윤달(0)/평달(1)", example = "0", required = true)
@Min(value = 0, message = "youn must not be less than 0")
@Max(value = 1, message = "youn must not be greater than 1")
private int youn;

@AssertTrue(message = "hour must be one of the following values: 0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22")
private boolean isValidHour() {
return List.of(0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22).contains(hour);
}
}
private FortuneApiRequest other;
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ private Map<String, CommonResponse> processApiResponse(ExternalInsaengResponse a
responseMap.put("currentgoodandbad",
buildSimpleResponse("현재의 길흉사", result.getCurrentGoodAndBadNews().getText()));
responseMap.put("eightstar", buildSimpleResponse("팔복궁", result.getEightStar()));
responseMap.put("pungsu", buildSimpleResponse("풍수로 보는 길흉", result.getGoodAndBadByPungsu()));
responseMap.put("goodandbadbypungsu", buildSimpleResponse("풍수로 보는 길흉", result.getGoodAndBadByPungsu()));
responseMap.put("soulmates", buildSimpleResponse("천생연분", result.getSoulMates().getText()));

return responseMap;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.palbang.unsemawang.common.util.pagination.CursorRequest;
import com.palbang.unsemawang.common.util.pagination.LongCursorResponse;
import com.palbang.unsemawang.community.constant.CommunityCategory;
import com.palbang.unsemawang.community.constant.Sortingtype;
import com.palbang.unsemawang.community.dto.response.PostListResponse;
import com.palbang.unsemawang.community.entity.Post;
import com.palbang.unsemawang.community.repository.PostRepository;
Expand Down Expand Up @@ -53,7 +54,8 @@ void testGetPostList_firstPage() {
.thenReturn(mockPosts);

// when
LongCursorResponse<PostListResponse> response = postListService.getPostList(category, cursorRequest);
LongCursorResponse<PostListResponse> response = postListService.getPostList(category, Sortingtype.LATEST,
cursorRequest);

// then
assertThat(response).isNotNull();
Expand All @@ -77,7 +79,8 @@ void testGetPostList_emptyResult() {
.thenReturn(mockPosts);

// when
LongCursorResponse<PostListResponse> response = postListService.getPostList(category, cursorRequest);
LongCursorResponse<PostListResponse> response = postListService.getPostList(category, Sortingtype.LATEST,
cursorRequest);

// then
assertThat(response).isNotNull();
Expand All @@ -100,7 +103,8 @@ void testGetPostList_nextPage() {
.thenReturn(mockPosts);

// when
LongCursorResponse<PostListResponse> response = postListService.getPostList(category, cursorRequest);
LongCursorResponse<PostListResponse> response = postListService.getPostList(category, Sortingtype.LATEST,
cursorRequest);

// then
assertThat(response).isNotNull();
Expand All @@ -111,6 +115,53 @@ void testGetPostList_nextPage() {
assertThat(response.nextCursorRequest().key()).isEqualTo(4L);
}

@Test
void testGetPostList_sortedByViewCount() {
// given
CommunityCategory category = CommunityCategory.FREE_BOARD;
CursorRequest<Long> cursorRequest = new CursorRequest<>(null, 3); // 첫 페이지 요청, 조회 크기 3

// Mock 반환 데이터 (Post 목록 생성 - 조회수 순서로 정렬되어야 함)
Post post1 = createPostWithViews(1L, "Title 1", true, 50); // 조회수 50
Post post2 = createPostWithViews(2L, "Title 2", true, 20); // 조회수 20
Post post3 = createPostWithViews(3L, "Title 3", true, 10); // 조회수 10
List<Post> mockPosts = List.of(post1, post2, post3);

when(postRepository.findMostViewedPostsByCategory(
category, null, PageRequest.of(0, 3)))
.thenReturn(mockPosts); // Mock 동작 정의

// when
LongCursorResponse<PostListResponse> response = postListService.getPostList(
category, Sortingtype.MOST_VIEWED, cursorRequest);

// then
assertThat(response).isNotNull();
assertThat(response.data()).hasSize(3);

assertThat(response.data().get(0).getId()).isEqualTo(1L); // ID 1, 조회수 50
assertThat(response.data().get(1).getId()).isEqualTo(2L); // ID 2, 조회수 20
assertThat(response.data().get(2).getId()).isEqualTo(3L); // ID 3, 조회수 10
assertThat(response.nextCursorRequest().key()).isEqualTo(3L); // 다음 커서는 마지막 항목의 ID
}

// 조회수를 포함한 Post 객체 생성 헬퍼 메서드
private Post createPostWithViews(Long id, String title, boolean isVisible, int viewCount) {
return Post.builder()
.id(id)
.title(title)
.isVisible(isVisible)
.content("Content for " + title)
.viewCount(viewCount) // 조회수 설정
.likeCount(0)
.commentCount(0)
.member(Member.builder()
.nickname("Nickname " + id)
.build())
.communityCategory(CommunityCategory.FREE_BOARD)
.build();
}

private Post createPost(Long id, String title, boolean isVisible) {
return Post.builder()
.id(id)
Expand Down
Loading