-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT/#104] 활동 삭제 기능 구현 #115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -373,6 +373,36 @@ 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()))); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: rg -n 'LifecycleConfiguration|lifecycleRule|expiration|public-read|BlockPublicAccess|bucket-policy|presign|preSigned|accessUrl|FileImageAccessUrlPort|`@Retryable`|RetryTemplate|TaskScheduler|`@Scheduled`' . --glob '!**/build/**'
find . -maxdepth 4 -type f \( -iname '*terraform*' -o -iname '*.tf' -o -iname '*cloudformation*' -o -iname '*s3*' \) -printRepository: Team-Routee/Routee-Server Length of output: 9342 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- changed service ---'
sed -n '360,430p' routee-activity/src/main/java/org/sopt/routee/activity/internal/service/ActivityService.java
printf '%s\n' '--- cleanup implementation and storage port/adapter ---'
rg -n -A35 -B8 'deleteActivityImageDirectories|deleteDirectory|interface FileDeletePort|class S3FileDeleteAdapter|S3ObjectKeyAssembler' routee-activity routee-external
printf '%s\n' '--- S3 configuration and access URL implementation ---'
sed -n '1,220p' routee-external/src/main/java/org/sopt/routee/external/internal/s3/adapter/S3FileImageAccessUrlAdapter.java
sed -n '1,180p' routee-external/src/main/java/org/sopt/routee/external/internal/s3/adapter/S3PresignAdapter.java
sed -n '1,180p' routee-external/src/main/java/org/sopt/routee/external/internal/s3/config/S3Properties.java
sed -n '1,180p' routee-app/src/main/resources/application.yml
printf '%s\n' '--- tracked infrastructure/config candidates ---'
git ls-files | rg -i '(^|/)(terraform|cloudformation|pulumi|cdk|helm|k8s|deploy|infra|docker|compose)|s3|bucket|aws|application.*ya?ml|application.*properties'
printf '%s\n' '--- retry/job/scheduler declarations ---'
rg -n -i '`@Retryable`|RetryTemplate|`@Scheduled`|TaskScheduler|Quartz|JobDetail|JobLauncher|DeadLetter|outbox|retry|recovery|cleanup' --glob '!**/build/**' --glob '!**/gradle-wrapper.properties' .Repository: Team-Routee/Routee-Server Length of output: 50381 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- delete implementation ---'
sed -n '1,220p' routee-external/src/main/java/org/sopt/routee/external/internal/s3/adapter/S3FileDeleteAdapter.java
printf '%s\n' '--- access URL implementation ---'
cat routee-external/src/main/java/org/sopt/routee/external/internal/s3/adapter/S3FileImageAccessUrlAdapter.java
printf '%s\n' '--- presign and properties ---'
cat routee-external/src/main/java/org/sopt/routee/external/internal/s3/adapter/S3PresignAdapter.java
cat routee-external/src/main/java/org/sopt/routee/external/internal/s3/config/S3Properties.java
printf '%s\n' '--- application S3 settings ---'
sed -n '35,60p' routee-app/src/main/resources/application.yml
printf '%s\n' '--- tracked infrastructure candidates (names only) ---'
git ls-files | grep -Ei '(^|/)(terraform|cloudformation|pulumi|cdk|helm|k8s|deploy|infra|docker|compose)(/|$)|(^|/)(s3|bucket|aws)[^/]*$' || true
printf '%s\n' '--- production retry/job declarations (names and matching lines only) ---'
rg -n -i --glob '!**/build/**' --glob '!**/gradle-wrapper.properties' --glob '!**/src/test/**' '`@Retryable`|RetryTemplate|`@Scheduled`|TaskScheduler|Quartz|JobDetail|JobLauncher|DeadLetter|outbox|retry|recovery|cleanup' . | head -120 || trueRepository: Team-Routee/Routee-Server Length of output: 9302 S3 이미지 삭제에 제한된 재시도를 추가하세요.
현재 저장소에는 lifecycle 정리나 별도 재시도 작업이 없습니다. 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| private void refreshDailySummaryAfterDelete(Activity activity) { | ||
| LocalDate activityDate = activity.getActivityDateWithTimezone(); | ||
| if (activityDate == null) { | ||
| return; | ||
| } | ||
|
|
||
| activityDailySummaryService.removeActivity(activity.getMemberId(), activityDate, activity.getDurationSec()); | ||
| activityDailySummaryService.refreshCoverAfterActivityChanged(activity.getMemberId(), activityDate, activity.getId()); | ||
| } | ||
|
|
||
| private String generateTimelineImageUrl(Long memberId, Long activityId, Timeline timeline, | ||
| FileUploadImageSize imageSize) { | ||
| FileImageAccessUrlCommand command = new FileImageAccessUrlCommand( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
공개 UseCase 인터페이스에 의존하도록 변경하세요.
ActivityController는internal.service.ActivityService구현체를 직접 호출합니다. 새 삭제 API도 내부 구현에 결합됩니다.공개 패키지에 활동 삭제 UseCase를 정의하세요. 컨트롤러에는 해당 인터페이스를 주입하세요. 구현체는
internal.service에 유지하세요.🤖 Prompt for AI Agents
Sources: Coding guidelines, Path instructions