From 94ca74ac9ae82805393ac2cf0c30b369ce9d64f8 Mon Sep 17 00:00:00 2001 From: Park Yun Chan Date: Tue, 24 Sep 2024 16:35:18 +0900 Subject: [PATCH 01/56] =?UTF-8?q?fix:=20=EC=9C=A0=ED=9A=A8=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20FCM=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20(#276)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/FcmNotificationService.java | 7 ++-- .../domain/fcm/dao/FcmTokenRepository.java | 2 + .../sqs/application/SqsMessageService.java | 41 ++++++++++++------- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java b/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java index 4adb016c..aebd82e7 100644 --- a/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java +++ b/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java @@ -28,13 +28,14 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; -import org.springdoc.core.parsers.ReturnTypeParser; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@Slf4j @Service @RequiredArgsConstructor @Transactional @@ -53,7 +54,7 @@ public class FcmNotificationService { private static final long FIRST_BOOST_THRESHOLD = 1; private static final long POPULAR_THRESHOLD = 1000; private static final long SUPER_POPULAR_THRESHOLD = 5000; - private final ReturnTypeParser genericReturnTypeParser; + private static final int BATCH_SIZE = 10; public void saveNotification( FcmNotificationType type, @@ -271,7 +272,7 @@ private List buildNotificationList( } public void sendAndNotifications(String title, String message, List tokens) { - List> batches = createBatches(tokens, 10); + List> batches = createBatches(tokens, BATCH_SIZE); String deepLink = FcmNotification.generateDeepLink(FcmNotificationType.MISSION, null, null); diff --git a/src/main/java/com/depromeet/stonebed/domain/fcm/dao/FcmTokenRepository.java b/src/main/java/com/depromeet/stonebed/domain/fcm/dao/FcmTokenRepository.java index 017b1e86..f89f3c32 100644 --- a/src/main/java/com/depromeet/stonebed/domain/fcm/dao/FcmTokenRepository.java +++ b/src/main/java/com/depromeet/stonebed/domain/fcm/dao/FcmTokenRepository.java @@ -17,4 +17,6 @@ public interface FcmTokenRepository List findAllByMemberStatus(MemberStatus status); List findAllByUpdatedAtBefore(LocalDateTime cutoffDate); + + void deleteByToken(String token); } diff --git a/src/main/java/com/depromeet/stonebed/domain/sqs/application/SqsMessageService.java b/src/main/java/com/depromeet/stonebed/domain/sqs/application/SqsMessageService.java index a63fd0c6..7fd176a5 100644 --- a/src/main/java/com/depromeet/stonebed/domain/sqs/application/SqsMessageService.java +++ b/src/main/java/com/depromeet/stonebed/domain/sqs/application/SqsMessageService.java @@ -1,11 +1,11 @@ package com.depromeet.stonebed.domain.sqs.application; +import com.depromeet.stonebed.domain.fcm.dao.FcmTokenRepository; import com.depromeet.stonebed.domain.fcm.domain.FcmMessage; import com.depromeet.stonebed.infra.properties.SqsProperties; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.ArrayList; import java.util.List; -import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -29,6 +29,7 @@ public class SqsMessageService { private final SqsProperties sqsProperties; private final ObjectMapper objectMapper; + private final FcmTokenRepository fcmTokenRepository; public void sendMessage(Object message) { try { @@ -49,13 +50,14 @@ public void sendMessage(Object message) { public void sendBatchMessages( List tokens, String title, String message, String deepLink) { List entries = new ArrayList<>(); + List failedTokens = new ArrayList<>(); for (String token : tokens) { try { FcmMessage fcmMessage = FcmMessage.of(title, message, token, deepLink); String messageBody = objectMapper.writeValueAsString(fcmMessage); SendMessageBatchRequestEntry entry = SendMessageBatchRequestEntry.builder() - .id(UUID.randomUUID().toString()) + .id(token) .messageBody(messageBody) .build(); entries.add(entry); @@ -64,25 +66,34 @@ public void sendBatchMessages( } } - SendMessageBatchRequest batchRequest = - SendMessageBatchRequest.builder() - .queueUrl(sqsProperties.queueUrl()) - .entries(entries) - .build(); + if (!entries.isEmpty()) { + SendMessageBatchRequest batchRequest = + SendMessageBatchRequest.builder() + .queueUrl(sqsProperties.queueUrl()) + .entries(entries) + .build(); - try { - SendMessageBatchResponse batchResponse = sqsClient.sendMessageBatch(batchRequest); + try { + SendMessageBatchResponse batchResponse = sqsClient.sendMessageBatch(batchRequest); - // 실패한 메시지 처리 - List failedMessages = batchResponse.failed(); - if (!failedMessages.isEmpty()) { + // 실패한 메시지 처리 + List failedMessages = batchResponse.failed(); for (BatchResultErrorEntry failed : failedMessages) { log.error("메시지 전송 실패, ID {}: {}", failed.id(), failed.message()); + failedTokens.add(failed.id()); } - } - } catch (Exception e) { - log.error("SQS 배치 메시지 전송 실패: {}", e.getMessage()); + // 실패한 토큰 삭제 등의 후속 작업 + for (String failedToken : failedTokens) { + fcmTokenRepository.deleteByToken(failedToken); + log.info("비활성화된 FCM 토큰 삭제: {}", failedToken); + } + + } catch (Exception e) { + log.error("SQS 배치 메시지 전송 실패: {}", e.getMessage()); + } + } else { + log.warn("전송할 메시지가 없습니다."); } } } From 6a03e3fb6cfa71bd67b8eb4408265e5e7a0d0867 Mon Sep 17 00:00:00 2001 From: yb__char <68099546+char-yb@users.noreply.github.com> Date: Tue, 24 Sep 2024 16:44:41 +0900 Subject: [PATCH 02/56] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EA=B5=AC=ED=98=84=20(#275)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/comment/dao/CommentRepository.java | 6 ++ .../domain/comment/domain/Comment.java | 74 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepository.java create mode 100644 src/main/java/com/depromeet/stonebed/domain/comment/domain/Comment.java diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepository.java b/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepository.java new file mode 100644 index 00000000..e211a076 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepository.java @@ -0,0 +1,6 @@ +package com.depromeet.stonebed.domain.comment.dao; + +import com.depromeet.stonebed.domain.comment.domain.Comment; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository {} diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/domain/Comment.java b/src/main/java/com/depromeet/stonebed/domain/comment/domain/Comment.java new file mode 100644 index 00000000..70944d8d --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/comment/domain/Comment.java @@ -0,0 +1,74 @@ +package com.depromeet.stonebed.domain.comment.domain; + +import com.depromeet.stonebed.domain.common.BaseTimeEntity; +import com.depromeet.stonebed.domain.member.domain.Member; +import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import java.util.ArrayList; +import java.util.List; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Table(name = "comment") +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Comment extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "comment_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "record_id", nullable = false) + private MissionRecord missionRecord; + + // 작성자 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "writer_id", nullable = false) + private Member writer; + + @Schema(description = "댓글 내용", example = "너무 이쁘자나~") + @Column(name = "content", columnDefinition = "TEXT") + private String content; + + // 부모 댓글 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private Comment parent; + + // 자식 댓글 + @OneToMany(mappedBy = "parent", orphanRemoval = true) + private List children = new ArrayList<>(); + + @Builder(access = AccessLevel.PRIVATE) + public Comment(MissionRecord missionRecord, Member writer, String content, Comment parent) { + this.missionRecord = missionRecord; + this.writer = writer; + this.content = content; + this.parent = parent; + } + + public static Comment createComment( + MissionRecord missionRecord, Member writer, String content, Comment parent) { + return Comment.builder() + .missionRecord(missionRecord) + .writer(writer) + .content(content) + .parent(parent) + .build(); + } +} From 76dc4b22668046c9d693fe6122dbb9157686d2dd Mon Sep 17 00:00:00 2001 From: Park Yun Chan Date: Wed, 25 Sep 2024 19:47:30 +0900 Subject: [PATCH 03/56] =?UTF-8?q?fix:=20sendReminderToIncompleteMissions?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=EC=97=90=EC=84=9C=20token=EA=B0=92?= =?UTF-8?q?=20null=EC=9D=B8=EA=B2=83=20=EB=8C=80=EC=83=81=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=A0=9C=EC=99=B8=20(#278)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/depromeet/stonebed/scheduler/fcm/FcmScheduler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/depromeet/stonebed/scheduler/fcm/FcmScheduler.java b/src/main/java/com/depromeet/stonebed/scheduler/fcm/FcmScheduler.java index 96e817eb..05dca145 100644 --- a/src/main/java/com/depromeet/stonebed/scheduler/fcm/FcmScheduler.java +++ b/src/main/java/com/depromeet/stonebed/scheduler/fcm/FcmScheduler.java @@ -73,6 +73,7 @@ private List getIncompleteMissionTokens() { return fcmTokenRepository.findAllByMemberStatus(MemberStatus.NORMAL).stream() .filter(fcmToken -> !completedMemberIds.contains(fcmToken.getMember().getId())) + .filter(fcmToken -> fcmToken.getToken() != null) .map(FcmToken::getToken) .collect(Collectors.toList()); } From eaed21447a31776706f09ff6e46c7d94e15d125f Mon Sep 17 00:00:00 2001 From: yb__char <68099546+char-yb@users.noreply.github.com> Date: Sun, 29 Sep 2024 21:16:58 +0900 Subject: [PATCH 04/56] =?UTF-8?q?feat:=20=EB=AF=B8=EC=85=98=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=99=84=EB=A3=8C=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?(#281)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 미션 기록 완료 리스트 * feat: 미션 기록 완료 리스트 Projection 개선 * refactor: 미션 완료 기록 리스트 조건 및 DTO 수정 * fix: completedAt Type 체크 --- .../api/MissionRecordController.java | 7 +++ .../application/MissionRecordService.java | 11 +++- .../dao/MissionRecordRepositoryCustom.java | 6 ++ .../dao/MissionRecordRepositoryImpl.java | 60 ++++++++++++++++++- .../MissionRecordTabListResponse.java | 25 ++++++++ .../dto/response/MissionTabResponse.java | 24 +++++++- .../application/MissionRecordServiceTest.java | 3 +- 7 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/depromeet/stonebed/domain/missionRecord/dto/response/MissionRecordTabListResponse.java diff --git a/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java b/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java index b9695c2e..371ea7da 100644 --- a/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java +++ b/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java @@ -8,6 +8,7 @@ import com.depromeet.stonebed.domain.missionRecord.dto.response.MissionRecordCalendarResponse; import com.depromeet.stonebed.domain.missionRecord.dto.response.MissionRecordCompleteTotal; import com.depromeet.stonebed.domain.missionRecord.dto.response.MissionRecordIdResponse; +import com.depromeet.stonebed.domain.missionRecord.dto.response.MissionRecordTabListResponse; import com.depromeet.stonebed.domain.missionRecord.dto.response.MissionTabResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -26,6 +27,12 @@ public class MissionRecordController { private final MissionRecordService missionRecordService; + @Operation(summary = "미션 탭 완료된 기록 리스트", description = "미션 탭에서 완료된 기록 리스트를 조회한다.") + @GetMapping + public MissionRecordTabListResponse missionRecordsFind() { + return missionRecordService.findCompleteMissionRecords(); + } + @Operation(summary = "미션 탭 상태 조회", description = "미션 탭의 상태를 조회한다.") @GetMapping("/status") public MissionTabResponse getMissionRecordStatus(@RequestParam Long missionId) { diff --git a/src/main/java/com/depromeet/stonebed/domain/missionRecord/application/MissionRecordService.java b/src/main/java/com/depromeet/stonebed/domain/missionRecord/application/MissionRecordService.java index 5d605f90..54e068e1 100644 --- a/src/main/java/com/depromeet/stonebed/domain/missionRecord/application/MissionRecordService.java +++ b/src/main/java/com/depromeet/stonebed/domain/missionRecord/application/MissionRecordService.java @@ -18,6 +18,7 @@ import com.depromeet.stonebed.domain.missionRecord.dto.response.MissionRecordCalendarResponse; import com.depromeet.stonebed.domain.missionRecord.dto.response.MissionRecordCompleteTotal; import com.depromeet.stonebed.domain.missionRecord.dto.response.MissionRecordIdResponse; +import com.depromeet.stonebed.domain.missionRecord.dto.response.MissionRecordTabListResponse; import com.depromeet.stonebed.domain.missionRecord.dto.response.MissionTabResponse; import com.depromeet.stonebed.global.error.ErrorCode; import com.depromeet.stonebed.global.error.exception.CustomException; @@ -246,7 +247,7 @@ public MissionTabResponse getMissionTabStatus(Long missionId) { mission.getTitle(), mission.getIllustrationUrl(), missionRecord.getContent(), - missionRecord.getUpdatedAt().toLocalDate()); + missionRecord.getUpdatedAt().toLocalDate().toString()); } @Transactional(propagation = Propagation.REQUIRES_NEW) @@ -275,4 +276,12 @@ public void expiredMissionsToNotCompletedUpdate() { LocalDateTime endOfYesterday = LocalDate.now().minusDays(1).atTime(23, 59, 59); missionRecordRepository.updateExpiredMissionsToNotCompleted(endOfYesterday); } + + @Transactional(readOnly = true) + public MissionRecordTabListResponse findCompleteMissionRecords() { + final Member member = memberUtil.getCurrentMember(); + return MissionRecordTabListResponse.from( + missionRecordRepository.findAllTabMissionsByMemberAndStatus( + member, MissionRecordStatus.COMPLETED)); + } } diff --git a/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordRepositoryCustom.java b/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordRepositoryCustom.java index 724856f4..d5eb9ed9 100644 --- a/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordRepositoryCustom.java +++ b/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordRepositoryCustom.java @@ -1,7 +1,10 @@ package com.depromeet.stonebed.domain.missionRecord.dao; +import com.depromeet.stonebed.domain.member.domain.Member; import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecordDisplay; +import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecordStatus; +import com.depromeet.stonebed.domain.missionRecord.dto.response.MissionTabResponse; import java.time.LocalDateTime; import java.util.List; import org.springframework.data.domain.Pageable; @@ -17,4 +20,7 @@ List findByMemberIdAndCreatedAtFromWithPagination( Pageable pageable); void updateExpiredMissionsToNotCompleted(LocalDateTime dateTime); + + List findAllTabMissionsByMemberAndStatus( + Member member, MissionRecordStatus status); } diff --git a/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordRepositoryImpl.java b/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordRepositoryImpl.java index 0177a21d..f62d4c65 100644 --- a/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordRepositoryImpl.java +++ b/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordRepositoryImpl.java @@ -1,12 +1,21 @@ package com.depromeet.stonebed.domain.missionRecord.dao; +import static com.depromeet.stonebed.domain.mission.domain.QMission.*; +import static com.depromeet.stonebed.domain.missionHistory.domain.QMissionHistory.*; import static com.depromeet.stonebed.domain.missionRecord.domain.QMissionRecord.missionRecord; +import com.depromeet.stonebed.domain.member.domain.Member; import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecordDisplay; import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecordStatus; +import com.depromeet.stonebed.domain.missionRecord.dto.response.MissionTabResponse; +import com.querydsl.core.types.ConstantImpl; +import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.DateTemplate; +import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; @@ -24,7 +33,7 @@ public List findByMemberIdWithPagination( Long memberId, List displays, Pageable pageable) { return queryFactory .selectFrom(missionRecord) - .where(isMemberId(memberId).and(InDisplays(displays))) + .where(isMemberId(memberId).and(inDisplays(displays))) .orderBy(missionRecord.createdAt.asc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) @@ -43,7 +52,7 @@ public List findByMemberIdAndCreatedAtFromWithPagination( isMemberId(memberId) .and(createdAtFrom(createdAt)) .and(isCompleted()) - .and(InDisplays(displays))) + .and(inDisplays(displays))) .orderBy(missionRecord.createdAt.asc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) @@ -64,6 +73,51 @@ public void updateExpiredMissionsToNotCompleted(LocalDateTime currentTime) { .execute(); } + @Override + public List findAllTabMissionsByMemberAndStatus( + Member member, MissionRecordStatus status) { + DateTemplate completedAt = + Expressions.dateTemplate( + String.class, + "DATE_FORMAT({0}, {1})", + missionRecord.updatedAt, + ConstantImpl.create("%Y-%m-%d")); + + DateTemplate year = + Expressions.dateTemplate(Integer.class, "YEAR({0})", missionRecord.updatedAt); + DateTemplate month = + Expressions.dateTemplate(Integer.class, "MONTH({0})", missionRecord.updatedAt); + + int currentYear = LocalDate.now().getYear(); + int currentMonth = LocalDate.now().getMonthValue(); + + return queryFactory + .select( + Projections.constructor( + MissionTabResponse.class, + missionRecord.id.as("recordId"), + missionRecord.imageUrl, + missionRecord.status, + mission.title.as("missionTitle"), + mission.illustrationUrl, + missionRecord.content, + completedAt)) + .from(missionRecord) + .leftJoin(missionRecord.missionHistory, missionHistory) + .on(missionHistory.id.eq(missionRecord.missionHistory.id)) + .leftJoin(missionHistory.mission, mission) + .on(mission.id.eq(missionHistory.mission.id)) + .where( + missionRecord + .member + .eq(member) + .and(missionRecord.status.eq(status)) + .and(year.eq(currentYear)) + .and(month.eq(currentMonth))) + .orderBy(missionRecord.updatedAt.desc()) + .fetch(); + } + private BooleanExpression isMemberId(Long memberId) { return missionRecord.member.id.eq(memberId); } @@ -76,7 +130,7 @@ private BooleanExpression isCompleted() { return missionRecord.status.eq(MissionRecordStatus.COMPLETED); } - private BooleanExpression InDisplays(List displays) { + private BooleanExpression inDisplays(List displays) { return missionRecord.display.in(displays); } } diff --git a/src/main/java/com/depromeet/stonebed/domain/missionRecord/dto/response/MissionRecordTabListResponse.java b/src/main/java/com/depromeet/stonebed/domain/missionRecord/dto/response/MissionRecordTabListResponse.java new file mode 100644 index 00000000..0303a632 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/missionRecord/dto/response/MissionRecordTabListResponse.java @@ -0,0 +1,25 @@ +package com.depromeet.stonebed.domain.missionRecord.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +public record MissionRecordTabListResponse( + @Schema( + description = "미션 탭 목록", + example = + "[" + + "{" + + "\"recordId\": 1," + + "\"imageUrl\": \"example.jpeg\"," + + "\"status\": \"NOT_COMPLETED\"," + + "\"missionTitle\": \"산책하기\"," + + "\"illustrationUrl\": \"example.jpeg\"," + + "\"content\": \"오늘 마실다녀왔어요\"," + + "\"completedAt\": \"2021-10-10\"" + + "}" + + "]") + List list) { + public static MissionRecordTabListResponse from(List list) { + return new MissionRecordTabListResponse(list); + } +} diff --git a/src/main/java/com/depromeet/stonebed/domain/missionRecord/dto/response/MissionTabResponse.java b/src/main/java/com/depromeet/stonebed/domain/missionRecord/dto/response/MissionTabResponse.java index 9ebfb74f..335ed52a 100644 --- a/src/main/java/com/depromeet/stonebed/domain/missionRecord/dto/response/MissionTabResponse.java +++ b/src/main/java/com/depromeet/stonebed/domain/missionRecord/dto/response/MissionTabResponse.java @@ -1,8 +1,8 @@ package com.depromeet.stonebed.domain.missionRecord.dto.response; import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecordStatus; +import com.querydsl.core.annotations.QueryProjection; import io.swagger.v3.oas.annotations.media.Schema; -import java.time.LocalDate; public record MissionTabResponse( @Schema(description = "기록 ID", example = "1") Long recordId, @@ -11,7 +11,25 @@ public record MissionTabResponse( @Schema(description = "미션 제목", example = "산책하기") String missionTitle, @Schema(description = "미션 일러스트 이미지", example = "example.jpeg") String illustrationUrl, @Schema(description = "기록 내용", example = "오늘 마실다녀왔어요") String content, - @Schema(description = "완료 일자", example = "2021-10-10") LocalDate completedAt) { + @Schema(description = "완료 일자", example = "2021-10-10") String completedAt) { + + @QueryProjection + public MissionTabResponse( + Long recordId, + String imageUrl, + MissionRecordStatus status, + String missionTitle, + String illustrationUrl, + String content, + String completedAt) { + this.recordId = recordId; + this.imageUrl = imageUrl; + this.status = status; + this.missionTitle = missionTitle; + this.illustrationUrl = illustrationUrl; + this.content = content; + this.completedAt = completedAt; + } public static MissionTabResponse of( Long recordId, @@ -20,7 +38,7 @@ public static MissionTabResponse of( String missionTitle, String illustrationUrl, String content, - LocalDate completedAt) { + String completedAt) { return new MissionTabResponse( recordId, imageUrl, status, missionTitle, illustrationUrl, content, completedAt); } diff --git a/src/test/java/com/depromeet/stonebed/domain/missionRecord/application/MissionRecordServiceTest.java b/src/test/java/com/depromeet/stonebed/domain/missionRecord/application/MissionRecordServiceTest.java index 7c44b1c0..4ea618d4 100644 --- a/src/test/java/com/depromeet/stonebed/domain/missionRecord/application/MissionRecordServiceTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/missionRecord/application/MissionRecordServiceTest.java @@ -331,7 +331,8 @@ class MissionRecordServiceTest extends FixtureMonkeySetUp { then(response.imageUrl()).isEqualTo(missionRecord.getImageUrl()); then(response.status()).isEqualTo(MissionRecordStatus.COMPLETED); then(response.recordId()).isEqualTo(missionRecord.getId()); - then(response.completedAt()).isEqualTo(missionRecord.getUpdatedAt().toLocalDate()); + then(response.completedAt()) + .isEqualTo(missionRecord.getUpdatedAt().toLocalDate().toString()); then(response.content()).isEqualTo(missionRecord.getContent()); verify(memberUtil).getCurrentMember(); From 1feeb013e0e8a5c43864687d24f77d25b43f277e Mon Sep 17 00:00:00 2001 From: yb__char <68099546+char-yb@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:30:59 +0900 Subject: [PATCH 05/56] =?UTF-8?q?fix:=20missionRecordStatus=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#284)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/MissionRecordController.java | 4 ++-- .../application/MissionRecordService.java | 20 ++++++++++++++++++- .../MissionRecordTabListResponse.java | 7 +++++-- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java b/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java index 371ea7da..6ef014dc 100644 --- a/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java +++ b/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java @@ -29,8 +29,8 @@ public class MissionRecordController { @Operation(summary = "미션 탭 완료된 기록 리스트", description = "미션 탭에서 완료된 기록 리스트를 조회한다.") @GetMapping - public MissionRecordTabListResponse missionRecordsFind() { - return missionRecordService.findCompleteMissionRecords(); + public MissionRecordTabListResponse missionRecordsFind(@RequestParam Long missionId) { + return missionRecordService.findCompleteMissionRecords(missionId); } @Operation(summary = "미션 탭 상태 조회", description = "미션 탭의 상태를 조회한다.") diff --git a/src/main/java/com/depromeet/stonebed/domain/missionRecord/application/MissionRecordService.java b/src/main/java/com/depromeet/stonebed/domain/missionRecord/application/MissionRecordService.java index 54e068e1..92f0d74a 100644 --- a/src/main/java/com/depromeet/stonebed/domain/missionRecord/application/MissionRecordService.java +++ b/src/main/java/com/depromeet/stonebed/domain/missionRecord/application/MissionRecordService.java @@ -278,9 +278,27 @@ public void expiredMissionsToNotCompletedUpdate() { } @Transactional(readOnly = true) - public MissionRecordTabListResponse findCompleteMissionRecords() { + public MissionRecordTabListResponse findCompleteMissionRecords(Long missionId) { final Member member = memberUtil.getCurrentMember(); + Mission mission = + missionRepository + .findById(missionId) + .orElseThrow(() -> new CustomException(ErrorCode.MISSION_NOT_FOUND)); + + MissionHistory missionHistory = + findMissionHistoryByIdAndRaisePet(missionId, mission.getRaisePet()); + + MissionRecord missionRecord = + missionRecordRepository + .findByMemberAndMissionHistory(member, missionHistory) + .orElse(null); + + MissionRecordStatus missionRecordStatus = MissionRecordStatus.COMPLETED; + if (missionRecord == null) { + missionRecordStatus = MissionRecordStatus.NOT_COMPLETED; + } return MissionRecordTabListResponse.from( + missionRecordStatus, missionRecordRepository.findAllTabMissionsByMemberAndStatus( member, MissionRecordStatus.COMPLETED)); } diff --git a/src/main/java/com/depromeet/stonebed/domain/missionRecord/dto/response/MissionRecordTabListResponse.java b/src/main/java/com/depromeet/stonebed/domain/missionRecord/dto/response/MissionRecordTabListResponse.java index 0303a632..51f0bcda 100644 --- a/src/main/java/com/depromeet/stonebed/domain/missionRecord/dto/response/MissionRecordTabListResponse.java +++ b/src/main/java/com/depromeet/stonebed/domain/missionRecord/dto/response/MissionRecordTabListResponse.java @@ -1,9 +1,11 @@ package com.depromeet.stonebed.domain.missionRecord.dto.response; +import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecordStatus; import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; public record MissionRecordTabListResponse( + @Schema(description = "미션 당일 상태", example = "NOT_COMPLETED") MissionRecordStatus status, @Schema( description = "미션 탭 목록", example = @@ -19,7 +21,8 @@ public record MissionRecordTabListResponse( + "}" + "]") List list) { - public static MissionRecordTabListResponse from(List list) { - return new MissionRecordTabListResponse(list); + public static MissionRecordTabListResponse from( + MissionRecordStatus status, List list) { + return new MissionRecordTabListResponse(status, list); } } From 30e7e74f4e9356ced4ad632d03b188d67d698fbc Mon Sep 17 00:00:00 2001 From: ybchar Date: Mon, 30 Sep 2024 19:39:23 +0900 Subject: [PATCH 06/56] =?UTF-8?q?hotfix:=20=EB=AF=B8=EC=85=98=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=EC=B9=B4=EB=93=9C=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=98=A4=EB=A6=84=EC=B0=A8=EC=88=9C=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/missionRecord/dao/MissionRecordRepositoryImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordRepositoryImpl.java b/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordRepositoryImpl.java index f62d4c65..7f63a118 100644 --- a/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordRepositoryImpl.java +++ b/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordRepositoryImpl.java @@ -114,7 +114,7 @@ public List findAllTabMissionsByMemberAndStatus( .and(missionRecord.status.eq(status)) .and(year.eq(currentYear)) .and(month.eq(currentMonth))) - .orderBy(missionRecord.updatedAt.desc()) + .orderBy(missionRecord.updatedAt.asc()) .fetch(); } From 37d24571fc1120a732675e9984cd8f82e0e1d211 Mon Sep 17 00:00:00 2001 From: Park Yun Chan Date: Tue, 1 Oct 2024 14:14:21 +0900 Subject: [PATCH 07/56] =?UTF-8?q?=ED=94=BC=EB=93=9C=20=EC=8B=A0=EA=B3=A0?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#285)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feature: 피드 신고 기록 기능 구현 * fix: 필요없는 메서드 삭제 * fix: Report 엔티티에 BaseTimeEntity추가 * fix: /reports/ 엔드포인트 제거 * fix: ErrorCode message수정 * fix: 정적 팩토리 메서드 사용 * fix: DTO에 Swagger Schema 어노테이션추가 * fix: reportReason을 request에서 String으로 요청 * fix: response example 수정 --- .../domain/report/api/ReportController.java | 28 +++++++++ .../report/application/ReportService.java | 38 ++++++++++++ .../domain/report/dao/ReportRepository.java | 6 ++ .../stonebed/domain/report/domain/Report.java | 58 +++++++++++++++++++ .../report/dto/request/ReportRequest.java | 18 ++++++ .../dto/response/ReportReasonResponse.java | 8 +++ .../stonebed/global/error/ErrorCode.java | 5 +- 7 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/depromeet/stonebed/domain/report/api/ReportController.java create mode 100644 src/main/java/com/depromeet/stonebed/domain/report/application/ReportService.java create mode 100644 src/main/java/com/depromeet/stonebed/domain/report/dao/ReportRepository.java create mode 100644 src/main/java/com/depromeet/stonebed/domain/report/domain/Report.java create mode 100644 src/main/java/com/depromeet/stonebed/domain/report/dto/request/ReportRequest.java create mode 100644 src/main/java/com/depromeet/stonebed/domain/report/dto/response/ReportReasonResponse.java diff --git a/src/main/java/com/depromeet/stonebed/domain/report/api/ReportController.java b/src/main/java/com/depromeet/stonebed/domain/report/api/ReportController.java new file mode 100644 index 00000000..aa9c13d8 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/report/api/ReportController.java @@ -0,0 +1,28 @@ +package com.depromeet.stonebed.domain.report.api; + +import com.depromeet.stonebed.domain.report.application.ReportService; +import com.depromeet.stonebed.domain.report.dto.request.ReportRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "8. [신고]", description = "신고 기능 관련 API입니다.") +@RestController +@RequestMapping("/reports") +@RequiredArgsConstructor +public class ReportController { + private final ReportService reportService; + + @Operation(summary = "신고하기", description = "특정 피드를 신고한다.") + @PostMapping + public ResponseEntity reportFeed(@RequestBody ReportRequest reportRequest) { + reportService.reportFeed(reportRequest); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } +} diff --git a/src/main/java/com/depromeet/stonebed/domain/report/application/ReportService.java b/src/main/java/com/depromeet/stonebed/domain/report/application/ReportService.java new file mode 100644 index 00000000..ba2f24a0 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/report/application/ReportService.java @@ -0,0 +1,38 @@ +package com.depromeet.stonebed.domain.report.application; + +import com.depromeet.stonebed.domain.member.domain.Member; +import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordRepository; +import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; +import com.depromeet.stonebed.domain.report.dao.ReportRepository; +import com.depromeet.stonebed.domain.report.domain.Report; +import com.depromeet.stonebed.domain.report.dto.request.ReportRequest; +import com.depromeet.stonebed.global.error.ErrorCode; +import com.depromeet.stonebed.global.error.exception.CustomException; +import com.depromeet.stonebed.global.util.MemberUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class ReportService { + private final ReportRepository reportRepository; + private final MissionRecordRepository missionRecordRepository; + private final MemberUtil memberUtil; + + public void reportFeed(ReportRequest reportRequest) { + final Member member = memberUtil.getCurrentMember(); + + MissionRecord missionRecord = + missionRecordRepository + .findById(reportRequest.recordId()) + .orElseThrow(() -> new CustomException(ErrorCode.MISSION_RECORD_NOT_FOUND)); + + Report report = + Report.createReport( + missionRecord, member, reportRequest.reason(), reportRequest.details()); + + reportRepository.save(report); + } +} diff --git a/src/main/java/com/depromeet/stonebed/domain/report/dao/ReportRepository.java b/src/main/java/com/depromeet/stonebed/domain/report/dao/ReportRepository.java new file mode 100644 index 00000000..fb70a0e3 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/report/dao/ReportRepository.java @@ -0,0 +1,6 @@ +package com.depromeet.stonebed.domain.report.dao; + +import com.depromeet.stonebed.domain.report.domain.Report; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReportRepository extends JpaRepository {} diff --git a/src/main/java/com/depromeet/stonebed/domain/report/domain/Report.java b/src/main/java/com/depromeet/stonebed/domain/report/domain/Report.java new file mode 100644 index 00000000..80d962b1 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/report/domain/Report.java @@ -0,0 +1,58 @@ +package com.depromeet.stonebed.domain.report.domain; + +import com.depromeet.stonebed.domain.common.BaseTimeEntity; +import com.depromeet.stonebed.domain.member.domain.Member; +import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "feed_report") +public class Report extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "mission_record_id", nullable = false) + private MissionRecord missionRecord; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + private String reason; + + private String details; + + @Builder(access = AccessLevel.PRIVATE) + private Report(MissionRecord missionRecord, Member member, String reason, String details) { + this.missionRecord = missionRecord; + this.member = member; + this.reason = reason; + this.details = details; + } + + public static Report createReport( + MissionRecord missionRecord, Member member, String reportReason, String details) { + return Report.builder() + .missionRecord(missionRecord) + .member(member) + .reason(reportReason) + .details(details) + .build(); + } +} diff --git a/src/main/java/com/depromeet/stonebed/domain/report/dto/request/ReportRequest.java b/src/main/java/com/depromeet/stonebed/domain/report/dto/request/ReportRequest.java new file mode 100644 index 00000000..fe0be098 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/report/dto/request/ReportRequest.java @@ -0,0 +1,18 @@ +package com.depromeet.stonebed.domain.report.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +@Schema(description = "신고 요청 정보") +public record ReportRequest( + @Schema(description = "신고할 대상 기록의 ID", example = "123", required = true) @NotNull + Long recordId, + @Schema(description = "신고 사유", example = "사기 또는 사칭", required = true) @NotNull + String reason, + @Schema( + description = "신고 상세 내용 (최대 500자)", + example = "해당 게시물은 부적절한 내용을 포함하고 있습니다.", + maxLength = 500) + @Size(max = 500) + String details) {} diff --git a/src/main/java/com/depromeet/stonebed/domain/report/dto/response/ReportReasonResponse.java b/src/main/java/com/depromeet/stonebed/domain/report/dto/response/ReportReasonResponse.java new file mode 100644 index 00000000..6c682e75 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/report/dto/response/ReportReasonResponse.java @@ -0,0 +1,8 @@ +package com.depromeet.stonebed.domain.report.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "신고 사유 응답 정보") +public record ReportReasonResponse( + @Schema(description = "신고 사유", example = "HARASSMENT") String enumValue, + @Schema(description = "신고 사유 설명", example = "사기 또는 사칭") String description) {} diff --git a/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java b/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java index 9d40cd91..dd76c07f 100644 --- a/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java +++ b/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java @@ -57,7 +57,10 @@ public enum ErrorCode { // fcm INVALID_FCM_TOKEN(HttpStatus.BAD_REQUEST, "FCM 토큰값이 비어있습니다."), FAILED_TO_FIND_FCM_TOKEN(HttpStatus.NOT_FOUND, "해당 FCM 토큰을 찾을 수 없습니다."), - NOTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 알림을 찾을 수 없습니다."); + NOTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 알림을 찾을 수 없습니다."), + + // report + INVALID_REPORT_REASON(HttpStatus.NOT_FOUND, "해당 신고 사유를 찾을 수 없습니다."); private final HttpStatus httpStatus; private final String message; } From 5b85d1eb449dd0938b408f2dd036ea11c33993f2 Mon Sep 17 00:00:00 2001 From: yb__char <68099546+char-yb@users.noreply.github.com> Date: Tue, 1 Oct 2024 22:44:32 +0900 Subject: [PATCH 08/56] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=B0=8F=20=EB=8C=93=EA=B8=80,=20=EB=8C=80?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=B6=94=EA=B0=80=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#287)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: comment API 중간 커밋 * feat: 댓글 추가 및 조회 * fix: DTO Swagger 정의 * fix: children -> replyComments * refactor: 개선 및 주석 * fix: @dbscks97 리뷰 반영 --- .../domain/comment/api/CommentController.java | 41 +++++ .../comment/application/CommentService.java | 152 ++++++++++++++++++ .../domain/comment/dao/CommentRepository.java | 2 +- .../comment/dao/CommentRepositoryCustom.java | 9 ++ .../comment/dao/CommentRepositoryImpl.java | 27 ++++ .../domain/comment/domain/Comment.java | 5 +- .../dto/request/CommentCreateRequest.java | 8 + .../dto/response/CommentCreateResponse.java | 9 ++ .../dto/response/CommentFindOneResponse.java | 35 ++++ .../dto/response/CommentFindResponse.java | 11 ++ .../domain/member/api/MemberController.java | 7 + .../member/application/MemberService.java | 5 + .../domain/member/dao/MemberRepository.java | 2 + .../stonebed/global/error/ErrorCode.java | 6 +- .../stonebed/global/util/MemberUtil.java | 6 + 15 files changed, 321 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/depromeet/stonebed/domain/comment/api/CommentController.java create mode 100644 src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java create mode 100644 src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepositoryCustom.java create mode 100644 src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepositoryImpl.java create mode 100644 src/main/java/com/depromeet/stonebed/domain/comment/dto/request/CommentCreateRequest.java create mode 100644 src/main/java/com/depromeet/stonebed/domain/comment/dto/response/CommentCreateResponse.java create mode 100644 src/main/java/com/depromeet/stonebed/domain/comment/dto/response/CommentFindOneResponse.java create mode 100644 src/main/java/com/depromeet/stonebed/domain/comment/dto/response/CommentFindResponse.java diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/api/CommentController.java b/src/main/java/com/depromeet/stonebed/domain/comment/api/CommentController.java new file mode 100644 index 00000000..69c45b83 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/comment/api/CommentController.java @@ -0,0 +1,41 @@ +package com.depromeet.stonebed.domain.comment.api; + +import com.depromeet.stonebed.domain.comment.application.CommentService; +import com.depromeet.stonebed.domain.comment.dto.request.CommentCreateRequest; +import com.depromeet.stonebed.domain.comment.dto.response.CommentCreateResponse; +import com.depromeet.stonebed.domain.comment.dto.response.CommentFindResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "9. [댓글]", description = "댓글 관련 API입니다.") +@RequestMapping("/comments") +@RestController +@RequiredArgsConstructor +public class CommentController { + + private final CommentService commentService; + + @Operation(summary = "댓글 작성", description = "댓글을 작성합니다.") + @PostMapping + public ResponseEntity commentCreate( + @RequestBody @Valid CommentCreateRequest request) { + return ResponseEntity.status(HttpStatus.CREATED) + .body(commentService.createComment(request)); + } + + @Operation(summary = "댓글 조회", description = "댓글을 조회합니다.") + @GetMapping + public CommentFindResponse commentFind(@RequestParam Long recordId) { + return commentService.findCommentsByRecordId(recordId); + } +} diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java new file mode 100644 index 00000000..fed5b6b1 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java @@ -0,0 +1,152 @@ +package com.depromeet.stonebed.domain.comment.application; + +import com.depromeet.stonebed.domain.comment.dao.CommentRepository; +import com.depromeet.stonebed.domain.comment.domain.Comment; +import com.depromeet.stonebed.domain.comment.dto.request.CommentCreateRequest; +import com.depromeet.stonebed.domain.comment.dto.response.CommentCreateResponse; +import com.depromeet.stonebed.domain.comment.dto.response.CommentFindOneResponse; +import com.depromeet.stonebed.domain.comment.dto.response.CommentFindResponse; +import com.depromeet.stonebed.domain.member.domain.Member; +import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordRepository; +import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; +import com.depromeet.stonebed.global.error.ErrorCode; +import com.depromeet.stonebed.global.error.exception.CustomException; +import com.depromeet.stonebed.global.util.MemberUtil; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class CommentService { + + private final MemberUtil memberUtil; + private final CommentRepository commentRepository; + private final MissionRecordRepository missionRecordRepository; + private static final Long ROOT_COMMENT_PARENT_ID = -1L; + + /** + * 댓글을 생성합니다. + * + * @param request 댓글 생성 요청 객체 (content, recordId, parentId 포함) + * @return 생성된 댓글의 ID를 포함한 응답 객체 + */ + public CommentCreateResponse createComment(CommentCreateRequest request) { + final Member member = memberUtil.getCurrentMember(); + final MissionRecord missionRecord = findMissionRecordById(request.recordId()); + + // 부모 댓글이 존재하는 경우 + + final Comment comment = + request.parentId() != null + ? Comment.createComment( + missionRecord, + member, + request.content(), + findCommentById(request.parentId())) + : Comment.createComment(missionRecord, member, request.content(), null); + + Comment savedComment = commentRepository.save(comment); + + return CommentCreateResponse.of(savedComment.getId()); + } + + /** + * 특정 기록 ID에 대한 모든 댓글을 조회합니다. + * + * @param recordId 조회할 기록의 ID + * @return 조회된 댓글 목록을 포함한 응답 객체 + */ + @Transactional(readOnly = true) + public CommentFindResponse findCommentsByRecordId(Long recordId) { + final MissionRecord missionRecord = findMissionRecordById(recordId); + final List allComments = + commentRepository.findAllCommentsByMissionRecord(missionRecord); + + // 댓글을 부모 ID로 그룹화, 부모 ID가 null인 경우 -1L로 처리 + Map> commentsByParentId = + allComments.stream() + .collect( + Collectors.groupingBy( + comment -> { + Comment parent = comment.getParent(); + return (parent != null) + ? parent.getId() + : ROOT_COMMENT_PARENT_ID; + }, + Collectors.toList())); + + // 부모 댓글 (부모 댓글이 없는 댓글) 조회 + List rootComments = + commentsByParentId.getOrDefault(ROOT_COMMENT_PARENT_ID, List.of()); + + // 부모 댓글을 CommentFindOneResponse로 변환 + List rootResponses = + rootComments.stream() + .map( + comment -> + convertToCommentFindOneResponse( + comment, commentsByParentId)) + .collect(Collectors.toList()); + + return CommentFindResponse.of(rootResponses); + } + + /** + * Comment 객체를 CommentFindOneResponse 객체로 변환합니다. + * + * @param comment 변환할 댓글 객체 + * @param commentsByParentId 부모 ID로 그룹화된 댓글 key-value 형태의 Map + * @return 변환된 댓글 응답 객체 + */ + private CommentFindOneResponse convertToCommentFindOneResponse( + Comment comment, Map> commentsByParentId) { + List replyCommentsResponses = + commentsByParentId.getOrDefault(comment.getId(), List.of()).stream() + .map( + childComment -> + convertToCommentFindOneResponse( + childComment, commentsByParentId)) + .collect(Collectors.toList()); + + return CommentFindOneResponse.of( + comment.getParent() != null ? comment.getParent().getId() : null, + comment.getId(), + comment.getContent(), + comment.getWriter().getId(), + comment.getWriter().getProfile().getNickname(), + comment.getWriter().getProfile().getProfileImageUrl(), + comment.getCreatedAt().toString(), + replyCommentsResponses); + } + + /** + * MissionRecord를 조회 + * + * @param recordId 조회할 기록의 ID + * @return 조회된 MissionRecord 객체 + * @throws CustomException 기록을 찾을 수 없는 경우 예외 발생 + */ + private MissionRecord findMissionRecordById(Long recordId) { + return missionRecordRepository + .findById(recordId) + .orElseThrow(() -> new CustomException(ErrorCode.MISSION_RECORD_NOT_FOUND)); + } + + /** + * Comment를 조회 + * + * @param commentId 조회할 댓글의 ID + * @return 조회된 Comment 객체 + * @throws CustomException 댓글을 찾을 수 없는 경우 예외 발생 + */ + private Comment findCommentById(Long commentId) { + return commentRepository + .findById(commentId) + .orElseThrow(() -> new CustomException(ErrorCode.COMMENT_NOT_FOUND)); + } +} diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepository.java b/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepository.java index e211a076..c2763a03 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepository.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepository.java @@ -3,4 +3,4 @@ import com.depromeet.stonebed.domain.comment.domain.Comment; import org.springframework.data.jpa.repository.JpaRepository; -public interface CommentRepository extends JpaRepository {} +public interface CommentRepository extends JpaRepository, CommentRepositoryCustom {} diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepositoryCustom.java b/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepositoryCustom.java new file mode 100644 index 00000000..7d1cb6ac --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepositoryCustom.java @@ -0,0 +1,9 @@ +package com.depromeet.stonebed.domain.comment.dao; + +import com.depromeet.stonebed.domain.comment.domain.Comment; +import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; +import java.util.List; + +public interface CommentRepositoryCustom { + List findAllCommentsByMissionRecord(MissionRecord missionRecord); +} diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepositoryImpl.java b/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepositoryImpl.java new file mode 100644 index 00000000..9d7912ed --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepositoryImpl.java @@ -0,0 +1,27 @@ +package com.depromeet.stonebed.domain.comment.dao; + +import static com.depromeet.stonebed.domain.comment.domain.QComment.comment; + +import com.depromeet.stonebed.domain.comment.domain.Comment; +import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class CommentRepositoryImpl implements CommentRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public List findAllCommentsByMissionRecord(MissionRecord missionRecord) { + return queryFactory + .selectFrom(comment) + .leftJoin(comment.parent) + .fetchJoin() + .leftJoin(comment.replyComments) + .fetchJoin() + .where(comment.missionRecord.eq(missionRecord)) + .fetch(); + } +} diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/domain/Comment.java b/src/main/java/com/depromeet/stonebed/domain/comment/domain/Comment.java index 70944d8d..cd6dc663 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/domain/Comment.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/domain/Comment.java @@ -4,6 +4,7 @@ import com.depromeet.stonebed.domain.member.domain.Member; import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.annotation.Nullable; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -52,7 +53,7 @@ public class Comment extends BaseTimeEntity { // 자식 댓글 @OneToMany(mappedBy = "parent", orphanRemoval = true) - private List children = new ArrayList<>(); + private List replyComments = new ArrayList<>(); @Builder(access = AccessLevel.PRIVATE) public Comment(MissionRecord missionRecord, Member writer, String content, Comment parent) { @@ -63,7 +64,7 @@ public Comment(MissionRecord missionRecord, Member writer, String content, Comme } public static Comment createComment( - MissionRecord missionRecord, Member writer, String content, Comment parent) { + MissionRecord missionRecord, Member writer, String content, @Nullable Comment parent) { return Comment.builder() .missionRecord(missionRecord) .writer(writer) diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/dto/request/CommentCreateRequest.java b/src/main/java/com/depromeet/stonebed/domain/comment/dto/request/CommentCreateRequest.java new file mode 100644 index 00000000..4b0e2d43 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/comment/dto/request/CommentCreateRequest.java @@ -0,0 +1,8 @@ +package com.depromeet.stonebed.domain.comment.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record CommentCreateRequest( + @Schema(description = "댓글 내용", example = "너무 이쁘자나~") String content, + @Schema(description = "기록 ID", example = "1") Long recordId, + @Schema(description = "부모 댓글 ID", example = "1") Long parentId) {} diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/dto/response/CommentCreateResponse.java b/src/main/java/com/depromeet/stonebed/domain/comment/dto/response/CommentCreateResponse.java new file mode 100644 index 00000000..d6ebf760 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/comment/dto/response/CommentCreateResponse.java @@ -0,0 +1,9 @@ +package com.depromeet.stonebed.domain.comment.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record CommentCreateResponse(@Schema(description = "댓글 ID", example = "1") Long commentId) { + public static CommentCreateResponse of(Long commentId) { + return new CommentCreateResponse(commentId); + } +} diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/dto/response/CommentFindOneResponse.java b/src/main/java/com/depromeet/stonebed/domain/comment/dto/response/CommentFindOneResponse.java new file mode 100644 index 00000000..5f5161d0 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/comment/dto/response/CommentFindOneResponse.java @@ -0,0 +1,35 @@ +package com.depromeet.stonebed.domain.comment.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +public record CommentFindOneResponse( + @Schema(description = "부모 댓글 ID", example = "1") Long parentId, + @Schema(description = "댓글 ID", example = "1") Long commentId, + @Schema(description = "댓글 내용", example = "너무 이쁘자나~") String content, + @Schema(description = "작성자 ID", example = "1") Long writerId, + @Schema(description = "작성자 닉네임", example = "왈왈대장") String writerNickname, + @Schema(description = "작성자 프로필 이미지 URL", example = "https://default.walwal/profile.jpg") + String writerProfileImageUrl, + @Schema(description = "작성일", example = "2021-10-01T00:00:00") String createdAt, + @Schema(description = "자식 댓글 목록") List replyComments) { + public static CommentFindOneResponse of( + Long parentId, + Long commentId, + String content, + Long writerId, + String writerNickname, + String writerProfileImageUrl, + String createdAt, + List replyComments) { + return new CommentFindOneResponse( + parentId, + commentId, + content, + writerId, + writerNickname, + writerProfileImageUrl, + createdAt, + replyComments); + } +} diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/dto/response/CommentFindResponse.java b/src/main/java/com/depromeet/stonebed/domain/comment/dto/response/CommentFindResponse.java new file mode 100644 index 00000000..56a747d1 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/comment/dto/response/CommentFindResponse.java @@ -0,0 +1,11 @@ +package com.depromeet.stonebed.domain.comment.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +public record CommentFindResponse( + @Schema(description = "댓글 목록") List comments) { + public static CommentFindResponse of(List comments) { + return new CommentFindResponse(comments); + } +} diff --git a/src/main/java/com/depromeet/stonebed/domain/member/api/MemberController.java b/src/main/java/com/depromeet/stonebed/domain/member/api/MemberController.java index 2abf384f..37828d78 100644 --- a/src/main/java/com/depromeet/stonebed/domain/member/api/MemberController.java +++ b/src/main/java/com/depromeet/stonebed/domain/member/api/MemberController.java @@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @Tag(name = "1-2. [회원]", description = "회원 관련 API입니다.") @@ -25,6 +26,12 @@ public class MemberController { private final MemberService memberService; + @Operation(summary = "회원 정보 조회", description = "회원 정보를 조회하는 API입니다.") + @GetMapping + public MemberInfoResponse memberInfoByNickname(@Valid @RequestParam String nickname) { + return memberService.findMemberInfoByNickname(nickname); + } + @Operation(summary = "내 정보 조회", description = "내 정보를 조회하는 API입니다.") @GetMapping("/me") public MemberInfoResponse memberMyInfo() { diff --git a/src/main/java/com/depromeet/stonebed/domain/member/application/MemberService.java b/src/main/java/com/depromeet/stonebed/domain/member/application/MemberService.java index a0925b03..177d11cc 100644 --- a/src/main/java/com/depromeet/stonebed/domain/member/application/MemberService.java +++ b/src/main/java/com/depromeet/stonebed/domain/member/application/MemberService.java @@ -41,4 +41,9 @@ public void modifyMemberProfile(MemberProfileUpdateRequest request) { Profile profile = Profile.createProfile(request.nickname(), request.profileImageUrl()); member.updateProfile(profile); } + + public MemberInfoResponse findMemberInfoByNickname(String nickname) { + Member member = memberUtil.getMemberByNickname(nickname); + return MemberInfoResponse.from(member); + } } diff --git a/src/main/java/com/depromeet/stonebed/domain/member/dao/MemberRepository.java b/src/main/java/com/depromeet/stonebed/domain/member/dao/MemberRepository.java index b30050ca..c2fab75b 100644 --- a/src/main/java/com/depromeet/stonebed/domain/member/dao/MemberRepository.java +++ b/src/main/java/com/depromeet/stonebed/domain/member/dao/MemberRepository.java @@ -7,4 +7,6 @@ public interface MemberRepository extends JpaRepository, MemberRepositoryCustom { Optional findByOauthInfoOauthProviderAndOauthInfoOauthId( String oauthProvider, String oauthId); + + Optional findByProfileNickname(String nickname); } diff --git a/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java b/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java index dd76c07f..4538396c 100644 --- a/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java +++ b/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java @@ -60,7 +60,11 @@ public enum ErrorCode { NOTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 알림을 찾을 수 없습니다."), // report - INVALID_REPORT_REASON(HttpStatus.NOT_FOUND, "해당 신고 사유를 찾을 수 없습니다."); + INVALID_REPORT_REASON(HttpStatus.NOT_FOUND, "해당 신고 사유를 찾을 수 없습니다."), + + // comment + COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 댓글을 찾을 수 없습니다."), + ; private final HttpStatus httpStatus; private final String message; } diff --git a/src/main/java/com/depromeet/stonebed/global/util/MemberUtil.java b/src/main/java/com/depromeet/stonebed/global/util/MemberUtil.java index de548731..9b2a2d40 100644 --- a/src/main/java/com/depromeet/stonebed/global/util/MemberUtil.java +++ b/src/main/java/com/depromeet/stonebed/global/util/MemberUtil.java @@ -55,4 +55,10 @@ private void validateNicknameNotDuplicate(String nickname, String currentNicknam throw new CustomException(ErrorCode.MEMBER_ALREADY_NICKNAME); } } + + public Member getMemberByNickname(String nickname) { + return memberRepository + .findByProfileNickname(nickname) + .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND)); + } } From b7608cba71e450831526a1d1d2af471793e2c3c7 Mon Sep 17 00:00:00 2001 From: Park Yun Chan Date: Wed, 2 Oct 2024 21:09:13 +0900 Subject: [PATCH 09/56] =?UTF-8?q?=ED=94=BC=EB=93=9C=20=EC=8B=A0=EA=B3=A0?= =?UTF-8?q?=EC=8B=9C=20discord=20=EC=95=8C=EB=A6=BC=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#290)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feature: 피드 신고시 discord 알림 기능 구현 * fix: properties사용하는방법으로 수정 * fix: comment기능 merge --- .../DiscordNotificationService.java | 52 +++++++++++++++++++ .../report/application/ReportService.java | 48 ++++++++++++++++- .../stonebed/global/error/ErrorCode.java | 6 ++- .../infra/properties/DiscordProperties.java | 6 +++ .../infra/properties/PropertiesConfig.java | 3 +- src/main/resources/application.yml | 3 ++ 6 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/depromeet/stonebed/domain/discord/application/DiscordNotificationService.java create mode 100644 src/main/java/com/depromeet/stonebed/infra/properties/DiscordProperties.java diff --git a/src/main/java/com/depromeet/stonebed/domain/discord/application/DiscordNotificationService.java b/src/main/java/com/depromeet/stonebed/domain/discord/application/DiscordNotificationService.java new file mode 100644 index 00000000..43f1452c --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/discord/application/DiscordNotificationService.java @@ -0,0 +1,52 @@ +package com.depromeet.stonebed.domain.discord.application; + +import com.depromeet.stonebed.global.error.ErrorCode; +import com.depromeet.stonebed.global.error.exception.CustomException; +import com.depromeet.stonebed.infra.properties.DiscordProperties; +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestClient; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class DiscordNotificationService { + + private final DiscordProperties discordProperties; + private final RestClient restClient; + + public void sendDiscordMessage(String message) { + Map payload = new HashMap<>(); + payload.put("content", message); + + try { + String discordWebhookUrl = discordProperties.url(); + log.info("Sending Discord notification to URL: {}", discordWebhookUrl); + + restClient + .post() + .uri(discordWebhookUrl) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .body(payload) + .exchange( + (request, response) -> { + if (!response.getStatusCode().is2xxSuccessful()) { + throw new CustomException( + ErrorCode.DISCORD_NOTIFICATION_FAILED); + } + log.info("Discord 알림 전송 성공: {}", message); + return response.bodyTo(String.class); + }); + + } catch (Exception e) { + log.error("Discord 알림 전송 중 예외 발생: {}", message, e); + } + } +} diff --git a/src/main/java/com/depromeet/stonebed/domain/report/application/ReportService.java b/src/main/java/com/depromeet/stonebed/domain/report/application/ReportService.java index ba2f24a0..64bc0906 100644 --- a/src/main/java/com/depromeet/stonebed/domain/report/application/ReportService.java +++ b/src/main/java/com/depromeet/stonebed/domain/report/application/ReportService.java @@ -1,5 +1,6 @@ package com.depromeet.stonebed.domain.report.application; +import com.depromeet.stonebed.domain.discord.application.DiscordNotificationService; import com.depromeet.stonebed.domain.member.domain.Member; import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordRepository; import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; @@ -9,6 +10,7 @@ import com.depromeet.stonebed.global.error.ErrorCode; import com.depromeet.stonebed.global.error.exception.CustomException; import com.depromeet.stonebed.global.util.MemberUtil; +import java.time.format.DateTimeFormatter; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,22 +19,64 @@ @RequiredArgsConstructor @Transactional public class ReportService { + private static final DateTimeFormatter DATE_TIME_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private final ReportRepository reportRepository; private final MissionRecordRepository missionRecordRepository; private final MemberUtil memberUtil; + private final DiscordNotificationService discordNotificationService; public void reportFeed(ReportRequest reportRequest) { - final Member member = memberUtil.getCurrentMember(); + final Member reporter = memberUtil.getCurrentMember(); MissionRecord missionRecord = missionRecordRepository .findById(reportRequest.recordId()) .orElseThrow(() -> new CustomException(ErrorCode.MISSION_RECORD_NOT_FOUND)); + Member reportedMember = missionRecord.getMember(); + Report report = Report.createReport( - missionRecord, member, reportRequest.reason(), reportRequest.details()); + missionRecord, reporter, reportRequest.reason(), reportRequest.details()); reportRepository.save(report); + + sendReportNotificationToDiscord(reporter, reportedMember, missionRecord, reportRequest); + } + + private void sendReportNotificationToDiscord( + Member reporter, + Member reportedMember, + MissionRecord missionRecord, + ReportRequest reportRequest) { + String reportTime = java.time.LocalDateTime.now().format(DATE_TIME_FORMATTER); + + String message = + String.format( + "🚨 **신고 접수 알림** 🚨\n\n" + + "**-- 신고자 정보 --**\n" + + "**닉네임**: %s\n" + + "**신고 시간**: %s\n\n" + + "**-- 신고 상세 내용 --**\n" + + "**신고 사유**: %s\n" + + "**신고 내용**: %s\n\n" + + "**-- 신고 대상 정보 --**\n" + + "**닉네임**: %s\n" + + "**게시글 ID**: %d\n" + + "**게시글 이미지 URL**: %s\n" + + "**게시글 내용**: %s", + reporter.getProfile().getNickname(), + reportTime, + reportRequest.reason(), + reportRequest.details(), + reportedMember.getProfile().getNickname(), + missionRecord.getId(), + missionRecord.getImageUrl() != null + ? missionRecord.getImageUrl() + : "이미지 없음", + missionRecord.getContent() != null ? missionRecord.getContent() : "내용 없음"); + + discordNotificationService.sendDiscordMessage(message); } } diff --git a/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java b/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java index 4538396c..c34e0efe 100644 --- a/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java +++ b/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java @@ -62,9 +62,11 @@ public enum ErrorCode { // report INVALID_REPORT_REASON(HttpStatus.NOT_FOUND, "해당 신고 사유를 찾을 수 없습니다."), + DISCORD_NOTIFICATION_FAILED(HttpStatus.BAD_REQUEST, "디스코드 알림 전송이 실패했습니다."), + // comment - COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 댓글을 찾을 수 없습니다."), - ; + COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 댓글을 찾을 수 없습니다."); + private final HttpStatus httpStatus; private final String message; } diff --git a/src/main/java/com/depromeet/stonebed/infra/properties/DiscordProperties.java b/src/main/java/com/depromeet/stonebed/infra/properties/DiscordProperties.java new file mode 100644 index 00000000..68da44d5 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/infra/properties/DiscordProperties.java @@ -0,0 +1,6 @@ +package com.depromeet.stonebed.infra.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "discord") +public record DiscordProperties(String url) {} diff --git a/src/main/java/com/depromeet/stonebed/infra/properties/PropertiesConfig.java b/src/main/java/com/depromeet/stonebed/infra/properties/PropertiesConfig.java index 0d8c3752..b0159b82 100644 --- a/src/main/java/com/depromeet/stonebed/infra/properties/PropertiesConfig.java +++ b/src/main/java/com/depromeet/stonebed/infra/properties/PropertiesConfig.java @@ -9,7 +9,8 @@ JwtProperties.class, AppleProperties.class, SwaggerProperties.class, - SqsProperties.class + SqsProperties.class, + DiscordProperties.class }) @Configuration public class PropertiesConfig {} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d8fd2762..1ca01776 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -20,3 +20,6 @@ logging: com.depromeet.stonebed.*.*.application.*: debug org.hibernate.SQL: debug org.hibernate.type: trace + +discord: + url: ${DISCORD_WEBHOOK_URL} From 0939143f0da59a62e81dea750c3df5c5e40f0687 Mon Sep 17 00:00:00 2001 From: yb__char <68099546+char-yb@users.noreply.github.com> Date: Thu, 3 Oct 2024 20:42:25 +0900 Subject: [PATCH 10/56] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=20=ED=91=B8=EC=8B=9C=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20(#291)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 댓글 추가 시 푸시알림 * feat: 자식 댓글 작성자에게도 푸시 알림 * feat: 자식 댓글 작성자에게도 푸시 알림 수정 --- .../comment/application/CommentService.java | 158 +++++++++++------- .../application/FcmNotificationService.java | 19 ++- .../fcm/domain/FcmNotificationType.java | 5 +- .../constants/FcmNotificationConstants.java | 5 +- .../stonebed/scheduler/fcm/FcmScheduler.java | 7 +- .../fcm/application/FcmSchedulerTest.java | 13 +- 6 files changed, 135 insertions(+), 72 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java index fed5b6b1..44362665 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java @@ -6,14 +6,23 @@ import com.depromeet.stonebed.domain.comment.dto.response.CommentCreateResponse; import com.depromeet.stonebed.domain.comment.dto.response.CommentFindOneResponse; import com.depromeet.stonebed.domain.comment.dto.response.CommentFindResponse; +import com.depromeet.stonebed.domain.fcm.application.FcmNotificationService; +import com.depromeet.stonebed.domain.fcm.dao.FcmTokenRepository; +import com.depromeet.stonebed.domain.fcm.domain.FcmNotificationType; +import com.depromeet.stonebed.domain.fcm.domain.FcmToken; import com.depromeet.stonebed.domain.member.domain.Member; import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordRepository; import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; +import com.depromeet.stonebed.global.common.constants.FcmNotificationConstants; import com.depromeet.stonebed.global.error.ErrorCode; import com.depromeet.stonebed.global.error.exception.CustomException; import com.depromeet.stonebed.global.util.MemberUtil; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -27,20 +36,23 @@ public class CommentService { private final MemberUtil memberUtil; private final CommentRepository commentRepository; private final MissionRecordRepository missionRecordRepository; + private final FcmNotificationService fcmNotificationService; + private final FcmTokenRepository fcmTokenRepository; private static final Long ROOT_COMMENT_PARENT_ID = -1L; - /** - * 댓글을 생성합니다. - * - * @param request 댓글 생성 요청 객체 (content, recordId, parentId 포함) - * @return 생성된 댓글의 ID를 포함한 응답 객체 - */ public CommentCreateResponse createComment(CommentCreateRequest request) { final Member member = memberUtil.getCurrentMember(); final MissionRecord missionRecord = findMissionRecordById(request.recordId()); - // 부모 댓글이 존재하는 경우 + final Comment comment = createAndSaveComment(request, member, missionRecord); + final FcmNotificationConstants notificationType = getNotificationType(request); + sendCommentNotification(missionRecord, comment, notificationType); + return CommentCreateResponse.of(comment.getId()); + } + + private Comment createAndSaveComment( + CommentCreateRequest request, Member member, MissionRecord missionRecord) { final Comment comment = request.parentId() != null ? Comment.createComment( @@ -50,59 +62,99 @@ public CommentCreateResponse createComment(CommentCreateRequest request) { findCommentById(request.parentId())) : Comment.createComment(missionRecord, member, request.content(), null); - Comment savedComment = commentRepository.save(comment); + return commentRepository.save(comment); + } + + private FcmNotificationConstants getNotificationType(CommentCreateRequest request) { + return request.parentId() == null + ? FcmNotificationConstants.COMMENT + : FcmNotificationConstants.RE_COMMENT; + } + + private void sendCommentNotification( + MissionRecord missionRecord, + Comment comment, + FcmNotificationConstants commentNotification) { + Set notificationRecipients = collectNotificationRecipients(missionRecord, comment); + List tokens = retrieveFcmTokens(notificationRecipients); + + FcmNotificationType fcmNotificationType = getFcmNotificationType(commentNotification); + fcmNotificationService.sendAndNotifications( + commentNotification.getTitle(), + commentNotification.getMessage(), + tokens, + fcmNotificationType); + } + + private Set collectNotificationRecipients( + MissionRecord missionRecord, Comment comment) { + Set notificationRecipients = new HashSet<>(); + notificationRecipients.add(missionRecord.getMember()); + + Comment currentComment = comment; + while (currentComment.getParent() != null) { + currentComment = currentComment.getParent(); + currentComment.getReplyComments().stream() + .map(Comment::getWriter) + .forEach(notificationRecipients::add); + } + notificationRecipients.remove(comment.getWriter()); + return notificationRecipients; + } - return CommentCreateResponse.of(savedComment.getId()); + private List retrieveFcmTokens(Set notificationRecipients) { + return notificationRecipients.stream() + .map(fcmTokenRepository::findByMember) + .filter(Optional::isPresent) + .map(Optional::get) + .map(FcmToken::getToken) + .filter(Objects::nonNull) + .filter(token -> !token.isEmpty() && !token.isBlank()) + .collect(Collectors.toList()); + } + + private FcmNotificationType getFcmNotificationType( + FcmNotificationConstants commentNotification) { + return FcmNotificationConstants.RE_COMMENT.equals(commentNotification) + ? FcmNotificationType.RE_COMMENT + : FcmNotificationType.COMMENT; } - /** - * 특정 기록 ID에 대한 모든 댓글을 조회합니다. - * - * @param recordId 조회할 기록의 ID - * @return 조회된 댓글 목록을 포함한 응답 객체 - */ @Transactional(readOnly = true) public CommentFindResponse findCommentsByRecordId(Long recordId) { final MissionRecord missionRecord = findMissionRecordById(recordId); final List allComments = commentRepository.findAllCommentsByMissionRecord(missionRecord); - // 댓글을 부모 ID로 그룹화, 부모 ID가 null인 경우 -1L로 처리 - Map> commentsByParentId = - allComments.stream() - .collect( - Collectors.groupingBy( - comment -> { - Comment parent = comment.getParent(); - return (parent != null) - ? parent.getId() - : ROOT_COMMENT_PARENT_ID; - }, - Collectors.toList())); - - // 부모 댓글 (부모 댓글이 없는 댓글) 조회 - List rootComments = - commentsByParentId.getOrDefault(ROOT_COMMENT_PARENT_ID, List.of()); - - // 부모 댓글을 CommentFindOneResponse로 변환 + Map> commentsByParentId = groupCommentsByParentId(allComments); List rootResponses = - rootComments.stream() - .map( - comment -> - convertToCommentFindOneResponse( - comment, commentsByParentId)) - .collect(Collectors.toList()); + convertToCommentFindOneResponses(commentsByParentId); return CommentFindResponse.of(rootResponses); } - /** - * Comment 객체를 CommentFindOneResponse 객체로 변환합니다. - * - * @param comment 변환할 댓글 객체 - * @param commentsByParentId 부모 ID로 그룹화된 댓글 key-value 형태의 Map - * @return 변환된 댓글 응답 객체 - */ + private Map> groupCommentsByParentId(List allComments) { + return allComments.stream() + .collect( + Collectors.groupingBy( + comment -> { + Comment parent = comment.getParent(); + return (parent != null) + ? parent.getId() + : ROOT_COMMENT_PARENT_ID; + }, + Collectors.toList())); + } + + private List convertToCommentFindOneResponses( + Map> commentsByParentId) { + List rootComments = + commentsByParentId.getOrDefault(ROOT_COMMENT_PARENT_ID, List.of()); + return rootComments.stream() + .map(comment -> convertToCommentFindOneResponse(comment, commentsByParentId)) + .collect(Collectors.toList()); + } + private CommentFindOneResponse convertToCommentFindOneResponse( Comment comment, Map> commentsByParentId) { List replyCommentsResponses = @@ -124,26 +176,12 @@ private CommentFindOneResponse convertToCommentFindOneResponse( replyCommentsResponses); } - /** - * MissionRecord를 조회 - * - * @param recordId 조회할 기록의 ID - * @return 조회된 MissionRecord 객체 - * @throws CustomException 기록을 찾을 수 없는 경우 예외 발생 - */ private MissionRecord findMissionRecordById(Long recordId) { return missionRecordRepository .findById(recordId) .orElseThrow(() -> new CustomException(ErrorCode.MISSION_RECORD_NOT_FOUND)); } - /** - * Comment를 조회 - * - * @param commentId 조회할 댓글의 ID - * @return 조회된 Comment 객체 - * @throws CustomException 댓글을 찾을 수 없는 경우 예외 발생 - */ private Comment findCommentById(Long commentId) { return commentRepository .findById(commentId) diff --git a/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java b/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java index aebd82e7..f78843f2 100644 --- a/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java +++ b/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java @@ -251,7 +251,10 @@ public void markNotificationAsRead(Long notificationId) { } private List buildNotificationList( - String title, String message, List tokens) { + String title, + String message, + List tokens, + FcmNotificationType notificationType) { List notifications = new ArrayList<>(); for (String token : tokens) { @@ -263,24 +266,28 @@ private List buildNotificationList( () -> new CustomException(ErrorCode.FAILED_TO_FIND_FCM_TOKEN)); FcmNotification newNotification = - FcmNotification.create( - FcmNotificationType.MISSION, title, message, member, null, false); + FcmNotification.create(notificationType, title, message, member, null, false); notifications.add(newNotification); } return notifications; } - public void sendAndNotifications(String title, String message, List tokens) { + public void sendAndNotifications( + String title, + String message, + List tokens, + FcmNotificationType notificationType) { List> batches = createBatches(tokens, BATCH_SIZE); - String deepLink = FcmNotification.generateDeepLink(FcmNotificationType.MISSION, null, null); + String deepLink = FcmNotification.generateDeepLink(notificationType, null, null); for (List batch : batches) { sqsMessageService.sendBatchMessages(batch, title, message, deepLink); } - List notifications = buildNotificationList(title, message, tokens); + List notifications = + buildNotificationList(title, message, tokens, notificationType); notificationRepository.saveAll(notifications); } diff --git a/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotificationType.java b/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotificationType.java index ba1fe118..61a179c9 100644 --- a/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotificationType.java +++ b/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotificationType.java @@ -7,7 +7,10 @@ @AllArgsConstructor public enum FcmNotificationType { MISSION("미션 알림"), - BOOSTER("부스터 알림"); + BOOSTER("부스터 알림"), + COMMENT("댓글 알림"), + RE_COMMENT("대댓글 알림"), + ; private final String value; } diff --git a/src/main/java/com/depromeet/stonebed/global/common/constants/FcmNotificationConstants.java b/src/main/java/com/depromeet/stonebed/global/common/constants/FcmNotificationConstants.java index 72ccc713..2662b122 100644 --- a/src/main/java/com/depromeet/stonebed/global/common/constants/FcmNotificationConstants.java +++ b/src/main/java/com/depromeet/stonebed/global/common/constants/FcmNotificationConstants.java @@ -11,7 +11,10 @@ public enum FcmNotificationConstants { POPULAR("인기쟁이", "부스터를 1000개를 달성했어요!"), SUPER_POPULAR("최고 인기 달성", "인기폭발! 부스터를 5000개 달성했어요!"), MISSION_START("미션 시작!", "새로운 미션을 지금 시작해보세요!"), - MISSION_REMINDER("미션 리마인드", "미션 종료까지 5시간 남았어요!"); + MISSION_REMINDER("미션 리마인드", "미션 종료까지 5시간 남았어요!"), + COMMENT("댓글 알림", "내 기록에 댓글이 달렸어요!"), + RE_COMMENT("대댓글 알림", "내 댓글에 답글이 달렸어요!"), + ; private final String title; private final String message; diff --git a/src/main/java/com/depromeet/stonebed/scheduler/fcm/FcmScheduler.java b/src/main/java/com/depromeet/stonebed/scheduler/fcm/FcmScheduler.java index 05dca145..f2459db1 100644 --- a/src/main/java/com/depromeet/stonebed/scheduler/fcm/FcmScheduler.java +++ b/src/main/java/com/depromeet/stonebed/scheduler/fcm/FcmScheduler.java @@ -2,6 +2,7 @@ import com.depromeet.stonebed.domain.fcm.application.FcmNotificationService; import com.depromeet.stonebed.domain.fcm.dao.FcmTokenRepository; +import com.depromeet.stonebed.domain.fcm.domain.FcmNotificationType; import com.depromeet.stonebed.domain.fcm.domain.FcmToken; import com.depromeet.stonebed.domain.member.domain.MemberStatus; import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordRepository; @@ -41,7 +42,8 @@ public void sendDailyNotification() { String message = notificationConstants.getMessage(); List tokens = fcmNotificationService.getAllTokens(); - fcmNotificationService.sendAndNotifications(title, message, tokens); + fcmNotificationService.sendAndNotifications( + title, message, tokens, FcmNotificationType.MISSION); log.info("모든 사용자에게 정규 알림 전송 및 저장 완료"); } @@ -54,7 +56,8 @@ public void sendReminderToIncompleteMissions() { String message = notificationConstants.getMessage(); List tokens = getIncompleteMissionTokens(); - fcmNotificationService.sendAndNotifications(title, message, tokens); + fcmNotificationService.sendAndNotifications( + title, message, tokens, FcmNotificationType.MISSION); log.info("미완료 미션 사용자에게 리마인더 전송 및 저장 완료. 총 토큰 수: {}", tokens.size()); } diff --git a/src/test/java/com/depromeet/stonebed/domain/fcm/application/FcmSchedulerTest.java b/src/test/java/com/depromeet/stonebed/domain/fcm/application/FcmSchedulerTest.java index 069f80e3..f4a4f2c9 100644 --- a/src/test/java/com/depromeet/stonebed/domain/fcm/application/FcmSchedulerTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/fcm/application/FcmSchedulerTest.java @@ -5,6 +5,7 @@ import com.depromeet.stonebed.FixtureMonkeySetUp; import com.depromeet.stonebed.domain.fcm.dao.FcmTokenRepository; +import com.depromeet.stonebed.domain.fcm.domain.FcmNotificationType; import com.depromeet.stonebed.domain.fcm.domain.FcmToken; import com.depromeet.stonebed.domain.member.domain.MemberStatus; import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordRepository; @@ -64,7 +65,11 @@ public class FcmSchedulerTest extends FixtureMonkeySetUp { // then verify(fcmNotificationService, times(1)) - .sendAndNotifications(eq("미션 시작!"), eq("새로운 미션을 지금 시작해보세요!"), eq(tokens)); + .sendAndNotifications( + eq("미션 시작!"), + eq("새로운 미션을 지금 시작해보세요!"), + eq(tokens), + eq(FcmNotificationType.MISSION)); } @Test @@ -104,6 +109,10 @@ public class FcmSchedulerTest extends FixtureMonkeySetUp { // then verify(fcmNotificationService, times(1)) - .sendAndNotifications(eq("미션 리마인드"), eq("미션 종료까지 5시간 남았어요!"), eq(tokens)); + .sendAndNotifications( + eq("미션 리마인드"), + eq("미션 종료까지 5시간 남았어요!"), + eq(tokens), + eq(FcmNotificationType.MISSION)); } } From 78e520df21ac1d94afafbffa19ab5bf60b268427 Mon Sep 17 00:00:00 2001 From: yb__char <68099546+char-yb@users.noreply.github.com> Date: Thu, 3 Oct 2024 23:17:51 +0900 Subject: [PATCH 11/56] =?UTF-8?q?feat:=20=ED=94=BC=EB=93=9C=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20count=20=EC=B6=94=EA=B0=80=20(#292)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 피드 댓글 count 추가 * fix: test 코드 필드 추가 --- .../domain/feed/dao/FeedRepositoryImpl.java | 2 ++ .../stonebed/domain/feed/dto/FindFeedDto.java | 14 +++++++++++--- .../feed/dto/response/FeedContentGetResponse.java | 2 ++ .../domain/missionRecord/domain/MissionRecord.java | 6 ++++++ .../domain/feed/application/FeedServiceTest.java | 5 +++++ 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryImpl.java b/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryImpl.java index e7822d5c..a0bc59da 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryImpl.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryImpl.java @@ -31,6 +31,8 @@ private JPAQuery getFeedBaseQuery(Long missionRecordId, Long member mission, missionRecord, member, + Expressions.asNumber(missionRecord.comments.size()) + .as("totalCommentCount"), Expressions.asNumber( missionRecordBoost.count.sumLong().coalesce(0L)) .as("totalBoostCount"))) diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/dto/FindFeedDto.java b/src/main/java/com/depromeet/stonebed/domain/feed/dto/FindFeedDto.java index 0e4a1d64..a8ab8407 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/dto/FindFeedDto.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/dto/FindFeedDto.java @@ -5,9 +5,17 @@ import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; public record FindFeedDto( - Mission mission, MissionRecord missionRecord, Member author, Long totalBoostCount) { + Mission mission, + MissionRecord missionRecord, + Member author, + Integer totalCommentCount, + Long totalBoostCount) { public static FindFeedDto from( - Mission mission, MissionRecord missionRecord, Member author, Long totalBoostCount) { - return new FindFeedDto(mission, missionRecord, author, totalBoostCount); + Mission mission, + MissionRecord missionRecord, + Member author, + Integer totalCommentCount, + Long totalBoostCount) { + return new FindFeedDto(mission, missionRecord, author, totalCommentCount, totalBoostCount); } } diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/FeedContentGetResponse.java b/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/FeedContentGetResponse.java index be95a1e5..b5165147 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/FeedContentGetResponse.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/FeedContentGetResponse.java @@ -17,6 +17,7 @@ public record FeedContentGetResponse( String missionRecordImageUrl, @Schema(description = "미션 기록 생성일") LocalDate createdDate, @Schema(description = "부스트") Long totalBoostCount, + @Schema(description = "댓글 수", example = "12") Integer totalCommentCount, @Schema(description = "미션 기록 컨텐츠") String content) { public static FeedContentGetResponse from(FindFeedDto missionRecord) { return new FeedContentGetResponse( @@ -30,6 +31,7 @@ public static FeedContentGetResponse from(FindFeedDto missionRecord) { missionRecord.missionRecord().getImageUrl(), missionRecord.missionRecord().getCreatedAt().toLocalDate(), missionRecord.totalBoostCount(), + missionRecord.totalCommentCount(), missionRecord.missionRecord().getContent()); } } diff --git a/src/main/java/com/depromeet/stonebed/domain/missionRecord/domain/MissionRecord.java b/src/main/java/com/depromeet/stonebed/domain/missionRecord/domain/MissionRecord.java index bd9be482..d96a9bd8 100644 --- a/src/main/java/com/depromeet/stonebed/domain/missionRecord/domain/MissionRecord.java +++ b/src/main/java/com/depromeet/stonebed/domain/missionRecord/domain/MissionRecord.java @@ -1,10 +1,13 @@ package com.depromeet.stonebed.domain.missionRecord.domain; +import com.depromeet.stonebed.domain.comment.domain.Comment; import com.depromeet.stonebed.domain.common.BaseTimeEntity; import com.depromeet.stonebed.domain.member.domain.Member; import com.depromeet.stonebed.domain.missionHistory.domain.MissionHistory; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -30,6 +33,9 @@ public class MissionRecord extends BaseTimeEntity { @JoinColumn(name = "member_id", nullable = false) private Member member; + @OneToMany(mappedBy = "missionRecord", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + @Schema(description = "미션 이미지 URL", example = "./missionRecord.jpg") @Column(name = "image_url") private String imageUrl; diff --git a/src/test/java/com/depromeet/stonebed/domain/feed/application/FeedServiceTest.java b/src/test/java/com/depromeet/stonebed/domain/feed/application/FeedServiceTest.java index 29589b7b..eab24e62 100644 --- a/src/test/java/com/depromeet/stonebed/domain/feed/application/FeedServiceTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/feed/application/FeedServiceTest.java @@ -28,6 +28,7 @@ class FeedServiceTest extends FixtureMonkeySetUp { int DEFAULT_LIMIT = 5; Long DEFAULT_TOTAL_BOOST_COUNT = 100L; + Integer DEFAULT_TOTAL_COMMENT_COUNT = 13; String DEFAULT_CURSOR = "5"; String INVALID_CURSOR = "2024-08-01"; @@ -46,6 +47,7 @@ class FeedServiceTest extends FixtureMonkeySetUp { fixtureMonkey.giveMeOne(Mission.class), fixtureMonkey.giveMeOne(MissionRecord.class), fixtureMonkey.giveMeOne(Member.class), + DEFAULT_TOTAL_COMMENT_COUNT, DEFAULT_TOTAL_BOOST_COUNT)); } @@ -75,6 +77,7 @@ class FeedServiceTest extends FixtureMonkeySetUp { fixtureMonkey.giveMeOne(Mission.class), fixtureMonkey.giveMeOne(MissionRecord.class), fixtureMonkey.giveMeOne(Member.class), + DEFAULT_TOTAL_COMMENT_COUNT, DEFAULT_TOTAL_BOOST_COUNT)); } @@ -87,6 +90,7 @@ class FeedServiceTest extends FixtureMonkeySetUp { fixtureMonkey.giveMeOne(Mission.class), fixtureMonkey.giveMeOne(MissionRecord.class), fixtureMonkey.giveMeOne(Member.class), + DEFAULT_TOTAL_COMMENT_COUNT, DEFAULT_TOTAL_BOOST_COUNT); when(feedRepository.getNextFeedContent( @@ -114,6 +118,7 @@ class FeedServiceTest extends FixtureMonkeySetUp { fixtureMonkey.giveMeOne(Mission.class), fixtureMonkey.giveMeOne(MissionRecord.class), fixtureMonkey.giveMeOne(Member.class), + DEFAULT_TOTAL_COMMENT_COUNT, DEFAULT_TOTAL_BOOST_COUNT)); } From 7090da0d7be5f12dc217537015599f63f5f437ed Mon Sep 17 00:00:00 2001 From: yb__char <68099546+char-yb@users.noreply.github.com> Date: Thu, 3 Oct 2024 23:18:03 +0900 Subject: [PATCH 12/56] =?UTF-8?q?feat:=20=EB=AF=B8=EC=85=98=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20image=20url=20=EC=BB=AC=EB=9F=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#294)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/depromeet/stonebed/domain/mission/domain/Mission.java | 3 +++ .../domain/missionRecord/dao/MissionRecordRepositoryImpl.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/mission/domain/Mission.java b/src/main/java/com/depromeet/stonebed/domain/mission/domain/Mission.java index 9d8a45dd..fc839fc9 100644 --- a/src/main/java/com/depromeet/stonebed/domain/mission/domain/Mission.java +++ b/src/main/java/com/depromeet/stonebed/domain/mission/domain/Mission.java @@ -34,6 +34,9 @@ public class Mission extends BaseTimeEntity { @Column(name = "raise_pet", nullable = false) private RaisePet raisePet; + @Column(name = "complete_image_url") + private String completeImageUrl; + @NotBlank @Size(max = 100) @Column(name = "complete_message", nullable = false) diff --git a/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordRepositoryImpl.java b/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordRepositoryImpl.java index 7f63a118..f11693b3 100644 --- a/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordRepositoryImpl.java +++ b/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordRepositoryImpl.java @@ -99,7 +99,7 @@ public List findAllTabMissionsByMemberAndStatus( missionRecord.imageUrl, missionRecord.status, mission.title.as("missionTitle"), - mission.illustrationUrl, + mission.completeImageUrl, missionRecord.content, completedAt)) .from(missionRecord) From 4bf1a31a6e67c580b25d89a5b8892cccccefec2f Mon Sep 17 00:00:00 2001 From: ybchar Date: Sun, 6 Oct 2024 23:54:49 +0900 Subject: [PATCH 13/56] =?UTF-8?q?fix:=20=ED=91=B8=EC=8B=9C=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentService.java | 79 ++++++++++++------- .../constants/FcmNotificationConstants.java | 5 +- 2 files changed, 55 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java index 44362665..22203cf1 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java @@ -45,9 +45,8 @@ public CommentCreateResponse createComment(CommentCreateRequest request) { final MissionRecord missionRecord = findMissionRecordById(request.recordId()); final Comment comment = createAndSaveComment(request, member, missionRecord); - final FcmNotificationConstants notificationType = getNotificationType(request); - sendCommentNotification(missionRecord, comment, notificationType); + sendCommentNotification(missionRecord, comment); return CommentCreateResponse.of(comment.getId()); } @@ -65,25 +64,58 @@ private Comment createAndSaveComment( return commentRepository.save(comment); } - private FcmNotificationConstants getNotificationType(CommentCreateRequest request) { - return request.parentId() == null - ? FcmNotificationConstants.COMMENT - : FcmNotificationConstants.RE_COMMENT; - } + private void sendCommentNotification(MissionRecord missionRecord, Comment comment) { + Set members = collectNotificationRecipients(missionRecord, comment); + // .forEach(member -> { + // String title = FcmNotificationConstants.COMMENT.getTitle(); + // String message = comment.getWriter().getProfile().getNickname() + + // FcmNotificationConstants.COMMENT.getMessage(); + // List tokens = retrieveFcmTokens(Set.of(member)); + // fcmNotificationService.sendAndNotifications(title, message, tokens, + // FcmNotificationType.COMMENT); + // }); + + // 게시물 작성자 + Member missionRecordOwner = missionRecord.getMember(); + Member commentWriter = comment.getWriter(); + + // 1. 게시물 작성자가 댓글 작성자가 아닐 때 알림 + if (!missionRecordOwner.equals(commentWriter)) { + String title = FcmNotificationConstants.COMMENT.getTitle(); + String message = + commentWriter.getProfile().getNickname() + + FcmNotificationConstants.COMMENT.getMessage(); + List tokens = retrieveFcmTokens(Set.of(missionRecordOwner)); + fcmNotificationService.sendAndNotifications( + title, message, tokens, FcmNotificationType.COMMENT); + } - private void sendCommentNotification( - MissionRecord missionRecord, - Comment comment, - FcmNotificationConstants commentNotification) { - Set notificationRecipients = collectNotificationRecipients(missionRecord, comment); - List tokens = retrieveFcmTokens(notificationRecipients); - - FcmNotificationType fcmNotificationType = getFcmNotificationType(commentNotification); - fcmNotificationService.sendAndNotifications( - commentNotification.getTitle(), - commentNotification.getMessage(), - tokens, - fcmNotificationType); + // 2. 대댓글 작성 시 부모 댓글 작성자에게 RE_COMMENT 알림 + if (comment.getParent() != null) { + Member parentCommentWriter = comment.getParent().getWriter(); + + // 부모 댓글 작성자가 대댓글 작성자가 아닌 경우에만 알림 전송 + if (!parentCommentWriter.equals(commentWriter)) { + String title = FcmNotificationConstants.RE_COMMENT.getTitle(); + String message = + commentWriter.getProfile().getNickname() + + FcmNotificationConstants.RE_COMMENT.getMessage(); + List tokens = retrieveFcmTokens(Set.of(parentCommentWriter)); + fcmNotificationService.sendAndNotifications( + title, message, tokens, FcmNotificationType.RE_COMMENT); + } + + // 게시물 작성자에게는 RECORD_RE_COMMENT 알림 + if (!missionRecordOwner.equals(commentWriter)) { + String title = FcmNotificationConstants.RECORD_RE_COMMENT.getTitle(); + String message = + commentWriter.getProfile().getNickname() + + FcmNotificationConstants.RECORD_RE_COMMENT.getMessage(); + List tokens = retrieveFcmTokens(Set.of(missionRecordOwner)); + fcmNotificationService.sendAndNotifications( + title, message, tokens, FcmNotificationType.RE_COMMENT); + } + } } private Set collectNotificationRecipients( @@ -113,13 +145,6 @@ private List retrieveFcmTokens(Set notificationRecipients) { .collect(Collectors.toList()); } - private FcmNotificationType getFcmNotificationType( - FcmNotificationConstants commentNotification) { - return FcmNotificationConstants.RE_COMMENT.equals(commentNotification) - ? FcmNotificationType.RE_COMMENT - : FcmNotificationType.COMMENT; - } - @Transactional(readOnly = true) public CommentFindResponse findCommentsByRecordId(Long recordId) { final MissionRecord missionRecord = findMissionRecordById(recordId); diff --git a/src/main/java/com/depromeet/stonebed/global/common/constants/FcmNotificationConstants.java b/src/main/java/com/depromeet/stonebed/global/common/constants/FcmNotificationConstants.java index 2662b122..b1150ec6 100644 --- a/src/main/java/com/depromeet/stonebed/global/common/constants/FcmNotificationConstants.java +++ b/src/main/java/com/depromeet/stonebed/global/common/constants/FcmNotificationConstants.java @@ -12,8 +12,9 @@ public enum FcmNotificationConstants { SUPER_POPULAR("최고 인기 달성", "인기폭발! 부스터를 5000개 달성했어요!"), MISSION_START("미션 시작!", "새로운 미션을 지금 시작해보세요!"), MISSION_REMINDER("미션 리마인드", "미션 종료까지 5시간 남았어요!"), - COMMENT("댓글 알림", "내 기록에 댓글이 달렸어요!"), - RE_COMMENT("대댓글 알림", "내 댓글에 답글이 달렸어요!"), + COMMENT("댓글 알림", "님이 내 게시물에 댓글을 남겼어요!"), + RE_COMMENT("댓글 알림", "님이 내 댓글에 대댓글을 남겼어요!"), + RECORD_RE_COMMENT("대댓글 알림", "님이 내 게시물에 대댓글을 남겼어요!"), ; private final String title; From a0e6655c2393aacacb81c5312b04b348fb0b3de9 Mon Sep 17 00:00:00 2001 From: ybchar Date: Fri, 11 Oct 2024 16:26:10 +0900 Subject: [PATCH 14/56] =?UTF-8?q?refactor:=20=EC=9E=90=EC=8B=9D=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=9E=91=EC=84=B1=EC=9E=90=20=ED=91=B8?= =?UTF-8?q?=EC=8B=9C=20=EC=95=8C=EB=A6=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentService.java | 70 ++++++++++--------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java index 22203cf1..948de1c4 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java @@ -65,29 +65,13 @@ private Comment createAndSaveComment( } private void sendCommentNotification(MissionRecord missionRecord, Comment comment) { - Set members = collectNotificationRecipients(missionRecord, comment); - // .forEach(member -> { - // String title = FcmNotificationConstants.COMMENT.getTitle(); - // String message = comment.getWriter().getProfile().getNickname() + - // FcmNotificationConstants.COMMENT.getMessage(); - // List tokens = retrieveFcmTokens(Set.of(member)); - // fcmNotificationService.sendAndNotifications(title, message, tokens, - // FcmNotificationType.COMMENT); - // }); - - // 게시물 작성자 Member missionRecordOwner = missionRecord.getMember(); Member commentWriter = comment.getWriter(); // 1. 게시물 작성자가 댓글 작성자가 아닐 때 알림 if (!missionRecordOwner.equals(commentWriter)) { - String title = FcmNotificationConstants.COMMENT.getTitle(); - String message = - commentWriter.getProfile().getNickname() - + FcmNotificationConstants.COMMENT.getMessage(); - List tokens = retrieveFcmTokens(Set.of(missionRecordOwner)); - fcmNotificationService.sendAndNotifications( - title, message, tokens, FcmNotificationType.COMMENT); + System.out.println("1. missionRecordOwner member= " + missionRecordOwner.getId()); + sendNotification(missionRecordOwner, FcmNotificationConstants.COMMENT, commentWriter); } // 2. 대댓글 작성 시 부모 댓글 작성자에게 RE_COMMENT 알림 @@ -96,28 +80,50 @@ private void sendCommentNotification(MissionRecord missionRecord, Comment commen // 부모 댓글 작성자가 대댓글 작성자가 아닌 경우에만 알림 전송 if (!parentCommentWriter.equals(commentWriter)) { - String title = FcmNotificationConstants.RE_COMMENT.getTitle(); - String message = - commentWriter.getProfile().getNickname() - + FcmNotificationConstants.RE_COMMENT.getMessage(); - List tokens = retrieveFcmTokens(Set.of(parentCommentWriter)); - fcmNotificationService.sendAndNotifications( - title, message, tokens, FcmNotificationType.RE_COMMENT); + System.out.println("2. parentCommentWriter member= " + parentCommentWriter.getId()); + sendNotification( + parentCommentWriter, FcmNotificationConstants.RE_COMMENT, commentWriter); } // 게시물 작성자에게는 RECORD_RE_COMMENT 알림 if (!missionRecordOwner.equals(commentWriter)) { - String title = FcmNotificationConstants.RECORD_RE_COMMENT.getTitle(); - String message = - commentWriter.getProfile().getNickname() - + FcmNotificationConstants.RECORD_RE_COMMENT.getMessage(); - List tokens = retrieveFcmTokens(Set.of(missionRecordOwner)); - fcmNotificationService.sendAndNotifications( - title, message, tokens, FcmNotificationType.RE_COMMENT); + System.out.println("3. missionRecordOwner member= " + missionRecordOwner.getId()); + sendNotification( + missionRecordOwner, + FcmNotificationConstants.RECORD_RE_COMMENT, + commentWriter); } + + // Collect unique recipients for notifications + Set uniqueRecipients = new HashSet<>(); + collectUniqueRecipients(comment.getParent(), uniqueRecipients); + uniqueRecipients.remove(commentWriter); // Remove the comment writer from recipients + + // Send notifications to unique recipients + for (Member recipient : uniqueRecipients) { + System.out.println("4. recipient member= " + recipient.getId()); + sendNotification(recipient, FcmNotificationConstants.RE_COMMENT, commentWriter); + } + } + } + + private void collectUniqueRecipients(Comment comment, Set uniqueRecipients) { + for (Comment reply : comment.getReplyComments()) { + uniqueRecipients.add(reply.getWriter()); + collectUniqueRecipients( + reply, uniqueRecipients); // Recursively collect from nested replies } } + private void sendNotification( + Member recipient, FcmNotificationConstants notificationType, Member commentWriter) { + String title = notificationType.getTitle(); + String message = commentWriter.getProfile().getNickname() + notificationType.getMessage(); + List tokens = retrieveFcmTokens(Set.of(recipient)); + fcmNotificationService.sendAndNotifications( + title, message, tokens, FcmNotificationType.valueOf(notificationType.name())); + } + private Set collectNotificationRecipients( MissionRecord missionRecord, Comment comment) { Set notificationRecipients = new HashSet<>(); From 7a83500b6a761ff15e9cd97925387c4218cc1662 Mon Sep 17 00:00:00 2001 From: ybchar Date: Fri, 11 Oct 2024 16:31:09 +0900 Subject: [PATCH 15/56] =?UTF-8?q?refactor:=20=EC=9E=90=EC=8B=9D=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=9E=91=EC=84=B1=EC=9E=90=20=ED=91=B8?= =?UTF-8?q?=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20=EB=B0=8F=20=EC=B6=9C=EB=A0=A5?= =?UTF-8?q?=EB=AC=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentService.java | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java index 948de1c4..66730446 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java @@ -70,7 +70,6 @@ private void sendCommentNotification(MissionRecord missionRecord, Comment commen // 1. 게시물 작성자가 댓글 작성자가 아닐 때 알림 if (!missionRecordOwner.equals(commentWriter)) { - System.out.println("1. missionRecordOwner member= " + missionRecordOwner.getId()); sendNotification(missionRecordOwner, FcmNotificationConstants.COMMENT, commentWriter); } @@ -80,14 +79,12 @@ private void sendCommentNotification(MissionRecord missionRecord, Comment commen // 부모 댓글 작성자가 대댓글 작성자가 아닌 경우에만 알림 전송 if (!parentCommentWriter.equals(commentWriter)) { - System.out.println("2. parentCommentWriter member= " + parentCommentWriter.getId()); sendNotification( parentCommentWriter, FcmNotificationConstants.RE_COMMENT, commentWriter); } // 게시물 작성자에게는 RECORD_RE_COMMENT 알림 if (!missionRecordOwner.equals(commentWriter)) { - System.out.println("3. missionRecordOwner member= " + missionRecordOwner.getId()); sendNotification( missionRecordOwner, FcmNotificationConstants.RECORD_RE_COMMENT, @@ -95,26 +92,15 @@ private void sendCommentNotification(MissionRecord missionRecord, Comment commen } // Collect unique recipients for notifications - Set uniqueRecipients = new HashSet<>(); - collectUniqueRecipients(comment.getParent(), uniqueRecipients); - uniqueRecipients.remove(commentWriter); // Remove the comment writer from recipients + Set commentRecipients = collectNotificationRecipients(missionRecord, comment); // Send notifications to unique recipients - for (Member recipient : uniqueRecipients) { - System.out.println("4. recipient member= " + recipient.getId()); + for (Member recipient : commentRecipients) { sendNotification(recipient, FcmNotificationConstants.RE_COMMENT, commentWriter); } } } - private void collectUniqueRecipients(Comment comment, Set uniqueRecipients) { - for (Comment reply : comment.getReplyComments()) { - uniqueRecipients.add(reply.getWriter()); - collectUniqueRecipients( - reply, uniqueRecipients); // Recursively collect from nested replies - } - } - private void sendNotification( Member recipient, FcmNotificationConstants notificationType, Member commentWriter) { String title = notificationType.getTitle(); From 1df81e2f9cc670e8fe67f942284a199b1ba8b5b5 Mon Sep 17 00:00:00 2001 From: ybchar Date: Fri, 11 Oct 2024 19:29:27 +0900 Subject: [PATCH 16/56] =?UTF-8?q?feat:=20=EB=8B=A8=EC=9D=BC=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=20=EC=A1=B0=ED=9A=8C=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/feed/api/FeedController.java | 11 ++- .../domain/feed/application/FeedService.java | 11 ++- .../domain/feed/dao/FeedRepositoryCustom.java | 2 + .../domain/feed/dao/FeedRepositoryImpl.java | 74 +++++++++++-------- .../stonebed/global/error/ErrorCode.java | 5 +- .../feed/application/FeedServiceTest.java | 8 +- 6 files changed, 73 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/api/FeedController.java b/src/main/java/com/depromeet/stonebed/domain/feed/api/FeedController.java index 277c464e..67f356a2 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/api/FeedController.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/api/FeedController.java @@ -2,6 +2,7 @@ import com.depromeet.stonebed.domain.feed.application.FeedService; import com.depromeet.stonebed.domain.feed.dto.request.FeedGetRequest; +import com.depromeet.stonebed.domain.feed.dto.response.FeedContentGetResponse; import com.depromeet.stonebed.domain.feed.dto.response.FeedGetResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -18,7 +19,13 @@ public class FeedController { @Operation(summary = "피드 조회", description = "내 피드를 조회하는 API입니다.") @GetMapping - public FeedGetResponse getFeed(@Valid FeedGetRequest request) { - return feedService.getFeed(request); + public FeedGetResponse feedFind(@Valid FeedGetRequest request) { + return feedService.findFeed(request); + } + + @Operation(summary = "단일 피드 조회", description = "단일 피드를 조회하는 API입니다.") + @GetMapping("/{recordId}") + public FeedContentGetResponse feedFindOne(@PathVariable Long recordId) { + return feedService.findFeedOne(recordId); } } diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/application/FeedService.java b/src/main/java/com/depromeet/stonebed/domain/feed/application/FeedService.java index 9335c41a..371ad318 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/application/FeedService.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/application/FeedService.java @@ -19,7 +19,7 @@ public class FeedService { private final FeedRepository feedRepository; @Transactional(readOnly = true) - public FeedGetResponse getFeed(FeedGetRequest request) { + public FeedGetResponse findFeed(FeedGetRequest request) { List feeds = getFeeds(request.cursor(), request.memberId(), request.limit()); List feedContentList = @@ -74,4 +74,13 @@ private Long parseCursor(String cursor) { throw new CustomException(ErrorCode.INVALID_CURSOR_FORMAT); } } + + @Transactional(readOnly = true) + public FeedContentGetResponse findFeedOne(Long recordId) { + FindFeedDto feedOne = feedRepository.findOneFeedContent(recordId); + if (feedOne == null) { + throw new CustomException(ErrorCode.FEED_NOT_FOUND); + } + return FeedContentGetResponse.from(feedOne); + } } diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryCustom.java b/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryCustom.java index 3d38dd73..7c889af3 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryCustom.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryCustom.java @@ -7,4 +7,6 @@ public interface FeedRepositoryCustom { List getFeedContentsUsingCursor(Long missionRecordId, Long memberId, int limit); FindFeedDto getNextFeedContent(Long missionRecordId, Long memberId); + + FindFeedDto findOneFeedContent(Long recordId); } diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryImpl.java b/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryImpl.java index a0bc59da..c5edcf00 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryImpl.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryImpl.java @@ -23,31 +23,30 @@ public class FeedRepositoryImpl implements FeedRepositoryCustom { private final JPAQueryFactory queryFactory; + @Override + public List getFeedContentsUsingCursor( + Long missionRecordId, Long memberId, int limit) { + return getFeedBaseQuery(missionRecordId, memberId).limit(limit).fetch(); + } + + @Override + public FindFeedDto getNextFeedContent(Long missionRecordId, Long memberId) { + return getFeedBaseQuery(missionRecordId, memberId).fetchFirst(); + } + + @Override + public FindFeedDto findOneFeedContent(Long recordId) { + // findOneFeedContent는 eqMissionRecordId 조건만 사용 + return applyJoinsAndConditions(getBaseSelectQuery()) + .where(eqMissionRecordId(recordId)) + .orderBy(missionRecord.updatedAt.desc()) + .fetchOne(); + } + private JPAQuery getFeedBaseQuery(Long missionRecordId, Long memberId) { - return queryFactory - .select( - Projections.constructor( - FindFeedDto.class, - mission, - missionRecord, - member, - Expressions.asNumber(missionRecord.comments.size()) - .as("totalCommentCount"), - Expressions.asNumber( - missionRecordBoost.count.sumLong().coalesce(0L)) - .as("totalBoostCount"))) - .from(missionRecord) - .leftJoin(missionRecordBoost) - .on(missionRecordBoost.missionRecord.eq(missionRecord)) - .leftJoin(member) - .on(missionRecord.member.eq(member)) - .leftJoin(missionHistory) - .on(missionRecord.missionHistory.eq(missionHistory)) - .leftJoin(mission) - .on(missionHistory.mission.eq(mission)) + return applyJoinsAndConditions(getBaseSelectQuery()) .where( missionRecord.status.eq(MissionRecordStatus.COMPLETED), - // TODO: 추후 피드 Request에 파라미터 전달받을 계획 missionRecord.display.in(MissionRecordDisplay.PUBLIC), ltMissionRecordId(missionRecordId), eqMemberId(memberId)) @@ -55,15 +54,28 @@ private JPAQuery getFeedBaseQuery(Long missionRecordId, Long member .orderBy(missionRecord.updatedAt.desc()); } - @Override - public List getFeedContentsUsingCursor( - Long missionRecordId, Long memberId, int limit) { - return getFeedBaseQuery(missionRecordId, memberId).limit(limit).fetch(); + private JPAQuery getBaseSelectQuery() { + return queryFactory.select( + Projections.constructor( + FindFeedDto.class, + mission, + missionRecord, + member, + Expressions.asNumber(missionRecord.comments.size()).as("totalCommentCount"), + Expressions.asNumber(missionRecordBoost.count.sumLong().coalesce(0L)) + .as("totalBoostCount"))); } - @Override - public FindFeedDto getNextFeedContent(Long missionRecordId, Long memberId) { - return getFeedBaseQuery(missionRecordId, memberId).fetchFirst(); + private JPAQuery applyJoinsAndConditions(JPAQuery query) { + return query.from(missionRecord) + .leftJoin(missionRecordBoost) + .on(missionRecordBoost.missionRecord.eq(missionRecord)) + .leftJoin(member) + .on(missionRecord.member.eq(member)) + .leftJoin(missionHistory) + .on(missionRecord.missionHistory.eq(missionHistory)) + .leftJoin(mission) + .on(missionHistory.mission.eq(mission)); } private BooleanExpression ltMissionRecordId(Long missionRecordId) { @@ -73,4 +85,8 @@ private BooleanExpression ltMissionRecordId(Long missionRecordId) { private BooleanExpression eqMemberId(Long memberId) { return memberId != null ? missionRecord.member.id.eq(memberId) : null; } + + private BooleanExpression eqMissionRecordId(Long missionRecordId) { + return missionRecordId != null ? missionRecord.id.eq(missionRecordId) : null; + } } diff --git a/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java b/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java index c34e0efe..b285a982 100644 --- a/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java +++ b/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java @@ -37,6 +37,7 @@ public enum ErrorCode { MISSION_RECORD_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 미션 기록을 찾을 수 없습니다."), NO_AVAILABLE_TODAY_MISSION(HttpStatus.INTERNAL_SERVER_ERROR, "할당 가능한 오늘의 미션이 없습니다."), DUPLICATE_MISSION_RECORD(HttpStatus.BAD_REQUEST, "오늘 완료한 미션이 존재합니다."), + FEED_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 피드를 찾을 수 없습니다."), // boost BOOST_UNAVAILABLE_MY_FEED(HttpStatus.BAD_REQUEST, "내 피드에는 부스트를 추가할 수 없습니다."), @@ -65,8 +66,8 @@ public enum ErrorCode { DISCORD_NOTIFICATION_FAILED(HttpStatus.BAD_REQUEST, "디스코드 알림 전송이 실패했습니다."), // comment - COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 댓글을 찾을 수 없습니다."); - + COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 댓글을 찾을 수 없습니다."), + ; private final HttpStatus httpStatus; private final String message; } diff --git a/src/test/java/com/depromeet/stonebed/domain/feed/application/FeedServiceTest.java b/src/test/java/com/depromeet/stonebed/domain/feed/application/FeedServiceTest.java index eab24e62..41dff0eb 100644 --- a/src/test/java/com/depromeet/stonebed/domain/feed/application/FeedServiceTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/feed/application/FeedServiceTest.java @@ -59,7 +59,7 @@ class FeedServiceTest extends FixtureMonkeySetUp { // When FeedGetResponse feedGetResponse = - feedService.getFeed(new FeedGetRequest(null, null, DEFAULT_LIMIT)); + feedService.findFeed(new FeedGetRequest(null, null, DEFAULT_LIMIT)); // Then assertThat(feedGetResponse.list().size()).isEqualTo(5); @@ -99,7 +99,7 @@ class FeedServiceTest extends FixtureMonkeySetUp { // When FeedGetResponse feedGetResponse = - feedService.getFeed(new FeedGetRequest(DEFAULT_CURSOR, null, DEFAULT_LIMIT)); + feedService.findFeed(new FeedGetRequest(DEFAULT_CURSOR, null, DEFAULT_LIMIT)); // Then assertThat(feedGetResponse.list().size()).isEqualTo(5); @@ -131,7 +131,7 @@ class FeedServiceTest extends FixtureMonkeySetUp { // When FeedGetResponse feedGetResponse = - feedService.getFeed(new FeedGetRequest(DEFAULT_CURSOR, null, DEFAULT_LIMIT)); + feedService.findFeed(new FeedGetRequest(DEFAULT_CURSOR, null, DEFAULT_LIMIT)); // Then assertThat(feedGetResponse.list().size()).isEqualTo(3); @@ -145,7 +145,7 @@ class FeedServiceTest extends FixtureMonkeySetUp { assertThrows( CustomException.class, () -> - feedService.getFeed( + feedService.findFeed( new FeedGetRequest(INVALID_CURSOR, null, DEFAULT_LIMIT))); // Then: 에러코드 검증 From 0a6d93bf0245f8bea4ee55092c16ec059260c550 Mon Sep 17 00:00:00 2001 From: ybchar Date: Fri, 11 Oct 2024 20:27:41 +0900 Subject: [PATCH 17/56] =?UTF-8?q?fix:=20=EB=8C=93=EA=B8=80=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20targetId=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentService.java | 55 +++++++++++++------ .../application/FcmNotificationService.java | 9 ++- .../domain/fcm/domain/FcmNotification.java | 2 + .../stonebed/scheduler/fcm/FcmScheduler.java | 4 +- .../fcm/application/FcmSchedulerTest.java | 2 + 5 files changed, 49 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java index 66730446..e5e64a63 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java @@ -50,6 +50,19 @@ public CommentCreateResponse createComment(CommentCreateRequest request) { return CommentCreateResponse.of(comment.getId()); } + @Transactional(readOnly = true) + public CommentFindResponse findCommentsByRecordId(Long recordId) { + final MissionRecord missionRecord = findMissionRecordById(recordId); + final List allComments = + commentRepository.findAllCommentsByMissionRecord(missionRecord); + + Map> commentsByParentId = groupCommentsByParentId(allComments); + List rootResponses = + convertToCommentFindOneResponses(commentsByParentId); + + return CommentFindResponse.of(rootResponses); + } + private Comment createAndSaveComment( CommentCreateRequest request, Member member, MissionRecord missionRecord) { final Comment comment = @@ -70,7 +83,11 @@ private void sendCommentNotification(MissionRecord missionRecord, Comment commen // 1. 게시물 작성자가 댓글 작성자가 아닐 때 알림 if (!missionRecordOwner.equals(commentWriter)) { - sendNotification(missionRecordOwner, FcmNotificationConstants.COMMENT, commentWriter); + sendNotification( + missionRecordOwner, + FcmNotificationConstants.COMMENT, + missionRecord, + commentWriter); } // 2. 대댓글 작성 시 부모 댓글 작성자에게 RE_COMMENT 알림 @@ -80,7 +97,10 @@ private void sendCommentNotification(MissionRecord missionRecord, Comment commen // 부모 댓글 작성자가 대댓글 작성자가 아닌 경우에만 알림 전송 if (!parentCommentWriter.equals(commentWriter)) { sendNotification( - parentCommentWriter, FcmNotificationConstants.RE_COMMENT, commentWriter); + parentCommentWriter, + FcmNotificationConstants.RE_COMMENT, + missionRecord, + commentWriter); } // 게시물 작성자에게는 RECORD_RE_COMMENT 알림 @@ -88,6 +108,7 @@ private void sendCommentNotification(MissionRecord missionRecord, Comment commen sendNotification( missionRecordOwner, FcmNotificationConstants.RECORD_RE_COMMENT, + missionRecord, commentWriter); } @@ -96,18 +117,29 @@ private void sendCommentNotification(MissionRecord missionRecord, Comment commen // Send notifications to unique recipients for (Member recipient : commentRecipients) { - sendNotification(recipient, FcmNotificationConstants.RE_COMMENT, commentWriter); + sendNotification( + recipient, + FcmNotificationConstants.RE_COMMENT, + missionRecord, + commentWriter); } } } private void sendNotification( - Member recipient, FcmNotificationConstants notificationType, Member commentWriter) { + Member recipient, + FcmNotificationConstants notificationType, + MissionRecord missionRecord, + Member commentWriter) { String title = notificationType.getTitle(); String message = commentWriter.getProfile().getNickname() + notificationType.getMessage(); List tokens = retrieveFcmTokens(Set.of(recipient)); fcmNotificationService.sendAndNotifications( - title, message, tokens, FcmNotificationType.valueOf(notificationType.name())); + title, + message, + tokens, + missionRecord.getId(), + FcmNotificationType.valueOf(notificationType.name())); } private Set collectNotificationRecipients( @@ -137,19 +169,6 @@ private List retrieveFcmTokens(Set notificationRecipients) { .collect(Collectors.toList()); } - @Transactional(readOnly = true) - public CommentFindResponse findCommentsByRecordId(Long recordId) { - final MissionRecord missionRecord = findMissionRecordById(recordId); - final List allComments = - commentRepository.findAllCommentsByMissionRecord(missionRecord); - - Map> commentsByParentId = groupCommentsByParentId(allComments); - List rootResponses = - convertToCommentFindOneResponses(commentsByParentId); - - return CommentFindResponse.of(rootResponses); - } - private Map> groupCommentsByParentId(List allComments) { return allComments.stream() .collect( diff --git a/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java b/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java index f78843f2..6ed35700 100644 --- a/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java +++ b/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java @@ -254,6 +254,7 @@ private List buildNotificationList( String title, String message, List tokens, + Long targetId, FcmNotificationType notificationType) { List notifications = new ArrayList<>(); @@ -266,7 +267,8 @@ private List buildNotificationList( () -> new CustomException(ErrorCode.FAILED_TO_FIND_FCM_TOKEN)); FcmNotification newNotification = - FcmNotification.create(notificationType, title, message, member, null, false); + FcmNotification.create( + notificationType, title, message, member, targetId, false); notifications.add(newNotification); } @@ -277,17 +279,18 @@ public void sendAndNotifications( String title, String message, List tokens, + Long targetId, FcmNotificationType notificationType) { List> batches = createBatches(tokens, BATCH_SIZE); - String deepLink = FcmNotification.generateDeepLink(notificationType, null, null); + String deepLink = FcmNotification.generateDeepLink(notificationType, targetId, null); for (List batch : batches) { sqsMessageService.sendBatchMessages(batch, title, message, deepLink); } List notifications = - buildNotificationList(title, message, tokens, notificationType); + buildNotificationList(title, message, tokens, targetId, notificationType); notificationRepository.saveAll(notifications); } diff --git a/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotification.java b/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotification.java index 40606464..b6bbe134 100644 --- a/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotification.java +++ b/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotification.java @@ -76,6 +76,8 @@ public static String generateDeepLink( return DEEP_LINK_PREFIX + "mission"; } else if (type == FcmNotificationType.BOOSTER) { return DEEP_LINK_PREFIX + "boost?id=" + targetId + "&type=" + boostCount; + } else if (type == FcmNotificationType.COMMENT || type == FcmNotificationType.RE_COMMENT) { + return DEEP_LINK_PREFIX + "comment?id=" + targetId; } return null; } diff --git a/src/main/java/com/depromeet/stonebed/scheduler/fcm/FcmScheduler.java b/src/main/java/com/depromeet/stonebed/scheduler/fcm/FcmScheduler.java index f2459db1..65210a16 100644 --- a/src/main/java/com/depromeet/stonebed/scheduler/fcm/FcmScheduler.java +++ b/src/main/java/com/depromeet/stonebed/scheduler/fcm/FcmScheduler.java @@ -43,7 +43,7 @@ public void sendDailyNotification() { List tokens = fcmNotificationService.getAllTokens(); fcmNotificationService.sendAndNotifications( - title, message, tokens, FcmNotificationType.MISSION); + title, message, tokens, null, FcmNotificationType.MISSION); log.info("모든 사용자에게 정규 알림 전송 및 저장 완료"); } @@ -57,7 +57,7 @@ public void sendReminderToIncompleteMissions() { List tokens = getIncompleteMissionTokens(); fcmNotificationService.sendAndNotifications( - title, message, tokens, FcmNotificationType.MISSION); + title, message, tokens, null, FcmNotificationType.MISSION); log.info("미완료 미션 사용자에게 리마인더 전송 및 저장 완료. 총 토큰 수: {}", tokens.size()); } diff --git a/src/test/java/com/depromeet/stonebed/domain/fcm/application/FcmSchedulerTest.java b/src/test/java/com/depromeet/stonebed/domain/fcm/application/FcmSchedulerTest.java index f4a4f2c9..fcb6940d 100644 --- a/src/test/java/com/depromeet/stonebed/domain/fcm/application/FcmSchedulerTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/fcm/application/FcmSchedulerTest.java @@ -69,6 +69,7 @@ public class FcmSchedulerTest extends FixtureMonkeySetUp { eq("미션 시작!"), eq("새로운 미션을 지금 시작해보세요!"), eq(tokens), + eq(null), eq(FcmNotificationType.MISSION)); } @@ -113,6 +114,7 @@ public class FcmSchedulerTest extends FixtureMonkeySetUp { eq("미션 리마인드"), eq("미션 종료까지 5시간 남았어요!"), eq(tokens), + eq(null), eq(FcmNotificationType.MISSION)); } } From 6945ecf36422676dedb2192f66bfb54a836a8660 Mon Sep 17 00:00:00 2001 From: ybchar Date: Fri, 11 Oct 2024 20:49:55 +0900 Subject: [PATCH 18/56] =?UTF-8?q?fix:=20=EC=95=8C=EB=A6=BC=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=95=84=ED=84=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../discord/application/DiscordNotificationService.java | 3 ++- .../domain/fcm/application/FcmNotificationService.java | 9 ++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/discord/application/DiscordNotificationService.java b/src/main/java/com/depromeet/stonebed/domain/discord/application/DiscordNotificationService.java index 43f1452c..363d2d90 100644 --- a/src/main/java/com/depromeet/stonebed/domain/discord/application/DiscordNotificationService.java +++ b/src/main/java/com/depromeet/stonebed/domain/discord/application/DiscordNotificationService.java @@ -5,6 +5,7 @@ import com.depromeet.stonebed.infra.properties.DiscordProperties; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; @@ -42,7 +43,7 @@ public void sendDiscordMessage(String message) { ErrorCode.DISCORD_NOTIFICATION_FAILED); } log.info("Discord 알림 전송 성공: {}", message); - return response.bodyTo(String.class); + return Objects.requireNonNull(response.bodyTo(String.class)); }); } catch (Exception e) { diff --git a/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java b/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java index 6ed35700..8471b0ae 100644 --- a/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java +++ b/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java @@ -90,14 +90,9 @@ private Pageable createPageable(int limit) { } private List convertToNotificationDto(List notifications) { - List targetIds = - notifications.stream() - .filter( - notification -> - notification.getType() == FcmNotificationType.BOOSTER) - .map(FcmNotification::getTargetId) - .toList(); + List targetIds = notifications.stream().map(FcmNotification::getTargetId).toList(); + System.out.println("targetIds: " + targetIds); Map missionRecordMap = missionRecordRepository.findByIdIn(targetIds).stream() .collect( From d8c0cb4c4d6965e70fe8e00b132a0b7269b24d34 Mon Sep 17 00:00:00 2001 From: ybchar Date: Fri, 11 Oct 2024 20:51:26 +0900 Subject: [PATCH 19/56] =?UTF-8?q?fix:=20=EC=B6=9C=EB=A0=A5=EB=AC=B8=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stonebed/domain/fcm/application/FcmNotificationService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java b/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java index 8471b0ae..1741f0a4 100644 --- a/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java +++ b/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java @@ -92,7 +92,6 @@ private Pageable createPageable(int limit) { private List convertToNotificationDto(List notifications) { List targetIds = notifications.stream().map(FcmNotification::getTargetId).toList(); - System.out.println("targetIds: " + targetIds); Map missionRecordMap = missionRecordRepository.findByIdIn(targetIds).stream() .collect( From f8951d418a11e38509bc499bd081c1871c634da4 Mon Sep 17 00:00:00 2001 From: ybchar Date: Mon, 14 Oct 2024 22:09:36 +0900 Subject: [PATCH 20/56] =?UTF-8?q?hotfix:=20FcmNotificationType=20value=20n?= =?UTF-8?q?ame=20=EC=A1=B0=EA=B1=B4=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stonebed/domain/comment/application/CommentService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java index e5e64a63..b0feecd2 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java @@ -134,12 +134,16 @@ private void sendNotification( String title = notificationType.getTitle(); String message = commentWriter.getProfile().getNickname() + notificationType.getMessage(); List tokens = retrieveFcmTokens(Set.of(recipient)); + String notificationTypeName = notificationType.name(); + if (notificationType.name().equals(FcmNotificationConstants.RECORD_RE_COMMENT.name())) { + notificationTypeName = FcmNotificationConstants.RE_COMMENT.name(); + } fcmNotificationService.sendAndNotifications( title, message, tokens, missionRecord.getId(), - FcmNotificationType.valueOf(notificationType.name())); + FcmNotificationType.valueOf(notificationTypeName)); } private Set collectNotificationRecipients( From b78ad70c50a0511e116b913837bf3aa74bd4f503 Mon Sep 17 00:00:00 2001 From: ybchar Date: Mon, 14 Oct 2024 22:56:09 +0900 Subject: [PATCH 21/56] =?UTF-8?q?feat:=20v2=20=EC=97=94=EB=93=9C=20?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EB=B0=8F=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/feed/api/FeedController.java | 11 +++++- .../domain/feed/application/FeedService.java | 21 ++++++++++- .../{ => v1}/FeedContentGetResponse.java | 2 +- .../response/{ => v1}/FeedGetResponse.java | 2 +- .../response/v2/FeedContentGetResponseV2.java | 37 +++++++++++++++++++ .../dto/response/v2/FeedGetResponseV2.java | 12 ++++++ .../feed/application/FeedServiceTest.java | 2 +- 7 files changed, 80 insertions(+), 7 deletions(-) rename src/main/java/com/depromeet/stonebed/domain/feed/dto/response/{ => v1}/FeedContentGetResponse.java (97%) rename src/main/java/com/depromeet/stonebed/domain/feed/dto/response/{ => v1}/FeedGetResponse.java (86%) create mode 100644 src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v2/FeedContentGetResponseV2.java create mode 100644 src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v2/FeedGetResponseV2.java diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/api/FeedController.java b/src/main/java/com/depromeet/stonebed/domain/feed/api/FeedController.java index 67f356a2..6179ab1e 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/api/FeedController.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/api/FeedController.java @@ -2,8 +2,9 @@ import com.depromeet.stonebed.domain.feed.application.FeedService; import com.depromeet.stonebed.domain.feed.dto.request.FeedGetRequest; -import com.depromeet.stonebed.domain.feed.dto.response.FeedContentGetResponse; -import com.depromeet.stonebed.domain.feed.dto.response.FeedGetResponse; +import com.depromeet.stonebed.domain.feed.dto.response.v1.FeedContentGetResponse; +import com.depromeet.stonebed.domain.feed.dto.response.v1.FeedGetResponse; +import com.depromeet.stonebed.domain.feed.dto.response.v2.FeedGetResponseV2; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -23,6 +24,12 @@ public FeedGetResponse feedFind(@Valid FeedGetRequest request) { return feedService.findFeed(request); } + @Operation(summary = "피드 조회", description = "내 피드를 조회하는 API입니다.") + @GetMapping("/v2") + public FeedGetResponseV2 feedFindV2(@Valid FeedGetRequest request) { + return feedService.findFeedV2(request); + } + @Operation(summary = "단일 피드 조회", description = "단일 피드를 조회하는 API입니다.") @GetMapping("/{recordId}") public FeedContentGetResponse feedFindOne(@PathVariable Long recordId) { diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/application/FeedService.java b/src/main/java/com/depromeet/stonebed/domain/feed/application/FeedService.java index 371ad318..47a2d22c 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/application/FeedService.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/application/FeedService.java @@ -3,8 +3,10 @@ import com.depromeet.stonebed.domain.feed.dao.FeedRepository; import com.depromeet.stonebed.domain.feed.dto.FindFeedDto; import com.depromeet.stonebed.domain.feed.dto.request.FeedGetRequest; -import com.depromeet.stonebed.domain.feed.dto.response.FeedContentGetResponse; -import com.depromeet.stonebed.domain.feed.dto.response.FeedGetResponse; +import com.depromeet.stonebed.domain.feed.dto.response.v1.FeedContentGetResponse; +import com.depromeet.stonebed.domain.feed.dto.response.v1.FeedGetResponse; +import com.depromeet.stonebed.domain.feed.dto.response.v2.FeedContentGetResponseV2; +import com.depromeet.stonebed.domain.feed.dto.response.v2.FeedGetResponseV2; import com.depromeet.stonebed.global.error.ErrorCode; import com.depromeet.stonebed.global.error.exception.CustomException; import java.util.List; @@ -83,4 +85,19 @@ public FeedContentGetResponse findFeedOne(Long recordId) { } return FeedContentGetResponse.from(feedOne); } + + public FeedGetResponseV2 findFeedV2(FeedGetRequest request) { + List feeds = getFeeds(request.cursor(), request.memberId(), request.limit()); + + List feedContentList = + feeds.stream().map(FeedContentGetResponseV2::from).toList(); + + String nextCursor = getNextCursor(feeds, request.limit()); + + if (nextFeedNotExists(feeds, request.memberId())) { + nextCursor = null; + } + + return FeedGetResponseV2.from(feedContentList, nextCursor); + } } diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/FeedContentGetResponse.java b/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v1/FeedContentGetResponse.java similarity index 97% rename from src/main/java/com/depromeet/stonebed/domain/feed/dto/response/FeedContentGetResponse.java rename to src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v1/FeedContentGetResponse.java index b5165147..e572059e 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/FeedContentGetResponse.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v1/FeedContentGetResponse.java @@ -1,4 +1,4 @@ -package com.depromeet.stonebed.domain.feed.dto.response; +package com.depromeet.stonebed.domain.feed.dto.response.v1; import com.depromeet.stonebed.domain.feed.dto.FindFeedDto; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/FeedGetResponse.java b/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v1/FeedGetResponse.java similarity index 86% rename from src/main/java/com/depromeet/stonebed/domain/feed/dto/response/FeedGetResponse.java rename to src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v1/FeedGetResponse.java index 527c47a6..cfd08916 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/FeedGetResponse.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v1/FeedGetResponse.java @@ -1,4 +1,4 @@ -package com.depromeet.stonebed.domain.feed.dto.response; +package com.depromeet.stonebed.domain.feed.dto.response.v1; import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v2/FeedContentGetResponseV2.java b/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v2/FeedContentGetResponseV2.java new file mode 100644 index 00000000..01bbf99a --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v2/FeedContentGetResponseV2.java @@ -0,0 +1,37 @@ +package com.depromeet.stonebed.domain.feed.dto.response.v2; + +import com.depromeet.stonebed.domain.feed.dto.FindFeedDto; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; + +public record FeedContentGetResponseV2( + @Schema(description = "미션 ID", example = "1") Long missionId, + @Schema(description = "미션 제목", example = "산책하기") String missionTitle, + @Schema(description = "미션 완료 메시지", example = "산책하기 미션을 수행했어요!") + String missionCompleteMessage, + @Schema(description = "미션 기록 ID", example = "1") Long missionRecordId, + @Schema(description = "작성자 ID", example = "1") Long authorId, + @Schema(description = "작성자 프로필 닉네임") String authorProfileNickname, + @Schema(description = "작성자 프로필 이미지 URL") String authorProfileImageUrl, + @Schema(description = "미션 기록 이미지 URL", example = "example.jpeg") + String missionRecordImageUrl, + @Schema(description = "미션 기록 생성일") LocalDate createdDate, + @Schema(description = "부스트") Long totalBoostCount, + @Schema(description = "댓글 수", example = "12") Integer totalCommentCount, + @Schema(description = "미션 기록 컨텐츠") String content) { + public static FeedContentGetResponseV2 from(FindFeedDto missionRecord) { + return new FeedContentGetResponseV2( + missionRecord.mission().getId(), + missionRecord.mission().getTitle(), + missionRecord.mission().getCompleteMessage(), + missionRecord.missionRecord().getId(), + missionRecord.author().getId(), + missionRecord.author().getProfile().getNickname(), + missionRecord.author().getProfile().getProfileImageUrl(), + missionRecord.missionRecord().getImageUrl(), + missionRecord.missionRecord().getCreatedAt().toLocalDate(), + missionRecord.totalBoostCount(), + missionRecord.totalCommentCount(), + missionRecord.missionRecord().getContent()); + } +} diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v2/FeedGetResponseV2.java b/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v2/FeedGetResponseV2.java new file mode 100644 index 00000000..6fbc7470 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v2/FeedGetResponseV2.java @@ -0,0 +1,12 @@ +package com.depromeet.stonebed.domain.feed.dto.response.v2; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +public record FeedGetResponseV2( + List list, + @Schema(description = "커서 위치", example = "1") String nextCursor) { + public static FeedGetResponseV2 from(List list, String nextCursor) { + return new FeedGetResponseV2(list, nextCursor); + } +} diff --git a/src/test/java/com/depromeet/stonebed/domain/feed/application/FeedServiceTest.java b/src/test/java/com/depromeet/stonebed/domain/feed/application/FeedServiceTest.java index 41dff0eb..636a3f77 100644 --- a/src/test/java/com/depromeet/stonebed/domain/feed/application/FeedServiceTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/feed/application/FeedServiceTest.java @@ -8,7 +8,7 @@ import com.depromeet.stonebed.domain.feed.dao.FeedRepository; import com.depromeet.stonebed.domain.feed.dto.FindFeedDto; import com.depromeet.stonebed.domain.feed.dto.request.FeedGetRequest; -import com.depromeet.stonebed.domain.feed.dto.response.FeedGetResponse; +import com.depromeet.stonebed.domain.feed.dto.response.v1.FeedGetResponse; import com.depromeet.stonebed.domain.member.domain.Member; import com.depromeet.stonebed.domain.mission.domain.Mission; import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; From 98a2c0aa590cd2bb88083e7774aa714a5e83771c Mon Sep 17 00:00:00 2001 From: ybchar Date: Tue, 15 Oct 2024 10:36:46 +0900 Subject: [PATCH 22/56] =?UTF-8?q?refactor:=20=EB=94=A5=EB=A7=81=ED=81=AC?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=80=EC=9E=A5=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/FcmNotificationService.java | 27 ++++++++++--------- .../domain/fcm/domain/FcmNotification.java | 11 +++++--- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java b/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java index 1741f0a4..b5ae3b51 100644 --- a/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java +++ b/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java @@ -62,14 +62,16 @@ public void saveNotification( String message, Long targetId, Long memberId, - Boolean isRead) { + Boolean isRead, + String deepLink) { Member member = memberRepository .findById(memberId) .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND)); FcmNotification notification = - FcmNotification.create(type, title, message, member, targetId, isRead); + FcmNotification.createNotification( + type, title, message, member, targetId, isRead, deepLink); notificationRepository.save(notification); } @@ -199,11 +201,6 @@ private Optional validateTokenForMember(Member member) { return getTokenForMember(member).filter(token -> !token.isEmpty()); } - private String generateDeepLink(MissionRecord missionRecord, long boostCount) { - return FcmNotification.generateDeepLink( - FcmNotificationType.BOOSTER, missionRecord.getId(), boostCount); - } - private void createAndSendFcmMessage( String title, String message, String token, String deepLink) { FcmMessage fcmMessage = FcmMessage.of(title, message, token, deepLink); @@ -217,7 +214,9 @@ private void sendBoostNotification( String token = validateTokenForMember(missionRecord.getMember()).orElse(null); if (token == null) return; - String deepLink = generateDeepLink(missionRecord, boostCount); + String deepLink = + FcmNotification.generateDeepLink( + FcmNotificationType.BOOSTER, missionRecord.getId(), boostCount); createAndSendFcmMessage( notificationConstants.getTitle(), notificationConstants.getMessage(), @@ -230,7 +229,8 @@ private void sendBoostNotification( notificationConstants.getMessage(), missionRecord.getId(), missionRecord.getMember().getId(), - false); + false, + deepLink); } public void markNotificationAsRead(Long notificationId) { @@ -249,7 +249,8 @@ private List buildNotificationList( String message, List tokens, Long targetId, - FcmNotificationType notificationType) { + FcmNotificationType notificationType, + String deepLink) { List notifications = new ArrayList<>(); for (String token : tokens) { @@ -261,8 +262,8 @@ private List buildNotificationList( () -> new CustomException(ErrorCode.FAILED_TO_FIND_FCM_TOKEN)); FcmNotification newNotification = - FcmNotification.create( - notificationType, title, message, member, targetId, false); + FcmNotification.createNotification( + notificationType, title, message, member, targetId, false, deepLink); notifications.add(newNotification); } @@ -284,7 +285,7 @@ public void sendAndNotifications( } List notifications = - buildNotificationList(title, message, tokens, targetId, notificationType); + buildNotificationList(title, message, tokens, targetId, notificationType, deepLink); notificationRepository.saveAll(notifications); } diff --git a/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotification.java b/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotification.java index b6bbe134..5f25636c 100644 --- a/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotification.java +++ b/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotification.java @@ -47,23 +47,26 @@ private FcmNotification( String message, Member member, Long targetId, - Boolean isRead) { + Boolean isRead, + String deepLink) { this.type = type; this.title = title; this.message = message; this.member = member; this.targetId = targetId; this.isRead = isRead; + this.deepLink = deepLink; } - public static FcmNotification create( + public static FcmNotification createNotification( FcmNotificationType type, String title, String message, Member member, Long targetId, - Boolean isRead) { - return new FcmNotification(type, title, message, member, targetId, isRead); + Boolean isRead, + String deepLink) { + return new FcmNotification(type, title, message, member, targetId, isRead, deepLink); } public void markAsRead() { From acd8755fd41052e36777393c5784bb2a9edd9a95 Mon Sep 17 00:00:00 2001 From: ybchar Date: Tue, 15 Oct 2024 10:46:45 +0900 Subject: [PATCH 23/56] =?UTF-8?q?refactor:=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EB=94=A5=EB=A7=81=ED=81=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/comment/application/CommentService.java | 6 ++++++ .../domain/fcm/application/FcmNotificationService.java | 6 ++++++ .../stonebed/domain/fcm/domain/FcmNotification.java | 6 ++++-- .../depromeet/stonebed/scheduler/fcm/FcmScheduler.java | 4 ++-- .../fcm/application/FcmNotificationServiceTest.java | 8 +++++++- .../stonebed/domain/fcm/application/FcmSchedulerTest.java | 2 ++ 6 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java index b0feecd2..97a5ecb0 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java @@ -87,6 +87,7 @@ private void sendCommentNotification(MissionRecord missionRecord, Comment commen missionRecordOwner, FcmNotificationConstants.COMMENT, missionRecord, + comment, commentWriter); } @@ -100,6 +101,7 @@ private void sendCommentNotification(MissionRecord missionRecord, Comment commen parentCommentWriter, FcmNotificationConstants.RE_COMMENT, missionRecord, + comment, commentWriter); } @@ -109,6 +111,7 @@ private void sendCommentNotification(MissionRecord missionRecord, Comment commen missionRecordOwner, FcmNotificationConstants.RECORD_RE_COMMENT, missionRecord, + comment, commentWriter); } @@ -121,6 +124,7 @@ private void sendCommentNotification(MissionRecord missionRecord, Comment commen recipient, FcmNotificationConstants.RE_COMMENT, missionRecord, + comment, commentWriter); } } @@ -130,6 +134,7 @@ private void sendNotification( Member recipient, FcmNotificationConstants notificationType, MissionRecord missionRecord, + Comment comment, Member commentWriter) { String title = notificationType.getTitle(); String message = commentWriter.getProfile().getNickname() + notificationType.getMessage(); @@ -143,6 +148,7 @@ private void sendNotification( message, tokens, missionRecord.getId(), + comment.getId(), FcmNotificationType.valueOf(notificationTypeName)); } diff --git a/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java b/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java index b5ae3b51..7c2f8b2c 100644 --- a/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java +++ b/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java @@ -274,12 +274,18 @@ public void sendAndNotifications( String title, String message, List tokens, + Long sourceId, Long targetId, FcmNotificationType notificationType) { List> batches = createBatches(tokens, BATCH_SIZE); String deepLink = FcmNotification.generateDeepLink(notificationType, targetId, null); + if (notificationType == FcmNotificationType.COMMENT + || notificationType == FcmNotificationType.RE_COMMENT) { + deepLink = FcmNotification.generateCommentDeepLink(sourceId, targetId); + } + for (List batch : batches) { sqsMessageService.sendBatchMessages(batch, title, message, deepLink); } diff --git a/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotification.java b/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotification.java index 5f25636c..b6ea831b 100644 --- a/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotification.java +++ b/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotification.java @@ -79,9 +79,11 @@ public static String generateDeepLink( return DEEP_LINK_PREFIX + "mission"; } else if (type == FcmNotificationType.BOOSTER) { return DEEP_LINK_PREFIX + "boost?id=" + targetId + "&type=" + boostCount; - } else if (type == FcmNotificationType.COMMENT || type == FcmNotificationType.RE_COMMENT) { - return DEEP_LINK_PREFIX + "comment?id=" + targetId; } return null; } + + public static String generateCommentDeepLink(Long recordId, Long commentId) { + return DEEP_LINK_PREFIX + "comment?recordId=" + recordId + "&commentId=" + commentId; + } } diff --git a/src/main/java/com/depromeet/stonebed/scheduler/fcm/FcmScheduler.java b/src/main/java/com/depromeet/stonebed/scheduler/fcm/FcmScheduler.java index 65210a16..ab74c46d 100644 --- a/src/main/java/com/depromeet/stonebed/scheduler/fcm/FcmScheduler.java +++ b/src/main/java/com/depromeet/stonebed/scheduler/fcm/FcmScheduler.java @@ -43,7 +43,7 @@ public void sendDailyNotification() { List tokens = fcmNotificationService.getAllTokens(); fcmNotificationService.sendAndNotifications( - title, message, tokens, null, FcmNotificationType.MISSION); + title, message, tokens, null, null, FcmNotificationType.MISSION); log.info("모든 사용자에게 정규 알림 전송 및 저장 완료"); } @@ -57,7 +57,7 @@ public void sendReminderToIncompleteMissions() { List tokens = getIncompleteMissionTokens(); fcmNotificationService.sendAndNotifications( - title, message, tokens, null, FcmNotificationType.MISSION); + title, message, tokens, null, null, FcmNotificationType.MISSION); log.info("미완료 미션 사용자에게 리마인더 전송 및 저장 완료. 총 토큰 수: {}", tokens.size()); } diff --git a/src/test/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationServiceTest.java b/src/test/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationServiceTest.java index c6f4356c..225e11fe 100644 --- a/src/test/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationServiceTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationServiceTest.java @@ -45,7 +45,13 @@ public class FcmNotificationServiceTest extends FixtureMonkeySetUp { // when fcmNotificationService.saveNotification( - FcmNotificationType.MISSION, "title", "message", 1L, member.getId(), false); + FcmNotificationType.MISSION, + "title", + "message", + 1L, + member.getId(), + false, + "myapp://mission"); // then verify(notificationRepository, times(1)).save(any(FcmNotification.class)); diff --git a/src/test/java/com/depromeet/stonebed/domain/fcm/application/FcmSchedulerTest.java b/src/test/java/com/depromeet/stonebed/domain/fcm/application/FcmSchedulerTest.java index fcb6940d..2813ec27 100644 --- a/src/test/java/com/depromeet/stonebed/domain/fcm/application/FcmSchedulerTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/fcm/application/FcmSchedulerTest.java @@ -70,6 +70,7 @@ public class FcmSchedulerTest extends FixtureMonkeySetUp { eq("새로운 미션을 지금 시작해보세요!"), eq(tokens), eq(null), + eq(null), eq(FcmNotificationType.MISSION)); } @@ -115,6 +116,7 @@ public class FcmSchedulerTest extends FixtureMonkeySetUp { eq("미션 종료까지 5시간 남았어요!"), eq(tokens), eq(null), + eq(null), eq(FcmNotificationType.MISSION)); } } From ab0fb52463c4b981cbe51ed14732ec7bed6b53ce Mon Sep 17 00:00:00 2001 From: ybchar Date: Tue, 15 Oct 2024 10:54:24 +0900 Subject: [PATCH 24/56] =?UTF-8?q?fix:=20totalCommentCount=20v1=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/feed/dto/response/v1/FeedContentGetResponse.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v1/FeedContentGetResponse.java b/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v1/FeedContentGetResponse.java index e572059e..00427e1d 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v1/FeedContentGetResponse.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v1/FeedContentGetResponse.java @@ -17,7 +17,6 @@ public record FeedContentGetResponse( String missionRecordImageUrl, @Schema(description = "미션 기록 생성일") LocalDate createdDate, @Schema(description = "부스트") Long totalBoostCount, - @Schema(description = "댓글 수", example = "12") Integer totalCommentCount, @Schema(description = "미션 기록 컨텐츠") String content) { public static FeedContentGetResponse from(FindFeedDto missionRecord) { return new FeedContentGetResponse( @@ -31,7 +30,6 @@ public static FeedContentGetResponse from(FindFeedDto missionRecord) { missionRecord.missionRecord().getImageUrl(), missionRecord.missionRecord().getCreatedAt().toLocalDate(), missionRecord.totalBoostCount(), - missionRecord.totalCommentCount(), missionRecord.missionRecord().getContent()); } } From 32623bbf2a51ee062e8de289614866f51eeab03c Mon Sep 17 00:00:00 2001 From: ybchar Date: Tue, 15 Oct 2024 11:09:15 +0900 Subject: [PATCH 25/56] =?UTF-8?q?codeowner=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b7cdc9ba..50e87468 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @char-yb @dbscks97 @kwanok +* @char-yb @dbscks97 From b250f315850403938aa914d4951ce4a25f5d5274 Mon Sep 17 00:00:00 2001 From: ybchar Date: Tue, 15 Oct 2024 11:12:05 +0900 Subject: [PATCH 26/56] =?UTF-8?q?fix:=20=EC=95=8C=EB=A6=BC=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20deeplink=20=ED=95=84=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stonebed/domain/fcm/dto/response/FcmNotificationDto.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/depromeet/stonebed/domain/fcm/dto/response/FcmNotificationDto.java b/src/main/java/com/depromeet/stonebed/domain/fcm/dto/response/FcmNotificationDto.java index 2a09715a..53f87ca2 100644 --- a/src/main/java/com/depromeet/stonebed/domain/fcm/dto/response/FcmNotificationDto.java +++ b/src/main/java/com/depromeet/stonebed/domain/fcm/dto/response/FcmNotificationDto.java @@ -15,6 +15,7 @@ public record FcmNotificationDto( String imageUrl, @Schema(description = "읽음 여부", example = "false") Boolean isRead, @Schema(description = "타겟 ID", example = "1") Long targetId, + @Schema(description = "알림 딥링크 URL", example = "myapp://notification/1") String deepLink, @Schema(description = "알림 전송 시간", example = "2024-08-17 13:31:19") LocalDateTime createdAt) { @@ -30,6 +31,7 @@ public static FcmNotificationDto from( imageUrl, notification.getIsRead(), notification.getTargetId(), + notification.getDeepLink(), notification.getCreatedAt()); } } From 125ad38408495b99b646629c241f9a46f7d35185 Mon Sep 17 00:00:00 2001 From: ybchar Date: Tue, 15 Oct 2024 11:20:21 +0900 Subject: [PATCH 27/56] =?UTF-8?q?fix:=20=EB=8C=93=EA=B8=80=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20deepLink=20target=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stonebed/domain/comment/application/CommentService.java | 2 +- .../depromeet/stonebed/domain/fcm/domain/FcmNotification.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java index 97a5ecb0..ea90dac6 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java @@ -147,8 +147,8 @@ private void sendNotification( title, message, tokens, - missionRecord.getId(), comment.getId(), + missionRecord.getId(), FcmNotificationType.valueOf(notificationTypeName)); } diff --git a/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotification.java b/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotification.java index b6ea831b..627853bd 100644 --- a/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotification.java +++ b/src/main/java/com/depromeet/stonebed/domain/fcm/domain/FcmNotification.java @@ -83,7 +83,7 @@ public static String generateDeepLink( return null; } - public static String generateCommentDeepLink(Long recordId, Long commentId) { + public static String generateCommentDeepLink(Long commentId, Long recordId) { return DEEP_LINK_PREFIX + "comment?recordId=" + recordId + "&commentId=" + commentId; } } From 6cb219bb13f044330a1dd8f44169397f6254b0ea Mon Sep 17 00:00:00 2001 From: ybchar Date: Tue, 15 Oct 2024 18:09:36 +0900 Subject: [PATCH 28/56] =?UTF-8?q?fix:=20=ED=94=BC=EB=93=9C=20v1=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20count=20=EC=9B=90=EB=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/feed/dto/response/v1/FeedContentGetResponse.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v1/FeedContentGetResponse.java b/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v1/FeedContentGetResponse.java index 00427e1d..e572059e 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v1/FeedContentGetResponse.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v1/FeedContentGetResponse.java @@ -17,6 +17,7 @@ public record FeedContentGetResponse( String missionRecordImageUrl, @Schema(description = "미션 기록 생성일") LocalDate createdDate, @Schema(description = "부스트") Long totalBoostCount, + @Schema(description = "댓글 수", example = "12") Integer totalCommentCount, @Schema(description = "미션 기록 컨텐츠") String content) { public static FeedContentGetResponse from(FindFeedDto missionRecord) { return new FeedContentGetResponse( @@ -30,6 +31,7 @@ public static FeedContentGetResponse from(FindFeedDto missionRecord) { missionRecord.missionRecord().getImageUrl(), missionRecord.missionRecord().getCreatedAt().toLocalDate(), missionRecord.totalBoostCount(), + missionRecord.totalCommentCount(), missionRecord.missionRecord().getContent()); } } From 64a0f266eeca3a04e9ce227716a9d38750c84fde Mon Sep 17 00:00:00 2001 From: ybchar Date: Wed, 16 Oct 2024 00:11:01 +0900 Subject: [PATCH 29/56] =?UTF-8?q?fix:=20=EC=A4=91=EB=B3=B5=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EC=95=8C=EB=A6=BC=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentService.java | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java index ea90dac6..e32bf9ef 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java @@ -17,11 +17,11 @@ import com.depromeet.stonebed.global.error.ErrorCode; import com.depromeet.stonebed.global.error.exception.CustomException; import com.depromeet.stonebed.global.util.MemberUtil; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -46,7 +46,7 @@ public CommentCreateResponse createComment(CommentCreateRequest request) { final Comment comment = createAndSaveComment(request, member, missionRecord); - sendCommentNotification(missionRecord, comment); + sendCommentNotification(missionRecord, comment, request.parentId()); return CommentCreateResponse.of(comment.getId()); } @@ -77,37 +77,46 @@ private Comment createAndSaveComment( return commentRepository.save(comment); } - private void sendCommentNotification(MissionRecord missionRecord, Comment comment) { + /** + * 댓글 알림 메서드 + * + * @param missionRecord: 작성된 미션 기록 + * @param comment: 새로 생성한 댓글 + * @param parentId: 부모 댓글의 ID + */ + private void sendCommentNotification( + MissionRecord missionRecord, Comment comment, Long parentId) { Member missionRecordOwner = missionRecord.getMember(); Member commentWriter = comment.getWriter(); - // 1. 게시물 작성자가 댓글 작성자가 아닐 때 알림 - if (!missionRecordOwner.equals(commentWriter)) { - sendNotification( + // 1. 부모 댓글이 없는 경우 게시물 작성자에게만 COMMENT 알림 + if (!missionRecordOwner.equals(commentWriter) && parentId == null) { + sendCommentNotification( missionRecordOwner, FcmNotificationConstants.COMMENT, missionRecord, comment, commentWriter); + return; } // 2. 대댓글 작성 시 부모 댓글 작성자에게 RE_COMMENT 알림 if (comment.getParent() != null) { + // 부모 댓글 작성자 Member parentCommentWriter = comment.getParent().getWriter(); // 부모 댓글 작성자가 대댓글 작성자가 아닌 경우에만 알림 전송 if (!parentCommentWriter.equals(commentWriter)) { - sendNotification( + sendCommentNotification( parentCommentWriter, FcmNotificationConstants.RE_COMMENT, missionRecord, comment, commentWriter); } - - // 게시물 작성자에게는 RECORD_RE_COMMENT 알림 if (!missionRecordOwner.equals(commentWriter)) { - sendNotification( + // 게시물 작성자에게는 RECORD_RE_COMMENT 알림 + sendCommentNotification( missionRecordOwner, FcmNotificationConstants.RECORD_RE_COMMENT, missionRecord, @@ -115,12 +124,12 @@ private void sendCommentNotification(MissionRecord missionRecord, Comment commen commentWriter); } - // Collect unique recipients for notifications - Set commentRecipients = collectNotificationRecipients(missionRecord, comment); + Set commentRecipients = collectNotificationRecipients(comment); + commentRecipients.remove(parentCommentWriter); // Send notifications to unique recipients for (Member recipient : commentRecipients) { - sendNotification( + sendCommentNotification( recipient, FcmNotificationConstants.RE_COMMENT, missionRecord, @@ -130,7 +139,7 @@ private void sendCommentNotification(MissionRecord missionRecord, Comment commen } } - private void sendNotification( + private void sendCommentNotification( Member recipient, FcmNotificationConstants notificationType, MissionRecord missionRecord, @@ -152,27 +161,24 @@ private void sendNotification( FcmNotificationType.valueOf(notificationTypeName)); } - private Set collectNotificationRecipients( - MissionRecord missionRecord, Comment comment) { - Set notificationRecipients = new HashSet<>(); - notificationRecipients.add(missionRecord.getMember()); + private Set collectNotificationRecipients(Comment comment) { + Map notificationRecipientsMap = new HashMap<>(); Comment currentComment = comment; while (currentComment.getParent() != null) { currentComment = currentComment.getParent(); currentComment.getReplyComments().stream() .map(Comment::getWriter) - .forEach(notificationRecipients::add); + .forEach(writer -> notificationRecipientsMap.put(writer.getId(), writer)); } - notificationRecipients.remove(comment.getWriter()); - return notificationRecipients; + notificationRecipientsMap.remove(comment.getWriter().getId()); + + return new HashSet<>(notificationRecipientsMap.values()); } private List retrieveFcmTokens(Set notificationRecipients) { return notificationRecipients.stream() - .map(fcmTokenRepository::findByMember) - .filter(Optional::isPresent) - .map(Optional::get) + .flatMap(member -> fcmTokenRepository.findByMember(member).stream()) .map(FcmToken::getToken) .filter(Objects::nonNull) .filter(token -> !token.isEmpty() && !token.isBlank()) From 7ed8d5779f78fa1d91b7e4f9d3160c32442d9c5d Mon Sep 17 00:00:00 2001 From: ybchar Date: Wed, 16 Oct 2024 00:28:37 +0900 Subject: [PATCH 30/56] =?UTF-8?q?test:=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/CommentServiceTest.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java diff --git a/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java b/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java new file mode 100644 index 00000000..57814e4e --- /dev/null +++ b/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java @@ -0,0 +1,19 @@ +package com.depromeet.stonebed.domain.comment.application; + +import static org.junit.jupiter.api.Assertions.*; + +import com.depromeet.stonebed.FixtureMonkeySetUp; +import com.depromeet.stonebed.global.util.MemberUtil; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) +class CommentServiceTest extends FixtureMonkeySetUp { + + @InjectMocks private CommentService commentService; + @Mock private MemberUtil memberUtil; +} From 41ecda6f709df8b491352cf01555e1c38af3947b Mon Sep 17 00:00:00 2001 From: ybchar Date: Wed, 16 Oct 2024 01:06:09 +0900 Subject: [PATCH 31/56] =?UTF-8?q?fix:=20=EC=A4=91=EB=B3=B5=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20else-if=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentService.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java index e32bf9ef..ed8e08c0 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java @@ -105,20 +105,19 @@ private void sendCommentNotification( // 부모 댓글 작성자 Member parentCommentWriter = comment.getParent().getWriter(); - // 부모 댓글 작성자가 대댓글 작성자가 아닌 경우에만 알림 전송 - if (!parentCommentWriter.equals(commentWriter)) { + // 게시물 작성자에게는 RECORD_RE_COMMENT 알림 + if (!missionRecordOwner.equals(commentWriter)) { sendCommentNotification( - parentCommentWriter, - FcmNotificationConstants.RE_COMMENT, + missionRecordOwner, + FcmNotificationConstants.RECORD_RE_COMMENT, missionRecord, comment, commentWriter); - } - if (!missionRecordOwner.equals(commentWriter)) { - // 게시물 작성자에게는 RECORD_RE_COMMENT 알림 + } else if (!parentCommentWriter.equals(commentWriter)) { + // 부모 댓글 작성자가 대댓글 작성자가 아닌 경우에만 알림 전송 sendCommentNotification( - missionRecordOwner, - FcmNotificationConstants.RECORD_RE_COMMENT, + parentCommentWriter, + FcmNotificationConstants.RE_COMMENT, missionRecord, comment, commentWriter); From c487b2d26c128739214a2fdf3a23cf89adfe8744 Mon Sep 17 00:00:00 2001 From: ybchar Date: Wed, 16 Oct 2024 18:35:48 +0900 Subject: [PATCH 32/56] =?UTF-8?q?refactor:=20=ED=83=88=ED=87=B4=20?= =?UTF-8?q?=EC=8B=9C=20fcm=20token=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/application/AuthService.java | 3 +++ .../fcm/application/FcmNotificationService.java | 15 ++++++++++----- .../domain/fcm/dao/FcmTokenRepository.java | 7 +++++++ .../domain/auth/application/AuthServiceTest.java | 2 ++ 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java b/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java index 34ca3a41..bf3f4e48 100644 --- a/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java +++ b/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java @@ -9,6 +9,7 @@ import com.depromeet.stonebed.domain.auth.dto.response.SocialClientResponse; import com.depromeet.stonebed.domain.auth.dto.response.TokenPairResponse; import com.depromeet.stonebed.domain.fcm.dao.FcmNotificationRepository; +import com.depromeet.stonebed.domain.fcm.dao.FcmTokenRepository; import com.depromeet.stonebed.domain.member.dao.MemberRepository; import com.depromeet.stonebed.domain.member.domain.Member; import com.depromeet.stonebed.domain.member.domain.MemberRole; @@ -38,6 +39,7 @@ public class AuthService { private final MemberRepository memberRepository; private final MissionRecordRepository missionRecordRepository; private final MissionRecordBoostRepository missionRecordBoostRepository; + private final FcmTokenRepository fcmTokenRepository; private final AppleClient appleClient; private final KakaoClient kakaoClient; @@ -179,5 +181,6 @@ private void withdrawMemberRelationByMemberId(List recordIds, Long memberI missionRecordBoostRepository.deleteAllByMember(recordIds); missionRecordRepository.deleteAllByMember(memberId); fcmNotificationRepository.deleteAllByMember(memberId); + fcmTokenRepository.deleteAllByMember(memberId); } } diff --git a/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java b/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java index 7c2f8b2c..b44cc9ef 100644 --- a/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java +++ b/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java @@ -277,7 +277,7 @@ public void sendAndNotifications( Long sourceId, Long targetId, FcmNotificationType notificationType) { - List> batches = createBatches(tokens, BATCH_SIZE); + List> batches = createBatches(tokens); String deepLink = FcmNotification.generateDeepLink(notificationType, targetId, null); @@ -295,13 +295,18 @@ public void sendAndNotifications( notificationRepository.saveAll(notifications); } - private List> createBatches(List tokens, int batchSize) { - return IntStream.range(0, (tokens.size() + batchSize - 1) / batchSize) + private List> createBatches(List tokens) { + return IntStream.range( + 0, + (tokens.size() + FcmNotificationService.BATCH_SIZE - 1) + / FcmNotificationService.BATCH_SIZE) .mapToObj( i -> tokens.subList( - i * batchSize, - Math.min(tokens.size(), (i + 1) * batchSize))) + i * FcmNotificationService.BATCH_SIZE, + Math.min( + tokens.size(), + (i + 1) * FcmNotificationService.BATCH_SIZE))) .collect(Collectors.toList()); } diff --git a/src/main/java/com/depromeet/stonebed/domain/fcm/dao/FcmTokenRepository.java b/src/main/java/com/depromeet/stonebed/domain/fcm/dao/FcmTokenRepository.java index f89f3c32..cd7a4b44 100644 --- a/src/main/java/com/depromeet/stonebed/domain/fcm/dao/FcmTokenRepository.java +++ b/src/main/java/com/depromeet/stonebed/domain/fcm/dao/FcmTokenRepository.java @@ -7,6 +7,8 @@ import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; public interface FcmTokenRepository extends JpaRepository, FcmTokenRepositoryCustom { @@ -19,4 +21,9 @@ public interface FcmTokenRepository List findAllByUpdatedAtBefore(LocalDateTime cutoffDate); void deleteByToken(String token); + + // Delete + @Modifying + @Query("DELETE FROM FcmToken ft WHERE ft.member.id = :memberId") + void deleteAllByMember(Long memberId); } diff --git a/src/test/java/com/depromeet/stonebed/domain/auth/application/AuthServiceTest.java b/src/test/java/com/depromeet/stonebed/domain/auth/application/AuthServiceTest.java index 809c8bfe..77ff7d8e 100644 --- a/src/test/java/com/depromeet/stonebed/domain/auth/application/AuthServiceTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/auth/application/AuthServiceTest.java @@ -8,6 +8,7 @@ import com.depromeet.stonebed.domain.auth.dto.response.AuthTokenResponse; import com.depromeet.stonebed.domain.auth.dto.response.TokenPairResponse; import com.depromeet.stonebed.domain.fcm.dao.FcmNotificationRepository; +import com.depromeet.stonebed.domain.fcm.dao.FcmTokenRepository; import com.depromeet.stonebed.domain.member.dao.MemberRepository; import com.depromeet.stonebed.domain.member.domain.Member; import com.depromeet.stonebed.domain.member.domain.MemberRole; @@ -38,6 +39,7 @@ class AuthServiceTest extends FixtureMonkeySetUp { @Mock private FcmNotificationRepository fcmNotificationRepository; @Mock private MissionRecordRepository missionRecordRepository; @Mock private MissionRecordBoostRepository missionRecordBoostRepository; + @Mock private FcmTokenRepository fcmTokenRepository; @Mock private MemberUtil memberUtil; From 7d6125f3b29a10e749b1be662cbe57d774ace17d Mon Sep 17 00:00:00 2001 From: ybchar Date: Thu, 17 Oct 2024 22:56:36 +0900 Subject: [PATCH 33/56] =?UTF-8?q?refactor:=20CommentService=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentService.java | 108 ++++++++---------- 1 file changed, 50 insertions(+), 58 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java index ed8e08c0..3895afe7 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java @@ -43,9 +43,7 @@ public class CommentService { public CommentCreateResponse createComment(CommentCreateRequest request) { final Member member = memberUtil.getCurrentMember(); final MissionRecord missionRecord = findMissionRecordById(request.recordId()); - final Comment comment = createAndSaveComment(request, member, missionRecord); - sendCommentNotification(missionRecord, comment, request.parentId()); return CommentCreateResponse.of(comment.getId()); } @@ -55,11 +53,9 @@ public CommentFindResponse findCommentsByRecordId(Long recordId) { final MissionRecord missionRecord = findMissionRecordById(recordId); final List allComments = commentRepository.findAllCommentsByMissionRecord(missionRecord); - Map> commentsByParentId = groupCommentsByParentId(allComments); List rootResponses = convertToCommentFindOneResponses(commentsByParentId); - return CommentFindResponse.of(rootResponses); } @@ -73,68 +69,65 @@ private Comment createAndSaveComment( request.content(), findCommentById(request.parentId())) : Comment.createComment(missionRecord, member, request.content(), null); - return commentRepository.save(comment); } - /** - * 댓글 알림 메서드 - * - * @param missionRecord: 작성된 미션 기록 - * @param comment: 새로 생성한 댓글 - * @param parentId: 부모 댓글의 ID - */ private void sendCommentNotification( MissionRecord missionRecord, Comment comment, Long parentId) { Member missionRecordOwner = missionRecord.getMember(); Member commentWriter = comment.getWriter(); - // 1. 부모 댓글이 없는 경우 게시물 작성자에게만 COMMENT 알림 - if (!missionRecordOwner.equals(commentWriter) && parentId == null) { + if (isRootComment(parentId, missionRecordOwner, commentWriter)) { sendCommentNotification( missionRecordOwner, FcmNotificationConstants.COMMENT, missionRecord, comment, commentWriter); - return; + } else if (isReplyComment(comment)) { + sendReplyNotifications(missionRecord, comment, commentWriter); } + } + + private boolean isRootComment(Long parentId, Member missionRecordOwner, Member commentWriter) { + return !missionRecordOwner.equals(commentWriter) && parentId == null; + } - // 2. 대댓글 작성 시 부모 댓글 작성자에게 RE_COMMENT 알림 - if (comment.getParent() != null) { - // 부모 댓글 작성자 - Member parentCommentWriter = comment.getParent().getWriter(); - - // 게시물 작성자에게는 RECORD_RE_COMMENT 알림 - if (!missionRecordOwner.equals(commentWriter)) { - sendCommentNotification( - missionRecordOwner, - FcmNotificationConstants.RECORD_RE_COMMENT, - missionRecord, - comment, - commentWriter); - } else if (!parentCommentWriter.equals(commentWriter)) { - // 부모 댓글 작성자가 대댓글 작성자가 아닌 경우에만 알림 전송 - sendCommentNotification( - parentCommentWriter, - FcmNotificationConstants.RE_COMMENT, - missionRecord, - comment, - commentWriter); - } - - Set commentRecipients = collectNotificationRecipients(comment); - commentRecipients.remove(parentCommentWriter); - - // Send notifications to unique recipients - for (Member recipient : commentRecipients) { - sendCommentNotification( - recipient, - FcmNotificationConstants.RE_COMMENT, - missionRecord, - comment, - commentWriter); - } + private boolean isReplyComment(Comment comment) { + return comment.getParent() != null; + } + + private void sendReplyNotifications( + MissionRecord missionRecord, Comment comment, Member commentWriter) { + Member parentCommentWriter = comment.getParent().getWriter(); + Member missionRecordOwner = missionRecord.getMember(); + + if (!missionRecordOwner.equals(commentWriter)) { + sendCommentNotification( + missionRecordOwner, + FcmNotificationConstants.RECORD_RE_COMMENT, + missionRecord, + comment, + commentWriter); + } else if (!parentCommentWriter.equals(commentWriter)) { + sendCommentNotification( + parentCommentWriter, + FcmNotificationConstants.RE_COMMENT, + missionRecord, + comment, + commentWriter); + } + + Set commentRecipients = collectNotificationRecipients(comment); + commentRecipients.remove(parentCommentWriter); + + for (Member recipient : commentRecipients) { + sendCommentNotification( + recipient, + FcmNotificationConstants.RE_COMMENT, + missionRecord, + comment, + commentWriter); } } @@ -147,10 +140,10 @@ private void sendCommentNotification( String title = notificationType.getTitle(); String message = commentWriter.getProfile().getNickname() + notificationType.getMessage(); List tokens = retrieveFcmTokens(Set.of(recipient)); - String notificationTypeName = notificationType.name(); - if (notificationType.name().equals(FcmNotificationConstants.RECORD_RE_COMMENT.name())) { - notificationTypeName = FcmNotificationConstants.RE_COMMENT.name(); - } + String notificationTypeName = + notificationType.name().equals(FcmNotificationConstants.RECORD_RE_COMMENT.name()) + ? FcmNotificationConstants.RE_COMMENT.name() + : notificationType.name(); fcmNotificationService.sendAndNotifications( title, message, @@ -162,16 +155,16 @@ private void sendCommentNotification( private Set collectNotificationRecipients(Comment comment) { Map notificationRecipientsMap = new HashMap<>(); - Comment currentComment = comment; + while (currentComment.getParent() != null) { currentComment = currentComment.getParent(); currentComment.getReplyComments().stream() .map(Comment::getWriter) .forEach(writer -> notificationRecipientsMap.put(writer.getId(), writer)); } - notificationRecipientsMap.remove(comment.getWriter().getId()); + notificationRecipientsMap.remove(comment.getWriter().getId()); return new HashSet<>(notificationRecipientsMap.values()); } @@ -193,8 +186,7 @@ private Map> groupCommentsByParentId(List allCommen return (parent != null) ? parent.getId() : ROOT_COMMENT_PARENT_ID; - }, - Collectors.toList())); + })); } private List convertToCommentFindOneResponses( From 2dac40b89c40d875f32cc0608e494153168715d4 Mon Sep 17 00:00:00 2001 From: Park Yun Chan Date: Fri, 18 Oct 2024 10:45:29 +0900 Subject: [PATCH 34/56] =?UTF-8?q?test:=20fcmScheduler=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20(#309)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../missionRecord/api/MissionRecordController.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java b/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java index 6ef014dc..a7a0f083 100644 --- a/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java +++ b/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java @@ -10,6 +10,7 @@ import com.depromeet.stonebed.domain.missionRecord.dto.response.MissionRecordIdResponse; import com.depromeet.stonebed.domain.missionRecord.dto.response.MissionRecordTabListResponse; import com.depromeet.stonebed.domain.missionRecord.dto.response.MissionTabResponse; +import com.depromeet.stonebed.scheduler.fcm.FcmScheduler; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -26,6 +27,7 @@ public class MissionRecordController { private final MissionRecordService missionRecordService; + private final FcmScheduler fcmScheduler; @Operation(summary = "미션 탭 완료된 기록 리스트", description = "미션 탭에서 완료된 기록 리스트를 조회한다.") @GetMapping @@ -86,4 +88,16 @@ public ResponseEntity createMissionRecordBoost( missionRecordService.createBoost(recordId, request.count()); return ResponseEntity.status(HttpStatus.CREATED).build(); } + + @PostMapping + public ResponseEntity dailyTest() { + fcmScheduler.sendDailyNotification(); + return ResponseEntity.ok().build(); + } + + @PostMapping + public ResponseEntity remindTest() { + fcmScheduler.sendReminderToIncompleteMissions(); + return ResponseEntity.ok().build(); + } } From 3b33a110a9bc929bbfeb281ec1ed80bb4946f132 Mon Sep 17 00:00:00 2001 From: Park Yun Chan Date: Fri, 18 Oct 2024 10:54:35 +0900 Subject: [PATCH 35/56] Test/fcmschedulertest (#310) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: fcmScheduler테스트 * test * test: missionRecordTest코드 Mockbean추가 --- .../domain/missionRecord/api/MissionRecordController.java | 4 ++-- .../domain/missionRecord/api/MissionRecordControllerTest.java | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java b/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java index a7a0f083..b8dbd100 100644 --- a/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java +++ b/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java @@ -89,13 +89,13 @@ public ResponseEntity createMissionRecordBoost( return ResponseEntity.status(HttpStatus.CREATED).build(); } - @PostMapping + @PostMapping("/daily") public ResponseEntity dailyTest() { fcmScheduler.sendDailyNotification(); return ResponseEntity.ok().build(); } - @PostMapping + @PostMapping("/remind") public ResponseEntity remindTest() { fcmScheduler.sendReminderToIncompleteMissions(); return ResponseEntity.ok().build(); diff --git a/src/test/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordControllerTest.java b/src/test/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordControllerTest.java index cce3afdf..9448e889 100644 --- a/src/test/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordControllerTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordControllerTest.java @@ -5,6 +5,7 @@ import com.depromeet.stonebed.domain.missionRecord.application.MissionRecordService; import com.depromeet.stonebed.domain.missionRecord.dto.request.MissionRecordBoostRequest; +import com.depromeet.stonebed.scheduler.fcm.FcmScheduler; import com.google.gson.Gson; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -31,6 +32,7 @@ public class MissionRecordControllerTest { @Autowired private MockMvc mockMvc; @MockBean private MissionRecordService missionRecordService; + @MockBean private FcmScheduler fcmScheduler; private final Gson gson = new Gson(); From 02201d1f02e1df2c268df9e07c69293ad89f7915 Mon Sep 17 00:00:00 2001 From: Park Yun Chan Date: Fri, 18 Oct 2024 17:41:00 +0900 Subject: [PATCH 36/56] =?UTF-8?q?fix:=20BatchEntry=20ID=20=EA=B0=92=20UUID?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95=20(#312)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/MissionRecordController.java | 14 ---- .../sqs/application/SqsMessageService.java | 81 ++++++++++--------- .../api/MissionRecordControllerTest.java | 3 - 3 files changed, 44 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java b/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java index b8dbd100..6ef014dc 100644 --- a/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java +++ b/src/main/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordController.java @@ -10,7 +10,6 @@ import com.depromeet.stonebed.domain.missionRecord.dto.response.MissionRecordIdResponse; import com.depromeet.stonebed.domain.missionRecord.dto.response.MissionRecordTabListResponse; import com.depromeet.stonebed.domain.missionRecord.dto.response.MissionTabResponse; -import com.depromeet.stonebed.scheduler.fcm.FcmScheduler; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -27,7 +26,6 @@ public class MissionRecordController { private final MissionRecordService missionRecordService; - private final FcmScheduler fcmScheduler; @Operation(summary = "미션 탭 완료된 기록 리스트", description = "미션 탭에서 완료된 기록 리스트를 조회한다.") @GetMapping @@ -88,16 +86,4 @@ public ResponseEntity createMissionRecordBoost( missionRecordService.createBoost(recordId, request.count()); return ResponseEntity.status(HttpStatus.CREATED).build(); } - - @PostMapping("/daily") - public ResponseEntity dailyTest() { - fcmScheduler.sendDailyNotification(); - return ResponseEntity.ok().build(); - } - - @PostMapping("/remind") - public ResponseEntity remindTest() { - fcmScheduler.sendReminderToIncompleteMissions(); - return ResponseEntity.ok().build(); - } } diff --git a/src/main/java/com/depromeet/stonebed/domain/sqs/application/SqsMessageService.java b/src/main/java/com/depromeet/stonebed/domain/sqs/application/SqsMessageService.java index 7fd176a5..b146c4b6 100644 --- a/src/main/java/com/depromeet/stonebed/domain/sqs/application/SqsMessageService.java +++ b/src/main/java/com/depromeet/stonebed/domain/sqs/application/SqsMessageService.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -49,51 +50,57 @@ public void sendMessage(Object message) { public void sendBatchMessages( List tokens, String title, String message, String deepLink) { - List entries = new ArrayList<>(); + // SQS 메시지의 최대 전송 크기는 10개이므로 이를 고려하여 분할합니다. + int batchSize = 10; List failedTokens = new ArrayList<>(); - for (String token : tokens) { - try { - FcmMessage fcmMessage = FcmMessage.of(title, message, token, deepLink); - String messageBody = objectMapper.writeValueAsString(fcmMessage); - SendMessageBatchRequestEntry entry = - SendMessageBatchRequestEntry.builder() - .id(token) - .messageBody(messageBody) - .build(); - entries.add(entry); - } catch (Exception e) { - log.error("메시지 직렬화 실패: {}", e.getMessage()); + + for (int i = 0; i < tokens.size(); i += batchSize) { + List batchTokens = tokens.subList(i, Math.min(i + batchSize, tokens.size())); + List entries = new ArrayList<>(); + + for (String token : batchTokens) { + try { + FcmMessage fcmMessage = FcmMessage.of(title, message, token, deepLink); + String messageBody = objectMapper.writeValueAsString(fcmMessage); + SendMessageBatchRequestEntry entry = + SendMessageBatchRequestEntry.builder() + .id(UUID.randomUUID().toString()) + .messageBody(messageBody) + .build(); + entries.add(entry); + } catch (Exception e) { + log.error("메시지 직렬화 실패: {}", e.getMessage()); + } } - } - if (!entries.isEmpty()) { - SendMessageBatchRequest batchRequest = - SendMessageBatchRequest.builder() - .queueUrl(sqsProperties.queueUrl()) - .entries(entries) - .build(); + if (!entries.isEmpty()) { + SendMessageBatchRequest batchRequest = + SendMessageBatchRequest.builder() + .queueUrl(sqsProperties.queueUrl()) + .entries(entries) + .build(); - try { - SendMessageBatchResponse batchResponse = sqsClient.sendMessageBatch(batchRequest); + try { + SendMessageBatchResponse batchResponse = + sqsClient.sendMessageBatch(batchRequest); + log.info("배치 메시지 전송 응답: {}", batchResponse); + // 실패한 메시지 처리 + List failedMessages = batchResponse.failed(); + for (BatchResultErrorEntry failed : failedMessages) { + log.error("메시지 전송 실패, ID {}: {}", failed.id(), failed.message()); + failedTokens.add(failed.id()); + } - // 실패한 메시지 처리 - List failedMessages = batchResponse.failed(); - for (BatchResultErrorEntry failed : failedMessages) { - log.error("메시지 전송 실패, ID {}: {}", failed.id(), failed.message()); - failedTokens.add(failed.id()); - } + // 실패한 토큰 삭제 + for (String failedToken : failedTokens) { + fcmTokenRepository.deleteByToken(failedToken); + log.info("비활성화된 FCM 토큰 삭제: {}", failedToken); + } - // 실패한 토큰 삭제 등의 후속 작업 - for (String failedToken : failedTokens) { - fcmTokenRepository.deleteByToken(failedToken); - log.info("비활성화된 FCM 토큰 삭제: {}", failedToken); + } catch (Exception e) { + log.error("SQS 배치 메시지 전송 실패: {}", e.getMessage()); } - - } catch (Exception e) { - log.error("SQS 배치 메시지 전송 실패: {}", e.getMessage()); } - } else { - log.warn("전송할 메시지가 없습니다."); } } } diff --git a/src/test/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordControllerTest.java b/src/test/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordControllerTest.java index 9448e889..aa73bb2e 100644 --- a/src/test/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordControllerTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/missionRecord/api/MissionRecordControllerTest.java @@ -5,7 +5,6 @@ import com.depromeet.stonebed.domain.missionRecord.application.MissionRecordService; import com.depromeet.stonebed.domain.missionRecord.dto.request.MissionRecordBoostRequest; -import com.depromeet.stonebed.scheduler.fcm.FcmScheduler; import com.google.gson.Gson; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -32,8 +31,6 @@ public class MissionRecordControllerTest { @Autowired private MockMvc mockMvc; @MockBean private MissionRecordService missionRecordService; - @MockBean private FcmScheduler fcmScheduler; - private final Gson gson = new Gson(); @Test From d70bc2e909bb38d867afcac19bb49cfcc0277d79 Mon Sep 17 00:00:00 2001 From: ybchar Date: Fri, 18 Oct 2024 21:21:47 +0900 Subject: [PATCH 37/56] =?UTF-8?q?refactor:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=83=81=EC=88=98=ED=99=94=20=EB=B0=8F=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/FcmNotificationService.java | 27 +--- .../sqs/application/SqsMessageService.java | 122 +++++++++++------- .../constants/NotificationConstants.java | 12 ++ 3 files changed, 92 insertions(+), 69 deletions(-) create mode 100644 src/main/java/com/depromeet/stonebed/global/common/constants/NotificationConstants.java diff --git a/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java b/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java index b44cc9ef..e129b2c1 100644 --- a/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java +++ b/src/main/java/com/depromeet/stonebed/domain/fcm/application/FcmNotificationService.java @@ -1,5 +1,7 @@ package com.depromeet.stonebed.domain.fcm.application; +import static com.depromeet.stonebed.global.common.constants.NotificationConstants.*; + import com.depromeet.stonebed.domain.fcm.dao.FcmNotificationRepository; import com.depromeet.stonebed.domain.fcm.dao.FcmTokenRepository; import com.depromeet.stonebed.domain.fcm.domain.FcmMessage; @@ -19,7 +21,6 @@ import com.depromeet.stonebed.global.error.exception.CustomException; import com.depromeet.stonebed.global.util.MemberUtil; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.List; @@ -48,14 +49,6 @@ public class FcmNotificationService { private final MemberRepository memberRepository; private final MemberUtil memberUtil; - private static final DateTimeFormatter DATE_FORMATTER = - DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); - - private static final long FIRST_BOOST_THRESHOLD = 1; - private static final long POPULAR_THRESHOLD = 1000; - private static final long SUPER_POPULAR_THRESHOLD = 5000; - private static final int BATCH_SIZE = 10; - public void saveNotification( FcmNotificationType type, String title, @@ -79,7 +72,7 @@ public void saveNotification( public FcmNotificationResponse getNotificationsForCurrentMember(String cursor, int limit) { Member member = memberUtil.getCurrentMember(); - Pageable pageable = createPageable(limit); + Pageable pageable = PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "createdAt")); List notifications = getNotifications(cursor, member.getId(), pageable); List notificationData = convertToNotificationDto(notifications); String nextCursor = getNextCursor(notifications); @@ -87,10 +80,6 @@ public FcmNotificationResponse getNotificationsForCurrentMember(String cursor, i return FcmNotificationResponse.from(notificationData, nextCursor); } - private Pageable createPageable(int limit) { - return PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "createdAt")); - } - private List convertToNotificationDto(List notifications) { List targetIds = notifications.stream().map(FcmNotification::getTargetId).toList(); @@ -298,15 +287,13 @@ public void sendAndNotifications( private List> createBatches(List tokens) { return IntStream.range( 0, - (tokens.size() + FcmNotificationService.BATCH_SIZE - 1) - / FcmNotificationService.BATCH_SIZE) + (tokens.size() + SQS_BATCH_SIZE - 1) + / SQS_BATCH_SIZE) // ceil(tokens.size() / SQS_BATCH_SIZE .mapToObj( i -> tokens.subList( - i * FcmNotificationService.BATCH_SIZE, - Math.min( - tokens.size(), - (i + 1) * FcmNotificationService.BATCH_SIZE))) + i * SQS_BATCH_SIZE, + Math.min(tokens.size(), (i + 1) * SQS_BATCH_SIZE))) .collect(Collectors.toList()); } diff --git a/src/main/java/com/depromeet/stonebed/domain/sqs/application/SqsMessageService.java b/src/main/java/com/depromeet/stonebed/domain/sqs/application/SqsMessageService.java index b146c4b6..28fe441a 100644 --- a/src/main/java/com/depromeet/stonebed/domain/sqs/application/SqsMessageService.java +++ b/src/main/java/com/depromeet/stonebed/domain/sqs/application/SqsMessageService.java @@ -1,5 +1,7 @@ package com.depromeet.stonebed.domain.sqs.application; +import static com.depromeet.stonebed.global.common.constants.NotificationConstants.*; + import com.depromeet.stonebed.domain.fcm.dao.FcmTokenRepository; import com.depromeet.stonebed.domain.fcm.domain.FcmMessage; import com.depromeet.stonebed.infra.properties.SqsProperties; @@ -24,13 +26,10 @@ @RequiredArgsConstructor @Service public class SqsMessageService { - - private final SqsClient sqsClient; - private final SqsProperties sqsProperties; - private final ObjectMapper objectMapper; private final FcmTokenRepository fcmTokenRepository; + private final SqsClient sqsClient; public void sendMessage(Object message) { try { @@ -50,57 +49,82 @@ public void sendMessage(Object message) { public void sendBatchMessages( List tokens, String title, String message, String deepLink) { - // SQS 메시지의 최대 전송 크기는 10개이므로 이를 고려하여 분할합니다. - int batchSize = 10; + List failedTokens = new ArrayList<>(); - for (int i = 0; i < tokens.size(); i += batchSize) { - List batchTokens = tokens.subList(i, Math.min(i + batchSize, tokens.size())); - List entries = new ArrayList<>(); - - for (String token : batchTokens) { - try { - FcmMessage fcmMessage = FcmMessage.of(title, message, token, deepLink); - String messageBody = objectMapper.writeValueAsString(fcmMessage); - SendMessageBatchRequestEntry entry = - SendMessageBatchRequestEntry.builder() - .id(UUID.randomUUID().toString()) - .messageBody(messageBody) - .build(); - entries.add(entry); - } catch (Exception e) { - log.error("메시지 직렬화 실패: {}", e.getMessage()); - } - } + // 토큰 리스트를 10개씩 분할하여 처리 + for (int i = 0; i < tokens.size(); i += SQS_BATCH_SIZE) { + List batchTokens = + tokens.subList(i, Math.min(i + SQS_BATCH_SIZE, tokens.size())); + + List entries = + createBatchEntries(batchTokens, title, message, deepLink); if (!entries.isEmpty()) { - SendMessageBatchRequest batchRequest = - SendMessageBatchRequest.builder() - .queueUrl(sqsProperties.queueUrl()) - .entries(entries) - .build(); + sendBatchRequest(entries, failedTokens); + } + } + + // 실패한 토큰 삭제 처리 + deleteFailedTokens(failedTokens); + } + + private List createBatchEntries( + List batchTokens, String title, String message, String deepLink) { + + List entries = new ArrayList<>(); - try { - SendMessageBatchResponse batchResponse = - sqsClient.sendMessageBatch(batchRequest); - log.info("배치 메시지 전송 응답: {}", batchResponse); - // 실패한 메시지 처리 - List failedMessages = batchResponse.failed(); - for (BatchResultErrorEntry failed : failedMessages) { - log.error("메시지 전송 실패, ID {}: {}", failed.id(), failed.message()); - failedTokens.add(failed.id()); - } - - // 실패한 토큰 삭제 - for (String failedToken : failedTokens) { - fcmTokenRepository.deleteByToken(failedToken); - log.info("비활성화된 FCM 토큰 삭제: {}", failedToken); - } - - } catch (Exception e) { - log.error("SQS 배치 메시지 전송 실패: {}", e.getMessage()); - } + for (String token : batchTokens) { + try { + FcmMessage fcmMessage = FcmMessage.of(title, message, token, deepLink); + String messageBody = objectMapper.writeValueAsString(fcmMessage); + SendMessageBatchRequestEntry entry = + SendMessageBatchRequestEntry.builder() + .id(UUID.randomUUID().toString()) + .messageBody(messageBody) + .build(); + entries.add(entry); + } catch (Exception e) { + log.error("메시지 직렬화 실패: {}", e.getMessage()); } } + + return entries; + } + + private void sendBatchRequest( + List entries, List failedTokens) { + SendMessageBatchRequest batchRequest = + SendMessageBatchRequest.builder() + .queueUrl(sqsProperties.queueUrl()) + .entries(entries) + .build(); + + try { + SendMessageBatchResponse batchResponse = sqsClient.sendMessageBatch(batchRequest); + log.info("배치 메시지 전송 응답: {}", batchResponse); + + // 실패한 메시지 처리 + handleFailedMessages(batchResponse, failedTokens); + + } catch (Exception e) { + log.error("SQS 배치 메시지 전송 실패: {}", e.getMessage()); + } + } + + private void handleFailedMessages( + SendMessageBatchResponse batchResponse, List failedTokens) { + List failedMessages = batchResponse.failed(); + for (BatchResultErrorEntry failed : failedMessages) { + log.error("메시지 전송 실패, ID {}: {}", failed.id(), failed.message()); + failedTokens.add(failed.id()); + } + } + + private void deleteFailedTokens(List failedTokens) { + for (String failedToken : failedTokens) { + fcmTokenRepository.deleteByToken(failedToken); + log.info("비활성화된 FCM 토큰 삭제: {}", failedToken); + } } } diff --git a/src/main/java/com/depromeet/stonebed/global/common/constants/NotificationConstants.java b/src/main/java/com/depromeet/stonebed/global/common/constants/NotificationConstants.java new file mode 100644 index 00000000..185e4571 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/global/common/constants/NotificationConstants.java @@ -0,0 +1,12 @@ +package com.depromeet.stonebed.global.common.constants; + +import java.time.format.DateTimeFormatter; + +public final class NotificationConstants { + public static final int SQS_BATCH_SIZE = 10; + public static final long FIRST_BOOST_THRESHOLD = 1; + public static final long POPULAR_THRESHOLD = 1000; + public static final long SUPER_POPULAR_THRESHOLD = 5000; + public static final DateTimeFormatter DATE_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); +} From 199eac274aa363a43e2def5a167196bf1c9d8184 Mon Sep 17 00:00:00 2001 From: ybchar Date: Sun, 20 Oct 2024 00:58:20 +0900 Subject: [PATCH 38/56] =?UTF-8?q?fix:=20comment-record=20=EC=97=B0?= =?UTF-8?q?=EA=B4=80=EA=B4=80=EA=B3=84=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stonebed/domain/comment/domain/Comment.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/domain/Comment.java b/src/main/java/com/depromeet/stonebed/domain/comment/domain/Comment.java index cd6dc663..11982b55 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/domain/Comment.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/domain/Comment.java @@ -2,7 +2,6 @@ import com.depromeet.stonebed.domain.common.BaseTimeEntity; import com.depromeet.stonebed.domain.member.domain.Member; -import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.annotation.Nullable; import jakarta.persistence.Column; @@ -33,13 +32,13 @@ public class Comment extends BaseTimeEntity { @Column(name = "comment_id") private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "record_id", nullable = false) - private MissionRecord missionRecord; + @Schema(description = "미션 기록 ID", example = "1") + @Column(name = "record_id", nullable = false) + private Long recordId; // 작성자 @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "writer_id", nullable = false) + @JoinColumn(name = "writer_id") private Member writer; @Schema(description = "댓글 내용", example = "너무 이쁘자나~") @@ -56,17 +55,17 @@ public class Comment extends BaseTimeEntity { private List replyComments = new ArrayList<>(); @Builder(access = AccessLevel.PRIVATE) - public Comment(MissionRecord missionRecord, Member writer, String content, Comment parent) { - this.missionRecord = missionRecord; + public Comment(Long recordId, Member writer, String content, Comment parent) { + this.recordId = recordId; this.writer = writer; this.content = content; this.parent = parent; } public static Comment createComment( - MissionRecord missionRecord, Member writer, String content, @Nullable Comment parent) { + Long recordId, Member writer, String content, @Nullable Comment parent) { return Comment.builder() - .missionRecord(missionRecord) + .recordId(recordId) .writer(writer) .content(content) .parent(parent) From bcd8cbd505faafc6123b89af1f91f88eb7527e2b Mon Sep 17 00:00:00 2001 From: ybchar Date: Sun, 20 Oct 2024 00:59:50 +0900 Subject: [PATCH 39/56] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20writer=20null=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/comment/dao/CommentRepositoryCustom.java | 2 ++ .../domain/comment/dao/CommentRepositoryImpl.java | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepositoryCustom.java b/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepositoryCustom.java index 7d1cb6ac..654e1c39 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepositoryCustom.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepositoryCustom.java @@ -6,4 +6,6 @@ public interface CommentRepositoryCustom { List findAllCommentsByMissionRecord(MissionRecord missionRecord); + + void updateEmptyMemberAllByMember(Long memberId); } diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepositoryImpl.java b/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepositoryImpl.java index 9d7912ed..c5b139d1 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepositoryImpl.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepositoryImpl.java @@ -3,6 +3,7 @@ import static com.depromeet.stonebed.domain.comment.domain.QComment.comment; import com.depromeet.stonebed.domain.comment.domain.Comment; +import com.depromeet.stonebed.domain.member.domain.Member; import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; @@ -21,7 +22,16 @@ public List findAllCommentsByMissionRecord(MissionRecord missionRecord) .fetchJoin() .leftJoin(comment.replyComments) .fetchJoin() - .where(comment.missionRecord.eq(missionRecord)) + .where(comment.recordId.eq(missionRecord.getId())) .fetch(); } + + @Override + public void updateEmptyMemberAllByMember(Long memberId) { + queryFactory + .update(comment) + .set(comment.writer, (Member) null) + .where(comment.writer.id.eq(memberId)) + .execute(); + } } From df0157d08690cb9e2e63a34a541bbcd0b4171737 Mon Sep 17 00:00:00 2001 From: ybchar Date: Sun, 20 Oct 2024 01:00:26 +0900 Subject: [PATCH 40/56] =?UTF-8?q?fix:=20=ED=83=88=ED=87=B4=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20=EB=8C=80=EC=83=81=20null=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentService.java | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java index 3895afe7..ba5559df 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java @@ -64,11 +64,12 @@ private Comment createAndSaveComment( final Comment comment = request.parentId() != null ? Comment.createComment( - missionRecord, + missionRecord.getId(), member, request.content(), findCommentById(request.parentId())) - : Comment.createComment(missionRecord, member, request.content(), null); + : Comment.createComment( + missionRecord.getId(), member, request.content(), null); return commentRepository.save(comment); } @@ -200,6 +201,7 @@ private List convertToCommentFindOneResponses( private CommentFindOneResponse convertToCommentFindOneResponse( Comment comment, Map> commentsByParentId) { + List replyCommentsResponses = commentsByParentId.getOrDefault(comment.getId(), List.of()).stream() .map( @@ -208,13 +210,24 @@ private CommentFindOneResponse convertToCommentFindOneResponse( childComment, commentsByParentId)) .collect(Collectors.toList()); + // 작성자가 null인지 확인 + Long writerId = comment.getWriter() != null ? comment.getWriter().getId() : null; + String writerNickname = + comment.getWriter() != null + ? comment.getWriter().getProfile().getNickname() + : "탈퇴한 회원입니다."; + String writerProfileImageUrl = + comment.getWriter() != null + ? comment.getWriter().getProfile().getProfileImageUrl() + : null; + return CommentFindOneResponse.of( comment.getParent() != null ? comment.getParent().getId() : null, comment.getId(), comment.getContent(), - comment.getWriter().getId(), - comment.getWriter().getProfile().getNickname(), - comment.getWriter().getProfile().getProfileImageUrl(), + writerId, + writerNickname, + writerProfileImageUrl, comment.getCreatedAt().toString(), replyCommentsResponses); } From 3d9269afc48dd7941717b3ba3315df5bfc6df9ba Mon Sep 17 00:00:00 2001 From: ybchar Date: Sun, 20 Oct 2024 01:01:05 +0900 Subject: [PATCH 41/56] =?UTF-8?q?fix:=20comment-record=20=EC=97=B0?= =?UTF-8?q?=EA=B4=80=EA=B4=80=EA=B3=84=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stonebed/domain/missionRecord/domain/MissionRecord.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/missionRecord/domain/MissionRecord.java b/src/main/java/com/depromeet/stonebed/domain/missionRecord/domain/MissionRecord.java index d96a9bd8..b8b21d69 100644 --- a/src/main/java/com/depromeet/stonebed/domain/missionRecord/domain/MissionRecord.java +++ b/src/main/java/com/depromeet/stonebed/domain/missionRecord/domain/MissionRecord.java @@ -33,7 +33,7 @@ public class MissionRecord extends BaseTimeEntity { @JoinColumn(name = "member_id", nullable = false) private Member member; - @OneToMany(mappedBy = "missionRecord", cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) private List comments = new ArrayList<>(); @Schema(description = "미션 이미지 URL", example = "./missionRecord.jpg") From 5c6408fb481f44b35d70eee3ed6e0db0181a25e0 Mon Sep 17 00:00:00 2001 From: ybchar Date: Sun, 20 Oct 2024 01:02:01 +0900 Subject: [PATCH 42/56] =?UTF-8?q?refactor:=20comment=20=EC=97=B0=EA=B4=80?= =?UTF-8?q?=EA=B4=80=EA=B3=84=20=ED=95=B4=EC=A0=9C=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=EC=BF=BC=EB=A6=AC=20=EC=88=98=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stonebed/domain/feed/application/FeedService.java | 1 + .../stonebed/domain/feed/dao/FeedRepositoryImpl.java | 7 +++++-- .../depromeet/stonebed/domain/feed/dto/FindFeedDto.java | 4 ++-- .../feed/dto/response/v1/FeedContentGetResponse.java | 4 ++-- .../feed/dto/response/v2/FeedContentGetResponseV2.java | 4 ++-- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/application/FeedService.java b/src/main/java/com/depromeet/stonebed/domain/feed/application/FeedService.java index 47a2d22c..c31dd199 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/application/FeedService.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/application/FeedService.java @@ -86,6 +86,7 @@ public FeedContentGetResponse findFeedOne(Long recordId) { return FeedContentGetResponse.from(feedOne); } + @Transactional(readOnly = true) public FeedGetResponseV2 findFeedV2(FeedGetRequest request) { List feeds = getFeeds(request.cursor(), request.memberId(), request.limit()); diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryImpl.java b/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryImpl.java index c5edcf00..0e80ab8c 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryImpl.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryImpl.java @@ -1,5 +1,6 @@ package com.depromeet.stonebed.domain.feed.dao; +import static com.depromeet.stonebed.domain.comment.domain.QComment.*; import static com.depromeet.stonebed.domain.member.domain.QMember.*; import static com.depromeet.stonebed.domain.mission.domain.QMission.*; import static com.depromeet.stonebed.domain.missionHistory.domain.QMissionHistory.*; @@ -61,7 +62,7 @@ private JPAQuery getBaseSelectQuery() { mission, missionRecord, member, - Expressions.asNumber(missionRecord.comments.size()).as("totalCommentCount"), + comment.id.count().as("commentCount"), Expressions.asNumber(missionRecordBoost.count.sumLong().coalesce(0L)) .as("totalBoostCount"))); } @@ -75,7 +76,9 @@ private JPAQuery applyJoinsAndConditions(JPAQuery quer .leftJoin(missionHistory) .on(missionRecord.missionHistory.eq(missionHistory)) .leftJoin(mission) - .on(missionHistory.mission.eq(mission)); + .on(missionHistory.mission.eq(mission)) + .leftJoin(comment) + .on(comment.recordId.eq(missionRecord.id)); // Join Comment entity } private BooleanExpression ltMissionRecordId(Long missionRecordId) { diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/dto/FindFeedDto.java b/src/main/java/com/depromeet/stonebed/domain/feed/dto/FindFeedDto.java index a8ab8407..0a968481 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/dto/FindFeedDto.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/dto/FindFeedDto.java @@ -8,13 +8,13 @@ public record FindFeedDto( Mission mission, MissionRecord missionRecord, Member author, - Integer totalCommentCount, + Long totalCommentCount, Long totalBoostCount) { public static FindFeedDto from( Mission mission, MissionRecord missionRecord, Member author, - Integer totalCommentCount, + Long totalCommentCount, Long totalBoostCount) { return new FindFeedDto(mission, missionRecord, author, totalCommentCount, totalBoostCount); } diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v1/FeedContentGetResponse.java b/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v1/FeedContentGetResponse.java index e572059e..b60d4ff3 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v1/FeedContentGetResponse.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v1/FeedContentGetResponse.java @@ -17,7 +17,7 @@ public record FeedContentGetResponse( String missionRecordImageUrl, @Schema(description = "미션 기록 생성일") LocalDate createdDate, @Schema(description = "부스트") Long totalBoostCount, - @Schema(description = "댓글 수", example = "12") Integer totalCommentCount, + @Schema(description = "댓글 수", example = "12") Long totalCommentCount, @Schema(description = "미션 기록 컨텐츠") String content) { public static FeedContentGetResponse from(FindFeedDto missionRecord) { return new FeedContentGetResponse( @@ -29,7 +29,7 @@ public static FeedContentGetResponse from(FindFeedDto missionRecord) { missionRecord.author().getProfile().getNickname(), missionRecord.author().getProfile().getProfileImageUrl(), missionRecord.missionRecord().getImageUrl(), - missionRecord.missionRecord().getCreatedAt().toLocalDate(), + missionRecord.missionRecord().getUpdatedAt().toLocalDate(), missionRecord.totalBoostCount(), missionRecord.totalCommentCount(), missionRecord.missionRecord().getContent()); diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v2/FeedContentGetResponseV2.java b/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v2/FeedContentGetResponseV2.java index 01bbf99a..5bc33656 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v2/FeedContentGetResponseV2.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/dto/response/v2/FeedContentGetResponseV2.java @@ -17,7 +17,7 @@ public record FeedContentGetResponseV2( String missionRecordImageUrl, @Schema(description = "미션 기록 생성일") LocalDate createdDate, @Schema(description = "부스트") Long totalBoostCount, - @Schema(description = "댓글 수", example = "12") Integer totalCommentCount, + @Schema(description = "댓글 수", example = "12") Long totalCommentCount, @Schema(description = "미션 기록 컨텐츠") String content) { public static FeedContentGetResponseV2 from(FindFeedDto missionRecord) { return new FeedContentGetResponseV2( @@ -29,7 +29,7 @@ public static FeedContentGetResponseV2 from(FindFeedDto missionRecord) { missionRecord.author().getProfile().getNickname(), missionRecord.author().getProfile().getProfileImageUrl(), missionRecord.missionRecord().getImageUrl(), - missionRecord.missionRecord().getCreatedAt().toLocalDate(), + missionRecord.missionRecord().getUpdatedAt().toLocalDate(), missionRecord.totalBoostCount(), missionRecord.totalCommentCount(), missionRecord.missionRecord().getContent()); From 26f2eb4fcfcda6d877f78ba99bff3839aafd1375 Mon Sep 17 00:00:00 2001 From: ybchar Date: Sun, 20 Oct 2024 01:02:34 +0900 Subject: [PATCH 43/56] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stonebed/domain/auth/application/AuthService.java | 3 +++ .../stonebed/domain/auth/application/AuthServiceTest.java | 2 ++ .../stonebed/domain/feed/application/FeedServiceTest.java | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java b/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java index bf3f4e48..42fa2485 100644 --- a/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java +++ b/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java @@ -8,6 +8,7 @@ import com.depromeet.stonebed.domain.auth.dto.response.AuthTokenResponse; import com.depromeet.stonebed.domain.auth.dto.response.SocialClientResponse; import com.depromeet.stonebed.domain.auth.dto.response.TokenPairResponse; +import com.depromeet.stonebed.domain.comment.dao.CommentRepository; import com.depromeet.stonebed.domain.fcm.dao.FcmNotificationRepository; import com.depromeet.stonebed.domain.fcm.dao.FcmTokenRepository; import com.depromeet.stonebed.domain.member.dao.MemberRepository; @@ -40,6 +41,7 @@ public class AuthService { private final MissionRecordRepository missionRecordRepository; private final MissionRecordBoostRepository missionRecordBoostRepository; private final FcmTokenRepository fcmTokenRepository; + private final CommentRepository commentRepository; private final AppleClient appleClient; private final KakaoClient kakaoClient; @@ -178,6 +180,7 @@ private void updateMemberNormalStatus(Member member) { } private void withdrawMemberRelationByMemberId(List recordIds, Long memberId) { + commentRepository.updateEmptyMemberAllByMember(memberId); missionRecordBoostRepository.deleteAllByMember(recordIds); missionRecordRepository.deleteAllByMember(memberId); fcmNotificationRepository.deleteAllByMember(memberId); diff --git a/src/test/java/com/depromeet/stonebed/domain/auth/application/AuthServiceTest.java b/src/test/java/com/depromeet/stonebed/domain/auth/application/AuthServiceTest.java index 77ff7d8e..3cb45329 100644 --- a/src/test/java/com/depromeet/stonebed/domain/auth/application/AuthServiceTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/auth/application/AuthServiceTest.java @@ -7,6 +7,7 @@ import com.depromeet.stonebed.domain.auth.domain.OAuthProvider; import com.depromeet.stonebed.domain.auth.dto.response.AuthTokenResponse; import com.depromeet.stonebed.domain.auth.dto.response.TokenPairResponse; +import com.depromeet.stonebed.domain.comment.dao.CommentRepository; import com.depromeet.stonebed.domain.fcm.dao.FcmNotificationRepository; import com.depromeet.stonebed.domain.fcm.dao.FcmTokenRepository; import com.depromeet.stonebed.domain.member.dao.MemberRepository; @@ -40,6 +41,7 @@ class AuthServiceTest extends FixtureMonkeySetUp { @Mock private MissionRecordRepository missionRecordRepository; @Mock private MissionRecordBoostRepository missionRecordBoostRepository; @Mock private FcmTokenRepository fcmTokenRepository; + @Mock private CommentRepository commentRepository; @Mock private MemberUtil memberUtil; diff --git a/src/test/java/com/depromeet/stonebed/domain/feed/application/FeedServiceTest.java b/src/test/java/com/depromeet/stonebed/domain/feed/application/FeedServiceTest.java index 636a3f77..3e72c5bc 100644 --- a/src/test/java/com/depromeet/stonebed/domain/feed/application/FeedServiceTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/feed/application/FeedServiceTest.java @@ -28,7 +28,7 @@ class FeedServiceTest extends FixtureMonkeySetUp { int DEFAULT_LIMIT = 5; Long DEFAULT_TOTAL_BOOST_COUNT = 100L; - Integer DEFAULT_TOTAL_COMMENT_COUNT = 13; + Long DEFAULT_TOTAL_COMMENT_COUNT = 13L; String DEFAULT_CURSOR = "5"; String INVALID_CURSOR = "2024-08-01"; From c5eb4d089bc054b4c79fb7b3e4f33f6e0bc0670f Mon Sep 17 00:00:00 2001 From: ybchar Date: Sun, 20 Oct 2024 16:57:02 +0900 Subject: [PATCH 44/56] =?UTF-8?q?test:=20=EB=B6=80=EB=AA=A8=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/CommentCreateRequest.java | 6 +- .../application/CommentServiceTest.java | 62 ++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/dto/request/CommentCreateRequest.java b/src/main/java/com/depromeet/stonebed/domain/comment/dto/request/CommentCreateRequest.java index 4b0e2d43..17f88ed3 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/dto/request/CommentCreateRequest.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/dto/request/CommentCreateRequest.java @@ -5,4 +5,8 @@ public record CommentCreateRequest( @Schema(description = "댓글 내용", example = "너무 이쁘자나~") String content, @Schema(description = "기록 ID", example = "1") Long recordId, - @Schema(description = "부모 댓글 ID", example = "1") Long parentId) {} + @Schema(description = "부모 댓글 ID", example = "1") Long parentId) { + public static CommentCreateRequest of(String content, Long recordId, Long parentId) { + return new CommentCreateRequest(content, recordId, parentId); + } +} diff --git a/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java b/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java index 57814e4e..c90977f0 100644 --- a/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java @@ -1,9 +1,23 @@ +// src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java + package com.depromeet.stonebed.domain.comment.application; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; import com.depromeet.stonebed.FixtureMonkeySetUp; +import com.depromeet.stonebed.domain.comment.dao.CommentRepository; +import com.depromeet.stonebed.domain.comment.domain.Comment; +import com.depromeet.stonebed.domain.comment.dto.request.CommentCreateRequest; +import com.depromeet.stonebed.domain.comment.dto.response.CommentCreateResponse; +import com.depromeet.stonebed.domain.fcm.application.FcmNotificationService; +import com.depromeet.stonebed.domain.fcm.dao.FcmTokenRepository; +import com.depromeet.stonebed.domain.member.domain.Member; +import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordRepository; +import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; import com.depromeet.stonebed.global.util.MemberUtil; +import java.util.Optional; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -14,6 +28,52 @@ @ExtendWith(MockitoExtension.class) class CommentServiceTest extends FixtureMonkeySetUp { - @InjectMocks private CommentService commentService; @Mock private MemberUtil memberUtil; + + @Mock private CommentRepository commentRepository; + + @Mock private MissionRecordRepository missionRecordRepository; + + @Mock private FcmNotificationService fcmNotificationService; + + @Mock private FcmTokenRepository fcmTokenRepository; + + @InjectMocks private CommentService commentService; + + private Member member; + private MissionRecord missionRecord; + + @Test + void 부모_댓글_생성_성공() { + // given + Long recordId = 1L; + Long parentId = null; + String content = "너무 이쁘자나~"; + + Member member = fixtureMonkey.giveMeOne(Member.class); + MissionRecord missionRecord = fixtureMonkey.giveMeOne(MissionRecord.class); + CommentCreateRequest request = CommentCreateRequest.of(content, recordId, parentId); + Comment comment = + fixtureMonkey + .giveMeBuilder(Comment.class) + .set("missionRecord", missionRecord) + .set("writer", member) + .set("content", content) + .sample(); + + when(memberUtil.getCurrentMember()).thenReturn(member); + when(missionRecordRepository.findById(recordId)).thenReturn(Optional.of(missionRecord)); + when(commentRepository.save(any(Comment.class))).thenReturn(comment); + + // when + CommentCreateResponse response = commentService.createComment(request); + + // then + assertNotNull(response); + assertEquals(comment.getId(), response.commentId()); + + verify(memberUtil).getCurrentMember(); + verify(missionRecordRepository).findById(recordId); + verify(commentRepository).save(any(Comment.class)); + } } From b0e621b360c22c76389cf60c08592596eda5aa48 Mon Sep 17 00:00:00 2001 From: ybchar Date: Sun, 20 Oct 2024 17:45:33 +0900 Subject: [PATCH 45/56] =?UTF-8?q?test:=20=EC=9E=90=EC=8B=9D=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/CommentServiceTest.java | 159 +++++++++++++++--- 1 file changed, 140 insertions(+), 19 deletions(-) diff --git a/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java b/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java index c90977f0..d65d2244 100644 --- a/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java @@ -1,5 +1,3 @@ -// src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java - package com.depromeet.stonebed.domain.comment.application; import static org.junit.jupiter.api.Assertions.*; @@ -16,7 +14,11 @@ import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordRepository; import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; import com.depromeet.stonebed.global.util.MemberUtil; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -40,30 +42,20 @@ class CommentServiceTest extends FixtureMonkeySetUp { @InjectMocks private CommentService commentService; - private Member member; - private MissionRecord missionRecord; + private static final int CHILD_COMMENT_COUNT = 5; // 자식 댓글 생성 횟수 @Test void 부모_댓글_생성_성공() { // given Long recordId = 1L; - Long parentId = null; String content = "너무 이쁘자나~"; Member member = fixtureMonkey.giveMeOne(Member.class); MissionRecord missionRecord = fixtureMonkey.giveMeOne(MissionRecord.class); - CommentCreateRequest request = CommentCreateRequest.of(content, recordId, parentId); - Comment comment = - fixtureMonkey - .giveMeBuilder(Comment.class) - .set("missionRecord", missionRecord) - .set("writer", member) - .set("content", content) - .sample(); + CommentCreateRequest request = CommentCreateRequest.of(content, recordId, null); + Comment comment = createMockComment(member, missionRecord, content); - when(memberUtil.getCurrentMember()).thenReturn(member); - when(missionRecordRepository.findById(recordId)).thenReturn(Optional.of(missionRecord)); - when(commentRepository.save(any(Comment.class))).thenReturn(comment); + mockCommonDependencies(member, missionRecord, recordId, comment); // when CommentCreateResponse response = commentService.createComment(request); @@ -72,8 +64,137 @@ class CommentServiceTest extends FixtureMonkeySetUp { assertNotNull(response); assertEquals(comment.getId(), response.commentId()); - verify(memberUtil).getCurrentMember(); - verify(missionRecordRepository).findById(recordId); - verify(commentRepository).save(any(Comment.class)); + verifyCommonInvocations(recordId); + } + + @Test + void 자식_댓글_생성_성공() { + // given + String content = "너무 이쁘자나~"; + Member member = fixtureMonkey.giveMeOne(Member.class); + MissionRecord missionRecord = fixtureMonkey.giveMeOne(MissionRecord.class); + Long recordId = missionRecord.getId(); + Comment parentComment = createMockComment(member, missionRecord, content); + + mockCommonDependencies(member, missionRecord, recordId, parentComment); + + // when: 부모 댓글 생성 + CommentCreateResponse parentResponse = + commentService.createComment(CommentCreateRequest.of(content, recordId, null)); + + // 부모 댓글 조회 + when(commentRepository.findById(parentResponse.commentId())) + .thenReturn(Optional.of(parentComment)); + + // 자식 댓글 생성 및 검증 + Comment childComment = createMockComment(member, missionRecord, "자식 댓글 내용", parentComment); + when(commentRepository.save(any(Comment.class))).thenReturn(childComment); + + CommentCreateResponse childResponse = + commentService.createComment( + CommentCreateRequest.of( + "자식 댓글 내용", missionRecord.getId(), parentResponse.commentId())); + + // then: 검증 + verifyCommonInvocations(recordId, 2); // 부모 + 자식 댓글 생성 + verify(commentRepository, times(2)).save(any(Comment.class)); + + assertChildComment(childResponse, parentResponse, childComment, parentComment); + } + + @Test + void 부모_댓글에_여러_자식_댓글_생성_성공() { + // given + String content = "부모 댓글 내용"; + Member member = fixtureMonkey.giveMeOne(Member.class); + MissionRecord missionRecord = fixtureMonkey.giveMeOne(MissionRecord.class); + Long recordId = missionRecord.getId(); + Comment parentComment = createMockComment(member, missionRecord, content); + + mockCommonDependencies(member, missionRecord, recordId, parentComment); + + CommentCreateResponse parentResponse = + commentService.createComment(CommentCreateRequest.of(content, recordId, null)); + + // 부모 댓글 조회 + when(commentRepository.findById(parentResponse.commentId())) + .thenReturn(Optional.of(parentComment)); + + // 자식 댓글 생성 및 검증 + List childResponses = new ArrayList<>(); + for (int i = 1; i <= CHILD_COMMENT_COUNT; i++) { + Comment childComment = + createMockComment(member, missionRecord, "자식 댓글 내용 " + i, parentComment); + when(commentRepository.save(any(Comment.class))).thenReturn(childComment); + + CommentCreateResponse childResponse = + commentService.createComment( + CommentCreateRequest.of( + "자식 댓글 내용 " + i, + missionRecord.getId(), + parentResponse.commentId())); + childResponses.add(childResponse); + + assertChildComment(childResponse, parentResponse, childComment, parentComment); + } + + // then: 모든 자식 댓글의 ID가 유일한지 확인 + verifyCommonInvocations(recordId, CHILD_COMMENT_COUNT + 1); // 부모 + 자식 댓글 생성 + verify(commentRepository, times(CHILD_COMMENT_COUNT + 1)).save(any(Comment.class)); + + Set uniqueChildCommentIds = + childResponses.stream() + .map(CommentCreateResponse::commentId) + .collect(Collectors.toSet()); + assertEquals( + CHILD_COMMENT_COUNT, uniqueChildCommentIds.size()); // 자식 댓글 수와 고유한 ID 수가 동일해야 함 + } + + // 중복된 모의 객체 설정을 처리하는 메서드 + private void mockCommonDependencies( + Member member, MissionRecord missionRecord, Long recordId, Comment comment) { + when(memberUtil.getCurrentMember()).thenReturn(member); + when(missionRecordRepository.findById(recordId)).thenReturn(Optional.of(missionRecord)); + when(commentRepository.save(any(Comment.class))).thenReturn(comment); + } + + // 부모/자식 댓글 공통 검증 메서드 + private void assertChildComment( + CommentCreateResponse childResponse, + CommentCreateResponse parentResponse, + Comment childComment, + Comment parentComment) { + assertNotNull(childResponse); + assertNotNull(childResponse.commentId()); + assertNotEquals( + parentResponse.commentId(), childResponse.commentId()); // 부모와 자식 댓글 ID는 달라야 함 + assertEquals(childComment.getId(), childResponse.commentId()); // 자식 댓글 ID 확인 + assertEquals(parentComment.getId(), childComment.getParent().getId()); // 자식 댓글의 부모가 올바른지 확인 + } + + // 중복된 verify 호출을 처리하는 메서드 + private void verifyCommonInvocations(Long recordId, int totalInvocations) { + verify(memberUtil, times(totalInvocations)).getCurrentMember(); + verify(missionRecordRepository, times(totalInvocations)).findById(recordId); + } + + private void verifyCommonInvocations(Long recordId) { + verifyCommonInvocations(recordId, 1); + } + + // Comment 모킹 생성을 처리하는 메서드 + private Comment createMockComment(Member member, MissionRecord missionRecord, String content) { + return createMockComment(member, missionRecord, content, null); + } + + private Comment createMockComment( + Member member, MissionRecord missionRecord, String content, Comment parent) { + return fixtureMonkey + .giveMeBuilder(Comment.class) + .set("missionRecord", missionRecord) + .set("writer", member) + .set("content", content) + .set("parent", parent) + .sample(); } } From c7ac781d6bba2d596f16c0248e9676d4ea0d01d2 Mon Sep 17 00:00:00 2001 From: ybchar Date: Sun, 20 Oct 2024 18:23:35 +0900 Subject: [PATCH 46/56] =?UTF-8?q?test:=20=EB=B6=80=EB=AA=A8=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/CommentServiceTest.java | 95 ++++++++++++++++++- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java b/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java index d65d2244..5f6040b3 100644 --- a/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java @@ -8,6 +8,8 @@ import com.depromeet.stonebed.domain.comment.domain.Comment; import com.depromeet.stonebed.domain.comment.dto.request.CommentCreateRequest; import com.depromeet.stonebed.domain.comment.dto.response.CommentCreateResponse; +import com.depromeet.stonebed.domain.comment.dto.response.CommentFindOneResponse; +import com.depromeet.stonebed.domain.comment.dto.response.CommentFindResponse; import com.depromeet.stonebed.domain.fcm.application.FcmNotificationService; import com.depromeet.stonebed.domain.fcm.dao.FcmTokenRepository; import com.depromeet.stonebed.domain.member.domain.Member; @@ -16,6 +18,7 @@ import com.depromeet.stonebed.global.util.MemberUtil; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -44,8 +47,9 @@ class CommentServiceTest extends FixtureMonkeySetUp { private static final int CHILD_COMMENT_COUNT = 5; // 자식 댓글 생성 횟수 + // 생성 @Test - void 부모_댓글_생성_성공() { + void 부모_댓글_생성합니다() { // given Long recordId = 1L; String content = "너무 이쁘자나~"; @@ -68,7 +72,7 @@ class CommentServiceTest extends FixtureMonkeySetUp { } @Test - void 자식_댓글_생성_성공() { + void 자식_댓글_생성합니다() { // given String content = "너무 이쁘자나~"; Member member = fixtureMonkey.giveMeOne(Member.class); @@ -103,7 +107,7 @@ class CommentServiceTest extends FixtureMonkeySetUp { } @Test - void 부모_댓글에_여러_자식_댓글_생성_성공() { + void 부모_댓글에_여러_자식_댓글을_생성합니다() { // given String content = "부모 댓글 내용"; Member member = fixtureMonkey.giveMeOne(Member.class); @@ -197,4 +201,89 @@ private Comment createMockComment( .set("parent", parent) .sample(); } + + // 조회 + @Test + void 부모_댓글을_조회합니다() { + // given + Long recordId = 1L; + String content = "너무 이쁘자나~"; + + Member member = fixtureMonkey.giveMeOne(Member.class); + MissionRecord missionRecord = fixtureMonkey.giveMeOne(MissionRecord.class); + CommentCreateRequest request = CommentCreateRequest.of(content, recordId, null); + Comment comment = createMockComment(member, missionRecord, content); // 부모 댓글 생성 + + mockCommonDependencies(member, missionRecord, recordId, comment); // 공통 모의 설정 + + // when: 부모 댓글 생성 + CommentCreateResponse response = commentService.createComment(request); + + // 부모 댓글 조회 + when(commentRepository.findById(response.commentId())).thenReturn(Optional.of(comment)); + + // 부모 댓글을 포함한 댓글 리스트 생성 + Comment parentComment = commentRepository.findById(response.commentId()).orElse(null); + List comments = List.of(Objects.requireNonNull(parentComment)); + + // 부모 댓글을 CommentFindOneResponse로 변환 + List commentResponses = + comments.stream() + .map( + comment1 -> + CommentFindOneResponse.of( + comment1.getParent() != null + ? comment1.getParent().getId() + : null, + comment1.getId(), + comment1.getContent(), + comment1.getWriter().getId(), + comment1.getWriter().getProfile().getNickname(), + comment1.getWriter() + .getProfile() + .getProfileImageUrl(), + comment1.getCreatedAt().toString(), + comment1.getReplyComments().stream() + .map( + reply -> + CommentFindOneResponse.of( + reply.getParent() + != null + ? reply.getParent() + .getId() + : null, + reply.getId(), + reply.getContent(), + reply.getWriter() + .getId(), + reply.getWriter() + .getProfile() + .getNickname(), + reply.getWriter() + .getProfile() + .getProfileImageUrl(), + reply.getCreatedAt() + .toString(), + List.of())) + .collect(Collectors.toList()))) + .toList(); + + // 모의 객체 설정 + when(missionRecordRepository.findById(recordId)).thenReturn(Optional.of(missionRecord)); + when(commentRepository.findAllCommentsByMissionRecord(missionRecord)).thenReturn(comments); + + // when: 댓글 조회 + CommentFindResponse result = commentService.findCommentsByRecordId(recordId); + + // then: 검증 + assertNotNull(result); + assertEquals(commentResponses.size(), result.comments().size()); // 댓글 개수 비교 + assertEquals( + commentResponses.get(0).commentId(), + result.comments().get(0).commentId()); // 첫 번째 댓글 ID 비교 + + assertEquals(commentResponses.get(0).content(), content); // 첫 번째 댓글 내용 비교 + assertEquals(result.comments().get(0).content(), content); + assertEquals(commentResponses.get(0).content(), result.comments().get(0).content()); + } } From 122a651a90e2775545325cd65a5ea88bce7337fe Mon Sep 17 00:00:00 2001 From: ybchar Date: Mon, 21 Oct 2024 00:59:25 +0900 Subject: [PATCH 47/56] =?UTF-8?q?test:=20=EC=9E=90=EC=8B=9D=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/CommentServiceTest.java | 86 +++++++++++++++++-- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java b/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java index 5f6040b3..efe88885 100644 --- a/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +import static org.springframework.transaction.annotation.Isolation.*; import com.depromeet.stonebed.FixtureMonkeySetUp; import com.depromeet.stonebed.domain.comment.dao.CommentRepository; @@ -57,7 +58,7 @@ class CommentServiceTest extends FixtureMonkeySetUp { Member member = fixtureMonkey.giveMeOne(Member.class); MissionRecord missionRecord = fixtureMonkey.giveMeOne(MissionRecord.class); CommentCreateRequest request = CommentCreateRequest.of(content, recordId, null); - Comment comment = createMockComment(member, missionRecord, content); + Comment comment = createMockParentComment(member, missionRecord, content); mockCommonDependencies(member, missionRecord, recordId, comment); @@ -78,7 +79,7 @@ class CommentServiceTest extends FixtureMonkeySetUp { Member member = fixtureMonkey.giveMeOne(Member.class); MissionRecord missionRecord = fixtureMonkey.giveMeOne(MissionRecord.class); Long recordId = missionRecord.getId(); - Comment parentComment = createMockComment(member, missionRecord, content); + Comment parentComment = createMockParentComment(member, missionRecord, content); mockCommonDependencies(member, missionRecord, recordId, parentComment); @@ -113,7 +114,7 @@ class CommentServiceTest extends FixtureMonkeySetUp { Member member = fixtureMonkey.giveMeOne(Member.class); MissionRecord missionRecord = fixtureMonkey.giveMeOne(MissionRecord.class); Long recordId = missionRecord.getId(); - Comment parentComment = createMockComment(member, missionRecord, content); + Comment parentComment = createMockParentComment(member, missionRecord, content); mockCommonDependencies(member, missionRecord, recordId, parentComment); @@ -187,7 +188,8 @@ private void verifyCommonInvocations(Long recordId) { } // Comment 모킹 생성을 처리하는 메서드 - private Comment createMockComment(Member member, MissionRecord missionRecord, String content) { + private Comment createMockParentComment( + Member member, MissionRecord missionRecord, String content) { return createMockComment(member, missionRecord, content, null); } @@ -212,7 +214,7 @@ private Comment createMockComment( Member member = fixtureMonkey.giveMeOne(Member.class); MissionRecord missionRecord = fixtureMonkey.giveMeOne(MissionRecord.class); CommentCreateRequest request = CommentCreateRequest.of(content, recordId, null); - Comment comment = createMockComment(member, missionRecord, content); // 부모 댓글 생성 + Comment comment = createMockParentComment(member, missionRecord, content); // 부모 댓글 생성 mockCommonDependencies(member, missionRecord, recordId, comment); // 공통 모의 설정 @@ -286,4 +288,78 @@ private Comment createMockComment( assertEquals(result.comments().get(0).content(), content); assertEquals(commentResponses.get(0).content(), result.comments().get(0).content()); } + + @Test + void 자식_댓글을_조회합니다() { + // given + Long recordId = 1L; + String parentContent = "부모 댓글입니다."; + String childContentPrefix = "자식 댓글 내용 "; + + // 부모 댓글 생성 + Member member = fixtureMonkey.giveMeOne(Member.class); + MissionRecord missionRecord = fixtureMonkey.giveMeOne(MissionRecord.class); + Comment parentComment = createMockParentComment(member, missionRecord, parentContent); + + // 자식 댓글 생성 + List childComments = new ArrayList<>(); + for (int i = 1; i <= CHILD_COMMENT_COUNT; i++) { + String childContent = childContentPrefix + i; + Comment childComment = + createMockComment(member, missionRecord, childContent, parentComment); + childComments.add(childComment); + } + + // 부모 댓글과 자식 댓글이 포함된 댓글 리스트 생성 + List allComments = new ArrayList<>(); + allComments.add(parentComment); + allComments.addAll(childComments); + + // Mock 설정: 댓글 조회 + when(missionRecordRepository.findById(recordId)).thenReturn(Optional.of(missionRecord)); + when(commentRepository.findAllCommentsByMissionRecord(missionRecord)) + .thenReturn(allComments); + + // when: 댓글 조회 메서드 호출 + CommentFindResponse result = commentService.findCommentsByRecordId(recordId); + + // then: 부모 댓글 검증 + assertNotNull(result); + assertEquals(1, result.comments().size(), "부모 댓글은 하나만 있어야 합니다."); + + CommentFindOneResponse parentResponse = result.comments().get(0); + assertEquals(parentComment.getId(), parentResponse.commentId(), "부모 댓글 ID가 일치해야 합니다."); + assertEquals(parentComment.getContent(), parentResponse.content(), "부모 댓글 내용이 일치해야 합니다."); + assertEquals( + parentComment.getWriter().getId(), + parentResponse.writerId(), + "부모 댓글 작성자 ID가 일치해야 합니다."); + assertEquals( + CHILD_COMMENT_COUNT, parentResponse.replyComments().size(), "자식 댓글의 개수가 일치해야 합니다."); + + // 자식 댓글 검증 + for (int i = 0; i < CHILD_COMMENT_COUNT; i++) { + CommentFindOneResponse childResponse = parentResponse.replyComments().get(i); + Comment expectedChildComment = childComments.get(i); + + assertEquals( + expectedChildComment.getId(), childResponse.commentId(), "자식 댓글 ID가 일치해야 합니다."); + assertEquals( + expectedChildComment.getContent(), + childResponse.content(), + "자식 댓글 내용이 일치해야 합니다."); + assertEquals( + expectedChildComment.getWriter().getId(), + childResponse.writerId(), + "자식 댓글 작성자 ID가 일치해야 합니다."); + assertEquals( + parentComment.getId(), childResponse.parentId(), "자식 댓글의 부모 ID가 일치해야 합니다."); + } + + // 자식 댓글의 부모가 부모 댓글로 설정되었는지 확인 + assertTrue( + parentResponse.replyComments().stream() + .allMatch(reply -> reply.parentId().equals(parentComment.getId())), + "모든 자식 댓글의 부모 ID는 부모 댓글 ID와 일치해야 합니다."); + } } From f253cbb9f6355cd9a99a57908971c4062aaa6e00 Mon Sep 17 00:00:00 2001 From: ybchar Date: Mon, 21 Oct 2024 01:01:05 +0900 Subject: [PATCH 48/56] =?UTF-8?q?test:=20=EC=9E=90=EC=8B=9D=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentServiceTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java b/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java index efe88885..77fce14b 100644 --- a/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java @@ -361,5 +361,18 @@ private Comment createMockComment( parentResponse.replyComments().stream() .allMatch(reply -> reply.parentId().equals(parentComment.getId())), "모든 자식 댓글의 부모 ID는 부모 댓글 ID와 일치해야 합니다."); + // 자식 댓글 내용 검증 + assertTrue( + parentResponse.replyComments().stream() + .allMatch(reply -> reply.content().startsWith(childContentPrefix)), + "모든 자식 댓글의 내용은 '자식 댓글 내용'으로 시작해야 합니다."); + // assertEquals로 자식 댓글 내용 검증 + + for (int i = 0; i < CHILD_COMMENT_COUNT; i++) { + assertEquals( + childContentPrefix + (i + 1), + parentResponse.replyComments().get(i).content(), + "자식 댓글 내용이 일치해야 합니다."); + } } } From f3838378f28ee9f4cb6601c6127fb6f00dc9e46e Mon Sep 17 00:00:00 2001 From: ybchar Date: Mon, 21 Oct 2024 22:19:22 +0900 Subject: [PATCH 49/56] =?UTF-8?q?fix:=20=ED=83=88=ED=87=B4=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20nickname=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stonebed/domain/comment/application/CommentService.java | 2 +- .../stonebed/domain/comment/application/CommentServiceTest.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java index ba5559df..9d2d0448 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java @@ -215,7 +215,7 @@ private CommentFindOneResponse convertToCommentFindOneResponse( String writerNickname = comment.getWriter() != null ? comment.getWriter().getProfile().getNickname() - : "탈퇴한 회원입니다."; + : "탈퇴한 회원"; String writerProfileImageUrl = comment.getWriter() != null ? comment.getWriter().getProfile().getProfileImageUrl() diff --git a/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java b/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java index 77fce14b..0cafc2af 100644 --- a/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java @@ -2,7 +2,6 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; -import static org.springframework.transaction.annotation.Isolation.*; import com.depromeet.stonebed.FixtureMonkeySetUp; import com.depromeet.stonebed.domain.comment.dao.CommentRepository; From 9f5fd3604e4891869bca46231a9df376dc412ca4 Mon Sep 17 00:00:00 2001 From: ybchar Date: Tue, 22 Oct 2024 12:09:23 +0900 Subject: [PATCH 50/56] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=ED=83=88=ED=87=B4=20report=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stonebed/domain/auth/application/AuthService.java | 5 ++++- .../missionRecord/dao/MissionRecordBoostRepository.java | 3 +-- .../stonebed/domain/report/dao/ReportRepository.java | 8 +++++++- .../stonebed/domain/auth/application/AuthServiceTest.java | 2 ++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java b/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java index 42fa2485..4d95ea6e 100644 --- a/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java +++ b/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java @@ -20,6 +20,7 @@ import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordBoostRepository; import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordRepository; import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; +import com.depromeet.stonebed.domain.report.dao.ReportRepository; import com.depromeet.stonebed.global.error.ErrorCode; import com.depromeet.stonebed.global.error.exception.CustomException; import com.depromeet.stonebed.global.security.JwtTokenProvider; @@ -42,6 +43,7 @@ public class AuthService { private final MissionRecordBoostRepository missionRecordBoostRepository; private final FcmTokenRepository fcmTokenRepository; private final CommentRepository commentRepository; + private final ReportRepository reportRepository; private final AppleClient appleClient; private final KakaoClient kakaoClient; @@ -180,8 +182,9 @@ private void updateMemberNormalStatus(Member member) { } private void withdrawMemberRelationByMemberId(List recordIds, Long memberId) { + reportRepository.deleteAllByMember(memberId); commentRepository.updateEmptyMemberAllByMember(memberId); - missionRecordBoostRepository.deleteAllByMember(recordIds); + missionRecordBoostRepository.deleteAllByRecordIds(recordIds); missionRecordRepository.deleteAllByMember(memberId); fcmNotificationRepository.deleteAllByMember(memberId); fcmTokenRepository.deleteAllByMember(memberId); diff --git a/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordBoostRepository.java b/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordBoostRepository.java index bbb2a28b..ce3bfdde 100644 --- a/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordBoostRepository.java +++ b/src/main/java/com/depromeet/stonebed/domain/missionRecord/dao/MissionRecordBoostRepository.java @@ -12,8 +12,7 @@ public interface MissionRecordBoostRepository extends JpaRepository missionRecordIds); + void deleteAllByRecordIds(@Param("missionRecordIds") List missionRecordIds); } diff --git a/src/main/java/com/depromeet/stonebed/domain/report/dao/ReportRepository.java b/src/main/java/com/depromeet/stonebed/domain/report/dao/ReportRepository.java index fb70a0e3..d9a49e6b 100644 --- a/src/main/java/com/depromeet/stonebed/domain/report/dao/ReportRepository.java +++ b/src/main/java/com/depromeet/stonebed/domain/report/dao/ReportRepository.java @@ -2,5 +2,11 @@ import com.depromeet.stonebed.domain.report.domain.Report; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; -public interface ReportRepository extends JpaRepository {} +public interface ReportRepository extends JpaRepository { + @Modifying + @Query("DELETE FROM Report r WHERE r.member.id = :memberId") + void deleteAllByMember(Long memberId); +} diff --git a/src/test/java/com/depromeet/stonebed/domain/auth/application/AuthServiceTest.java b/src/test/java/com/depromeet/stonebed/domain/auth/application/AuthServiceTest.java index 3cb45329..c608da4e 100644 --- a/src/test/java/com/depromeet/stonebed/domain/auth/application/AuthServiceTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/auth/application/AuthServiceTest.java @@ -16,6 +16,7 @@ import com.depromeet.stonebed.domain.member.domain.MemberStatus; import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordBoostRepository; import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordRepository; +import com.depromeet.stonebed.domain.report.dao.ReportRepository; import com.depromeet.stonebed.global.security.JwtTokenProvider; import com.depromeet.stonebed.global.util.MemberUtil; import java.util.Optional; @@ -42,6 +43,7 @@ class AuthServiceTest extends FixtureMonkeySetUp { @Mock private MissionRecordBoostRepository missionRecordBoostRepository; @Mock private FcmTokenRepository fcmTokenRepository; @Mock private CommentRepository commentRepository; + @Mock private ReportRepository reportRepository; @Mock private MemberUtil memberUtil; From be6044b6feff482cd2093a78cf8a315834087b28 Mon Sep 17 00:00:00 2001 From: ybchar Date: Tue, 22 Oct 2024 13:32:56 +0900 Subject: [PATCH 51/56] =?UTF-8?q?refactor:=20report=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/application/AuthService.java | 1 - .../domain/report/api/ReportController.java | 10 +++--- .../report/application/ReportService.java | 22 +++++++----- .../domain/report/dao/ReportRepository.java | 2 +- .../stonebed/domain/report/domain/Report.java | 35 +++++++++++++------ .../domain/report/domain/ReportDomain.java | 12 +++++++ ...tRequest.java => ReportCreateRequest.java} | 2 +- 7 files changed, 58 insertions(+), 26 deletions(-) create mode 100644 src/main/java/com/depromeet/stonebed/domain/report/domain/ReportDomain.java rename src/main/java/com/depromeet/stonebed/domain/report/dto/request/{ReportRequest.java => ReportCreateRequest.java} (95%) diff --git a/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java b/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java index 4d95ea6e..d8cbc495 100644 --- a/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java +++ b/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java @@ -182,7 +182,6 @@ private void updateMemberNormalStatus(Member member) { } private void withdrawMemberRelationByMemberId(List recordIds, Long memberId) { - reportRepository.deleteAllByMember(memberId); commentRepository.updateEmptyMemberAllByMember(memberId); missionRecordBoostRepository.deleteAllByRecordIds(recordIds); missionRecordRepository.deleteAllByMember(memberId); diff --git a/src/main/java/com/depromeet/stonebed/domain/report/api/ReportController.java b/src/main/java/com/depromeet/stonebed/domain/report/api/ReportController.java index aa9c13d8..e274fdbc 100644 --- a/src/main/java/com/depromeet/stonebed/domain/report/api/ReportController.java +++ b/src/main/java/com/depromeet/stonebed/domain/report/api/ReportController.java @@ -1,7 +1,7 @@ package com.depromeet.stonebed.domain.report.api; import com.depromeet.stonebed.domain.report.application.ReportService; -import com.depromeet.stonebed.domain.report.dto.request.ReportRequest; +import com.depromeet.stonebed.domain.report.dto.request.ReportCreateRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -19,10 +19,10 @@ public class ReportController { private final ReportService reportService; - @Operation(summary = "신고하기", description = "특정 피드를 신고한다.") - @PostMapping - public ResponseEntity reportFeed(@RequestBody ReportRequest reportRequest) { - reportService.reportFeed(reportRequest); + @Operation(summary = "피드 신고하기", description = "특정 피드를 신고한다.") + @PostMapping("/feed") + public ResponseEntity reportFeed(@RequestBody ReportCreateRequest reportCreateRequest) { + reportService.reportFeed(reportCreateRequest); return ResponseEntity.status(HttpStatus.CREATED).build(); } } diff --git a/src/main/java/com/depromeet/stonebed/domain/report/application/ReportService.java b/src/main/java/com/depromeet/stonebed/domain/report/application/ReportService.java index 64bc0906..e7827f2e 100644 --- a/src/main/java/com/depromeet/stonebed/domain/report/application/ReportService.java +++ b/src/main/java/com/depromeet/stonebed/domain/report/application/ReportService.java @@ -6,7 +6,8 @@ import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; import com.depromeet.stonebed.domain.report.dao.ReportRepository; import com.depromeet.stonebed.domain.report.domain.Report; -import com.depromeet.stonebed.domain.report.dto.request.ReportRequest; +import com.depromeet.stonebed.domain.report.domain.ReportDomain; +import com.depromeet.stonebed.domain.report.dto.request.ReportCreateRequest; import com.depromeet.stonebed.global.error.ErrorCode; import com.depromeet.stonebed.global.error.exception.CustomException; import com.depromeet.stonebed.global.util.MemberUtil; @@ -26,30 +27,35 @@ public class ReportService { private final MemberUtil memberUtil; private final DiscordNotificationService discordNotificationService; - public void reportFeed(ReportRequest reportRequest) { + public void reportFeed(ReportCreateRequest reportCreateRequest) { final Member reporter = memberUtil.getCurrentMember(); MissionRecord missionRecord = missionRecordRepository - .findById(reportRequest.recordId()) + .findById(reportCreateRequest.recordId()) .orElseThrow(() -> new CustomException(ErrorCode.MISSION_RECORD_NOT_FOUND)); Member reportedMember = missionRecord.getMember(); Report report = Report.createReport( - missionRecord, reporter, reportRequest.reason(), reportRequest.details()); + missionRecord.getId(), + reporter, + ReportDomain.MISSION_RECORD, + reportCreateRequest.reason(), + reportCreateRequest.details()); reportRepository.save(report); - sendReportNotificationToDiscord(reporter, reportedMember, missionRecord, reportRequest); + sendReportNotificationToDiscord( + reporter, reportedMember, missionRecord, reportCreateRequest); } private void sendReportNotificationToDiscord( Member reporter, Member reportedMember, MissionRecord missionRecord, - ReportRequest reportRequest) { + ReportCreateRequest reportCreateRequest) { String reportTime = java.time.LocalDateTime.now().format(DATE_TIME_FORMATTER); String message = @@ -68,8 +74,8 @@ private void sendReportNotificationToDiscord( + "**게시글 내용**: %s", reporter.getProfile().getNickname(), reportTime, - reportRequest.reason(), - reportRequest.details(), + reportCreateRequest.reason(), + reportCreateRequest.details(), reportedMember.getProfile().getNickname(), missionRecord.getId(), missionRecord.getImageUrl() != null diff --git a/src/main/java/com/depromeet/stonebed/domain/report/dao/ReportRepository.java b/src/main/java/com/depromeet/stonebed/domain/report/dao/ReportRepository.java index d9a49e6b..76f7c775 100644 --- a/src/main/java/com/depromeet/stonebed/domain/report/dao/ReportRepository.java +++ b/src/main/java/com/depromeet/stonebed/domain/report/dao/ReportRepository.java @@ -7,6 +7,6 @@ public interface ReportRepository extends JpaRepository { @Modifying - @Query("DELETE FROM Report r WHERE r.member.id = :memberId") + @Query("DELETE FROM Report r WHERE r.member.id in (:memberId)") void deleteAllByMember(Long memberId); } diff --git a/src/main/java/com/depromeet/stonebed/domain/report/domain/Report.java b/src/main/java/com/depromeet/stonebed/domain/report/domain/Report.java index 80d962b1..65fe9a58 100644 --- a/src/main/java/com/depromeet/stonebed/domain/report/domain/Report.java +++ b/src/main/java/com/depromeet/stonebed/domain/report/domain/Report.java @@ -2,8 +2,10 @@ import com.depromeet.stonebed.domain.common.BaseTimeEntity; import com.depromeet.stonebed.domain.member.domain.Member; -import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; +import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -19,38 +21,51 @@ @Getter @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "feed_report") +@Table(name = "report") public class Report extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "mission_record_id", nullable = false) - private MissionRecord missionRecord; - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id", nullable = false) private Member member; + @Enumerated(EnumType.STRING) + @Column(name = "report_domain", nullable = false) + private ReportDomain reportDomain; + + private Long targetId; + private String reason; private String details; @Builder(access = AccessLevel.PRIVATE) - private Report(MissionRecord missionRecord, Member member, String reason, String details) { - this.missionRecord = missionRecord; + private Report( + Long targetId, + Member member, + ReportDomain reportDomain, + String reason, + String details) { + this.targetId = targetId; this.member = member; + this.reportDomain = reportDomain; this.reason = reason; this.details = details; } public static Report createReport( - MissionRecord missionRecord, Member member, String reportReason, String details) { + Long targetId, + Member member, + ReportDomain reportDomain, + String reportReason, + String details) { return Report.builder() - .missionRecord(missionRecord) + .targetId(targetId) .member(member) + .reportDomain(reportDomain) .reason(reportReason) .details(details) .build(); diff --git a/src/main/java/com/depromeet/stonebed/domain/report/domain/ReportDomain.java b/src/main/java/com/depromeet/stonebed/domain/report/domain/ReportDomain.java new file mode 100644 index 00000000..feb470e1 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/report/domain/ReportDomain.java @@ -0,0 +1,12 @@ +package com.depromeet.stonebed.domain.report.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ReportDomain { + MISSION_RECORD("미션 기록"), + ; + private final String value; +} diff --git a/src/main/java/com/depromeet/stonebed/domain/report/dto/request/ReportRequest.java b/src/main/java/com/depromeet/stonebed/domain/report/dto/request/ReportCreateRequest.java similarity index 95% rename from src/main/java/com/depromeet/stonebed/domain/report/dto/request/ReportRequest.java rename to src/main/java/com/depromeet/stonebed/domain/report/dto/request/ReportCreateRequest.java index fe0be098..16dd5b61 100644 --- a/src/main/java/com/depromeet/stonebed/domain/report/dto/request/ReportRequest.java +++ b/src/main/java/com/depromeet/stonebed/domain/report/dto/request/ReportCreateRequest.java @@ -5,7 +5,7 @@ import jakarta.validation.constraints.Size; @Schema(description = "신고 요청 정보") -public record ReportRequest( +public record ReportCreateRequest( @Schema(description = "신고할 대상 기록의 ID", example = "123", required = true) @NotNull Long recordId, @Schema(description = "신고 사유", example = "사기 또는 사칭", required = true) @NotNull From 8eb390bf4db89220ee93bed6a13e49a82f564bae Mon Sep 17 00:00:00 2001 From: ybchar Date: Tue, 22 Oct 2024 13:37:11 +0900 Subject: [PATCH 52/56] fix: member -> reporter --- .../stonebed/domain/report/domain/Report.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/report/domain/Report.java b/src/main/java/com/depromeet/stonebed/domain/report/domain/Report.java index 65fe9a58..b3a42dbf 100644 --- a/src/main/java/com/depromeet/stonebed/domain/report/domain/Report.java +++ b/src/main/java/com/depromeet/stonebed/domain/report/domain/Report.java @@ -29,8 +29,8 @@ public class Report extends BaseTimeEntity { private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false) - private Member member; + @JoinColumn(name = "reporter_id", nullable = false) + private Member reporter; @Enumerated(EnumType.STRING) @Column(name = "report_domain", nullable = false) @@ -45,12 +45,12 @@ public class Report extends BaseTimeEntity { @Builder(access = AccessLevel.PRIVATE) private Report( Long targetId, - Member member, + Member reporter, ReportDomain reportDomain, String reason, String details) { this.targetId = targetId; - this.member = member; + this.reporter = reporter; this.reportDomain = reportDomain; this.reason = reason; this.details = details; @@ -58,13 +58,13 @@ private Report( public static Report createReport( Long targetId, - Member member, + Member reporter, ReportDomain reportDomain, String reportReason, String details) { return Report.builder() .targetId(targetId) - .member(member) + .reporter(reporter) .reportDomain(reportDomain) .reason(reportReason) .details(details) From 346bd48e41f7334fb25ef19e1ac44645f432e5a8 Mon Sep 17 00:00:00 2001 From: ybchar Date: Tue, 22 Oct 2024 13:41:41 +0900 Subject: [PATCH 53/56] =?UTF-8?q?fix:=20repository=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stonebed/domain/auth/application/AuthService.java | 2 -- .../stonebed/domain/report/dao/ReportRepository.java | 8 +------- .../stonebed/domain/auth/application/AuthServiceTest.java | 2 -- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java b/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java index d8cbc495..7e78054c 100644 --- a/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java +++ b/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java @@ -20,7 +20,6 @@ import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordBoostRepository; import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordRepository; import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; -import com.depromeet.stonebed.domain.report.dao.ReportRepository; import com.depromeet.stonebed.global.error.ErrorCode; import com.depromeet.stonebed.global.error.exception.CustomException; import com.depromeet.stonebed.global.security.JwtTokenProvider; @@ -43,7 +42,6 @@ public class AuthService { private final MissionRecordBoostRepository missionRecordBoostRepository; private final FcmTokenRepository fcmTokenRepository; private final CommentRepository commentRepository; - private final ReportRepository reportRepository; private final AppleClient appleClient; private final KakaoClient kakaoClient; diff --git a/src/main/java/com/depromeet/stonebed/domain/report/dao/ReportRepository.java b/src/main/java/com/depromeet/stonebed/domain/report/dao/ReportRepository.java index 76f7c775..fb70a0e3 100644 --- a/src/main/java/com/depromeet/stonebed/domain/report/dao/ReportRepository.java +++ b/src/main/java/com/depromeet/stonebed/domain/report/dao/ReportRepository.java @@ -2,11 +2,5 @@ import com.depromeet.stonebed.domain.report.domain.Report; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -public interface ReportRepository extends JpaRepository { - @Modifying - @Query("DELETE FROM Report r WHERE r.member.id in (:memberId)") - void deleteAllByMember(Long memberId); -} +public interface ReportRepository extends JpaRepository {} diff --git a/src/test/java/com/depromeet/stonebed/domain/auth/application/AuthServiceTest.java b/src/test/java/com/depromeet/stonebed/domain/auth/application/AuthServiceTest.java index c608da4e..3cb45329 100644 --- a/src/test/java/com/depromeet/stonebed/domain/auth/application/AuthServiceTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/auth/application/AuthServiceTest.java @@ -16,7 +16,6 @@ import com.depromeet.stonebed.domain.member.domain.MemberStatus; import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordBoostRepository; import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordRepository; -import com.depromeet.stonebed.domain.report.dao.ReportRepository; import com.depromeet.stonebed.global.security.JwtTokenProvider; import com.depromeet.stonebed.global.util.MemberUtil; import java.util.Optional; @@ -43,7 +42,6 @@ class AuthServiceTest extends FixtureMonkeySetUp { @Mock private MissionRecordBoostRepository missionRecordBoostRepository; @Mock private FcmTokenRepository fcmTokenRepository; @Mock private CommentRepository commentRepository; - @Mock private ReportRepository reportRepository; @Mock private MemberUtil memberUtil; From 5596675157ea6d2cdc9da305655fef95017b83b4 Mon Sep 17 00:00:00 2001 From: ybchar Date: Tue, 22 Oct 2024 16:13:35 +0900 Subject: [PATCH 54/56] =?UTF-8?q?hotfix:=20=ED=94=BC=EB=93=9C=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EC=88=98=20=EC=A1=B0=ED=9A=8C=20=EC=BF=BC=EB=A6=AC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=ED=83=88=ED=87=B4=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/application/AuthService.java | 1 - .../domain/comment/application/CommentService.java | 12 ++++++++---- .../stonebed/domain/feed/dao/FeedRepositoryImpl.java | 10 ++++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java b/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java index 7e78054c..33b5bdd7 100644 --- a/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java +++ b/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java @@ -180,7 +180,6 @@ private void updateMemberNormalStatus(Member member) { } private void withdrawMemberRelationByMemberId(List recordIds, Long memberId) { - commentRepository.updateEmptyMemberAllByMember(memberId); missionRecordBoostRepository.deleteAllByRecordIds(recordIds); missionRecordRepository.deleteAllByMember(memberId); fcmNotificationRepository.deleteAllByMember(memberId); diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java index 9d2d0448..5f21a088 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java @@ -11,6 +11,7 @@ import com.depromeet.stonebed.domain.fcm.domain.FcmNotificationType; import com.depromeet.stonebed.domain.fcm.domain.FcmToken; import com.depromeet.stonebed.domain.member.domain.Member; +import com.depromeet.stonebed.domain.member.domain.MemberStatus; import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordRepository; import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; import com.depromeet.stonebed.global.common.constants.FcmNotificationConstants; @@ -211,15 +212,18 @@ private CommentFindOneResponse convertToCommentFindOneResponse( .collect(Collectors.toList()); // 작성자가 null인지 확인 - Long writerId = comment.getWriter() != null ? comment.getWriter().getId() : null; + Long writerId = + comment.getWriter().getStatus() != MemberStatus.DELETED + ? comment.getWriter().getId() + : null; String writerNickname = - comment.getWriter() != null + comment.getWriter().getStatus() != MemberStatus.DELETED ? comment.getWriter().getProfile().getNickname() : "탈퇴한 회원"; String writerProfileImageUrl = - comment.getWriter() != null + comment.getWriter().getStatus() != MemberStatus.DELETED ? comment.getWriter().getProfile().getProfileImageUrl() - : null; + : "INACTIVE_" + comment.getWriter().getRaisePet().getValue(); return CommentFindOneResponse.of( comment.getParent() != null ? comment.getParent().getId() : null, diff --git a/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryImpl.java b/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryImpl.java index 0e80ab8c..80fa3bc2 100644 --- a/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryImpl.java +++ b/src/main/java/com/depromeet/stonebed/domain/feed/dao/FeedRepositoryImpl.java @@ -13,6 +13,7 @@ import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; @@ -62,7 +63,10 @@ private JPAQuery getBaseSelectQuery() { mission, missionRecord, member, - comment.id.count().as("commentCount"), + // 서브쿼리를 통해 댓글 개수 계산 + JPAExpressions.select(comment.id.count()) + .from(comment) + .where(comment.recordId.eq(missionRecord.id)), Expressions.asNumber(missionRecordBoost.count.sumLong().coalesce(0L)) .as("totalBoostCount"))); } @@ -76,9 +80,7 @@ private JPAQuery applyJoinsAndConditions(JPAQuery quer .leftJoin(missionHistory) .on(missionRecord.missionHistory.eq(missionHistory)) .leftJoin(mission) - .on(missionHistory.mission.eq(mission)) - .leftJoin(comment) - .on(comment.recordId.eq(missionRecord.id)); // Join Comment entity + .on(missionHistory.mission.eq(mission)); } private BooleanExpression ltMissionRecordId(Long missionRecordId) { From acd37624400ae2907e7bc121a4d6bd230460bac5 Mon Sep 17 00:00:00 2001 From: ybchar Date: Tue, 22 Oct 2024 17:12:54 +0900 Subject: [PATCH 55/56] =?UTF-8?q?chore:=20=EB=B0=B0=ED=8F=AC=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20workflow=20=EC=B6=94=EA=B0=80=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/develop-deploy.yml | 45 +++++++++++++++++++++++++ .github/workflows/production-deploy.yml | 44 ++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 .github/workflows/develop-deploy.yml create mode 100644 .github/workflows/production-deploy.yml diff --git a/.github/workflows/develop-deploy.yml b/.github/workflows/develop-deploy.yml new file mode 100644 index 00000000..5d794522 --- /dev/null +++ b/.github/workflows/develop-deploy.yml @@ -0,0 +1,45 @@ +name: Develop Deploy + +on: + workflow_dispatch: + inputs: + commit_hash: + description: 'commit_hash' + required: true + +env: + DOCKERHUB_IMAGE_NAME: walwal-server + +jobs: + build-deploy: + runs-on: ubuntu-latest + environment: DEV + steps: + # EC2로 배포 + - name: Deploy to EC2 Server + uses: appleboy/ssh-action@v1.0.3 + env: + IMAGE_FULL_URL: ${{ steps.metadata.outputs.tags }} + DOCKERHUB_IMAGE_NAME: ${{ env.DOCKERHUB_IMAGE_NAME }} + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USERNAME }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + envs: IMAGE_FULL_URL, DOCKERHUB_IMAGE_NAME # docker-compose.yml 에서 사용할 환경 변수 + debug: true + script: | + echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin + docker compose up -d + docker exec -d nginx nginx -s reload + docker image prune -a -f + + ## Slack + - name: Slack Alarm + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + author_name: GitHub-Actions CI/CD + fields: repo,message,commit,author,ref,job,took + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required + if: always() # Pick up events even if the job fails or is canceled. diff --git a/.github/workflows/production-deploy.yml b/.github/workflows/production-deploy.yml new file mode 100644 index 00000000..894f09a7 --- /dev/null +++ b/.github/workflows/production-deploy.yml @@ -0,0 +1,44 @@ +name: Production Deploy + +on: + workflow_dispatch: + inputs: + version: + description: 'version' + required: true + +env: + DOCKERHUB_IMAGE_NAME: walwal-server + +jobs: + build-deploy: + runs-on: ubuntu-latest + environment: PROD + steps: + - name: Deploy to EC2 Server + uses: appleboy/ssh-action@master + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + IMAGE_FULL_URL: ${{ steps.metadata.outputs.tags }} + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USERNAME }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + envs: IMAGE_FULL_URL # docker-compose.yaml 에서 사용할 환경 변수 + script: | + aws s3 cp ${{ env.S3_COPY_PATH }} docker-compose.yaml + echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin + docker pull ${{ env.IMAGE_FULL_URL }} + docker compose up -d + docker image prune -a -f + + ## Slack + - name: Slack Alarm + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + author_name: GitHub-Actions CI/CD + fields: repo,message,commit,author,ref,job,took + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required + if: always() # Pick up events even if the job fails or is canceled. From 9e688a9006b0d506ddd18f7a811614cf2d205f40 Mon Sep 17 00:00:00 2001 From: ybchar Date: Tue, 22 Oct 2024 17:27:13 +0900 Subject: [PATCH 56/56] =?UTF-8?q?hotfix:=20=EC=9E=90=EC=8B=9D=5F=EB=8C=93?= =?UTF-8?q?=EA=B8=80=EC=9D=84=5F=EC=A1=B0=ED=9A=8C=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4=20memberStatus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentService.java | 25 ++++++++++--------- .../application/CommentServiceTest.java | 8 +++++- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java index 5f21a088..c29e25a7 100644 --- a/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java +++ b/src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java @@ -212,18 +212,19 @@ private CommentFindOneResponse convertToCommentFindOneResponse( .collect(Collectors.toList()); // 작성자가 null인지 확인 - Long writerId = - comment.getWriter().getStatus() != MemberStatus.DELETED - ? comment.getWriter().getId() - : null; - String writerNickname = - comment.getWriter().getStatus() != MemberStatus.DELETED - ? comment.getWriter().getProfile().getNickname() - : "탈퇴한 회원"; - String writerProfileImageUrl = - comment.getWriter().getStatus() != MemberStatus.DELETED - ? comment.getWriter().getProfile().getProfileImageUrl() - : "INACTIVE_" + comment.getWriter().getRaisePet().getValue(); + Long writerId = null; + String writerNickname = "탈퇴한 회원"; + String writerProfileImageUrl = null; + + if (comment.getWriter() != null) { + if (comment.getWriter().getStatus() != MemberStatus.DELETED) { + writerId = comment.getWriter().getId(); + writerNickname = comment.getWriter().getProfile().getNickname(); + writerProfileImageUrl = comment.getWriter().getProfile().getProfileImageUrl(); + } else { + writerProfileImageUrl = "INACTIVE_" + comment.getWriter().getRaisePet().getValue(); + } + } return CommentFindOneResponse.of( comment.getParent() != null ? comment.getParent().getId() : null, diff --git a/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java b/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java index 0cafc2af..7d8c0abf 100644 --- a/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java +++ b/src/test/java/com/depromeet/stonebed/domain/comment/application/CommentServiceTest.java @@ -13,6 +13,7 @@ import com.depromeet.stonebed.domain.fcm.application.FcmNotificationService; import com.depromeet.stonebed.domain.fcm.dao.FcmTokenRepository; import com.depromeet.stonebed.domain.member.domain.Member; +import com.depromeet.stonebed.domain.member.domain.MemberStatus; import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordRepository; import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; import com.depromeet.stonebed.global.util.MemberUtil; @@ -296,7 +297,11 @@ private Comment createMockComment( String childContentPrefix = "자식 댓글 내용 "; // 부모 댓글 생성 - Member member = fixtureMonkey.giveMeOne(Member.class); + Member member = + fixtureMonkey + .giveMeBuilder(Member.class) + .set("status", MemberStatus.NORMAL) + .sample(); MissionRecord missionRecord = fixtureMonkey.giveMeOne(MissionRecord.class); Comment parentComment = createMockParentComment(member, missionRecord, parentContent); @@ -327,6 +332,7 @@ private Comment createMockComment( assertEquals(1, result.comments().size(), "부모 댓글은 하나만 있어야 합니다."); CommentFindOneResponse parentResponse = result.comments().get(0); + assertEquals(parentComment.getId(), parentResponse.commentId(), "부모 댓글 ID가 일치해야 합니다."); assertEquals(parentComment.getContent(), parentResponse.content(), "부모 댓글 내용이 일치해야 합니다."); assertEquals(