Skip to content

Commit 0feb4b4

Browse files
joyewon0705namgigun
authored andcommitted
Feat: 게시글 다건/단건 조회 API 구현 (#131) (#149)
* Feat: 게시글 다건/단건 조회 API 구현 * Feat: Security 인가 규칙 설정 * Test: 테스트 작성 * Docs: Swagger 문서 작성
1 parent fb3849c commit 0feb4b4

File tree

12 files changed

+775
-10
lines changed

12 files changed

+775
-10
lines changed

src/main/java/com/back/domain/board/controller/PostController.java

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package com.back.domain.board.controller;
22

3-
import com.back.domain.board.dto.PostRequest;
4-
import com.back.domain.board.dto.PostResponse;
3+
import com.back.domain.board.dto.*;
54
import com.back.domain.board.service.PostService;
65
import com.back.global.common.dto.RsData;
76
import com.back.global.security.user.CustomUserDetails;
87
import jakarta.validation.Valid;
98
import lombok.RequiredArgsConstructor;
9+
import org.springframework.data.domain.Pageable;
10+
import org.springframework.data.domain.Sort;
11+
import org.springframework.data.web.PageableDefault;
1012
import org.springframework.http.HttpStatus;
1113
import org.springframework.http.ResponseEntity;
1214
import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -32,4 +34,35 @@ public ResponseEntity<RsData<PostResponse>> createPost(
3234
response
3335
));
3436
}
37+
38+
// 게시글 다건 조회
39+
@GetMapping
40+
public ResponseEntity<RsData<PageResponse<PostListResponse>>> getPosts(
41+
@PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable,
42+
@RequestParam(required = false) String keyword,
43+
@RequestParam(required = false) String searchType,
44+
@RequestParam(required = false) Long categoryId
45+
) {
46+
PageResponse<PostListResponse> response = postService.getPosts(keyword, searchType, categoryId, pageable);
47+
return ResponseEntity
48+
.status(HttpStatus.OK)
49+
.body(RsData.success(
50+
"게시글 목록이 조회되었습니다.",
51+
response
52+
));
53+
}
54+
55+
// 게시글 단건 조회
56+
@GetMapping("/{postId}")
57+
public ResponseEntity<RsData<PostDetailResponse>> getPost(
58+
@PathVariable Long postId
59+
) {
60+
PostDetailResponse response = postService.getPost(postId);
61+
return ResponseEntity
62+
.status(HttpStatus.OK)
63+
.body(RsData.success(
64+
"게시글이 조회되었습니다.",
65+
response
66+
));
67+
}
3568
}

src/main/java/com/back/domain/board/controller/PostControllerDocs.java

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.back.domain.board.controller;
22

3-
import com.back.domain.board.dto.PostRequest;
4-
import com.back.domain.board.dto.PostResponse;
3+
import com.back.domain.board.dto.*;
54
import com.back.global.common.dto.RsData;
65
import com.back.global.security.user.CustomUserDetails;
76
import io.swagger.v3.oas.annotations.Operation;
@@ -10,9 +9,13 @@
109
import io.swagger.v3.oas.annotations.responses.ApiResponse;
1110
import io.swagger.v3.oas.annotations.responses.ApiResponses;
1211
import io.swagger.v3.oas.annotations.tags.Tag;
12+
import org.springframework.data.domain.Pageable;
13+
import org.springframework.data.web.PageableDefault;
1314
import org.springframework.http.ResponseEntity;
1415
import org.springframework.security.core.annotation.AuthenticationPrincipal;
16+
import org.springframework.web.bind.annotation.PathVariable;
1517
import org.springframework.web.bind.annotation.RequestBody;
18+
import org.springframework.web.bind.annotation.RequestParam;
1619

1720
@Tag(name = "Post API", description = "게시글 관련 API")
1821
public interface PostControllerDocs {
@@ -144,4 +147,151 @@ ResponseEntity<RsData<PostResponse>> createPost(
144147
@RequestBody PostRequest request,
145148
@AuthenticationPrincipal CustomUserDetails user
146149
);
150+
151+
@Operation(
152+
summary = "게시글 목록 조회",
153+
description = "모든 사용자가 게시글 목록을 조회할 수 있습니다. (로그인 불필요)"
154+
)
155+
@ApiResponses({
156+
@ApiResponse(
157+
responseCode = "200",
158+
description = "게시글 목록 조회 성공",
159+
content = @Content(
160+
mediaType = "application/json",
161+
examples = @ExampleObject(value = """
162+
{
163+
"success": true,
164+
"code": "SUCCESS_200",
165+
"message": "게시글 목록이 조회되었습니다.",
166+
"data": {
167+
"items": [
168+
{
169+
"postId": 1,
170+
"author": { "id": 10, "nickname": "홍길동" },
171+
"title": "첫 글",
172+
"categories": [{ "id": 1, "name": "공지사항" }],
173+
"likeCount": 5,
174+
"bookmarkCount": 2,
175+
"commentCount": 3,
176+
"createdAt": "2025-09-30T10:15:30",
177+
"updatedAt": "2025-09-30T10:20:00"
178+
}
179+
],
180+
"page": 0,
181+
"size": 10,
182+
"totalElements": 25,
183+
"totalPages": 3,
184+
"last": false
185+
}
186+
}
187+
""")
188+
)
189+
),
190+
@ApiResponse(
191+
responseCode = "400",
192+
description = "잘못된 요청 (페이징 파라미터 오류 등)",
193+
content = @Content(
194+
mediaType = "application/json",
195+
examples = @ExampleObject(value = """
196+
{
197+
"success": false,
198+
"code": "COMMON_400",
199+
"message": "잘못된 요청입니다.",
200+
"data": null
201+
}
202+
""")
203+
)
204+
),
205+
@ApiResponse(
206+
responseCode = "500",
207+
description = "서버 내부 오류",
208+
content = @Content(
209+
mediaType = "application/json",
210+
examples = @ExampleObject(value = """
211+
{
212+
"success": false,
213+
"code": "COMMON_500",
214+
"message": "서버 오류가 발생했습니다.",
215+
"data": null
216+
}
217+
""")
218+
)
219+
)
220+
})
221+
ResponseEntity<RsData<PageResponse<PostListResponse>>> getPosts(
222+
@PageableDefault(sort = "createdAt") Pageable pageable,
223+
@RequestParam(required = false) String keyword,
224+
@RequestParam(required = false) String searchType,
225+
@RequestParam(required = false) Long categoryId
226+
);
227+
228+
229+
@Operation(
230+
summary = "게시글 단건 조회",
231+
description = "모든 사용자가 특정 게시글의 상세 정보를 조회할 수 있습니다. (로그인 불필요)"
232+
)
233+
@ApiResponses({
234+
@ApiResponse(
235+
responseCode = "200",
236+
description = "게시글 단건 조회 성공",
237+
content = @Content(
238+
mediaType = "application/json",
239+
examples = @ExampleObject(value = """
240+
{
241+
"success": true,
242+
"code": "SUCCESS_200",
243+
"message": "게시글이 조회되었습니다.",
244+
"data": {
245+
"postId": 101,
246+
"author": { "id": 5, "nickname": "홍길동" },
247+
"title": "첫 번째 게시글",
248+
"content": "안녕하세요, 첫 글입니다!",
249+
"categories": [
250+
{ "id": 1, "name": "공지사항" },
251+
{ "id": 2, "name": "자유게시판" }
252+
],
253+
"likeCount": 10,
254+
"bookmarkCount": 2,
255+
"commentCount": 3,
256+
"createdAt": "2025-09-22T10:30:00",
257+
"updatedAt": "2025-09-22T10:30:00"
258+
}
259+
}
260+
""")
261+
)
262+
),
263+
@ApiResponse(
264+
responseCode = "404",
265+
description = "존재하지 않는 게시글",
266+
content = @Content(
267+
mediaType = "application/json",
268+
examples = @ExampleObject(value = """
269+
{
270+
"success": false,
271+
"code": "POST_001",
272+
"message": "존재하지 않는 게시글입니다.",
273+
"data": null
274+
}
275+
""")
276+
)
277+
),
278+
@ApiResponse(
279+
responseCode = "500",
280+
description = "서버 내부 오류",
281+
content = @Content(
282+
mediaType = "application/json",
283+
examples = @ExampleObject(value = """
284+
{
285+
"success": false,
286+
"code": "COMMON_500",
287+
"message": "서버 오류가 발생했습니다.",
288+
"data": null
289+
}
290+
""")
291+
)
292+
)
293+
})
294+
ResponseEntity<RsData<PostDetailResponse>> getPost(
295+
@PathVariable Long postId
296+
);
147297
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.back.domain.board.dto;
2+
3+
import org.springframework.data.domain.Page;
4+
5+
import java.util.List;
6+
7+
/**
8+
* 페이지 응답 DTO
9+
*
10+
* @param items 목록 데이터
11+
* @param page 현재 페이지 번호
12+
* @param size 페이지 크기
13+
* @param totalElements 전체 요소 수
14+
* @param totalPages 전체 페이지 수
15+
* @param last 마지막 페이지 여부
16+
* @param <T> 제네릭 타입
17+
*/
18+
public record PageResponse<T>(
19+
List<T> items,
20+
int page,
21+
int size,
22+
long totalElements,
23+
int totalPages,
24+
boolean last
25+
) {
26+
public static <T> PageResponse<T> from(Page<T> page) {
27+
return new PageResponse<>(
28+
page.getContent(),
29+
page.getNumber(),
30+
page.getSize(),
31+
page.getTotalElements(),
32+
page.getTotalPages(),
33+
page.isLast()
34+
);
35+
}
36+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.back.domain.board.dto;
2+
3+
import com.back.domain.board.entity.Post;
4+
5+
import java.time.LocalDateTime;
6+
import java.util.List;
7+
8+
/**
9+
* 게시글 상세 응답 DTO
10+
*
11+
* @param postId 게시글 ID
12+
* @param author 작성자 정보
13+
* @param title 게시글 제목
14+
* @param content 게시글 내용
15+
* @param categories 게시글 카테고리 목록
16+
* @param likeCount 좋아요 수
17+
* @param bookmarkCount 북마크 수
18+
* @param commentCount 댓글 수
19+
* @param createdAt 게시글 생성 일시
20+
* @param updatedAt 게시글 수정 일시
21+
*/
22+
public record PostDetailResponse(
23+
Long postId,
24+
AuthorResponse author,
25+
String title,
26+
String content,
27+
List<CategoryResponse> categories,
28+
long likeCount,
29+
long bookmarkCount,
30+
long commentCount,
31+
LocalDateTime createdAt,
32+
LocalDateTime updatedAt
33+
) {
34+
public static PostDetailResponse from(Post post) {
35+
return new PostDetailResponse(
36+
post.getId(),
37+
AuthorResponse.from(post.getUser()),
38+
post.getTitle(),
39+
post.getContent(),
40+
post.getCategories().stream()
41+
.map(CategoryResponse::from)
42+
.toList(),
43+
post.getPostLikes().size(),
44+
post.getPostBookmarks().size(),
45+
post.getComments().size(),
46+
post.getCreatedAt(),
47+
post.getUpdatedAt()
48+
);
49+
}
50+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.back.domain.board.dto;
2+
3+
import com.querydsl.core.annotations.QueryProjection;
4+
import lombok.Getter;
5+
import lombok.Setter;
6+
7+
import java.time.LocalDateTime;
8+
import java.util.List;
9+
10+
/**
11+
* 게시글 목록 응답 DTO
12+
*/
13+
@Getter
14+
public class PostListResponse {
15+
private final Long postId;
16+
private final AuthorResponse author;
17+
private final String title;
18+
private final long likeCount;
19+
private final long bookmarkCount;
20+
private final long commentCount;
21+
private final LocalDateTime createdAt;
22+
private final LocalDateTime updatedAt;
23+
24+
@Setter
25+
private List<CategoryResponse> categories;
26+
27+
@QueryProjection
28+
public PostListResponse(Long postId,
29+
AuthorResponse author,
30+
String title,
31+
List<CategoryResponse> categories,
32+
long likeCount,
33+
long bookmarkCount,
34+
long commentCount,
35+
LocalDateTime createdAt,
36+
LocalDateTime updatedAt) {
37+
this.postId = postId;
38+
this.author = author;
39+
this.title = title;
40+
this.categories = categories;
41+
this.likeCount = likeCount;
42+
this.bookmarkCount = bookmarkCount;
43+
this.commentCount = commentCount;
44+
this.createdAt = createdAt;
45+
this.updatedAt = updatedAt;
46+
}
47+
48+
/**
49+
* 작성자 응답 DTO
50+
*/
51+
@Getter
52+
public static class AuthorResponse {
53+
private final Long id;
54+
private final String nickname;
55+
56+
@QueryProjection
57+
public AuthorResponse(Long userId, String nickname) {
58+
this.id = userId;
59+
this.nickname = nickname;
60+
}
61+
}
62+
63+
/**
64+
* 카테고리 응답 DTO
65+
*/
66+
@Getter
67+
public static class CategoryResponse {
68+
private final Long id;
69+
private final String name;
70+
71+
@QueryProjection
72+
public CategoryResponse(Long id, String name) {
73+
this.id = id;
74+
this.name = name;
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)