Skip to content

Commit

Permalink
Merge pull request #30 from 2024-pre-onboarding-backend-F/feat/conten…
Browse files Browse the repository at this point in the history
…t_list_search

[feat] 게시물 목록 기능 구현
  • Loading branch information
K-0joo authored Aug 26, 2024
2 parents 1322c34 + b857c5a commit 5bd1017
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 3 deletions.
6 changes: 4 additions & 2 deletions src/main/java/wanted/media/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 엔티티입니다."),
ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 엔티티입니다."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다."),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증이 필요합니다."),
FORBIDDEN(HttpStatus.FORBIDDEN, "권한이 없습니다.");
// 클라이언트의 입력 값에 대한 일반적인 오류 (@PathVariable, @RequestParam가 잘못되었을 때)
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "클라이언트의 입력 값을 확인해주세요."),
INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."),

USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."),
INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다."),
VERIFICATION_CODE_MISMATCH(HttpStatus.BAD_REQUEST, "인증코드가 일치하지 않습니다."),
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/wanted/media/exception/PostListCustomException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package wanted.media.exception;

import lombok.Getter;

@Getter
public class PostListCustomException extends RuntimeException {
private final ErrorCode errorCode;

public PostListCustomException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import wanted.media.exception.PostListCustomException;
import wanted.media.exception.BaseException;
import wanted.media.exception.ErrorCode;
import wanted.media.exception.CustomException;
Expand All @@ -19,6 +20,20 @@ public ResponseEntity<ErrorResponse> handleBadRequestException(BadRequestExcepti
return ResponseEntity.badRequest()
.body(new ErrorResponse(400, e.getMessage()));
}

@ExceptionHandler(PostListCustomException.class)
public ResponseEntity<ErrorResponse> handlePostCustomException(PostListCustomException ex) {
ErrorCode errorCode = ex.getErrorCode();
ErrorResponse response = new ErrorResponse(errorCode);
return new ResponseEntity<>(response, errorCode.getStatus());
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
ErrorResponse response = new ErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR);
return new ResponseEntity<>(response, ErrorCode.INTERNAL_SERVER_ERROR.getStatus());
}

@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponse> handlePostNotFound(NotFoundException e) {
ErrorCode errorCode = e.getErrorCode();
Expand All @@ -28,16 +43,19 @@ public ResponseEntity<ErrorResponse> handlePostNotFound(NotFoundException e) {
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}

@ExceptionHandler(CustomException.class)
protected ResponseEntity<ErrorResponse> handleCustomException(final CustomException e) {
return ResponseEntity
.status(e.getErrorCode().getStatus().value())
.body(new ErrorResponse(e.getErrorCode().getStatus().value(), e.getCustomMessage()));
}

@ExceptionHandler(BaseException.class)
public ResponseEntity<ErrorResponse> handleBaseException(BaseException e) {
ErrorCode errorCode = e.getErrorCode();
return ResponseEntity.status(errorCode.getStatus())
.body(new ErrorResponse(errorCode.getStatus().value(), errorCode.getMessage()));

}
}
30 changes: 29 additions & 1 deletion src/main/java/wanted/media/post/controller/PostController.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
package wanted.media.post.controller;

import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import wanted.media.post.domain.Post;
import wanted.media.post.domain.Type;
import wanted.media.post.dto.PostDto;
import wanted.media.post.service.PostService;
import wanted.media.post.dto.PostDetailResponse;
import wanted.media.post.dto.PostIdResponse;
import wanted.media.post.service.PostService;

import java.util.List;
import java.util.stream.Collectors;
import java.util.HashMap;
import java.util.Map;

Expand Down Expand Up @@ -45,6 +52,27 @@ public ResponseEntity<PostDetailResponse> getPost(@PathVariable String postId) {
.build();
return ResponseEntity.ok(result);
}

@GetMapping
public ResponseEntity<List<PostDto>> list(@RequestParam(value = "hashtag", required = true) String account,
@RequestParam(value = "type", required = false) Type type,
@RequestParam(value = "orderBy", defaultValue = "createdAt") String orderBy,
@RequestParam(value = "sortDirection", defaultValue = "ASC") String sortDirection,
@RequestParam(value = "search_by", defaultValue = "title, content") String searchBy,
@RequestParam(value = "search", required = false) String search,
@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "page_count", defaultValue = "10") int pageCount) {
Page<Post> postPage = postService.findPosts(account, type, orderBy, sortDirection, searchBy, search, page, pageCount);
List<PostDto> postDtos = postPage.getContent().stream()
.map(PostDto::allPosts)
.collect(Collectors.toList());

if (postDtos.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}

return new ResponseEntity<>(postDtos, HttpStatus.OK);
}

@PostMapping("/likes/{postId}")
public ResponseEntity<?> getLikes(@PathVariable(name = "postId") String postId) {
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/wanted/media/post/dto/PostDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package wanted.media.post.dto;

import wanted.media.post.domain.Post;
import wanted.media.post.domain.Type;

import java.time.LocalDateTime;

public record PostDto(
String id,
Long likeCount,
Type type,
String title,
String content,
String hashtags,
Long viewCount,
Long shareCount,
LocalDateTime updatedAt,
LocalDateTime createdAt,
String account
) {
public static PostDto allPosts(Post post) {
return new PostDto(
post.getId(),
post.getLikeCount(),
post.getType(),
post.getTitle(),
post.getContent(),
post.getHashtags(),
post.getViewCount(),
post.getShareCount(),
post.getUpdatedAt(),
post.getCreatedAt(),
post.getUser().getAccount()
);
}
}
15 changes: 15 additions & 0 deletions src/main/java/wanted/media/post/repository/PostRepository.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
package wanted.media.post.repository;

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 wanted.media.post.domain.Post;
import wanted.media.post.domain.Type;

public interface PostRepository extends JpaRepository<Post, String> {
@Query("SELECT p FROM Post p " +
"WHERE p.user.account = :account " +
"AND (:type IS NULL OR p.type = :type) " +
"AND ((:searchBy = 'title' AND LOWER(p.title) LIKE LOWER(CONCAT('%', :search, '%'))) " +
"OR (:searchBy = 'content' AND LOWER(p.content) LIKE LOWER(CONCAT('%', :search, '%'))) " +
"OR (:searchBy = 'title,content' AND (LOWER(p.title) LIKE LOWER(CONCAT('%', :search, '%')) " +
"OR LOWER(p.content) LIKE LOWER(CONCAT('%', :search, '%')))))")
Page<Post> findBySearchContaining(@Param("account") String account, @Param("type") Type type,
@Param("searchBy") String searchBy, @Param("search") String search,
Pageable pageable);
}
17 changes: 17 additions & 0 deletions src/main/java/wanted/media/post/service/PostService.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
package wanted.media.post.service;

import lombok.RequiredArgsConstructor;

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

import wanted.media.exception.CustomException;
import wanted.media.exception.ErrorCode;
import wanted.media.exception.NotFoundException;
import wanted.media.post.domain.Post;
import wanted.media.post.domain.Type;
import wanted.media.post.repository.PostRepository;

@Service
@RequiredArgsConstructor
public class PostService {
private final PostRepository postRepository;

@Transactional(readOnly = true)
public Page<Post> findPosts(String account, Type type, String orderBy, String sortDirection, String searchBy, String search, int page, int pageCount) {
Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), orderBy);
Pageable pageable = PageRequest.of(page, pageCount, sort);

Page<Post> posts = postRepository.findBySearchContaining(account, type, searchBy, search, pageable);

return posts;
}

@Transactional
public Post getPost(String postId) {
Expand Down
94 changes: 94 additions & 0 deletions src/test/java/wanted/media/post/controller/PostControllerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package wanted.media.post.controller;

import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@RequiredArgsConstructor
@TestPropertySource(locations = "classpath:application-test.yml")
class PostControllerTest {
private MockMvc mockMvc;

@Test
void posts_list_성공() throws Exception {
mockMvc.perform(get("/api/posts")
.param("hashtag", "wanted")
.param("type", "FACEBOOK")
.param("orderBy", "createdAt")
.param("sortDirection", "ASC")
.param("search_by", "title")
.param("search", "판교")
.param("page", "0")
.param("page_count", "10")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isNotEmpty()); // 응답이 비어있지 않음을 확인
}

@Test
void search_by_성공() throws Exception {
mockMvc.perform(get("/api/posts")
.param("hashtag", "wanted")
.param("type", "FACEBOOK")
.param("orderBy", "createdAt")
.param("sortDirection", "ASC")
.param("search_by", "invalidSearchBy")
.param("page", "0")
.param("page_count", "10")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest()); // 잘못된 검색 조건에 대한 에러 확인
}

@Test
void 유효하지_않은_page값() throws Exception {
mockMvc.perform(get("/api/posts")
.param("hashtag", "wanted")
.param("type", "FACEBOOK")
.param("orderBy", "createdAt")
.param("sortDirection", "ASC")
.param("search_by", "title")
.param("search", "판교")
.param("page", "-1") // 유효하지 않은 페이지 번호
.param("page_count", "10")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest()); // 페이지 값이 유효하지 않음을 확인
}

@Test
void 유효하지_않은_page_count_값() throws Exception {
mockMvc.perform(get("/api/posts")
.param("hashtag", "wanted")
.param("type", "FACEBOOK")
.param("orderBy", "createdAt")
.param("sortDirection", "ASC")
.param("search_by", "title")
.param("search", "판교")
.param("page", "0")
.param("page_count", "-10") // 유효하지 않은 페이지 수
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest()); // 페이지 수가 유효하지 않음을 확인
}

@Test
void search_키워드가_없을_때() throws Exception {
mockMvc.perform(get("/api/posts")
.param("hashtag", "wanted")
.param("type", "FACEBOOK")
.param("orderBy", "createdAt")
.param("sortDirection", "ASC")
.param("search_by", "title")
.param("page", "0")
.param("page_count", "10")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isNotEmpty()); // 응답이 비어있지 않음을 확인
}
}

0 comments on commit 5bd1017

Please sign in to comment.