Skip to content
Open
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
@@ -1,22 +1,17 @@
package io.crops.warmletter.domain.share.controller;
import io.crops.warmletter.domain.share.dto.response.CursorResponse;
import io.crops.warmletter.domain.share.dto.response.SharePostDetailResponse;
import io.crops.warmletter.domain.share.dto.response.SharePostResponse;
import io.crops.warmletter.domain.share.service.SharePostService;
import io.crops.warmletter.global.response.BaseResponse;
import io.crops.warmletter.global.response.PageResponse;
import io.crops.warmletter.global.util.PageableConverter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;


@RestController
@RequestMapping("/api/share-posts")
@RequiredArgsConstructor
Expand All @@ -27,11 +22,12 @@ public class SharePostController {

@Operation(summary = "공유 게시글 목록 조회", description = "페이징 처리된 공유 게시글 목록을 조회합니다.")
@GetMapping()
public ResponseEntity<BaseResponse<PageResponse<SharePostResponse>>> getAllPosts(
@PageableDefault(size = 10, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable
public ResponseEntity<BaseResponse<CursorResponse<SharePostResponse>>> getAllPosts(
@RequestParam(required = false) Long cursorId,
@RequestParam(defaultValue = "10") int size
) {
return ResponseEntity.status(HttpStatus.OK)
.body(BaseResponse.of(new PageResponse<>(sharePostService.getAllPosts(PageableConverter.convertToPageable(pageable))), "공유 게시글 조회 성공"));
.body(BaseResponse.of(sharePostService.getAllPosts(cursorId, size), "공유 게시글 조회 성공"));
}

@Operation(summary = "공유 게시글 상세 조회", description = "특정 ID의 공유 게시글 상세 정보를 조회합니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.crops.warmletter.domain.share.dto.response;

import lombok.Getter;

import java.util.List;

@Getter
public class CursorResponse<T> {
private List<T> data;
private Long nextCursor;
private boolean hasNext;

public CursorResponse(List<T> data, Long nextCursor, boolean hasNext) {
this.data = data;
this.nextCursor = nextCursor;
this.hasNext = hasNext;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
@Getter
@Table(
indexes = {
@Index(name = "idx_sharepost_active_created", columnList = "isActive,createdAt")
@Index(name = "idx_sharepost_active_id", columnList = "isActive,id")
}
)
public class SharePost extends BaseEntity {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package io.crops.warmletter.domain.share.repository;
import io.crops.warmletter.domain.share.dto.response.SharePostResponse;
import io.crops.warmletter.domain.share.entity.SharePost;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;

public interface SharePostRepository extends JpaRepository<SharePost,Long>, CustomSharePostRepository {
Expand All @@ -15,14 +15,14 @@ public interface SharePostRepository extends JpaRepository<SharePost,Long>, Cust
"JOIN ShareProposal proposal ON sp.shareProposalId = proposal.id " +
"JOIN Member writer ON proposal.requesterId = writer.id " +
"JOIN Member recipient ON proposal.recipientId = recipient.id " +
"WHERE sp.isActive = true ")
Page<SharePostResponse> findAllActiveSharePostsWithZipCodes(Pageable pageable);
"WHERE sp.isActive = true " +
"AND (:cursorId IS NULL OR sp.id < :cursorId) " +
"ORDER BY sp.id DESC " +
"LIMIT :size")
List<SharePostResponse> findAllActiveSharePostsWithZipCodes(@Param("cursorId") Long cursorId, @Param("size") int size);

@Query("SELECT sp FROM SharePost sp " +
"JOIN ShareProposal proposal ON sp.shareProposalId = proposal.id " +
"WHERE sp.id = :sharePostId AND proposal.requesterId = :memberId")
Optional<SharePost> findByIdAndRequesterId(Long sharePostId, Long memberId);



}
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
package io.crops.warmletter.domain.share.service;
import io.crops.warmletter.domain.auth.facade.AuthFacade;
import io.crops.warmletter.domain.share.dto.response.CursorResponse;
import io.crops.warmletter.domain.share.dto.response.SharePostDetailResponse;
import io.crops.warmletter.domain.share.dto.response.SharePostResponse;
import io.crops.warmletter.domain.share.entity.SharePost;
import io.crops.warmletter.domain.share.exception.ShareAccessException;
import io.crops.warmletter.domain.share.exception.SharePageException;
import io.crops.warmletter.domain.share.exception.SharePostNotFoundException;
import io.crops.warmletter.domain.share.repository.SharePostRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
Expand All @@ -24,13 +22,17 @@ public class SharePostService {
private final AuthFacade authFacade;

@Transactional(readOnly = true)
public Page<SharePostResponse> getAllPosts(Pageable pageable) {
public CursorResponse<SharePostResponse> getAllPosts(Long cursorId, int size) {
List<SharePostResponse> responses = sharePostRepository.findAllActiveSharePostsWithZipCodes(cursorId, size+1);

if (pageable.getPageNumber() < 0) {
throw new SharePageException();
boolean hasNext = responses.size()>size;

if (hasNext) {
responses = responses.subList(0,size);
}
Long nextCursorId = hasNext && !responses.isEmpty() ? responses.get(responses.size() - 1).getSharePostId() : null;

return sharePostRepository.findAllActiveSharePostsWithZipCodes(pageable);
return new CursorResponse<>(responses, nextCursorId, hasNext);
}

@Transactional(readOnly = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
package io.crops.warmletter.domain.share.controller;
import io.crops.warmletter.domain.share.dto.response.CursorResponse;
import io.crops.warmletter.domain.share.dto.response.ShareLetterPostResponse;
import io.crops.warmletter.domain.share.dto.response.SharePostDetailResponse;
import io.crops.warmletter.domain.share.dto.response.SharePostResponse;
Expand All @@ -13,15 +14,13 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.data.domain.*;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import static org.hamcrest.Matchers.hasSize;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
Expand All @@ -43,73 +42,78 @@ class SharePostControllerTest {

@BeforeEach
void createSharePost() {
// 테스트에서 사용되는 값을 고정
LocalDateTime fixedCreatedAt = LocalDateTime.of(2025, 2, 28, 12, 0, 0, 0);

// SharePostResponse 객체를 새 생성자를 사용하여 생성
sharePostResponse1 = new SharePostResponse(1L, 1L, "12345", "67890", "to share my post", true, fixedCreatedAt);
sharePostResponse2 = new SharePostResponse(2L, 2L, "12345", "67890", "to share my post1", true, fixedCreatedAt);
}

@Test
@DisplayName("페이징된 공유 게시글 반환 ")
@DisplayName("커서 공유 게시글 반환 ")
void getAllPosts() throws Exception {
// given
Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.ASC, "createdAt"));
List<SharePostResponse> posts = List.of(sharePostResponse1, sharePostResponse2);
Page<SharePostResponse> postPage = new PageImpl<>(posts, pageable, posts.size());
when(sharePostService.getAllPosts(any(Pageable.class))).thenReturn(postPage);
CursorResponse<SharePostResponse> cursorResponse = new CursorResponse<>(posts, 2L, true);
when(sharePostService.getAllPosts(null, 10)).thenReturn(cursorResponse);

// when
mockMvc.perform(get("/api/share-posts")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.content", hasSize(2)))
.andExpect(jsonPath("$.data.content[0].content").value("to share my post"))
.andExpect(jsonPath("$.data.content[1].content").value("to share my post1"))
.andExpect(jsonPath("$.data.currentPage").value(1))
.andExpect(jsonPath("$.data.totalElements").value(2))
.andExpect(jsonPath("$.data.size").value(10))
.andExpect(jsonPath("$.data.data", hasSize(2)))
.andExpect(jsonPath("$.data.data[0].content").value("to share my post"))
.andExpect(jsonPath("$.data.data[1].content").value("to share my post1"))
.andExpect(jsonPath("$.data.nextCursor").value(2))
.andExpect(jsonPath("$.data.hasNext").value(true))
.andExpect(jsonPath("$.message").value("공유 게시글 조회 성공"))
.andDo(print());
}



@Test
@DisplayName("페이지 파라미터에 따라서 해당 페이지 반환 ")
@DisplayName("커서 ID 파라미터에 따라서 해당 페이지 반환 ")
void getAllPosts_ReturnsSpecificPage() throws Exception {
// given
Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "createdAt"));
Page<SharePostResponse> emptyPage = new PageImpl<>(Collections.emptyList(), pageable, 20);
List<SharePostResponse> posts = List.of(sharePostResponse2);
Long cursorId = 3L;
Long nextCursorId = 2L;
CursorResponse<SharePostResponse> cursorResponse = new CursorResponse<>(posts, nextCursorId, true);

when(sharePostService.getAllPosts(any(Pageable.class))).thenReturn(emptyPage);
when(sharePostService.getAllPosts(cursorId,10)).thenReturn(cursorResponse);

// when & then
mockMvc.perform(get("/api/share-posts")
.param("page", "1")
.param("cursorId", cursorId.toString())
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.content").isArray())
.andExpect(jsonPath("$.data.currentPage").value(1))
.andExpect(jsonPath("$.data.totalElements").value(20))
.andExpect(jsonPath("$.data.data").isArray())
.andExpect(jsonPath("$.data.data", hasSize(1)))
.andExpect(jsonPath("$.data.nextCursor").value(nextCursorId))
.andExpect(jsonPath("$.data.hasNext").value(true))
.andExpect(jsonPath("$.message").value("공유 게시글 조회 성공"))
.andDo(print());
}


@Test
@DisplayName("음수 페이지 요청시 예외 발생")
void getAllPosts_ThrowsException_WhenPageNumberIsNegative() throws Exception {
@DisplayName("마지막 페이지 조회 - 다음 페이지 없음")
void getAllPosts_LastPage() throws Exception {
// given
when(sharePostService.getAllPosts(any(Pageable.class)))
.thenThrow(new BusinessException(ErrorCode.INVALID_PAGE_REQUEST));
List<SharePostResponse> posts = List.of(sharePostResponse2);
Long cursorId = 3L;
CursorResponse<SharePostResponse> cursorResponse = new CursorResponse<>(posts, null, false);

when(sharePostService.getAllPosts(cursorId, 10)).thenReturn(cursorResponse);

// when & then
mockMvc.perform(get("/api/share-posts")
.param("page", "-1")
.param("cursorId", cursorId.toString())
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value(ErrorCode.INVALID_PAGE_REQUEST.getCode()))
.andExpect(jsonPath("$.message").value(ErrorCode.INVALID_PAGE_REQUEST.getMessage()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.data").isArray())
.andExpect(jsonPath("$.data.data", hasSize(1)))
.andExpect(jsonPath("$.data.nextCursor").isEmpty())
.andExpect(jsonPath("$.data.hasNext").value(false))
.andExpect(jsonPath("$.message").value("공유 게시글 조회 성공"))
.andDo(print());
}

Expand Down
Loading