Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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, "활동 통계 기록 조회에 성공했습니다."),
Expand All @@ -19,6 +20,7 @@ public enum SuccessCode implements SuccessResultCode {
ACTIVITY_TRACK_GET_SUCCESS(HttpStatus.OK, "활동 경로 데이터 조회에 성공했습니다."),
ARCHIVE_ACTIVITY_LIST_GET_SUCCESS(HttpStatus.OK, "특정 날짜의 활동 목록 조회에 성공했습니다."),
IMAGE_UPLOAD_URL_CREATED(HttpStatus.OK, "이미지 업로드 URL 발급에 성공했습니다."),
ROUTE_LIST_UPDATE_SUCCESS(HttpStatus.OK, "루트 목록 수정에 성공했습니다."),
ROUTE_LIST_GET_SUCCESS(HttpStatus.OK, "루트 목록 조회에 성공했습니다."),
TIMELINE_DELETED(HttpStatus.OK, "타임라인 삭제에 성공했습니다."),
TIMELINE_LIST_GET_SUCCESS(HttpStatus.OK, "타임라인 목록 조회에 성공했습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -140,6 +141,17 @@ public ResponseEntity<SuccessResponse<Void>> complete(
.body(ApiResponse.success(SuccessCode.ACTIVITY_COMPLETED));
}

@DeleteMapping("/activity/{activityId}")
public ResponseEntity<SuccessResponse<Void>> 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<SuccessResponse<ActivityStatisticsResponse>> getStatistics(
@AuthenticationPrincipal Long memberId,
Expand All @@ -149,7 +161,8 @@ public ResponseEntity<SuccessResponse<ActivityStatisticsResponse>> 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")
Expand Down Expand Up @@ -186,7 +199,8 @@ public ResponseEntity<SuccessResponse<ActivityEditListResponse>> 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")
Expand All @@ -198,6 +212,7 @@ public ResponseEntity<SuccessResponse<ActivitiesByDateResponse>> 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)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,23 @@ ResponseEntity<SuccessResponse<Void>> 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<SuccessResponse<Void>> delete(
Long memberId,
@PathVariable(name = "activityId") Long activityId
);

@Operation(summary = "활동 통계 기록 조회", description = "인증된 사용자의 활동 통계 기록을 조회합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "활동 통계 기록 조회 성공",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import java.util.List;

import org.sopt.routee.activity.internal.code.SuccessCode;
import org.sopt.routee.activity.internal.controller.dto.request.CreateRoutesRequest;
import org.sopt.routee.activity.internal.controller.dto.request.RoutesCreateRequest;
import org.sopt.routee.activity.internal.controller.dto.request.RoutesUpdateRequest;
import org.sopt.routee.activity.internal.controller.dto.response.RouteListResponse;
import org.sopt.routee.activity.internal.service.RouteService;
import org.sopt.routee.activity.internal.service.dto.result.RouteResult;
Expand All @@ -14,6 +15,7 @@
import org.springframework.web.bind.annotation.GetMapping;
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;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -31,14 +33,24 @@ public class RouteController implements RouteControllerDocs {
@PostMapping("/{activityId}/route")
public ResponseEntity<SuccessResponse<RouteListResponse>> createRoutes(
@PathVariable(name = "activityId") Long activityId,
@Valid @RequestBody CreateRoutesRequest request
@Valid @RequestBody RoutesCreateRequest request
) {
List<RouteResult> results = routeService.createRoutes(activityId, request.toCommands());

return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(SuccessCode.ROUTE_LIST_CREATE_SUCCESS, RouteListResponse.of(activityId, results)));
}

@PutMapping("/{activityId}/route")
public ResponseEntity<SuccessResponse<RouteListResponse>> updateRoutes(
@PathVariable(name = "activityId") Long activityId,
@Valid @RequestBody RoutesUpdateRequest request
) {
List<RouteResult> results = routeService.updateRoutes(activityId, request.toCommands());

return ResponseEntity.ok(ApiResponse.success(SuccessCode.ROUTE_LIST_UPDATE_SUCCESS, RouteListResponse.of(activityId, results)));
}

@GetMapping("/{activityId}/route")
public ResponseEntity<SuccessResponse<RouteListResponse>> getRoutes(
@PathVariable(name = "activityId") Long activityId
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.sopt.routee.activity.internal.controller;

import org.sopt.routee.activity.internal.controller.dto.request.CreateRoutesRequest;
import org.sopt.routee.activity.internal.controller.dto.request.RoutesCreateRequest;
import org.sopt.routee.activity.internal.controller.dto.request.RoutesUpdateRequest;
import org.sopt.routee.activity.internal.controller.dto.response.RouteListResponse;
import org.sopt.routee.response.FailureResponse;
import org.sopt.routee.response.SuccessResponse;
Expand Down Expand Up @@ -49,7 +50,23 @@ public interface RouteControllerDocs {
})
ResponseEntity<SuccessResponse<RouteListResponse>> createRoutes(
@PathVariable(name = "activityId") Long activityId,
@Valid @RequestBody CreateRoutesRequest request
@Valid @RequestBody RoutesCreateRequest request
);

@Operation(summary = "루트 목록 수정", description = "활동 기록의 기존 루트 목록을 요청한 루트 목록 전체로 수정합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "루트 목록 수정 성공",
content = @Content(schema = @Schema(implementation = RouteListResponse.class))),
@ApiResponse(responseCode = "400", description = "요청 값이 올바르지 않음",
content = @Content(schema = @Schema(implementation = FailureResponse.class))),
@ApiResponse(responseCode = "401", description = "인증 실패",
content = @Content(schema = @Schema(implementation = FailureResponse.class))),
@ApiResponse(responseCode = "404", description = "활동 기록이 존재하지 않음",
content = @Content(schema = @Schema(implementation = FailureResponse.class)))
})
ResponseEntity<SuccessResponse<RouteListResponse>> updateRoutes(
@PathVariable(name = "activityId") Long activityId,
@Valid @RequestBody RoutesUpdateRequest request
);

@Operation(summary = "루트 목록 조회", description = "활동 기록에 속하는 루트 목록을 조회합니다.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;

public record CreateRoutesRequest(
public record RoutesCreateRequest(
@Valid
@NotEmpty(message = "루트 목록 생성 시 최소 1개 이상의 루트를 입력해야 합니다.")
@Size(max = 20, message = "루트는 최대 20개까지 등록할 수 있습니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.sopt.routee.activity.internal.controller.dto.request;

import java.util.List;

import org.sopt.routee.activity.internal.service.dto.command.UpdateRouteCommand;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public record RoutesUpdateRequest(
@Valid
@NotNull(message = "routes는 필수입니다.")
@Size(max = 20, message = "루트는 최대 20개까지 등록할 수 있습니다.")
List<@NotNull(message = "루트는 null일 수 없습니다.") RouteRequest> routes
) {
public List<UpdateRouteCommand> toCommands() {
return routes.stream()
.map(route -> new UpdateRouteCommand(route.name(), route.sequence()))
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.sopt.routee.activity.internal.entity.activity.Activity;
import org.sopt.routee.activity.internal.entity.route.Route;
import org.sopt.routee.activity.internal.service.dto.command.CreateRouteCommand;
import org.sopt.routee.activity.internal.service.dto.command.UpdateRouteCommand;
import org.sopt.routee.activity.internal.service.dto.result.RouteResult;

public class RouteMapper {
Expand All @@ -15,6 +16,14 @@ public static Route toEntity(CreateRouteCommand command, Activity activity) {
.build();
}

public static Route toEntity(UpdateRouteCommand command, Activity activity) {
return Route.builder()
.name(command.name())
.sequence(command.sequence())
.activity(activity)
.build();
}

public static RouteResult toResult(Route route) {
return new RouteResult(route.getId(), route.getName(), route.getSequence());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ public interface RouteRepository extends JpaRepository<Route, Long> {

boolean existsByActivityId(Long activityId);

@Modifying
@Query("DELETE FROM Route r WHERE r.activity.id = :activityId")
void deleteByActivityId(@Param("activityId") Long activityId);

@Modifying
@Query("DELETE FROM Route r WHERE r.activity.memberId = :memberId")
void deleteByMemberId(@Param("memberId") Long memberId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,6 +27,7 @@
public class ActivityDailySummaryService {

private final ActivityDailySummaryRepository activityDailySummaryRepository;
private final ActivityRepository activityRepository;
private final FileImageAccessUrlPort fileImageAccessUrlPort;

@Transactional(readOnly = true)
Expand Down Expand Up @@ -50,8 +55,22 @@ 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 refreshCoverAfterActivityChanged(Long memberId, LocalDate activityDate, Long previousCoverActivityId) {
Optional<Activity> replacementCover = activityRepository
.findFirstByMemberIdAndActivityDateWithTimezoneAndActivityStatusAndCoverImageObjectKeyIsNotNullOrderByStartedAtAsc(
memberId, activityDate, ActivityStatus.ACTIVITY_COMPLETED);

activityDailySummaryRepository.updateCoverImage(
memberId, activityDate,
replacementCover.map(Activity::getId).orElse(null),
replacementCover.map(Activity::getCoverImageObjectKey).orElse(null),
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())));
}

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(
Expand Down
Loading
Loading