From 06ea9c45df59ce4fbe6bfe7e47a3b7867206ec88 Mon Sep 17 00:00:00 2001 From: youtheyeon Date: Fri, 18 Sep 2026 12:50:28 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=ED=99=9C=EB=8F=99=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/internal/code/SuccessCode.java | 1 + .../controller/ActivityController.java | 29 ++++++++++---- .../controller/ActivityControllerDocs.java | 17 ++++++++ .../ActivityDailySummaryRepository.java | 21 +++++++++- .../service/ActivityDailySummaryService.java | 13 ++++++- .../internal/service/ActivityService.java | 39 +++++++++++++++++++ .../internal/service/TimelineService.java | 19 ++++----- 7 files changed, 119 insertions(+), 20 deletions(-) diff --git a/routee-activity/src/main/java/org/sopt/routee/activity/internal/code/SuccessCode.java b/routee-activity/src/main/java/org/sopt/routee/activity/internal/code/SuccessCode.java index 9f5f985e..48d7c8ff 100644 --- a/routee-activity/src/main/java/org/sopt/routee/activity/internal/code/SuccessCode.java +++ b/routee-activity/src/main/java/org/sopt/routee/activity/internal/code/SuccessCode.java @@ -11,6 +11,7 @@ public enum SuccessCode implements SuccessResultCode { ACTIVITY_COMPLETED(HttpStatus.OK, "활동 기록 저장에 성공했습니다."), + ACTIVITY_DELETED(HttpStatus.OK, "활동 기록 삭제에 성공했습니다."), ACTIVITY_EDIT_LIST_GET_SUCCESS(HttpStatus.OK, "활동 수정 목록 조회에 성공했습니다."), ACTIVITY_RECAP_GET_SUCCESS(HttpStatus.OK, "활동 리캡 조회에 성공했습니다."), ACTIVITY_STATISTICS_GET_SUCCESS(HttpStatus.OK, "활동 통계 기록 조회에 성공했습니다."), diff --git a/routee-activity/src/main/java/org/sopt/routee/activity/internal/controller/ActivityController.java b/routee-activity/src/main/java/org/sopt/routee/activity/internal/controller/ActivityController.java index 4908ea91..4d5c6954 100644 --- a/routee-activity/src/main/java/org/sopt/routee/activity/internal/controller/ActivityController.java +++ b/routee-activity/src/main/java/org/sopt/routee/activity/internal/controller/ActivityController.java @@ -10,21 +10,21 @@ import org.sopt.routee.activity.internal.controller.dto.request.ActivityStatusUpdateRequest; import org.sopt.routee.activity.internal.controller.dto.request.ActivityTitleUpdateRequest; import org.sopt.routee.activity.internal.controller.dto.request.ImageUrlRequest; +import org.sopt.routee.activity.internal.controller.dto.response.ActivitiesByDateResponse; import org.sopt.routee.activity.internal.controller.dto.response.ActivityCreateResponse; import org.sopt.routee.activity.internal.controller.dto.response.ActivityEditListResponse; import org.sopt.routee.activity.internal.controller.dto.response.ActivityRecapResponse; -import org.sopt.routee.activity.internal.controller.dto.response.ActivityStatusResponse; import org.sopt.routee.activity.internal.controller.dto.response.ActivityStatisticsResponse; -import org.sopt.routee.activity.internal.controller.dto.response.ActivitiesByDateResponse; +import org.sopt.routee.activity.internal.controller.dto.response.ActivityStatusResponse; import org.sopt.routee.activity.internal.controller.dto.response.ActivityTitleResponse; import org.sopt.routee.activity.internal.controller.dto.response.ActivityTrackResponse; import org.sopt.routee.activity.internal.controller.dto.response.ImageUrlResponse; import org.sopt.routee.activity.internal.service.ActivityService; import org.sopt.routee.activity.internal.service.dto.command.GetActivityRecapCommand; +import org.sopt.routee.activity.internal.service.dto.result.ActivitiesByDateResult; import org.sopt.routee.activity.internal.service.dto.result.ActivityEditListResult; import org.sopt.routee.activity.internal.service.dto.result.ActivityRecapResult; import org.sopt.routee.activity.internal.service.dto.result.ActivityStatisticsResult; -import org.sopt.routee.activity.internal.service.dto.result.ActivitiesByDateResult; import org.sopt.routee.activity.internal.service.dto.result.ActivityTrackResult; import org.sopt.routee.activity.internal.service.dto.result.CreateActivityResult; import org.sopt.routee.activity.internal.service.dto.result.ImageUrlResult; @@ -39,9 +39,10 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -140,6 +141,17 @@ public ResponseEntity> complete( .body(ApiResponse.success(SuccessCode.ACTIVITY_COMPLETED)); } + @DeleteMapping("/activity/{activityId}") + public ResponseEntity> delete( + @AuthenticationPrincipal Long memberId, + @PathVariable(name = "activityId") Long activityId + ) { + activityService.delete(activityId, memberId); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.ACTIVITY_DELETED)); + } + @GetMapping("/activity/{activityId}/statistics") public ResponseEntity> getStatistics( @AuthenticationPrincipal Long memberId, @@ -149,7 +161,8 @@ public ResponseEntity> getStatistics ActivityStatisticsResult result = activityService.getStatistics(activityId, memberId, timeZone); return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(SuccessCode.ACTIVITY_STATISTICS_GET_SUCCESS, ActivityStatisticsResponse.from(result))); + .body(ApiResponse.success(SuccessCode.ACTIVITY_STATISTICS_GET_SUCCESS, + ActivityStatisticsResponse.from(result))); } @GetMapping("/activity/{activityId}/track") @@ -186,7 +199,8 @@ public ResponseEntity> getActivityEdit ); return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(SuccessCode.ACTIVITY_EDIT_LIST_GET_SUCCESS, ActivityEditListResponse.from(result))); + .body( + ApiResponse.success(SuccessCode.ACTIVITY_EDIT_LIST_GET_SUCCESS, ActivityEditListResponse.from(result))); } @GetMapping("/archive/activity") @@ -198,6 +212,7 @@ public ResponseEntity> getActivitiesBy ActivitiesByDateResult result = activityService.getActivitiesByDate(memberId, date, timeZone); return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(SuccessCode.ARCHIVE_ACTIVITY_LIST_GET_SUCCESS, ActivitiesByDateResponse.from(result))); + .body(ApiResponse.success(SuccessCode.ARCHIVE_ACTIVITY_LIST_GET_SUCCESS, + ActivitiesByDateResponse.from(result))); } } diff --git a/routee-activity/src/main/java/org/sopt/routee/activity/internal/controller/ActivityControllerDocs.java b/routee-activity/src/main/java/org/sopt/routee/activity/internal/controller/ActivityControllerDocs.java index 9f7919fe..b15ac3d5 100644 --- a/routee-activity/src/main/java/org/sopt/routee/activity/internal/controller/ActivityControllerDocs.java +++ b/routee-activity/src/main/java/org/sopt/routee/activity/internal/controller/ActivityControllerDocs.java @@ -256,6 +256,23 @@ ResponseEntity> complete( @Valid @RequestBody ActivityCompleteRequest request ); + @Operation(summary = "활동 삭제", description = "인증된 사용자의 활동 기록을 삭제합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "활동 삭제 성공", + content = @Content( + examples = @ExampleObject(value = "{\"status\":200,\"code\":\"ACTIVITY_DELETED\",\"message\":\"활동 기록 삭제에 성공했습니다.\",\"data\":null}"))), + @ApiResponse(responseCode = "401", description = "인증 실패", + content = @Content(schema = @Schema(implementation = FailureResponse.class))), + @ApiResponse(responseCode = "404", description = "활동 기록이 존재하지 않음", + content = @Content(schema = @Schema(implementation = FailureResponse.class), + examples = @ExampleObject(name = "ACTIVITY_NOT_FOUND", + value = "{\"status\":404,\"code\":\"ACTIVITY_NOT_FOUND\",\"message\":\"활동 기록이 존재하지 않습니다.\"}"))) + }) + ResponseEntity> delete( + Long memberId, + @PathVariable(name = "activityId") Long activityId + ); + @Operation(summary = "활동 통계 기록 조회", description = "인증된 사용자의 활동 통계 기록을 조회합니다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "활동 통계 기록 조회 성공", diff --git a/routee-activity/src/main/java/org/sopt/routee/activity/internal/repository/ActivityDailySummaryRepository.java b/routee-activity/src/main/java/org/sopt/routee/activity/internal/repository/ActivityDailySummaryRepository.java index 3eb8e901..3f33fe36 100644 --- a/routee-activity/src/main/java/org/sopt/routee/activity/internal/repository/ActivityDailySummaryRepository.java +++ b/routee-activity/src/main/java/org/sopt/routee/activity/internal/repository/ActivityDailySummaryRepository.java @@ -46,12 +46,29 @@ void upsertDailySummary( @Modifying @Query(""" UPDATE ActivityDailySummary ads SET ads.coverActivityId = :coverActivityId, ads.coverImageObjectKey = :coverImageObjectKey - WHERE ads.memberId = :memberId AND ads.activityDate = :activityDate + WHERE ads.memberId = :memberId AND ads.activityDate = :activityDate AND ads.coverActivityId = :previousCoverActivityId """) void updateCoverImage( @Param("memberId") Long memberId, @Param("activityDate") LocalDate activityDate, @Param("coverActivityId") Long coverActivityId, - @Param("coverImageObjectKey") String coverImageObjectKey + @Param("coverImageObjectKey") String coverImageObjectKey, + @Param("previousCoverActivityId") Long previousCoverActivityId + ); + + @Modifying + @Query(""" + UPDATE ActivityDailySummary ads + SET ads.totalDurationSec = ads.totalDurationSec - :durationSec, ads.activityCount = ads.activityCount - 1 + WHERE ads.memberId = :memberId AND ads.activityDate = :activityDate + """) + void decrementDailySummary( + @Param("memberId") Long memberId, + @Param("activityDate") LocalDate activityDate, + @Param("durationSec") Integer durationSec ); + + @Modifying + @Query("DELETE FROM ActivityDailySummary ads WHERE ads.memberId = :memberId AND ads.activityDate = :activityDate AND ads.activityCount <= 0") + void deleteIfEmpty(@Param("memberId") Long memberId, @Param("activityDate") LocalDate activityDate); } diff --git a/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/ActivityDailySummaryService.java b/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/ActivityDailySummaryService.java index 3ee492c1..ddad3bc1 100644 --- a/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/ActivityDailySummaryService.java +++ b/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/ActivityDailySummaryService.java @@ -50,8 +50,17 @@ public void deleteActivityDailySummariesByMemberId(long memberId) { } @Transactional - public void refreshCoverImage(Long memberId, LocalDate activityDate, Long coverActivityId, String coverImageObjectKey) { - activityDailySummaryRepository.updateCoverImage(memberId, activityDate, coverActivityId, coverImageObjectKey); + public void refreshCoverImage( + Long memberId, LocalDate activityDate, Long previousCoverActivityId, Long coverActivityId, String coverImageObjectKey + ) { + activityDailySummaryRepository.updateCoverImage( + memberId, activityDate, coverActivityId, coverImageObjectKey, previousCoverActivityId); + } + + @Transactional + public void removeActivity(Long memberId, LocalDate activityDate, Integer durationSec) { + activityDailySummaryRepository.decrementDailySummary(memberId, activityDate, durationSec); + activityDailySummaryRepository.deleteIfEmpty(memberId, activityDate); } private String generateCoverImageUrl(Long memberId, ActivityDailySummary summary) { diff --git a/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/ActivityService.java b/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/ActivityService.java index f5bc6ac6..cca14ff6 100644 --- a/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/ActivityService.java +++ b/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/ActivityService.java @@ -8,6 +8,7 @@ import java.util.EnumSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -373,6 +374,44 @@ public void deleteActivitiesByMemberId(long memberId) { activityRepository.deleteByMemberId(memberId); } + public void delete(Long activityId, Long memberId) { + Activity deletedActivity = transactionTemplate.execute(status -> { + Activity activity = activityRepository.findByIdAndMemberId(activityId, memberId) + .orElseThrow(ActivityNotFoundException::new); + + routeRepository.deleteByActivityIdIn(List.of(activityId)); + timelineRepository.deleteByActivityIdIn(List.of(activityId)); + activityRepository.delete(activity); + + if (activity.getActivityStatus().isCompleted()) { + refreshDailySummaryAfterDelete(activity); + } + + return activity; + }); + + log.info("Activity deleted. activityId={}, memberId={}", activityId, memberId); + Thread.startVirtualThread(() -> deleteActivityImageDirectories(memberId, List.of(deletedActivity.getId()))); + } + + private void refreshDailySummaryAfterDelete(Activity activity) { + LocalDate activityDate = activity.getActivityDateWithTimezone(); + if (activityDate == null) { + return; + } + + activityDailySummaryService.removeActivity(activity.getMemberId(), activityDate, activity.getDurationSec()); + + Optional firstActivityWithCover = activityRepository + .findFirstByMemberIdAndActivityDateWithTimezoneAndActivityStatusAndCoverImageObjectKeyIsNotNullOrderByStartedAtAsc( + activity.getMemberId(), activityDate, ActivityStatus.ACTIVITY_COMPLETED); + + activityDailySummaryService.refreshCoverImage( + activity.getMemberId(), activityDate, activity.getId(), + firstActivityWithCover.map(Activity::getId).orElse(null), + firstActivityWithCover.map(Activity::getCoverImageObjectKey).orElse(null)); + } + private String generateTimelineImageUrl(Long memberId, Long activityId, Timeline timeline, FileUploadImageSize imageSize) { FileImageAccessUrlCommand command = new FileImageAccessUrlCommand( diff --git a/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/TimelineService.java b/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/TimelineService.java index 032caba6..bbcc2587 100644 --- a/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/TimelineService.java +++ b/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/TimelineService.java @@ -3,6 +3,7 @@ import java.time.Instant; import java.time.LocalDate; import java.util.List; +import java.util.Optional; import org.sopt.routee.activity.internal.entity.activity.Activity; import org.sopt.routee.activity.internal.entity.activity.ActivityStatus; @@ -64,7 +65,8 @@ public List getTimelines(Long activityId, Long memberId) { } return timelineRepository.findByActivityIdOrderByCreatedAtAsc(activityId).stream() - .map(timeline -> TimelineMapper.toTimelineResult(timeline, generateImageUrl(memberId, activityId, timeline))) + .map( + timeline -> TimelineMapper.toTimelineResult(timeline, generateImageUrl(memberId, activityId, timeline))) .toList(); } @@ -131,15 +133,13 @@ private void refreshDailySummaryCoverIfNeeded(Activity activity) { return; } - Activity firstActivityWithCover = activityRepository + Optional firstActivityWithCover = activityRepository .findFirstByMemberIdAndActivityDateWithTimezoneAndActivityStatusAndCoverImageObjectKeyIsNotNullOrderByStartedAtAsc( - activity.getMemberId(), activityDate, ActivityStatus.ACTIVITY_COMPLETED) - .orElse(null); - - Long coverActivityId = firstActivityWithCover == null ? null : firstActivityWithCover.getId(); - String coverImageObjectKey = firstActivityWithCover == null ? null : firstActivityWithCover.getCoverImageObjectKey(); + activity.getMemberId(), activityDate, ActivityStatus.ACTIVITY_COMPLETED); - activityDailySummaryService.refreshCoverImage(activity.getMemberId(), activityDate, coverActivityId, coverImageObjectKey); + activityDailySummaryService.refreshCoverImage(activity.getMemberId(), activityDate, activity.getId(), + firstActivityWithCover.map(Activity::getId).orElse(null), + firstActivityWithCover.map(Activity::getCoverImageObjectKey).orElse(null)); } private Timeline findOwnedTimeline(Long activityId, Long timelineId, Long memberId) { @@ -152,7 +152,8 @@ private Timeline findOwnedTimeline(Long activityId, Long timelineId, Long member private void deleteTimelineImage(Long memberId, Long activityId, String objectKey) { try { fileDeletePort.deleteImage( - new FileDeleteCommand(FileUploadDirectory.TIMELINE, memberId.toString(), activityId.toString(), objectKey)); + new FileDeleteCommand(FileUploadDirectory.TIMELINE, memberId.toString(), activityId.toString(), + objectKey)); } catch (BaseException e) { log.warn("Timeline image delete failed. activityId={}, objectKey={}", activityId, objectKey, e); } From d805df6640d4679f8e13ae097cbf337332be2718 Mon Sep 17 00:00:00 2001 From: youtheyeon Date: Fri, 18 Sep 2026 12:59:28 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=EC=9D=BC=EB=B3=84=20=EC=9A=94?= =?UTF-8?q?=EC=95=BD=20=EC=BB=A4=EB=B2=84=20=EC=9E=AC=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ActivityDailySummaryService.java | 18 ++++++++++++++---- .../internal/service/ActivityService.java | 11 +---------- .../internal/service/TimelineService.java | 10 +--------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/ActivityDailySummaryService.java b/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/ActivityDailySummaryService.java index ddad3bc1..4b2e398f 100644 --- a/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/ActivityDailySummaryService.java +++ b/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/ActivityDailySummaryService.java @@ -3,10 +3,14 @@ import java.time.LocalDate; import java.time.YearMonth; import java.util.List; +import java.util.Optional; +import org.sopt.routee.activity.internal.entity.activity.Activity; +import org.sopt.routee.activity.internal.entity.activity.ActivityStatus; import org.sopt.routee.activity.internal.entity.summary.ActivityDailySummary; import org.sopt.routee.activity.internal.mapper.ActivityDailySummaryMapper; import org.sopt.routee.activity.internal.repository.ActivityDailySummaryRepository; +import org.sopt.routee.activity.internal.repository.ActivityRepository; import org.sopt.routee.activity.internal.service.dto.result.ActivityDailySummaryResult; import org.sopt.routee.external.api.command.FileImageAccessUrlCommand; import org.sopt.routee.external.api.port.FileImageAccessUrlPort; @@ -23,6 +27,7 @@ public class ActivityDailySummaryService { private final ActivityDailySummaryRepository activityDailySummaryRepository; + private final ActivityRepository activityRepository; private final FileImageAccessUrlPort fileImageAccessUrlPort; @Transactional(readOnly = true) @@ -50,11 +55,16 @@ public void deleteActivityDailySummariesByMemberId(long memberId) { } @Transactional - public void refreshCoverImage( - Long memberId, LocalDate activityDate, Long previousCoverActivityId, Long coverActivityId, String coverImageObjectKey - ) { + public void refreshCoverAfterActivityChanged(Long memberId, LocalDate activityDate, Long previousCoverActivityId) { + Optional replacementCover = activityRepository + .findFirstByMemberIdAndActivityDateWithTimezoneAndActivityStatusAndCoverImageObjectKeyIsNotNullOrderByStartedAtAsc( + memberId, activityDate, ActivityStatus.ACTIVITY_COMPLETED); + activityDailySummaryRepository.updateCoverImage( - memberId, activityDate, coverActivityId, coverImageObjectKey, previousCoverActivityId); + memberId, activityDate, + replacementCover.map(Activity::getId).orElse(null), + replacementCover.map(Activity::getCoverImageObjectKey).orElse(null), + previousCoverActivityId); } @Transactional diff --git a/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/ActivityService.java b/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/ActivityService.java index cca14ff6..f9aff13b 100644 --- a/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/ActivityService.java +++ b/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/ActivityService.java @@ -8,7 +8,6 @@ import java.util.EnumSet; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -401,15 +400,7 @@ private void refreshDailySummaryAfterDelete(Activity activity) { } activityDailySummaryService.removeActivity(activity.getMemberId(), activityDate, activity.getDurationSec()); - - Optional firstActivityWithCover = activityRepository - .findFirstByMemberIdAndActivityDateWithTimezoneAndActivityStatusAndCoverImageObjectKeyIsNotNullOrderByStartedAtAsc( - activity.getMemberId(), activityDate, ActivityStatus.ACTIVITY_COMPLETED); - - activityDailySummaryService.refreshCoverImage( - activity.getMemberId(), activityDate, activity.getId(), - firstActivityWithCover.map(Activity::getId).orElse(null), - firstActivityWithCover.map(Activity::getCoverImageObjectKey).orElse(null)); + activityDailySummaryService.refreshCoverAfterActivityChanged(activity.getMemberId(), activityDate, activity.getId()); } private String generateTimelineImageUrl(Long memberId, Long activityId, Timeline timeline, diff --git a/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/TimelineService.java b/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/TimelineService.java index bbcc2587..c785cbf5 100644 --- a/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/TimelineService.java +++ b/routee-activity/src/main/java/org/sopt/routee/activity/internal/service/TimelineService.java @@ -3,10 +3,8 @@ import java.time.Instant; import java.time.LocalDate; import java.util.List; -import java.util.Optional; import org.sopt.routee.activity.internal.entity.activity.Activity; -import org.sopt.routee.activity.internal.entity.activity.ActivityStatus; import org.sopt.routee.activity.internal.entity.timeline.Timeline; import org.sopt.routee.activity.internal.entity.timeline.TimelineStatus; import org.sopt.routee.activity.internal.exception.ActivityNotFoundException; @@ -133,13 +131,7 @@ private void refreshDailySummaryCoverIfNeeded(Activity activity) { return; } - Optional firstActivityWithCover = activityRepository - .findFirstByMemberIdAndActivityDateWithTimezoneAndActivityStatusAndCoverImageObjectKeyIsNotNullOrderByStartedAtAsc( - activity.getMemberId(), activityDate, ActivityStatus.ACTIVITY_COMPLETED); - - activityDailySummaryService.refreshCoverImage(activity.getMemberId(), activityDate, activity.getId(), - firstActivityWithCover.map(Activity::getId).orElse(null), - firstActivityWithCover.map(Activity::getCoverImageObjectKey).orElse(null)); + activityDailySummaryService.refreshCoverAfterActivityChanged(activity.getMemberId(), activityDate, activity.getId()); } private Timeline findOwnedTimeline(Long activityId, Long timelineId, Long memberId) { From 0476921dc50f8097c9e02e3ce1b7e34ba52a8030 Mon Sep 17 00:00:00 2001 From: youtheyeon Date: Fri, 18 Sep 2026 13:09:07 +0900 Subject: [PATCH 3/3] =?UTF-8?q?test:=20=ED=99=9C=EB=8F=99/=ED=83=80?= =?UTF-8?q?=EC=9E=84=EB=9D=BC=EC=9D=B8=20=EC=82=AD=EC=A0=9C=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=EC=9D=BC=EB=B3=84=20=EC=9A=94=EC=95=BD=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=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 --- .../ActivityDailySummaryServiceTest.java | 75 +++++++++++++++++++ .../internal/service/ActivityServiceTest.java | 40 ++++++++++ 2 files changed, 115 insertions(+) create mode 100644 routee-activity/src/test/java/org/sopt/routee/activity/internal/service/ActivityDailySummaryServiceTest.java diff --git a/routee-activity/src/test/java/org/sopt/routee/activity/internal/service/ActivityDailySummaryServiceTest.java b/routee-activity/src/test/java/org/sopt/routee/activity/internal/service/ActivityDailySummaryServiceTest.java new file mode 100644 index 00000000..ad79ef37 --- /dev/null +++ b/routee-activity/src/test/java/org/sopt/routee/activity/internal/service/ActivityDailySummaryServiceTest.java @@ -0,0 +1,75 @@ +package org.sopt.routee.activity.internal.service; + +import static org.mockito.Mockito.*; + +import java.time.LocalDate; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.sopt.routee.activity.internal.entity.activity.Activity; +import org.sopt.routee.activity.internal.entity.activity.ActivityStatus; +import org.sopt.routee.activity.internal.repository.ActivityDailySummaryRepository; +import org.sopt.routee.activity.internal.repository.ActivityRepository; +import org.sopt.routee.external.api.port.FileImageAccessUrlPort; + +@ExtendWith(MockitoExtension.class) +class ActivityDailySummaryServiceTest { + + private static final Long MEMBER_ID = 1L; + private static final LocalDate ACTIVITY_DATE = LocalDate.of(2026, 7, 7); + private static final Long DELETED_COVER_ACTIVITY_ID = 10L; + + @Mock + private ActivityDailySummaryRepository activityDailySummaryRepository; + + @Mock + private ActivityRepository activityRepository; + + @Mock + private FileImageAccessUrlPort fileImageAccessUrlPort; + + private ActivityDailySummaryService activityDailySummaryService; + + @BeforeEach + void setUp() { + activityDailySummaryService = new ActivityDailySummaryService( + activityDailySummaryRepository, + activityRepository, + fileImageAccessUrlPort + ); + } + + @Test + void refreshCoverAfterActivityChanged_대표_커버_삭제_후_남은_활동에_이미지가_있으면_다음_활동을_새_커버로_갱신한다() { + Activity nextActivity = Activity.builder() + .id(20L) + .coverImageObjectKey("next-cover.jpg") + .build(); + when(activityRepository + .findFirstByMemberIdAndActivityDateWithTimezoneAndActivityStatusAndCoverImageObjectKeyIsNotNullOrderByStartedAtAsc( + MEMBER_ID, ACTIVITY_DATE, ActivityStatus.ACTIVITY_COMPLETED)) + .thenReturn(Optional.of(nextActivity)); + + activityDailySummaryService.refreshCoverAfterActivityChanged(MEMBER_ID, ACTIVITY_DATE, DELETED_COVER_ACTIVITY_ID); + + verify(activityDailySummaryRepository).updateCoverImage( + MEMBER_ID, ACTIVITY_DATE, 20L, "next-cover.jpg", DELETED_COVER_ACTIVITY_ID); + } + + @Test + void refreshCoverAfterActivityChanged_대표_커버_삭제_후_남은_활동에_이미지가_없으면_커버를_null로_갱신한다() { + when(activityRepository + .findFirstByMemberIdAndActivityDateWithTimezoneAndActivityStatusAndCoverImageObjectKeyIsNotNullOrderByStartedAtAsc( + MEMBER_ID, ACTIVITY_DATE, ActivityStatus.ACTIVITY_COMPLETED)) + .thenReturn(Optional.empty()); + + activityDailySummaryService.refreshCoverAfterActivityChanged(MEMBER_ID, ACTIVITY_DATE, DELETED_COVER_ACTIVITY_ID); + + verify(activityDailySummaryRepository).updateCoverImage( + MEMBER_ID, ACTIVITY_DATE, null, null, DELETED_COVER_ACTIVITY_ID); + } +} diff --git a/routee-activity/src/test/java/org/sopt/routee/activity/internal/service/ActivityServiceTest.java b/routee-activity/src/test/java/org/sopt/routee/activity/internal/service/ActivityServiceTest.java index 1be6922b..775d65bb 100644 --- a/routee-activity/src/test/java/org/sopt/routee/activity/internal/service/ActivityServiceTest.java +++ b/routee-activity/src/test/java/org/sopt/routee/activity/internal/service/ActivityServiceTest.java @@ -5,6 +5,7 @@ import static org.mockito.Mockito.*; import java.time.Instant; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.EnumSet; @@ -288,4 +289,43 @@ private void completeWithSynchronizationActive(CompleteActivityCommand command) TransactionSynchronizationManager.clearSynchronization(); } } + + @Test + void delete_완료된_활동을_삭제하면_일별_요약_감소와_커버_재계산을_요청한다() { + Long activityId = 30L; + LocalDate activityDate = LocalDate.of(2026, 7, 7); + Activity activity = Activity.builder() + .id(activityId) + .memberId(MEMBER_ID) + .activityStatus(ActivityStatus.ACTIVITY_COMPLETED) + .durationSec(1800) + .activityDateWithTimezone(activityDate) + .build(); + + when(activityRepository.findByIdAndMemberId(activityId, MEMBER_ID)).thenReturn(Optional.of(activity)); + stubTransactionTemplateToRunCallback(); + + activityService.delete(activityId, MEMBER_ID); + + InOrder inOrder = inOrder(activityDailySummaryService); + inOrder.verify(activityDailySummaryService).removeActivity(MEMBER_ID, activityDate, 1800); + inOrder.verify(activityDailySummaryService).refreshCoverAfterActivityChanged(MEMBER_ID, activityDate, activityId); + } + + @Test + void delete_진행중인_활동을_삭제하면_일별_요약에_영향을_주지_않는다() { + Long activityId = 31L; + Activity activity = Activity.builder() + .id(activityId) + .memberId(MEMBER_ID) + .activityStatus(ActivityStatus.ACTIVITY_IN_PROGRESS) + .build(); + + when(activityRepository.findByIdAndMemberId(activityId, MEMBER_ID)).thenReturn(Optional.of(activity)); + stubTransactionTemplateToRunCallback(); + + activityService.delete(activityId, MEMBER_ID); + + verifyNoInteractions(activityDailySummaryService); + } }