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
37 changes: 35 additions & 2 deletions src/main/java/com/back/domain/board/controller/PostController.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.back.domain.board.controller;

import com.back.domain.board.dto.PostRequest;
import com.back.domain.board.dto.PostResponse;
import com.back.domain.board.dto.*;
import com.back.domain.board.service.PostService;
import com.back.global.common.dto.RsData;
import com.back.global.security.user.CustomUserDetails;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
Expand All @@ -32,4 +34,35 @@ public ResponseEntity<RsData<PostResponse>> createPost(
response
));
}

// 게시글 다건 조회
@GetMapping
public ResponseEntity<RsData<PageResponse<PostListResponse>>> getPosts(
@PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String searchType,
@RequestParam(required = false) Long categoryId
) {
PageResponse<PostListResponse> response = postService.getPosts(keyword, searchType, categoryId, pageable);
return ResponseEntity
.status(HttpStatus.OK)
.body(RsData.success(
"게시글 목록이 조회되었습니다.",
response
));
}

// 게시글 단건 조회
@GetMapping("/{postId}")
public ResponseEntity<RsData<PostDetailResponse>> getPost(
@PathVariable Long postId
) {
PostDetailResponse response = postService.getPost(postId);
return ResponseEntity
.status(HttpStatus.OK)
.body(RsData.success(
"게시글이 조회되었습니다.",
response
));
}
}
154 changes: 152 additions & 2 deletions src/main/java/com/back/domain/board/controller/PostControllerDocs.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.back.domain.board.controller;

import com.back.domain.board.dto.PostRequest;
import com.back.domain.board.dto.PostResponse;
import com.back.domain.board.dto.*;
import com.back.global.common.dto.RsData;
import com.back.global.security.user.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -10,9 +9,13 @@
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

@Tag(name = "Post API", description = "게시글 관련 API")
public interface PostControllerDocs {
Expand Down Expand Up @@ -144,4 +147,151 @@ ResponseEntity<RsData<PostResponse>> createPost(
@RequestBody PostRequest request,
@AuthenticationPrincipal CustomUserDetails user
);

@Operation(
summary = "게시글 목록 조회",
description = "모든 사용자가 게시글 목록을 조회할 수 있습니다. (로그인 불필요)"
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "게시글 목록 조회 성공",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": true,
"code": "SUCCESS_200",
"message": "게시글 목록이 조회되었습니다.",
"data": {
"items": [
{
"postId": 1,
"author": { "id": 10, "nickname": "홍길동" },
"title": "첫 글",
"categories": [{ "id": 1, "name": "공지사항" }],
"likeCount": 5,
"bookmarkCount": 2,
"commentCount": 3,
"createdAt": "2025-09-30T10:15:30",
"updatedAt": "2025-09-30T10:20:00"
}
],
"page": 0,
"size": 10,
"totalElements": 25,
"totalPages": 3,
"last": false
}
}
""")
)
),
@ApiResponse(
responseCode = "400",
description = "잘못된 요청 (페이징 파라미터 오류 등)",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "COMMON_400",
"message": "잘못된 요청입니다.",
"data": null
}
""")
)
),
@ApiResponse(
responseCode = "500",
description = "서버 내부 오류",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "COMMON_500",
"message": "서버 오류가 발생했습니다.",
"data": null
}
""")
)
)
})
ResponseEntity<RsData<PageResponse<PostListResponse>>> getPosts(
@PageableDefault(sort = "createdAt") Pageable pageable,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String searchType,
@RequestParam(required = false) Long categoryId
);


@Operation(
summary = "게시글 단건 조회",
description = "모든 사용자가 특정 게시글의 상세 정보를 조회할 수 있습니다. (로그인 불필요)"
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "게시글 단건 조회 성공",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": true,
"code": "SUCCESS_200",
"message": "게시글이 조회되었습니다.",
"data": {
"postId": 101,
"author": { "id": 5, "nickname": "홍길동" },
"title": "첫 번째 게시글",
"content": "안녕하세요, 첫 글입니다!",
"categories": [
{ "id": 1, "name": "공지사항" },
{ "id": 2, "name": "자유게시판" }
],
"likeCount": 10,
"bookmarkCount": 2,
"commentCount": 3,
"createdAt": "2025-09-22T10:30:00",
"updatedAt": "2025-09-22T10:30:00"
}
}
""")
)
),
@ApiResponse(
responseCode = "404",
description = "존재하지 않는 게시글",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "POST_001",
"message": "존재하지 않는 게시글입니다.",
"data": null
}
""")
)
),
@ApiResponse(
responseCode = "500",
description = "서버 내부 오류",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "COMMON_500",
"message": "서버 오류가 발생했습니다.",
"data": null
}
""")
)
)
})
ResponseEntity<RsData<PostDetailResponse>> getPost(
@PathVariable Long postId
);
}
36 changes: 36 additions & 0 deletions src/main/java/com/back/domain/board/dto/PageResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.back.domain.board.dto;

import org.springframework.data.domain.Page;

import java.util.List;

/**
* 페이지 응답 DTO
*
* @param items 목록 데이터
* @param page 현재 페이지 번호
* @param size 페이지 크기
* @param totalElements 전체 요소 수
* @param totalPages 전체 페이지 수
* @param last 마지막 페이지 여부
* @param <T> 제네릭 타입
*/
public record PageResponse<T>(
List<T> items,
int page,
int size,
long totalElements,
int totalPages,
boolean last
) {
public static <T> PageResponse<T> from(Page<T> page) {
return new PageResponse<>(
page.getContent(),
page.getNumber(),
page.getSize(),
page.getTotalElements(),
page.getTotalPages(),
page.isLast()
);
}
}
50 changes: 50 additions & 0 deletions src/main/java/com/back/domain/board/dto/PostDetailResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.back.domain.board.dto;

import com.back.domain.board.entity.Post;

import java.time.LocalDateTime;
import java.util.List;

/**
* 게시글 상세 응답 DTO
*
* @param postId 게시글 ID
* @param author 작성자 정보
* @param title 게시글 제목
* @param content 게시글 내용
* @param categories 게시글 카테고리 목록
* @param likeCount 좋아요 수
* @param bookmarkCount 북마크 수
* @param commentCount 댓글 수
* @param createdAt 게시글 생성 일시
* @param updatedAt 게시글 수정 일시
*/
public record PostDetailResponse(
Long postId,
AuthorResponse author,
String title,
String content,
List<CategoryResponse> categories,
long likeCount,
long bookmarkCount,
long commentCount,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
public static PostDetailResponse from(Post post) {
return new PostDetailResponse(
post.getId(),
AuthorResponse.from(post.getUser()),
post.getTitle(),
post.getContent(),
post.getCategories().stream()
.map(CategoryResponse::from)
.toList(),
post.getPostLikes().size(),
post.getPostBookmarks().size(),
post.getComments().size(),
post.getCreatedAt(),
post.getUpdatedAt()
);
}
}
77 changes: 77 additions & 0 deletions src/main/java/com/back/domain/board/dto/PostListResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.back.domain.board.dto;

import com.querydsl.core.annotations.QueryProjection;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.List;

/**
* 게시글 목록 응답 DTO
*/
@Getter
public class PostListResponse {
private final Long postId;
private final AuthorResponse author;
private final String title;
private final long likeCount;
private final long bookmarkCount;
private final long commentCount;
private final LocalDateTime createdAt;
private final LocalDateTime updatedAt;

@Setter
private List<CategoryResponse> categories;

@QueryProjection
public PostListResponse(Long postId,
AuthorResponse author,
String title,
List<CategoryResponse> categories,
long likeCount,
long bookmarkCount,
long commentCount,
LocalDateTime createdAt,
LocalDateTime updatedAt) {
this.postId = postId;
this.author = author;
this.title = title;
this.categories = categories;
this.likeCount = likeCount;
this.bookmarkCount = bookmarkCount;
this.commentCount = commentCount;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}

/**
* 작성자 응답 DTO
*/
@Getter
public static class AuthorResponse {
private final Long id;
private final String nickname;

@QueryProjection
public AuthorResponse(Long userId, String nickname) {
this.id = userId;
this.nickname = nickname;
}
}

/**
* 카테고리 응답 DTO
*/
@Getter
public static class CategoryResponse {
private final Long id;
private final String name;

@QueryProjection
public CategoryResponse(Long id, String name) {
this.id = id;
this.name = name;
}
}
}
Loading