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