Skip to content

[feat] 댓글 작성 api 개발#101

Merged
hd0rable merged 38 commits intodevelopfrom
feat/#97-comment-create
Jul 26, 2025
Merged

[feat] 댓글 작성 api 개발#101
hd0rable merged 38 commits intodevelopfrom
feat/#97-comment-create

Conversation

@hd0rable
Copy link
Member

@hd0rable hd0rable commented Jul 23, 2025

#️⃣ 연관된 이슈

closes #97

📝 작업 내용

  • 댓글 작성 api를 개발했습니다.
  • 현재 post 상속 엔티티로 조회 하는 것이 아닌, 후에 record,feed,vote가 개별 테이블로 분리되는것으로 생각하고 개발진행했습니다.
    이와 관련해서 생기는 문제점에 대해 도입한 공통 인터페이스에 대해 문서화 댓글 생성 시 게시물의 댓글 수 증가 처리 해두었으니 해당 문서 보시고 코드 리뷰하기에 더 편할것같습니다!
  • 이전 pr에서 리뷰해주신 내용처럼 도메인 생성시에 도메인 내에서 검증하고, 아예 DB까지 진입을 안해도 되는 케이스는 서비스에서 선행검증하도록 두곳에서 댓글 생성에 대한 검증 진행하였습니다.
  • 댓글 작성 상태 변경의 흐름은 다음과 같습니다.
    Controller에서 Request의 Command 변환뒤 서비스로 요청이들어옴 ->
  1. 댓글 생성시에 필요한 선행 검증(request dto 값 검증, 게시글 타입 변환 및 검증)
  2. 변환된 게시글 타입으로 해당 게시글 조회 (findPost())
  3. 댓글 생성 시, 답글이면 부모 댓글을 함께 조회하여 도메인 규칙 검증
  4. 댓글 도메인 객체를 생성 및 저장
  5. 게시글 도메인의 댓글 수를 증가 및 DB에 반영
  • Vote와 Record에 저희가 도입한 find 전략 함수들 적용하여 작성했습니다.
  • Comment에도 reportCount,likeCount 추가 하였습니다. ERD도 같이 수정진행했습니다.
  • Vote, Record, Feed에 각 PostType를 도입하려 했으나, 아직 상속 구조인 엔티티를 쉽게 건드리지 못할 것같아서 @DiscriminatorColumn(name = "dtype") 으로 자동생성되는 칼럼과 중복 될 것같아 수정하지않았습니다.
  • 관련 통합, 컨트롤러 단위, 도메인 단위 테스트도 진행했습니다.

📸 스크린샷

image

💬 리뷰 요구사항

Feed의 update시에 관련된 tag,content를 무조건 제거하고 다시 덮어씌우는식으로하고있는데 나중에 변경감지하여 변경된 필드만 바꾸는 전략을 사용해봐도 좋을 것같습니다. (아직 잘 찾아보진 않았지만) 개발 속도를 높이기 위해서 해당 사항은 배제하고 개발했습니다 ㅠ 후에 리펙 진행하겠습니다.

📌 PR 진행 시 이러한 점들을 참고해 주세요

* P1 : 꼭 반영해 주세요 (Request Changes) - 이슈가 발생하거나 취약점이 발견되는 케이스 등
* P2 : 반영을 적극적으로 고려해 주시면 좋을 것 같아요 (Comment)
* P3 : 이런 방법도 있을 것 같아요~ 등의 사소한 의견입니다 (Chore)

Summary by CodeRabbit

  • 신규 기능

    • 게시글(피드, 기록, 투표) 및 댓글에 댓글/답글 작성 기능이 추가되었습니다.
    • 댓글 작성 시 댓글/답글 여부 및 부모 댓글 지정이 가능합니다.
    • 댓글 좋아요 수 및 게시글 타입(postType) 정보가 댓글에 추가되어 관리됩니다.
    • 게시글(피드, 기록, 투표) 도메인에 댓글 수 증가 기능이 도입되었습니다.
    • 댓글 생성 및 저장을 위한 새로운 서비스, 인터페이스, REST 엔드포인트가 도입되었습니다.
    • 게시글 타입을 명확히 구분하는 PostType enum과 댓글 수 증가 인터페이스가 추가되었습니다.
    • 댓글 작성 권한 및 방 참여자 검증 기능이 추가되었습니다.
    • 방 참여자 검증 서비스가 도입되어 댓글 작성 권한 검증이 강화되었습니다.
    • 게시글(피드, 기록, 투표) 업데이트 기능에 댓글 수 반영 로직이 추가되었습니다.
  • 버그 수정

    • 댓글 생성 요청 시 빈 내용, 잘못된 postType, 부모 댓글 불일치 등 다양한 입력 검증과 상세 오류 메시지가 개선되었습니다.
    • 댓글 작성 시 부모 댓글 존재 여부 및 게시글 타입 불일치에 대한 검증이 강화되었습니다.
    • 피드 접근 권한 오류 메시지가 수정되었습니다.
  • 테스트

    • 댓글 생성 API에 대한 단위 및 통합 테스트가 추가되어 다양한 정상 및 예외 시나리오가 검증되었습니다.
    • 댓글 도메인 생성 로직에 대한 단위 테스트가 추가되었습니다.
  • 기타

    • 댓글, 게시글, 투표, 기록 도메인에 댓글 수 증가 기능이 도입되었습니다.
    • 불필요한 더미 클래스와 인터페이스가 제거되었습니다.
    • 테스트용 엔티티 생성 메서드명이 일부 변경되었습니다.

hd0rable added 27 commits July 24, 2025 01:44
@hd0rable hd0rable requested review from buzz0331 and seongjunnoh July 23, 2025 17:21
@hd0rable hd0rable self-assigned this Jul 23, 2025
buzz0331
buzz0331 previously approved these changes Jul 26, 2025
Copy link
Contributor

@buzz0331 buzz0331 left a comment

Choose a reason for hiding this comment

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

코드 잘 봤습니다!! 전체적으로 코드가 가독성이 좋아서 리뷰하기 수월했던 것 같습니다~ 👍🏻 👍🏻

Comment on lines +58 to +64
private CommentCountUpdatable findPost(PostType type, Long postId) {
return switch (type) {
case FEED -> feedCommandPort.getByIdOrThrow(postId);
case RECORD -> recordCommandPort.getByIdOrThrow(postId);
case VOTE -> voteCommandPort.getByIdOrThrow(postId);
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

공통 인터페이스를 사용하니 확실히 코드가 간단해지네여~

Comment on lines +34 to +47
public static Comment createComment(String content, Long postId, Long creatorId, String type,
boolean isReplyRequest, Long parentId, Comment parent) {

// 댓글/답글 생성 검증
validateCommentCreate(isReplyRequest,parentId);
PostType postType = PostType.from(type);

if (isReplyRequest) {
// 답글 생성 검증
validateReplyCommentCreate(postId, parent);
return withoutIdReplyComment(content, postId, creatorId, parent, postType);
}
return withoutIdRootComment(content, postId, creatorId, postType);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

내부에서 팩토리 메서드를 통해 잘 구현하신 것 같아요!! LGTM!!

Comment on lines +94 to +111
@Test
@DisplayName("각 게시물 타입별로 존재하는 게시물에 대해 (루트)댓글 생성을 할 수 있다.")
void createRootCommentEachPostType() throws Exception {

// given
String[] postTypes = {"feed", "record", "vote"};
Long[] postIds = {feed.getPostId(), record.getPostId(), vote.getPostId()};

// when & then
for (int i = 0; i < postTypes.length; i++) {
mockMvc.perform(post("/comments/{postId}", postIds[i])
.contentType(MediaType.APPLICATION_JSON)
.content(toJson("루트 댓글입니다", false, null, postTypes[i]))
.requestAttr("userId", user.getUserId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.commentId").exists());
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

p3: postTypes에 들어가는 상수들은 운영 코드에서 PostType으로 이미 정의되어 있는 상태이니 이를 활용해보는건 어떨까요?

Copy link
Contributor

@buzz0331 buzz0331 left a comment

Choose a reason for hiding this comment

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

뒤에 PR 리뷰하다가 갑자기 생각나서 추가적으로 리뷰 하나 남겼습니다!

@hd0rable hd0rable dismissed stale reviews from buzz0331 and seongjunnoh via f438ba6 July 26, 2025 09:12
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/main/java/konkuk/thip/room/domain/service/RoomParticipantService.java (2)

16-16: 메서드 파라미터 순서 검토 제안

메서드명이 "사용자가 방 멤버인지 검증"을 의미하므로 validateUserIsRoomMember(Long userId, Long roomId)로 순서를 변경하는 것이 더 직관적일 수 있습니다. 또는 내부 포트 호출 순서와 일치시키기 위해 현재 순서를 유지하는 것도 좋습니다.

-    public void validateUserIsRoomMember(Long roomId, Long userId) {
-        if (!participantPort.existByUserIdAndRoomId(roomId, userId)) {
+    public void validateUserIsRoomMember(Long userId, Long roomId) {
+        if (!participantPort.existByUserIdAndRoomId(roomId, userId)) {

17-20: 예외 처리 구조 개선 제안

현재 IllegalArgumentException을 cause로 사용하고 있는데, 이는 입력 파라미터 문제가 아니라 비즈니스 규칙 위반이므로 적절하지 않을 수 있습니다. 또한 사용자 ID를 예외 메시지에 직접 노출하는 것은 보안상 검토가 필요합니다.

-            throw new InvalidStateException(ROOM_ACCESS_FORBIDDEN,
-                new IllegalArgumentException("사용자가 이 방의 참가자가 아닙니다. roomId=" + roomId + ", userId=" + userId));
+            throw new InvalidStateException(ROOM_ACCESS_FORBIDDEN,
+                "사용자가 이 방의 참가자가 아닙니다. roomId=" + roomId);

또는 로깅으로 디버그 정보를 남기고 예외 메시지는 간소화하는 방법도 고려해보세요.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e77647 and f438ba6.

📒 Files selected for processing (13)
  • src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java (1 hunks)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (3 hunks)
  • src/main/java/konkuk/thip/common/post/CommentCountUpdatable.java (1 hunks)
  • src/main/java/konkuk/thip/feed/domain/Feed.java (4 hunks)
  • src/main/java/konkuk/thip/room/domain/service/RoomParticipantService.java (1 hunks)
  • src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java (1 hunks)
  • src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateAPITest.java (1 hunks)
  • src/test/java/konkuk/thip/common/util/TestEntityFactory.java (4 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java (1 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomCloseJoinApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java (3 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java (2 hunks)
✅ Files skipped from review due to trivial changes (4)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomCloseJoinApiTest.java
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java
  • src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateAPITest.java
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/main/java/konkuk/thip/feed/domain/Feed.java
  • src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java
  • src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java
  • src/test/java/konkuk/thip/common/util/TestEntityFactory.java
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#101
File: src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java:118-265
Timestamp: 2025-07-23T17:41:55.507Z
Learning: CommentControllerTest는 댓글 생성 API의 검증 로직과 예외 상황만을 테스트하는 단위 테스트이며, 성공 케이스는 별도의 통합 테스트(CommentCreateAPITest)에서 다룬다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#101
File: src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:36-39
Timestamp: 2025-07-26T06:09:00.850Z
Learning: THIP 프로젝트에서 Record와 Vote는 Room에 속하지만 Feed는 Room에 속하지 않는 구조이며, 댓글 작성 시 Record/Vote에 대해서만 사용자가 해당 Room의 참가자인지 검증이 필요하다.
src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java (1)

Learnt from: seongjunnoh
PR: #101
File: src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:36-39
Timestamp: 2025-07-26T06:09:00.850Z
Learning: THIP 프로젝트에서는 "사용자가 방에 속하는지 검증" 로직을 RoomParticipantPolicy 도메인 서비스로 캡슐화하여 재사용성을 높이고 비즈니스 로직의 중복을 방지하는 방식을 선호한다.

src/main/java/konkuk/thip/common/post/CommentCountUpdatable.java (2)

Learnt from: seongjunnoh
PR: #101
File: src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:36-39
Timestamp: 2025-07-26T06:09:00.850Z
Learning: THIP 프로젝트에서 Record와 Vote는 Room에 속하지만 Feed는 Room에 속하지 않는 구조이며, 댓글 작성 시 Record/Vote에 대해서만 사용자가 해당 Room의 참가자인지 검증이 필요하다.

Learnt from: buzz0331
PR: #75
File: src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteQueryRepositoryImpl.java:50-83
Timestamp: 2025-07-14T14:19:38.796Z
Learning: Vote와 VoteItem 엔티티는 자주 함께 사용되므로, N+1 문제를 방지하기 위해 양방향 매핑과 fetch join을 고려하는 것이 좋습니다. 특히 기록장 조회 API 등에서도 함께 사용될 가능성이 높습니다.

src/main/java/konkuk/thip/room/domain/service/RoomParticipantService.java (1)

Learnt from: seongjunnoh
PR: #101
File: src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:36-39
Timestamp: 2025-07-26T06:09:00.850Z
Learning: THIP 프로젝트에서는 "사용자가 방에 속하는지 검증" 로직을 RoomParticipantPolicy 도메인 서비스로 캡슐화하여 재사용성을 높이고 비즈니스 로직의 중복을 방지하는 방식을 선호한다.

🧬 Code Graph Analysis (1)
src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (28-240)
🔇 Additional comments (7)
src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java (1)

121-122: 에러 코드 변경이 적절하게 반영되었습니다.

FEED_UPDATE_FORBIDDEN에서 FEED_ACCESS_FORBIDDEN으로의 에러 코드 변경과 메시지 업데이트가 올바르게 테스트에 반영되었습니다. 더 일반적인 접근 권한 에러 코드를 사용함으로써 댓글 작성 등 다른 기능에서도 재사용 가능한 구조로 개선된 것으로 보입니다.

src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java (3)

66-66: 팩토리 메서드 이름 수정이 올바르게 적용되었습니다.

TestEntityFactory.createRoomParticipant 메서드 호출로 수정되어 일관성이 향상되었습니다.


76-77: 참여자 생성 로직이 일관되게 수정되었습니다.

방장과 일반 참여자 모두에 대해 올바른 팩토리 메서드를 사용하여 역할별로 적절히 구분되어 있습니다.


59-78: 테스트 설정 메서드들의 일관성이 개선되었습니다.

다른 방 관련 테스트 클래스들과 동일한 팩토리 메서드를 사용하도록 수정되어 코드베이스 전반의 일관성이 향상되었습니다. 기존 테스트 로직은 그대로 유지되면서 메서드 이름만 올바르게 수정되었습니다.

src/main/java/konkuk/thip/room/domain/service/RoomParticipantService.java (1)

10-13: 도메인 서비스 구조가 잘 설계되었습니다.

Spring Component로 적절히 등록되고 생성자 주입을 사용하여 의존성을 관리하고 있습니다. 방 참가자 검증 로직을 도메인 서비스로 캡슐화하여 재사용성을 높인 설계가 좋습니다.

src/main/java/konkuk/thip/common/post/CommentCountUpdatable.java (2)

5-5: getRoomId() 호출 경로 및 FEED 처리 검증 완료

  • CommentCreateService에서 post.getRoomId()는 FEED가 아닐 때(PostType != FEED)에만 호출됩니다.
  • FEED인 경우에는 feed.validateCreateComment(userId)로 별도 검증 로직이 수행되므로, FEED의 getRoomId()null을 반환해도 호출되지 않아 문제가 없습니다.

3-6: CommentCountUpdatable 인터페이스 설계 적합 – getRoomId()는 RECORD/VOTE에만 사용됩니다.

createComment 흐름을 확인한 결과,

  • roomParticipantService.validateUserIsRoomMember(post.getRoomId(), userId) 호출은 오직 PostType.RECORD 혹은 VOTE일 때만 발생
  • PostType.FEED인 경우에는 getRoomId()가 아닌 Feed.validateCreateComment(userId)로 처리

따라서 FEED 구현체에 getRoomId()가 존재하더라도 실제 코드 흐름에서 호출되지 않아 문제되지 않습니다.

@@ -0,0 +1,22 @@
package konkuk.thip.room.domain.service;
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM!! 패키지 위치 좋네여

Comment on lines +164 to +168
@Override
//Feed는 RoomId 없음
public Long getRoomId() {
return null;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM

Comment on lines +71 to +81
private void validateCommentCreateAuthorization(PostType type, CommentCountUpdatable post, Long userId) {
// 2-1. RECORD, VOTE는 방 멤버 자격 검증 필요
if (type == PostType.RECORD || type == PostType.VOTE) {
roomParticipantService.validateUserIsRoomMember(post.getRoomId(), userId);
}
// 2-2. FEED는 비공개 글 일시, 작성자 자격 검증 필요
else {
Feed feed = (Feed) post;
feed.validateCreateComment(userId);
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM

@ActiveProfiles("test")
@AutoConfigureMockMvc(addFilters = false)
@Transactional
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
Copy link
Collaborator

@seongjunnoh seongjunnoh Jul 26, 2025

Choose a reason for hiding this comment

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

p3 : 저희 각 테스트마다 @AfterEach 로 테스트 DB deleteAllInBatch 하는데도 각 테스트끼리 영향을 받나요??

어떤 이슈가 있어서 해당 메서드 추가하셨는지 궁금합니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

아 이게 @transactional을 사용한 테스트코드에서는 @AfterEach없이 영향을 받아서 전체테스트에서는 실패하길래 독립성 보장을위해 추가했습니닷

Copy link
Collaborator

@seongjunnoh seongjunnoh Jul 26, 2025

Choose a reason for hiding this comment

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

앗 테스트 클래스에 트랜잭션을 사용하셨군요!! 뭐 상관없습니다ㅎ

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[THIP2025-142] [Feat] 댓글 작성 api 개발

3 participants