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] 게시물 목록 기능 구현 #30

Merged
merged 13 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from 10 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
36 changes: 36 additions & 0 deletions src/main/java/wanted/media/content/service/PostService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package wanted.media.content.service;

import org.springframework.beans.factory.annotation.Autowired;
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.content.domain.Post;
import wanted.media.content.domain.Type;
import wanted.media.content.repository.PostRepository;
import wanted.media.user.repository.UserRepository;

@Service
public class PostService {
@Autowired
Copy link
Contributor

Choose a reason for hiding this comment

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

@Autowired 빼고 @Service 어노테이션 밑에 @RequiredArgsConstructor 사용해주시면 더 깔끔할 것 같슴당

private UserRepository userRepository;

@Autowired
private 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);

// if (search == null || search.isEmpty()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

주석처리한 로직은 사용하지 않으시는건가요?

// return postRepository.findAll();
// //throw new IllegalStateException("해당하는 태그를 찾을 수 없습니다.");
// }

return postRepository.findBySearchContaining(account, type, searchBy, search, pageable);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

불필요한 공백 제거해주세요~

}
13 changes: 13 additions & 0 deletions src/main/java/wanted/media/exception/CustomException.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 CustomException extends RuntimeException {
Copy link
Contributor

Choose a reason for hiding this comment

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

CustomException이라는 클래스명은 어떤 Exception인지에 대해 알 수 가 없어서 어떤 Exception인지에 대한 이름을 작성하시는게 좋습니다!

private final ErrorCode errorCode;

public CustomException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
13 changes: 8 additions & 5 deletions src/main/java/wanted/media/exception/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package wanted.media.exception;

import org.springframework.http.HttpStatus;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@Getter
@RequiredArgsConstructor
public enum ErrorCode {
ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 엔티티입니다.");
ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 엔티티입니다."),
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "클라이언트의 입력 값을 확인해주세요."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다."),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증이 필요합니다."),
FORBIDDEN(HttpStatus.FORBIDDEN, "권한이 없습니다.");

private final HttpStatus status;
private final String message;
private final HttpStatus status;
private final String message;
}
10 changes: 8 additions & 2 deletions src/main/java/wanted/media/exception/ErrorResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class ErrorResponse {

private final int statusCode;
private final String message;
private final int statusCode;
private final String message;

public ErrorResponse(ErrorCode errorCode) {
this.statusCode = errorCode.getStatus().value();
this.message = errorCode.getMessage();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,29 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import wanted.media.exception.CustomException;
import wanted.media.exception.ErrorCode;
import wanted.media.exception.ErrorResponse;

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ErrorResponse> handleBadRequestException(BadRequestException e) {
return ResponseEntity.badRequest()
.body(new ErrorResponse(400, e.getMessage()));
}
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ErrorResponse> handleBadRequestException(BadRequestException e) {
return ResponseEntity.badRequest()
.body(new ErrorResponse(400, e.getMessage()));
}

@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResponse> handleCustomException(CustomException 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());
}
}
35 changes: 34 additions & 1 deletion src/main/java/wanted/media/post/controller/PostController.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,42 @@
package wanted.media.post.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.GetMapping;
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 java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/posts")
@RequestMapping("/api/posts")
public class PostController {
@Autowired
private PostService postService;

@GetMapping
public 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());

//System.out.println("PostDtos: " + postDtos);
Copy link
Contributor

Choose a reason for hiding this comment

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

사용하지 않는 로직은 제거해서 올려주시면 됩니당 41라인 공백도 제거해 주세요~!


return postDtos;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

헉 보내고 나니 ResponseEntity로 구현하는게 기억나서..나중에 다시 코드 리뷰 받고 refactor로 다시 고치겠습니다 죄송합니다 😭

}

}
44 changes: 44 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,44 @@
package wanted.media.post.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import wanted.media.post.domain.Post;
import wanted.media.post.domain.Type;

import java.time.LocalDateTime;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class PostDto {
Copy link
Contributor

Choose a reason for hiding this comment

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

record 클래스로 바꾸시면 더 깔끔할 것 같습니다!

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()
        );
    }
}

private String id;
private Long likeCount;
private Type type;
private String title;
private String content;
private String hashtags;
private Long viewCount;
private Long shareCount;
private LocalDateTime updatedAt;
private LocalDateTime createdAt;
private 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()
);
}
}
22 changes: 22 additions & 0 deletions src/main/java/wanted/media/post/repository/PostRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +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, Long> {
@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);
}
31 changes: 31 additions & 0 deletions src/main/java/wanted/media/post/service/PostService.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,38 @@
package wanted.media.post.service;

import org.springframework.beans.factory.annotation.Autowired;
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.post.domain.Post;
import wanted.media.post.domain.Type;
import wanted.media.post.repository.PostRepository;
import wanted.media.user.repository.UserRepository;

@Service
public class PostService {
@Autowired
Copy link
Contributor

@pie2457 pie2457 Aug 26, 2024

Choose a reason for hiding this comment

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

여기도 @Autowired 를 제거하고 @RequiredArgsConstructor를 달아주시면 좋을 것 같습니당

private UserRepository userRepository;

@Autowired
private PostRepository postRepository;
Copy link
Contributor

Choose a reason for hiding this comment

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

final을 붙여주셔야 @RequiredArgsConstructor가 동작하는 걸로 알고 있어용


@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);
if (posts.isEmpty()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

해당 게시물이 없으면 에러를 던지는게 아니라 그냥 빈 Post를 반환하는것도 좋은거 같은데 어떻게 생각하세용?

throw new CustomException(ErrorCode.ENTITY_NOT_FOUND);
Copy link
Contributor

Choose a reason for hiding this comment

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

여기는 CustomException보단 NotFoundException을 사용하시는게 맞는것같아요
35라인에 불필요한 공백 제거해주세요!

}

return posts;
}
}
19 changes: 19 additions & 0 deletions src/main/resources/data.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
INSERT INTO users(account, email, password, grade)
VALUES ('wanted', '[email protected]', 'wanted', 'orange');

INSERT INTO contents(like_count, type, title, content, hashtags, view_count, share_count, updated_at, created_at,
user_id)
VALUES (5, 'FACEBOOK', '판교 나들이', '오늘은 판교에 와보았어요. 미래의 직장이 될 곳들이 많이...', 'wanted', 54, 30, '2024-08-23', '2024-08-20',
'wanted');
INSERT INTO contents(like_count, type, title, content, hashtags, view_count, share_count, updated_at, created_at,
user_id)
VALUES (15, 'TWITTER', '판교 나들이', '오늘은 판교에 와보았어요. 미래의 직장이 될 곳들이 많이...', 'wanted', 154, 10, '2024-08-23', '2024-08-20',
'wanted');
INSERT INTO contents(like_count, type, title, content, hashtags, view_count, share_count, updated_at, created_at,
user_id)
VALUES (1, 'THREADS', '판교 나들이', '오늘은 판교에 와보았어요. 미래의 직장이 될 곳들이 많이...', 'wanted', 43, 2, '2024-08-23', '2024-08-20',
'wanted');
INSERT INTO contents(like_count, type, title, content, hashtags, view_count, share_count, updated_at, created_at,
user_id)
VALUES (30, 'INSTAGRAM', '판교 나들이', '오늘은 판교에 와보았어요. 미래의 직장이 될 곳들이 많이...', 'wanted', 24, 7, '2024-08-23', '2024-08-20',
'wanted');
Copy link
Contributor

Choose a reason for hiding this comment

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

EOL이 안지켜졌네요 !

67 changes: 67 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,67 @@
package wanted.media.post.controller;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.TestPropertySource;
import wanted.media.post.dto.PostDto;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@TestPropertySource(locations = "classpath:application-test.properties")
class PostControllerTest {
@Autowired
private TestRestTemplate restTemplate;

@Test
public void posts_list_성공() {
// When
String url = "/api/posts?hashtag=wanted&type=FACEBOOK&orderBy=createdAt&sortDirection=ASC&search_by=title&search=판교&page=0&page_count=10";
ResponseEntity<List> responseEntity = restTemplate.getForEntity(url, List.class);

// Then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
List<PostDto> posts = responseEntity.getBody();
assertThat(posts).isNotEmpty();
}

@Test
public void search_by_성공() {
String url = "/api/posts?hashtag=wanted&type=FACEBOOK&orderBy=createdAt&sortDirection=ASC&search_by=invalidSearchBy&page=0&page_count=10";
ResponseEntity<List> responseEntity = restTemplate.getForEntity(url, List.class);

assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}

@Test
public void 유효하지_않은_page값() {
String url = "/api/posts?hashtag=wanted&type=FACEBOOK&orderBy=createdAt&sortDirection=ASC&search_by=title&search=판교&page=-1&page_count=10";
ResponseEntity<List> responseEntity = restTemplate.getForEntity(url, List.class);

assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}

@Test
public void 유효하지_않은_page_count_값() {
String url = "/api/posts?hashtag=wanted&type=FACEBOOK&orderBy=createdAt&sortDirection=ASC&search_by=title&search=판교&page=0&page_count=-10";
ResponseEntity<List> responseEntity = restTemplate.getForEntity(url, List.class);

assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}

@Test
public void search_키워드가_없을_때() {
String url = "/api/posts?hashtag=wanted&type=FACEBOOK&orderBy=createdAt&sortDirection=ASC&search_by=title&page=0&page_count=10";
ResponseEntity<List> responseEntity = restTemplate.getForEntity(url, List.class);

assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
List<PostDto> posts = responseEntity.getBody();
assertThat(posts).isNotEmpty();
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

여기도 EOL이 안지켜졌네요!