diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/category/dto/CategorySimpleDto.java b/BACK/spring-app/src/main/java/com/starchive/springapp/category/dto/CategorySimpleDto.java new file mode 100644 index 0000000..4cfbcb6 --- /dev/null +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/category/dto/CategorySimpleDto.java @@ -0,0 +1,18 @@ +package com.starchive.springapp.category.dto; + +import com.starchive.springapp.category.domain.Category; +import lombok.Data; + +@Data +public class CategorySimpleDto { + private Long categoryId; + private String categoryName; + + public static CategorySimpleDto from(Category category) { + CategorySimpleDto categorySimpleDto = new CategorySimpleDto(); + categorySimpleDto.categoryId = category.getId(); + categorySimpleDto.categoryName = category.getName(); + + return categorySimpleDto; + } +} diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/category/repository/CategoryRepository.java b/BACK/spring-app/src/main/java/com/starchive/springapp/category/repository/CategoryRepository.java index 8cdd02e..37d0bc6 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/category/repository/CategoryRepository.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/category/repository/CategoryRepository.java @@ -8,7 +8,7 @@ import org.springframework.data.repository.query.Param; public interface CategoryRepository extends JpaRepository { - @Query("SELECT c FROM Category c LEFT JOIN FETCH c.children WHERE c.id = :id") + @Query("SELECT c FROM Category c LEFT JOIN FETCH c.children left join fetch c.parent WHERE c.id = :id") Optional findByIdWithChildren(@Param("id") Long id); @Query("SELECT DISTINCT c FROM Category c LEFT JOIN FETCH c.children WHERE c.parent IS NULL") diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/category/service/CategoryService.java b/BACK/spring-app/src/main/java/com/starchive/springapp/category/service/CategoryService.java index 2357e4b..77ed129 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/category/service/CategoryService.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/category/service/CategoryService.java @@ -19,7 +19,8 @@ public List findAll() { } public Category findOne(Long id) { - return categoryRepository.findById(id).orElseThrow(CategoryNotFoundException::new); + return categoryRepository.findByIdWithChildren(id).orElseThrow(CategoryNotFoundException::new); } + } diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/global/ErrorMessage.java b/BACK/spring-app/src/main/java/com/starchive/springapp/global/ErrorMessage.java index 4499ff2..a96681d 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/global/ErrorMessage.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/global/ErrorMessage.java @@ -5,5 +5,6 @@ public class ErrorMessage { final public static String NOT_IMAGE_EXTENSION = "올바르지 않은 이미지 확장자(허용 확장자: \"jpg\", \"png\", \"gif\", \"jpeg\""; final public static String INVALID_FILE_SIZE = "파일 크기가 너무 큽니다. 최대 이미지 크기: 5MB"; final public static String CATEGORY_NOT_FOUND = "카테고리가 존재하지 않습니다."; - final public static String HASHTAG_NOT_FOUND = "카테고리가 존재하지 않습니다."; + final public static String HASHTAG_NOT_FOUND = "해쉬태그가 존재하지 않습니다."; + final public static String POST_NOT_FOUND = "게시글이 존재하지 않습니다."; } diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/global/exception/GlobalExceptionHandler.java b/BACK/spring-app/src/main/java/com/starchive/springapp/global/exception/GlobalExceptionHandler.java index 89c3465..abba3eb 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/global/exception/GlobalExceptionHandler.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/global/exception/GlobalExceptionHandler.java @@ -4,6 +4,7 @@ import com.starchive.springapp.category.exception.CategoryNotFoundException; import com.starchive.springapp.hashtag.exception.HashTagNotFoundException; +import com.starchive.springapp.post.exception.PostNotFoundException; import java.util.HashMap; import java.util.Map; import org.springframework.http.HttpStatus; @@ -33,8 +34,14 @@ public ResponseEntity handleMaxSizeException(CategoryNotFoundException e .body(ex.getMessage()); } + @ExceptionHandler(PostNotFoundException.class) + public ResponseEntity handlePostNotFoundException(PostNotFoundException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(ex.getMessage()); + } + @ExceptionHandler(HashTagNotFoundException.class) - public ResponseEntity handleMaxSizeException(HashTagNotFoundException ex) { + public ResponseEntity handleHashTagNotFoundException(HashTagNotFoundException ex) { return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE) .body(INVALID_FILE_SIZE); } diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/hashtag/dto/HashTagResponse.java b/BACK/spring-app/src/main/java/com/starchive/springapp/hashtag/dto/HashTagResponse.java new file mode 100644 index 0000000..fe4bf9b --- /dev/null +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/hashtag/dto/HashTagResponse.java @@ -0,0 +1,17 @@ +package com.starchive.springapp.hashtag.dto; + +import com.starchive.springapp.hashtag.domain.HashTag; +import lombok.Data; + +@Data +public class HashTagResponse { + private Long hashTagId; + private String name; + + public static HashTagResponse from(HashTag hashTag) { + HashTagResponse hashTagDto = new HashTagResponse(); + hashTagDto.hashTagId = hashTag.getId(); + hashTagDto.name = hashTag.getName(); + return hashTagDto; + } +} diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/hashtag/repository/HashTagRepository.java b/BACK/spring-app/src/main/java/com/starchive/springapp/hashtag/repository/HashTagRepository.java index 37583c7..7221ad9 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/hashtag/repository/HashTagRepository.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/hashtag/repository/HashTagRepository.java @@ -19,6 +19,10 @@ public interface HashTagRepository extends JpaRepository { + "where c.id = :categoryId") List findAllByCategoryId(@Param("categoryId") Long categoryId); + @Query("select h from HashTag h " + + "join fetch PostHashTag ph on h.id = ph.hashTag.id " + + "where ph.post.id = :postId") + List findAllByPostId(@Param("postId") Long postId); List findManyByIdIn(@Param("ids") List ids); } diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/hashtag/service/HashTagService.java b/BACK/spring-app/src/main/java/com/starchive/springapp/hashtag/service/HashTagService.java index dcb8f18..b511497 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/hashtag/service/HashTagService.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/hashtag/service/HashTagService.java @@ -5,6 +5,7 @@ import com.starchive.springapp.category.repository.CategoryRepository; import com.starchive.springapp.hashtag.domain.HashTag; import com.starchive.springapp.hashtag.dto.HashTagDto; +import com.starchive.springapp.hashtag.dto.HashTagResponse; import com.starchive.springapp.hashtag.exception.HashTagNotFoundException; import com.starchive.springapp.hashtag.repository.HashTagRepository; import com.starchive.springapp.posthashtag.repository.PostHashTagRepository; @@ -20,6 +21,7 @@ public class HashTagService { private final HashTagRepository hashTagRepository; private final CategoryRepository categoryRepository; private final PostHashTagRepository postHashTagRepository; + //private final PostService postService; public HashTag save(String name) { HashTag hashTag = new HashTag(name); @@ -54,6 +56,12 @@ public List findManyByCategory(Long CategoryId) { return hashTagRepository.findAllByCategoryId(category.getId()).stream().map(HashTagDto::from).toList(); } + public List findManyByPost(Long postId) { + //postService.findOne(postId); + + return hashTagRepository.findAllByPostId(postId).stream().map(HashTagResponse::from).toList(); + } + public HashTagDto updateName(Long hashTagId, String newName) { HashTag hashTag = hashTagRepository.findById(hashTagId).orElseThrow(HashTagNotFoundException::new); hashTag.changeName(newName); diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/post/controller/PostController.java b/BACK/spring-app/src/main/java/com/starchive/springapp/post/controller/PostController.java index cbc626a..d75aaef 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/post/controller/PostController.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/post/controller/PostController.java @@ -1,14 +1,17 @@ package com.starchive.springapp.post.controller; import com.starchive.springapp.post.dto.PostCreateRequest; +import com.starchive.springapp.post.dto.PostListResponse; import com.starchive.springapp.post.service.PostService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +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.RequestParam; import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @@ -25,4 +28,17 @@ public ResponseEntity post(@Valid @RequestBody PostCreateRequest request) { return ResponseEntity.status(201).build(); } + + @GetMapping("/posts") + @Operation(summary = "게시글 목록 조회") + public ResponseEntity findPosts( + @RequestParam(name = "category", required = false) Long category, + @RequestParam(name = "tag", required = false) Long tag, + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "pageSize", defaultValue = "10") int pageSize) { + + PostListResponse postListResponse = postService.findPosts(category, tag, page, pageSize); + + return ResponseEntity.ok(postListResponse); + } } diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/post/dto/PostDto.java b/BACK/spring-app/src/main/java/com/starchive/springapp/post/dto/PostDto.java new file mode 100644 index 0000000..e120f8c --- /dev/null +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/post/dto/PostDto.java @@ -0,0 +1,62 @@ +package com.starchive.springapp.post.dto; + +import com.starchive.springapp.category.domain.Category; +import com.starchive.springapp.category.dto.CategorySimpleDto; +import com.starchive.springapp.hashtag.dto.HashTagResponse; +import com.starchive.springapp.post.domain.Post; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import lombok.Data; + +@Data +public class PostDto { + private List categoryHier; + private Long postId; + private String title; + private String author; + private LocalDateTime createdAt; + private String content; + private List hashTags; + + public static PostDto of(Post post, List hashTagDtos) { + PostDto postDto = new PostDto(); + + postDto.categoryHier = setCategoryHier(post.getCategory()); + + postDto.postId = post.getId(); + postDto.title = post.getTitle(); + postDto.author = post.getAuthor(); + postDto.createdAt = post.getCreateAt(); + postDto.content = setContent(post.getContent()); + + postDto.hashTags = hashTagDtos; + + return postDto; + + } + + private static String setContent(String content) { + if (content != null && content.length() > 350) { + return content.substring(0, 350); + } + return content; + } + + private static List setCategoryHier(Category category) { + List categoryHier = new ArrayList<>(); + if (category == null) { + return categoryHier; + } + if (category.getParent() != null) { + Category parent = category.getParent(); + CategorySimpleDto parentCategoryDto = CategorySimpleDto.from(parent); + categoryHier.add(parentCategoryDto); + } + + CategorySimpleDto categoryDto = CategorySimpleDto.from(category); + categoryHier.add(categoryDto); + return categoryHier; + } + +} diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/post/dto/PostListResponse.java b/BACK/spring-app/src/main/java/com/starchive/springapp/post/dto/PostListResponse.java new file mode 100644 index 0000000..f89aea9 --- /dev/null +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/post/dto/PostListResponse.java @@ -0,0 +1,23 @@ +package com.starchive.springapp.post.dto; + +import java.util.List; +import lombok.Data; +import org.springframework.data.domain.Page; + +@Data +public class PostListResponse { + private int currentPage; + private int totalPages; + private long totalCount; + private List posts; + + public static PostListResponse from(Page dtoPage) { + PostListResponse postListResponse = new PostListResponse(); + postListResponse.currentPage = dtoPage.getNumber(); + postListResponse.totalPages = dtoPage.getTotalPages(); + postListResponse.totalCount = dtoPage.getTotalElements(); + postListResponse.posts = dtoPage.getContent(); + return postListResponse; + } + +} diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/post/exception/PostNotFoundException.java b/BACK/spring-app/src/main/java/com/starchive/springapp/post/exception/PostNotFoundException.java new file mode 100644 index 0000000..c318e3a --- /dev/null +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/post/exception/PostNotFoundException.java @@ -0,0 +1,9 @@ +package com.starchive.springapp.post.exception; + +import static com.starchive.springapp.global.ErrorMessage.POST_NOT_FOUND; + +public class PostNotFoundException extends RuntimeException { + public PostNotFoundException() { + super(POST_NOT_FOUND); + } +} diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/post/repository/PostRepository.java b/BACK/spring-app/src/main/java/com/starchive/springapp/post/repository/PostRepository.java index 50ef03b..6622a6e 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/post/repository/PostRepository.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/post/repository/PostRepository.java @@ -1,7 +1,19 @@ package com.starchive.springapp.post.repository; import com.starchive.springapp.post.domain.Post; +import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface PostRepository extends JpaRepository { + @Query("select p from Post p " + + "where (:categoryIds is null or p.category.id in :categoryIds) " + + "and (:postIds is null or p.id in :postIds)") + Page findManyByCategoryIds(@Param("categoryIds") List categoryIds, + @Param("postIds") List postIds, + Pageable pageable); + } diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/post/service/PostService.java b/BACK/spring-app/src/main/java/com/starchive/springapp/post/service/PostService.java index 57cf878..3257bfc 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/post/service/PostService.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/post/service/PostService.java @@ -2,12 +2,22 @@ import com.starchive.springapp.category.domain.Category; import com.starchive.springapp.category.service.CategoryService; +import com.starchive.springapp.hashtag.dto.HashTagResponse; +import com.starchive.springapp.hashtag.service.HashTagService; import com.starchive.springapp.image.service.PostImageService; import com.starchive.springapp.post.domain.Post; import com.starchive.springapp.post.dto.PostCreateRequest; +import com.starchive.springapp.post.dto.PostDto; +import com.starchive.springapp.post.dto.PostListResponse; import com.starchive.springapp.post.repository.PostRepository; +import com.starchive.springapp.posthashtag.domain.PostHashTag; import com.starchive.springapp.posthashtag.service.PostHashTagService; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,6 +29,7 @@ public class PostService { private final PostHashTagService postHashTagService; private final CategoryService categoryService; private final PostImageService postImageService; + private final HashTagService hashTagService; public void createPost(PostCreateRequest request) { Category category = categoryService.findOne(request.getCategoryId()); @@ -32,4 +43,57 @@ public void createPost(PostCreateRequest request) { } + public Post findOne(Long postId) { + return postRepository.findById(postId).orElseThrow(); + } + + //todo: fetch join (양방향 연관관계) + public PostListResponse findPosts(Long categoryId, Long hashTagId, int pageNum, int pageSize) { + Pageable pageable = PageRequest.of(pageNum, pageSize); + List categoryIds = null; + if (categoryId != null) { + Category category = categoryService.findOne(categoryId); + categoryIds = extractCategoryIds(category); + + } + + List postIds = null; + if (hashTagId != null) { + postIds = new ArrayList<>(); + List postHashTags = postHashTagService.findManyByHashTag(hashTagId); + for (PostHashTag postHashTag : postHashTags) { + postIds.add(postHashTag.getPost().getId()); + } + if (postIds.isEmpty()) { + postIds = null; + } + } + + Page posts = postRepository.findManyByCategoryIds(categoryIds, postIds, pageable); + + Page dtoPage = getPostDtos(posts); + + return PostListResponse.from(dtoPage); + } + + private List extractCategoryIds(Category category) { + List categoryIds = new ArrayList<>(); + + categoryIds.add(category.getId()); + + if (category.getChildren() != null) { + category.getChildren().stream().forEach(child -> categoryIds.add(child.getId())); + } + + return categoryIds; + } + + + private Page getPostDtos(Page posts) { + Page dtoPage = posts.map(post -> { + List hashTagDtos = hashTagService.findManyByPost(post.getId()); + return PostDto.of(post, hashTagDtos); + }); + return dtoPage; + } } diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/posthashtag/domain/PostHashTag.java b/BACK/spring-app/src/main/java/com/starchive/springapp/posthashtag/domain/PostHashTag.java index b76e73e..3d1dbe3 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/posthashtag/domain/PostHashTag.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/posthashtag/domain/PostHashTag.java @@ -13,10 +13,12 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import lombok.AccessLevel; +import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Table(name = "PostHashTag") +@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class PostHashTag { @Id diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/posthashtag/service/PostHashTagService.java b/BACK/spring-app/src/main/java/com/starchive/springapp/posthashtag/service/PostHashTagService.java index ca0f9e4..476f0da 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/posthashtag/service/PostHashTagService.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/posthashtag/service/PostHashTagService.java @@ -34,4 +34,8 @@ public void storePostHashTag(List hashTagsIds, Post post) { postHashTagRepository.saveAll(postHashTags); } + + public List findManyByHashTag(Long hashTagId) { + return postHashTagRepository.findAllByHashTagId(hashTagId); + } } diff --git a/BACK/spring-app/src/test/java/com/starchive/springapp/post/service/PostServiceTest.java b/BACK/spring-app/src/test/java/com/starchive/springapp/post/service/PostServiceTest.java index afee606..f140145 100644 --- a/BACK/spring-app/src/test/java/com/starchive/springapp/post/service/PostServiceTest.java +++ b/BACK/spring-app/src/test/java/com/starchive/springapp/post/service/PostServiceTest.java @@ -10,9 +10,11 @@ import com.starchive.springapp.image.repository.PostImageRepository; import com.starchive.springapp.post.domain.Post; import com.starchive.springapp.post.dto.PostCreateRequest; +import com.starchive.springapp.post.dto.PostListResponse; import com.starchive.springapp.post.repository.PostRepository; import com.starchive.springapp.posthashtag.domain.PostHashTag; import com.starchive.springapp.posthashtag.repository.PostHashTagRepository; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; @@ -75,4 +77,26 @@ class PostServiceTest { } + @Test + public void 게시글_전체조회_통합_테스트() throws Exception { + //given + Category category = new Category("카테고리", null); + categoryRepository.save(category); + + Post post1 = new Post(null, "title1", "content", "author1", "123", LocalDateTime.now(), category); + Post post2 = new Post(null, "title2", "content", "author2", "123", LocalDateTime.now(), category); + postRepository.saveAll(List.of(post1, post2)); + + HashTag hashTag = new HashTag("tag1"); + hashTagRepository.save(hashTag); + postHashTagRepository.save(new PostHashTag(post1, hashTag)); + + //when + PostListResponse response = postService.findPosts(category.getId(), null, 0, 10); + + //then + assertThat(response.getPosts()).hasSize(2); + assertThat(response.getPosts()).extracting("title").containsExactlyInAnyOrder("title1", "title2"); + assertThat(response.getPosts().get(0).getHashTags()).extracting("name").containsExactly("tag1"); + } } \ No newline at end of file