Skip to content
Open
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
7 changes: 7 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(./gradlew compileJava)"
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.example.leets7th.domain.comment.controller;

import com.example.leets7th.domain.comment.dto.CommentCreateRequest;
import com.example.leets7th.domain.comment.service.CommentService;
import com.example.leets7th.global.common.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/posts/{postId}/comments")
@RequiredArgsConstructor
public class CommentController implements CommentControllerDocs {

private final CommentService commentService;

@PostMapping
public ResponseEntity<ApiResponse<Map<String, Object>>> createComment(
@PathVariable Long postId,
@RequestBody @Valid CommentCreateRequest request,
@RequestHeader(value = "X-User-Id", defaultValue = "1") Long userId
) {
Long commentId = commentService.createComment(postId, request, userId);
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(Map.of("commentId", commentId, "message", "댓글이 작성되었습니다.")));
}

@PatchMapping("/{commentId}/adopt")
public ResponseEntity<ApiResponse<Map<String, String>>> adoptComment(
@PathVariable Long postId,
@PathVariable Long commentId,
@RequestHeader(value = "X-User-Id", defaultValue = "1") Long userId
) {
commentService.adoptComment(postId, commentId, userId);
return ResponseEntity.ok(ApiResponse.success(Map.of("message", "댓글이 채택되었습니다.")));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.example.leets7th.domain.comment.controller;

import com.example.leets7th.domain.comment.dto.CommentCreateRequest;
import com.example.leets7th.global.common.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;

import java.util.Map;

@Tag(name = "Comment", description = "댓글 관련 API")
public interface CommentControllerDocs {

@Operation(summary = "댓글 작성", description = "게시글에 댓글을 작성합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "201", description = "작성 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "게시글 없음")
})
ResponseEntity<ApiResponse<Map<String, Object>>> createComment(
@Parameter(description = "게시글 ID", example = "1") @PathVariable Long postId,
@RequestBody @Valid CommentCreateRequest request,
@Parameter(description = "작성자 ID (임시)", example = "1")
@RequestHeader(value = "X-User-Id", defaultValue = "1") Long userId
);

@Operation(summary = "댓글 채택", description = "게시글 작성자가 댓글을 채택합니다. 게시글당 하나의 댓글만 채택 가능합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "채택 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "해당 게시글의 댓글이 아님"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "채택 권한 없음 (게시글 작성자가 아님)"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "게시글 또는 댓글 없음"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "이미 채택된 댓글 존재")
Comment on lines +34 to +38
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

역시 스웨거 문서관리 👍 👍 👍

})
ResponseEntity<ApiResponse<Map<String, String>>> adoptComment(
@Parameter(description = "게시글 ID", example = "1") @PathVariable Long postId,
@Parameter(description = "댓글 ID", example = "1") @PathVariable Long commentId,
@Parameter(description = "요청자 ID (임시)", example = "1")
@RequestHeader(value = "X-User-Id", defaultValue = "1") Long userId
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.leets7th.domain.comment.dto;

import jakarta.validation.constraints.NotBlank;

public record CommentCreateRequest(
@NotBlank(message = "댓글 내용을 입력해주세요.")
String content
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
import com.example.leets7th.global.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.AccessLevel;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "comments")
public class Comment extends BaseEntity {
Expand All @@ -29,6 +32,22 @@ public class Comment extends BaseEntity {
@JoinColumn(name = "post_id", nullable = false)
private Post post;

// 채택 여부
@Column(nullable = false)
private boolean adopted = false;

public static Comment create(String content, User user, Post post) {
Comment comment = new Comment();
comment.content = content;
comment.user = user;
comment.post = post;
return comment;
}

public void adopt() {
this.adopted = true;
}

// 연관관계 편의 메서드
public void setUser(User user) {
this.user = user;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.leets7th.domain.comment.repository;

import com.example.leets7th.domain.comment.entity.Comment;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CommentRepository extends JpaRepository<Comment, Long> {

boolean existsByPostIdAndAdoptedTrue(Long postId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.example.leets7th.domain.comment.service;

import com.example.leets7th.domain.comment.dto.CommentCreateRequest;
import com.example.leets7th.domain.comment.entity.Comment;
import com.example.leets7th.domain.comment.repository.CommentRepository;
import com.example.leets7th.domain.post.entity.Post;
import com.example.leets7th.domain.post.repository.PostRepository;
import com.example.leets7th.domain.user.entity.User;
import com.example.leets7th.domain.user.repository.UserRepository;
import com.example.leets7th.global.exception.AlreadyAdoptedException;
import com.example.leets7th.global.exception.CommentNotFoundException;
import com.example.leets7th.global.exception.ForbiddenException;
import com.example.leets7th.global.exception.PostNotFoundException;
import com.example.leets7th.global.exception.UserNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CommentService {

private final CommentRepository commentRepository;
private final PostRepository postRepository;
private final UserRepository userRepository;

@Transactional
public Long createComment(Long postId, CommentCreateRequest request, Long userId) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new PostNotFoundException(postId));
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
Comment comment = Comment.create(request.content(), user, post);
commentRepository.save(comment);
return comment.getId();
}

@Transactional
public void adoptComment(Long postId, Long commentId, Long userId) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new PostNotFoundException(postId));

// 게시글 작성자만 채택 가능
if (!post.getUser().getId().equals(userId)) {
throw new ForbiddenException("게시글 작성자만 댓글을 채택할 수 있습니다.");
}

Comment comment = commentRepository.findById(commentId)
.orElseThrow(() -> new CommentNotFoundException(commentId));

// 해당 게시글의 댓글인지 확인
if (!comment.getPost().getId().equals(postId)) {
throw new IllegalArgumentException("해당 게시글에 속한 댓글이 아닙니다.");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기도 동일하게 커스텀 예외처리하는건 어떨까요~?!

}

// 이미 채택된 댓글이 있는지 확인
if (commentRepository.existsByPostIdAndAdoptedTrue(postId)) {
throw new AlreadyAdoptedException();
}

comment.adopt();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@
import com.example.leets7th.domain.post.dto.response.PostListResponse;
import com.example.leets7th.domain.post.service.PostService;
import com.example.leets7th.global.common.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
Expand All @@ -20,97 +16,62 @@

import java.util.Map;

@Tag(name = "Post", description = "게시글 관련 API")
@RestController
@RequestMapping("/api/posts")
@RequiredArgsConstructor
@Validated
public class PostController {
public class PostController implements PostControllerDocs {

private final PostService postService;

// 게시글 목록 조회
@Operation(summary = "게시글 목록 조회", description = "페이지 단위로 게시글 목록을 조회합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공")
})
@GetMapping
public ResponseEntity<ApiResponse<PostListResponse>> getPosts(
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0")
@RequestParam(defaultValue = "0") @Min(value = 0, message = "페이지 번호는 0 이상이어야 합니다.") int page,
@Parameter(description = "페이지 크기", example = "10")
@RequestParam(defaultValue = "10") @Min(value = 1, message = "페이지 크기는 1 이상이어야 합니다.") int size
) {
PostListResponse response = postService.getPosts(page, size);
return ResponseEntity.ok(ApiResponse.success("POST_LIST_SUCCESS", "게시글 목록 조회 성공", response));
return ResponseEntity.ok(ApiResponse.success("POST_LIST_SUCCESS", "게시글 목록 조회 성공", postService.getPosts(page, size)));
}

// 게시글 상세 조회
@Operation(summary = "게시글 상세 조회", description = "게시글 ID로 단건 조회합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "게시글 없음")
})
@GetMapping("/{postId}")
public ResponseEntity<ApiResponse<PostDetailResponse>> getPost(
@Parameter(description = "게시글 ID", example = "1") @PathVariable Long postId) {
PostDetailResponse response = postService.getPost(postId);
return ResponseEntity.ok(ApiResponse.success("POST_DETAIL_SUCCESS", "게시글 상세 조회 성공", response));
public ResponseEntity<ApiResponse<PostDetailResponse>> getPost(@PathVariable Long postId) {
return ResponseEntity.ok(ApiResponse.success("POST_DETAIL_SUCCESS", "게시글 상세 조회 성공", postService.getPost(postId)));
}

// 게시글 작성
// TODO: 실제 인증 구현 시 @AuthenticationPrincipal로 userId 주입
@Operation(summary = "게시글 작성", description = "새 게시글을 생성합니다. X-User-Id 헤더로 작성자를 지정합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "201", description = "생성 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "입력값 오류")
})
@PostMapping
public ResponseEntity<ApiResponse<Map<String, Object>>> createPost(
@RequestBody @Valid PostCreateRequest request,
@Parameter(description = "작성자 ID (임시)", example = "1")
@RequestHeader(value = "X-User-Id", defaultValue = "1") Long userId
) {
Long postId = postService.createPost(request, userId);
Map<String, Object> data = Map.of(
"postId", postId,
"message", "게시글이 생성되었습니다."
);
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(data));
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(Map.of("postId", postId, "message", "게시글이 생성되었습니다.")));
}

// 게시글 수정
@Operation(summary = "게시글 수정", description = "게시글 제목/내용을 수정합니다. 작성자만 수정 가능합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "수정 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "수정 권한 없음"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "게시글 없음")
})
@PatchMapping("/{postId}")
public ResponseEntity<ApiResponse<Map<String, String>>> updatePost(
@Parameter(description = "게시글 ID", example = "1") @PathVariable Long postId,
@PathVariable Long postId,
@RequestBody PostUpdateRequest request,
@Parameter(description = "요청자 ID (임시)", example = "1")
@RequestHeader(value = "X-User-Id", defaultValue = "1") Long userId
) {
postService.updatePost(postId, request, userId);
return ResponseEntity.ok(ApiResponse.success(Map.of("message", "게시글이 수정되었습니다.")));
}

// 게시글 삭제
@Operation(summary = "게시글 삭제", description = "게시글을 삭제합니다. 작성자만 삭제 가능합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "삭제 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "삭제 권한 없음"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "게시글 없음")
})
@DeleteMapping("/{postId}")
public ResponseEntity<ApiResponse<Map<String, String>>> deletePost(
@Parameter(description = "게시글 ID", example = "1") @PathVariable Long postId,
@Parameter(description = "요청자 ID (임시)", example = "1")
@PathVariable Long postId,
@RequestHeader(value = "X-User-Id", defaultValue = "1") Long userId
) {
postService.deletePost(postId, userId);
return ResponseEntity.ok(ApiResponse.success(Map.of("message", "게시글이 삭제되었습니다.")));
}

@PatchMapping("/{postId}/hide")
public ResponseEntity<ApiResponse<Map<String, String>>> hidePost(
@PathVariable Long postId,
@RequestHeader(value = "X-User-Id", defaultValue = "1") Long userId
) {
postService.hidePost(postId, userId);
return ResponseEntity.ok(ApiResponse.success(Map.of("message", "게시글이 숨김 처리되었습니다.")));
}
}
Loading