Skip to content

Commit d8e7aab

Browse files
authored
Merge pull request #306 from DevKor-github/develop
#302~#305 반영해 배포
2 parents 3a6a616 + 589a7f6 commit d8e7aab

44 files changed

Lines changed: 1905 additions & 15 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,6 @@ out/
4444
node_modules
4545

4646
# Firebase Admin Key
47-
src/main/resources/config/firebase-admin-key.json
47+
src/main/resources/config/firebase-admin-key.json
48+
49+
.claude

src/main/java/org/devkor/apu/saerok_server/domain/collection/api/dto/request/CreateCollectionCommentRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ public record CreateCollectionCommentRequest(
77
String content,
88

99
@Schema(description = "부모 댓글 ID (대댓글 작성 시)", example = "123", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
10-
Long commentId
10+
Long parentId
1111
) {
1212
}

src/main/java/org/devkor/apu/saerok_server/domain/collection/api/dto/response/GetCollectionCommentsResponse.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
import java.util.List;
77

88
public record GetCollectionCommentsResponse(
9+
@Schema(description = "댓글 목록 (원댓글만 포함, 대댓글은 각 원댓글의 replies에 포함)")
910
List<Item> items,
1011
@Schema(description = "내 컬렉션인지 여부", example = "true")
1112
Boolean isMyCollection,
12-
@Schema(description = "다음 페이지 존재 여부 (페이징 요청 시에만 유효)", example = "true")
13+
@Schema(description = "다음 페이지 존재 여부 (페이징 요청 시에만 유효, 미사용 시 null)", example = "true", nullable = true)
1314
Boolean hasNext
1415
) {
1516

@@ -28,19 +29,19 @@ public record Item(
2829
String content,
2930
@Schema(description = "댓글 상태 (ACTIVE, DELETED, BANNED)", example = "ACTIVE", requiredMode = Schema.RequiredMode.REQUIRED)
3031
String status,
31-
@Schema(description = "부모 댓글 ID (대댓글인 경우)", example = "5", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
32+
@Schema(description = "부모 댓글 ID (원댓글이면 null, 대댓글이면 부모 댓글 ID)", example = "null", nullable = true, requiredMode = Schema.RequiredMode.NOT_REQUIRED)
3233
Long parentId,
3334
@Schema(description = "좋아요 수", example = "5", requiredMode = Schema.RequiredMode.REQUIRED)
3435
int likeCount,
35-
@Schema(description = "좋아요 눌렀는지 여부", example = "true")
36+
@Schema(description = "좋아요 눌렀는지 여부 (비로그인 시 false)", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
3637
Boolean isLiked,
37-
@Schema(description = "내 댓글인지 여부", example = "false")
38+
@Schema(description = "내 댓글인지 여부 (비로그인 시 false)", example = "false", requiredMode = Schema.RequiredMode.REQUIRED)
3839
Boolean isMine,
3940
@Schema(description = "작성 시각", example = "2025-07-05T03:10:00", requiredMode = Schema.RequiredMode.REQUIRED)
4041
LocalDateTime createdAt,
4142
@Schema(description = "수정 시각", example = "2025-07-05T04:21:00", requiredMode = Schema.RequiredMode.REQUIRED)
4243
LocalDateTime updatedAt,
43-
@Schema(description = "대댓글 목록", requiredMode = Schema.RequiredMode.REQUIRED)
44+
@Schema(description = "대댓글 목록 (대댓글의 replies는 항상 빈 배열)", example = "[]", requiredMode = Schema.RequiredMode.REQUIRED)
4445
List<Item> replies
4546
) {
4647

src/main/java/org/devkor/apu/saerok_server/domain/collection/application/CollectionCommentCommandService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ public CreateCollectionCommentResponse createComment(Long userId,
4444
UserBirdCollectionComment comment;
4545
UserBirdCollectionComment parentComment = null;
4646

47-
if (req.commentId() != null) {
48-
parentComment = commentRepository.findById(req.commentId())
47+
if (req.parentId() != null) {
48+
parentComment = commentRepository.findById(req.parentId())
4949
.orElseThrow(() -> new NotFoundException("존재하지 않는 댓글 id예요"));
5050

5151
// 같은 컬렉션에 속하는지 확인

src/main/java/org/devkor/apu/saerok_server/domain/community/api/dto/response/GetCommunityMainResponse.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@
22

33
import io.swagger.v3.oas.annotations.media.Schema;
44
import org.devkor.apu.saerok_server.domain.community.api.dto.common.CommunityCollectionInfo;
5+
import org.devkor.apu.saerok_server.domain.freeboard.api.dto.response.FreeBoardPostPreviewResponse;
56

67
import java.util.List;
78

89
@Schema(description = "커뮤니티 메인 화면 조회 응답")
910
public record GetCommunityMainResponse(
1011
@Schema(description = "최근에 올라온 새록 목록 (최대 3개)")
1112
List<CommunityCollectionInfo> recentCollections,
12-
13+
1314
@Schema(description = "요즘 인기 있는 새록 목록 (최대 3개)")
1415
List<CommunityCollectionInfo> popularCollections,
15-
16+
1617
@Schema(description = "동정 요청 새록 목록 (최대 3개)")
17-
List<CommunityCollectionInfo> pendingCollections
18+
List<CommunityCollectionInfo> pendingCollections,
19+
20+
@Schema(description = "최근 자유게시판 글 (최대 3개)")
21+
List<FreeBoardPostPreviewResponse> recentFreeBoardPosts
1822
) {}

src/main/java/org/devkor/apu/saerok_server/domain/community/application/CommunityQueryService.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import org.devkor.apu.saerok_server.domain.community.api.dto.response.GetCommunitySearchUsersResponse;
99
import org.devkor.apu.saerok_server.domain.community.application.dto.CommunityQueryCommand;
1010
import org.devkor.apu.saerok_server.domain.community.core.repository.CommunityRepository;
11+
import org.devkor.apu.saerok_server.domain.freeboard.api.dto.response.FreeBoardPostPreviewResponse;
12+
import org.devkor.apu.saerok_server.domain.freeboard.application.FreeBoardPostQueryService;
1113
import org.devkor.apu.saerok_server.domain.user.core.entity.User;
1214
import org.springframework.stereotype.Service;
1315
import org.springframework.transaction.annotation.Transactional;
@@ -21,6 +23,7 @@ public class CommunityQueryService {
2123

2224
private final CommunityRepository communityRepository;
2325
private final CommunityDataAssembler dataAssembler;
26+
private final FreeBoardPostQueryService freeBoardPostQueryService;
2427

2528
public GetCommunityMainResponse getCommunityMain(Long userId) {
2629
// 메인 페이지용으로 각각 3개씩 조회
@@ -30,11 +33,13 @@ public GetCommunityMainResponse getCommunityMain(Long userId) {
3033
List<UserBirdCollection> recentCollections = communityRepository.findRecentPublicCollections(mainCommand);
3134
List<UserBirdCollection> popularCollections = communityRepository.findPopularCollections(mainCommand);
3235
List<UserBirdCollection> pendingCollections = communityRepository.findPendingBirdIdCollections(pendingCommand);
36+
List<FreeBoardPostPreviewResponse> recentFreeBoardPosts = freeBoardPostQueryService.getRecentPostsForMain(3);
3337

3438
return new GetCommunityMainResponse(
3539
dataAssembler.toCollectionInfos(recentCollections, userId),
3640
dataAssembler.toCollectionInfos(popularCollections, userId),
37-
dataAssembler.toCollectionInfos(pendingCollections, userId)
41+
dataAssembler.toCollectionInfos(pendingCollections, userId),
42+
recentFreeBoardPosts
3843
);
3944
}
4045

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package org.devkor.apu.saerok_server.domain.freeboard.api;
2+
3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.Parameter;
5+
import io.swagger.v3.oas.annotations.media.Content;
6+
import io.swagger.v3.oas.annotations.media.Schema;
7+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
8+
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
9+
import io.swagger.v3.oas.annotations.tags.Tag;
10+
import jakarta.annotation.security.PermitAll;
11+
import jakarta.validation.Valid;
12+
import lombok.RequiredArgsConstructor;
13+
import org.devkor.apu.saerok_server.domain.freeboard.api.dto.request.CreateFreeBoardPostCommentRequest;
14+
import org.devkor.apu.saerok_server.domain.freeboard.api.dto.request.UpdateFreeBoardPostCommentRequest;
15+
import org.devkor.apu.saerok_server.domain.freeboard.api.dto.response.CreateFreeBoardPostCommentResponse;
16+
import org.devkor.apu.saerok_server.domain.freeboard.api.dto.response.GetFreeBoardPostCommentCountResponse;
17+
import org.devkor.apu.saerok_server.domain.freeboard.api.dto.response.GetFreeBoardPostCommentsResponse;
18+
import org.devkor.apu.saerok_server.domain.freeboard.api.dto.response.UpdateFreeBoardPostCommentResponse;
19+
import org.devkor.apu.saerok_server.domain.freeboard.application.FreeBoardPostCommentCommandService;
20+
import org.devkor.apu.saerok_server.domain.freeboard.application.FreeBoardPostCommentQueryService;
21+
import org.devkor.apu.saerok_server.domain.freeboard.application.dto.FreeBoardCommentQueryCommand;
22+
import org.devkor.apu.saerok_server.global.security.principal.UserPrincipal;
23+
import org.devkor.apu.saerok_server.global.shared.exception.BadRequestException;
24+
import org.springframework.http.HttpStatus;
25+
import org.springframework.security.access.prepost.PreAuthorize;
26+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
27+
import org.springframework.web.bind.annotation.*;
28+
29+
@Tag(name = "Free Board API", description = "자유게시판 관련 API")
30+
@RestController
31+
@RequiredArgsConstructor
32+
@RequestMapping("${api_prefix}/community/freeboard/posts")
33+
public class FreeBoardPostCommentController {
34+
35+
private final FreeBoardPostCommentCommandService commentCommandService;
36+
private final FreeBoardPostCommentQueryService commentQueryService;
37+
38+
/* 댓글 작성 */
39+
@PostMapping("/{postId}/comments")
40+
@PreAuthorize("isAuthenticated()")
41+
@Operation(
42+
summary = "자유게시판 댓글 작성",
43+
security = @SecurityRequirement(name = "bearerAuth"),
44+
responses = {
45+
@ApiResponse(responseCode = "201", description = "댓글 작성 성공",
46+
content = @Content(schema = @Schema(implementation = CreateFreeBoardPostCommentResponse.class))),
47+
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
48+
@ApiResponse(responseCode = "401", description = "사용자 인증 실패", content = @Content),
49+
@ApiResponse(responseCode = "404", description = "게시글이 존재하지 않음", content = @Content)
50+
}
51+
)
52+
@ResponseStatus(HttpStatus.CREATED)
53+
public CreateFreeBoardPostCommentResponse createComment(
54+
@AuthenticationPrincipal UserPrincipal userPrincipal,
55+
@PathVariable Long postId,
56+
@Valid @RequestBody CreateFreeBoardPostCommentRequest request
57+
) {
58+
return commentCommandService.createComment(userPrincipal.getId(), postId, request);
59+
}
60+
61+
/* 댓글 목록 조회 */
62+
@GetMapping("/{postId}/comments")
63+
@PermitAll
64+
@Operation(
65+
summary = "자유게시판 댓글 목록 조회 (인증: optional)",
66+
description = """
67+
게시글의 댓글 목록을 조회합니다.
68+
69+
📄 **페이징 (선택)**
70+
- `page`와 `size`는 둘 다 제공해야 하며, 하나만 제공 시 Bad Request가 발생합니다.
71+
- 생략하면 전체 결과를 반환합니다.
72+
""",
73+
security = @SecurityRequirement(name = "bearerAuth"),
74+
responses = {
75+
@ApiResponse(responseCode = "200", description = "댓글 목록 조회 성공",
76+
content = @Content(schema = @Schema(implementation = GetFreeBoardPostCommentsResponse.class))),
77+
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
78+
@ApiResponse(responseCode = "404", description = "게시글이 존재하지 않음", content = @Content)
79+
}
80+
)
81+
public GetFreeBoardPostCommentsResponse listComments(
82+
@PathVariable Long postId,
83+
@AuthenticationPrincipal UserPrincipal userPrincipal,
84+
@Parameter(description = "페이지 번호 (1부터 시작)", example = "1") @RequestParam(required = false) Integer page,
85+
@Parameter(description = "페이지 크기", example = "20") @RequestParam(required = false) Integer size
86+
) {
87+
FreeBoardCommentQueryCommand command = new FreeBoardCommentQueryCommand(page, size);
88+
if (!command.hasValidPagination()) {
89+
throw new BadRequestException("page와 size 값이 유효하지 않아요.");
90+
}
91+
92+
Long userId = userPrincipal == null ? null : userPrincipal.getId();
93+
return commentQueryService.getComments(postId, userId, command);
94+
}
95+
96+
/* 댓글 수 조회 */
97+
@GetMapping("/{postId}/comments/count")
98+
@PermitAll
99+
@Operation(
100+
summary = "자유게시판 댓글 수 조회",
101+
responses = {
102+
@ApiResponse(responseCode = "200", description = "댓글 수 조회 성공",
103+
content = @Content(schema = @Schema(implementation = GetFreeBoardPostCommentCountResponse.class))),
104+
@ApiResponse(responseCode = "404", description = "게시글이 존재하지 않음", content = @Content)
105+
}
106+
)
107+
public GetFreeBoardPostCommentCountResponse getCommentCount(
108+
@PathVariable Long postId
109+
) {
110+
return commentQueryService.getCommentCount(postId);
111+
}
112+
113+
/* 댓글 수정 */
114+
@PatchMapping("/{postId}/comments/{commentId}")
115+
@PreAuthorize("isAuthenticated()")
116+
@Operation(
117+
summary = "자유게시판 댓글 수정",
118+
security = @SecurityRequirement(name = "bearerAuth"),
119+
responses = {
120+
@ApiResponse(responseCode = "200", description = "댓글 수정 성공",
121+
content = @Content(schema = @Schema(implementation = UpdateFreeBoardPostCommentResponse.class))),
122+
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
123+
@ApiResponse(responseCode = "401", description = "사용자 인증 실패", content = @Content),
124+
@ApiResponse(responseCode = "403", description = "권한 없음", content = @Content),
125+
@ApiResponse(responseCode = "404", description = "댓글 또는 게시글이 존재하지 않음", content = @Content)
126+
}
127+
)
128+
public UpdateFreeBoardPostCommentResponse updateComment(
129+
@AuthenticationPrincipal UserPrincipal userPrincipal,
130+
@PathVariable Long postId,
131+
@PathVariable Long commentId,
132+
@Valid @RequestBody UpdateFreeBoardPostCommentRequest request
133+
) {
134+
return commentCommandService.updateComment(userPrincipal.getId(), postId, commentId, request);
135+
}
136+
137+
/* 댓글 삭제 */
138+
@DeleteMapping("/{postId}/comments/{commentId}")
139+
@PreAuthorize("isAuthenticated()")
140+
@Operation(
141+
summary = "자유게시판 댓글 삭제",
142+
security = @SecurityRequirement(name = "bearerAuth"),
143+
responses = {
144+
@ApiResponse(responseCode = "204", description = "댓글 삭제 성공"),
145+
@ApiResponse(responseCode = "401", description = "사용자 인증 실패", content = @Content),
146+
@ApiResponse(responseCode = "403", description = "권한 없음", content = @Content),
147+
@ApiResponse(responseCode = "404", description = "댓글 또는 게시글이 존재하지 않음", content = @Content)
148+
}
149+
)
150+
@ResponseStatus(HttpStatus.NO_CONTENT)
151+
public void deleteComment(
152+
@AuthenticationPrincipal UserPrincipal userPrincipal,
153+
@PathVariable Long postId,
154+
@PathVariable Long commentId
155+
) {
156+
commentCommandService.deleteComment(userPrincipal.getId(), postId, commentId);
157+
}
158+
}

0 commit comments

Comments
 (0)