-
Notifications
You must be signed in to change notification settings - Fork 20
[3주차] 김동빈/[feat] 게시글 도메인 API 구현 #103
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
base: 김동빈/main
Are you sure you want to change the base?
The head ref may contain hidden characters: "\uAE40\uB3D9\uBE48/3\uC8FC\uCC28"
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package com.leets.blog.domain.post.controller; | ||
|
|
||
| import com.leets.blog.global.common.ApiResponse; | ||
| import com.leets.blog.domain.post.dto.PostRequest; | ||
| import com.leets.blog.domain.post.dto.PostResponse; | ||
| import com.leets.blog.domain.post.service.PostService; | ||
| import jakarta.validation.Valid; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/posts") | ||
| @RequiredArgsConstructor | ||
| public class PostController { | ||
|
|
||
| private final PostService postService; | ||
|
|
||
| // 1. 게시글 목록 조회 (PDF 설계: GET /posts) | ||
| @GetMapping | ||
| public ApiResponse<List<PostResponse>> getPostList() { | ||
| return ApiResponse.onSuccess(postService.getPostList()); | ||
| } | ||
|
|
||
| // 2. 게시글 상세 조회 (PDF 설계: GET /posts/{postId}) | ||
| @GetMapping("/{postId}") | ||
| public ApiResponse<PostResponse> getPostDetail(@PathVariable Long postId) { | ||
| return ApiResponse.onSuccess(postService.getPostDetail(postId)); | ||
| } | ||
|
|
||
| // 3. 게시글 작성 (PDF 설계: POST /posts, @Valid 적용) | ||
| @PostMapping | ||
| public ApiResponse<PostResponse> createPost(@RequestBody @Valid PostRequest request) { | ||
| return ApiResponse.onSuccess(postService.createPost(request)); | ||
| } | ||
|
|
||
| // 4. 게시글 삭제 (PDF 설계: DELETE /posts/{postId}) | ||
| @DeleteMapping("/{postId}") | ||
| public ApiResponse<String> deletePost(@PathVariable Long postId) { | ||
| postService.deletePost(postId); | ||
| return ApiResponse.onSuccess("게시글이 성공적으로 삭제되었습니다."); | ||
| } | ||
|
|
||
| // 5. 게시글 수정 (PDF 설계: PATCH /posts/{postId}) | ||
| @PatchMapping("/{postId}") | ||
| public ApiResponse<PostResponse> updatePost(@PathVariable Long postId, @RequestBody @Valid PostRequest request) { | ||
| return ApiResponse.onSuccess(postService.updatePost(postId, request)); | ||
| } | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Entity와 DTO 간의 변환 로직을 PostConverter라는 별도 클래스로 분리하신 점이 인상 깊네요! 덕분에 서비스 코드가 비즈니스 로직에만 집중할 수 있어 가독성이 훨씬 좋아진 것 같습니다. 저도 다음 작업 때 이런 계층 분리를 적용해보고 싶네요. 잘 배웠습니다! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package com.leets.blog.domain.post.converter; | ||
|
|
||
| import com.leets.blog.domain.post.dto.PostRequest; | ||
| import com.leets.blog.domain.post.dto.PostResponse; | ||
| import com.leets.blog.domain.post.entity.Post; | ||
| import com.leets.blog.domain.user.entity.User; | ||
|
|
||
| public class PostConverter { | ||
|
|
||
| // Request -> Entity | ||
| public static Post toPost(PostRequest request, User user) { | ||
| return Post.builder() | ||
| .title(request.getTitle()) | ||
| .content(request.getContent()) | ||
| .user(user) | ||
| .isDeleted(false) | ||
| .build(); | ||
| } | ||
|
|
||
| // Entity -> Response | ||
| public static PostResponse toPostResponse(Post post) { | ||
| return PostResponse.builder() | ||
| .id(post.getId()) | ||
| .title(post.getTitle()) | ||
| .content(post.getContent()) | ||
| .authorName(post.getUser().getName()) | ||
| .createdAt(post.getCreatedAt()) | ||
| .build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.leets.blog.domain.post.dto; | ||
|
|
||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import jakarta.validation.constraints.Size; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class PostRequest { | ||
|
|
||
| @NotBlank(message = "제목은 필수입니다.") | ||
| @Size(max = 100, message = "제목은 100자 이내여야 합니다.") | ||
| private String title; | ||
|
|
||
| @NotBlank(message = "내용은 필수입니다.") | ||
| private String content; | ||
|
|
||
| @NotNull(message = "작성자 ID는 필수입니다.") | ||
| private Long userId; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.leets.blog.domain.post.dto; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import java.time.LocalDateTime; | ||
|
|
||
| @Getter | ||
| @Builder | ||
| @AllArgsConstructor | ||
| public class PostResponse { | ||
| private Long id; | ||
| private String title; | ||
| private String content; | ||
| private String authorName; | ||
| private LocalDateTime createdAt; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package com.leets.blog.domain.post.repository; | ||
|
|
||
| import com.leets.blog.domain.post.entity.Post; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @Repository | ||
| public interface PostRepository extends JpaRepository<Post, Long> { | ||
| // 삭제되지 않은 게시글만 조회 | ||
| List<Post> findAllByIsDeletedFalse(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| package com.leets.blog.domain.post.service; | ||
|
|
||
| import com.leets.blog.domain.post.converter.PostConverter; | ||
| import com.leets.blog.domain.post.dto.PostRequest; | ||
| import com.leets.blog.domain.post.dto.PostResponse; | ||
| import com.leets.blog.domain.post.entity.Post; | ||
| import com.leets.blog.domain.user.entity.User; | ||
| import com.leets.blog.domain.post.repository.PostRepository; | ||
| import com.leets.blog.domain.user.repository.UserRepository; | ||
| import com.leets.blog.global.common.BaseErrorCode; | ||
| import com.leets.blog.global.exception.PostException; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.util.List; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| @Transactional(readOnly = true) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 서비스 전체에 Transactional을 붙이기 보다는 각 메서드별로 Transactional을 붙이시는게 이후 코드 유지보수에 더 좋을 것 같아요! |
||
| public class PostService { | ||
|
|
||
| private final PostRepository postRepository; | ||
| private final UserRepository userRepository; | ||
|
|
||
| // 1. 게시글 작성 | ||
| @Transactional | ||
| public PostResponse createPost(PostRequest request) { | ||
| User user = userRepository.findById(request.getUserId()) | ||
| .orElseThrow(() -> new PostException(BaseErrorCode.USER_NOT_FOUND)); | ||
|
Comment on lines
+28
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3주차 개발도 수고 많으셨습니다 :) |
||
|
|
||
| Post post = PostConverter.toPost(request, user); | ||
| return PostConverter.toPostResponse(postRepository.save(post)); | ||
| } | ||
|
|
||
| // 2. 게시글 목록 조회 | ||
| public List<PostResponse> getPostList() { | ||
| return postRepository.findAllByIsDeletedFalse().stream() | ||
| .map(PostConverter::toPostResponse) | ||
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
| // 3. 게시글 상세 조회 | ||
| public PostResponse getPostDetail(Long id) { | ||
| Post post = postRepository.findById(id) | ||
| .filter(p -> !p.isDeleted()) | ||
| .orElseThrow(() -> new PostException(BaseErrorCode.POST_NOT_FOUND)); | ||
|
|
||
| return PostConverter.toPostResponse(post); | ||
| } | ||
|
|
||
| // 4. 게시글 삭제 (Soft Delete) | ||
| @Transactional | ||
| public void deletePost(Long id) { | ||
| Post post = postRepository.findById(id) | ||
| .orElseThrow(() -> new PostException(BaseErrorCode.POST_NOT_FOUND)); | ||
|
|
||
| // Soft delete 로직 추가 가능 | ||
| } | ||
|
|
||
| // 5. 게시글 수정 (PATCH) | ||
| @Transactional | ||
| public PostResponse updatePost(Long id, PostRequest request) { | ||
| Post post = postRepository.findById(id) | ||
| .filter(p -> !p.isDeleted()) | ||
| .orElseThrow(() -> new PostException(BaseErrorCode.POST_NOT_FOUND)); | ||
|
|
||
| post.update(request.getTitle(), request.getContent()); | ||
| return PostConverter.toPostResponse(post); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.leets.blog.domain.user.repository; | ||
|
|
||
| import com.leets.blog.domain.user.entity.User; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| @Repository | ||
| public interface UserRepository extends JpaRepository<User, Long> { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package com.leets.blog.global.common; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonInclude; | ||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
| import com.fasterxml.jackson.annotation.JsonPropertyOrder; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| @AllArgsConstructor | ||
| @JsonPropertyOrder({"isSuccess", "code", "message", "result"}) | ||
| public class ApiResponse<T> { | ||
|
|
||
| @JsonProperty("isSuccess") | ||
| private final Boolean isSuccess; | ||
| private final String code; | ||
| private final String message; | ||
|
|
||
| @JsonInclude(JsonInclude.Include.NON_NULL) | ||
| private final T result; | ||
|
|
||
| // 성공 시 | ||
| public static <T> ApiResponse<T> onSuccess(T result) { | ||
| return new ApiResponse<>(true, "COMMON200", "요청에 성공하였습니다.", result); | ||
| } | ||
|
|
||
| // 실패 시 (상황에 따라 result에 에러 정보를 담거나 null 처리) | ||
| public static <T> ApiResponse<T> onFailure(String code, String message, T result) { | ||
| return new ApiResponse<>(false, code, message, result); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.leets.blog.global.common; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import org.springframework.http.HttpStatus; | ||
|
|
||
| @Getter | ||
| @AllArgsConstructor | ||
| public enum BaseErrorCode { | ||
| // 공통 에러 | ||
| SUCCESS(HttpStatus.OK, "COMMON200", "요청에 성공하였습니다."), | ||
| BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."), | ||
| INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 내부 오류가 발생했습니다."), | ||
|
|
||
| // 도메인별 에러 (예시) | ||
| USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER4001", "사용자를 찾을 수 없습니다."), | ||
| POST_NOT_FOUND(HttpStatus.NOT_FOUND, "POST4001", "게시글을 찾을 수 없습니다."); | ||
|
|
||
| private final HttpStatus httpStatus; | ||
| private final String code; | ||
| private final String message; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
추후에 api 버전으로 API를 구성해보면 어떠실까요?? 예를 들어 api/v1/posts라던지 api/v1 디렉토리를 만드시면 유지보수에 더 좋은 코드가 될 것 같아요!