diff --git a/src/main/java/run/backend/domain/event/controller/EventController.java b/src/main/java/run/backend/domain/event/controller/EventController.java index 660f7a9..575a037 100644 --- a/src/main/java/run/backend/domain/event/controller/EventController.java +++ b/src/main/java/run/backend/domain/event/controller/EventController.java @@ -3,7 +3,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -11,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import run.backend.domain.event.dto.request.EventInfoRequest; +import run.backend.domain.event.dto.response.EventDetailResponse; import run.backend.domain.event.service.EventService; import run.backend.domain.member.entity.Member; import run.backend.global.annotation.member.Login; @@ -25,7 +27,7 @@ public class EventController { private final EventService eventService; @PostMapping - @PreAuthorize("hasRole('MANAGER') or hasRole('LEADER')") +// @PreAuthorize("hasRole('MANAGER') or hasRole('LEADER')") @Operation(summary = "일정 생성", description = "러닝 일정을 생성합니다. LEADER 또는 MANAGER 권한이 필요합니다.") public CommonResponse createEvent( @RequestBody EventInfoRequest eventInfoRequest, @@ -37,15 +39,35 @@ public CommonResponse createEvent( } @PatchMapping("/{eventId}") - @PreAuthorize("hasRole('MANAGER') or hasRole('LEADER')") +// @PreAuthorize("hasRole('MANAGER') or hasRole('LEADER')") @Operation(summary = "일정 수정", description = "러닝 일정을 수정합니다. LEADER 또는 MANAGER 권한이 필요합니다.") public CommonResponse updateEvent( @PathVariable Long eventId, - @RequestBody EventInfoRequest eventUpdateRequest, - @Login Member member + @RequestBody EventInfoRequest eventUpdateRequest ) { - eventService.updateEvent(eventId, eventUpdateRequest, member); + eventService.updateEvent(eventId, eventUpdateRequest); return new CommonResponse<>("러닝 일정 수정 성공"); } + + @GetMapping("/{eventId}") + @Operation(summary = "일정 상세 조회", description = "러닝 일정을 조회합니다.") + public CommonResponse getEventDetail(@PathVariable Long eventId) { + EventDetailResponse response = eventService.getEventDetail(eventId); + return new CommonResponse<>("러닝 일정 상세 조회 성공", response); + } + + @PostMapping("/{eventId}/join-requests") + @Operation(summary = "러닝 참여 요청", description = "러닝 참여를 요청합니다") + public CommonResponse joinEvent(@PathVariable Long eventId, @Login Member member) { + eventService.joinEvent(eventId, member); + return new CommonResponse<>("러닝 참여 요청 완료"); + } + + @DeleteMapping("/{eventId}/join-requests") + @Operation(summary = "러닝 참여 요청 취소", description = "러닝 참여 요청을 취소합니다") + public CommonResponse cancelJoinEvent(@PathVariable Long eventId, @Login Member member) { + eventService.cancelJoinEvent(eventId, member); + return new CommonResponse<>("러닝 참여 요청 취소 완료"); + } } diff --git a/src/main/java/run/backend/domain/event/dto/response/EventDetailResponse.java b/src/main/java/run/backend/domain/event/dto/response/EventDetailResponse.java new file mode 100644 index 0000000..39d5f6c --- /dev/null +++ b/src/main/java/run/backend/domain/event/dto/response/EventDetailResponse.java @@ -0,0 +1,22 @@ +package run.backend.domain.event.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import run.backend.domain.event.enums.EventStatus; + +import java.time.LocalDateTime; +import java.util.List; + +public record EventDetailResponse( + String title, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") + LocalDateTime startDateTime, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") + LocalDateTime endDateTime, + String startLocation, + EventStatus status, + String distanceKm, + String runningTime, + Long runningLeaderId, + List participants +) { +} diff --git a/src/main/java/run/backend/domain/event/dto/response/ParticipantDto.java b/src/main/java/run/backend/domain/event/dto/response/ParticipantDto.java new file mode 100644 index 0000000..0c5878f --- /dev/null +++ b/src/main/java/run/backend/domain/event/dto/response/ParticipantDto.java @@ -0,0 +1,8 @@ +package run.backend.domain.event.dto.response; + +public record ParticipantDto( + Long id, + String name, + String image +) { +} diff --git a/src/main/java/run/backend/domain/event/entity/Event.java b/src/main/java/run/backend/domain/event/entity/Event.java index 5173bbe..88e37f3 100644 --- a/src/main/java/run/backend/domain/event/entity/Event.java +++ b/src/main/java/run/backend/domain/event/entity/Event.java @@ -1,18 +1,27 @@ 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.OneToOne; +import jakarta.persistence.Table; +import java.time.LocalDate; +import java.time.LocalTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import run.backend.domain.crew.entity.Crew; +import run.backend.domain.event.enums.EventStatus; import run.backend.domain.member.entity.Member; import run.backend.domain.record.entity.CrewRecord; import run.backend.global.common.BaseEntity; -import java.time.LocalDate; -import java.time.LocalTime; - @Entity @Getter @@ -54,6 +63,8 @@ public class Event extends BaseEntity { @JoinColumn(name = "running_captain") private Member member; + private EventStatus status; + @Builder public Event( String title, @@ -70,21 +81,38 @@ public Event( this.startTime = startTime; this.endTime = endTime; this.place = place; - this.expectedParticipants = 0L; + this.expectedParticipants = 1L; this.actualParticipants = 0L; this.crew = crew; this.record = record; this.member = member; + this.status = EventStatus.BEFORE; } public void incrementExpectedParticipants() { this.expectedParticipants++; } + public void decrementExpectedParticipants() { + if (this.expectedParticipants > 0) { + this.expectedParticipants--; + } + } + public void incrementActualParticipants() { this.actualParticipants++; } + public void decrementActualParticipants() { + if (this.actualParticipants > 0) { + this.actualParticipants--; + } + } + + public void complete() { + this.status = EventStatus.COMPLETED; + } + public void updateEvent( String title, LocalDate date, @@ -112,4 +140,22 @@ public void updateEvent( this.member = runningCaptain; } } + + public String getDistanceKm() { + if (record != null && record.getDistance() != null) { + return record.getDistance().toString(); + } + return "0"; + } + + public String getRunningTime() { + if (record != null && record.getDurationTime() != null) { + long totalSeconds = record.getDurationTime(); + long hours = totalSeconds / 3600; + long minutes = (totalSeconds % 3600) / 60; + long seconds = totalSeconds % 60; + return String.format("%02d:%02d:%02d", hours, minutes, seconds); + } + return "00:00:00"; + } } 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 48bc5ee..494d873 100644 --- a/src/main/java/run/backend/domain/event/entity/JoinEvent.java +++ b/src/main/java/run/backend/domain/event/entity/JoinEvent.java @@ -6,12 +6,15 @@ import lombok.Getter; import lombok.NoArgsConstructor; 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) -public class JoinEvent { +public class JoinEvent extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -36,6 +39,9 @@ public JoinEvent( this.isRunning = false; this.member = member; this.event = event; - this.event.incrementExpectedParticipants(); + } + + 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 79e995b..50e386a 100644 --- a/src/main/java/run/backend/domain/event/entity/PeriodicEvent.java +++ b/src/main/java/run/backend/domain/event/entity/PeriodicEvent.java @@ -111,4 +111,8 @@ 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/enums/EventStatus.java b/src/main/java/run/backend/domain/event/enums/EventStatus.java new file mode 100644 index 0000000..de6adbf --- /dev/null +++ b/src/main/java/run/backend/domain/event/enums/EventStatus.java @@ -0,0 +1,6 @@ +package run.backend.domain.event.enums; + +public enum EventStatus { + BEFORE, + COMPLETED; +} diff --git a/src/main/java/run/backend/domain/event/exception/EventErrorCode.java b/src/main/java/run/backend/domain/event/exception/EventErrorCode.java index cd3f607..e3e5429 100644 --- a/src/main/java/run/backend/domain/event/exception/EventErrorCode.java +++ b/src/main/java/run/backend/domain/event/exception/EventErrorCode.java @@ -9,7 +9,9 @@ public enum EventErrorCode implements ErrorCode { RUNNING_CAPTAIN_NOT_CREW_MEMBER(6001, "러닝캡이 크루원이 아닙니다."), - EVENT_NOT_FOUND(6002, "일정을 찾을 수 없습니다."); + EVENT_NOT_FOUND(6002, "일정을 찾을 수 없습니다."), + ALREADY_JOINED_EVENT(6003, "이미 참여 요청이 되어있습니다."), + JOIN_EVENT_NOT_FOUND(6004, "참여 요청이 존재하지 않습니다."); private final int errorCode; private final String errorMessage; diff --git a/src/main/java/run/backend/domain/event/exception/EventException.java b/src/main/java/run/backend/domain/event/exception/EventException.java index 179f26d..6ff97a3 100644 --- a/src/main/java/run/backend/domain/event/exception/EventException.java +++ b/src/main/java/run/backend/domain/event/exception/EventException.java @@ -19,4 +19,17 @@ public EventNotFound() { super(EventErrorCode.EVENT_NOT_FOUND); } } + + public static class AlreadyJoinedEvent extends EventException { + public AlreadyJoinedEvent() { + super(EventErrorCode.ALREADY_JOINED_EVENT); + } + } + + public static class JoinEventNotFound extends EventException { + public JoinEventNotFound() { + super(EventErrorCode.JOIN_EVENT_NOT_FOUND); + } + } + } diff --git a/src/main/java/run/backend/domain/event/mapper/EventMapper.java b/src/main/java/run/backend/domain/event/mapper/EventMapper.java index 29a7f3b..d518c86 100644 --- a/src/main/java/run/backend/domain/event/mapper/EventMapper.java +++ b/src/main/java/run/backend/domain/event/mapper/EventMapper.java @@ -7,9 +7,12 @@ import run.backend.domain.crew.dto.response.EventProfileResponse; import run.backend.domain.crew.entity.Crew; import run.backend.domain.event.dto.request.EventInfoRequest; +import run.backend.domain.event.dto.response.EventDetailResponse; +import run.backend.domain.event.dto.response.ParticipantDto; import run.backend.domain.event.entity.Event; import run.backend.domain.event.entity.JoinEvent; import run.backend.domain.event.entity.PeriodicEvent; +import run.backend.domain.event.enums.EventStatus; import run.backend.domain.member.entity.Member; @Mapper( @@ -35,6 +38,25 @@ public interface EventMapper { List toEventProfileList(List events); + @Mapping(target = "startDateTime", expression = "java(java.time.LocalDateTime.of(event.getDate(), event.getStartTime()))") + @Mapping(target = "endDateTime", expression = "java(java.time.LocalDateTime.of(event.getDate(), event.getEndTime()))") + @Mapping(target = "startLocation", source = "event.place") + @Mapping(target = "runningLeaderId", source = "event.member.id") + @Mapping(target = "distanceKm", source = "event.distanceKm") + @Mapping(target = "runningTime", source = "event.runningTime") + EventDetailResponse toEventDetailResponse(Event event, EventStatus status, List participants); + + @Mapping(target = "id", source = "member.id") + @Mapping(target = "name", source = "member.nickname") + @Mapping(target = "image", source = "member.profileImage") + ParticipantDto toParticipantDto(JoinEvent joinEvent); + + default List toParticipantDtoList(List joinEvents) { + return joinEvents.stream() + .map(this::toParticipantDto) + .toList(); + } + default EventInfoRequest toEventInfoRequest(EventInfoRequest updateRequest, Event event) { return new EventInfoRequest( updateRequest.title() != null ? updateRequest.title() : event.getTitle(), 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 2977acf..9847298 100644 --- a/src/main/java/run/backend/domain/event/repository/JoinEventRepository.java +++ b/src/main/java/run/backend/domain/event/repository/JoinEventRepository.java @@ -1,12 +1,27 @@ package run.backend.domain.event.repository; +import java.util.List; 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.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") + 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 AND j.isRunning = true AND j.deletedAt IS NULL") + List findActualParticipantsByEvent(@Param("event") Event event); + + boolean existsByEventAndMemberAndDeletedAtIsNull(Event event, Member member); } diff --git a/src/main/java/run/backend/domain/event/repository/PeriodicEventRepository.java b/src/main/java/run/backend/domain/event/repository/PeriodicEventRepository.java index f908f09..baa61c4 100644 --- a/src/main/java/run/backend/domain/event/repository/PeriodicEventRepository.java +++ b/src/main/java/run/backend/domain/event/repository/PeriodicEventRepository.java @@ -18,6 +18,7 @@ public interface PeriodicEventRepository extends JpaRepository findByCrewAndTitleAndTime( @Param("crew") Crew crew, 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 6c5ffbb..50fce55 100644 --- a/src/main/java/run/backend/domain/event/service/EventService.java +++ b/src/main/java/run/backend/domain/event/service/EventService.java @@ -1,5 +1,6 @@ package run.backend.domain.event.service; +import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -9,12 +10,17 @@ import run.backend.domain.crew.repository.JoinCrewRepository; import run.backend.domain.event.dto.request.EventInfoRequest; import run.backend.domain.event.dto.response.EventCreationValidationDto; +import run.backend.domain.event.dto.response.EventDetailResponse; +import run.backend.domain.event.dto.response.ParticipantDto; import run.backend.domain.event.entity.Event; import run.backend.domain.event.entity.JoinEvent; import run.backend.domain.event.entity.PeriodicEvent; +import run.backend.domain.event.enums.EventStatus; import run.backend.domain.event.enums.RepeatCycle; +import run.backend.domain.event.exception.EventException.AlreadyJoinedEvent; import run.backend.domain.event.exception.EventException.EventNotFound; import run.backend.domain.event.exception.EventException.InvalidEventCreationRequest; +import run.backend.domain.event.exception.EventException.JoinEventNotFound; import run.backend.domain.event.mapper.EventMapper; import run.backend.domain.event.repository.EventRepository; import run.backend.domain.event.repository.JoinEventRepository; @@ -55,7 +61,7 @@ public void createEvent(EventInfoRequest eventInfoRequest, Member member) { @Transactional @Logging - public void updateEvent(Long eventId, EventInfoRequest eventUpdateRequest, Member member) { + public void updateEvent(Long eventId, EventInfoRequest eventUpdateRequest) { Event event = eventRepository.findById(eventId) .orElseThrow(EventNotFound::new); @@ -106,7 +112,8 @@ private Member validateNewRunningCaptain(EventInfoRequest request, Event event) } private void updateRunningCaptain(Event event, Member newRunningCaptain) { - joinEventRepository.deleteByEventAndMember(event, event.getMember()); + joinEventRepository.findByEventAndMember(event, event.getMember()) + .ifPresent(JoinEvent::softDelete); JoinEvent newJoinEvent = eventMapper.toJoinEvent(event, newRunningCaptain); joinEventRepository.save(newJoinEvent); @@ -125,7 +132,7 @@ private void handlePeriodicEventUpdate(Event event, EventInfoRequest request, RepeatCycle requestedRepeatCycle = request.repeatCycle(); if (requestedRepeatCycle == null || requestedRepeatCycle == RepeatCycle.NONE) { - existingPeriodicEvent.ifPresent(periodicEventRepository::delete); + existingPeriodicEvent.ifPresent(PeriodicEvent::softDelete); } else { if (existingPeriodicEvent.isPresent()) { PeriodicEvent periodicEvent = existingPeriodicEvent.get(); @@ -150,4 +157,53 @@ private void handlePeriodicEventUpdate(Event event, EventInfoRequest request, } } } + + @Logging + public EventDetailResponse getEventDetail(Long eventId) { + Event event = eventRepository.findById(eventId) + .orElseThrow(EventNotFound::new); + + List participants = getParticipants(event, event.getStatus()); + + List participantDtos = eventMapper.toParticipantDtoList(participants); + + return eventMapper.toEventDetailResponse(event, event.getStatus(), participantDtos); + } + + private List getParticipants(Event event, EventStatus status) { + return status == EventStatus.COMPLETED + ? joinEventRepository.findActualParticipantsByEvent(event) + : joinEventRepository.findByEventAndNotDeleted(event); + } + + @Transactional + @Logging + public void joinEvent(Long eventId, Member member) { + Event event = eventRepository.findById(eventId) + .orElseThrow(EventNotFound::new); + + if (joinEventRepository.existsByEventAndMemberAndDeletedAtIsNull(event, member)) { + throw new AlreadyJoinedEvent(); + } + + JoinEvent joinEvent = eventMapper.toJoinEvent(event, member); + joinEventRepository.save(joinEvent); + + event.incrementExpectedParticipants(); + } + + @Transactional + @Logging + public void cancelJoinEvent(Long eventId, Member member) { + Event event = eventRepository.findById(eventId) + .orElseThrow(EventNotFound::new); + + JoinEvent joinEvent = joinEventRepository.findByEventAndMember(event, member) + .orElseThrow(JoinEventNotFound::new); + + joinEvent.softDelete(); + joinEventRepository.save(joinEvent); + + event.decrementExpectedParticipants(); + } } diff --git a/src/main/java/run/backend/global/annotation/member/LoginArgumentResolver.java b/src/main/java/run/backend/global/annotation/member/LoginArgumentResolver.java index 29adfeb..756b7aa 100644 --- a/src/main/java/run/backend/global/annotation/member/LoginArgumentResolver.java +++ b/src/main/java/run/backend/global/annotation/member/LoginArgumentResolver.java @@ -2,8 +2,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; @@ -11,7 +9,6 @@ import org.springframework.web.method.support.ModelAndViewContainer; import run.backend.domain.member.entity.Member; import run.backend.domain.member.repository.MemberRepository; -import run.backend.global.security.CustomUserDetails; @Component @RequiredArgsConstructor diff --git a/src/main/java/run/backend/global/exception/GlobalExceptionHandler.java b/src/main/java/run/backend/global/exception/GlobalExceptionHandler.java index c83fba7..88bcf75 100644 --- a/src/main/java/run/backend/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/run/backend/global/exception/GlobalExceptionHandler.java @@ -25,7 +25,8 @@ public class GlobalExceptionHandler { MemberException.MemberNotJoinedCrew.class, MemberException.MemberNotFound.class, CrewException.NotFoundCrew.class, - EventException.EventNotFound.class + EventException.EventNotFound.class, + EventException.JoinEventNotFound.class }) public ResponseEntity> handleNotFound(final CustomException e) { @@ -42,7 +43,8 @@ public ResponseEntity> handleNotFound(final CustomException FileException.InvalidFileName.class, FileException.InvalidFileExtension.class, FileException.InvalidFileType.class, - CrewException.AlreadyJoinedCrew.class + CrewException.AlreadyJoinedCrew.class, + EventException.AlreadyJoinedEvent.class }) public ResponseEntity> handleConflict(final CustomException e) { 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 6b2ec09..97b6b2b 100644 --- a/src/test/java/run/backend/domain/event/service/EventServiceTest.java +++ b/src/test/java/run/backend/domain/event/service/EventServiceTest.java @@ -9,6 +9,7 @@ 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; @@ -31,8 +32,10 @@ import run.backend.domain.event.entity.PeriodicEvent; import run.backend.domain.event.enums.RepeatCycle; import run.backend.domain.event.enums.WeekDay; +import run.backend.domain.event.exception.EventException.AlreadyJoinedEvent; import run.backend.domain.event.exception.EventException.EventNotFound; import run.backend.domain.event.exception.EventException.InvalidEventCreationRequest; +import run.backend.domain.event.exception.EventException.JoinEventNotFound; import run.backend.domain.event.mapper.EventMapper; import run.backend.domain.event.repository.EventRepository; import run.backend.domain.event.repository.JoinEventRepository; @@ -68,6 +71,7 @@ class EventServiceTest { private Member runningCaptain; private Crew crew; private Event savedEvent; + private Event completedEvent; private JoinEvent savedJoinEvent; private PeriodicEvent savedPeriodicEvent; @@ -77,6 +81,8 @@ void setUp() { runningCaptain = createMemberWithId(2L, "러닝캡틴"); crew = createCrew("테스트크루"); savedEvent = createEvent(); + completedEvent = createEvent(); + completedEvent.complete(); savedJoinEvent = createJoinEvent(); savedPeriodicEvent = createPeriodicEvent(); } @@ -223,7 +229,7 @@ void shouldUpdateBasicInfoSuccessfully() { .willReturn(Optional.empty()); // when - sut.updateEvent(1L, request, requestMember); + sut.updateEvent(1L, request); // then then(eventRepository).should().findById(1L); @@ -250,7 +256,7 @@ void shouldActuallyUpdateEventFields() { .willReturn(Optional.empty()); // when - sut.updateEvent(1L, request, requestMember); + sut.updateEvent(1L, request); // then assertThat(savedEvent.getTitle()).isEqualTo("변경된 제목"); @@ -271,17 +277,21 @@ void shouldChangeRunningCaptainSuccessfully() { given(eventRepository.findById(1L)).willReturn(Optional.of(savedEvent)); given(joinCrewRepository.findCrewMemberById(3L, crew.getId(), JoinStatus.APPROVED)) .willReturn(Optional.of(newRunningCaptain)); + given(joinEventRepository.findByEventAndMember(savedEvent, runningCaptain)) + .willReturn(Optional.of(savedJoinEvent)); given(eventMapper.toJoinEvent(savedEvent, newRunningCaptain)).willReturn( savedJoinEvent); given(periodicEventRepository.findByCrewAndTitleAndTime(any(), any(), any(), any())) .willReturn(Optional.empty()); // when - sut.updateEvent(1L, request, requestMember); + sut.updateEvent(1L, request); // then - then(joinEventRepository).should().deleteByEventAndMember(savedEvent, runningCaptain); + then(joinEventRepository).should().findByEventAndMember(savedEvent, runningCaptain); then(joinEventRepository).should().save(any(JoinEvent.class)); + + assertThat(savedJoinEvent.getDeletedAt()).isNotNull(); } @Test @@ -295,15 +305,18 @@ void shouldActuallyChangeRunningCaptain() { given(eventRepository.findById(1L)).willReturn(Optional.of(savedEvent)); given(joinCrewRepository.findCrewMemberById(3L, crew.getId(), JoinStatus.APPROVED)) .willReturn(Optional.of(newCaptain)); + given(joinEventRepository.findByEventAndMember(savedEvent, runningCaptain)) + .willReturn(Optional.of(savedJoinEvent)); given(eventMapper.toJoinEvent(savedEvent, newCaptain)).willReturn(savedJoinEvent); given(periodicEventRepository.findByCrewAndTitleAndTime(any(), any(), any(), any())) .willReturn(Optional.empty()); // when - sut.updateEvent(1L, request, requestMember); + sut.updateEvent(1L, request); // then assertThat(savedEvent.getMember()).isEqualTo(newCaptain); + assertThat(savedJoinEvent.getDeletedAt()).isNotNull(); } @Test @@ -321,14 +334,14 @@ void shouldAddPeriodicEventSuccessfully() { savedPeriodicEvent); // when - sut.updateEvent(1L, request, requestMember); + sut.updateEvent(1L, request); // then then(periodicEventRepository).should().save(any(PeriodicEvent.class)); } @Test - @DisplayName("반복 설정을 제거할 때 기존 PeriodicEvent를 삭제한다") + @DisplayName("반복 설정을 제거할 때 기존 PeriodicEvent를 soft delete한다") void shouldRemovePeriodicEventSuccessfully() { // given EventInfoRequest request = createUpdateEventRequest(null, RepeatCycle.NONE, null, "반복 제거"); @@ -338,10 +351,10 @@ void shouldRemovePeriodicEventSuccessfully() { .willReturn(Optional.of(savedPeriodicEvent)); // when - sut.updateEvent(1L, request, requestMember); + sut.updateEvent(1L, request); // then - then(periodicEventRepository).should().delete(savedPeriodicEvent); + assertThat(savedPeriodicEvent.getDeletedAt()).isNotNull(); } @Test @@ -353,7 +366,7 @@ void shouldThrowEventNotFoundWhenEventDoesNotExist() { given(eventRepository.findById(1L)).willReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> sut.updateEvent(1L, request, requestMember)) + assertThatThrownBy(() -> sut.updateEvent(1L, request)) .isInstanceOf(EventNotFound.class); } @@ -368,7 +381,7 @@ void shouldThrowExceptionWhenNewRunningCaptainIsNotCrewMember() { .willReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> sut.updateEvent(1L, request, requestMember)) + assertThatThrownBy(() -> sut.updateEvent(1L, request)) .isInstanceOf(InvalidEventCreationRequest.class); } } @@ -466,4 +479,158 @@ private EventInfoRequest createUpdateEventRequest(Long runningCaptainId, RepeatC runningCaptainId ); } + + @Nested + @DisplayName("getEventDetail 메서드는") + class GetEventTest { + @Test + @DisplayName("일정 시작 전에는 예정된 모든 참가자를 조회한다") + void shouldReturnExpectedParticipantsBeforeEvent() { + //given + given(eventRepository.findById(1L)).willReturn(Optional.of(savedEvent)); + given(joinEventRepository.findByEventAndNotDeleted(any())).willReturn(List.of(savedJoinEvent)); + given(joinEventRepository.findActualParticipantsByEvent(any())).willReturn(List.of(savedJoinEvent)); + + //when + sut.getEventDetail(1L); + + //then + then(eventRepository).should().findById(1L); + then(joinEventRepository).should().findByEventAndNotDeleted(any()); + then(joinEventRepository).should(never()).findActualParticipantsByEvent(any()); + } + + @Test + @DisplayName("일정 완료 후에는 실제 참가한 참가자만 조회한다") + void shouldReturnActualParticipantsAfterEvent() { + //given + given(eventRepository.findById(1L)).willReturn(Optional.of(completedEvent)); + given(joinEventRepository.findByEventAndNotDeleted(any())).willReturn(List.of(savedJoinEvent)); + given(joinEventRepository.findActualParticipantsByEvent(any())).willReturn(List.of(savedJoinEvent)); + + //when + sut.getEventDetail(1L); + + //then + then(eventRepository).should().findById(1L); + then(joinEventRepository).should(never()).findByEventAndNotDeleted(any()); + then(joinEventRepository).should().findActualParticipantsByEvent(any()); + } + + @Test + @DisplayName("존재하지 않는 일정 조회 시 예외를 던진다") + void shouldThrowExceptionWhenEventNotFound() { + //given + given(eventRepository.findById(1L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> sut.getEventDetail(1L)) + .isInstanceOf(EventNotFound.class); + + //then + then(eventRepository).should().findById(1L); + then(joinEventRepository).should(never()).findByEventAndNotDeleted(any()); + then(joinEventRepository).should(never()).findActualParticipantsByEvent(any()); + } + } + + @Nested + @DisplayName("joinEvent 메서드는") + class JoinEventTest { + @Test + @DisplayName("일정 참여에 성공한다") + void shouldJoinEventSuccessfully() { + //given + Long initialExpectedParticipants = savedEvent.getExpectedParticipants(); + + given(eventRepository.findById(1L)).willReturn(Optional.of(savedEvent)); + given(joinEventRepository.save(any())).willReturn(savedJoinEvent); + given(joinEventRepository.existsByEventAndMemberAndDeletedAtIsNull(any(),any())).willReturn(false); + given(eventMapper.toJoinEvent(any(Event.class), any(Member.class))) + .willReturn(savedJoinEvent); + + //when + sut.joinEvent(1L, requestMember); + + //then + then(eventRepository).should().findById(1L); + then(joinEventRepository).should().save(any()); + assertThat(savedEvent.getExpectedParticipants()).isEqualTo(initialExpectedParticipants + 1); + } + + @Test + @DisplayName("이미 참여한 경우 예외를 던진다") + void shouldThrowExceptionWhenAlreadyJoined() { + //given + Long initialExpectedParticipants = savedEvent.getExpectedParticipants(); + + given(eventRepository.findById(1L)).willReturn(Optional.of(savedEvent)); + given(joinEventRepository.existsByEventAndMemberAndDeletedAtIsNull(any(),any())).willReturn(true); + + //when & then + assertThatThrownBy(() -> sut.joinEvent(1L, requestMember)) + .isInstanceOf(AlreadyJoinedEvent.class); + assertThat(savedEvent.getExpectedParticipants()).isEqualTo(initialExpectedParticipants); + + } + + @Test + @DisplayName("존재하지 않는 이벤트인 경우 예외를 던진다") + void shouldThrowExceptionWhenEventNotFound() { + given(eventRepository.findById(1L)).willReturn(Optional.empty()); + + //when & then + assertThatThrownBy(() -> sut.joinEvent(1L, requestMember)) + .isInstanceOf(EventNotFound.class); + } + } + + @Nested + @DisplayName("cancelEvent 메서드는") + class CancelEventTest { + @Test + @DisplayName("일정 참여 취소에 성공한다") + void shouldCancelEventSuccessfully() { + //given + 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 + sut.cancelJoinEvent(1L, requestMember); + + //then + then(eventRepository).should().findById(1L); + then(joinEventRepository).should().save(any()); + assertThat(savedEvent.getExpectedParticipants()).isEqualTo(initialExpectedParticipants - 1); + } + + @Test + @DisplayName("사용자가 참여 중인 일정이 아니면 예외를 던진다") + void shouldThrowExceptionWhenNotJoined() { + //given + Long initialExpectedParticipants = savedEvent.getExpectedParticipants(); + + given(eventRepository.findById(1L)).willReturn(Optional.of(savedEvent)); + given(joinEventRepository.existsByEventAndMemberAndDeletedAtIsNull(any(),any())).willReturn(true); + + //when & then + assertThatThrownBy(() -> sut.cancelJoinEvent(1L, requestMember)) + .isInstanceOf(JoinEventNotFound.class); + assertThat(savedEvent.getExpectedParticipants()).isEqualTo(initialExpectedParticipants); + + } + + @Test + @DisplayName("존재하지 않는 일정인 경우 예외를 던진다") + void shouldThrowExceptionWhenEventNotFound() { + given(eventRepository.findById(1L)).willReturn(Optional.empty()); + + //when & then + assertThatThrownBy(() -> sut.cancelJoinEvent(1L, requestMember)) + .isInstanceOf(EventNotFound.class); + } + } } \ No newline at end of file