From 5e163496d833b406a0f52589fb54e519efc6b18b Mon Sep 17 00:00:00 2001 From: ssunnykku <108388578+ssunnykku@users.noreply.github.com> Date: Mon, 26 Aug 2024 17:00:28 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20post=20=EC=83=81=EC=84=B8=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20api=20(#27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: post 상세보기 api * refactor: 테스트용 security 설정 * refactor: review 반영 - 공백 제거 - dto record로 변경 - DetailResponse 반환 코드 PostService -> PostController 수정 작성 - .gitignore 수정 - PostNotFoundException -> NotFoundException * refactor: repository test code 삭제 * refactor: Entity, DTO Colum post -> content 변경 * fix: conflict 해결 --- .gitignore | 2 + .../media/exception/NotFoundException.java | 10 +++ .../handler/GlobalExceptionHandler.java | 18 +++++- .../media/post/controller/PostController.java | 37 ++++++++++- .../java/wanted/media/post/domain/Post.java | 38 ++++++----- .../media/post/dto/PostDetailResponse.java | 25 ++++++++ .../media/post/repository/PostRepository.java | 7 +++ .../media/post/service/PostService.java | 17 +++++ .../media/user/config/SecurityConfig.java | 2 - .../java/wanted/media/user/domain/Code.java | 1 - .../java/wanted/media/user/domain/Token.java | 1 - src/main/resources/application-dev.yml | 21 +++++++ .../media/post/service/PostServiceTest.java | 63 +++++++++++++++++++ 13 files changed, 214 insertions(+), 28 deletions(-) create mode 100644 src/main/java/wanted/media/exception/NotFoundException.java create mode 100644 src/main/java/wanted/media/post/dto/PostDetailResponse.java create mode 100644 src/main/java/wanted/media/post/repository/PostRepository.java create mode 100644 src/main/resources/application-dev.yml create mode 100644 src/test/java/wanted/media/post/service/PostServiceTest.java diff --git a/.gitignore b/.gitignore index 02485a7..d9327e5 100644 --- a/.gitignore +++ b/.gitignore @@ -267,4 +267,6 @@ gradle-app.setting # End of https://www.toptal.com/developers/gitignore/api/macos,intellij,windows,java,gradle src/main/resources/application-secret.yml +src/main/resources/application-test.yml src/main/generated + diff --git a/src/main/java/wanted/media/exception/NotFoundException.java b/src/main/java/wanted/media/exception/NotFoundException.java new file mode 100644 index 0000000..1d1fd12 --- /dev/null +++ b/src/main/java/wanted/media/exception/NotFoundException.java @@ -0,0 +1,10 @@ +package wanted.media.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class NotFoundException extends RuntimeException { + private final ErrorCode errorCode; +} \ No newline at end of file diff --git a/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java b/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java index 9bc4019..f341fdd 100644 --- a/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java @@ -1,11 +1,13 @@ package wanted.media.exception.handler; +import org.apache.coyote.BadRequestException; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; - -import wanted.media.exception.BadRequestException; +import wanted.media.exception.ErrorCode; import wanted.media.exception.ErrorResponse; +import wanted.media.exception.NotFoundException; @RestControllerAdvice public class GlobalExceptionHandler { @@ -13,6 +15,16 @@ public class GlobalExceptionHandler { @ExceptionHandler(BadRequestException.class) public ResponseEntity handleBadRequestException(BadRequestException e) { return ResponseEntity.badRequest() - .body(new ErrorResponse(400, e.getErrorCode().getMessage())); + .body(new ErrorResponse(400, e.getMessage())); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handlePostNotFound(NotFoundException e) { + ErrorCode errorCode = e.getErrorCode(); + ErrorResponse errorResponse = new ErrorResponse( + errorCode.getStatus().value(), + errorCode.getMessage() + ); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); } } diff --git a/src/main/java/wanted/media/post/controller/PostController.java b/src/main/java/wanted/media/post/controller/PostController.java index 97212e6..66913de 100644 --- a/src/main/java/wanted/media/post/controller/PostController.java +++ b/src/main/java/wanted/media/post/controller/PostController.java @@ -1,9 +1,44 @@ package wanted.media.post.controller; +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.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import wanted.media.post.domain.Post; +import wanted.media.post.dto.PostDetailResponse; +import wanted.media.post.service.PostService; @RestController -@RequestMapping("/posts") +@RequestMapping("/api/posts") +@RequiredArgsConstructor public class PostController { + + private final PostService posetService; + + /** + * @param postId + * @return PostDetailResponse + */ + @GetMapping("/{postId}") + public ResponseEntity getPost(@PathVariable String postId) { + Post post = posetService.getPost(postId); + PostDetailResponse result = PostDetailResponse.builder() + .postId(post.getId()) + .likeCount(post.getLikeCount()) + .type(post.getType()) + .title(post.getTitle()) + .content(post.getContent()) + .hashtags(post.getHashtags()) + .viewCount(post.getViewCount()) + .shareCount(post.getShareCount()) + .updatedAt(post.getUpdatedAt()) + .createdAt(post.getCreatedAt()) + .userId(post.getUser().getUserId()) + .account(post.getUser().getAccount()) + .email(post.getUser().getEmail()) + .build(); + return ResponseEntity.ok(result); + } } diff --git a/src/main/java/wanted/media/post/domain/Post.java b/src/main/java/wanted/media/post/domain/Post.java index 0e1d996..617544b 100644 --- a/src/main/java/wanted/media/post/domain/Post.java +++ b/src/main/java/wanted/media/post/domain/Post.java @@ -1,32 +1,22 @@ package wanted.media.post.domain; -import java.time.LocalDateTime; - +import jakarta.persistence.*; +import jakarta.validation.constraints.Size; +import lombok.*; import org.hibernate.annotations.ColumnDefault; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import jakarta.validation.constraints.Size; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; import wanted.media.user.domain.User; +import java.time.LocalDateTime; + @Entity @Getter @Table(name = "posts") @NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder @EntityListeners(AuditingEntityListener.class) public class Post { @Id @@ -40,6 +30,7 @@ public class Post { @Size(max = 150) @Column(nullable = false) private String title; + private String content; private String hashtags; @@ -52,13 +43,20 @@ public class Post { @ColumnDefault("0") private Long shareCount; - @CreatedDate - private LocalDateTime createdAt; - @LastModifiedDate private LocalDateTime updatedAt; + @CreatedDate + private LocalDateTime createdAt; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user; + + public void incrementViewCount() { + if (this.viewCount == null) { + this.viewCount = 0L; + } + this.viewCount += 1; + } } diff --git a/src/main/java/wanted/media/post/dto/PostDetailResponse.java b/src/main/java/wanted/media/post/dto/PostDetailResponse.java new file mode 100644 index 0000000..69b9217 --- /dev/null +++ b/src/main/java/wanted/media/post/dto/PostDetailResponse.java @@ -0,0 +1,25 @@ +package wanted.media.post.dto; + +import lombok.Builder; +import wanted.media.post.domain.Type; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Builder +public record PostDetailResponse( + String postId, + Type type, + String title, + String content, + String hashtags, + Long likeCount, + Long viewCount, + Long shareCount, + LocalDateTime updatedAt, + LocalDateTime createdAt, + UUID userId, + String account, + String email +) { +} diff --git a/src/main/java/wanted/media/post/repository/PostRepository.java b/src/main/java/wanted/media/post/repository/PostRepository.java new file mode 100644 index 0000000..fb74f17 --- /dev/null +++ b/src/main/java/wanted/media/post/repository/PostRepository.java @@ -0,0 +1,7 @@ +package wanted.media.post.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import wanted.media.post.domain.Post; + +public interface PostRepository extends JpaRepository { +} diff --git a/src/main/java/wanted/media/post/service/PostService.java b/src/main/java/wanted/media/post/service/PostService.java index cb52ee0..aa1ecb5 100644 --- a/src/main/java/wanted/media/post/service/PostService.java +++ b/src/main/java/wanted/media/post/service/PostService.java @@ -1,7 +1,24 @@ package wanted.media.post.service; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import wanted.media.exception.ErrorCode; +import wanted.media.exception.NotFoundException; +import wanted.media.post.domain.Post; +import wanted.media.post.repository.PostRepository; @Service +@RequiredArgsConstructor public class PostService { + private final PostRepository postRepository; + + @Transactional + public Post getPost(String postId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new NotFoundException(ErrorCode.ENTITY_NOT_FOUND)); + + post.incrementViewCount(); + return post; + } } diff --git a/src/main/java/wanted/media/user/config/SecurityConfig.java b/src/main/java/wanted/media/user/config/SecurityConfig.java index afbbcf7..320ed5b 100644 --- a/src/main/java/wanted/media/user/config/SecurityConfig.java +++ b/src/main/java/wanted/media/user/config/SecurityConfig.java @@ -2,10 +2,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; @EnableWebSecurity @Configuration diff --git a/src/main/java/wanted/media/user/domain/Code.java b/src/main/java/wanted/media/user/domain/Code.java index 93240bc..c6167c8 100644 --- a/src/main/java/wanted/media/user/domain/Code.java +++ b/src/main/java/wanted/media/user/domain/Code.java @@ -14,7 +14,6 @@ @Entity @Table(name = "codes") public class Code { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(nullable = false) diff --git a/src/main/java/wanted/media/user/domain/Token.java b/src/main/java/wanted/media/user/domain/Token.java index 81fc2f5..e14e5ed 100644 --- a/src/main/java/wanted/media/user/domain/Token.java +++ b/src/main/java/wanted/media/user/domain/Token.java @@ -11,7 +11,6 @@ @Entity @Table(name = "tokens") public class Token { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(nullable = false) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..9dc08f0 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,21 @@ +# application-test +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:db;MODE=MYSQL + username: sa + password: + h2: + console: + enabled: true + path: /h2-console + jpa: + show-sql: true + properties: + hibernate: + format_sql: true + hibernate: + ddl-auto: update + +jwt: + secret_key: key diff --git a/src/test/java/wanted/media/post/service/PostServiceTest.java b/src/test/java/wanted/media/post/service/PostServiceTest.java new file mode 100644 index 0000000..ea8295a --- /dev/null +++ b/src/test/java/wanted/media/post/service/PostServiceTest.java @@ -0,0 +1,63 @@ +package wanted.media.post.service; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import wanted.media.post.domain.Post; +import wanted.media.post.domain.Type; +import wanted.media.post.repository.PostRepository; +import wanted.media.user.domain.Grade; +import wanted.media.user.domain.User; +import wanted.media.user.repository.UserRepository; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ActiveProfiles("test") +class PostServiceTest { + @Autowired + private PostService postService; + + @Autowired + private UserRepository userRepository; + + @Autowired + private PostRepository postRepository; + + @Test + @Transactional + void getPostTest() { + // given + User user = User.builder() + .account("sun") + .email("sun@gmail.com") + .password("1234") + .grade(Grade.NORMAL_USER) + .build(); + + userRepository.save(user); + + Post post = Post.builder() + .id("qwer") + .type(Type.TWITTER) + .title("제목 입력") + .content("내용 입력") + .user(user) + .viewCount(100L) + .build(); + + postRepository.save(post); + + // when + Post getData = postService.getPost(post.getId()); + + // then + assertThat(getData.getTitle()).isEqualTo("제목 입력"); + assertThat(getData.getContent()).isEqualTo("내용 입력"); + assertThat(getData.getViewCount()).isEqualTo(101); + assertThat(getData.getUser().getAccount()).isEqualTo("sun"); + assertThat(getData.getUser().getEmail()).isEqualTo("sun@gmail.com"); + } +} \ No newline at end of file