From bdabceb191dd8e3afa0cf99096530d963501d2d4 Mon Sep 17 00:00:00 2001 From: west_east Date: Thu, 24 Jul 2025 20:33:40 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[#19]=20fix:=20soft=20delete=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/run/backend/domain/event/entity/Event.java | 2 +- .../run/backend/domain/event/entity/JoinEvent.java | 10 ++++++++-- .../backend/domain/event/entity/PeriodicEvent.java | 4 ++++ .../event/repository/JoinEventRepository.java | 6 ++++++ .../event/repository/PeriodicEventRepository.java | 1 + .../backend/domain/event/service/EventService.java | 5 +++-- .../domain/event/service/EventServiceTest.java | 13 ++++++++++--- 7 files changed, 33 insertions(+), 8 deletions(-) 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..d77924a 100644 --- a/src/main/java/run/backend/domain/event/entity/Event.java +++ b/src/main/java/run/backend/domain/event/entity/Event.java @@ -70,7 +70,7 @@ 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; 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/repository/JoinEventRepository.java b/src/main/java/run/backend/domain/event/repository/JoinEventRepository.java index 2977acf..750105b 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,18 @@ package run.backend.domain.event.repository; 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); } 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..10a7ad0 100644 --- a/src/main/java/run/backend/domain/event/service/EventService.java +++ b/src/main/java/run/backend/domain/event/service/EventService.java @@ -106,7 +106,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 +126,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(); 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..865eb4f 100644 --- a/src/test/java/run/backend/domain/event/service/EventServiceTest.java +++ b/src/test/java/run/backend/domain/event/service/EventServiceTest.java @@ -271,6 +271,8 @@ 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())) @@ -280,8 +282,10 @@ void shouldChangeRunningCaptainSuccessfully() { sut.updateEvent(1L, request, requestMember); // 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,6 +299,8 @@ 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()); @@ -304,6 +310,7 @@ void shouldActuallyChangeRunningCaptain() { // then assertThat(savedEvent.getMember()).isEqualTo(newCaptain); + assertThat(savedJoinEvent.getDeletedAt()).isNotNull(); } @Test @@ -328,7 +335,7 @@ void shouldAddPeriodicEventSuccessfully() { } @Test - @DisplayName("반복 설정을 제거할 때 기존 PeriodicEvent를 삭제한다") + @DisplayName("반복 설정을 제거할 때 기존 PeriodicEvent를 soft delete한다") void shouldRemovePeriodicEventSuccessfully() { // given EventInfoRequest request = createUpdateEventRequest(null, RepeatCycle.NONE, null, "반복 제거"); @@ -341,7 +348,7 @@ void shouldRemovePeriodicEventSuccessfully() { sut.updateEvent(1L, request, requestMember); // then - then(periodicEventRepository).should().delete(savedPeriodicEvent); + assertThat(savedPeriodicEvent.getDeletedAt()).isNotNull(); } @Test From 9ae3bfc84da9b74b8833e679433d9ddea2315a36 Mon Sep 17 00:00:00 2001 From: west_east Date: Fri, 25 Jul 2025 14:42:26 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[#19]=20feat:=20=EB=9F=AC=EB=8B=9D=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=EC=A1=B0=ED=9A=8C=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/controller/EventController.java | 14 +++++-- .../dto/response/EventDetailResponse.java | 22 +++++++++++ .../event/dto/response/ParticipantDto.java | 8 ++++ .../backend/domain/event/entity/Event.java | 38 +++++++++++++++++-- .../domain/event/enums/EventStatus.java | 6 +++ .../domain/event/mapper/EventMapper.java | 22 +++++++++++ .../event/repository/JoinEventRepository.java | 8 ++++ .../domain/event/service/EventService.java | 24 ++++++++++++ 8 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 src/main/java/run/backend/domain/event/dto/response/EventDetailResponse.java create mode 100644 src/main/java/run/backend/domain/event/dto/response/ParticipantDto.java create mode 100644 src/main/java/run/backend/domain/event/enums/EventStatus.java 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..08f3455 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,7 @@ 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.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -11,6 +11,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 +26,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,7 +38,7 @@ 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, @@ -48,4 +49,11 @@ public CommonResponse updateEvent( eventService.updateEvent(eventId, eventUpdateRequest, member); return new CommonResponse<>("러닝 일정 수정 성공"); } + + @GetMapping("/{eventId}") + @Operation(summary = "일정 상세 조회", description = "러닝 일정을 조회합니다.") + public CommonResponse getEventDetail(@PathVariable Long eventId) { + EventDetailResponse response = eventService.getEventDetail(eventId); + return new CommonResponse<>("러닝 일정 상세 조회 성공", response); + } } 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 d77924a..1926c5d 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, @@ -75,6 +86,7 @@ public Event( this.crew = crew; this.record = record; this.member = member; + this.status = EventStatus.BEFORE; } public void incrementExpectedParticipants() { @@ -112,4 +124,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/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/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 750105b..a4e025d 100644 --- a/src/main/java/run/backend/domain/event/repository/JoinEventRepository.java +++ b/src/main/java/run/backend/domain/event/repository/JoinEventRepository.java @@ -1,5 +1,6 @@ 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; @@ -15,4 +16,11 @@ public interface JoinEventRepository extends JpaRepository { @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); + } 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 10a7ad0..eacda8e 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,9 +10,12 @@ 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.EventNotFound; import run.backend.domain.event.exception.EventException.InvalidEventCreationRequest; @@ -59,6 +63,8 @@ public void updateEvent(Long eventId, EventInfoRequest eventUpdateRequest, Membe Event event = eventRepository.findById(eventId) .orElseThrow(EventNotFound::new); + // member 매개변수를 검증 로직에서 사용할 수 있도록 추가 + // 현재는 러닝 캡틴 검증에만 사용하므로 별도 권한 검증은 생략 Member newRunningCaptain = validateNewRunningCaptain(eventUpdateRequest, event); if (newRunningCaptain != null && !event.getMember().getId() @@ -151,4 +157,22 @@ 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); + } } From 0d76a103313a9f9295024eff42659b0899970a0f Mon Sep 17 00:00:00 2001 From: west_east Date: Fri, 25 Jul 2025 15:38:09 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[#19]=20feat:=20=EB=9F=AC=EB=8B=9D=20?= =?UTF-8?q?=EC=B0=B8=EC=97=AC=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/controller/EventController.java | 12 ++++++++--- .../event/exception/EventErrorCode.java | 3 ++- .../event/exception/EventException.java | 6 ++++++ .../event/repository/JoinEventRepository.java | 1 + .../domain/event/service/EventService.java | 21 ++++++++++++++++--- .../member/LoginArgumentResolver.java | 3 --- .../exception/GlobalExceptionHandler.java | 3 ++- .../event/service/EventServiceTest.java | 16 +++++++------- 8 files changed, 46 insertions(+), 19 deletions(-) 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 08f3455..cf8fdc5 100644 --- a/src/main/java/run/backend/domain/event/controller/EventController.java +++ b/src/main/java/run/backend/domain/event/controller/EventController.java @@ -42,11 +42,10 @@ public CommonResponse createEvent( @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<>("러닝 일정 수정 성공"); } @@ -56,4 +55,11 @@ public CommonResponse getEventDetail(@PathVariable Long eve 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<>("러닝 참여 요청 완료"); + } } 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..ed4dc8c 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,8 @@ public enum EventErrorCode implements ErrorCode { RUNNING_CAPTAIN_NOT_CREW_MEMBER(6001, "러닝캡이 크루원이 아닙니다."), - EVENT_NOT_FOUND(6002, "일정을 찾을 수 없습니다."); + EVENT_NOT_FOUND(6002, "일정을 찾을 수 없습니다."), + ALREADY_JOINED_EVENT(6003, "이미 참여 요청이 되어있습니다."); 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..f5c2624 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,10 @@ public EventNotFound() { super(EventErrorCode.EVENT_NOT_FOUND); } } + + public static class AlreadyJoinedEvent extends EventException { + public AlreadyJoinedEvent() { + super(EventErrorCode.ALREADY_JOINED_EVENT); + } + } } 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 a4e025d..9847298 100644 --- a/src/main/java/run/backend/domain/event/repository/JoinEventRepository.java +++ b/src/main/java/run/backend/domain/event/repository/JoinEventRepository.java @@ -23,4 +23,5 @@ public interface JoinEventRepository extends JpaRepository { @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/service/EventService.java b/src/main/java/run/backend/domain/event/service/EventService.java index eacda8e..a034597 100644 --- a/src/main/java/run/backend/domain/event/service/EventService.java +++ b/src/main/java/run/backend/domain/event/service/EventService.java @@ -17,6 +17,7 @@ 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.mapper.EventMapper; @@ -59,12 +60,10 @@ 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); - // member 매개변수를 검증 로직에서 사용할 수 있도록 추가 - // 현재는 러닝 캡틴 검증에만 사용하므로 별도 권한 검증은 생략 Member newRunningCaptain = validateNewRunningCaptain(eventUpdateRequest, event); if (newRunningCaptain != null && !event.getMember().getId() @@ -175,4 +174,20 @@ private List getParticipants(Event event, EventStatus status) { ? 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(); + } } 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..88bc273 100644 --- a/src/main/java/run/backend/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/run/backend/global/exception/GlobalExceptionHandler.java @@ -42,7 +42,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 865eb4f..40469fa 100644 --- a/src/test/java/run/backend/domain/event/service/EventServiceTest.java +++ b/src/test/java/run/backend/domain/event/service/EventServiceTest.java @@ -223,7 +223,7 @@ void shouldUpdateBasicInfoSuccessfully() { .willReturn(Optional.empty()); // when - sut.updateEvent(1L, request, requestMember); + sut.updateEvent(1L, request); // then then(eventRepository).should().findById(1L); @@ -250,7 +250,7 @@ void shouldActuallyUpdateEventFields() { .willReturn(Optional.empty()); // when - sut.updateEvent(1L, request, requestMember); + sut.updateEvent(1L, request); // then assertThat(savedEvent.getTitle()).isEqualTo("변경된 제목"); @@ -279,7 +279,7 @@ void shouldChangeRunningCaptainSuccessfully() { .willReturn(Optional.empty()); // when - sut.updateEvent(1L, request, requestMember); + sut.updateEvent(1L, request); // then then(joinEventRepository).should().findByEventAndMember(savedEvent, runningCaptain); @@ -306,7 +306,7 @@ void shouldActuallyChangeRunningCaptain() { .willReturn(Optional.empty()); // when - sut.updateEvent(1L, request, requestMember); + sut.updateEvent(1L, request); // then assertThat(savedEvent.getMember()).isEqualTo(newCaptain); @@ -328,7 +328,7 @@ void shouldAddPeriodicEventSuccessfully() { savedPeriodicEvent); // when - sut.updateEvent(1L, request, requestMember); + sut.updateEvent(1L, request); // then then(periodicEventRepository).should().save(any(PeriodicEvent.class)); @@ -345,7 +345,7 @@ void shouldRemovePeriodicEventSuccessfully() { .willReturn(Optional.of(savedPeriodicEvent)); // when - sut.updateEvent(1L, request, requestMember); + sut.updateEvent(1L, request); // then assertThat(savedPeriodicEvent.getDeletedAt()).isNotNull(); @@ -360,7 +360,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); } @@ -375,7 +375,7 @@ void shouldThrowExceptionWhenNewRunningCaptainIsNotCrewMember() { .willReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> sut.updateEvent(1L, request, requestMember)) + assertThatThrownBy(() -> sut.updateEvent(1L, request)) .isInstanceOf(InvalidEventCreationRequest.class); } } From ffa0f17d4bf994c06c11e78e0c417c48d3691481 Mon Sep 17 00:00:00 2001 From: west_east Date: Fri, 25 Jul 2025 15:58:01 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[#19]=20feat:=20=EB=9F=AC=EB=8B=9D=20?= =?UTF-8?q?=EC=B0=B8=EC=97=AC=20=EC=B7=A8=EC=86=8C=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/event/controller/EventController.java | 10 +++++++++- .../run/backend/domain/event/entity/Event.java | 12 ++++++++++++ .../domain/event/exception/EventErrorCode.java | 3 ++- .../domain/event/exception/EventException.java | 7 +++++++ .../domain/event/service/EventService.java | 16 ++++++++++++++++ .../global/exception/GlobalExceptionHandler.java | 3 ++- 6 files changed, 48 insertions(+), 3 deletions(-) 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 cf8fdc5..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,6 +3,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +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; @@ -57,9 +58,16 @@ public CommonResponse getEventDetail(@PathVariable Long eve } @PostMapping("/{eventId}/join-requests") - @Operation(summary = "러닝 참여", description = "러닝 일정에 참여 요청합니다") + @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/entity/Event.java b/src/main/java/run/backend/domain/event/entity/Event.java index 1926c5d..dc5f75b 100644 --- a/src/main/java/run/backend/domain/event/entity/Event.java +++ b/src/main/java/run/backend/domain/event/entity/Event.java @@ -93,10 +93,22 @@ 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 updateEvent( String title, LocalDate date, 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 ed4dc8c..e3e5429 100644 --- a/src/main/java/run/backend/domain/event/exception/EventErrorCode.java +++ b/src/main/java/run/backend/domain/event/exception/EventErrorCode.java @@ -10,7 +10,8 @@ public enum EventErrorCode implements ErrorCode { RUNNING_CAPTAIN_NOT_CREW_MEMBER(6001, "러닝캡이 크루원이 아닙니다."), EVENT_NOT_FOUND(6002, "일정을 찾을 수 없습니다."), - ALREADY_JOINED_EVENT(6003, "이미 참여 요청이 되어있습니다."); + 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 f5c2624..6ff97a3 100644 --- a/src/main/java/run/backend/domain/event/exception/EventException.java +++ b/src/main/java/run/backend/domain/event/exception/EventException.java @@ -25,4 +25,11 @@ 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/service/EventService.java b/src/main/java/run/backend/domain/event/service/EventService.java index a034597..50fce55 100644 --- a/src/main/java/run/backend/domain/event/service/EventService.java +++ b/src/main/java/run/backend/domain/event/service/EventService.java @@ -20,6 +20,7 @@ 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; @@ -190,4 +191,19 @@ public void joinEvent(Long eventId, Member member) { 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/exception/GlobalExceptionHandler.java b/src/main/java/run/backend/global/exception/GlobalExceptionHandler.java index 88bc273..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) { From d0e7640c3c06f48c1c70c885c143d83ca4a0c70a Mon Sep 17 00:00:00 2001 From: west_east Date: Fri, 25 Jul 2025 16:35:30 +0900 Subject: [PATCH 5/6] =?UTF-8?q?[#19]=20test:=20=EB=9F=AC=EB=8B=9D=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/event/entity/Event.java | 4 ++ .../event/service/EventServiceTest.java | 58 +++++++++++++++++++ 2 files changed, 62 insertions(+) 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 dc5f75b..88e37f3 100644 --- a/src/main/java/run/backend/domain/event/entity/Event.java +++ b/src/main/java/run/backend/domain/event/entity/Event.java @@ -109,6 +109,10 @@ public void decrementActualParticipants() { } } + public void complete() { + this.status = EventStatus.COMPLETED; + } + public void updateEvent( String title, LocalDate date, 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 40469fa..858952d 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; @@ -68,6 +69,7 @@ class EventServiceTest { private Member runningCaptain; private Crew crew; private Event savedEvent; + private Event completedEvent; private JoinEvent savedJoinEvent; private PeriodicEvent savedPeriodicEvent; @@ -77,6 +79,8 @@ void setUp() { runningCaptain = createMemberWithId(2L, "러닝캡틴"); crew = createCrew("테스트크루"); savedEvent = createEvent(); + completedEvent = createEvent(); + completedEvent.complete(); savedJoinEvent = createJoinEvent(); savedPeriodicEvent = createPeriodicEvent(); } @@ -473,4 +477,58 @@ 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()); + } + } } \ No newline at end of file From 84aa90fe2d310bf3a749e19afcd79879cccd26ab Mon Sep 17 00:00:00 2001 From: west_east Date: Thu, 31 Jul 2025 14:53:40 +0900 Subject: [PATCH 6/6] =?UTF-8?q?[#19]=20test:=20=EB=9F=AC=EB=8B=9D=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=EC=B0=B8=EC=97=AC=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/service/EventServiceTest.java | 108 +++++++++++++++++- 1 file changed, 105 insertions(+), 3 deletions(-) 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 858952d..97b6b2b 100644 --- a/src/test/java/run/backend/domain/event/service/EventServiceTest.java +++ b/src/test/java/run/backend/domain/event/service/EventServiceTest.java @@ -32,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; @@ -482,7 +484,7 @@ private EventInfoRequest createUpdateEventRequest(Long runningCaptainId, RepeatC @DisplayName("getEventDetail 메서드는") class GetEventTest { @Test - @DisplayName("이벤트 시작 전에는 예정된 모든 참가자를 조회한다") + @DisplayName("일정 시작 전에는 예정된 모든 참가자를 조회한다") void shouldReturnExpectedParticipantsBeforeEvent() { //given given(eventRepository.findById(1L)).willReturn(Optional.of(savedEvent)); @@ -499,7 +501,7 @@ void shouldReturnExpectedParticipantsBeforeEvent() { } @Test - @DisplayName("이벤트 완료 후에는 실제 참가한 참가자만 조회한다") + @DisplayName("일정 완료 후에는 실제 참가한 참가자만 조회한다") void shouldReturnActualParticipantsAfterEvent() { //given given(eventRepository.findById(1L)).willReturn(Optional.of(completedEvent)); @@ -516,7 +518,7 @@ void shouldReturnActualParticipantsAfterEvent() { } @Test - @DisplayName("존재하지 않는 이벤트 조회 시 예외를 던진다") + @DisplayName("존재하지 않는 일정 조회 시 예외를 던진다") void shouldThrowExceptionWhenEventNotFound() { //given given(eventRepository.findById(1L)).willReturn(Optional.empty()); @@ -531,4 +533,104 @@ void shouldThrowExceptionWhenEventNotFound() { 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