Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 3 additions & 2 deletions 7th-be-blog/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ java {
sourceCompatibility = '21'
}

repositories {
repositories {1
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.16'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

// runtimeOnly 'com.mysql:mysql-connector-j'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.leets.blog.entity.comment;
package com.leets.blog.domain.comment.entity;

import com.leets.blog.entity.BaseEntity;
import com.leets.blog.entity.post.Post;
import com.leets.blog.entity.user.User;
import com.leets.blog.global.common.BaseEntity;
import com.leets.blog.domain.post.entity.Post;
import com.leets.blog.domain.user.entity.User;
import jakarta.persistence.*;
import lombok.*;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.leets.blog.controller;
package com.leets.blog.domain.health.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
Expand Down
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")
Copy link
Copy Markdown

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 디렉토리를 만드시면 유지보수에 더 좋은 코드가 될 것 같아요!

@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));
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
@@ -1,8 +1,8 @@
package com.leets.blog.entity.post;
package com.leets.blog.domain.post.entity;

import com.leets.blog.entity.BaseEntity;
import com.leets.blog.entity.comment.Comment;
import com.leets.blog.entity.user.User;
import com.leets.blog.global.common.BaseEntity;
import com.leets.blog.domain.comment.entity.Comment;
import com.leets.blog.domain.user.entity.User;
import jakarta.persistence.*;
import lombok.*;

Expand Down Expand Up @@ -42,4 +42,14 @@ public class Post extends BaseEntity {
// 상태 변경
@Column(name = "is_deleted", nullable = false)
private boolean isDeleted = false;

// 게시글 수정 메서드
public void update(String title, String content) {
if (title != null && !title.isBlank()) {
this.title = title;
}
if (content != null && !content.isBlank()) {
this.content = content;
}
}
}
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

3주차 개발도 수고 많으셨습니다 :)
userId를 Request에서 직접 받으면 다른 사람 id를 넣어서 요청할 수도 있을 것 같은데, 추후 어떤 방식으로 개발하실 생각이신지 궁금합니다!


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
@@ -1,8 +1,8 @@
package com.leets.blog.controller;
package com.leets.blog.domain.string.controller;

import com.leets.blog.dto.StringRequestDto;
import com.leets.blog.dto.StringResponseDto;
import com.leets.blog.service.StringService;
import com.leets.blog.domain.string.dto.StringRequestDto;
import com.leets.blog.domain.string.dto.StringResponseDto;
import com.leets.blog.domain.string.service.StringService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.leets.blog.dto;
package com.leets.blog.domain.string.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.leets.blog.dto;
package com.leets.blog.domain.string.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.leets.blog.service;
package com.leets.blog.domain.string.service;

import org.springframework.stereotype.Service;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.leets.blog.entity.user;
package com.leets.blog.domain.user.entity;

import com.leets.blog.entity.BaseEntity;
import com.leets.blog.entity.post.Post;
import com.leets.blog.entity.comment.Comment;
import com.leets.blog.global.common.BaseEntity;
import com.leets.blog.domain.post.entity.Post;
import com.leets.blog.domain.comment.entity.Comment;
import jakarta.persistence.*;
import lombok.*;

Expand Down
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
@@ -1,4 +1,4 @@
package com.leets.blog.entity;
package com.leets.blog.global.common;

import jakarta.persistence.*;
import lombok.Getter;
Expand Down
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;
}
Loading