Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ ResponseEntity<ApiResponse<MeetingRetrospectiveResponse>> getMeetingRetrospectiv
);

@Operation(
summary = "토픽별 코멘트 조회 (developer: 오주현)",
summary = "코멘트 조회 (developer: 오주현)",
description = """
특정 토픽의 코멘트를 조회합니다.
약속 회고의 코멘트를 조회합니다.
- 커서 기반 무한스크롤을 지원합니다.
- 첫 페이지: cursorCreatedAt, cursorCommentId 없이 호출
- 다음 페이지: 응답의 nextCursor 값을 파라미터로 전달
Expand All @@ -133,11 +133,11 @@ ResponseEntity<ApiResponse<MeetingRetrospectiveResponse>> getMeetingRetrospectiv
examples = @ExampleObject(value = """
{
"code": "SUCCESS",
"message": "토픽 코멘트 조회 성공",
"message": "코멘트 조회 성공",
"data": {
"items": [
{
"meetingRetrospectiveId": 1,
"commentId": 1,
"userId": 1,
"nickname": "사용자1",
"profileImageUrl": "https://example.com/profile.jpg",
Expand Down Expand Up @@ -179,21 +179,14 @@ ResponseEntity<ApiResponse<MeetingRetrospectiveResponse>> getMeetingRetrospectiv
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "404",
description = "약속 또는 토픽을 찾을 수 없음",
description = "약속을 찾을 수 없음",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
examples = {
@ExampleObject(
name = "약속 없음",
value = """
{"code": "M001", "message": "약속을 찾을 수 없습니다.", "data": null}
"""
),
@ExampleObject(
name = "토픽 없음",
value = """
{"code": "T001", "message": "토픽을 찾을 수 없습니다.", "data": null}
"""
)
}
)
Expand All @@ -214,9 +207,6 @@ ResponseEntity<ApiResponse<CursorResponse<MeetingRetrospectiveResponse.CommentRe
@Parameter(description = "약속 ID", required = true, example = "1")
@PathVariable Long meetingId,

@Parameter(description = "토픽 ID", required = true, example = "1")
@RequestParam Long topicId,

@Parameter(description = "페이지 크기", example = "10")
@RequestParam(defaultValue = "10") int pageSize,

Expand Down Expand Up @@ -248,7 +238,7 @@ ResponseEntity<ApiResponse<CursorResponse<MeetingRetrospectiveResponse.CommentRe
"code": "CREATED",
"message": "공동 회고 작성 완료",
"data": {
"meetingRetrospectiveId": 1,
"commentId": 1,
"userId": 1,
"nickname": "독서왕",
"profileImageUrl": "https://example.com/profile.jpg",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,15 @@ public ResponseEntity<ApiResponse<MeetingRetrospectiveResponse>> getMeetingRetro
@GetMapping("/comments")
public ResponseEntity<ApiResponse<CursorResponse<MeetingRetrospectiveResponse.CommentResponse, CommentCursor>>> getTopicComments(
@PathVariable Long meetingId,
@RequestParam Long topicId,
@RequestParam(defaultValue = "10") int pageSize,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime cursorCreatedAt,
@RequestParam(required = false) Long cursorCommentId
) {
CursorResponse<MeetingRetrospectiveResponse.CommentResponse, CommentCursor> response =
meetingRetrospectiveService.getTopicComments(
meetingId, topicId, pageSize, cursorCreatedAt, cursorCommentId
meetingRetrospectiveService.getComments(
meetingId, pageSize, cursorCreatedAt, cursorCommentId
);
return ApiResponse.success(response, "토픽 코멘트 조회 성공");
return ApiResponse.success(response, "코멘트 조회 성공");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@
@Schema(description = "모임 회고 작성 요청")
@Builder
public record MeetingRetrospectiveRequest(

@Schema(description = "주제 ID", example = "1", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull
Long topicId,

@Schema(description = "회고 코멘트 (공백 포함 500자 이내)", example = "이번 모임에서 핵심 논의가 잘 정리되었습니다.", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank
@Size(max = 500, message = "회고 코멘트는 500자 이내로 작성해주세요.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ public static KeyPointResponse from(TopicRetrospectiveSummary.KeyPoint keyPoint)
@Schema(description = "모임 회고 코멘트")
@Builder
public record CommentResponse(
@Schema(description = "모임 회고 ID", example = "1")
Long meetingRetrospectiveId,
@Schema(description = "코멘트 ID", example = "1")
Long commentId,
@Schema(description = "작성자 사용자 ID", example = "1")
Long userId,
@Schema(description = "닉네임", example = "독서왕")
Expand All @@ -112,7 +112,7 @@ public record CommentResponse(

public static CommentResponse from(MeetingRetrospective retrospective, String presignedProfileImageUrl) {
return CommentResponse.builder()
.meetingRetrospectiveId(retrospective.getId())
.commentId(retrospective.getId())
.userId(retrospective.getCreatedBy().getId())
.nickname(retrospective.getCreatedBy().getNickname())
.profileImageUrl(presignedProfileImageUrl)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,14 @@ public class MeetingRetrospective extends BaseTimeEntity {
@JoinColumn(name = "created_by", nullable = false)
private User createdBy;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "topic_id")
private Topic topic;

@Column(name = "comment", columnDefinition = "TEXT")
private String comment;

public static MeetingRetrospective of(Meeting meeting, User user, Topic topic, String comment){
public static MeetingRetrospective of(Meeting meeting, User user, String comment){
return MeetingRetrospective.builder()
.meeting(meeting)
.createdBy(user)
.topic(topic)
.comment(comment).
build();
.comment(comment)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,26 @@ public interface RetrospectiveRepository extends JpaRepository<MeetingRetrospect

@Query("SELECT mr FROM MeetingRetrospective mr " +
"JOIN FETCH mr.createdBy " +
"WHERE mr.topic.id = :topicId " +
"WHERE mr.meeting.id = :meetingId " +
"ORDER BY mr.createdAt DESC, mr.id DESC")
List<MeetingRetrospective> findByTopicIdFirstPage(
@Param("topicId") Long topicId,
@Param("meetingId") Long meetingId,
Pageable pageable
);

@Query("SELECT mr FROM MeetingRetrospective mr " +
"JOIN FETCH mr.createdBy " +
"WHERE mr.topic.id = :topicId " +
"WHERE mr.meeting.id = :meetingId " +
"AND (mr.createdAt < :cursorCreatedAt " +
" OR (mr.createdAt = :cursorCreatedAt AND mr.id < :cursorCommentId)) " +
"ORDER BY mr.createdAt DESC, mr.id DESC")
List<MeetingRetrospective> findByTopicIdAfterCursor(
@Param("topicId") Long topicId,
@Param("meetingId") Long meetingId,
@Param("cursorCreatedAt") LocalDateTime cursorCreatedAt,
@Param("cursorCommentId") Long cursorCommentId,
Pageable pageable
);

@Query("SELECT COUNT(mr) FROM MeetingRetrospective mr WHERE mr.topic.id = :topicId")
int countByTopicId(@Param("topicId") Long topicId);
@Query("SELECT COUNT(mr) FROM MeetingRetrospective mr WHERE mr.meeting.id = :meetingId")
int countByTopicId(@Param("meetingId") Long meetingId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,8 @@ public MeetingRetrospectiveResponse getMeetingRetrospective(Long meetingId){
/**
* 토픽별 코멘트 조회 (무한 스크롤)
*/
public CursorResponse<MeetingRetrospectiveResponse.CommentResponse, CommentCursor> getTopicComments(
public CursorResponse<MeetingRetrospectiveResponse.CommentResponse, CommentCursor> getComments(
Long meetingId,
Long topicId,
int pageSize,
LocalDateTime cursorCreatedAt,
Long cursorCommentId
Expand All @@ -90,9 +89,7 @@ public CursorResponse<MeetingRetrospectiveResponse.CommentResponse, CommentCurso

retrospectiveValidator.validateMeetingRetrospectiveAccess(meeting.getGathering().getId(), meetingId, userId);

topicValidator.getTopicInMeeting(topicId, meetingId);

return fetchComments(topicId, pageSize, cursorCreatedAt, cursorCommentId);
return fetchComments(meetingId, pageSize, cursorCreatedAt, cursorCommentId);
}

@Transactional
Expand All @@ -110,11 +107,8 @@ public MeetingRetrospectiveResponse.CommentResponse createMeetingRetrospective(
// 권한 검증
retrospectiveValidator.validateMeetingRetrospectiveAccess(meeting.getGathering().getId(),meetingId,userId);

// Topic 조회
Topic topic = topicValidator.getTopicInMeeting(request.topicId(), meetingId);

// save
MeetingRetrospective retrospective = MeetingRetrospective.of(meeting, user, topic, request.comment());
MeetingRetrospective retrospective = MeetingRetrospective.of(meeting, user, request.comment());
MeetingRetrospective saved = retrospectiveRepository.save(retrospective);

return buildCommentResponse(saved);
Expand Down Expand Up @@ -148,7 +142,7 @@ private Map<Long, TopicRetrospectiveSummary> buildSummaryMap(List<Topic> topics)
}

private CursorResponse<MeetingRetrospectiveResponse.CommentResponse, CommentCursor> fetchComments(
Long topicId,
Long meetingId,
int pageSize,
LocalDateTime cursorCreatedAt,
Long cursorCommentId
Expand All @@ -157,8 +151,8 @@ private CursorResponse<MeetingRetrospectiveResponse.CommentResponse, CommentCurs
boolean isFirstPage = cursorCreatedAt == null || cursorCommentId == null;

List<MeetingRetrospective> comments = isFirstPage
? retrospectiveRepository.findByTopicIdFirstPage(topicId, pageable)
: retrospectiveRepository.findByTopicIdAfterCursor(topicId, cursorCreatedAt, cursorCommentId, pageable);
? retrospectiveRepository.findByTopicIdFirstPage(meetingId, pageable)
: retrospectiveRepository.findByTopicIdAfterCursor(meetingId, cursorCreatedAt, cursorCommentId, pageable);

boolean hasNext = comments.size() > pageSize;
List<MeetingRetrospective> pageComments = hasNext
Expand All @@ -170,7 +164,7 @@ private CursorResponse<MeetingRetrospectiveResponse.CommentResponse, CommentCurs
.toList();

CommentCursor nextCursor = buildNextCursor(pageComments, hasNext);
Integer totalCount = isFirstPage ? retrospectiveRepository.countByTopicId(topicId) : null;
Integer totalCount = isFirstPage ? retrospectiveRepository.countByTopicId(meetingId) : null;

return CursorResponse.of(items, pageSize, hasNext, nextCursor, totalCount);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import com.dokdok.topic.entity.TopicStatus;
import com.dokdok.topic.repository.TopicAnswerRepository;
import com.dokdok.topic.repository.TopicRepository;
import com.dokdok.topic.service.TopicValidator;
import com.dokdok.user.entity.User;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -72,9 +71,6 @@ class MeetingRetrospectiveServiceTest {
@Mock
private MeetingValidator meetingValidator;

@Mock
private TopicValidator topicValidator;

@Test
@DisplayName("약속이 없으면 예외가 발생한다")
void getMeetingRetrospective_throwsWhenMeetingNotFound() {
Expand Down Expand Up @@ -190,21 +186,18 @@ void createMeetingRetrospective_success() {
// given
Long meetingId = 1L;
Long userId = 1L;
Long topicId = 1L;
Long gatheringId = 1L;

Gathering gathering = Gathering.builder().id(gatheringId).build();
Meeting meeting = Meeting.builder().id(meetingId).gathering(gathering).build();
User user = User.builder().id(userId).nickname("사용자1").profileImageUrl("https://image.jpg").build();
Topic topic = Topic.builder().id(topicId).meeting(meeting).title("토픽1").build();

MeetingRetrospectiveRequest request = new MeetingRetrospectiveRequest(topicId, "회고 코멘트입니다.");
MeetingRetrospectiveRequest request = new MeetingRetrospectiveRequest("회고 코멘트입니다.");

MeetingRetrospective saved = MeetingRetrospective.builder()
.id(1L)
.meeting(meeting)
.createdBy(user)
.topic(topic)
.comment("회고 코멘트입니다.")
.build();

Expand All @@ -217,7 +210,6 @@ void createMeetingRetrospective_success() {

when(meetingValidator.findMeetingOrThrow(meetingId)).thenReturn(meeting);
doNothing().when(retrospectiveValidator).validateMeetingRetrospectiveAccess(gatheringId, meetingId, userId);
when(topicValidator.getTopicInMeeting(topicId, meetingId)).thenReturn(topic);
when(retrospectiveRepository.save(any(MeetingRetrospective.class))).thenReturn(saved);
when(storageService.getPresignedProfileImage("https://image.jpg")).thenReturn("https://image.jpg");

Expand All @@ -226,13 +218,12 @@ void createMeetingRetrospective_success() {
meetingRetrospectiveService.createMeetingRetrospective(meetingId, request);

// then
assertThat(response.meetingRetrospectiveId()).isEqualTo(1L);
assertThat(response.commentId()).isEqualTo(1L);
assertThat(response.userId()).isEqualTo(userId);
assertThat(response.comment()).isEqualTo("회고 코멘트입니다.");

verify(meetingValidator).findMeetingOrThrow(meetingId);
verify(retrospectiveValidator).validateMeetingRetrospectiveAccess(gatheringId, meetingId, userId);
verify(topicValidator).getTopicInMeeting(topicId, meetingId);
verify(retrospectiveRepository).save(any(MeetingRetrospective.class));
}
}
Expand All @@ -243,7 +234,7 @@ void createMeetingRetrospective_throwsWhenMeetingNotFound() {
Long meetingId = 999L;
Long userId = 1L;
User user = User.builder().id(userId).build();
MeetingRetrospectiveRequest request = new MeetingRetrospectiveRequest(1L, "코멘트");
MeetingRetrospectiveRequest request = new MeetingRetrospectiveRequest("코멘트");

CustomOAuth2User customOAuth2User = mock(CustomOAuth2User.class);
when(customOAuth2User.getUser()).thenReturn(user);
Expand Down Expand Up @@ -273,7 +264,7 @@ void createMeetingRetrospective_throwsWhenNoAccess() {
Gathering gathering = Gathering.builder().id(gatheringId).build();
Meeting meeting = Meeting.builder().id(meetingId).gathering(gathering).build();
User user = User.builder().id(userId).build();
MeetingRetrospectiveRequest request = new MeetingRetrospectiveRequest(1L, "코멘트");
MeetingRetrospectiveRequest request = new MeetingRetrospectiveRequest( "코멘트");

CustomOAuth2User customOAuth2User = mock(CustomOAuth2User.class);
when(customOAuth2User.getUser()).thenReturn(user);
Expand Down