diff --git a/src/main/java/com/back/domain/post/category/entity/Category.java b/src/main/java/com/back/domain/post/category/entity/Category.java new file mode 100644 index 00000000..6dda2f13 --- /dev/null +++ b/src/main/java/com/back/domain/post/category/entity/Category.java @@ -0,0 +1,39 @@ +package com.back.domain.post.category.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Entity +@Getter +@Setter +@Table(name = "category") +@EntityListeners(AuditingEntityListener.class) +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class Category { + // 각 게시글 카테고리을 구분하는 유일한 번호 + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + // 카테고리 이름 + @Column(name = "name", nullable = false, unique = true, length = 50) + private String name; + + // 카테고리 설명 + @Column(name = "description", length = 200) + private String description; +} diff --git a/src/main/java/com/back/domain/post/category/repository/CategoryRepository.java b/src/main/java/com/back/domain/post/category/repository/CategoryRepository.java new file mode 100644 index 00000000..97e1be2f --- /dev/null +++ b/src/main/java/com/back/domain/post/category/repository/CategoryRepository.java @@ -0,0 +1,10 @@ +package com.back.domain.post.category.repository; + +import com.back.domain.post.category.entity.Category; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CategoryRepository extends JpaRepository { + +} diff --git a/src/main/java/com/back/domain/post/post/controller/PostController.java b/src/main/java/com/back/domain/post/post/controller/PostController.java new file mode 100644 index 00000000..346977e8 --- /dev/null +++ b/src/main/java/com/back/domain/post/post/controller/PostController.java @@ -0,0 +1,45 @@ +package com.back.domain.post.post.controller; + +import com.back.domain.post.post.dto.request.PostRequestDto; +import com.back.domain.post.post.dto.response.PostResponseDto; +import com.back.domain.post.post.entity.Post; +import com.back.domain.post.post.service.PostService; +import com.back.global.rsData.RsData; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +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; + +@RestController +@RequestMapping("/api/posts") +@RequiredArgsConstructor +public class PostController { + + private final PostService postService; + + /** + * 게시글 작성 API + * @param postRequestDto 게시글 작성 요청 DTO + * @return 작성된 게시글 정보 + */ + @PostMapping + public RsData createPost( + @RequestBody PostRequestDto postRequestDto + ) { + return RsData.successOf(postService.createPost(postRequestDto)); // code=200, message="success" + } + + /** + * 게시글 다건 조회 API + * @return 모든 게시글 리스트 + */ + @GetMapping + public RsData> getAllPosts() { + List posts = postService.getAllPosts(); + return RsData.successOf(posts); // code=200, message="success" + } + +} diff --git a/src/main/java/com/back/domain/post/post/dto/request/PostRequestDto.java b/src/main/java/com/back/domain/post/post/dto/request/PostRequestDto.java new file mode 100644 index 00000000..7ddafc49 --- /dev/null +++ b/src/main/java/com/back/domain/post/post/dto/request/PostRequestDto.java @@ -0,0 +1,13 @@ +package com.back.domain.post.post.dto.request; + +import java.util.List; + +public record PostRequestDto( + Long categoryId, + String title, + String content, + String userNickName, + String imageUrl, + List tags +) { +} diff --git a/src/main/java/com/back/domain/post/post/dto/response/PostResponseDto.java b/src/main/java/com/back/domain/post/post/dto/response/PostResponseDto.java new file mode 100644 index 00000000..c527a7b1 --- /dev/null +++ b/src/main/java/com/back/domain/post/post/dto/response/PostResponseDto.java @@ -0,0 +1,43 @@ +package com.back.domain.post.post.dto.response; + +import com.back.domain.post.post.entity.Post; +import com.back.domain.post.post.enums.PostStatus; +import java.time.LocalDateTime; +import java.util.List; + +public record PostResponseDto( + Long postId, + String categoryName, + String userNickName, + LocalDateTime createdAt, + LocalDateTime updatedAt, + PostStatus status, + String title, + String content, + String imageUrl, + List tags, + Integer likeCount, + Integer commentCount, + Integer viewCount +) { + + public PostResponseDto(Post post) { + this( + post.getId(), + post.getCategory().getName(), + post.getUser().getNickname(), + post.getCreatedAt(), + post.getUpdatedAt(), + post.getStatus(), + post.getTitle(), + post.getContent(), + post.getImageUrl(), + post.getPostTags().stream() + .map(postTag -> postTag.getTag().getName()) + .toList(), + post.getLikeCount(), + post.getCommentCount(), + post.getViewCount() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/back/domain/post/post/entity/Post.java b/src/main/java/com/back/domain/post/post/entity/Post.java new file mode 100644 index 00000000..275a8667 --- /dev/null +++ b/src/main/java/com/back/domain/post/post/entity/Post.java @@ -0,0 +1,104 @@ +package com.back.domain.post.post.entity; + +import com.back.domain.post.category.entity.Category; +import com.back.domain.post.post.enums.PostStatus; +import com.back.domain.user.entity.User; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Entity +@Getter +@Setter +@Table(name = "post") +@EntityListeners(AuditingEntityListener.class) +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class Post { + + // 각 게시글을 구분하는 유일한 번호 + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + // 해당 게시글의 카테고리 고유 식별자 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "category_id") + private Category category; + + // 해당 게시글을 작성한 유저의 고유 식별자 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + // 게시글 작성 날짜 + @CreatedDate + private LocalDateTime createdAt; + + // 게시글 수정 날짜 + @LastModifiedDate + private LocalDateTime updatedAt; + + // 게시글 게시 상태 (기본값: 공개) + @Builder.Default + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + private PostStatus status = PostStatus.PUBLIC; + + // 게시글 제목 + @Column(name = "title", nullable = false, length = 100) + private String title; + + // 게시글 내용 + @Column(name = "content", nullable = false, columnDefinition = "TEXT") + private String content; + + // 게시글 이미지 URL + @Column(name = "image_url") + private String imageUrl; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List postTags = new ArrayList<>(); + + // 게시글 추천 수 (기본값: 0) + @Builder.Default + @Column(name = "like_count", nullable = false) + private Integer likeCount = 0; + + // 게시글 댓글 수 + @Column(name = "comment_count") + private Integer commentCount; + + // 게시글 조회 수 + @Column(name = "view_count") + private Integer viewCount; + + public void addTag(Tag tag) { + PostTag postTag = PostTag.create(this, tag); + this.postTags.add(postTag); + } +} diff --git a/src/main/java/com/back/domain/post/post/entity/PostTag.java b/src/main/java/com/back/domain/post/post/entity/PostTag.java new file mode 100644 index 00000000..9a09a924 --- /dev/null +++ b/src/main/java/com/back/domain/post/post/entity/PostTag.java @@ -0,0 +1,52 @@ +package com.back.domain.post.post.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Entity +@Getter +@Table(name = "post_tag") +@EntityListeners(AuditingEntityListener.class) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PostTag { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tag_id") + private Tag tag; + + @Builder + public PostTag(Post post, Tag tag) { + this.post = post; + this.tag = tag; + } + + // 정적 팩토리 메서드 + public static PostTag create(Post post, Tag tag) { + return PostTag.builder() + .post(post) + .tag(tag) + .build(); + } +} diff --git a/src/main/java/com/back/domain/post/post/entity/Tag.java b/src/main/java/com/back/domain/post/post/entity/Tag.java new file mode 100644 index 00000000..d215e43d --- /dev/null +++ b/src/main/java/com/back/domain/post/post/entity/Tag.java @@ -0,0 +1,29 @@ +package com.back.domain.post.post.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "tag") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class Tag { // Tag는 Post와 직접적인 관계를 맺지 않습니다. + + @Id + @GeneratedValue + @Column(name = "id") + private Long id; + + @Column(unique = true, nullable = false) + private String name; +} diff --git a/src/main/java/com/back/domain/post/post/enums/PostStatus.java b/src/main/java/com/back/domain/post/post/enums/PostStatus.java new file mode 100644 index 00000000..03093b69 --- /dev/null +++ b/src/main/java/com/back/domain/post/post/enums/PostStatus.java @@ -0,0 +1,15 @@ +package com.back.domain.post.post.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum PostStatus { + PUBLIC("공개", "모든 사용자가 볼 수 있는 상태"), + PRIVATE("비공개", "작성자만 볼 수 있는 상태"), + DELETED("삭제됨", "삭제 처리된 게시글 상태"); + + private final String title; + private final String description; +} diff --git a/src/main/java/com/back/domain/post/post/repository/PostRepository.java b/src/main/java/com/back/domain/post/post/repository/PostRepository.java new file mode 100644 index 00000000..d48774c3 --- /dev/null +++ b/src/main/java/com/back/domain/post/post/repository/PostRepository.java @@ -0,0 +1,10 @@ +package com.back.domain.post.post.repository; + +import com.back.domain.post.post.entity.Post; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PostRepository extends JpaRepository { + +} diff --git a/src/main/java/com/back/domain/post/post/repository/TagRepository.java b/src/main/java/com/back/domain/post/post/repository/TagRepository.java new file mode 100644 index 00000000..18d13e1a --- /dev/null +++ b/src/main/java/com/back/domain/post/post/repository/TagRepository.java @@ -0,0 +1,12 @@ +package com.back.domain.post.post.repository; + +import com.back.domain.post.post.entity.Tag; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TagRepository extends JpaRepository { + // 태그 이름으로 Tag 엔티티를 찾기 위한 메서드 + Optional findByName(String name); +} diff --git a/src/main/java/com/back/domain/post/post/service/PostService.java b/src/main/java/com/back/domain/post/post/service/PostService.java new file mode 100644 index 00000000..f09bed7c --- /dev/null +++ b/src/main/java/com/back/domain/post/post/service/PostService.java @@ -0,0 +1,67 @@ +package com.back.domain.post.post.service; + +import com.back.domain.post.category.entity.Category; +import com.back.domain.post.category.repository.CategoryRepository; +import com.back.domain.post.post.dto.request.PostRequestDto; +import com.back.domain.post.post.dto.response.PostResponseDto; +import com.back.domain.post.post.entity.Post; +import com.back.domain.post.post.entity.Tag; +import com.back.domain.post.post.repository.PostRepository; +import com.back.domain.post.post.repository.TagRepository; +import com.back.domain.user.entity.User; +import com.back.global.rq.Rq; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class PostService { + + private final PostRepository postRepository; + private final CategoryRepository categoryRepository; + private final TagRepository tagRepository; + private final Rq rq; + + // 게시글 작성 로직 + @Transactional + public PostResponseDto createPost(PostRequestDto postRequestDto) { + User user = rq.getActor(); // 현재 로그인한 사용자의 정보 가져오기 + + Category category = categoryRepository.findById(postRequestDto.categoryId()) + .orElseThrow(() -> new IllegalArgumentException("해당 카테고리를 찾을 수 없습니다. ID: " + postRequestDto.categoryId())); + + Post post = Post.builder() + .category(category) + .user(user) + .title(postRequestDto.title()) + .content(postRequestDto.content()) + .imageUrl(postRequestDto.imageUrl()) + .build(); + + List tagNames = postRequestDto.tags(); + if (tagNames != null && !tagNames.isEmpty()) { + for (String tagName : tagNames) { + // 태그 이름으로 Tag 엔티티를 조회하거나, 없으면 새로 생성하여 저장 + Tag tag = tagRepository.findByName(tagName) + .orElseGet(() -> tagRepository.save( + Tag.builder() + .name(tagName) + .build() + ) + ); + + post.addTag(tag); + } + } + + return new PostResponseDto(postRepository.save(post)); + } + + // 게시글 다건 조회 로직 + @Transactional(readOnly = true) + public List getAllPosts() { + return postRepository.findAll(); + } +}