diff --git a/build.gradle b/build.gradle index 586ca31..04255d2 100644 --- a/build.gradle +++ b/build.gradle @@ -43,6 +43,9 @@ dependencies { compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + // Valid + implementation 'org.springframework.boot:spring-boot-starter-validation' + // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9' diff --git a/src/main/java/com/moongeul/backend/api/member/entity/Role.java b/src/main/java/com/moongeul/backend/api/member/entity/Role.java index 97a604e..43b7a99 100644 --- a/src/main/java/com/moongeul/backend/api/member/entity/Role.java +++ b/src/main/java/com/moongeul/backend/api/member/entity/Role.java @@ -11,4 +11,3 @@ public enum Role { private final String key; } - diff --git a/src/main/java/com/moongeul/backend/api/post/controller/PostController.java b/src/main/java/com/moongeul/backend/api/post/controller/PostController.java new file mode 100644 index 0000000..1823dde --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/post/controller/PostController.java @@ -0,0 +1,47 @@ +package com.moongeul.backend.api.post.controller; + +import com.moongeul.backend.api.post.dto.PostCreateRequestDTO; +import com.moongeul.backend.api.post.dto.PostCreateResponseDTO; +import com.moongeul.backend.api.post.service.PostService; +import com.moongeul.backend.common.response.ApiResponse; +import com.moongeul.backend.common.response.SuccessStatus; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Post", description = "Post(게시글) 관련 API 입니다.") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v2/post") +public class PostController { + + private final PostService postService; + + @Operation( + summary = "글쓰기 API", + description = "기록(게시글)을 작성하는 글쓰기 API 입니다." + + "
필수: isbn, readDate / 선택: rating(default = 5.0), page(default = 300), content, quotes" + + "
선택 요소의 경우 입력되지 않았을 때 'null'로 전달 바랍니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "글쓰기 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "ISBN은 필수입니다. (isbn)"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "해당 도서를 찾을 수 없습니다.") + }) + @PostMapping("/create") + public ResponseEntity> createPost(@AuthenticationPrincipal UserDetails userDetails, + @Valid @RequestBody PostCreateRequestDTO postCreateRequestDTO) { + + PostCreateResponseDTO response = postService.createPost(postCreateRequestDTO, userDetails.getUsername()); + return ApiResponse.success(SuccessStatus.CREATE_POST_SUCCESS, response); + } +} diff --git a/src/main/java/com/moongeul/backend/api/post/dto/PostCreateRequestDTO.java b/src/main/java/com/moongeul/backend/api/post/dto/PostCreateRequestDTO.java new file mode 100644 index 0000000..8129e69 --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/post/dto/PostCreateRequestDTO.java @@ -0,0 +1,80 @@ +package com.moongeul.backend.api.post.dto; + +import com.moongeul.backend.api.book.entity.Book; +import com.moongeul.backend.api.member.entity.Member; +import com.moongeul.backend.api.post.entity.Post; +import com.moongeul.backend.api.post.entity.Quote; +import jakarta.validation.constraints.*; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +@Getter +@Builder +public class PostCreateRequestDTO { + + /* 드롭다운 - 필수 입력*/ + + /* 필수 입력 */ + @NotBlank(message = "ISBN은 필수입니다") + private String isbn; // 책 + + @NotNull(message = "독서 완료일은 필수입니다") + @PastOrPresent(message = "독서 완료일은 미래일 수 없습니다") + private LocalDate readDate; // 읽은 날짜 + + /* 기본값 있는 필드 */ + @Min(value = 0, message = "평점은 0.0 이상이어야 합니다.") + @Max(value = 5, message = "평점은 5.0 이하이어야 합니다.") + private Double rating; // 평점 + + @Positive(message = "페이지 수는 양수여야 합니다") + private Integer page; // 페이지 수 + + /* 선택 입력 */ + @Size(max = 3000, message = "내용은 5000자 이하로 작성해야 합니다.") + private String content; // 감상평 + private List quotes; // 인상깊은구절 + + @Getter + public static class QuoteRequestDTO { + private String quote; // 인용문 내용 + private Integer pageNumber; // 페이지 번호 + } + + public Post toEntity(Member member, Book book) { + + // rating과 page가 null로 들어오면 기본값으로 대체 + Double finalRating = (this.rating != null) ? this.rating : 5.0; + Integer finalPage = (this.page != null) ? this.page : 300; + + Post post = Post.builder() + .readDate(this.readDate) + .rating(finalRating) + .page(finalPage) + .content(this.content) + .member(member) + .book(book) + .build(); + + // Quote 처리 + if (this.quotes != null && !this.quotes.isEmpty()) { + List quoteList = new ArrayList<>(); + for (QuoteRequestDTO quoteDTO : this.quotes) { + Quote quote = Quote.builder() + .quoteContent(quoteDTO.getQuote()) + .pageNumber(quoteDTO.getPageNumber()) + .post(post) + .build(); + quoteList.add(quote); + + post.addQuotes(quoteList); + } + } + return post; + } + +} diff --git a/src/main/java/com/moongeul/backend/api/post/dto/PostCreateResponseDTO.java b/src/main/java/com/moongeul/backend/api/post/dto/PostCreateResponseDTO.java new file mode 100644 index 0000000..351f6ab --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/post/dto/PostCreateResponseDTO.java @@ -0,0 +1,11 @@ +package com.moongeul.backend.api.post.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class PostCreateResponseDTO { + + private Long postId; // 생성된 Post id +} diff --git a/src/main/java/com/moongeul/backend/api/post/dto/PostResponseDTO.java b/src/main/java/com/moongeul/backend/api/post/dto/PostResponseDTO.java new file mode 100644 index 0000000..6de484d --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/post/dto/PostResponseDTO.java @@ -0,0 +1,21 @@ +package com.moongeul.backend.api.post.dto; + +import com.moongeul.backend.api.book.dto.BookDTO; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +public class PostResponseDTO { + + // 필수 + private BookDTO bookDTO; // 필요 - 책 제목/작가/출판사 + private double rating; + + // 선택 + private String content; // 감상평 + private List quotes; // 인상깊은구절 리스트 + +} diff --git a/src/main/java/com/moongeul/backend/api/post/entity/Category.java b/src/main/java/com/moongeul/backend/api/post/entity/Category.java new file mode 100644 index 0000000..1abb152 --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/post/entity/Category.java @@ -0,0 +1,27 @@ +package com.moongeul.backend.api.post.entity; + +import com.moongeul.backend.api.member.entity.Member; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder // 빌더 패턴 사용을 위한 롬복 애너테이션 +@NoArgsConstructor // 기본 생성자 +@AllArgsConstructor // 모든 필드를 포함한 생성자 +@Table(name = "CATEGORY") // 데이터베이스 테이블 이름 지정 +public class Category { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 카테고리 id + + private String title; // 카테고리 제목 + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; +} diff --git a/src/main/java/com/moongeul/backend/api/post/entity/Post.java b/src/main/java/com/moongeul/backend/api/post/entity/Post.java new file mode 100644 index 0000000..6e98e7a --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/post/entity/Post.java @@ -0,0 +1,56 @@ +package com.moongeul.backend.api.post.entity; + +import com.moongeul.backend.api.book.entity.Book; +import com.moongeul.backend.api.member.entity.Member; +import com.moongeul.backend.common.entity.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Builder // 빌더 패턴 사용을 위한 롬복 애너테이션 +@NoArgsConstructor // 기본 생성자 +@AllArgsConstructor // 모든 필드를 포함한 생성자 +@Table(name = "POST") // 데이터베이스 테이블 이름 지정 +public class Post extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 게시글 id + + private LocalDate readDate; // 읽은날짜 + private Double rating; // 평점 + private Integer page; // 페이지 수 + private String content; // 감상평 + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "book_isbn", nullable = false) + private Book book; + +// @ManyToOne(fetch = FetchType.LAZY) +// @JoinColumn(name = "category_id", nullable = false) +// private Category category; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List quotes = new ArrayList<>(); // 인상깊은구절 + + public void addQuote(Quote quote) { + quotes.add(quote); + } + + public void addQuotes(List quoteList) { + quoteList.forEach(this::addQuote); + } +} diff --git a/src/main/java/com/moongeul/backend/api/post/entity/Quote.java b/src/main/java/com/moongeul/backend/api/post/entity/Quote.java new file mode 100644 index 0000000..9302791 --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/post/entity/Quote.java @@ -0,0 +1,27 @@ +package com.moongeul.backend.api.post.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder // 빌더 패턴 사용을 위한 롬복 애너테이션 +@NoArgsConstructor // 기본 생성자 +@AllArgsConstructor // 모든 필드를 포함한 생성자 +@Table(name = "QUOTE") // 데이터베이스 테이블 이름 지정 +public class Quote { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 인상깊은구절 id + + private String quoteContent; // 인상깊은구절 내용 + private int pageNumber; // 페이지 번호 + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id", nullable = false) + private Post post; +} diff --git a/src/main/java/com/moongeul/backend/api/post/repository/PostRepository.java b/src/main/java/com/moongeul/backend/api/post/repository/PostRepository.java new file mode 100644 index 0000000..02b3ea6 --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/post/repository/PostRepository.java @@ -0,0 +1,7 @@ +package com.moongeul.backend.api.post.repository; + +import com.moongeul.backend.api.post.entity.Post; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostRepository extends JpaRepository { +} diff --git a/src/main/java/com/moongeul/backend/api/post/service/PostService.java b/src/main/java/com/moongeul/backend/api/post/service/PostService.java new file mode 100644 index 0000000..cccd4ad --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/post/service/PostService.java @@ -0,0 +1,42 @@ +package com.moongeul.backend.api.post.service; + +import com.moongeul.backend.api.book.entity.Book; +import com.moongeul.backend.api.book.repository.BookRepository; +import com.moongeul.backend.api.member.entity.Member; +import com.moongeul.backend.api.member.repository.MemberRepository; +import com.moongeul.backend.api.post.dto.PostCreateRequestDTO; +import com.moongeul.backend.api.post.dto.PostCreateResponseDTO; +import com.moongeul.backend.api.post.entity.Post; +import com.moongeul.backend.api.post.repository.PostRepository; +import com.moongeul.backend.common.exception.NotFoundException; +import com.moongeul.backend.common.response.ErrorStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class PostService { + + private final MemberRepository memberRepository; + private final BookRepository bookRepository; + private final PostRepository postRepository; + + /* 글쓰기 */ + @Transactional + public PostCreateResponseDTO createPost(PostCreateRequestDTO postCreateRequestDTO, String email){ + + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage())); + + Book book = bookRepository.findByIsbn(postCreateRequestDTO.getIsbn()) + .orElseThrow(() -> new NotFoundException(ErrorStatus.BOOK_NOTFOUND_EXCEPTION.getMessage())); + + Post newPost = postCreateRequestDTO.toEntity(member, book); + Post savedPost = postRepository.save(newPost); + + return PostCreateResponseDTO.builder() + .postId(savedPost.getId()) + .build(); + } +} diff --git a/src/main/java/com/moongeul/backend/common/response/SuccessStatus.java b/src/main/java/com/moongeul/backend/common/response/SuccessStatus.java index 7d4df05..ede2288 100644 --- a/src/main/java/com/moongeul/backend/common/response/SuccessStatus.java +++ b/src/main/java/com/moongeul/backend/common/response/SuccessStatus.java @@ -19,6 +19,7 @@ public enum SuccessStatus { ADD_WISH_READ_BOOK_SUCCESS(HttpStatus.OK, "읽고 싶은 책 등록 성공"), REMOVE_WISH_READ_BOOK_SUCCESS(HttpStatus.OK, "읽고 싶은 책 삭제 성공"), REISSUE_TOKEN_SUCCESS(HttpStatus.OK, "토큰 재발급 성공"), + CREATE_POST_SUCCESS(HttpStatus.OK, "글쓰기 성공"), /** * 201