-
Notifications
You must be signed in to change notification settings - Fork 20
[3주차] 정대교/[feat] 게시글 도메인 API 구현 #113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 정대교/main
Are you sure you want to change the base?
The head ref may contain hidden characters: "\uC815\uB300\uAD50/3\uC8FC\uCC28"
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| package com.example.springbootassignment.controller; | ||
|
|
||
| import com.example.springbootassignment.dto.PostCreateRequest; | ||
| import com.example.springbootassignment.dto.PostListResponse; | ||
| import com.example.springbootassignment.dto.PostResponse; | ||
| import com.example.springbootassignment.dto.PostUpdateRequest; | ||
| import com.example.springbootassignment.service.PostService; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/posts") | ||
| public class PostController { | ||
|
|
||
| private final PostService postService; | ||
|
|
||
| public PostController(PostService postService) { | ||
| this.postService = postService; | ||
| } | ||
|
|
||
| /** | ||
| * 게시글 목록 조회 | ||
| * GET /posts?page=1&size=10&sort=createdAt&order=desc | ||
| */ | ||
| @GetMapping | ||
| public ResponseEntity<PostListResponse> getPostList( | ||
| @RequestParam(defaultValue = "1") int page, | ||
| @RequestParam(defaultValue = "10") int size, | ||
| @RequestParam(defaultValue = "createdAt") String sort, | ||
| @RequestParam(defaultValue = "desc") String order) { | ||
| PostListResponse response = postService.getPostList(page, size, sort, order); | ||
| return ResponseEntity.ok(response); | ||
| } | ||
|
|
||
| /** | ||
| * 게시글 상세 조회 | ||
| * GET /posts/{postId} | ||
| */ | ||
| @GetMapping("/{postId}") | ||
| public ResponseEntity<PostResponse> getPostDetail(@PathVariable Long postId) { | ||
| PostResponse response = postService.getPostDetail(postId); | ||
| return ResponseEntity.ok(response); | ||
| } | ||
|
|
||
| /** | ||
| * 게시글 작성 | ||
| * POST /posts | ||
| */ | ||
| @PostMapping | ||
| public ResponseEntity<PostResponse> createPost(@RequestBody PostCreateRequest request) { | ||
| PostResponse response = postService.createPost(request); | ||
| return ResponseEntity.status(HttpStatus.CREATED).body(response); | ||
| } | ||
|
|
||
| /** | ||
| * 게시글 수정 | ||
| * PUT /posts/{postId} | ||
| */ | ||
| @PutMapping("/{postId}") | ||
| public ResponseEntity<PostResponse> updatePost( | ||
| @PathVariable Long postId, | ||
| @RequestBody PostUpdateRequest request) { | ||
| PostResponse response = postService.updatePost(postId, request); | ||
| return ResponseEntity.ok(response); | ||
| } | ||
|
|
||
| /** | ||
| * 게시글 삭제 | ||
| * DELETE /posts/{postId} | ||
| */ | ||
| @DeleteMapping("/{postId}") | ||
| public ResponseEntity<Void> deletePost(@PathVariable Long postId) { | ||
| postService.deletePost(postId); | ||
| return ResponseEntity.noContent().build(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,15 +13,16 @@ | |
| @Entity | ||
| @Table(name = "posts") | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @Setter | ||
| @NoArgsConstructor | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 엔티티에
|
||
| public class Post { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "user_id", nullable = false) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| @JoinColumn(name = "user_id", nullable = true) // ← nullable = true로 변경 | ||
| private User user; | ||
|
|
||
| @Column(nullable = false, length = 100) | ||
|
|
@@ -52,4 +53,11 @@ public class Post { | |
|
|
||
| @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) | ||
| private List<Comment> comments = new ArrayList<>(); | ||
|
|
||
| /** | ||
| * 조회수 증가 | ||
| */ | ||
| public void increaseViewCount() { | ||
| this.viewCount++; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.example.springbootassignment.domain.post.entity; | ||
|
|
||
| import com.example.springbootassignment.domain.post.entity.Post; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface PostRepository extends JpaRepository<Post, Long> { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package com.example.springbootassignment.dto; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import lombok.Setter; | ||
|
|
||
| @Getter | ||
| @Setter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class ErrorResponse { | ||
| private String code; // 에러 코드 (COMMON400, POST4041 등) | ||
| private String message; // 사용자 친화적인 메시지 | ||
| private String details; // 상세 정보 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.example.springbootassignment.dto; | ||
|
|
||
| import jakarta.validation.constraints.NotBlank; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| public class PostCreateRequest { | ||
|
|
||
| @NotBlank(message = "제목은 필수 입력 사항입니다.") | ||
| private String title; | ||
|
|
||
| @NotBlank(message = "내용은 필수 입력 사항입니다.") | ||
| private String content; | ||
|
|
||
| private Long userId; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package com.example.springbootassignment.dto; | ||
|
|
||
| import com.example.springbootassignment.domain.post.entity.Post; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import lombok.Setter; | ||
| import org.springframework.data.domain.Page; | ||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
|
|
||
| @Getter | ||
| @Setter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class PostListResponse { | ||
|
|
||
| private List<Long> postId; | ||
| private List<String> title; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 응답 DTO가 컬럼별로 @Getter
@AllArgsConstructor
public class PostListResponse {
private List<PostSummary> posts;
@Getter
@Builder
public static class PostSummary {
private Long postId;
private String title;
private String nickname;
private LocalDateTime createdAt;
}
} |
||
| private List<String> nickname; | ||
| private List<LocalDateTime> createdAt; | ||
|
|
||
| /** | ||
| * Page<Post>를 PostListResponse로 변환 | ||
| */ | ||
| public static PostListResponse from(Page<Post> posts) { | ||
| PostListResponse response = new PostListResponse(); | ||
| response.postId = posts.getContent().stream() | ||
| .map(Post::getId) | ||
| .toList(); | ||
| response.title = posts.getContent().stream() | ||
| .map(Post::getTitle) | ||
| .toList(); | ||
| response.nickname = posts.getContent().stream() | ||
| .map(post -> post.getUser() != null ? post.getUser().getNickname() : "unknown") | ||
| .toList(); | ||
| response.createdAt = posts.getContent().stream() | ||
| .map(Post::getCreatedAt) | ||
| .toList(); | ||
| return response; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| package com.example.springbootassignment.dto; | ||
|
|
||
| import com.example.springbootassignment.domain.post.entity.Post; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import lombok.Setter; | ||
| import java.time.LocalDateTime; | ||
|
|
||
| @Getter | ||
| @Setter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class PostResponse { | ||
|
|
||
| private Long postId; | ||
| private String title; | ||
| private String content; | ||
| private LocalDateTime createdAt; | ||
| private LocalDateTime updatedAt; | ||
|
|
||
| /** | ||
| * Post Entity를 PostResponse DTO로 변환 | ||
| */ | ||
| public static PostResponse from(Post post) { | ||
| PostResponse response = new PostResponse(); | ||
| response.postId = post.getId(); | ||
| response.title = post.getTitle(); | ||
| response.content = post.getContent(); | ||
| response.createdAt = post.getCreatedAt(); | ||
| response.updatedAt = post.getUpdatedAt(); | ||
| return response; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package com.example.springbootassignment.dto; | ||
|
|
||
| import jakarta.validation.constraints.NotBlank; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| public class PostUpdateRequest { | ||
|
|
||
| @NotBlank(message = "제목은 필수 입력 사항입니다.") | ||
| private String title; | ||
|
|
||
| @NotBlank(message = "내용은 필수 입력 사항입니다.") | ||
| private String content; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.example.springbootassignment.exception; | ||
|
|
||
| public class CommentNotFoundException extends RuntimeException { | ||
| public CommentNotFoundException(Long commentId) { | ||
| super("존재하지 않는 댓글입니다. id: " + commentId); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.example.springbootassignment.exception; | ||
|
|
||
| public class ForbiddenException extends RuntimeException { | ||
| public ForbiddenException(Long postId, Long userId) { | ||
| super("게시글 id: " + postId + "를(을) 수정/삭제할 권한이 없습니다. (사용자 id: " + userId + ")"); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| package com.example.springbootassignment.exception; | ||
|
|
||
| import com.example.springbootassignment.dto.ErrorResponse; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||
| import org.springframework.web.bind.annotation.RestControllerAdvice; | ||
|
|
||
| @RestControllerAdvice | ||
| public class GlobalExceptionHandler { | ||
|
|
||
| /** | ||
| * 잘못된 요청 (400 Bad Request) | ||
| * - 제목이나 내용이 비어있는 경우 | ||
| * - 유효하지 않은 입력값 | ||
| */ | ||
| @ExceptionHandler(IllegalArgumentException.class) | ||
| public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException e) { | ||
| ErrorResponse errorResponse = new ErrorResponse( | ||
| "COMMON400", | ||
| "잘못된 요청입니다. 입력값을 확인해주세요.", | ||
| e.getMessage() | ||
| ); | ||
| return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse); | ||
| } | ||
|
|
||
| /** | ||
| * 인증 오류 (401 Unauthorized) | ||
| * - 게시글 작성 시 필수 항목 누락 | ||
| * - JWT 토큰 없음 | ||
| */ | ||
| @ExceptionHandler(UnauthorizedException.class) | ||
| public ResponseEntity<ErrorResponse> handleUnauthorizedException(UnauthorizedException e) { | ||
| ErrorResponse errorResponse = new ErrorResponse( | ||
| "COMMON401", | ||
| "인증이 필요합니다.", | ||
| e.getMessage() | ||
| ); | ||
| return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse); | ||
| } | ||
|
|
||
| /** | ||
| * 권한 오류 (403 Forbidden) | ||
| * - 자신이 쓴 글이 아닌데 수정/삭제 시도 | ||
| */ | ||
| @ExceptionHandler(ForbiddenException.class) | ||
| public ResponseEntity<ErrorResponse> handleForbiddenException(ForbiddenException e) { | ||
| ErrorResponse errorResponse = new ErrorResponse( | ||
| "COMMON403", | ||
| "접근 권한이 없습니다.", | ||
| e.getMessage() | ||
| ); | ||
| return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorResponse); | ||
| } | ||
|
|
||
| /** | ||
| * 게시글 조회 실패 (404 Not Found) | ||
| * - 존재하지 않는 게시글 ID로 조회/수정/삭제 시도 | ||
| */ | ||
| @ExceptionHandler(PostNotFoundException.class) | ||
| public ResponseEntity<ErrorResponse> handlePostNotFoundException(PostNotFoundException e) { | ||
| ErrorResponse errorResponse = new ErrorResponse( | ||
| "POST4041", | ||
| "게시글을 찾을 수 없습니다.", | ||
| e.getMessage() | ||
| ); | ||
| return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); | ||
| } | ||
|
|
||
| /** | ||
| * 댓글 조회 실패 (404 Not Found) | ||
| * - 존재하지 않는 댓글 ID | ||
| */ | ||
| @ExceptionHandler(CommentNotFoundException.class) | ||
| public ResponseEntity<ErrorResponse> handleCommentNotFoundException(CommentNotFoundException e) { | ||
| ErrorResponse errorResponse = new ErrorResponse( | ||
| "COMMON404", | ||
| "댓글을 찾을 수 없습니다.", | ||
| e.getMessage() | ||
| ); | ||
| return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); | ||
| } | ||
|
|
||
| /** | ||
| * 내부 서버 오류 (500 Internal Server Error) | ||
| * - 예상치 못한 오류 | ||
| */ | ||
| @ExceptionHandler(Exception.class) | ||
| public ResponseEntity<ErrorResponse> handleGeneralException(Exception e) { | ||
| ErrorResponse errorResponse = new ErrorResponse( | ||
| "COMMON500", | ||
| "서버 내부 오류가 발생했습니다. 잠시 후 다시 시도해주세요.", | ||
| e.getMessage() | ||
| ); | ||
| return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.example.springbootassignment.exception; | ||
|
|
||
| public class PostNotFoundException extends RuntimeException { | ||
|
|
||
| public PostNotFoundException(Long postId) { | ||
| super("존재하지 않는 게시글입니다. id: " + postId); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.example.springbootassignment.exception; | ||
|
|
||
| public class UnauthorizedException extends RuntimeException { | ||
| public UnauthorizedException(String message) { | ||
| super(message); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@RequestBody앞에@Valid가 빠져 있어서PostCreateRequest의@NotBlank검증이 실제로 동작하지 않는 상태예요.@Valid를 추가해주시면GlobalExceptionHandler의MethodArgumentNotValidException처리와 바로 연결됩니다!updatePost의@RequestBody에도 동일하게 적용해주시면 좋을 것 같아요.