Skip to content

Commit

Permalink
Merge pull request #30 from prgrms-web-devcourse-final-project/featur…
Browse files Browse the repository at this point in the history
…e/read-share-board(WR9-25)

Feature/read share board(wr9 25)
  • Loading branch information
HeoJeongHyeon authored Feb 19, 2025
2 parents 7cf999a + 3792e81 commit 2b194f3
Show file tree
Hide file tree
Showing 14 changed files with 550 additions and 0 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ def excludePatterns = [
'**/*Application.class',
'**/dto/**',
'**/global/**',
'**/repository/**',

]

jacocoTestReport {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.crops.warmletter.domain.share.controller;

import io.crops.warmletter.domain.share.dto.response.SharePostResponse;
import io.crops.warmletter.domain.share.service.SharePostService;
import io.crops.warmletter.global.response.PageResponse;
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.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class SharePostController {

private final SharePostService sharePostService;

@GetMapping("/share-posts")
// @PreAuthorize("hasRole('USER')")
public ResponseEntity<PageResponse<SharePostResponse>> getAllPosts(
@PageableDefault(size = 10, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable
// @AuthenticationPrincipal UserDetails userDetails
) {
return ResponseEntity.ok(new PageResponse<>(sharePostService.getAllPosts(pageable)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.crops.warmletter.domain.share.dto.response;
import io.crops.warmletter.domain.share.entity.SharePost;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
public class SharePostResponse {

private Long shareProposalId;
private String content;
private boolean isActive;
private LocalDateTime createdAt;

public SharePostResponse(SharePost sharePost) {
this.shareProposalId = sharePost.getShareProposalId();
this.content = sharePost.getContent();
this.isActive = sharePost.isActive();
this.createdAt = sharePost.getCreatedAt();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.crops.warmletter.domain.share.entity;

import io.crops.warmletter.global.entity.BaseEntity;
import io.crops.warmletter.global.entity.BaseTimeEntity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;

import java.time.LocalDateTime;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class SharePost extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private Long shareProposalId;

private boolean isActive;

@Column(nullable = false)
private String content;

@Builder
public SharePost(Long shareProposalId, String title, String content,boolean isActive) {
this.shareProposalId = shareProposalId;
this.content = content;
this.isActive = isActive;
}

// 비즈니스 로직 메서드
public void activate() {
this.isActive = true;
}

public void deactivate() {
this.isActive = false;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.crops.warmletter.domain.share.entity;

import io.crops.warmletter.global.entity.BaseEntity;
import io.crops.warmletter.global.entity.BaseTimeEntity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;

import java.time.LocalDateTime;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class SharePostLike extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private Long sharePostId;

@Column(nullable = false)
private Long memberId;

private LocalDateTime createdAt;

private LocalDateTime updatedAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.crops.warmletter.domain.share.entity;

import io.crops.warmletter.domain.share.enums.ProposalStatus;
import io.crops.warmletter.global.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

import java.time.LocalDateTime;

@Entity
@Getter
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class ShareProposal extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private Long requesterId;

@Column(nullable = false)
private Long recipientId;

@Enumerated(EnumType.STRING)
private ProposalStatus status;

@Column(nullable = false)
private String message;

private LocalDateTime createdAt;

private LocalDateTime updatedAt;


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.crops.warmletter.domain.share.entity;

import io.crops.warmletter.global.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ShareProposalLetter extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private Long proposalId;

@Column(nullable = false)
private long letterId;

@Column(nullable = false)
private int displayOrder;

private LocalDateTime createdAt;




}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.crops.warmletter.domain.share.enums;

public enum ProposalStatus {

PENDING,APPROVED,REJECTED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.crops.warmletter.domain.share.repository;

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;

public interface ShareRepository extends JpaRepository<SharePost,Long> {

Page<SharePost> findAllByIsActiveTrue(Pageable pageable);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.crops.warmletter.domain.share.service;

import io.crops.warmletter.domain.share.dto.response.SharePostResponse;
import io.crops.warmletter.domain.share.entity.SharePost;
import io.crops.warmletter.domain.share.repository.ShareRepository;
import io.crops.warmletter.global.error.common.ErrorCode;
import io.crops.warmletter.global.error.exception.BusinessException;
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;

@Service
@RequiredArgsConstructor
public class SharePostService {

private final ShareRepository shareRepository;

@Transactional(readOnly = true)
public Page<SharePostResponse> getAllPosts(Pageable pageable) {
// 0보다 작은 페이지를 요청한 경우
if (pageable.getPageNumber() < 0) {
throw new BusinessException(ErrorCode.INVALID_PAGE_REQUEST);
}
Page<SharePost> sharePosts = shareRepository.findAllByIsActiveTrue(pageable);
return sharePosts.map(SharePostResponse::new);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ public enum ErrorCode {
INVALID_INPUT_VALUE("COM-001", HttpStatus.BAD_REQUEST, "잘못된 입력값입니다."),
INTERNAL_SERVER_ERROR("COM-002", HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다."),

// 페이징 관련 에러 코드
INVALID_PAGE_REQUEST("PAGE-001", HttpStatus.BAD_REQUEST, "요청페이지 번호가 0보다 작습니다."),

//금치어
DUPLICATE_BANNED_WORD("MOD-001", HttpStatus.CONFLICT, "이미 등록된 금칙어입니다."),

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package io.crops.warmletter.domain.share.controller;

import io.crops.warmletter.domain.share.dto.response.SharePostResponse;
import io.crops.warmletter.domain.share.entity.SharePost;
import io.crops.warmletter.domain.share.service.SharePostService;
import io.crops.warmletter.global.error.common.ErrorCode;
import io.crops.warmletter.global.error.exception.BusinessException;
import io.crops.warmletter.global.error.handler.GlobalExceptionHandler;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
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.util.Collections;
import java.util.List;

import static org.hamcrest.Matchers.hasSize;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest({SharePostController.class, GlobalExceptionHandler.class})
@AutoConfigureMockMvc(addFilters = false)
class SharePostControllerTest {

@Autowired
private MockMvc mockMvc;

@MockitoBean
private SharePostService sharePostService;


private SharePostResponse sharePostResponse1;
private SharePostResponse sharePostResponse2;
@BeforeEach
void createSharePost() {
SharePost sharePost = new SharePost(1L, "게시글1", "to share my post",true);
SharePost sharePost1 = new SharePost(2L, "게시글2", "to share my post1",true);
sharePostResponse1 = new SharePostResponse(sharePost);
sharePostResponse2 = new SharePostResponse(sharePost1);

}

@Test
@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);

//when
mockMvc.perform(get("/api/share-posts")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content",hasSize(2)))
.andExpect(jsonPath("$.content[0].content").value("to share my post"))
.andExpect(jsonPath("$.content[1].content").value("to share my post1"))
.andExpect(jsonPath("$.currentPage").value(1))
.andExpect(jsonPath("$.totalElements").value(2))
.andExpect(jsonPath("$.size").value(10))
.andDo(print());
}

@Test
@DisplayName("페이지 파라미터에 따라서 해당 페이지 반환 ")
void getAllPosts_ReturnsSpecificPage() throws Exception {
// given
Pageable pageable = PageRequest.of(1, 10, Sort.by(Sort.Direction.DESC, "createdAt"));
Page<SharePostResponse> emptyPage = new PageImpl<>(Collections.emptyList(), pageable, 20);

when(sharePostService.getAllPosts(any(Pageable.class))).thenReturn(emptyPage);

// when & then
mockMvc.perform(get("/api/share-posts")
.param("page", "1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.currentPage").value(2))
.andExpect(jsonPath("$.totalElements").value(20))
.andDo(print());
}

@Test
@DisplayName("음수 페이지 요청시 예외 발생")
void getAllPosts_ThrowsException_WhenPageNumberIsNegative() throws Exception {
// given
when(sharePostService.getAllPosts(any(Pageable.class)))
.thenThrow(new BusinessException(ErrorCode.INVALID_PAGE_REQUEST));

// when & then
mockMvc.perform(get("/api/share-posts")
.param("page", "-1")
.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()))
.andDo(print());
}

}
Loading

0 comments on commit 2b194f3

Please sign in to comment.