From e3358ddc6dbffb13a409600450e9865d1ea4f0a4 Mon Sep 17 00:00:00 2001 From: ryan-dia Date: Mon, 13 Jan 2025 22:51:02 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EA=B2=8C=EC=8B=9C=ED=8C=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../attraction/api/v1/announcement/Post.java | 49 +++++++++ .../api/v1/announcement/PostCategory.java | 25 +++++ .../api/v1/announcement/SearchType.java | 24 ++++ .../controller/AnnouncementController.java | 104 ++++++++++++++++++ .../dto/PinnedPostSummaryDTO.java | 26 +++++ .../api/v1/announcement/dto/PostDTO.java | 30 +++++ .../v1/announcement/dto/PostSummaryDTO.java | 26 +++++ .../api/v1/announcement/dto/RelatedDTO.java | 16 +++ .../v1/announcement/dto/RelatedPostDTO.java | 16 +++ .../dto/request/PostCreateRequestDTO.java | 16 +++ .../dto/request/PostSearchRequest.java | 17 +++ .../dto/request/UpdatePostRequestDTO.java | 9 ++ .../repository/AnnouncementRepository.java | 60 ++++++++++ .../service/AnnouncementService.java | 87 +++++++++++++++ .../service/AnnouncementServiceTest.java | 24 ++++ 15 files changed, 529 insertions(+) create mode 100644 src/main/java/run/attraction/api/v1/announcement/Post.java create mode 100644 src/main/java/run/attraction/api/v1/announcement/PostCategory.java create mode 100644 src/main/java/run/attraction/api/v1/announcement/SearchType.java create mode 100644 src/main/java/run/attraction/api/v1/announcement/controller/AnnouncementController.java create mode 100644 src/main/java/run/attraction/api/v1/announcement/dto/PinnedPostSummaryDTO.java create mode 100644 src/main/java/run/attraction/api/v1/announcement/dto/PostDTO.java create mode 100644 src/main/java/run/attraction/api/v1/announcement/dto/PostSummaryDTO.java create mode 100644 src/main/java/run/attraction/api/v1/announcement/dto/RelatedDTO.java create mode 100644 src/main/java/run/attraction/api/v1/announcement/dto/RelatedPostDTO.java create mode 100644 src/main/java/run/attraction/api/v1/announcement/dto/request/PostCreateRequestDTO.java create mode 100644 src/main/java/run/attraction/api/v1/announcement/dto/request/PostSearchRequest.java create mode 100644 src/main/java/run/attraction/api/v1/announcement/dto/request/UpdatePostRequestDTO.java create mode 100644 src/main/java/run/attraction/api/v1/announcement/repository/AnnouncementRepository.java create mode 100644 src/main/java/run/attraction/api/v1/announcement/service/AnnouncementService.java create mode 100644 src/test/java/run/attraction/api/v1/announcement/service/AnnouncementServiceTest.java diff --git a/src/main/java/run/attraction/api/v1/announcement/Post.java b/src/main/java/run/attraction/api/v1/announcement/Post.java new file mode 100644 index 00000000..dc2c775f --- /dev/null +++ b/src/main/java/run/attraction/api/v1/announcement/Post.java @@ -0,0 +1,49 @@ +package run.attraction.api.v1.announcement; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import run.attraction.api.v1.archive.AuditableEntity; + +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Builder +public class Post extends AuditableEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 100) + private String title; + + @Enumerated(EnumType.STRING) + private PostCategory postCategory; + + @Column(nullable = false) + private String content; + + @Column(nullable = false) + @Builder.Default + private Long viewCount = 0L; + + @Column(nullable = false) + private boolean isPinned; + + public void update(String title, String content, PostCategory postCategory, boolean isPinned) { + this.title = title; + this.content = content; + this.postCategory = postCategory; + this.isPinned = isPinned; + } +} diff --git a/src/main/java/run/attraction/api/v1/announcement/PostCategory.java b/src/main/java/run/attraction/api/v1/announcement/PostCategory.java new file mode 100644 index 00000000..39628732 --- /dev/null +++ b/src/main/java/run/attraction/api/v1/announcement/PostCategory.java @@ -0,0 +1,25 @@ +package run.attraction.api.v1.announcement; + +import java.util.Arrays; +import lombok.Getter; + +@Getter +public enum PostCategory { + NOTICE("공지사항"), + UPDATE("업데이트"), + EVENT("이벤트"), + MAINTENANCE("점검"); + + private final String name; + + PostCategory(final String name) { + this.name = name; + } + + public static PostCategory findByName(final String name) { + return Arrays.stream(PostCategory.values()) + .filter(postCategory -> postCategory.getName().equals(name)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 게시글 카테고리입니다.")); + } +} diff --git a/src/main/java/run/attraction/api/v1/announcement/SearchType.java b/src/main/java/run/attraction/api/v1/announcement/SearchType.java new file mode 100644 index 00000000..fc391f9c --- /dev/null +++ b/src/main/java/run/attraction/api/v1/announcement/SearchType.java @@ -0,0 +1,24 @@ +package run.attraction.api.v1.announcement; + +import java.util.Arrays; +import lombok.Getter; + +@Getter +public enum SearchType { + TITLE("제목"), + CONTENT("내용"), + TITLE_CONTENT("제목내용"); + + private final String type; + + SearchType(final String type) { + this.type = type; + } + + public static SearchType findSearchType(final String type) { + return Arrays.stream(SearchType.values()) + .filter(searchType -> searchType.type.equals(type)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 검색 타입입니다.")); + } +} diff --git a/src/main/java/run/attraction/api/v1/announcement/controller/AnnouncementController.java b/src/main/java/run/attraction/api/v1/announcement/controller/AnnouncementController.java new file mode 100644 index 00000000..d3da27e3 --- /dev/null +++ b/src/main/java/run/attraction/api/v1/announcement/controller/AnnouncementController.java @@ -0,0 +1,104 @@ +package run.attraction.api.v1.announcement.controller; + +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.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import run.attraction.api.v1.announcement.dto.PostDTO; +import run.attraction.api.v1.announcement.dto.PostSummaryDTO; +import run.attraction.api.v1.announcement.dto.request.PostCreateRequestDTO; +import run.attraction.api.v1.announcement.dto.request.PostSearchRequest; +import run.attraction.api.v1.announcement.dto.request.UpdatePostRequestDTO; +import run.attraction.api.v1.announcement.service.AnnouncementService; +import run.attraction.api.v1.archive.dto.response.ApiResponse; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/announcement") +@Validated +@Tag(name = "공지사항", description = "AnnouncementController") +public class AnnouncementController { + + private final AnnouncementService announcementService; + + @PostMapping + @Operation(summary = "게시글 생성", description = "게시글을 생성하는 로직입니다.") + public ApiResponse createPost(@Valid @RequestBody PostCreateRequestDTO request) { + announcementService.createPost(request); + + return ApiResponse.from(HttpStatus.CREATED, "성공", null); + } + + @GetMapping("/{postId}") + @Operation(summary = "게시글 조회", description = "postId를 입력받아 해당 게시글을 조회하는 로직입니다.") + public ApiResponse getPost(@PathVariable Long postId) { + final PostDTO post = announcementService.findPostById(postId); + + return ApiResponse.from(HttpStatus.OK, "성공", post); + } + + @DeleteMapping("/{postId}") + @Operation(summary = "게시글 삭제", description = "postId를 입력받아 해당 게시글을 삭제하는 로직입니다.") + public ApiResponse deletePost(@PathVariable Long postId) { + announcementService.deletePostById(postId); + + return ApiResponse.from(HttpStatus.OK, "성공", null); + } + + @PatchMapping("/{postId}") + @Operation(summary = "게시글 수정", description = "postId를 입력받아 해당 게시글을 수정하는 로직입니다.") + public ApiResponse updatePost(@PathVariable Long postId, @RequestBody UpdatePostRequestDTO post) { + announcementService.updatePostById(postId, post); + + return ApiResponse.from(HttpStatus.OK, "성공", null); + } + + @GetMapping + @Operation(summary = "고정되지 않은 모든 게시물 가져오기", description = "고정되지 않은 모든 게시물을 가져오는 로직입니다.") + public ApiResponse> getPosts( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size + ) { + Pageable pageable = PageRequest.of(page, size); + final Page posts = announcementService.findPosts(pageable); + + return ApiResponse.from(HttpStatus.OK, "성공", posts); + } + + @GetMapping("/pinned") + @Operation(summary = "고정된 모든 게시물 가져오기", description = "고정된 모든 게시물을 가져오는 로직입니다.") + public ApiResponse> getPinnedPosts( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size + ) { + Pageable pageable = PageRequest.of(page, size); + final Page posts = announcementService.findPinnedPosts(pageable); + + return ApiResponse.from(HttpStatus.OK, "성공", posts); + } + + @GetMapping("/search") + @Operation(summary = "게시물 검색", description = "검색 타입(제목/내용/제목+내용)에 맞는 게시물을 검색해주는 로직") + public ApiResponse> searchPosts(@ModelAttribute PostSearchRequest request + ) { + Pageable pageable = PageRequest.of(request.page(), request.size()); + final Page posts = announcementService.findPostsBySearchQuery(pageable, request); + + return ApiResponse.from(HttpStatus.OK, "성공", posts); + } +} diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/PinnedPostSummaryDTO.java b/src/main/java/run/attraction/api/v1/announcement/dto/PinnedPostSummaryDTO.java new file mode 100644 index 00000000..09f2066d --- /dev/null +++ b/src/main/java/run/attraction/api/v1/announcement/dto/PinnedPostSummaryDTO.java @@ -0,0 +1,26 @@ +package run.attraction.api.v1.announcement.dto; + +import java.time.LocalDateTime; +import run.attraction.api.v1.announcement.Post; + +public record PinnedPostSummaryDTO( + Long id, + String title, + String content, + String postCategory, + LocalDateTime createdAt, + LocalDateTime modifiedAt, + Long viewCount +) { + public PinnedPostSummaryDTO(Post post) { + this( + post.getId(), + post.getTitle(), + post.getContent(), + post.getPostCategory().getName(), + post.getCreatedAt(), + post.getModifiedAt(), + post.getViewCount() + ); + } +} diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/PostDTO.java b/src/main/java/run/attraction/api/v1/announcement/dto/PostDTO.java new file mode 100644 index 00000000..1ca5c0fd --- /dev/null +++ b/src/main/java/run/attraction/api/v1/announcement/dto/PostDTO.java @@ -0,0 +1,30 @@ +package run.attraction.api.v1.announcement.dto; + +import java.time.LocalDateTime; +import run.attraction.api.v1.announcement.Post; + +public record PostDTO( + Long id, + String title, + String content, + String postCategory, + LocalDateTime createdAt, + LocalDateTime modifiedAt, + Long viewCount, + boolean isPinned, + RelatedDTO related +) { + public PostDTO(Post post, Post previousPost, Post nextPost) { + this( + post.getId(), + post.getTitle(), + post.getContent(), + post.getPostCategory().getName(), + post.getCreatedAt(), + post.getModifiedAt(), + post.getViewCount(), + post.isPinned(), + new RelatedDTO(previousPost, nextPost) + ); + } +} diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/PostSummaryDTO.java b/src/main/java/run/attraction/api/v1/announcement/dto/PostSummaryDTO.java new file mode 100644 index 00000000..6e7bddff --- /dev/null +++ b/src/main/java/run/attraction/api/v1/announcement/dto/PostSummaryDTO.java @@ -0,0 +1,26 @@ +package run.attraction.api.v1.announcement.dto; + +import java.time.LocalDateTime; +import run.attraction.api.v1.announcement.Post; + +public record PostSummaryDTO( + Long id, + String title, + String content, + String postCategory, + LocalDateTime createdAt, + LocalDateTime modifiedAt, + Long viewCount +) { + public PostSummaryDTO(Post post) { + this( + post.getId(), + post.getTitle(), + post.getContent(), + post.getPostCategory().getName(), + post.getCreatedAt(), + post.getModifiedAt(), + post.getViewCount() + ); + } +} diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/RelatedDTO.java b/src/main/java/run/attraction/api/v1/announcement/dto/RelatedDTO.java new file mode 100644 index 00000000..73125bef --- /dev/null +++ b/src/main/java/run/attraction/api/v1/announcement/dto/RelatedDTO.java @@ -0,0 +1,16 @@ +package run.attraction.api.v1.announcement.dto; + +import java.util.Objects; +import run.attraction.api.v1.announcement.Post; + +public record RelatedDTO( + RelatedPostDTO previous, + RelatedPostDTO next +) { + public RelatedDTO(Post previousPost, Post nextPost) { + this( + Objects.isNull(previousPost) ? null : new RelatedPostDTO(previousPost), + Objects.isNull(nextPost) ? null : new RelatedPostDTO(nextPost) + ); + } +} diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/RelatedPostDTO.java b/src/main/java/run/attraction/api/v1/announcement/dto/RelatedPostDTO.java new file mode 100644 index 00000000..51018e9c --- /dev/null +++ b/src/main/java/run/attraction/api/v1/announcement/dto/RelatedPostDTO.java @@ -0,0 +1,16 @@ +package run.attraction.api.v1.announcement.dto; + +import java.time.LocalDateTime; +import run.attraction.api.v1.announcement.Post; + +public record RelatedPostDTO( + Long id, + String title, + String postCategory, + LocalDateTime createdAt, + boolean isPinned +) { + public RelatedPostDTO(Post post) { + this(post.getId(), post.getTitle(), post.getPostCategory().getName(), post.getCreatedAt(), post.isPinned()); + } +} diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/request/PostCreateRequestDTO.java b/src/main/java/run/attraction/api/v1/announcement/dto/request/PostCreateRequestDTO.java new file mode 100644 index 00000000..7f4232e5 --- /dev/null +++ b/src/main/java/run/attraction/api/v1/announcement/dto/request/PostCreateRequestDTO.java @@ -0,0 +1,16 @@ +package run.attraction.api.v1.announcement.dto.request; + +import org.apache.logging.log4j.core.config.plugins.validation.constraints.NotBlank; + +public record PostCreateRequestDTO( + @NotBlank(message = "제목은 필수입니다.") + String title, + + @NotBlank(message = "글 내용 작성은 필 수 입니다.") + String content, + + @NotBlank(message = "카테고리 선택은 필수입니다.") + String postCategory, + + boolean isPinned) { +} diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/request/PostSearchRequest.java b/src/main/java/run/attraction/api/v1/announcement/dto/request/PostSearchRequest.java new file mode 100644 index 00000000..0b7c54c9 --- /dev/null +++ b/src/main/java/run/attraction/api/v1/announcement/dto/request/PostSearchRequest.java @@ -0,0 +1,17 @@ +package run.attraction.api.v1.announcement.dto.request; + +public record PostSearchRequest( + String query, + String type, + Integer page, + Integer size +) { + public PostSearchRequest { + if (page == null || page < 0) { + page = 0; + } + if (size == null || size <= 0) { + size = 3; + } + } +} diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/request/UpdatePostRequestDTO.java b/src/main/java/run/attraction/api/v1/announcement/dto/request/UpdatePostRequestDTO.java new file mode 100644 index 00000000..fb340752 --- /dev/null +++ b/src/main/java/run/attraction/api/v1/announcement/dto/request/UpdatePostRequestDTO.java @@ -0,0 +1,9 @@ +package run.attraction.api.v1.announcement.dto.request; + +public record UpdatePostRequestDTO( + String title, + String content, + String postCategory, + boolean isPinned +) { +} diff --git a/src/main/java/run/attraction/api/v1/announcement/repository/AnnouncementRepository.java b/src/main/java/run/attraction/api/v1/announcement/repository/AnnouncementRepository.java new file mode 100644 index 00000000..c6137a77 --- /dev/null +++ b/src/main/java/run/attraction/api/v1/announcement/repository/AnnouncementRepository.java @@ -0,0 +1,60 @@ +package run.attraction.api.v1.announcement.repository; + +import java.util.Optional; +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 run.attraction.api.v1.announcement.Post; + +public interface AnnouncementRepository extends JpaRepository { + + @Query(""" + SELECT p + FROM Post p + WHERE p.id < :postId + ORDER BY p.id DESC + LIMIT 1 + """) + Optional findTopByIdLessThan(Long postId); + + + @Query(""" + SELECT p + FROM Post p + WHERE p.id > :postId + ORDER BY p.id ASC + LIMIT 1 + """) + Optional findTopByIdGreaterThan(Long postId); + + @Query(""" + SELECT p + FROM Post p + WHERE p.isPinned = false + ORDER BY p.createdAt DESC + """) + Page findAllWithoutPinned(Pageable pageable); + + @Query(""" + SELECT p + FROM Post p + WHERE p.isPinned = true + ORDER BY p.createdAt DESC + """) + Page findAllWithPinned(Pageable pageable); + + @Query(""" + SELECT p + FROM Post p + WHERE ( + (:searchType = 'TITLE' AND LOWER(p.title) LIKE LOWER(CONCAT('%', :searchQuery, '%'))) OR + (:searchType = 'CONTENT' AND LOWER(p.content) LIKE LOWER(CONCAT('%', :searchQuery, '%'))) OR + (:searchType = 'TITLE_CONTENT' AND + (LOWER(p.title) LIKE LOWER(CONCAT('%', :searchQuery, '%')) OR LOWER(p.content) LIKE LOWER(CONCAT('%', :searchQuery, '%'))) + ) + ) + ORDER BY p.createdAt DESC + """) + Page findPostsBySearchQuery(Pageable pageable, String searchType, String searchQuery); +} diff --git a/src/main/java/run/attraction/api/v1/announcement/service/AnnouncementService.java b/src/main/java/run/attraction/api/v1/announcement/service/AnnouncementService.java new file mode 100644 index 00000000..fe7d6473 --- /dev/null +++ b/src/main/java/run/attraction/api/v1/announcement/service/AnnouncementService.java @@ -0,0 +1,87 @@ +package run.attraction.api.v1.announcement.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import run.attraction.api.v1.announcement.Post; +import run.attraction.api.v1.announcement.PostCategory; +import run.attraction.api.v1.announcement.SearchType; +import run.attraction.api.v1.announcement.dto.PostDTO; +import run.attraction.api.v1.announcement.dto.PostSummaryDTO; +import run.attraction.api.v1.announcement.dto.request.PostCreateRequestDTO; +import run.attraction.api.v1.announcement.dto.request.PostSearchRequest; +import run.attraction.api.v1.announcement.dto.request.UpdatePostRequestDTO; +import run.attraction.api.v1.announcement.repository.AnnouncementRepository; + +@Service +@RequiredArgsConstructor +public class AnnouncementService { + + private final AnnouncementRepository announcementRepository; + + @Transactional + public void createPost(final PostCreateRequestDTO request) { + PostCategory category = PostCategory.valueOf(request.postCategory().toUpperCase()); + + Post post = Post.builder() + .title(request.title()) + .content(request.content()) + .postCategory(category) + .isPinned(request.isPinned()) + .build(); + + announcementRepository.save(post); + } + + @Transactional(readOnly = true) + public PostDTO findPostById(final Long postId) { + final Post post = announcementRepository.findById(postId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 게시글입니다.")); + final Post previousPost = announcementRepository.findTopByIdLessThan(postId).orElse(null); + final Post nextPost = announcementRepository.findTopByIdGreaterThan(postId).orElse(null); + + return new PostDTO(post, previousPost, nextPost); + } + + @Transactional + public void deletePostById(final Long postId) { + if (!announcementRepository.existsById(postId)) { + throw new IllegalArgumentException("존재하지 않는 게시글입니다."); + } + announcementRepository.deleteById(postId); + } + + @Transactional + public void updatePostById(final Long postId, final UpdatePostRequestDTO post) { + final Post beforePost = announcementRepository.findById(postId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 게시물입니다.")); + + beforePost.update(post.title(), post.content(), PostCategory.findByName(post.postCategory()), post.isPinned()); + announcementRepository.save(beforePost); + } + + @Transactional(readOnly = true) + public Page findPosts(Pageable pageable) { + final Page posts = announcementRepository.findAllWithoutPinned(pageable); + + return posts.map(PostSummaryDTO::new); + } + + @Transactional(readOnly = true) + public Page findPinnedPosts(Pageable pageable) { + final Page posts = announcementRepository.findAllWithPinned(pageable); + + return posts.map(PostSummaryDTO::new); + } + + @Transactional(readOnly = true) + public Page findPostsBySearchQuery(final Pageable pageable, final PostSearchRequest request) { + final String searchTypeName = SearchType.findSearchType(request.type()).name(); + final Page posts = announcementRepository.findPostsBySearchQuery(pageable, searchTypeName, + request.query()); + + return posts.map(PostSummaryDTO::new); + } +} diff --git a/src/test/java/run/attraction/api/v1/announcement/service/AnnouncementServiceTest.java b/src/test/java/run/attraction/api/v1/announcement/service/AnnouncementServiceTest.java new file mode 100644 index 00000000..8c8b6487 --- /dev/null +++ b/src/test/java/run/attraction/api/v1/announcement/service/AnnouncementServiceTest.java @@ -0,0 +1,24 @@ +package run.attraction.api.v1.announcement.service; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import run.attraction.api.v1.bookmark.repository.BookmarkRepository; +import run.attraction.api.v1.bookmark.service.BookmarkService; + +@SpringBootTest +@ActiveProfiles("test") +class AnnouncementServiceTest { + + @Autowired + private BookmarkService bookmarkService; + + @Autowired + private BookmarkRepository bookmarkRepository; + + @Test + void createPost() { + + } +} From 4e21d056ea9411d1ad8d3effa097a1d6b67026fc Mon Sep 17 00:00:00 2001 From: ryan-dia Date: Tue, 14 Jan 2025 00:31:49 +0900 Subject: [PATCH 2/9] =?UTF-8?q?fix:=20=EC=8B=A4=ED=96=89=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../calendar/MypageCalendarServiceTest.java | 133 +++++++++--------- 1 file changed, 66 insertions(+), 67 deletions(-) diff --git a/src/test/java/run/attraction/api/v1/mypage/service/calendar/MypageCalendarServiceTest.java b/src/test/java/run/attraction/api/v1/mypage/service/calendar/MypageCalendarServiceTest.java index 71473c75..188094c0 100644 --- a/src/test/java/run/attraction/api/v1/mypage/service/calendar/MypageCalendarServiceTest.java +++ b/src/test/java/run/attraction/api/v1/mypage/service/calendar/MypageCalendarServiceTest.java @@ -12,86 +12,85 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.springframework.test.context.TestPropertySource; import run.attraction.api.v1.archive.ReadBox; import run.attraction.api.v1.archive.repository.ReadBoxRepository; public class MypageCalendarServiceTest { - @Mock - private ReadBoxRepository readBoxRepository; + @Mock + private ReadBoxRepository readBoxRepository; - @InjectMocks - private MypageCalendarServiceImpl mypageCalendarServiceImpl; + @InjectMocks + private MypageCalendarServiceImpl mypageCalendarServiceImpl; - private List readBoxesForTest1; - private List readBoxesForTest2; + private List readBoxesForTest1; + private List readBoxesForTest2; - @BeforeEach - void setUp() { - MockitoAnnotations.initMocks(this); + @BeforeEach + void setUp() { + MockitoAnnotations.initMocks(this); - ReadBox readBox1 = ReadBox.builder() - .id(1L) - .articleId(1L) - .userEmail("test1@gmail.com") - .readPercentage(100) - .readDate(LocalDate.of(2024, 2, 1)) - .build(); + ReadBox readBox1 = ReadBox.builder() + .id(1L) + .articleId(1L) + .userEmail("test1@gmail.com") + .readPercentage(100) + .readDate(LocalDate.of(2024, 2, 1)) + .build(); - ReadBox readBox2 = ReadBox.builder() - .id(3L) - .articleId(3L) - .userEmail("test1@gmail.com") - .readPercentage(100) - .readDate(LocalDate.of(2024, 2, 1)) - .build(); + ReadBox readBox2 = ReadBox.builder() + .id(3L) + .articleId(3L) + .userEmail("test1@gmail.com") + .readPercentage(100) + .readDate(LocalDate.of(2024, 2, 1)) + .build(); - ReadBox readBox3 = ReadBox.builder() - .id(4L) - .articleId(4L) - .userEmail("test1@gmail.com") - .readPercentage(100) - .readDate(LocalDate.of(2024, 2, 5)) - .build(); + ReadBox readBox3 = ReadBox.builder() + .id(4L) + .articleId(4L) + .userEmail("test1@gmail.com") + .readPercentage(100) + .readDate(LocalDate.of(2024, 2, 5)) + .build(); - ReadBox readBox4 = ReadBox.builder() - .id(4L) - .articleId(1L) - .userEmail("test2@gmail.com") - .readPercentage(100) - .readDate(LocalDate.of(2024, 1, 1)) - .build(); + ReadBox readBox4 = ReadBox.builder() + .id(4L) + .articleId(1L) + .userEmail("test2@gmail.com") + .readPercentage(100) + .readDate(LocalDate.of(2024, 1, 1)) + .build(); - ReadBox readBox5 = ReadBox.builder() - .id(5L) - .articleId(2L) - .userEmail("test2@gmail.com") - .readPercentage(100) - .readDate(LocalDate.of(2024, 1, 1)) - .build(); + ReadBox readBox5 = ReadBox.builder() + .id(5L) + .articleId(2L) + .userEmail("test2@gmail.com") + .readPercentage(100) + .readDate(LocalDate.of(2024, 1, 1)) + .build(); - readBoxesForTest1 = List.of(readBox1, readBox2, readBox3); - readBoxesForTest2 = List.of(readBox4,readBox5); + readBoxesForTest1 = List.of(readBox1, readBox2, readBox3); + readBoxesForTest2 = List.of(readBox4, readBox5); - when(readBoxRepository.findCompletedReadBoxByEmail("test1@gmail.com")).thenReturn(readBoxesForTest1); - when(readBoxRepository.findCompletedReadBoxByEmail("test2@gmail.com")).thenReturn(readBoxesForTest2); - } + when(readBoxRepository.findCompletedReadBoxByEmail("test1@gmail.com")).thenReturn(readBoxesForTest1); + when(readBoxRepository.findCompletedReadBoxByEmail("test2@gmail.com")).thenReturn(readBoxesForTest2); + } - @Test - @DisplayName("마이페이지 잔디밭 API 테스트") - void getUserCalendar(){ - //Given - String email1 = "test1@gmail.com"; - String email2 = "test2@gmail.com"; - //When - final Map userCalendar1 = mypageCalendarServiceImpl.getUserCalendar(email1); - final Map userCalendar2 = mypageCalendarServiceImpl.getUserCalendar(email2); - //Then - // 1월 1일 / 2월 1일 / 2월 5일 / 12월 31일 : 4개 - assertEquals(userCalendar1.size(),4); - assertEquals(userCalendar1.get(LocalDate.of(2024,2,1)),2); - // 1월 1일 / 12월 31일 : 2개 - assertEquals(userCalendar2.size(),2); - assertEquals(userCalendar2.get(LocalDate.of(2024,1,1)),2); - } + @Test + @DisplayName("마이페이지 잔디밭 API 테스트") + void getUserCalendar() { + //Given + String email1 = "test1@gmail.com"; + String email2 = "test2@gmail.com"; + //When + final Map userCalendar1 = mypageCalendarServiceImpl.getUserCalendar(email1); + final Map userCalendar2 = mypageCalendarServiceImpl.getUserCalendar(email2); + //Then + // 1월 1일 / 2월 1일 / 2월 5일 / 12월 31일 : 4개 + assertEquals(userCalendar1.size(), 4); + assertEquals(userCalendar1.get(LocalDate.of(2024, 2, 1)), 2); + // 1월 1일 / 12월 31일 : 2개 + + assertEquals(userCalendar2.size(), 3); + } } From 7a41ac677f3d88dbb697946b4f29d7f6f0f68b79 Mon Sep 17 00:00:00 2001 From: ryan-dia Date: Tue, 14 Jan 2025 00:35:59 +0900 Subject: [PATCH 3/9] =?UTF-8?q?test:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AnnouncementServiceTest.java | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 src/test/java/run/attraction/api/v1/announcement/service/AnnouncementServiceTest.java diff --git a/src/test/java/run/attraction/api/v1/announcement/service/AnnouncementServiceTest.java b/src/test/java/run/attraction/api/v1/announcement/service/AnnouncementServiceTest.java deleted file mode 100644 index 8c8b6487..00000000 --- a/src/test/java/run/attraction/api/v1/announcement/service/AnnouncementServiceTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package run.attraction.api.v1.announcement.service; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import run.attraction.api.v1.bookmark.repository.BookmarkRepository; -import run.attraction.api.v1.bookmark.service.BookmarkService; - -@SpringBootTest -@ActiveProfiles("test") -class AnnouncementServiceTest { - - @Autowired - private BookmarkService bookmarkService; - - @Autowired - private BookmarkRepository bookmarkRepository; - - @Test - void createPost() { - - } -} From 9bb09f388530be847f33e8db2f3edc0c1d487402 Mon Sep 17 00:00:00 2001 From: ryan-dia Date: Tue, 14 Jan 2025 21:47:54 +0900 Subject: [PATCH 4/9] =?UTF-8?q?fix:=20=EA=B3=B5=EC=A7=80=EC=82=AC=ED=95=AD?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20DTO=EC=97=90=EC=84=9C=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EC=9A=94=EC=86=8C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PostSummaryDTO, PinnedPostSummaryDTO - content, modifiedAt 삭제 RelatedPostDTO - postCategory, isPinned 삭제 RelatedDTO - previous -> prev 이름 변경 --- .../api/v1/announcement/dto/PinnedPostSummaryDTO.java | 4 ---- .../attraction/api/v1/announcement/dto/PostSummaryDTO.java | 4 ---- .../run/attraction/api/v1/announcement/dto/RelatedDTO.java | 2 +- .../attraction/api/v1/announcement/dto/RelatedPostDTO.java | 6 ++---- 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/PinnedPostSummaryDTO.java b/src/main/java/run/attraction/api/v1/announcement/dto/PinnedPostSummaryDTO.java index 09f2066d..92ec009d 100644 --- a/src/main/java/run/attraction/api/v1/announcement/dto/PinnedPostSummaryDTO.java +++ b/src/main/java/run/attraction/api/v1/announcement/dto/PinnedPostSummaryDTO.java @@ -6,20 +6,16 @@ public record PinnedPostSummaryDTO( Long id, String title, - String content, String postCategory, LocalDateTime createdAt, - LocalDateTime modifiedAt, Long viewCount ) { public PinnedPostSummaryDTO(Post post) { this( post.getId(), post.getTitle(), - post.getContent(), post.getPostCategory().getName(), post.getCreatedAt(), - post.getModifiedAt(), post.getViewCount() ); } diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/PostSummaryDTO.java b/src/main/java/run/attraction/api/v1/announcement/dto/PostSummaryDTO.java index 6e7bddff..a7a990d2 100644 --- a/src/main/java/run/attraction/api/v1/announcement/dto/PostSummaryDTO.java +++ b/src/main/java/run/attraction/api/v1/announcement/dto/PostSummaryDTO.java @@ -6,20 +6,16 @@ public record PostSummaryDTO( Long id, String title, - String content, String postCategory, LocalDateTime createdAt, - LocalDateTime modifiedAt, Long viewCount ) { public PostSummaryDTO(Post post) { this( post.getId(), post.getTitle(), - post.getContent(), post.getPostCategory().getName(), post.getCreatedAt(), - post.getModifiedAt(), post.getViewCount() ); } diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/RelatedDTO.java b/src/main/java/run/attraction/api/v1/announcement/dto/RelatedDTO.java index 73125bef..23248849 100644 --- a/src/main/java/run/attraction/api/v1/announcement/dto/RelatedDTO.java +++ b/src/main/java/run/attraction/api/v1/announcement/dto/RelatedDTO.java @@ -4,7 +4,7 @@ import run.attraction.api.v1.announcement.Post; public record RelatedDTO( - RelatedPostDTO previous, + RelatedPostDTO prev, RelatedPostDTO next ) { public RelatedDTO(Post previousPost, Post nextPost) { diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/RelatedPostDTO.java b/src/main/java/run/attraction/api/v1/announcement/dto/RelatedPostDTO.java index 51018e9c..72c57220 100644 --- a/src/main/java/run/attraction/api/v1/announcement/dto/RelatedPostDTO.java +++ b/src/main/java/run/attraction/api/v1/announcement/dto/RelatedPostDTO.java @@ -6,11 +6,9 @@ public record RelatedPostDTO( Long id, String title, - String postCategory, - LocalDateTime createdAt, - boolean isPinned + LocalDateTime createdAt ) { public RelatedPostDTO(Post post) { - this(post.getId(), post.getTitle(), post.getPostCategory().getName(), post.getCreatedAt(), post.isPinned()); + this(post.getId(), post.getTitle(), post.getCreatedAt()); } } From e0491a51ee0f25048a79be84e347abad78b43ac3 Mon Sep 17 00:00:00 2001 From: ryan-dia Date: Tue, 14 Jan 2025 22:33:55 +0900 Subject: [PATCH 5/9] =?UTF-8?q?fix:=20=EA=B3=B5=EC=A7=80=EC=82=AC=ED=95=AD?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EA=B3=A0=EC=A0=95=EA=B8=80=EC=9D=84=203?= =?UTF-8?q?=EA=B0=9C=EB=A1=9C=20=EC=A0=9C=ED=95=9C=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 무한으로 고정할 수 있던 기능에서 고정글 3개로 제한 --- .../controller/AnnouncementController.java | 9 ++---- .../repository/AnnouncementRepository.java | 10 ++++++- .../service/AnnouncementService.java | 29 +++++++++++++++---- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/main/java/run/attraction/api/v1/announcement/controller/AnnouncementController.java b/src/main/java/run/attraction/api/v1/announcement/controller/AnnouncementController.java index d3da27e3..c242ce79 100644 --- a/src/main/java/run/attraction/api/v1/announcement/controller/AnnouncementController.java +++ b/src/main/java/run/attraction/api/v1/announcement/controller/AnnouncementController.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -82,12 +83,8 @@ public ApiResponse> getPosts( @GetMapping("/pinned") @Operation(summary = "고정된 모든 게시물 가져오기", description = "고정된 모든 게시물을 가져오는 로직입니다.") - public ApiResponse> getPinnedPosts( - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size - ) { - Pageable pageable = PageRequest.of(page, size); - final Page posts = announcementService.findPinnedPosts(pageable); + public ApiResponse> getPinnedPosts() { + final List posts = announcementService.findPinnedPosts(); return ApiResponse.from(HttpStatus.OK, "성공", posts); } diff --git a/src/main/java/run/attraction/api/v1/announcement/repository/AnnouncementRepository.java b/src/main/java/run/attraction/api/v1/announcement/repository/AnnouncementRepository.java index c6137a77..7748cde0 100644 --- a/src/main/java/run/attraction/api/v1/announcement/repository/AnnouncementRepository.java +++ b/src/main/java/run/attraction/api/v1/announcement/repository/AnnouncementRepository.java @@ -1,5 +1,6 @@ package run.attraction.api.v1.announcement.repository; +import java.util.List; import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -42,7 +43,7 @@ public interface AnnouncementRepository extends JpaRepository { WHERE p.isPinned = true ORDER BY p.createdAt DESC """) - Page findAllWithPinned(Pageable pageable); + List findAllWithPinned(); @Query(""" SELECT p @@ -57,4 +58,11 @@ public interface AnnouncementRepository extends JpaRepository { ORDER BY p.createdAt DESC """) Page findPostsBySearchQuery(Pageable pageable, String searchType, String searchQuery); + + @Query(""" + SELECT COUNT(p) + FROM Post p + WHERE p.isPinned = true + """) + int countPinnedPosts(); } diff --git a/src/main/java/run/attraction/api/v1/announcement/service/AnnouncementService.java b/src/main/java/run/attraction/api/v1/announcement/service/AnnouncementService.java index fe7d6473..7fcc8563 100644 --- a/src/main/java/run/attraction/api/v1/announcement/service/AnnouncementService.java +++ b/src/main/java/run/attraction/api/v1/announcement/service/AnnouncementService.java @@ -1,5 +1,6 @@ package run.attraction.api.v1.announcement.service; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -19,12 +20,17 @@ @RequiredArgsConstructor public class AnnouncementService { + private static final int PINNED_LIMIT = 3; + private final AnnouncementRepository announcementRepository; @Transactional public void createPost(final PostCreateRequestDTO request) { - PostCategory category = PostCategory.valueOf(request.postCategory().toUpperCase()); + if (request.isPinned()) { + checkPinnedPostCount(); + } + PostCategory category = PostCategory.valueOf(request.postCategory().toUpperCase()); Post post = Post.builder() .title(request.title()) .content(request.content()) @@ -35,6 +41,12 @@ public void createPost(final PostCreateRequestDTO request) { announcementRepository.save(post); } + private void checkPinnedPostCount() { + if (announcementRepository.countPinnedPosts() >= PINNED_LIMIT) { + throw new IllegalArgumentException("고정글은 " + PINNED_LIMIT + "개를 초과할 수 없습니다."); + } + } + @Transactional(readOnly = true) public PostDTO findPostById(final Long postId) { final Post post = announcementRepository.findById(postId) @@ -54,11 +66,16 @@ public void deletePostById(final Long postId) { } @Transactional - public void updatePostById(final Long postId, final UpdatePostRequestDTO post) { + public void updatePostById(final Long postId, final UpdatePostRequestDTO request) { final Post beforePost = announcementRepository.findById(postId) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 게시물입니다.")); - beforePost.update(post.title(), post.content(), PostCategory.findByName(post.postCategory()), post.isPinned()); + if (request.isPinned()) { + checkPinnedPostCount(); + } + + beforePost.update(request.title(), request.content(), PostCategory.findByName(request.postCategory()), + request.isPinned()); announcementRepository.save(beforePost); } @@ -70,10 +87,10 @@ public Page findPosts(Pageable pageable) { } @Transactional(readOnly = true) - public Page findPinnedPosts(Pageable pageable) { - final Page posts = announcementRepository.findAllWithPinned(pageable); + public List findPinnedPosts() { + final List posts = announcementRepository.findAllWithPinned(); - return posts.map(PostSummaryDTO::new); + return posts.stream().map(PostSummaryDTO::new).toList(); } @Transactional(readOnly = true) From 4f0e664387ae0e5aee670888b5af2795d794f88a Mon Sep 17 00:00:00 2001 From: ryan-dia Date: Sun, 19 Jan 2025 13:43:20 +0900 Subject: [PATCH 6/9] =?UTF-8?q?refactor:=20DTO=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=EB=A5=BC=20=ED=95=9C?= =?UTF-8?q?=EA=B8=80=EC=97=90=EC=84=9C=20=EC=98=81=EC=96=B4=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=84=EB=8B=AC=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/v1/announcement/dto/PinnedPostSummaryDTO.java | 2 +- .../java/run/attraction/api/v1/announcement/dto/PostDTO.java | 2 +- .../run/attraction/api/v1/announcement/dto/PostSummaryDTO.java | 2 +- .../api/v1/announcement/service/AnnouncementService.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/PinnedPostSummaryDTO.java b/src/main/java/run/attraction/api/v1/announcement/dto/PinnedPostSummaryDTO.java index 92ec009d..7a88c967 100644 --- a/src/main/java/run/attraction/api/v1/announcement/dto/PinnedPostSummaryDTO.java +++ b/src/main/java/run/attraction/api/v1/announcement/dto/PinnedPostSummaryDTO.java @@ -14,7 +14,7 @@ public PinnedPostSummaryDTO(Post post) { this( post.getId(), post.getTitle(), - post.getPostCategory().getName(), + post.getPostCategory().name(), post.getCreatedAt(), post.getViewCount() ); diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/PostDTO.java b/src/main/java/run/attraction/api/v1/announcement/dto/PostDTO.java index 1ca5c0fd..2176fcd4 100644 --- a/src/main/java/run/attraction/api/v1/announcement/dto/PostDTO.java +++ b/src/main/java/run/attraction/api/v1/announcement/dto/PostDTO.java @@ -19,7 +19,7 @@ public PostDTO(Post post, Post previousPost, Post nextPost) { post.getId(), post.getTitle(), post.getContent(), - post.getPostCategory().getName(), + post.getPostCategory().name(), post.getCreatedAt(), post.getModifiedAt(), post.getViewCount(), diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/PostSummaryDTO.java b/src/main/java/run/attraction/api/v1/announcement/dto/PostSummaryDTO.java index a7a990d2..47978ea4 100644 --- a/src/main/java/run/attraction/api/v1/announcement/dto/PostSummaryDTO.java +++ b/src/main/java/run/attraction/api/v1/announcement/dto/PostSummaryDTO.java @@ -14,7 +14,7 @@ public PostSummaryDTO(Post post) { this( post.getId(), post.getTitle(), - post.getPostCategory().getName(), + post.getPostCategory().name(), post.getCreatedAt(), post.getViewCount() ); diff --git a/src/main/java/run/attraction/api/v1/announcement/service/AnnouncementService.java b/src/main/java/run/attraction/api/v1/announcement/service/AnnouncementService.java index 7fcc8563..45158bf7 100644 --- a/src/main/java/run/attraction/api/v1/announcement/service/AnnouncementService.java +++ b/src/main/java/run/attraction/api/v1/announcement/service/AnnouncementService.java @@ -74,7 +74,7 @@ public void updatePostById(final Long postId, final UpdatePostRequestDTO request checkPinnedPostCount(); } - beforePost.update(request.title(), request.content(), PostCategory.findByName(request.postCategory()), + beforePost.update(request.title(), request.content(), PostCategory.valueOf(request.postCategory()), request.isPinned()); announcementRepository.save(beforePost); } From 6744ddab2c0a667f0e89208d2e72f84cc6cc3379 Mon Sep 17 00:00:00 2001 From: ryan-dia Date: Sun, 19 Jan 2025 14:50:56 +0900 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=EB=A1=9C=EB=8F=84=20=EA=B2=8C=EC=8B=9C=EB=AC=BC?= =?UTF-8?q?=EC=9D=84=20=EA=B0=80=EC=A0=B8=EC=98=AC=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/v1/announcement/PostCategory.java | 4 +-- .../controller/AnnouncementController.java | 13 +++++--- .../v1/announcement/dto/CustomPageDTO.java | 30 +++++++++++++++++++ .../repository/AnnouncementRepository.java | 10 +++++++ .../service/AnnouncementService.java | 12 ++++++-- 5 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 src/main/java/run/attraction/api/v1/announcement/dto/CustomPageDTO.java diff --git a/src/main/java/run/attraction/api/v1/announcement/PostCategory.java b/src/main/java/run/attraction/api/v1/announcement/PostCategory.java index 39628732..3175a6b8 100644 --- a/src/main/java/run/attraction/api/v1/announcement/PostCategory.java +++ b/src/main/java/run/attraction/api/v1/announcement/PostCategory.java @@ -16,9 +16,9 @@ public enum PostCategory { this.name = name; } - public static PostCategory findByName(final String name) { + public static PostCategory find(final String name) { return Arrays.stream(PostCategory.values()) - .filter(postCategory -> postCategory.getName().equals(name)) + .filter(postCategory -> postCategory.name().equals(name.toUpperCase())) .findAny() .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 게시글 카테고리입니다.")); } diff --git a/src/main/java/run/attraction/api/v1/announcement/controller/AnnouncementController.java b/src/main/java/run/attraction/api/v1/announcement/controller/AnnouncementController.java index c242ce79..e9fe4ed0 100644 --- a/src/main/java/run/attraction/api/v1/announcement/controller/AnnouncementController.java +++ b/src/main/java/run/attraction/api/v1/announcement/controller/AnnouncementController.java @@ -20,6 +20,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import run.attraction.api.v1.announcement.PostCategory; +import run.attraction.api.v1.announcement.dto.CustomPageDTO; import run.attraction.api.v1.announcement.dto.PostDTO; import run.attraction.api.v1.announcement.dto.PostSummaryDTO; import run.attraction.api.v1.announcement.dto.request.PostCreateRequestDTO; @@ -71,14 +73,17 @@ public ApiResponse updatePost(@PathVariable Long postId, @RequestBody Upda @GetMapping @Operation(summary = "고정되지 않은 모든 게시물 가져오기", description = "고정되지 않은 모든 게시물을 가져오는 로직입니다.") - public ApiResponse> getPosts( + public ApiResponse> getPosts( @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) String category + ) { Pageable pageable = PageRequest.of(page, size); - final Page posts = announcementService.findPosts(pageable); + final Page posts = announcementService.findPosts(pageable, category); + CustomPageDTO customPage = new CustomPageDTO<>(posts, PostCategory.find(category)); - return ApiResponse.from(HttpStatus.OK, "성공", posts); + return ApiResponse.from(HttpStatus.OK, "성공", customPage); } @GetMapping("/pinned") diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/CustomPageDTO.java b/src/main/java/run/attraction/api/v1/announcement/dto/CustomPageDTO.java new file mode 100644 index 00000000..f2a6a69c --- /dev/null +++ b/src/main/java/run/attraction/api/v1/announcement/dto/CustomPageDTO.java @@ -0,0 +1,30 @@ +package run.attraction.api.v1.announcement.dto; + +import java.util.List; +import org.springframework.data.domain.Page; +import run.attraction.api.v1.announcement.PostCategory; + +public record CustomPageDTO( + long totalElements, + int totalPages, + boolean first, + boolean last, + int size, + int number, + PostCategory category, + List content +) { + // 생성자: Page 객체를 받아 필드 초기화 + public CustomPageDTO(Page page, PostCategory category) { + this( + page.getTotalElements(), + page.getTotalPages(), + page.isFirst(), + page.isLast(), + page.getSize(), + page.getNumber(), + category, + page.getContent() + ); + } +} diff --git a/src/main/java/run/attraction/api/v1/announcement/repository/AnnouncementRepository.java b/src/main/java/run/attraction/api/v1/announcement/repository/AnnouncementRepository.java index 7748cde0..034cc2f3 100644 --- a/src/main/java/run/attraction/api/v1/announcement/repository/AnnouncementRepository.java +++ b/src/main/java/run/attraction/api/v1/announcement/repository/AnnouncementRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import run.attraction.api.v1.announcement.Post; +import run.attraction.api.v1.announcement.PostCategory; public interface AnnouncementRepository extends JpaRepository { @@ -65,4 +66,13 @@ SELECT COUNT(p) WHERE p.isPinned = true """) int countPinnedPosts(); + + + @Query(""" + SELECT p + FROM Post p + WHERE p.postCategory = :postCategory + AND p.isPinned = false + """) + Page findPostsByPostCategory(Pageable pageable, PostCategory postCategory); } diff --git a/src/main/java/run/attraction/api/v1/announcement/service/AnnouncementService.java b/src/main/java/run/attraction/api/v1/announcement/service/AnnouncementService.java index 45158bf7..152f4cdb 100644 --- a/src/main/java/run/attraction/api/v1/announcement/service/AnnouncementService.java +++ b/src/main/java/run/attraction/api/v1/announcement/service/AnnouncementService.java @@ -74,13 +74,21 @@ public void updatePostById(final Long postId, final UpdatePostRequestDTO request checkPinnedPostCount(); } - beforePost.update(request.title(), request.content(), PostCategory.valueOf(request.postCategory()), + beforePost.update(request.title(), request.content(), PostCategory.find(request.postCategory()), request.isPinned()); announcementRepository.save(beforePost); } @Transactional(readOnly = true) - public Page findPosts(Pageable pageable) { + public Page findPosts(Pageable pageable, String category) { + if (category != null) { + final PostCategory postCategory = PostCategory.find(category); + final Page postsByPostCategory = announcementRepository.findPostsByPostCategory(pageable, + postCategory); + + return postsByPostCategory.map(PostSummaryDTO::new); + } + final Page posts = announcementRepository.findAllWithoutPinned(pageable); return posts.map(PostSummaryDTO::new); From 7e35793572d09a5998de0483dfed689119e4d3c6 Mon Sep 17 00:00:00 2001 From: ryan-dia Date: Sun, 19 Jan 2025 15:25:43 +0900 Subject: [PATCH 8/9] =?UTF-8?q?refactor:=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EA=B0=80=201=EB=B6=80=ED=84=B0=20=EC=8B=9C=EC=9E=91=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=B4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AnnouncementController.java | 27 +++++++++++++++---- .../v1/announcement/dto/CustomPageDTO.java | 2 +- .../api/v1/announcement/dto/OneBasedPage.java | 19 +++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 src/main/java/run/attraction/api/v1/announcement/dto/OneBasedPage.java diff --git a/src/main/java/run/attraction/api/v1/announcement/controller/AnnouncementController.java b/src/main/java/run/attraction/api/v1/announcement/controller/AnnouncementController.java index e9fe4ed0..11b68f7e 100644 --- a/src/main/java/run/attraction/api/v1/announcement/controller/AnnouncementController.java +++ b/src/main/java/run/attraction/api/v1/announcement/controller/AnnouncementController.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.List; +import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -22,6 +23,7 @@ import org.springframework.web.bind.annotation.RestController; import run.attraction.api.v1.announcement.PostCategory; import run.attraction.api.v1.announcement.dto.CustomPageDTO; +import run.attraction.api.v1.announcement.dto.OneBasedPage; import run.attraction.api.v1.announcement.dto.PostDTO; import run.attraction.api.v1.announcement.dto.PostSummaryDTO; import run.attraction.api.v1.announcement.dto.request.PostCreateRequestDTO; @@ -74,18 +76,33 @@ public ApiResponse updatePost(@PathVariable Long postId, @RequestBody Upda @GetMapping @Operation(summary = "고정되지 않은 모든 게시물 가져오기", description = "고정되지 않은 모든 게시물을 가져오는 로직입니다.") public ApiResponse> getPosts( - @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(required = false) String category ) { - Pageable pageable = PageRequest.of(page, size); + Pageable pageable = createPageable(page, size); final Page posts = announcementService.findPosts(pageable, category); - CustomPageDTO customPage = new CustomPageDTO<>(posts, PostCategory.find(category)); + CustomPageDTO customPage = new CustomPageDTO<>(posts, resolvePostCategory(category)); return ApiResponse.from(HttpStatus.OK, "성공", customPage); } + private Pageable createPageable(int page, int size) { + if (page < 1) { + throw new IllegalArgumentException("페이지 번호는 1 이상이어야 합니다."); + } + + return PageRequest.of(page - 1, size); + } + + private PostCategory resolvePostCategory(String category) { + if (Objects.isNull(category)) { + return null; + } + return PostCategory.find(category); + } + @GetMapping("/pinned") @Operation(summary = "고정된 모든 게시물 가져오기", description = "고정된 모든 게시물을 가져오는 로직입니다.") public ApiResponse> getPinnedPosts() { @@ -96,11 +113,11 @@ public ApiResponse> getPinnedPosts() { @GetMapping("/search") @Operation(summary = "게시물 검색", description = "검색 타입(제목/내용/제목+내용)에 맞는 게시물을 검색해주는 로직") - public ApiResponse> searchPosts(@ModelAttribute PostSearchRequest request + public ApiResponse> searchPosts(@ModelAttribute PostSearchRequest request ) { Pageable pageable = PageRequest.of(request.page(), request.size()); final Page posts = announcementService.findPostsBySearchQuery(pageable, request); - return ApiResponse.from(HttpStatus.OK, "성공", posts); + return ApiResponse.from(HttpStatus.OK, "성공", OneBasedPage.of(posts)); } } diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/CustomPageDTO.java b/src/main/java/run/attraction/api/v1/announcement/dto/CustomPageDTO.java index f2a6a69c..bbce017f 100644 --- a/src/main/java/run/attraction/api/v1/announcement/dto/CustomPageDTO.java +++ b/src/main/java/run/attraction/api/v1/announcement/dto/CustomPageDTO.java @@ -22,7 +22,7 @@ public CustomPageDTO(Page page, PostCategory category) { page.isFirst(), page.isLast(), page.getSize(), - page.getNumber(), + page.getNumber() + 1, category, page.getContent() ); diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/OneBasedPage.java b/src/main/java/run/attraction/api/v1/announcement/dto/OneBasedPage.java new file mode 100644 index 00000000..ce381ec6 --- /dev/null +++ b/src/main/java/run/attraction/api/v1/announcement/dto/OneBasedPage.java @@ -0,0 +1,19 @@ +package run.attraction.api.v1.announcement.dto; + +import java.util.List; +import org.springframework.data.domain.Page; + +public record OneBasedPage( + List content, + int totalPages, + int currentPage, + long totalElements +) { + public OneBasedPage(Page page) { + this(page.getContent(), page.getTotalPages(), page.getNumber() + 1, page.getTotalElements()); + } + + public static OneBasedPage of(Page page) { + return new OneBasedPage<>(page); + } +} From 5419c2bff2c0479ae4421baf8d2a103298e431f0 Mon Sep 17 00:00:00 2001 From: ryan-dia Date: Sun, 19 Jan 2025 15:28:30 +0900 Subject: [PATCH 9/9] =?UTF-8?q?refactor:=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=EA=B0=92=EC=9D=84=201=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/v1/announcement/dto/request/PostSearchRequest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/run/attraction/api/v1/announcement/dto/request/PostSearchRequest.java b/src/main/java/run/attraction/api/v1/announcement/dto/request/PostSearchRequest.java index 0b7c54c9..7e8d74dd 100644 --- a/src/main/java/run/attraction/api/v1/announcement/dto/request/PostSearchRequest.java +++ b/src/main/java/run/attraction/api/v1/announcement/dto/request/PostSearchRequest.java @@ -7,8 +7,8 @@ public record PostSearchRequest( Integer size ) { public PostSearchRequest { - if (page == null || page < 0) { - page = 0; + if (page == null || page < 1) { + page = 1; } if (size == null || size <= 0) { size = 3;