diff --git a/aics-api/src/main/java/kgu/developers/api/post/application/PostService.java b/aics-api/src/main/java/kgu/developers/api/post/application/PostService.java index 1d7934e5..d60d607e 100644 --- a/aics-api/src/main/java/kgu/developers/api/post/application/PostService.java +++ b/aics-api/src/main/java/kgu/developers/api/post/application/PostService.java @@ -1,5 +1,13 @@ package kgu.developers.api.post.application; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import org.springframework.data.domain.PageRequest; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + import kgu.developers.api.post.presentation.exception.PostNotFoundException; import kgu.developers.api.post.presentation.request.PostRequest; import kgu.developers.api.post.presentation.response.PostDetailResponse; @@ -7,17 +15,11 @@ import kgu.developers.api.post.presentation.response.PostSummaryPageResponse; import kgu.developers.api.user.application.UserService; import kgu.developers.common.response.PaginatedListResponse; +import kgu.developers.domain.post.domain.Category; import kgu.developers.domain.post.domain.Post; import kgu.developers.domain.post.domain.PostRepository; import kgu.developers.domain.user.domain.User; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; @Service @RequiredArgsConstructor @@ -30,16 +32,17 @@ public class PostService { private LocalDateTime lastScheduledRun; @Transactional - public PostPersistResponse createPost(PostRequest request) { + public PostPersistResponse createPost(PostRequest request, Category category) { User author = userService.me(); - Post createPost = Post.create(request.title(), request.content(), author); + Post createPost = Post.create(request.title(), request.content(), category, author); postRepository.save(createPost); return PostPersistResponse.from(createPost.getId()); } - public PostSummaryPageResponse getPostsByKeyword(PageRequest request, String keyword) { - PaginatedListResponse paginatedListResponse = postRepository.findAllByTitleContainingOrderByCreatedAtDesc( - keyword, request); + public PostSummaryPageResponse getPostsByKeywordAndCategory(PageRequest request, String keyword, + Category category) { + PaginatedListResponse paginatedListResponse = postRepository.findAllByTitleContainingAndCategoryOrderByCreatedAtDesc( + keyword, category, request); return PostSummaryPageResponse.of(paginatedListResponse.contents(), paginatedListResponse.pageable()); } diff --git a/aics-api/src/main/java/kgu/developers/api/post/presentation/PostController.java b/aics-api/src/main/java/kgu/developers/api/post/presentation/PostController.java index 2d5c1981..7be574a0 100644 --- a/aics-api/src/main/java/kgu/developers/api/post/presentation/PostController.java +++ b/aics-api/src/main/java/kgu/developers/api/post/presentation/PostController.java @@ -2,6 +2,17 @@ import static org.springframework.http.HttpStatus.CREATED; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +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 io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -16,17 +27,8 @@ import kgu.developers.api.post.presentation.response.PostDetailResponse; import kgu.developers.api.post.presentation.response.PostPersistResponse; import kgu.developers.api.post.presentation.response.PostSummaryPageResponse; +import kgu.developers.domain.post.domain.Category; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -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; @RestController @RequiredArgsConstructor @@ -42,9 +44,10 @@ public class PostController { @ApiResponse(responseCode = "201", content = @Content(schema = @Schema(implementation = PostPersistResponse.class))) @PostMapping public ResponseEntity createPost( + @Parameter(description = "게시글 카테고리", example = "DEPT_INFO", required = true) @RequestParam Category category, @RequestBody PostRequest request ) { - PostPersistResponse response = postService.createPost(request); + PostPersistResponse response = postService.createPost(request, category); return ResponseEntity.status(CREATED).body(response); } @@ -54,12 +57,14 @@ public ResponseEntity createPost( """) @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = PostSummaryPageResponse.class))) @GetMapping - public ResponseEntity getPostsByKeyword( + public ResponseEntity getPostsByKeywordAndCategory( + @Parameter(description = "게시글 카테고리", example = "DEPT_INFO") @RequestParam(required = false) Category category, @Parameter(description = "페이지 인덱스", example = "0", required = true) @RequestParam(defaultValue = "0") @PositiveOrZero int page, @Parameter(description = "응답 개수", example = "10", required = true) @RequestParam(defaultValue = "10") @Positive int size, @Parameter(description = "검색 키워드", example = "컴퓨터공학과") @RequestParam(required = false) String keyword ) { - PostSummaryPageResponse response = postService.getPostsByKeyword(PageRequest.of(page, size), keyword); + PostSummaryPageResponse response = postService.getPostsByKeywordAndCategory(PageRequest.of(page, size), keyword, + category); return ResponseEntity.ok(response); } diff --git a/aics-api/src/main/java/kgu/developers/api/post/presentation/response/PostDetailResponse.java b/aics-api/src/main/java/kgu/developers/api/post/presentation/response/PostDetailResponse.java index 7710e3d9..0a4f3b69 100644 --- a/aics-api/src/main/java/kgu/developers/api/post/presentation/response/PostDetailResponse.java +++ b/aics-api/src/main/java/kgu/developers/api/post/presentation/response/PostDetailResponse.java @@ -2,19 +2,23 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; +import java.time.format.DateTimeFormatter; + +import org.springframework.format.annotation.DateTimeFormat; + import io.swagger.v3.oas.annotations.media.Schema; import kgu.developers.api.file.presentation.response.FileResponse; import kgu.developers.domain.post.domain.Post; import lombok.Builder; -import org.springframework.format.annotation.DateTimeFormat; - -import java.time.format.DateTimeFormatter; @Builder public record PostDetailResponse( @Schema(description = "게시글 id", example = "1", requiredMode = REQUIRED) Long postId, + @Schema(description = "게시글 카테고리", example = "학과공지", requiredMode = REQUIRED) + String category, + @Schema(description = "게시글 제목", example = "SW 부트캠프 4기 교육생 모집", requiredMode = REQUIRED) String title, @@ -53,6 +57,7 @@ public static PostDetailResponse from(Post post) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); return PostDetailResponse.builder() .postId(post.getId()) + .category(post.getCategory().getDescription()) .title(post.getTitle()) .content(post.getContent()) .author(post.getAuthor().getName()) diff --git a/aics-api/src/main/java/kgu/developers/api/post/presentation/response/PostSummaryResponse.java b/aics-api/src/main/java/kgu/developers/api/post/presentation/response/PostSummaryResponse.java index 78c4ee3e..e08f469e 100644 --- a/aics-api/src/main/java/kgu/developers/api/post/presentation/response/PostSummaryResponse.java +++ b/aics-api/src/main/java/kgu/developers/api/post/presentation/response/PostSummaryResponse.java @@ -2,18 +2,22 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; +import java.time.format.DateTimeFormatter; + +import org.springframework.format.annotation.DateTimeFormat; + import io.swagger.v3.oas.annotations.media.Schema; import kgu.developers.domain.post.domain.Post; import lombok.Builder; -import org.springframework.format.annotation.DateTimeFormat; - -import java.time.format.DateTimeFormatter; @Builder public record PostSummaryResponse( @Schema(description = "게시글 id", example = "1", requiredMode = REQUIRED) Long postId, + @Schema(description = "게시글 카테고리", example = "학과공지", requiredMode = REQUIRED) + String category, + @Schema(description = "게시글 제목", example = "SW 부트캠프 4기 교육생 모집", requiredMode = REQUIRED) String title, @@ -37,6 +41,7 @@ public static PostSummaryResponse from(Post post) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); return PostSummaryResponse.builder() .postId(post.getId()) + .category(post.getCategory().getDescription()) .title(post.getTitle()) .author(post.getAuthor().getName()) .views(post.getViews()) diff --git a/aics-common/src/main/java/kgu/developers/common/exception/GlobalExceptionHandler.java b/aics-common/src/main/java/kgu/developers/common/exception/GlobalExceptionHandler.java index 1f0361ec..e51dd448 100644 --- a/aics-common/src/main/java/kgu/developers/common/exception/GlobalExceptionHandler.java +++ b/aics-common/src/main/java/kgu/developers/common/exception/GlobalExceptionHandler.java @@ -1,12 +1,12 @@ package kgu.developers.common.exception; - import static kgu.developers.common.exception.GlobalExceptionCode.INVALID_INPUT; import static kgu.developers.common.exception.GlobalExceptionCode.SERVER_ERROR; import java.util.List; import java.util.stream.Collectors; +import org.springframework.beans.TypeMismatchException; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.MessageSourceResolvable; import org.springframework.http.HttpHeaders; @@ -68,4 +68,17 @@ protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotV return ResponseEntity.status(response.status()).body(response); } + + @Override + protected ResponseEntity handleTypeMismatch(TypeMismatchException exception, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request) { + String message = String.format("Failed to convert '%s' with value: '%s'.", + exception.getPropertyName(), + exception.getValue()); + ExceptionResponse response = ExceptionResponse.of(INVALID_INPUT.getStatus(), INVALID_INPUT.getCode(), message); + + return ResponseEntity.status(response.status()).body(response); + } } \ No newline at end of file diff --git a/aics-domain/src/main/java/kgu/developers/domain/post/domain/Post.java b/aics-domain/src/main/java/kgu/developers/domain/post/domain/Post.java index 47017ab7..29e1d584 100644 --- a/aics-domain/src/main/java/kgu/developers/domain/post/domain/Post.java +++ b/aics-domain/src/main/java/kgu/developers/domain/post/domain/Post.java @@ -65,12 +65,13 @@ public class Post extends BaseTimeEntity { @OneToMany(mappedBy = "post", fetch = LAZY, cascade = ALL, orphanRemoval = true) private List comments = new ArrayList<>(); - public static Post create(String title, String content, User author) { + public static Post create(String title, String content, Category category, User author) { return Post.builder() .title(title) .content(content) .views(0) .isPinned(false) + .category(category) .author(author) // NOTE: User Setter 주입 방지 위해 생성자 주입 .build(); } diff --git a/aics-domain/src/main/java/kgu/developers/domain/post/domain/PostRepository.java b/aics-domain/src/main/java/kgu/developers/domain/post/domain/PostRepository.java index b7132eae..536e0e4b 100644 --- a/aics-domain/src/main/java/kgu/developers/domain/post/domain/PostRepository.java +++ b/aics-domain/src/main/java/kgu/developers/domain/post/domain/PostRepository.java @@ -9,7 +9,8 @@ public interface PostRepository { Post save(Post post); - PaginatedListResponse findAllByTitleContainingOrderByCreatedAtDesc(String keyword, Pageable pageable); + PaginatedListResponse findAllByTitleContainingAndCategoryOrderByCreatedAtDesc(String keyword, + Category category, Pageable pageable); Optional findById(Long postId); diff --git a/aics-domain/src/main/java/kgu/developers/domain/post/infrastructure/PostRepositoryImpl.java b/aics-domain/src/main/java/kgu/developers/domain/post/infrastructure/PostRepositoryImpl.java index 2213043c..af306ea5 100644 --- a/aics-domain/src/main/java/kgu/developers/domain/post/infrastructure/PostRepositoryImpl.java +++ b/aics-domain/src/main/java/kgu/developers/domain/post/infrastructure/PostRepositoryImpl.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Repository; import kgu.developers.common.response.PaginatedListResponse; +import kgu.developers.domain.post.domain.Category; import kgu.developers.domain.post.domain.Post; import kgu.developers.domain.post.domain.PostRepository; import lombok.RequiredArgsConstructor; @@ -27,8 +28,9 @@ public Optional findById(Long postId) { } @Override - public PaginatedListResponse findAllByTitleContainingOrderByCreatedAtDesc(String keyword, Pageable pageable) { - return queryPostRepository.findAllByTitleContainingOrderByCreatedAtDesc(keyword, pageable); + public PaginatedListResponse findAllByTitleContainingAndCategoryOrderByCreatedAtDesc(String keyword, + Category category, Pageable pageable) { + return queryPostRepository.findAllByTitleContainingAndCategoryOrderByCreatedAtDesc(keyword, category, pageable); } @Override diff --git a/aics-domain/src/main/java/kgu/developers/domain/post/infrastructure/QueryPostRepository.java b/aics-domain/src/main/java/kgu/developers/domain/post/infrastructure/QueryPostRepository.java index 285bfd1e..15259c82 100644 --- a/aics-domain/src/main/java/kgu/developers/domain/post/infrastructure/QueryPostRepository.java +++ b/aics-domain/src/main/java/kgu/developers/domain/post/infrastructure/QueryPostRepository.java @@ -9,10 +9,12 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; +import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import kgu.developers.common.response.PageableResponse; import kgu.developers.common.response.PaginatedListResponse; +import kgu.developers.domain.post.domain.Category; import kgu.developers.domain.post.domain.Post; import lombok.RequiredArgsConstructor; @@ -21,12 +23,16 @@ public class QueryPostRepository { private final JPAQueryFactory queryFactory; - public PaginatedListResponse findAllByTitleContainingOrderByCreatedAtDesc(String keyword, Pageable pageable) { - if (keyword == null) - keyword = ""; + public PaginatedListResponse findAllByTitleContainingAndCategoryOrderByCreatedAtDesc(String keyword, + Category category, Pageable pageable) { + + BooleanExpression whereClause = post.deletedAt.isNull() + .and(keyword != null ? post.title.contains(keyword) : null) + .and(category != null ? post.category.eq(category) : null); + List posts = queryFactory.select(post) .from(post) - .where(post.title.contains(keyword).and(post.deletedAt.isNull())) + .where(whereClause) .orderBy(post.isPinned.desc(), post.createdAt.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) @@ -34,7 +40,7 @@ public PaginatedListResponse findAllByTitleContainingOrderByCreatedAtDesc(String List postIds = queryFactory.select(post.id) .from(post) - .where(post.title.contains(keyword).and(post.deletedAt.isNull())) + .where(whereClause) .orderBy(post.isPinned.desc(), post.createdAt.desc()) .fetch();