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] post 상세보기 api #27

Merged
merged 7 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,5 @@ 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-dev.yml
ssunnykku marked this conversation as resolved.
Show resolved Hide resolved
src/main/resources/application-test.yml

This file was deleted.

This file was deleted.

This file was deleted.

11 changes: 11 additions & 0 deletions src/main/java/wanted/media/exception/PostNotFoundException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package wanted.media.exception;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

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

}
Copy link
Contributor

Choose a reason for hiding this comment

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

Post라고 한정 짓는거 보단 NotFoundException으로 클래스명을 변경하고 전역에서 사용하는게 더 좋을 것 같습니당
여기도 EOL이 안지켜졌어요~!

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.ErrorCode;
import wanted.media.exception.ErrorResponse;
import wanted.media.exception.PostNotFoundException;

@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(PostNotFoundException.class)
public ResponseEntity<ErrorResponse> handlePostNotFound(PostNotFoundException e) {
ErrorCode errorCode = e.getErrorCode();
ErrorResponse errorResponse = new ErrorResponse(
errorCode.getStatus().value(),
errorCode.getMessage()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}
}
28 changes: 28 additions & 0 deletions src/main/java/wanted/media/post/controller/PostController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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.dto.DetailResponse;
import wanted.media.post.service.PostService;

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

Copy link
Contributor

Choose a reason for hiding this comment

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

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

private final PostService contentService;
Copy link
Contributor

Choose a reason for hiding this comment

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

contentService라고 되어있네요?~


/**
* @param postId
* @return
*/
@GetMapping("/{postId}")
public ResponseEntity<DetailResponse> getPost(@PathVariable String postId) {
return ResponseEntity.ok(contentService.getPost(postId));
}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package wanted.media.content.domain;
package wanted.media.post.domain;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
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;
Expand All @@ -13,19 +14,15 @@

@Entity
@Getter
@Table(name = "contents")
@Table(name = "posts")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@EntityListeners(AuditingEntityListener.class)
public class Content {
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "content_id", nullable = false)
private Long id;

@Column(name = "like_count")
private Long likeCount;
@Column(name = "post_id", nullable = false)
private String id;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
Expand All @@ -35,12 +32,16 @@ public class Content {
@Column(nullable = false)
private String title;

private String content;

private String post;
private String hashtags;

@ColumnDefault("0")
private Long likeCount;

@ColumnDefault("0")
private Long viewCount;

@ColumnDefault("0")
private Long shareCount;

@LastModifiedDate
Expand All @@ -54,4 +55,11 @@ public class Content {
@NotNull
private User user;

public void incrementViewCount() {
if (this.viewCount == null) {
this.viewCount = 0L;
}
this.viewCount += 1;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

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

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package wanted.media.content.domain;
package wanted.media.post.domain;

public enum Type {
FACEBOOK, TWITTER, INSTAGRAM, THREADS;
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/wanted/media/post/dto/DetailResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package wanted.media.post.dto;

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

import java.time.LocalDateTime;
import java.util.UUID;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class DetailResponse {
Copy link
Contributor

Choose a reason for hiding this comment

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

record 클래스에 대해 찾아보시고 적용하시면 더 깔끔할 것 같습니당
그리고 DatailResponse보단 PostDetailResponse가 ㅇ어떤 엔티티에 대한 Response인지 더 명확할 것 같습니다!

private String postId;
private Type type;
private String title;
private String post;
private String hashtags;
private Long likeCount;
private Long viewCount;
private Long shareCount;
private LocalDateTime updatedAt;
private LocalDateTime createdAt;
private UUID userId;
private String account;
private 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> {
}
44 changes: 44 additions & 0 deletions src/main/java/wanted/media/post/service/PostService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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.PostNotFoundException;
import wanted.media.post.domain.Post;
import wanted.media.post.dto.DetailResponse;
import wanted.media.post.repository.PostRepository;

@Service
@RequiredArgsConstructor
public class PostService {

ssunnykku marked this conversation as resolved.
Show resolved Hide resolved
private final PostRepository postRepository;

@Transactional
public DetailResponse getPost(String postId) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new PostNotFoundException(ErrorCode.ENTITY_NOT_FOUND));

post.incrementViewCount();
postRepository.save(post);
Copy link
Contributor

Choose a reason for hiding this comment

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

JPA 변경감지 기능을 사용하시면 따로 save하지 않아도 괜찮습니다~


DetailResponse result = DetailResponse.builder()
Copy link
Contributor

Choose a reason for hiding this comment

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

service 레이어에서 Response를 반환해주는것 보다 Controller 레이어에서 DetailResponse에게 post 객체를 넘겨주고 내부적으로 처리하는게 더 맞는것 같아요 ~!

.postId(post.getId())
.likeCount(post.getLikeCount())
.type(post.getType())
.title(post.getTitle())
.post(post.getPost())
.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 result;
}

}
31 changes: 30 additions & 1 deletion src/main/java/wanted/media/user/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,36 @@
package wanted.media.user.config;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
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.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.web.SecurityFilterChain;


@EnableWebSecurity
@Configuration
public class SecurityConfig {
}

/**
* 테스트용 메서드
*/
@Bean
@ConditionalOnProperty(name = "spring.h2.console.enabled", havingValue = "true")
public WebSecurityCustomizer configureH2ConsoleEnable() {
return web -> web.ignoring()
.requestMatchers(PathRequest.toH2Console());
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
request -> request.requestMatchers("/**").permitAll()
.anyRequest().authenticated())
.csrf(csrf -> csrf.disable());

return http.build();
}
}
85 changes: 85 additions & 0 deletions src/test/java/wanted/media/post/repository/PostRepositoryTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package wanted.media.post.repository;

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.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 PostRepositoryTest {
Copy link
Contributor

Choose a reason for hiding this comment

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

repository 테스트랑 service 테스트랑 다른게 없는 것 같아요 service 테스트만 있어도 될 것 같습니다~!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

삭제해야 한다고 생각하시나요?? 테스트는 촘촘하면 더 좋지 않을까요~?

Copy link
Contributor

Choose a reason for hiding this comment

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

옴.. 테스트는 촘촘하면 좋지만 repository 테스트는 일반적으로 작성하지 않아서 드린 말씀이었어요 !!
service 테스트만으로도 충분한 것 같아서 드린 말씀이었습니다 ㅎㅎ

Copy link
Contributor Author

Choose a reason for hiding this comment

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

저는 필수적이진 않지만 repository도 테스트를 작성하는게 좋다고 생각합니다. 그런데 이렇게 단순한 로직의 경우는 필요 없을 수도 있겠네요!

@Autowired
private PostRepository postRepository;

@Autowired
private UserRepository userRepository;

@Test
@Transactional
void getPostTest() {

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("제목 입력")
.user(user)
.build();

postRepository.save(post);

Post result = postRepository.findById(post.getId()).orElseThrow(() -> new IllegalArgumentException("Content not found"));

assertThat(result.getTitle()).isEqualTo("제목 입력");
assertThat(result.getUser().getAccount()).isEqualTo("sun");
}

@Test
@Transactional
void addViewCountTest() {
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("제목 입력")
.user(user)
.viewCount(100L)
.build();

postRepository.save(post);

Post getContent = postRepository.findById(post.getId()).orElseThrow(() -> new RuntimeException("Content not found"));


getContent.incrementViewCount();
assertThat(getContent.getViewCount()).isEqualTo(101);


postRepository.save(getContent);

}
}
Loading