Conversation
예제 설명Walkthrough게시판 기능의 완전한 구현을 소개합니다. 게시글 및 댓글의 CRUD 작업, 좋아요/북마크 토글, 파일 첨부 관리 기능이 포함된 엔티티, 저장소, 서비스, REST 컨트롤러, 파일 업로드 서비스가 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant PC as PostController
participant PS as PostServiceImpl
participant DB as Repositories
participant FUS as FileUploadService
User->>PC: POST /api/post (PostRequest + files)
PC->>PS: savePost(request, userId)
PS->>DB: userRepository.findById(userId)
activate PS
PS->>PS: 새 Post 생성
PS->>DB: postRepository.save(post)
PS->>FUS: store(file) for each file
FUS->>FUS: UUID + 파일명 생성
FUS-->>PS: 저장된 파일명
PS->>DB: postAttachmentRepository.save(attachment)
deactivate PS
PS-->>PC: void
PC-->>User: 201 Created
User->>PC: GET /api/post/{postId}?commentPage=0&commentSize=10
PC->>PS: getPostDetail(postId, pageNum, pageSize)
PS->>DB: postRepository.findById(postId)
PS->>DB: commentRepository.findAllByPostId(postId, pageable)
PS->>DB: postAttachmentRepository.findAllByPostId(postId)
PS-->>PC: PostResponse (comments + attachments)
PC-->>User: 200 OK + PostResponse
User->>PC: POST /api/post/{postId}/like
PC->>PS: toggleLike(postId, userId)
PS->>DB: postLikeRepository.findByPostIdAndUserId(...)
alt 좋아요 없음
PS->>DB: postLikeRepository.save(new PostLike)
PS->>DB: post.likeCount++
else 좋아요 있음
PS->>DB: postLikeRepository.delete(postLike)
PS->>DB: post.likeCount--
end
PS-->>PC: void
PC-->>User: 200 OK
User->>PC: DELETE /api/post/{postId}
PC->>PS: deletePost(postId, userId)
PS->>DB: postRepository.findById(postId)
PS->>PS: 소유권 검증
PS->>DB: commentRepository.findAllByPostId(postId)
PS->>FUS: delete(filename) for each attachment
PS->>DB: postAttachmentRepository.deleteAllByPostId(postId)
PS->>DB: postLikeRepository.deleteAllByPostId(postId)
PS->>DB: postBookmarkRepository.deleteAllByPostId(postId)
PS->>DB: commentRepository.deleteAllByPostId(postId)
PS->>DB: postRepository.delete(post)
PS-->>PC: void
PC-->>User: 204 No Content
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45분 주의가 필요한 영역:
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (9)
backend/src/main/java/org/sejongisc/backend/board/dto/BoardDto.java (2)
8-10: 클래스 구조가 적절합니다.Lombok 어노테이션을 사용한 불변 DTO 설계가 올바르며, 코드베이스의 다른 DTO들과 일관성 있는 패턴을 따르고 있습니다.
선택사항: 객체 생성 시 더 유연한 방식을 위해
@Builder어노테이션을 추가하는 것을 고려해보세요.@Getter +@Builder @AllArgsConstructor public class BoardDto {
11-13: 필드 타입이 적절하나 유효성 검증 추가를 권장합니다.필드 타입과 명명은 적절하지만, API 안정성 향상을 위해 유효성 검증 어노테이션 추가를 고려해보세요.
+import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + @Getter @AllArgsConstructor public class BoardDto { + @NotNull private UUID id; + @NotBlank private String name; private boolean isPrivate; }backend/src/main/java/org/sejongisc/backend/board/domain/Board.java (3)
20-20: Team 엔티티와의 관계 매핑을 고려하세요.현재
teamId가 단순 UUID 필드로 되어있는데, 관련 코드 스니펫에서 Team 엔티티가 존재하는 것을 확인했습니다. JPA의 관계 매핑을 사용하면 객체 지향적인 탐색과 참조 무결성 검증이 가능합니다.관계 매핑 사용을 권장합니다:
- private UUID teamId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "team_id") + private Team team;
28-28: JPA Auditing 활용을 권장합니다.
createdAt필드를 수동으로 관리하는 대신, Spring Data JPA의 Auditing 기능을 사용하면 생성/수정 시간이 자동으로 관리됩니다. 코드베이스에BasePostgresEntity가 이미 존재하는 것으로 보이니 일관성을 위해 확장하거나 Auditing 애노테이션을 사용하세요.다음과 같이 수정하세요:
+ @CreationTimestamp private LocalDateTime createdAt;또는
BasePostgresEntity를 상속:-public class Board { +public class Board extends BasePostgresEntity { @Id @GeneratedValue(strategy = GenerationType.UUID) @Column(name = "board_id", columnDefinition = "BINARY(16)") private UUID id; // ... - - private LocalDateTime createdAt;
10-10: 엔티티에 @Setter 사용을 재고해주세요.엔티티에 @Setter를 사용하면 모든 필드가 외부에서 수정 가능해져 도메인 불변성이 깨질 수 있습니다. 필요한 경우에만 선택적으로 setter를 만들거나, 비즈니스 메서드를 통해 상태를 변경하는 것을 권장합니다.
backend/src/main/java/org/sejongisc/backend/board/domain/Post.java (2)
16-20: Hibernate 특정 애노테이션 사용을 재고하세요.
@GenericGenerator는 Hibernate 특정 기능입니다. Spring Data JPA 3.0+에서는GenerationType.UUID를 직접 지원하므로, 이식성을 위해 표준 방식 사용을 권장합니다.다음과 같이 수정하세요:
@Id - @GeneratedValue(generator = "uuid2") - @GenericGenerator(name = "uuid2", strategy = "uuid2") + @GeneratedValue(strategy = GenerationType.UUID) @Column(name = "post_id", columnDefinition = "BINARY(16)") private UUID id;그리고 import 제거:
-import org.hibernate.annotations.GenericGenerator;
36-40: update 메서드에 입력 검증을 추가하세요.null이나 빈 문자열 검증 없이 필드를 업데이트하면 데이터 무결성 문제가 발생할 수 있습니다.
다음과 같이 개선하세요:
public void update(String title, String content, PostType type) { + if (title == null || title.trim().isEmpty()) { + throw new IllegalArgumentException("제목은 필수입니다"); + } + if (content == null || content.trim().isEmpty()) { + throw new IllegalArgumentException("내용은 필수입니다"); + } + if (type == null) { + throw new IllegalArgumentException("게시물 타입은 필수입니다"); + } this.title = title; this.content = content; this.postType = type; }backend/src/main/java/org/sejongisc/backend/board/dto/CommentCreateRequest.java (1)
12-12: 유효성 검증 애노테이션 추가를 권장합니다.댓글 내용에 대한 검증이 없으면 빈 내용이나 과도하게 긴 내용이 저장될 수 있습니다. Jakarta Validation API를 사용하여 입력을 검증하세요.
다음과 같이 수정하세요:
+import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + @Getter @NoArgsConstructor public class CommentCreateRequest { private UUID postId; + @NotBlank(message = "댓글 내용은 필수입니다") + @Size(max = 5000, message = "댓글은 5000자를 초과할 수 없습니다") private String content; private UUID parentId; // 대댓글이면 존재, 아니면 null }컨트롤러에서
@Valid애노테이션도 추가해야 합니다.backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java (1)
190-199: 댓글 수 계산에서 N+1 쿼리가 발생합니다.
toSummary가 목록의 각 게시글마다commentRepository.findByPostId를 호출하고 있어 게시글 N개에 대해 추가로 N번의 쿼리가 발생합니다. 조회 빈도가 높은 API인 만큼,findByPostId대신 댓글 수를 일괄로 구하거나 게시글 목록 조회 시 함께 조인하여 가져오는 방식으로 개선하는 편이 좋겠습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (27)
backend/src/main/java/org/sejongisc/backend/board/controller/PostController.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/domain/Board.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/domain/Comment.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/domain/Post.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/domain/PostAttachment.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/domain/PostBookmark.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/domain/PostLike.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/domain/PostType.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/dto/BoardDto.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/dto/CommentCreateRequest.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/dto/CommentResponse.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/dto/CommentUpdateRequest.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/dto/PostAttachmentDto.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/dto/PostCreateRequest.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/dto/PostSummaryResponse.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/dto/PostUpdateRequest.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/repository/BoardRepository.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/repository/CommentRepository.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/repository/PostAttachmentRepository.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/repository/PostBookmarkRepository.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/repository/PostLikeRepository.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/repository/PostRepository.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/service/PostService.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java(1 hunks)backend/src/test/java/org/sejongisc/backend/board/controller/PostControllerTest.java(1 hunks)backend/src/test/java/org/sejongisc/backend/board/service/PostServiceImplTest.java(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (18)
backend/src/main/java/org/sejongisc/backend/board/dto/PostCreateRequest.java (4)
backend/src/main/java/org/sejongisc/backend/board/dto/BoardDto.java (1)
Getter(8-14)backend/src/main/java/org/sejongisc/backend/board/dto/PostAttachmentDto.java (1)
Getter(7-14)backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java (1)
Getter(11-24)backend/src/main/java/org/sejongisc/backend/board/dto/PostUpdateRequest.java (1)
Getter(10-17)
backend/src/main/java/org/sejongisc/backend/board/dto/CommentCreateRequest.java (2)
backend/src/main/java/org/sejongisc/backend/board/dto/CommentResponse.java (1)
Getter(9-17)backend/src/main/java/org/sejongisc/backend/board/dto/CommentUpdateRequest.java (1)
Getter(6-10)
backend/src/main/java/org/sejongisc/backend/board/domain/PostLike.java (3)
backend/src/main/java/org/sejongisc/backend/board/domain/Comment.java (1)
Entity(9-36)backend/src/main/java/org/sejongisc/backend/board/domain/PostAttachment.java (1)
Entity(9-36)backend/src/main/java/org/sejongisc/backend/board/domain/PostBookmark.java (1)
Entity(9-30)
backend/src/main/java/org/sejongisc/backend/board/domain/PostType.java (1)
backend/src/main/java/org/sejongisc/backend/betting/entity/MarketType.java (1)
MarketType(3-6)
backend/src/main/java/org/sejongisc/backend/board/dto/BoardDto.java (4)
backend/src/main/java/org/sejongisc/backend/board/dto/CommentResponse.java (1)
Getter(9-17)backend/src/main/java/org/sejongisc/backend/board/dto/PostCreateRequest.java (1)
Getter(11-20)backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java (1)
Getter(11-24)backend/src/main/java/org/sejongisc/backend/board/dto/PostSummaryResponse.java (1)
Getter(9-17)
backend/src/main/java/org/sejongisc/backend/board/domain/Post.java (9)
backend/src/main/java/org/sejongisc/backend/board/domain/Board.java (1)
Entity(8-29)backend/src/main/java/org/sejongisc/backend/board/domain/Comment.java (1)
Entity(9-36)backend/src/main/java/org/sejongisc/backend/board/domain/PostAttachment.java (1)
Entity(9-36)backend/src/main/java/org/sejongisc/backend/board/domain/PostBookmark.java (1)
Entity(9-30)backend/src/main/java/org/sejongisc/backend/board/domain/PostLike.java (1)
Entity(9-30)backend/src/main/java/org/sejongisc/backend/board/dto/PostCreateRequest.java (1)
Getter(11-20)backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java (1)
Getter(11-24)backend/src/main/java/org/sejongisc/backend/board/dto/PostUpdateRequest.java (1)
Getter(10-17)backend/src/main/java/org/sejongisc/backend/common/entity/postgres/BasePostgresEntity.java (1)
ToString(17-35)
backend/src/main/java/org/sejongisc/backend/board/domain/Board.java (3)
backend/src/main/java/org/sejongisc/backend/board/domain/Post.java (1)
Entity(9-41)backend/src/main/java/org/sejongisc/backend/board/dto/BoardDto.java (1)
Getter(8-14)backend/src/main/java/org/sejongisc/backend/user/entity/Team.java (1)
Entity(9-27)
backend/src/main/java/org/sejongisc/backend/board/domain/PostBookmark.java (3)
backend/src/main/java/org/sejongisc/backend/board/domain/Comment.java (1)
Entity(9-36)backend/src/main/java/org/sejongisc/backend/board/domain/PostAttachment.java (1)
Entity(9-36)backend/src/main/java/org/sejongisc/backend/board/domain/PostLike.java (1)
Entity(9-30)
backend/src/main/java/org/sejongisc/backend/board/dto/CommentResponse.java (2)
backend/src/main/java/org/sejongisc/backend/board/dto/CommentCreateRequest.java (1)
Getter(8-14)backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java (1)
Getter(11-24)
backend/src/main/java/org/sejongisc/backend/board/repository/PostLikeRepository.java (4)
backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRepository.java (1)
Repository(17-55)backend/src/main/java/org/sejongisc/backend/betting/repository/UserBetRepository.java (1)
UserBetRepository(11-17)backend/src/main/java/org/sejongisc/backend/template/repository/TemplateRepository.java (1)
Repository(10-13)backend/src/main/java/org/sejongisc/backend/auth/repository/RefreshTokenRepository.java (1)
RefreshTokenRepository(9-14)
backend/src/test/java/org/sejongisc/backend/board/controller/PostControllerTest.java (2)
backend/src/test/java/org/sejongisc/backend/template/controller/TemplateControllerTest.java (2)
Test(142-169)WebMvcTest(46-265)backend/src/test/java/org/sejongisc/backend/backtest/controller/BacktestControllerTest.java (3)
WebMvcTest(43-207)Test(120-141)Test(71-87)
backend/src/main/java/org/sejongisc/backend/board/dto/PostSummaryResponse.java (1)
backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java (1)
Getter(11-24)
backend/src/main/java/org/sejongisc/backend/board/domain/Comment.java (7)
backend/src/main/java/org/sejongisc/backend/board/domain/Post.java (1)
Entity(9-41)backend/src/main/java/org/sejongisc/backend/board/domain/PostAttachment.java (1)
Entity(9-36)backend/src/main/java/org/sejongisc/backend/board/domain/PostBookmark.java (1)
Entity(9-30)backend/src/main/java/org/sejongisc/backend/board/domain/PostLike.java (1)
Entity(9-30)backend/src/main/java/org/sejongisc/backend/board/dto/CommentCreateRequest.java (1)
Getter(8-14)backend/src/main/java/org/sejongisc/backend/board/dto/CommentResponse.java (1)
Getter(9-17)backend/src/main/java/org/sejongisc/backend/board/dto/CommentUpdateRequest.java (1)
Getter(6-10)
backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java (4)
backend/src/main/java/org/sejongisc/backend/board/dto/PostAttachmentDto.java (1)
Getter(7-14)backend/src/main/java/org/sejongisc/backend/board/dto/PostCreateRequest.java (1)
Getter(11-20)backend/src/main/java/org/sejongisc/backend/board/dto/PostSummaryResponse.java (1)
Getter(9-17)backend/src/main/java/org/sejongisc/backend/board/dto/PostUpdateRequest.java (1)
Getter(10-17)
backend/src/main/java/org/sejongisc/backend/board/dto/PostUpdateRequest.java (3)
backend/src/main/java/org/sejongisc/backend/board/dto/PostAttachmentDto.java (1)
Getter(7-14)backend/src/main/java/org/sejongisc/backend/board/dto/PostCreateRequest.java (1)
Getter(11-20)backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java (1)
Getter(11-24)
backend/src/main/java/org/sejongisc/backend/board/dto/CommentUpdateRequest.java (3)
backend/src/main/java/org/sejongisc/backend/board/dto/CommentCreateRequest.java (1)
Getter(8-14)backend/src/main/java/org/sejongisc/backend/board/dto/CommentResponse.java (1)
Getter(9-17)backend/src/main/java/org/sejongisc/backend/board/dto/PostUpdateRequest.java (1)
Getter(10-17)
backend/src/main/java/org/sejongisc/backend/board/domain/PostAttachment.java (3)
backend/src/main/java/org/sejongisc/backend/board/domain/Post.java (1)
Entity(9-41)backend/src/main/java/org/sejongisc/backend/board/domain/PostBookmark.java (1)
Entity(9-30)backend/src/main/java/org/sejongisc/backend/board/dto/PostAttachmentDto.java (1)
Getter(7-14)
backend/src/main/java/org/sejongisc/backend/board/dto/PostAttachmentDto.java (3)
backend/src/main/java/org/sejongisc/backend/board/dto/PostCreateRequest.java (1)
Getter(11-20)backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java (1)
Getter(11-24)backend/src/main/java/org/sejongisc/backend/board/dto/PostUpdateRequest.java (1)
Getter(10-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Backend Gate
🔇 Additional comments (6)
backend/src/main/java/org/sejongisc/backend/board/repository/PostAttachmentRepository.java (1)
9-11: 깔끔한 리포지토리 구현입니다.Spring Data JPA 메서드 네이밍 규칙을 잘 따르고 있으며, PostAttachment 엔티티와의 관계가 명확합니다.
backend/src/main/java/org/sejongisc/backend/board/repository/PostRepository.java (1)
11-12: 리포지토리 메서드가 올바르게 정의되었습니다.Spring Data JPA의 쿼리 메서드 네이밍을 잘 따르고 있으며, 보드별 게시물 조회와 타입별 필터링이 명확합니다.
backend/src/main/java/org/sejongisc/backend/board/domain/PostType.java (1)
3-5: 깔끔한 enum 정의입니다.공지사항과 일반 게시물을 구분하는 명확한 enum으로, PR 목표와 잘 일치합니다.
backend/src/main/java/org/sejongisc/backend/board/repository/BoardRepository.java (1)
8-9: 표준 리포지토리 구현입니다.JpaRepository의 기본 CRUD 메서드만 사용하는 깔끔한 구조입니다.
backend/src/main/java/org/sejongisc/backend/board/domain/Post.java (1)
22-24: 지연 로딩 설정이 적절합니다.Board와의 관계에서
FetchType.LAZY를 사용한 것은 N+1 문제 방지를 위한 좋은 선택입니다.backend/src/main/java/org/sejongisc/backend/board/repository/CommentRepository.java (1)
9-12: 계층형 댓글 구조를 잘 지원합니다.게시물별 댓글 조회와 대댓글 조회를 위한 메서드가 명확하게 정의되어 있습니다.
| @PostMapping("/{postId}/comments") | ||
| public UUID createComment(@PathVariable UUID postId, | ||
| @RequestBody CommentCreateRequest request, | ||
| @RequestParam UUID userId) { | ||
| return postService.createComment(request, userId); | ||
| } |
There was a problem hiding this comment.
경로 변수 postId를 실제로 활용하도록 수정이 필요합니다.
현재 구현은 경로 변수로 받은 postId를 전혀 사용하지 않고 CommentCreateRequest만 그대로 서비스에 전달합니다. 그러나 PostServiceImpl.createComment는 request.getPostId()가 null이면 IllegalArgumentException("postId 가 필요합니다.")를 던집니다. 일반적인 REST 호출(경로에만 postId를 지정하고 본문에는 content만 담는 경우)에서는 무조건 500 계열 예외로 실패하게 됩니다. 반드시 경로 값을 서비스에 전달하도록 변경해야 합니다.
- return postService.createComment(request, userId);
+ return postService.createComment(postId, request, userId);서비스/인터페이스 시그니처도 함께 아래처럼 조정해주세요(별도 파일 수정 필요):
-public interface PostService {
- UUID createComment(CommentCreateRequest request, UUID userId);
+public interface PostService {
+ UUID createComment(UUID postId, CommentCreateRequest request, UUID userId);- public UUID createComment(CommentCreateRequest request, UUID userId) {
- if (request.getPostId() == null) throw new IllegalArgumentException("postId 가 필요합니다.");
- postRepository.findById(request.getPostId()) …
- Comment comment = Comment.builder()
- .postId(request.getPostId())
+ public UUID createComment(UUID postId, CommentCreateRequest request, UUID userId) {
+ if (postId == null) throw new IllegalArgumentException("postId 가 필요합니다.");
+ postRepository.findById(postId) …
+ Comment comment = Comment.builder()
+ .postId(postId)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @PostMapping("/{postId}/comments") | |
| public UUID createComment(@PathVariable UUID postId, | |
| @RequestBody CommentCreateRequest request, | |
| @RequestParam UUID userId) { | |
| return postService.createComment(request, userId); | |
| } | |
| @PostMapping("/{postId}/comments") | |
| public UUID createComment(@PathVariable UUID postId, | |
| @RequestBody CommentCreateRequest request, | |
| @RequestParam UUID userId) { | |
| return postService.createComment(postId, request, userId); | |
| } |
🤖 Prompt for AI Agents
In
backend/src/main/java/org/sejongisc/backend/board/controller/PostController.java
around lines 54 to 59, the method ignores the path variable postId and passes
only the request to the service; update the controller to set the postId from
the @PathVariable onto the CommentCreateRequest (or call a service method
overload that accepts postId) before invoking postService.createComment so the
service receives a non-null postId and avoids IllegalArgumentException; also
update the PostService/PostServiceImpl interface and implementation signatures
as suggested so they accept the postId (or a request already populated with it)
and adjust any callers accordingly.
| @Id | ||
| @GeneratedValue | ||
| private UUID id; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
UUID 생성 전략을 명시해주세요.
@GeneratedValue만 사용하면 데이터베이스에 따라 예상치 못한 동작이 발생할 수 있습니다. UUID 생성 전략을 명시적으로 지정하는 것이 좋습니다.
다음과 같이 수정하세요:
@Id
- @GeneratedValue
+ @GeneratedValue(strategy = GenerationType.UUID)
+ @Column(name = "board_id", columnDefinition = "BINARY(16)")
private UUID id;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Id | |
| @GeneratedValue | |
| private UUID id; | |
| @Id | |
| @GeneratedValue(strategy = GenerationType.UUID) | |
| @Column(name = "board_id", columnDefinition = "BINARY(16)") | |
| private UUID id; |
🤖 Prompt for AI Agents
In backend/src/main/java/org/sejongisc/backend/board/domain/Board.java around
lines 16-18, the UUID field uses @GeneratedValue without a concrete generator
which can behave inconsistently across databases; change the annotations to
specify a UUID generator (e.g., add @GeneratedValue(generator = "UUID") and
declare a @GenericGenerator(name = "UUID", strategy =
"org.hibernate.id.UUIDGenerator") on the entity or field), keep the UUID type,
and ensure necessary imports are added so Hibernate will reliably generate
UUIDs.
| @Entity | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @AllArgsConstructor | ||
| @Builder | ||
| public class Post { | ||
|
|
||
| @Id | ||
| @GeneratedValue(generator = "uuid2") | ||
| @GenericGenerator(name = "uuid2", strategy = "uuid2") | ||
| @Column(name = "post_id", columnDefinition = "BINARY(16)") | ||
| private UUID id; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "board_id") | ||
| private Board board; | ||
|
|
||
| @Column(nullable = false) | ||
| private String title; | ||
|
|
||
| @Column(nullable = false, columnDefinition = "TEXT") | ||
| private String content; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| @Column(nullable = false) | ||
| private PostType postType; | ||
|
|
||
| public void update(String title, String content, PostType type) { | ||
| this.title = title; | ||
| this.content = content; | ||
| this.postType = type; | ||
| } | ||
| } |
There was a problem hiding this comment.
감사(Audit) 필드가 누락되었습니다.
게시물 엔티티에 생성 시간과 수정 시간이 없으면 게시물 관리와 정렬에 문제가 생길 수 있습니다. 코드베이스에 BasePostgresEntity가 있는 것을 확인했는데, 이를 상속하거나 직접 audit 애노테이션을 추가하세요.
방법 1 (권장): BasePostgresEntity 상속
+import org.sejongisc.backend.common.entity.postgres.BasePostgresEntity;
+
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
-public class Post {
+public class Post extends BasePostgresEntity {방법 2: 직접 필드 추가
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
@Entity
+@EntityListeners(AuditingEntityListener.class)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Post {
// ... existing fields ...
+ @CreatedDate
+ @Column(nullable = false, updatable = false)
+ private LocalDateTime createdAt;
+
+ @LastModifiedDate
+ @Column(nullable = false)
+ private LocalDateTime updatedAt;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Entity | |
| @Getter | |
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | |
| @AllArgsConstructor | |
| @Builder | |
| public class Post { | |
| @Id | |
| @GeneratedValue(generator = "uuid2") | |
| @GenericGenerator(name = "uuid2", strategy = "uuid2") | |
| @Column(name = "post_id", columnDefinition = "BINARY(16)") | |
| private UUID id; | |
| @ManyToOne(fetch = FetchType.LAZY) | |
| @JoinColumn(name = "board_id") | |
| private Board board; | |
| @Column(nullable = false) | |
| private String title; | |
| @Column(nullable = false, columnDefinition = "TEXT") | |
| private String content; | |
| @Enumerated(EnumType.STRING) | |
| @Column(nullable = false) | |
| private PostType postType; | |
| public void update(String title, String content, PostType type) { | |
| this.title = title; | |
| this.content = content; | |
| this.postType = type; | |
| } | |
| } | |
| import org.sejongisc.backend.common.entity.postgres.BasePostgresEntity; | |
| @Entity | |
| @Getter | |
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | |
| @AllArgsConstructor | |
| @Builder | |
| public class Post extends BasePostgresEntity { | |
| @Id | |
| @GeneratedValue(generator = "uuid2") | |
| @GenericGenerator(name = "uuid2", strategy = "uuid2") | |
| @Column(name = "post_id", columnDefinition = "BINARY(16)") | |
| private UUID id; | |
| @ManyToOne(fetch = FetchType.LAZY) | |
| @JoinColumn(name = "board_id") | |
| private Board board; | |
| @Column(nullable = false) | |
| private String title; | |
| @Column(nullable = false, columnDefinition = "TEXT") | |
| private String content; | |
| @Enumerated(EnumType.STRING) | |
| @Column(nullable = false) | |
| private PostType postType; | |
| public void update(String title, String content, PostType type) { | |
| this.title = title; | |
| this.content = content; | |
| this.postType = type; | |
| } | |
| } |
🤖 Prompt for AI Agents
In backend/src/main/java/org/sejongisc/backend/board/domain/Post.java around
lines 9 to 41, the Post entity is missing audit fields (created/modified
timestamps); either make Post extend the project's BasePostgresEntity so it
inherits audit columns and remove duplicate id/annotations if BasePostgresEntity
already defines them, or add createdAt and updatedAt fields with appropriate JPA
audit annotations (@CreatedDate, @LastModifiedDate) and ensure the class is
annotated with @EntityListeners(AuditingEntityListener.class) and Spring Data
JPA auditing is enabled; update imports and constructors/builders accordingly
and run tests to confirm there are no duplicate column definitions.
| @Column(nullable = false) | ||
| private UUID postId; | ||
|
|
||
| @Column(nullable = false) | ||
| private UUID userId; | ||
|
|
||
| @CreationTimestamp | ||
| private LocalDateTime createdAt; |
There was a problem hiding this comment.
중복 북마크를 막는 유니크 제약이 필요합니다.
서비스 레이어에서 existsByUserIdAndPostId로 중복을 체크하더라도 동시에 두 요청이 들어오면 동일 userId/postId 조합이 여러 건 저장될 수 있어 북마크 카운트나 해제 로직이 꼬일 위험이 있습니다. 테이블 차원의 유니크 제약으로 데이터 정합성을 보장해 주세요.
다음과 같은 형태로 유니크 제약과 명시적 컬럼명을 추가하는 방안을 제안드립니다.
-@Table(name = "post_bookmark")
+@Table(
+ name = "post_bookmark",
+ uniqueConstraints = @UniqueConstraint(columnNames = {"post_id", "user_id"})
+)
...
- @Column(nullable = false)
+ @Column(name = "post_id", nullable = false)
private UUID postId;
- @Column(nullable = false)
+ @Column(name = "user_id", nullable = false)
private UUID userId;Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In backend/src/main/java/org/sejongisc/backend/board/domain/PostBookmark.java
around lines 22–29, the entity lacks a table-level unique constraint on the
(userId, postId) pair and explicit column names, which allows race conditions to
create duplicate bookmarks; add a @Table annotation with a composite
uniqueConstraints definition for (user_id, post_id) and change the field @Column
annotations to explicit column names (e.g., @Column(name = "post_id", nullable =
false) and @Column(name = "user_id", nullable = false)); also create the
corresponding DB migration (Flyway/Liquibase) to add the unique index/constraint
on the existing table so the database enforces uniqueness.
| @Getter | ||
| @NoArgsConstructor | ||
| public class CommentCreateRequest { | ||
| private UUID postId; |
There was a problem hiding this comment.
🧩 Analysis chain
postId 필드의 중복 가능성을 확인하세요.
댓글 생성 시 postId가 URL 경로 파라미터로 이미 전달될 가능성이 높습니다. 컨트롤러 구현을 확인하여 필드가 중복되지 않는지 검증하세요.
다음 스크립트로 컨트롤러에서 postId 사용을 확인하세요:
🏁 Script executed:
#!/bin/bash
# PostController에서 댓글 생성 엔드포인트 확인
ast-grep --pattern $'createComment($$$@PathVariable$$$ postId$$$, $$$CommentCreateRequest$$$) {
$$$
}'Length of output: 0
🏁 Script executed:
#!/bin/bash
# PostController 찾기
fd -t f "PostController" -e javaLength of output: 165
🏁 Script executed:
#!/bin/bash
# 댓글 생성 관련 컨트롤러 메서드 검색 (유연한 패턴)
rg -n "createComment|create.*comment" --type java -i -A 5 -B 2Length of output: 9042
🏁 Script executed:
#!/bin/bash
# CommentCreateRequest 필드 확인
fd -t f "CommentCreateRequest" -e java | head -1 | xargs catLength of output: 288
@PathVariable postId가 추출되지만 서비스 레이어에 전달되지 않아 무시됩니다.
컨트롤러에서 @PathVariable UUID postId로 URL 경로에서 추출되지만, 서비스 호출 시 전달되지 않습니다. 대신 서비스는 CommentCreateRequest의 postId 필드를 사용하므로, URL의 postId와 요청 본문의 postId가 다를 경우 요청 본문의 값이 우선됩니다.
CommentCreateRequest에서 postId 필드를 제거하고 컨트롤러에서 경로 파라미터를 서비스에 전달하도록 수정해야 합니다:
// PostController.java - line 55-59
return postService.createComment(postId, request, userId);🤖 Prompt for AI Agents
In
backend/src/main/java/org/sejongisc/backend/board/dto/CommentCreateRequest.java
around line 11, the DTO contains a postId field which allows the request body to
override the @PathVariable postId extracted in the controller; remove the postId
field from CommentCreateRequest and update controller/service call sites to pass
the path variable explicitly (e.g., postService.createComment(postId,
requestBody, userId)) so the URL path parameter is the single source of truth.
| public interface PostRepository extends JpaRepository<Post, UUID> { | ||
| List<Post> findByBoardId(UUID boardId); | ||
| List<Post> findByBoardIdAndPostType(UUID boardId, PostType postType); | ||
| List<Post> findByTitleContainingIgnoreCaseOrContentContainingIgnoreCase(String titleKeyword, String contentKeyword); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
검색 메서드의 파라미터 중복을 개선하세요.
동일한 키워드를 두 번 전달해야 하는 메서드 시그니처는 오류를 유발하기 쉽습니다. @Query 애노테이션을 사용하여 단일 파라미터로 제목과 내용을 모두 검색하도록 개선하는 것이 좋습니다.
다음과 같이 수정하세요:
+ @Query("SELECT p FROM Post p WHERE LOWER(p.title) LIKE LOWER(CONCAT('%', :keyword, '%')) OR LOWER(p.content) LIKE LOWER(CONCAT('%', :keyword, '%'))")
+ List<Post> searchByKeyword(@Param("keyword") String keyword);
- List<Post> findByTitleContainingIgnoreCaseOrContentContainingIgnoreCase(String titleKeyword, String contentKeyword);필요한 import 추가:
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;🤖 Prompt for AI Agents
In
backend/src/main/java/org/sejongisc/backend/board/repository/PostRepository.java
around line 13, the repository method requires passing the same keyword twice
which is error-prone; replace the derived query method with a custom @Query that
searches title OR content using a single @Param("keyword") parameter (e.g.,
WHERE LOWER(p.title) LIKE LOWER(CONCAT('%', :keyword, '%')) OR LOWER(p.content)
LIKE LOWER(CONCAT('%', :keyword, '%'))), update the method signature to accept
one String keyword annotated with @Param("keyword"), and add the imports for
org.springframework.data.jpa.repository.Query and
org.springframework.data.repository.query.Param.
| public UUID createComment(CommentCreateRequest request, UUID userId) { | ||
| if (request == null) throw new IllegalArgumentException("요청이 존재하지 않습니다."); | ||
| if (userId == null) throw new IllegalArgumentException("userId 가 필요합니다."); | ||
| if (request.getPostId() == null) throw new IllegalArgumentException("postId 가 필요합니다."); | ||
| if (isBlank(request.getContent())) throw new IllegalArgumentException("댓글 내용이 비어 있습니다."); | ||
|
|
||
| // 게시글 존재 확인 | ||
| postRepository.findById(request.getPostId()) | ||
| .orElseThrow(() -> new NoSuchElementException("게시글을 찾을 수 없습니다. id=" + request.getPostId())); | ||
|
|
||
| Comment comment = Comment.builder() | ||
| .postId(request.getPostId()) | ||
| .userId(userId) | ||
| .content(request.getContent()) | ||
| .parentId(request.getParentId()) | ||
| .build(); | ||
|
|
||
| return commentRepository.save(comment).getId(); | ||
| } |
There was a problem hiding this comment.
postId를 PathVariable 로 전달받도록 시그니처를 조정해주세요.
컨트롤러에서 postId를 경로로 받고 있음에도 이 구현은 여전히 request.getPostId()를 필수로 요구합니다. 결과적으로 본문에 postId를 넣지 않으면 즉시 IllegalArgumentException이 발생합니다. 서비스 시그니처를 createComment(UUID postId, CommentCreateRequest request, UUID userId) 형태로 변경하고, 여기서 받은 postId를 검증·사용하도록 수정해야 합니다. 반드시 컨트롤러/인터페이스와 함께 맞춰 주세요.
- public UUID createComment(CommentCreateRequest request, UUID userId) {
- if (request == null) throw new IllegalArgumentException("요청이 존재하지 않습니다.");
- if (userId == null) throw new IllegalArgumentException("userId 가 필요합니다.");
- if (request.getPostId() == null) throw new IllegalArgumentException("postId 가 필요합니다.");
+ public UUID createComment(UUID postId, CommentCreateRequest request, UUID userId) {
+ if (postId == null) throw new IllegalArgumentException("postId 가 필요합니다.");
+ if (request == null) throw new IllegalArgumentException("요청이 존재하지 않습니다.");
+ if (userId == null) throw new IllegalArgumentException("userId 가 필요합니다.");
if (isBlank(request.getContent())) throw new IllegalArgumentException("댓글 내용이 비어 있습니다.");
- postRepository.findById(request.getPostId())
- .orElseThrow(() -> new NoSuchElementException("게시글을 찾을 수 없습니다. id=" + request.getPostId()));
+ postRepository.findById(postId)
+ .orElseThrow(() -> new NoSuchElementException("게시글을 찾을 수 없습니다. id=" + postId));
Comment comment = Comment.builder()
- .postId(request.getPostId())
+ .postId(postId)
.userId(userId)
.content(request.getContent())
.parentId(request.getParentId())
.build();Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java
around lines 131-149, the method currently demands postId from the request body
which conflicts with the controller passing postId as a PathVariable; change the
service method signature to createComment(UUID postId, CommentCreateRequest
request, UUID userId), remove the request.getPostId() null-check, validate that
the incoming postId param is not null (throw IllegalArgumentException if it is),
use that postId when checking post existence and when building the Comment, and
update the corresponding service interface and controller method signatures to
match this new parameter order.
| @WebMvcTest(PostController.class) | ||
| class PostControllerTest { | ||
|
|
||
| @Autowired private MockMvc mockMvc; | ||
| @Autowired private ObjectMapper objectMapper; | ||
|
|
||
| @MockBean private PostService postService; | ||
|
|
||
| // 슬라이스 테스트 시 JPA 메타모델 경고 방지 (환경에 따라 필요) | ||
| @MockBean JpaMetamodelMappingContext jpaMetamodelMappingContext; | ||
|
|
||
| @Test | ||
| @DisplayName("[POST] /api/posts — 생성 200 & UUID 반환") | ||
| void createPost_ok() throws Exception { | ||
| UUID userId = UUID.randomUUID(); | ||
| UUID postId = UUID.randomUUID(); | ||
|
|
||
| PostCreateRequest req = new PostCreateRequest( | ||
| UUID.randomUUID(), | ||
| "제목", | ||
| "내용", | ||
| PostType.NORMAL, | ||
| List.of(new PostAttachmentDto("a.hwp","application/haansoft-hwp","/a.hwp")) | ||
| ); | ||
|
|
||
| given(postService.createPost(any(PostCreateRequest.class), eq(userId))).willReturn(postId); | ||
|
|
||
| mockMvc.perform(post("/api/posts") | ||
| .param("userId", userId.toString()) | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .content(objectMapper.writeValueAsString(req))) | ||
| .andExpect(status().isOk()) | ||
| .andExpect(content().string(postId.toString())); | ||
|
|
||
| verify(postService).createPost(any(PostCreateRequest.class), eq(userId)); | ||
| } | ||
|
|
There was a problem hiding this comment.
보안 설정 누락으로 모든 요청이 401로 실패합니다.
@WebMvcTest는 기본적으로 Spring Security를 활성화합니다. 그런데 이 테스트 클래스에는 우리 프로젝트의 SecurityConfig나 그 안에서 필요한 JwtParser, AuditorAware<String> 등의 빈이 전혀 로드되지 않으므로, 모든 요청이 인증 없이 실행되어 401 응답만 돌려주게 됩니다. 지금 상태로는 테스트 실행 시 전부 실패합니다. @Import(SecurityConfig.class)로 실제 보안 설정을 포함시키고 필요한 빈을 @MockBean으로 주입하거나, 테스트 목적이라면 @AutoConfigureMockMvc(addFilters = false)/@WithMockUser 등으로 보안 필터를 비활성화해 주세요.
🤖 Prompt for AI Agents
backend/src/test/java/org/sejongisc/backend/board/controller/PostControllerTest.java
lines 25-61: the WebMvcTest is failing with 401 because Spring Security is
active but SecurityConfig and required security beans (e.g., JwtParser,
AuditorAware<String>) are not loaded; fix by either importing your real security
config and mocking required beans (add @Import(SecurityConfig.class) on the test
and add @MockBean for JwtParser, AuditorAware<String>, etc.), or disable
security for this slice test by using @AutoConfigureMockMvc(addFilters = false)
or annotating the test method/class with @WithMockUser so requests are
authenticated during the test.
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (2)
backend/src/main/java/org/sejongisc/backend/board/controller/PostController.java (1)
110-116:postIdPathVariable 미사용으로 댓글 생성 실패
경로에/api/post/{postId}/comment를 두었지만 메소드 파라미터에@PathVariable UUID postId가 없고, 서비스에도 경로 값을 전달하지 않습니다.PostServiceImpl.createComment는request.getPostId()가null이면CustomException(ErrorCode.POST_NOT_FOUND)를 던지므로, 일반적인 REST 호출(본문에 postId 미포함)에서는 항상 예외가 발생합니다. 컨트롤러 시그니처를createComment(@PathVariable UUID postId, …)로 바꾸고, 서비스도postService.createComment(postId, request, userId);형태로 수정해야 합니다. 이와 함께PostService/PostServiceImpl시그니처,Comment빌드 로직도 경로로 받은postId를 사용하도록 맞춰주세요.수정 안내 예시:
- public ResponseEntity<Void> createComment( - @RequestBody CommentRequest request, - @AuthenticationPrincipal CustomUserDetails customUserDetails) { + public ResponseEntity<Void> createComment( + @PathVariable UUID postId, + @RequestBody CommentRequest request, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { UUID userId = customUserDetails.getUserId(); - postService.createComment(request, userId); + postService.createComment(postId, request, userId); return ResponseEntity.ok().build(); }backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java (1)
227-249: 댓글 생성 시 본문 postId 의존으로 500 발생
createComment가request.getPostId()를 필수로 요구해 본문에 postId를 넣지 않으면POST_NOT_FOUND예외가 발생합니다. 컨트롤러가 경로 변수/api/post/{postId}/comment를 제공하므로 서비스 시그니처를createComment(UUID postId, CommentRequest request, UUID userId)로 바꾸고, 아래처럼 경로로 받은 값을 검증·사용하도록 수정해주세요.Comment.builder()에도 동일한 값을 전달하고,CommentRequest에서 postId 필드는 optional 로 취급하거나 제거해야 합니다.수정 예시:
- public void createComment(CommentRequest request, UUID userId) { - Post post = postRepository.findById(request.getPostId()) + public void createComment(UUID postId, CommentRequest request, UUID userId) { + if (postId == null) throw new CustomException(ErrorCode.POST_NOT_FOUND); + Post post = postRepository.findById(postId) .orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND)); User user = userRepository.findById(userId) .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); Comment comment = Comment.builder() - .postId(request.getPostId()) + .postId(postId) .user(user) .content(request.getContent()) .build();
🧹 Nitpick comments (6)
backend/src/main/java/org/sejongisc/backend/board/service/FileUploadService.java (3)
40-70: 커스텀 예외 사용 권장파일 저장 실패 시 범용
RuntimeException대신 커스텀 예외를 사용하는 것이 좋습니다. 이렇게 하면 상위 계층에서 예외를 더 명확하게 처리할 수 있습니다.
ErrorCode에 파일 관련 에러 코드를 추가하고CustomException을 사용하세요:// ErrorCode.java에 추가 FILE_EMPTY(HttpStatus.BAD_REQUEST, "빈 파일은 저장할 수 없습니다."), FILE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "파일 저장에 실패했습니다."), FILE_PATH_INVALID(HttpStatus.BAD_REQUEST, "유효하지 않은 파일 경로입니다.");그리고 store() 메서드에서:
public String store(MultipartFile file) { if (file.isEmpty()) { - throw new RuntimeException("빈 파일은 저장할 수 없습니다."); + throw new CustomException(ErrorCode.FILE_EMPTY); } ... if (!destinationFile.getParent().equals(this.rootLocation)) { - throw new RuntimeException("현재 디렉토리 밖에 저장할 수 없습니다."); + throw new CustomException(ErrorCode.FILE_PATH_INVALID); } ... } catch (IOException e) { - throw new RuntimeException("파일 저장 실패: " + originalFilename, e); + throw new CustomException(ErrorCode.FILE_UPLOAD_FAILED); } }
52-58: 경로 보안 검증 개선 제안현재
getParent().equals()검증은 작동하지만,startsWith()를 사용하면 더 명확하고 안전합니다.다음과 같이 개선할 수 있습니다:
- // 상위 디렉토리로 벗어나려는지 보안 체크 - if (!destinationFile.getParent().equals(this.rootLocation)) { - throw new RuntimeException("현재 디렉토리 밖에 저장할 수 없습니다."); + // 경로 순회 공격 방지 + if (!destinationFile.startsWith(this.rootLocation)) { + throw new CustomException(ErrorCode.FILE_PATH_INVALID); }
76-83: 파일 삭제 예외 처리 개선삭제 메서드에서도 커스텀 예외를 사용하는 것이 좋습니다.
public void delete(String filename) { try { Path file = this.rootLocation.resolve(filename).normalize(); Files.deleteIfExists(file); } catch (IOException e) { - throw new RuntimeException("파일 삭제 실패: " + filename, e); + throw new CustomException(ErrorCode.FILE_DELETE_FAILED); } }
ErrorCode에 추가:FILE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "파일 삭제에 실패했습니다.")backend/src/main/java/org/sejongisc/backend/board/repository/PostAttachmentRepository.java (1)
13-13: 삭제 메서드 성능 개선 권장
deleteAllByPostId는 현재 Spring Data JPA의 네이밍 컨벌션을 따르고 있어 작동하지만, 내부적으로 SELECT 후 DELETE를 수행하여 비효율적입니다.다음 diff를 적용하여 성능을 개선하세요:
+import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.transaction.annotation.Transactional; + public interface PostAttachmentRepository extends JpaRepository<PostAttachment, UUID> { List<PostAttachment> findAllByPostId(UUID postId); + @Modifying + @Transactional + @Query("DELETE FROM PostAttachment pa WHERE pa.postId = :postId") void deleteAllByPostId(UUID postId); }이렇게 하면 한 번의 DELETE 쿼리로 처리되어 성능이 향상됩니다.
backend/src/main/java/org/sejongisc/backend/board/entity/PostAttachment.java (1)
22-32: 첨부파일과 게시글 간 외래키 연관을 명시해주세요
postId만 보관하면 잘못된 게시글 ID로도 레코드가 생성돼 데이터 무결성이 깨질 수 있습니다.@ManyToOne+@JoinColumn(FK) 또는 유니크 제약으로 참조 무결성을 확보해 주세요.- @Column(nullable = false) - private UUID postId; + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "post_id", nullable = false, + foreignKey = @ForeignKey(name = "fk_post_attachment_post")) + private Post post;backend/src/main/java/org/sejongisc/backend/board/dto/CommentRequest.java (1)
21-22: 검증 메시지 일관성을 위해 메시지 추가 권장
postId필드의@NotNull어노테이션에 메시지가 누락되어 있습니다.content필드(Line 24)와 마찬가지로 사용자 친화적인 검증 메시지를 추가하는 것이 좋습니다.다음 diff를 적용하여 일관성을 개선하세요:
- @NotNull + @NotNull(message = "게시글 ID는 필수 항목입니다.") private UUID postId;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (26)
backend/src/main/java/org/sejongisc/backend/board/controller/PostController.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/dto/CommentRequest.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/dto/CommentResponse.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/dto/PostAttachmentResponse.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/dto/PostRequest.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/entity/BoardType.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/entity/Comment.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/entity/Post.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/entity/PostAttachment.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/entity/PostBookmark.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/entity/PostLike.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/entity/PostType.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/repository/CommentRepository.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/repository/PostAttachmentRepository.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/repository/PostBookmarkRepository.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/repository/PostLikeRepository.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/repository/PostRepository.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/service/FileUploadService.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/service/PostService.java(1 hunks)backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java(1 hunks)backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java(1 hunks)backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java(1 hunks)backend/src/main/java/org/sejongisc/backend/user/entity/User.java(2 hunks)backend/src/test/java/org/sejongisc/backend/board/service/FileUploadServiceTest.java(1 hunks)backend/src/test/java/org/sejongisc/backend/board/service/PostServiceImplTest.java(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- backend/src/main/java/org/sejongisc/backend/board/repository/PostRepository.java
- backend/src/main/java/org/sejongisc/backend/board/repository/PostBookmarkRepository.java
🧰 Additional context used
🧬 Code graph analysis (13)
backend/src/main/java/org/sejongisc/backend/board/entity/PostLike.java (2)
backend/src/main/java/org/sejongisc/backend/board/entity/Post.java (1)
Entity(12-57)backend/src/main/java/org/sejongisc/backend/board/entity/PostBookmark.java (1)
Entity(10-27)
backend/src/main/java/org/sejongisc/backend/board/entity/PostAttachment.java (5)
backend/src/main/java/org/sejongisc/backend/board/entity/Comment.java (1)
Entity(11-31)backend/src/main/java/org/sejongisc/backend/board/entity/Post.java (1)
Entity(12-57)backend/src/main/java/org/sejongisc/backend/board/entity/PostBookmark.java (1)
Entity(10-27)backend/src/main/java/org/sejongisc/backend/board/entity/PostLike.java (1)
Entity(10-27)backend/src/main/java/org/sejongisc/backend/board/dto/PostAttachmentResponse.java (1)
Getter(10-26)
backend/src/main/java/org/sejongisc/backend/board/entity/Post.java (5)
backend/src/main/java/org/sejongisc/backend/board/entity/Comment.java (1)
Entity(11-31)backend/src/main/java/org/sejongisc/backend/user/entity/User.java (1)
Entity(13-67)backend/src/main/java/org/sejongisc/backend/board/entity/PostAttachment.java (1)
Entity(10-33)backend/src/main/java/org/sejongisc/backend/board/entity/PostBookmark.java (1)
Entity(10-27)backend/src/main/java/org/sejongisc/backend/board/entity/PostLike.java (1)
Entity(10-27)
backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java (1)
backend/src/main/java/org/sejongisc/backend/board/service/FileUploadService.java (1)
Service(16-92)
backend/src/main/java/org/sejongisc/backend/board/entity/Comment.java (5)
backend/src/main/java/org/sejongisc/backend/board/entity/Post.java (1)
Entity(12-57)backend/src/main/java/org/sejongisc/backend/user/entity/User.java (1)
Entity(13-67)backend/src/main/java/org/sejongisc/backend/board/entity/PostAttachment.java (1)
Entity(10-33)backend/src/main/java/org/sejongisc/backend/board/entity/PostBookmark.java (1)
Entity(10-27)backend/src/main/java/org/sejongisc/backend/board/entity/PostLike.java (1)
Entity(10-27)
backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java (4)
backend/src/main/java/org/sejongisc/backend/board/dto/PostAttachmentResponse.java (1)
Getter(10-26)backend/src/main/java/org/sejongisc/backend/board/dto/PostSummaryResponse.java (1)
Getter(9-17)backend/src/main/java/org/sejongisc/backend/board/dto/PostRequest.java (1)
ToString(16-37)backend/src/main/java/org/sejongisc/backend/board/dto/CommentResponse.java (1)
ToString(14-36)
backend/src/main/java/org/sejongisc/backend/board/dto/PostRequest.java (3)
backend/src/main/java/org/sejongisc/backend/board/dto/PostAttachmentResponse.java (1)
Getter(10-26)backend/src/main/java/org/sejongisc/backend/board/dto/CommentRequest.java (1)
ToString(13-26)backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java (1)
ToString(19-40)
backend/src/main/java/org/sejongisc/backend/board/controller/PostController.java (1)
backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/CustomUserDetailsService.java (1)
RequiredArgsConstructor(16-38)
backend/src/main/java/org/sejongisc/backend/board/entity/PostBookmark.java (4)
backend/src/main/java/org/sejongisc/backend/board/entity/Comment.java (1)
Entity(11-31)backend/src/main/java/org/sejongisc/backend/board/entity/Post.java (1)
Entity(12-57)backend/src/main/java/org/sejongisc/backend/board/entity/PostAttachment.java (1)
Entity(10-33)backend/src/main/java/org/sejongisc/backend/board/entity/PostLike.java (1)
Entity(10-27)
backend/src/main/java/org/sejongisc/backend/board/dto/CommentResponse.java (2)
backend/src/main/java/org/sejongisc/backend/board/dto/CommentRequest.java (1)
ToString(13-26)backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java (1)
ToString(19-40)
backend/src/main/java/org/sejongisc/backend/board/service/FileUploadService.java (1)
backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java (1)
Service(24-368)
backend/src/main/java/org/sejongisc/backend/board/dto/CommentRequest.java (4)
backend/src/main/java/org/sejongisc/backend/board/dto/PostAttachmentResponse.java (1)
Getter(10-26)backend/src/main/java/org/sejongisc/backend/board/dto/PostRequest.java (1)
ToString(16-37)backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java (1)
ToString(19-40)backend/src/main/java/org/sejongisc/backend/board/dto/CommentResponse.java (1)
ToString(14-36)
backend/src/test/java/org/sejongisc/backend/board/service/PostServiceImplTest.java (1)
backend/src/test/java/org/sejongisc/backend/board/service/FileUploadServiceTest.java (1)
ExtendWith(20-98)
🔇 Additional comments (17)
backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java (1)
66-66: 변경사항 승인게시판 엔티티 패키지가 JPA 엔티티 스캔 대상에 올바르게 추가되었습니다. 다른 모듈과 일관된 패턴을 따르고 있습니다.
backend/src/test/java/org/sejongisc/backend/board/service/FileUploadServiceTest.java (2)
35-54: 테스트 커버리지 우수파일 저장 성공 케이스에 대한 테스트가 잘 작성되었습니다. 파일명 형식, 파일 존재 여부, 내용 검증을 모두 포함하고 있습니다.
71-97: 삭제 기능 테스트 잘 작성됨파일 삭제 성공 케이스와 존재하지 않는 파일 삭제 시나리오를 모두 커버하고 있어 좋습니다.
backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java (1)
75-85: 에러 코드 정의 적절게시판 기능을 위한 새로운 에러 코드들이 적절하게 정의되었습니다. HTTP 상태 코드와 메시지가 각 상황에 맞게 설정되어 있습니다.
backend/src/main/java/org/sejongisc/backend/user/entity/User.java (1)
3-3: Hibernate 직렬화 이슈 해결
@JsonIgnoreProperties애노테이션 추가가 적절합니다. Hibernate의 lazy loading 프록시로 인한 JSON 직렬화 문제를 방지하는 표준 패턴입니다.Also applies to: 20-20
backend/src/main/java/org/sejongisc/backend/board/entity/BoardType.java (1)
3-7: 게시판 타입 enum 정의 적절게시판 타입이 명확하게 정의되었습니다. 주석도 이해하기 쉽게 작성되었습니다.
backend/src/main/java/org/sejongisc/backend/board/dto/PostAttachmentResponse.java (1)
19-25: LGTM!정적 팩토리 메서드가 엔티티를 DTO로 적절하게 매핑하고 있으며, 빌더 패턴을 통해 깔끔하게 구현되어 있습니다.
backend/src/main/java/org/sejongisc/backend/board/dto/CommentResponse.java (1)
27-35: LGTM!Comment 엔티티에서 CommentResponse로의 매핑이 올바르게 구현되어 있으며, 모든 필수 필드가 포함되어 있습니다.
backend/src/test/java/org/sejongisc/backend/board/service/PostServiceImplTest.java (4)
66-96: LGTM!파일 첨부를 포함한 게시글 저장 테스트가 잘 구현되어 있습니다. Mock 설정과 검증이 적절합니다.
98-122: LGTM!게시글 수정 시 기존 첨부 파일 삭제 및 신규 파일 저장 로직이 올바르게 테스트되고 있습니다.
137-159: LGTM!게시글 삭제 시 연관된 모든 데이터(첨부파일, 댓글, 좋아요, 북마크)가 함께 삭제되는지 검증하고 있어 데이터 무결성을 보장합니다.
231-261: LGTM!댓글 삭제 권한 검증이 잘 구현되어 있습니다. 작성자와 관리자 모두 테스트하고 있으며, 댓글 수 감소도 함께 검증합니다.
backend/src/main/java/org/sejongisc/backend/board/dto/PostRequest.java (1)
24-36: LGTM!필수 필드에 대한 검증 어노테이션과 메시지가 일관되게 잘 적용되어 있습니다.
files필드가 선택적으로 처리되는 것도 적절합니다.backend/src/main/java/org/sejongisc/backend/board/entity/Comment.java (2)
26-27: LGTM!
User관계에FetchType.LAZY를 사용하여 N+1 문제를 방지하고 있습니다. 좋은 성능 최적화입니다.
23-24: 설계 확인: postId를 UUID로 저장
postId를@ManyToOne관계가 아닌 일반 UUID 컬럼으로 저장하고 있습니다. 이는 순환 참조를 방지하기 위한 의도적인 설계로 보이며, 대부분의 경우 적절한 선택입니다. 다만 Post 엔티티의 필드에 직접 접근이 필요한 경우에는 별도로 조회해야 한다는 점을 인지하세요.backend/src/main/java/org/sejongisc/backend/board/repository/PostLikeRepository.java (1)
13-20: LGTM!Spring Data JPA 명명 규칙을 따르는 적절한 쿼리 메서드들입니다. 특히
deleteAllByPostId에@Transactional어노테이션을 추가한 것이 대량 삭제 작업에 적합합니다.backend/src/main/java/org/sejongisc/backend/board/service/PostService.java (1)
13-44: LGTM!서비스 인터페이스가 명확하고 잘 정의되어 있습니다. 메서드 시그니처가 단일 책임 원칙을 따르고 있으며, 반환 타입도 적절합니다.
|
|
||
| private UUID postId; | ||
| private BoardType boardType; | ||
| private User user; |
There was a problem hiding this comment.
사용자 엔티티 직접 노출로 인한 보안/직렬화 위험 차단 필요
PostResponse가 User 엔티티를 그대로 반환하면 민감 정보가 외부로 노출되고, LAZY 로딩 환경에서 직렬화 시 LazyInitializationException이 빈번히 발생합니다. API 전용 사용자 DTO 또는 최소한의 식별 정보만 노출하도록 구조를 바꿔주세요.
- private User user;
+ private UUID authorId;
+ private String authorNickname;
+ // 필요 시 별도 UserResponse DTO 도입 고려🤖 Prompt for AI Agents
backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java around
line 29: 현재 PostResponse가 User 엔티티를 직접 보유하고 있어 민감정보가 노출되고 LAZY 로딩 시 직렬화 예외가 발생할
수 있으므로 User 엔티티를 제거하고 API 전용 최소 정보 DTO(예: UserSummaryDto 또는 UserIdNameDto)를 필드로
사용하도록 바꿉니다. 구체적으로 PostResponse에서 User 타입 필드를 삭제하고 id, username 등 필요한 식별/표시 필드만
담는 새로운 DTO 클래스를 만들거나 기존 DTO를 사용하며, 엔티티 -> DTO 매핑은 서비스 계층에서 수행해서 필요한 필드만 복사(또는
JPA 프로젝션/쿼리로 선택 조회)하도록 변경하고, 컨트롤러/매퍼 호출부도 이 DTO로 반환되게 수정하세요.
| @Entity | ||
| @Getter | ||
| @Setter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| @Builder | ||
| public class PostBookmark extends BasePostgresEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.UUID) | ||
| private UUID postBookmarkId; | ||
|
|
||
| @Column(nullable = false) | ||
| private UUID postId; | ||
|
|
||
| @Column(nullable = false) | ||
| private UUID userId; |
There was a problem hiding this comment.
중복 북마크를 막기 위한 유니크 제약이 필요합니다
postId와 userId에 대한 유니크 제약이 없어 동시 요청 시 중복 북마크가 생성되고 집계 수치가 틀어질 수 있습니다. DB 레벨 유니크 인덱스로 무결성을 보장해주세요.
-@Entity
+@Entity
+@Table(
+ name = "post_bookmark",
+ uniqueConstraints = @UniqueConstraint(
+ name = "uk_post_bookmark_post_user",
+ columnNames = {"post_id", "user_id"}
+ )
+)
public class PostBookmark extends BasePostgresEntity {🤖 Prompt for AI Agents
In backend/src/main/java/org/sejongisc/backend/board/entity/PostBookmark.java
around lines 10 to 26, there is no DB-level uniqueness constraint on the
combination of postId and userId which allows duplicate bookmarks under
concurrent requests; add a unique constraint at the entity/table level (e.g.,
annotate the entity with a @Table(uniqueConstraints =
@UniqueConstraint(columnNames = {"post_id","user_id"})) or equivalent using the
actual column names) and ensure the column names match the DB columns, and also
add a corresponding schema migration (SQL/ Flyway/Liquibase) to create a unique
index on (post_id, user_id) so the database enforces uniqueness for existing and
new rows.
| import org.hibernate.annotations.CreationTimestamp; | ||
| import java.time.LocalDateTime; |
There was a problem hiding this comment.
사용하지 않는 import 제거 필요
CreationTimestamp와 LocalDateTime import가 코드에서 사용되지 않습니다. BasePostgresEntity가 이미 타임스탬프 필드를 제공하는 것으로 보입니다.
다음 diff를 적용하여 사용하지 않는 import를 제거하세요:
-import org.hibernate.annotations.CreationTimestamp;
-import java.time.LocalDateTime;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import org.hibernate.annotations.CreationTimestamp; | |
| import java.time.LocalDateTime; |
🤖 Prompt for AI Agents
In backend/src/main/java/org/sejongisc/backend/board/entity/PostLike.java around
lines 5-6, remove the unused imports
"org.hibernate.annotations.CreationTimestamp" and "java.time.LocalDateTime"
since they are not referenced (BasePostgresEntity already provides timestamps);
delete those import lines and run a build/static analysis to ensure no remaining
unused imports or compilation issues.
| @Entity | ||
| @Getter | ||
| @Setter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| @Builder | ||
| public class PostLike extends BasePostgresEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.UUID) | ||
| private UUID postLikeId; | ||
|
|
||
| @Column(nullable = false) | ||
| private UUID postId; | ||
|
|
||
| @Column(nullable = false) | ||
| private UUID userId; | ||
| } |
There was a problem hiding this comment.
데이터 무결성: 유니크 제약조건 추가 필요
동일한 사용자가 같은 게시물에 여러 번 좋아요를 등록할 수 있는 문제가 있습니다. (postId, userId) 조합에 대한 유니크 제약조건이 필요합니다.
다음 diff를 적용하여 유니크 제약조건을 추가하세요:
@Entity
+@Table(uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"postId", "userId"})
+})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PostLike extends BasePostgresEntity {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Entity | |
| @Getter | |
| @Setter | |
| @NoArgsConstructor | |
| @AllArgsConstructor | |
| @Builder | |
| public class PostLike extends BasePostgresEntity { | |
| @Id | |
| @GeneratedValue(strategy = GenerationType.UUID) | |
| private UUID postLikeId; | |
| @Column(nullable = false) | |
| private UUID postId; | |
| @Column(nullable = false) | |
| private UUID userId; | |
| } | |
| @Entity | |
| @Table(uniqueConstraints = { | |
| @UniqueConstraint(columnNames = {"postId", "userId"}) | |
| }) | |
| @Getter | |
| @Setter | |
| @NoArgsConstructor | |
| @AllArgsConstructor | |
| @Builder | |
| public class PostLike extends BasePostgresEntity { | |
| @Id | |
| @GeneratedValue(strategy = GenerationType.UUID) | |
| private UUID postLikeId; | |
| @Column(nullable = false) | |
| private UUID postId; | |
| @Column(nullable = false) | |
| private UUID userId; | |
| } |
🤖 Prompt for AI Agents
In backend/src/main/java/org/sejongisc/backend/board/entity/PostLike.java around
lines 10 to 27, the entity lacks a uniqueness constraint on the (postId, userId)
pair which allows duplicate likes; add a class-level @Table annotation with a
UniqueConstraint on columnNames = {"postId","userId"} (e.g.
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"postId",
"userId"}))) so JPA/Ddl generation enforces uniqueness, and also add a
corresponding DB migration (or alter table) to create the unique
index/constraint in the database if schema changes are managed outside JPA.
| // @Value("${file.upload-dir}") | ||
| private final static String UPLOAD_DIRS = "C:/uploads/"; |
There was a problem hiding this comment.
하드코딩된 경로 제거 필요
업로드 디렉토리가 Windows 전용 경로로 하드코딩되어 있어 다른 운영 체제에서 작동하지 않습니다. 프로덕션 환경(Linux)에서 배포 시 문제가 발생할 수 있습니다.
다음 diff를 적용하여 설정 파일에서 경로를 주입받도록 수정하세요:
-// @Value("${file.upload-dir}")
- private final static String UPLOAD_DIRS = "C:/uploads/";
+ @Value("${file.upload-dir:./uploads}")
+ private String uploadDir;
private Path rootLocation;그리고 init() 메서드를 수정하세요:
@PostConstruct
public void init() {
try {
- this.rootLocation = Paths.get(UPLOAD_DIRS).toAbsolutePath().normalize();
+ this.rootLocation = Paths.get(uploadDir).toAbsolutePath().normalize();
Files.createDirectories(this.rootLocation);
} catch (IOException e) {application.properties 또는 application.yml에 다음을 추가하세요:
file.upload-dir=/var/uploads🤖 Prompt for AI Agents
In
backend/src/main/java/org/sejongisc/backend/board/service/FileUploadService.java
around lines 19-20, the upload directory is hardcoded to a Windows path which
must be removed; replace the static constant with a configurable field injected
from application properties (e.g. use @Value("${file.upload-dir}") private
String uploadDir), update any constructor/field declarations accordingly, and
modify the init() method to use the injected uploadDir (create the directory if
it doesn't exist using OS-independent APIs like java.nio.file.Paths/Files and
handle exceptions); also add file.upload-dir=/var/uploads to
application.properties/application.yml as instructed.
Sisc1 151 be 게시물목록조회
Summary by CodeRabbit
새로운 기능
게시물 관리
댓글 기능
사용자 상호작용
게시물 검색 및 첨부
테스트