Skip to content

Commit

Permalink
Feat: 테크 블로그 게시글 (#122)
Browse files Browse the repository at this point in the history
* Feat: 부제목 필드 추가

* Feat: Post DTO 정의

* Feat: Post Service 기본 틀 정의

* Feat: Post API spec 정의

* Fix: dev 브랜치와 ResponseDTO 동기화

* Fix: summary를 subTitle로 변경

* Fix: inner class로 조회 반환형 변경

* Feat: Post 엔티티 갱신 편의 메서드 추가

* Feat: Post 엔티티 생성자 추가

* Feat: Post 저장 여부 판별 편의 메서드 추가

* Feat: 게시글 수정 권한 오류 ErrorCode 작성

* Fix: commentCount를 요청마다 size를 계산

* Feat: 카테고리별 조회 레포지토리 작성

* Feat: 게시글 CRUD 서비스 구현

* Comment: API spec 수정

* Comment: 서비스 메서드 주석 추가

* Fix: 서비스 관련 Conflict 해결

* Feat: 임시글 조회 DTO 정의

* Feat: 임시 글 리스트 조회 서비스 구현

* Feat: 본인 글 목록 조회 레포지토리 작성

* Feat: 임시 글 목록 조회 API 구현

* Feat: 게시글 Detail DTO 구현

* Fix: 게시글 단건 조회 반환형 Detail로 알맞게 변경

* Feat: 게시글 수정용 정보 조회를 위한 Modify DTO 구현

* Feat: 게시글 수정용 정보 조회를 위한 서비스 구현

* Feat: 게시글 수정용 정보 조회를 위한 컨트롤러 구현

* Chore: 테크블로그 게시글 조회 예외 등록

* Fix: DTO - entity 의존성 제거

* Feat: 게시글 수정, 삭제여부 나타내는 필드 및 기능 추가

* Fix: filter() 대신 레포지토리에서 필터링

* Fix: QueryDSL을 사용해 하나의 쿼리로 압축

* Fix: Serializable 제거

* Fix: subTitle null 처리 추가

* Fix: 의미없는 @builder 제거

* Feat: 게시글 목록 조회 페이징 적용

* Fix: QueryDSL limit 조건 추가

* Fix: 게시글 썸네일 이미지 Multipart로 받게 수정

* Feat: 키워드로 검색 기능 추가

* Fix: withoutCountFrom으로 필요없는 정보 제거

* Fix: 게시글 생성 시 이미지 업로드와 게시글 저장 트랜잭션 분리

* Fix: subTitle 필드 삭제

* Fix: subTitle 필드 삭제 대응

* Fix: 썸네일 프론트에서 처리 후 url만 받는 것으로 변경

* Fix: QueryDSL에서도 subTitle 삭제

* Fix: 수정용 정보 API 주석화 (개발 완료 후 삭제 예정)

* Feat: 임시 저장 리스트 조회 페이징 적용

* Feat: 임시 글 목록만 조회에서 출간 된 글 목록 조회도 추가

* Fix: S3 Service import 제거
  • Loading branch information
chaejm55 authored Oct 19, 2024
1 parent 4e5f244 commit ce25ea7
Show file tree
Hide file tree
Showing 11 changed files with 496 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public class SecurityConfig {

private static final String[] WHITE_LIST = {
"/api/post/{postId:\\d+}/comment/**",
"/api/post/trending"
"/api/post/trending",
"/api/post/**"
};
private static final String[] MEMBER_AUTHENTICATION_LIST = {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,110 @@
package com.gdsc_knu.official_homepage.controller.post;

import com.gdsc_knu.official_homepage.dto.fileupload.FileUploadRequest;
import com.gdsc_knu.official_homepage.dto.fileupload.FileUploadResponse;
import com.gdsc_knu.official_homepage.annotation.TokenMember;
import com.gdsc_knu.official_homepage.authentication.jwt.JwtMemberDetail;
import com.gdsc_knu.official_homepage.dto.PagingResponse;
import com.gdsc_knu.official_homepage.dto.post.PostRequest;
import com.gdsc_knu.official_homepage.dto.post.PostResponse;
import com.gdsc_knu.official_homepage.entity.post.enumeration.Category;
import com.gdsc_knu.official_homepage.entity.post.enumeration.PostStatus;
import com.gdsc_knu.official_homepage.dto.fileupload.FileUploadRequest;
import com.gdsc_knu.official_homepage.dto.fileupload.FileUploadResponse;
import com.gdsc_knu.official_homepage.service.fileupload.FileUploader;

import com.gdsc_knu.official_homepage.service.post.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.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Tag(name = "Post", description = "테크블로그 관련 API")
@Tag(name = "Post", description = "테크블로그 게시글 관련 API")
@RestController
@RequestMapping("/api/post")
@RequiredArgsConstructor
public class PostController {
private final PostService postService;
private final FileUploader fileUploader;

@GetMapping()
@Operation(summary = "게시글 목록 조회 API", description = "게시글 목록을 조회한다. 카테고리별로 조회할 수 있다. 없으면 전체 조회를 한다.")
public ResponseEntity<PagingResponse<PostResponse.Main>> getPostList(@RequestParam(value = "category", required = false) Category category,
@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "20") int size) {
PagingResponse<PostResponse.Main> postList = postService.getPostList(category, page, size);
return ResponseEntity.ok().body(postList);
}

@GetMapping("/mypost")
@Operation(summary = "본인 게시글 목록 조회 API", description = "본인의 게시글 목록을 조회한다. 조회할 게시글 상태가 필요하다.")
public ResponseEntity<PagingResponse<PostResponse.Temp>> getTemporalPostList(@TokenMember JwtMemberDetail jwtMemberDetail,
@RequestParam(value = "status") PostStatus status,
@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "20") int size) {
PagingResponse<PostResponse.Temp> postList = postService.getTemporalPostList(jwtMemberDetail.getId(), status, page, size);
return ResponseEntity.ok().body(postList);
}

@GetMapping("/{postId}")
@Operation(summary = "게시글 조회 API", description = "게시글 id로 게시글을 단건 조회한다.")
public ResponseEntity<PostResponse.Detail> getPost(Authentication authentication,
@PathVariable("postId") Long postId) {
Long memberId = 0L;
if (authentication != null){
JwtMemberDetail jwtMemberDetail = (JwtMemberDetail) authentication.getPrincipal();
memberId = jwtMemberDetail.getId();
}

PostResponse.Detail post = postService.getPost(memberId, postId);
return ResponseEntity.ok().body(post);
}

// @GetMapping("/{postId}/modify")
// @Operation(summary = "게시글 수정용 정보 조회 API", description = "게시글 수정을 위해 필요한 정보를 조회한다. 작성자만 조회 가능하다.")
// public ResponseEntity<PostResponse.Modify> getTemporalPost(@PathVariable("postId") Long postId,
// @TokenMember JwtMemberDetail jwtMemberDetail) {
// PostResponse.Modify modifyPost = postService.getModifyPost(jwtMemberDetail.getId(), postId);
// return ResponseEntity.ok().body(modifyPost);
// }
@PostMapping()
@Operation(summary = "게시글 작성 API", description = "게시글을 작성한다. 회원만 작성 가능하다.")
public ResponseEntity<Void> createPost(@TokenMember JwtMemberDetail jwtMemberDetail,
@RequestBody PostRequest.Create postRequestDto) {
postService.createPost(jwtMemberDetail.getId(), postRequestDto);
return ResponseEntity.status(HttpStatus.CREATED).build();
}

@PatchMapping("/{postId}")
@Operation(summary = "게시글 수정 API", description = "게시글을 수정한다. 작성자만 수정 가능하다.")
public ResponseEntity<Void> updatePost(@TokenMember JwtMemberDetail jwtMemberDetail,
@PathVariable("postId") Long postId,
@RequestBody PostRequest.Update postRequestDto) {
postService.updatePost(jwtMemberDetail.getId(), postId, postRequestDto);
return ResponseEntity.ok().build();
}

@DeleteMapping("/{postId}")
@Operation(summary = "게시글 삭제 API", description = "게시글을 삭제한다. 작성자 혹은 관리자(Core)만 삭제 가능하다.")
public ResponseEntity<Void> deletePost(@TokenMember JwtMemberDetail jwtMemberDetail,
@PathVariable("postId") Long postId) {
postService.deletePost(jwtMemberDetail.getId(), postId);
return ResponseEntity.ok().build();
}

@GetMapping("/search")
@Operation(summary = "게시글 검색 API", description = "제목, 부제목, 본문 내용에 키워드 포함 여부로 게시글을 검색한다.")
public ResponseEntity<PagingResponse<PostResponse.Main>> searchPosts(@RequestParam(value = "keyword") String keyword,
@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "20") int size) {
return ResponseEntity.ok().body(postService.searchPostList(keyword, page, size));
}

@GetMapping("trending")
@Operation(summary = "카테고리별 인기글 5개 조회", description = "category가 null이면 전제를 조회한다.")
public ResponseEntity<List<PostResponse.Main>> getTrendingPosts(
Expand All @@ -40,5 +122,4 @@ public ResponseEntity<FileUploadResponse> uploadImage(@Valid @ModelAttribute Fil
String url = fileUploader.upload(request.getImage(), "post");
return ResponseEntity.ok().body(FileUploadResponse.of(url));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.gdsc_knu.official_homepage.dto.post;

import com.gdsc_knu.official_homepage.entity.Member;
import com.gdsc_knu.official_homepage.entity.post.Post;
import com.gdsc_knu.official_homepage.entity.post.enumeration.Category;
import com.gdsc_knu.official_homepage.entity.post.enumeration.PostStatus;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;

import java.time.LocalDateTime;


public class PostRequest {
@Getter
@AllArgsConstructor
@NoArgsConstructor
public static class Create {
private String title;
private String content;
private String thumbnailUrl;
private Category category;
private PostStatus status;

public static Post toEntity(Create create, Member member) {
LocalDateTime now = LocalDateTime.now();
return Post.builder()
.title(create.getTitle())
.content(create.getContent())
.thumbnailUrl(create.getThumbnailUrl())
.category(create.getCategory())
.status(create.getStatus())
.member(member)
.publishedAt(now)
.modifiedAt(now)
.build();
}
}

@Getter
@AllArgsConstructor
@NoArgsConstructor
public static class Update {
private String title;
private String content;
private String thumbnailUrl;
private Category category;
private PostStatus status;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.gdsc_knu.official_homepage.entity.post.Post;
import com.gdsc_knu.official_homepage.entity.post.enumeration.Category;
import com.gdsc_knu.official_homepage.entity.post.enumeration.PostStatus;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -17,7 +19,7 @@ public class PostResponse {
@Getter
@Builder
@AllArgsConstructor
public static class Main implements Serializable{
public static class Main implements Serializable {
private Long id;
private String title;
private String summary;
Expand All @@ -29,20 +31,111 @@ public static class Main implements Serializable{
private LocalDateTime createAt;
private int likeCount;
private int commentCount;
private int sharedCount;

public static Main from(Post post) {
int length = Math.min(post.getContent().length(), 20);
return Main.builder()
.id(post.getId())
.title(post.getTitle())
.summary(post.getContent().substring(0,length))
.summary(post.getContent().substring(0, length))
.thumbnailUrl(post.getThumbnailUrl())
.category(post.getCategory().name())
.createAt(post.getPublishedAt())
.likeCount(post.getLikeCount())
.commentCount(post.getCommentCount())
.commentCount(post.getCommentList().size())
.sharedCount(post.getSharedCount())
.build();

}
}

@Getter
@Builder
@AllArgsConstructor
public static class Detail {
private Long id;
private String title;
private String summary;
private String thumbnailUrl;
private String category;
private String content;
private String authorName;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime createAt;
private int likeCount;
private int commentCount;
private int sharedCount;
private boolean canDelete;
private boolean canModify;

public static Detail from(Post post, AccessModel access) {
int length = Math.min(post.getContent().length(), 20);
return Detail.builder()
.id(post.getId())
.title(post.getTitle())
.summary(post.getContent().substring(0, length))
.thumbnailUrl(post.getThumbnailUrl())
.category(post.getCategory().name())
.content(post.getContent())
.authorName(post.getMember().getName())
.createAt(post.getPublishedAt())
.likeCount(post.getLikeCount())
.commentCount(post.getCommentList().size())
.sharedCount(post.getSharedCount())
.canDelete(access.canDelete())
.canModify(access.canModify())
.build();

}
}

@Getter
@Builder
@AllArgsConstructor
public static class Temp {
private Long id;
private String title;
private String summary;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime createAt;

public static Temp from(Post post) {
int length = Math.min(post.getContent().length(), 20);
return Temp.builder()
.id(post.getId())
.title(post.getTitle())
.summary(post.getContent().substring(0, length))
.createAt(post.getPublishedAt())
.build();
}
}

// @Getter
// @Builder
// @AllArgsConstructor
// public static class Modify {
// private String title;
// private String content;
// private String summary;
// private String thumbnailUrl;
// private Category category;
// private PostStatus status;
//
// public static Modify from(Post post) {
// int length = Math.min(post.getContent().length(), 20);
// return Modify.builder()
// .title(post.getTitle())
// .summary(post.getContent().substring(0, length))
// .content(post.getContent())
// .thumbnailUrl(post.getThumbnailUrl())
// .category(post.getCategory())
// .status(post.getStatus())
// .build();
// }
// }
}
13 changes: 13 additions & 0 deletions src/main/java/com/gdsc_knu/official_homepage/entity/post/Post.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.gdsc_knu.official_homepage.entity.post;

import com.gdsc_knu.official_homepage.dto.post.PostRequest;
import com.gdsc_knu.official_homepage.entity.Member;
import com.gdsc_knu.official_homepage.entity.post.enumeration.Category;
import com.gdsc_knu.official_homepage.entity.post.enumeration.PostStatus;
Expand Down Expand Up @@ -47,4 +48,16 @@ public class Post {
private LocalDateTime publishedAt;
private LocalDateTime modifiedAt;

public void update(PostRequest.Update postRequest) {
this.title = postRequest.getTitle();
this.content = postRequest.getContent();
this.thumbnailUrl = postRequest.getThumbnailUrl();
this.category = postRequest.getCategory();
this.status = postRequest.getStatus();
this.modifiedAt = LocalDateTime.now();
}

public boolean isSaved() {
return this.status.equals(PostStatus.SAVED);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public enum ErrorCode {

// Post
POST_NOT_FOUND(404, HttpStatus.NOT_FOUND, "게시글을 찾을 수 없습니다."),
POST_FORBIDDEN(403, HttpStatus.FORBIDDEN, "게시글을 수정할 수 있는 권한이 없습니다."),

// Comment
COMMENT_NOT_FOUND(404, HttpStatus.NOT_FOUND, "댓글을 찾을 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@

import com.gdsc_knu.official_homepage.entity.post.Post;
import com.gdsc_knu.official_homepage.entity.post.enumeration.Category;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.List;

public interface PostQueryFactory {
List<Post> findTop5ByCategory(Category category, int size);

Page<Post> findAllByCategory(Pageable pageable, Category category);

Page<Post> searchByKeyword(Pageable pageable, String keyword);
}
Loading

0 comments on commit ce25ea7

Please sign in to comment.