Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions src/main/java/run/attraction/api/v1/announcement/Post.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
25 changes: 25 additions & 0 deletions src/main/java/run/attraction/api/v1/announcement/PostCategory.java
Original file line number Diff line number Diff line change
@@ -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 find(final String name) {
return Arrays.stream(PostCategory.values())
.filter(postCategory -> postCategory.name().equals(name.toUpperCase()))
.findAny()
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 게시글 카테고리입니다."));
}
}
24 changes: 24 additions & 0 deletions src/main/java/run/attraction/api/v1/announcement/SearchType.java
Original file line number Diff line number Diff line change
@@ -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("존재하지 않는 검색 타입입니다."));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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 java.util.List;
import java.util.Objects;
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.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;
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<Void> createPost(@Valid @RequestBody PostCreateRequestDTO request) {
announcementService.createPost(request);

return ApiResponse.from(HttpStatus.CREATED, "성공", null);
}

@GetMapping("/{postId}")
@Operation(summary = "게시글 조회", description = "postId를 입력받아 해당 게시글을 조회하는 로직입니다.")
public ApiResponse<PostDTO> getPost(@PathVariable Long postId) {
final PostDTO post = announcementService.findPostById(postId);

return ApiResponse.from(HttpStatus.OK, "성공", post);
}

@DeleteMapping("/{postId}")
@Operation(summary = "게시글 삭제", description = "postId를 입력받아 해당 게시글을 삭제하는 로직입니다.")
public ApiResponse<Void> deletePost(@PathVariable Long postId) {
announcementService.deletePostById(postId);

return ApiResponse.from(HttpStatus.OK, "성공", null);
}

@PatchMapping("/{postId}")
@Operation(summary = "게시글 수정", description = "postId를 입력받아 해당 게시글을 수정하는 로직입니다.")
public ApiResponse<Void> updatePost(@PathVariable Long postId, @RequestBody UpdatePostRequestDTO post) {
announcementService.updatePostById(postId, post);

return ApiResponse.from(HttpStatus.OK, "성공", null);
}

@GetMapping
@Operation(summary = "고정되지 않은 모든 게시물 가져오기", description = "고정되지 않은 모든 게시물을 가져오는 로직입니다.")
public ApiResponse<CustomPageDTO<PostSummaryDTO>> getPosts(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String category

) {
Pageable pageable = createPageable(page, size);
final Page<PostSummaryDTO> posts = announcementService.findPosts(pageable, category);
CustomPageDTO<PostSummaryDTO> 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<List<PostSummaryDTO>> getPinnedPosts() {
final List<PostSummaryDTO> posts = announcementService.findPinnedPosts();

return ApiResponse.from(HttpStatus.OK, "성공", posts);
}

@GetMapping("/search")
@Operation(summary = "게시물 검색", description = "검색 타입(제목/내용/제목+내용)에 맞는 게시물을 검색해주는 로직")
public ApiResponse<OneBasedPage<PostSummaryDTO>> searchPosts(@ModelAttribute PostSearchRequest request
) {
Pageable pageable = PageRequest.of(request.page(), request.size());
final Page<PostSummaryDTO> posts = announcementService.findPostsBySearchQuery(pageable, request);

return ApiResponse.from(HttpStatus.OK, "성공", OneBasedPage.of(posts));
}
}
Original file line number Diff line number Diff line change
@@ -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<T>(
long totalElements,
int totalPages,
boolean first,
boolean last,
int size,
int number,
PostCategory category,
List<T> content
) {
// 생성자: Page 객체를 받아 필드 초기화
public CustomPageDTO(Page<T> page, PostCategory category) {
this(
page.getTotalElements(),
page.getTotalPages(),
page.isFirst(),
page.isLast(),
page.getSize(),
page.getNumber() + 1,
category,
page.getContent()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package run.attraction.api.v1.announcement.dto;

import java.util.List;
import org.springframework.data.domain.Page;

public record OneBasedPage<T>(
List<T> content,
int totalPages,
int currentPage,
long totalElements
) {
public OneBasedPage(Page<T> page) {
this(page.getContent(), page.getTotalPages(), page.getNumber() + 1, page.getTotalElements());
}

public static <T> OneBasedPage<T> of(Page<T> page) {
return new OneBasedPage<>(page);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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 postCategory,
LocalDateTime createdAt,
Long viewCount
) {
public PinnedPostSummaryDTO(Post post) {
this(
post.getId(),
post.getTitle(),
post.getPostCategory().name(),
post.getCreatedAt(),
post.getViewCount()
);
}
}
30 changes: 30 additions & 0 deletions src/main/java/run/attraction/api/v1/announcement/dto/PostDTO.java
Original file line number Diff line number Diff line change
@@ -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().name(),
post.getCreatedAt(),
post.getModifiedAt(),
post.getViewCount(),
post.isPinned(),
new RelatedDTO(previousPost, nextPost)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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 postCategory,
LocalDateTime createdAt,
Long viewCount
) {
public PostSummaryDTO(Post post) {
this(
post.getId(),
post.getTitle(),
post.getPostCategory().name(),
post.getCreatedAt(),
post.getViewCount()
);
}
}
Original file line number Diff line number Diff line change
@@ -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 prev,
RelatedPostDTO next
) {
public RelatedDTO(Post previousPost, Post nextPost) {
this(
Objects.isNull(previousPost) ? null : new RelatedPostDTO(previousPost),
Objects.isNull(nextPost) ? null : new RelatedPostDTO(nextPost)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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,
LocalDateTime createdAt
) {
public RelatedPostDTO(Post post) {
this(post.getId(), post.getTitle(), post.getCreatedAt());
}
}
Loading
Loading