diff --git a/src/main/java/com/daytodo/domain/course/entity/Course.java b/src/main/java/com/daytodo/domain/course/entity/Course.java index b5eb975..b4cb6ee 100644 --- a/src/main/java/com/daytodo/domain/course/entity/Course.java +++ b/src/main/java/com/daytodo/domain/course/entity/Course.java @@ -83,6 +83,11 @@ public Course( this.courseStatus = CourseStatus.PLANNING; } + // 코스 시작 (계획 중 -> 진행 중). courseDate 가 도래해 투데이 조회 시점에 승격된다. + public void start() { + this.courseStatus = CourseStatus.IN_PROGRESS; + } + // 코스 종료 public void complete() { this.courseStatus = CourseStatus.COMPLETED; diff --git a/src/main/java/com/daytodo/domain/course/repository/CourseRepository.java b/src/main/java/com/daytodo/domain/course/repository/CourseRepository.java index ad2aef3..456a978 100644 --- a/src/main/java/com/daytodo/domain/course/repository/CourseRepository.java +++ b/src/main/java/com/daytodo/domain/course/repository/CourseRepository.java @@ -11,6 +11,7 @@ import org.springframework.data.repository.query.Param; import java.time.LocalDate; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -38,7 +39,8 @@ List findAllByOwnerIdAndCourseDateBetweenOrderByCreatedAtDesc( /* * 투데이 코스 조회 - * 사용자가 참여 중인 코스 중 해당 날짜의 진행 중 코스를 조회한다. + * 사용자가 참여 중인 코스 중 해당 날짜의 코스를 조회한다. + * PLANNING 은 조회 시점에 IN_PROGRESS 로 승격되므로 두 상태를 모두 대상으로 한다. * 이론상 1건이지만 데이터 이상으로 여러 건일 수 있어 최근 생성 순으로 정렬해 첫 건을 사용한다. */ @Query(""" @@ -46,13 +48,13 @@ List findAllByOwnerIdAndCourseDateBetweenOrderByCreatedAtDesc( where cm.user.id = :userId and cm.memberStatus = :memberStatus and cm.course.courseDate = :courseDate - and cm.course.courseStatus = :courseStatus + and cm.course.courseStatus in :courseStatuses order by cm.course.createdAt desc, cm.course.courseId desc """) - List findMemberCoursesByDateAndStatus( + List findMemberCoursesByDateAndStatuses( @Param("userId") Long userId, @Param("courseDate") LocalDate courseDate, - @Param("courseStatus") CourseStatus courseStatus, + @Param("courseStatuses") Collection courseStatuses, @Param("memberStatus") MemberStatus memberStatus ); diff --git a/src/main/java/com/daytodo/domain/course/service/CourseService.java b/src/main/java/com/daytodo/domain/course/service/CourseService.java index bbcc534..49200dd 100644 --- a/src/main/java/com/daytodo/domain/course/service/CourseService.java +++ b/src/main/java/com/daytodo/domain/course/service/CourseService.java @@ -66,14 +66,20 @@ public class CourseService { private final RecommendationLikeRepository recommendationLikeRepository; private final PlaceRepository placeRepository; private final RecommendationCommentRepository recommendationCommentRepository; + private final TodayCoursePromoter todayCoursePromoter; private final Clock clock; + // 진행중 목록/배너가 IN_PROGRESS 상태에 의존하므로, 조회 전에 오늘 날짜의 + // PLANNING 코스를 IN_PROGRESS 로 승격시킨다(투데이 조회와 동일 규칙). 쓰기 발생. + @Transactional public CourseResponse.Courses getCourses(Long userId, LocalDate startDate, LocalDate endDate) { userService.getActiveUser(userId); validatePeriod(startDate, endDate); LocalDate today = LocalDate.now(clock); + todayCoursePromoter.promoteDueCourses(userId, today); + List inProgress = courseRepository.findMemberCoursesByStatus( userId, MemberStatus.JOINED, CourseStatus.IN_PROGRESS ); diff --git a/src/main/java/com/daytodo/domain/course/service/TodayCoursePromoter.java b/src/main/java/com/daytodo/domain/course/service/TodayCoursePromoter.java new file mode 100644 index 0000000..d9ea21a --- /dev/null +++ b/src/main/java/com/daytodo/domain/course/service/TodayCoursePromoter.java @@ -0,0 +1,39 @@ +package com.daytodo.domain.course.service; + +import com.daytodo.domain.course.entity.Course; +import com.daytodo.domain.course.enums.CourseStatus; +import com.daytodo.domain.course.enums.MemberStatus; +import com.daytodo.domain.course.repository.CourseRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; + +/** + * 코스 상태 지연 전환 헬퍼. + * 별도 스케줄러가 없으므로, courseDate 가 도래한 PLANNING 코스를 + * 홈/투데이 조회 진입 시점에 IN_PROGRESS 로 승격시킨다. + * 두 조회 경로가 같은 규칙을 쓰도록 승격 로직을 한곳에 모은다. + */ +@Component +@RequiredArgsConstructor +public class TodayCoursePromoter { + + private final CourseRepository courseRepository; + + /** + * 사용자가 참여 중인 코스 중 courseDate 가 today 인 PLANNING 코스를 IN_PROGRESS 로 승격. + * 호출 측 트랜잭션에 합류하며(REQUIRED), 변경 감지로 커밋 시 반영된다. + */ + @Transactional + public void promoteDueCourses(Long userId, LocalDate today) { + courseRepository.findMemberCoursesByDateAndStatuses( + userId, + today, + List.of(CourseStatus.PLANNING), + MemberStatus.JOINED + ).forEach(Course::start); + } +} diff --git a/src/main/java/com/daytodo/domain/course/service/TodayCourseService.java b/src/main/java/com/daytodo/domain/course/service/TodayCourseService.java index 307e072..ab1ae01 100644 --- a/src/main/java/com/daytodo/domain/course/service/TodayCourseService.java +++ b/src/main/java/com/daytodo/domain/course/service/TodayCourseService.java @@ -20,6 +20,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.Clock; import java.time.LocalDate; import java.util.HashMap; import java.util.List; @@ -35,17 +36,24 @@ public class TodayCourseService { private final CourseMemberRepository courseMemberRepository; private final CoursePlaceRepository coursePlaceRepository; private final MemoryPhotoRepository memoryPhotoRepository; + private final TodayCoursePromoter todayCoursePromoter; + private final Clock clock; /* * 투데이 코스 조회 - * 오늘 진행 중인 코스가 없으면 예외가 아니라 todayCourse: null 로 응답한다. + * 오늘 날짜의 코스가 없으면 예외가 아니라 todayCourse: null 로 응답한다. + * 별도의 상태 전이 스케줄러가 없으므로, 조회 진입 시점에 오늘 날짜의 PLANNING + * 코스를 IN_PROGRESS 로 승격시킨다(홈/투데이 공용 규칙). */ - @Transactional(readOnly = true) + @Transactional public TodayCourseResponse.GetTodayCourse getTodayCourse(Long userId) { - Optional todayCourse = courseRepository.findMemberCoursesByDateAndStatus( + LocalDate today = LocalDate.now(clock); + todayCoursePromoter.promoteDueCourses(userId, today); + + Optional todayCourse = courseRepository.findMemberCoursesByDateAndStatuses( userId, - LocalDate.now(), - CourseStatus.IN_PROGRESS, + today, + List.of(CourseStatus.IN_PROGRESS), MemberStatus.JOINED ).stream().findFirst(); diff --git a/src/test/java/com/daytodo/domain/course/repository/CourseQueryValidationTest.java b/src/test/java/com/daytodo/domain/course/repository/CourseQueryValidationTest.java index 901a122..161bef5 100644 --- a/src/test/java/com/daytodo/domain/course/repository/CourseQueryValidationTest.java +++ b/src/test/java/com/daytodo/domain/course/repository/CourseQueryValidationTest.java @@ -28,8 +28,10 @@ class CourseQueryValidationTest { @Test @DisplayName("투데이 코스 관련 JPQL이 정상적으로 실행된다") void queriesRun() { - assertThat(courseRepository.findMemberCoursesByDateAndStatus( - 1L, LocalDate.now(), CourseStatus.IN_PROGRESS, MemberStatus.JOINED + assertThat(courseRepository.findMemberCoursesByDateAndStatuses( + 1L, LocalDate.now(), + java.util.List.of(CourseStatus.PLANNING, CourseStatus.IN_PROGRESS), + MemberStatus.JOINED )).isEmpty(); assertThat(courseMemberRepository.findMembersByCourseId(1L, MemberStatus.JOINED)).isEmpty(); diff --git a/src/test/java/com/daytodo/domain/course/service/CourseServiceTest.java b/src/test/java/com/daytodo/domain/course/service/CourseServiceTest.java index 6fb92bd..1e34b5e 100644 --- a/src/test/java/com/daytodo/domain/course/service/CourseServiceTest.java +++ b/src/test/java/com/daytodo/domain/course/service/CourseServiceTest.java @@ -70,6 +70,7 @@ class CourseServiceTest { @Mock RecommendationLikeRepository recommendationLikeRepository; @Mock PlaceRepository placeRepository; @Mock RecommendationCommentRepository recommendationCommentRepository; + @Mock TodayCoursePromoter todayCoursePromoter; CourseService courseService; User user; @@ -88,6 +89,7 @@ void setUp() { recommendationLikeRepository, placeRepository, recommendationCommentRepository, + todayCoursePromoter, CLOCK ); user = new User("user@example.com", "password", "user", null, LoginType.LOCAL);