diff --git a/src/main/java/run/backend/domain/crew/controller/CrewController.java b/src/main/java/run/backend/domain/crew/controller/CrewController.java index 3142f25..3dd944c 100644 --- a/src/main/java/run/backend/domain/crew/controller/CrewController.java +++ b/src/main/java/run/backend/domain/crew/controller/CrewController.java @@ -114,9 +114,9 @@ public CommonResponse getMonthlyEvent( @GetMapping("/events/upcoming") @Operation(summary = "upcoming 일정 조회", description = "크루의 upcoming 일정 조회하는 API 입니다.") - public CommonResponse getUpcomingEvent(@MemberCrew Crew crew) { + public CommonResponse getUpcomingEvent(@MemberCrew Crew crew) { - CrewUpcomingEventResponse response = crewEventService.getCrewUpcomingEvent(crew); + EventResponseDto response = crewEventService.getCrewUpcomingEvent(crew); return new CommonResponse<>("크루 다가오는 일정 조회 성공", response); } diff --git a/src/main/java/run/backend/domain/crew/dto/response/CrewUpcomingEventResponse.java b/src/main/java/run/backend/domain/crew/dto/response/EventResponseDto.java similarity index 75% rename from src/main/java/run/backend/domain/crew/dto/response/CrewUpcomingEventResponse.java rename to src/main/java/run/backend/domain/crew/dto/response/EventResponseDto.java index a221793..b85c18c 100644 --- a/src/main/java/run/backend/domain/crew/dto/response/CrewUpcomingEventResponse.java +++ b/src/main/java/run/backend/domain/crew/dto/response/EventResponseDto.java @@ -2,7 +2,7 @@ import java.util.List; -public record CrewUpcomingEventResponse( +public record EventResponseDto( List eventProfiles ) { } diff --git a/src/main/java/run/backend/domain/crew/service/CrewEventService.java b/src/main/java/run/backend/domain/crew/service/CrewEventService.java index 1311638..37a0f0e 100644 --- a/src/main/java/run/backend/domain/crew/service/CrewEventService.java +++ b/src/main/java/run/backend/domain/crew/service/CrewEventService.java @@ -5,7 +5,7 @@ import org.springframework.transaction.annotation.Transactional; import run.backend.domain.crew.dto.common.DayStatusDto; import run.backend.domain.crew.dto.response.CrewMonthlyCanlendarResponse; -import run.backend.domain.crew.dto.response.CrewUpcomingEventResponse; +import run.backend.domain.crew.dto.response.EventResponseDto; import run.backend.domain.crew.dto.response.CrewWeeklyEventResponse; import run.backend.domain.crew.dto.response.EventProfileResponse; import run.backend.domain.crew.entity.Crew; @@ -52,13 +52,13 @@ public CrewMonthlyCanlendarResponse getCrewMonthlyCalendar(Crew crew, int year, return new CrewMonthlyCanlendarResponse(statusMap); } - public CrewUpcomingEventResponse getCrewUpcomingEvent(Crew crew) { + public EventResponseDto getCrewUpcomingEvent(Crew crew) { LocalDate today = LocalDate.now(); List events = eventRepository.findAllByCrewAndDateAfter(crew, today); List eventProfiles = eventMapper.toEventProfileList(events); - return new CrewUpcomingEventResponse(eventProfiles); + return new EventResponseDto(eventProfiles); } } diff --git a/src/main/java/run/backend/domain/event/entity/JoinEvent.java b/src/main/java/run/backend/domain/event/entity/JoinEvent.java index 494d873..cb2776b 100644 --- a/src/main/java/run/backend/domain/event/entity/JoinEvent.java +++ b/src/main/java/run/backend/domain/event/entity/JoinEvent.java @@ -1,19 +1,29 @@ package run.backend.domain.event.entity; -import jakarta.persistence.*; +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.Table; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; import run.backend.domain.member.entity.Member; import run.backend.global.common.BaseEntity; -import java.time.LocalDateTime; - @Entity @Getter @Table(name = "join_events") @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE join_events SET deleted_at = NOW() WHERE id = ?") +@Where(clause = "deleted_at IS NULL") public class JoinEvent extends BaseEntity { @Id @@ -40,8 +50,4 @@ public JoinEvent( this.member = member; this.event = event; } - - public void softDelete() { - this.setDeletedAt(LocalDateTime.now()); - } } diff --git a/src/main/java/run/backend/domain/event/entity/PeriodicEvent.java b/src/main/java/run/backend/domain/event/entity/PeriodicEvent.java index 50e386a..8b51d8e 100644 --- a/src/main/java/run/backend/domain/event/entity/PeriodicEvent.java +++ b/src/main/java/run/backend/domain/event/entity/PeriodicEvent.java @@ -5,6 +5,8 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; import run.backend.domain.crew.entity.Crew; import run.backend.domain.event.enums.RepeatCycle; import run.backend.domain.event.enums.WeekDay; @@ -18,6 +20,8 @@ @Getter @Table(name = "periodic_events") @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE periodic_events SET deleted_at = NOW() WHERE id = ?") +@Where(clause = "deleted_at IS NULL") public class PeriodicEvent extends BaseEntity { @Id @@ -111,8 +115,4 @@ public void updatePeriodicEvent( this.member = runningCaptain; } } - - public void softDelete() { - this.setDeletedAt(java.time.LocalDateTime.now()); - } } diff --git a/src/main/java/run/backend/domain/event/repository/JoinEventRepository.java b/src/main/java/run/backend/domain/event/repository/JoinEventRepository.java index 9847298..28e3982 100644 --- a/src/main/java/run/backend/domain/event/repository/JoinEventRepository.java +++ b/src/main/java/run/backend/domain/event/repository/JoinEventRepository.java @@ -1,27 +1,44 @@ package run.backend.domain.event.repository; +import java.time.LocalDate; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import run.backend.domain.crew.dto.response.EventProfileResponse; import run.backend.domain.event.entity.Event; import run.backend.domain.event.entity.JoinEvent; import run.backend.domain.member.entity.Member; -import java.util.Optional; - public interface JoinEventRepository extends JpaRepository { - void deleteByEventAndMember(Event event, Member member); - - @Query("SELECT j FROM JoinEvent j WHERE j.event = :event AND j.member = :member AND j.deletedAt IS NULL") + @Query("SELECT j FROM JoinEvent j WHERE j.event = :event AND j.member = :member") Optional findByEventAndMember(@Param("event") Event event, @Param("member") Member member); - @Query("SELECT j FROM JoinEvent j WHERE j.event = :event AND j.deletedAt IS NULL") - List findByEventAndNotDeleted(@Param("event") Event event); + @Query("SELECT j FROM JoinEvent j WHERE j.event = :event") + List findByEvent(@Param("event") Event event); - @Query("SELECT j FROM JoinEvent j WHERE j.event = :event AND j.isRunning = true AND j.deletedAt IS NULL") + @Query("SELECT j FROM JoinEvent j WHERE j.event = :event AND j.event.status = 'COMPLETED'") List findActualParticipantsByEvent(@Param("event") Event event); - boolean existsByEventAndMemberAndDeletedAtIsNull(Event event, Member member); + @Query("SELECT j FROM JoinEvent j WHERE j.member = :member " + + "AND j.event.date >= :startDate AND j.event.date <= :endDate " + + "AND j.event.status = 'COMPLETED'") + List findMonthlyParticipatedEvents(@Param("member") Member member, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + @Query(""" + SELECT new run.backend.domain.crew.dto.response.EventProfileResponse(\ + e.id, e.title, e.date, e.startTime, e.endTime, e.expectedParticipants) \ + FROM JoinEvent j JOIN j.event e \ + WHERE j.member = :member \ + AND e.date >= :startDate AND e.date <= :endDate \ + AND e.status = 'COMPLETED' ORDER BY e.date DESC""") + List findMonthlyCompletedEvents(@Param("member") Member member, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + boolean existsByEventAndMember(Event event, Member member); } diff --git a/src/main/java/run/backend/domain/event/service/EventService.java b/src/main/java/run/backend/domain/event/service/EventService.java index 50fce55..39b4190 100644 --- a/src/main/java/run/backend/domain/event/service/EventService.java +++ b/src/main/java/run/backend/domain/event/service/EventService.java @@ -113,7 +113,7 @@ private Member validateNewRunningCaptain(EventInfoRequest request, Event event) private void updateRunningCaptain(Event event, Member newRunningCaptain) { joinEventRepository.findByEventAndMember(event, event.getMember()) - .ifPresent(JoinEvent::softDelete); + .ifPresent(joinEventRepository::delete); JoinEvent newJoinEvent = eventMapper.toJoinEvent(event, newRunningCaptain); joinEventRepository.save(newJoinEvent); @@ -132,7 +132,7 @@ private void handlePeriodicEventUpdate(Event event, EventInfoRequest request, RepeatCycle requestedRepeatCycle = request.repeatCycle(); if (requestedRepeatCycle == null || requestedRepeatCycle == RepeatCycle.NONE) { - existingPeriodicEvent.ifPresent(PeriodicEvent::softDelete); + existingPeriodicEvent.ifPresent(periodicEventRepository::delete); } else { if (existingPeriodicEvent.isPresent()) { PeriodicEvent periodicEvent = existingPeriodicEvent.get(); @@ -173,7 +173,7 @@ public EventDetailResponse getEventDetail(Long eventId) { private List getParticipants(Event event, EventStatus status) { return status == EventStatus.COMPLETED ? joinEventRepository.findActualParticipantsByEvent(event) - : joinEventRepository.findByEventAndNotDeleted(event); + : joinEventRepository.findByEvent(event); } @Transactional @@ -182,7 +182,7 @@ public void joinEvent(Long eventId, Member member) { Event event = eventRepository.findById(eventId) .orElseThrow(EventNotFound::new); - if (joinEventRepository.existsByEventAndMemberAndDeletedAtIsNull(event, member)) { + if (joinEventRepository.existsByEventAndMember(event, member)) { throw new AlreadyJoinedEvent(); } @@ -201,8 +201,7 @@ public void cancelJoinEvent(Long eventId, Member member) { JoinEvent joinEvent = joinEventRepository.findByEventAndMember(event, member) .orElseThrow(JoinEventNotFound::new); - joinEvent.softDelete(); - joinEventRepository.save(joinEvent); + joinEventRepository.delete(joinEvent); event.decrementExpectedParticipants(); } diff --git a/src/main/java/run/backend/domain/member/controller/MemberController.java b/src/main/java/run/backend/domain/member/controller/MemberController.java index a29a3f2..4acdb87 100644 --- a/src/main/java/run/backend/domain/member/controller/MemberController.java +++ b/src/main/java/run/backend/domain/member/controller/MemberController.java @@ -3,11 +3,18 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import run.backend.domain.crew.dto.response.EventResponseDto; import run.backend.domain.member.dto.request.MemberInfoRequest; import run.backend.domain.member.dto.response.MemberCrewStatusResponse; import run.backend.domain.member.dto.response.MemberInfoResponse; +import run.backend.domain.member.dto.response.MemberParticipatedCountResponse; import run.backend.domain.member.entity.Member; import run.backend.domain.member.service.MemberService; import run.backend.global.annotation.member.Login; @@ -48,4 +55,20 @@ public CommonResponse getMembersCrewExists(@Login Memb MemberCrewStatusResponse response = memberService.getMembersCrewExists(member); return new CommonResponse<>("유저 크루 가입 여부 조회 완료", response); } + + @GetMapping("/participated/preview") + @Operation(summary = "유저의 이번 시즌 참여 횟수 조회", description = "유저가 이번 달에 참여한 러닝 횟수를 조회하는 API 입니다.") + public CommonResponse getParticipatedCount(@Login Member member) { + + MemberParticipatedCountResponse response = memberService.getParticipatedEventCount(member); + return new CommonResponse<>("유저의 이번 시즌 참여 횟수 조회 성공", response); + } + + @GetMapping("/participated") + @Operation(summary = "유저의 이번 시즌 참여한 러닝 리스트 조회", description = "유저가 이번 시즌에 참여한 러닝 리스트를 조회하는 API 입니다.") + public CommonResponse getParticipated(@Login Member member) { + + EventResponseDto response = memberService.getParticipatedEvent(member); + return new CommonResponse<>("러닝에 대한 상세 참여 내역 조회 성공", response); + } } diff --git a/src/main/java/run/backend/domain/member/dto/response/MemberParticipatedCountResponse.java b/src/main/java/run/backend/domain/member/dto/response/MemberParticipatedCountResponse.java new file mode 100644 index 0000000..e8b442b --- /dev/null +++ b/src/main/java/run/backend/domain/member/dto/response/MemberParticipatedCountResponse.java @@ -0,0 +1,9 @@ +package run.backend.domain.member.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record MemberParticipatedCountResponse( + @Schema(description = "유저의 이번 시즌 참여 횟수", example = "4") + Long participatedCount +) { +} diff --git a/src/main/java/run/backend/domain/member/service/MemberService.java b/src/main/java/run/backend/domain/member/service/MemberService.java index e6e82a9..725165f 100644 --- a/src/main/java/run/backend/domain/member/service/MemberService.java +++ b/src/main/java/run/backend/domain/member/service/MemberService.java @@ -1,21 +1,29 @@ package run.backend.domain.member.service; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import run.backend.domain.crew.dto.response.EventResponseDto; +import run.backend.domain.crew.dto.response.EventProfileResponse; import run.backend.domain.crew.entity.Crew; import run.backend.domain.crew.entity.JoinCrew; import run.backend.domain.crew.enums.JoinStatus; import run.backend.domain.crew.repository.JoinCrewRepository; +import run.backend.domain.event.entity.JoinEvent; +import run.backend.domain.event.repository.JoinEventRepository; import run.backend.domain.file.service.FileService; import run.backend.domain.member.dto.request.MemberInfoRequest; import run.backend.domain.member.dto.response.MemberCrewStatusResponse; import run.backend.domain.member.dto.response.MemberInfoResponse; +import run.backend.domain.member.dto.response.MemberParticipatedCountResponse; import run.backend.domain.member.entity.Member; import run.backend.domain.member.repository.MemberRepository; - -import java.util.Optional; +import run.backend.global.dto.DateRange; +import run.backend.global.util.DateRangeUtil; @Service @RequiredArgsConstructor @@ -25,6 +33,8 @@ public class MemberService { private final FileService fileService; private final MemberRepository memberRepository; private final JoinCrewRepository joinCrewRepository; + private final JoinEventRepository joinEventRepository; + private final DateRangeUtil dateRangeUtil; public MemberInfoResponse getMemberInfo(Member member) { @@ -59,4 +69,27 @@ public MemberCrewStatusResponse getMembersCrewExists(Member member) { } return new MemberCrewStatusResponse(joinCrew.get().getJoinStatus().toString()); } + + public MemberParticipatedCountResponse getParticipatedEventCount(Member member) { + + LocalDate today = LocalDate.now(); + DateRange monthRange = dateRangeUtil.getMonthRange(today.getYear(), today.getMonthValue()); + + List monthlyJoinEvents = joinEventRepository.findMonthlyParticipatedEvents( + member, monthRange.start(), monthRange.end()); + + Long participatedCount = (long) monthlyJoinEvents.size(); + return new MemberParticipatedCountResponse(participatedCount); + } + + public EventResponseDto getParticipatedEvent(Member member) { + + LocalDate today = LocalDate.now(); + DateRange monthRange = dateRangeUtil.getMonthRange(today.getYear(), today.getMonthValue()); + + List eventProfiles = joinEventRepository.findMonthlyCompletedEvents( + member, monthRange.start(), monthRange.end()); + + return new EventResponseDto(eventProfiles); + } } diff --git a/src/test/java/run/backend/domain/crew/service/CrewEventServiceTest.java b/src/test/java/run/backend/domain/crew/service/CrewEventServiceTest.java index 5139bd5..616967f 100644 --- a/src/test/java/run/backend/domain/crew/service/CrewEventServiceTest.java +++ b/src/test/java/run/backend/domain/crew/service/CrewEventServiceTest.java @@ -8,7 +8,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import run.backend.domain.crew.dto.common.DayStatusDto; import run.backend.domain.crew.dto.response.CrewMonthlyCanlendarResponse; -import run.backend.domain.crew.dto.response.CrewUpcomingEventResponse; +import run.backend.domain.crew.dto.response.EventResponseDto; import run.backend.domain.crew.dto.response.CrewWeeklyEventResponse; import run.backend.domain.crew.dto.response.EventProfileResponse; import run.backend.domain.crew.entity.Crew; @@ -128,7 +128,7 @@ void shouldReturnUpcomingEvent() { when(eventMapper.toEventProfileList(events)).thenReturn(eventProfiles); // when - CrewUpcomingEventResponse response = crewEventService.getCrewUpcomingEvent(crew); + EventResponseDto response = crewEventService.getCrewUpcomingEvent(crew); // then assertThat(response.eventProfiles()).isEqualTo(eventProfiles); diff --git a/src/test/java/run/backend/domain/event/service/EventServiceTest.java b/src/test/java/run/backend/domain/event/service/EventServiceTest.java index 97b6b2b..0237d8c 100644 --- a/src/test/java/run/backend/domain/event/service/EventServiceTest.java +++ b/src/test/java/run/backend/domain/event/service/EventServiceTest.java @@ -233,7 +233,7 @@ void shouldUpdateBasicInfoSuccessfully() { // then then(eventRepository).should().findById(1L); - then(joinEventRepository).should(never()).deleteByEventAndMember(any(), any()); + then(joinEventRepository).should(never()).delete(any()); } @Test @@ -289,9 +289,8 @@ void shouldChangeRunningCaptainSuccessfully() { // then then(joinEventRepository).should().findByEventAndMember(savedEvent, runningCaptain); + then(joinEventRepository).should().delete(savedJoinEvent); then(joinEventRepository).should().save(any(JoinEvent.class)); - - assertThat(savedJoinEvent.getDeletedAt()).isNotNull(); } @Test @@ -315,8 +314,8 @@ void shouldActuallyChangeRunningCaptain() { sut.updateEvent(1L, request); // then + then(joinEventRepository).should().delete(savedJoinEvent); assertThat(savedEvent.getMember()).isEqualTo(newCaptain); - assertThat(savedJoinEvent.getDeletedAt()).isNotNull(); } @Test @@ -354,7 +353,7 @@ void shouldRemovePeriodicEventSuccessfully() { sut.updateEvent(1L, request); // then - assertThat(savedPeriodicEvent.getDeletedAt()).isNotNull(); + then(periodicEventRepository).should().delete(savedPeriodicEvent); } @Test @@ -488,7 +487,7 @@ class GetEventTest { void shouldReturnExpectedParticipantsBeforeEvent() { //given given(eventRepository.findById(1L)).willReturn(Optional.of(savedEvent)); - given(joinEventRepository.findByEventAndNotDeleted(any())).willReturn(List.of(savedJoinEvent)); + given(joinEventRepository.findByEvent(any())).willReturn(List.of(savedJoinEvent)); given(joinEventRepository.findActualParticipantsByEvent(any())).willReturn(List.of(savedJoinEvent)); //when @@ -496,7 +495,7 @@ void shouldReturnExpectedParticipantsBeforeEvent() { //then then(eventRepository).should().findById(1L); - then(joinEventRepository).should().findByEventAndNotDeleted(any()); + then(joinEventRepository).should().findByEvent(any()); then(joinEventRepository).should(never()).findActualParticipantsByEvent(any()); } @@ -505,7 +504,7 @@ void shouldReturnExpectedParticipantsBeforeEvent() { void shouldReturnActualParticipantsAfterEvent() { //given given(eventRepository.findById(1L)).willReturn(Optional.of(completedEvent)); - given(joinEventRepository.findByEventAndNotDeleted(any())).willReturn(List.of(savedJoinEvent)); + given(joinEventRepository.findByEvent(any())).willReturn(List.of(savedJoinEvent)); given(joinEventRepository.findActualParticipantsByEvent(any())).willReturn(List.of(savedJoinEvent)); //when @@ -513,7 +512,7 @@ void shouldReturnActualParticipantsAfterEvent() { //then then(eventRepository).should().findById(1L); - then(joinEventRepository).should(never()).findByEventAndNotDeleted(any()); + then(joinEventRepository).should(never()).findByEvent(any()); then(joinEventRepository).should().findActualParticipantsByEvent(any()); } @@ -529,7 +528,7 @@ void shouldThrowExceptionWhenEventNotFound() { //then then(eventRepository).should().findById(1L); - then(joinEventRepository).should(never()).findByEventAndNotDeleted(any()); + then(joinEventRepository).should(never()).findByEvent(any()); then(joinEventRepository).should(never()).findActualParticipantsByEvent(any()); } } @@ -545,7 +544,7 @@ void shouldJoinEventSuccessfully() { given(eventRepository.findById(1L)).willReturn(Optional.of(savedEvent)); given(joinEventRepository.save(any())).willReturn(savedJoinEvent); - given(joinEventRepository.existsByEventAndMemberAndDeletedAtIsNull(any(),any())).willReturn(false); + given(joinEventRepository.existsByEventAndMember(any(),any())).willReturn(false); given(eventMapper.toJoinEvent(any(Event.class), any(Member.class))) .willReturn(savedJoinEvent); @@ -565,7 +564,7 @@ void shouldThrowExceptionWhenAlreadyJoined() { Long initialExpectedParticipants = savedEvent.getExpectedParticipants(); given(eventRepository.findById(1L)).willReturn(Optional.of(savedEvent)); - given(joinEventRepository.existsByEventAndMemberAndDeletedAtIsNull(any(),any())).willReturn(true); + given(joinEventRepository.existsByEventAndMember(any(),any())).willReturn(true); //when & then assertThatThrownBy(() -> sut.joinEvent(1L, requestMember)) @@ -595,7 +594,6 @@ void shouldCancelEventSuccessfully() { Long initialExpectedParticipants = savedEvent.getExpectedParticipants(); given(eventRepository.findById(1L)).willReturn(Optional.of(savedEvent)); - given(joinEventRepository.save(any())).willReturn(savedJoinEvent); given(joinEventRepository.findByEventAndMember(any(), any())).willReturn(Optional.of(savedJoinEvent)); //when @@ -603,7 +601,7 @@ void shouldCancelEventSuccessfully() { //then then(eventRepository).should().findById(1L); - then(joinEventRepository).should().save(any()); + then(joinEventRepository).should().delete(savedJoinEvent); assertThat(savedEvent.getExpectedParticipants()).isEqualTo(initialExpectedParticipants - 1); } @@ -614,7 +612,7 @@ void shouldThrowExceptionWhenNotJoined() { Long initialExpectedParticipants = savedEvent.getExpectedParticipants(); given(eventRepository.findById(1L)).willReturn(Optional.of(savedEvent)); - given(joinEventRepository.existsByEventAndMemberAndDeletedAtIsNull(any(),any())).willReturn(true); + given(joinEventRepository.findByEventAndMember(any(), any())).willReturn(Optional.empty()); //when & then assertThatThrownBy(() -> sut.cancelJoinEvent(1L, requestMember)) diff --git a/src/test/java/run/backend/domain/member/service/MemberServiceTest.java b/src/test/java/run/backend/domain/member/service/MemberServiceTest.java index c206c79..084729b 100644 --- a/src/test/java/run/backend/domain/member/service/MemberServiceTest.java +++ b/src/test/java/run/backend/domain/member/service/MemberServiceTest.java @@ -1,33 +1,44 @@ package run.backend.domain.member.service; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.web.multipart.MultipartFile; +import org.springframework.test.util.ReflectionTestUtils; +import run.backend.domain.crew.dto.response.EventResponseDto; +import run.backend.domain.crew.dto.response.EventProfileResponse; import run.backend.domain.crew.entity.Crew; import run.backend.domain.crew.entity.JoinCrew; import run.backend.domain.crew.enums.JoinStatus; import run.backend.domain.crew.repository.JoinCrewRepository; -import run.backend.domain.file.service.FileService; -import run.backend.domain.member.dto.request.MemberInfoRequest; +import run.backend.domain.event.entity.JoinEvent; +import run.backend.domain.event.repository.JoinEventRepository; import run.backend.domain.member.dto.response.MemberCrewStatusResponse; import run.backend.domain.member.dto.response.MemberInfoResponse; +import run.backend.domain.member.dto.response.MemberParticipatedCountResponse; import run.backend.domain.member.entity.Member; import run.backend.domain.member.enums.Gender; import run.backend.domain.member.enums.OAuthType; import run.backend.domain.member.repository.MemberRepository; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.*; +import run.backend.global.dto.DateRange; +import run.backend.global.util.DateRangeUtil; @ExtendWith(MockitoExtension.class) +@DisplayName("MemberService") public class MemberServiceTest { @Mock @@ -36,37 +47,52 @@ public class MemberServiceTest { @Mock private JoinCrewRepository joinCrewRepository; + @Mock + private JoinEventRepository joinEventRepository; + + @Mock + private DateRangeUtil dateRangeUtil; + @InjectMocks - private MemberService memberService; + private MemberService sut; // System Under Test private Member testMember; private Crew testCrew; - private MemberInfoRequest data; - @BeforeEach public void setUp() { // member - testMember = spy(Member.builder() - .username("test username") - .oauthId("test id") - .oauthType(OAuthType.GOOGLE) - .profileImage("test image") - .build()); + testMember = createMemberWithId(1L, "테스트사용자"); // crew - testCrew = spy(Crew.builder() - .name("test crew name") - .description("크루 소개 테스트") - .image("test image url") - .build()); - - // memberInfoRequest - data = new MemberInfoRequest( - Gender.FEMALE, - 20, - "newNickname"); + testCrew = createCrew("테스트크루"); + } + + private Member createMemberWithId(Long id, String nickname) { + Member member = Member.builder() + .username("test_user") + .nickname(nickname) + .gender(Gender.MALE) + .age(25) + .oauthId("oauth_id_" + id) + .oauthType(OAuthType.GOOGLE) + .profileImage("profile.jpg") + .build(); + + ReflectionTestUtils.setField(member, "id", id); + return member; + } + + private Crew createCrew(String name) { + Crew crew = Crew.builder() + .name(name) + .description("테스트 크루 설명") + .image("crew.jpg") + .build(); + + ReflectionTestUtils.setField(crew, "id", 1L); + return crew; } @Test @@ -78,7 +104,7 @@ public void getMemberInfoTest() { .thenReturn(Optional.of(testCrew)); // when - MemberInfoResponse response = memberService.getMemberInfo(testMember); + MemberInfoResponse response = sut.getMemberInfo(testMember); // then assertEquals(testMember.getNickname(), response.nickName()); @@ -93,7 +119,7 @@ void getMembersCrewExists_shouldReturnNone_whenNoJoinCrew() { when(joinCrewRepository.findByMember(member)).thenReturn(Optional.empty()); // When - MemberCrewStatusResponse response = memberService.getMembersCrewExists(member); + MemberCrewStatusResponse response = sut.getMembersCrewExists(member); // Then assertEquals("NONE", response.status()); @@ -109,9 +135,140 @@ void getMembersCrewExists_shouldReturnJoinStatus_whenJoinCrewExists() { when(joinCrewRepository.findByMember(member)).thenReturn(Optional.of(joinCrew)); // When - MemberCrewStatusResponse response = memberService.getMembersCrewExists(member); + MemberCrewStatusResponse response = sut.getMembersCrewExists(member); // Then assertEquals("APPROVED", response.status()); } + + @Nested + @DisplayName("getParticipatedEventCount 메서드는") + class GetParticipatedEventCountTest { + + @Test + @DisplayName("이번 달 참여한 이벤트 개수를 반환한다") + void shouldReturnMonthlyParticipatedEventCount() { + // given + LocalDate today = LocalDate.now(); + DateRange monthRange = new DateRange( + today.withDayOfMonth(1), + today.withDayOfMonth(today.lengthOfMonth()) + ); + + JoinEvent joinEvent1 = createJoinEvent(1L); + JoinEvent joinEvent2 = createJoinEvent(2L); + List monthlyJoinEvents = List.of(joinEvent1, joinEvent2); + + given(dateRangeUtil.getMonthRange(today.getYear(), today.getMonthValue())) + .willReturn(monthRange); + given(joinEventRepository.findMonthlyParticipatedEvents( + testMember, monthRange.start(), monthRange.end())) + .willReturn(monthlyJoinEvents); + + // when + MemberParticipatedCountResponse response = sut.getParticipatedEventCount(testMember); + + // then + assertThat(response.participatedCount()).isEqualTo(2); + } + + @Test + @DisplayName("참여한 이벤트가 없을 때 0을 반환한다") + void shouldReturnZeroWhenNoParticipatedEvents() { + // given + LocalDate today = LocalDate.now(); + DateRange monthRange = new DateRange( + today.withDayOfMonth(1), + today.withDayOfMonth(today.lengthOfMonth()) + ); + + given(dateRangeUtil.getMonthRange(today.getYear(), today.getMonthValue())) + .willReturn(monthRange); + given(joinEventRepository.findMonthlyParticipatedEvents( + testMember, monthRange.start(), monthRange.end())) + .willReturn(List.of()); + + // when + MemberParticipatedCountResponse response = sut.getParticipatedEventCount(testMember); + + // then + assertThat(response.participatedCount()).isEqualTo(0); + } + } + + @Nested + @DisplayName("getParticipatedEvent 메서드는") + class GetParticipatedEventTest { + + @Test + @DisplayName("이번 달 완료된 이벤트 목록을 반환한다") + void shouldReturnMonthlyCompletedEvents() { + // given + LocalDate today = LocalDate.now(); + DateRange monthRange = new DateRange( + today.withDayOfMonth(1), + today.withDayOfMonth(today.lengthOfMonth()) + ); + + EventProfileResponse eventProfile1 = createEventProfileResponse(1L, "러닝 이벤트 1"); + EventProfileResponse eventProfile2 = createEventProfileResponse(2L, "러닝 이벤트 2"); + List eventProfiles = List.of(eventProfile1, eventProfile2); + + given(dateRangeUtil.getMonthRange(today.getYear(), today.getMonthValue())) + .willReturn(monthRange); + given(joinEventRepository.findMonthlyCompletedEvents( + testMember, monthRange.start(), monthRange.end())) + .willReturn(eventProfiles); + + // when + EventResponseDto response = sut.getParticipatedEvent(testMember); + + // then + assertThat(response.eventProfiles()).hasSize(2); + assertThat(response.eventProfiles()).containsExactly(eventProfile1, eventProfile2); + } + + @Test + @DisplayName("완료된 이벤트가 없을 때 빈 목록을 반환한다") + void shouldReturnEmptyListWhenNoCompletedEvents() { + // given + LocalDate today = LocalDate.now(); + DateRange monthRange = new DateRange( + today.withDayOfMonth(1), + today.withDayOfMonth(today.lengthOfMonth()) + ); + + given(dateRangeUtil.getMonthRange(today.getYear(), today.getMonthValue())) + .willReturn(monthRange); + given(joinEventRepository.findMonthlyCompletedEvents( + testMember, monthRange.start(), monthRange.end())) + .willReturn(List.of()); + + // when + EventResponseDto response = sut.getParticipatedEvent(testMember); + + // then + assertThat(response.eventProfiles()).isEmpty(); + } + } + + private JoinEvent createJoinEvent(Long eventId) { + JoinEvent joinEvent = JoinEvent.builder() + .member(testMember) + .build(); + + ReflectionTestUtils.setField(joinEvent, "id", eventId); + return joinEvent; + } + + private EventProfileResponse createEventProfileResponse(Long eventId, String title) { + return new EventProfileResponse( + eventId, + title, + LocalDate.now(), + LocalTime.of(9, 0), + LocalTime.of(10, 0), + 5L + ); + } }