Skip to content

Commit

Permalink
[feat] post 상세보기 api (#27)
Browse files Browse the repository at this point in the history
* 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 해결
  • Loading branch information
ssunnykku authored Aug 26, 2024
1 parent 8996a0b commit 5e16349
Show file tree
Hide file tree
Showing 13 changed files with 214 additions and 28 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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

10 changes: 10 additions & 0 deletions src/main/java/wanted/media/exception/NotFoundException.java
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
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 {

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

@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponse> handlePostNotFound(NotFoundException e) {
ErrorCode errorCode = e.getErrorCode();
ErrorResponse errorResponse = new ErrorResponse(
errorCode.getStatus().value(),
errorCode.getMessage()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}
}
37 changes: 36 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,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<PostDetailResponse> 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);
}
}
38 changes: 18 additions & 20 deletions src/main/java/wanted/media/post/domain/Post.java
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -40,6 +30,7 @@ public class Post {
@Size(max = 150)
@Column(nullable = false)
private String title;

private String content;
private String hashtags;

Expand All @@ -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;
}
}
25 changes: 25 additions & 0 deletions src/main/java/wanted/media/post/dto/PostDetailResponse.java
Original file line number Diff line number Diff line change
@@ -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
) {
}
Original file line number Diff line number Diff line change
@@ -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<Post, String> {
}
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,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;
}
}
2 changes: 0 additions & 2 deletions src/main/java/wanted/media/user/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion src/main/java/wanted/media/user/domain/Code.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
@Entity
@Table(name = "codes")
public class Code {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false)
Expand Down
1 change: 0 additions & 1 deletion src/main/java/wanted/media/user/domain/Token.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
@Entity
@Table(name = "tokens")
public class Token {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false)
Expand Down
21 changes: 21 additions & 0 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
@@ -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
63 changes: 63 additions & 0 deletions src/test/java/wanted/media/post/service/PostServiceTest.java
Original file line number Diff line number Diff line change
@@ -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("[email protected]")
.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("[email protected]");
}
}

0 comments on commit 5e16349

Please sign in to comment.