diff --git a/src/main/java/com/fullcar/carpool/application/carpool/CarpoolService.java b/src/main/java/com/fullcar/carpool/application/carpool/CarpoolService.java index 5de3f94..2e0f01a 100644 --- a/src/main/java/com/fullcar/carpool/application/carpool/CarpoolService.java +++ b/src/main/java/com/fullcar/carpool/application/carpool/CarpoolService.java @@ -4,6 +4,9 @@ import com.fullcar.carpool.domain.carpool.CarpoolId; import com.fullcar.carpool.domain.carpool.CarpoolRepository; import com.fullcar.carpool.domain.carpool.Driver; +import com.fullcar.carpool.domain.form.Form; +import com.fullcar.carpool.domain.form.FormRepository; +import com.fullcar.carpool.domain.form.FormState; import com.fullcar.carpool.presentation.carpool.dto.request.CarpoolRequestDto; import com.fullcar.carpool.presentation.carpool.dto.response.CarpoolResponseDto; import com.fullcar.carpool.presentation.carpool.dto.response.MyCarpoolDto; @@ -13,6 +16,7 @@ import com.fullcar.member.domain.car.CarRepository; import com.fullcar.member.domain.member.Member; +import com.fullcar.member.domain.member.MemberRepository; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.*; @@ -29,6 +33,8 @@ public class CarpoolService { private final CarpoolRepository carpoolRepository; private final CarRepository carRepository; //:TODO Event 기반으로 변경 + private final MemberRepository memberRepository; //:TODO Event 기반으로 변경 + private final FormRepository formRepository; private final CarpoolMapper carpoolMapper; @Transactional @@ -79,4 +85,34 @@ public List getMyCarpoolList(Member member) { .map(carpool -> carpoolMapper.toMyCarpoolDto(carpool, member)) .toList(); } + + @Transactional + public CarpoolResponseDto.CarpoolDetailDtO closeCarpool(Member member, CarpoolId carpoolId) { + Carpool carpool = carpoolRepository.findByCarpoolIdAndIsDeletedOrThrow(carpoolId, false); + Car car = carRepository.findByCarIdAndIsDeletedOrThrow(member.getCarId(), false); + + if (!carpool.isMyCarpool(member.getId())) { + throw new CustomException(ErrorCode.CANNOT_CLOSE_CARPOOL); + } + List
forms = formRepository.findAllByCarpoolIdAndIsDeleted(carpoolId, false); + + carpool.close(); + + for (Form form: forms) { + if (form.getFormState() == FormState.REQUEST) { + form.reject( + memberRepository.findByIdAndIsDeletedOrThrow( + form.getPassenger().getMemberId(), + false + ) + ); + } + + } // TODO: N+1 문제 개선 필요. + + carpoolRepository.save(carpool); + formRepository.saveAllAndFlush(forms); + + return carpoolMapper.toDetailDto(carpool, member, car); + } } diff --git a/src/main/java/com/fullcar/carpool/application/form/FormService.java b/src/main/java/com/fullcar/carpool/application/form/FormService.java index 6b89f42..b3d7c2e 100644 --- a/src/main/java/com/fullcar/carpool/application/form/FormService.java +++ b/src/main/java/com/fullcar/carpool/application/form/FormService.java @@ -3,8 +3,11 @@ import com.fullcar.carpool.domain.carpool.Carpool; import com.fullcar.carpool.domain.carpool.CarpoolId; import com.fullcar.carpool.domain.carpool.CarpoolRepository; +import com.fullcar.carpool.domain.carpool.CarpoolState; import com.fullcar.carpool.domain.form.*; -import com.fullcar.carpool.infra.NotificationClient; +import com.fullcar.carpool.domain.form.event.FormStateChangedEvent; +import com.fullcar.carpool.domain.service.NotificationService; +import com.fullcar.carpool.infra.dto.NotificationDto; import com.fullcar.carpool.presentation.form.dto.request.FormRequestDto; import com.fullcar.carpool.presentation.form.dto.request.FormUpdateDto; import com.fullcar.carpool.presentation.form.dto.response.FormResponseDto; @@ -14,6 +17,7 @@ import com.fullcar.member.domain.member.MemberRepository; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; @@ -29,13 +33,18 @@ public class FormService { private final CarpoolRepository carpoolRepository; private final MemberRepository memberRepository; //TODO: Event 기반으로 변경 필요. private final FormMapper formMapper; - private final NotificationClient pushNotificationClient; + private final NotificationService notificationService; + private final ApplicationEventPublisher eventPublisher; @Transactional public FormResponseDto requestForm(Member member, CarpoolId carpoolId, FormRequestDto formRequestDto) { Carpool carpool = carpoolRepository.findByCarpoolIdAndIsDeletedOrThrow(carpoolId, false); Member driver = memberRepository.findByIdAndIsDeletedOrThrow(carpool.getDriver().getMemberId(), false); + if (carpool.getCarpoolState() == CarpoolState.CLOSE) { + throw new CustomException(ErrorCode.CANNOT_SEND_TO_CLOSED_CARPOOL); + } + if (carpool.isMyCarpool(member.getId())) { throw new CustomException(ErrorCode.CANNOT_SEND_TO_OWN_CARPOOL); } @@ -53,7 +62,17 @@ public FormResponseDto requestForm(Member member, CarpoolId carpoolId, FormReque } Form form = formMapper.toEntity(member, carpoolId, formRequestDto); - pushNotificationClient.sendNotification(driver.getNickname(), driver.getDeviceToken(), "탑승 요청이 들어왔어요!", "탑승자 정보를 확인하고 승인해 주세요🚘"); + + eventPublisher.publishEvent( + new FormStateChangedEvent( + NotificationDto.builder() + .nickName(driver.getNickname()) + .deviceToken(driver.getDeviceToken()) + .title(FormMessage.REQUEST_TITLE.getMessage()) + .body(FormMessage.REQUEST_BODY.getMessage()) + .build() + ) + ); return formMapper.toDto( formRepository.saveAndFlush(form), @@ -95,8 +114,6 @@ public List readReceivedFormList(Member member) { @Transactional public FormResponseDto.FormDetailDto updateForm(Member member, FormId formId, FormUpdateDto formUpdateDto) { Form form = formRepository.findByFormIdAndIsDeletedOrThrow(formId, false); - form.changeFormState(formUpdateDto); - Member passenger = memberRepository.findByIdAndIsDeletedOrThrow(form.getPassenger().getMemberId(), false); Carpool carpool = carpoolRepository.findByCarpoolIdAndIsDeletedOrThrow(form.getCarpoolId(), false); @@ -104,18 +121,7 @@ public FormResponseDto.FormDetailDto updateForm(Member member, FormId formId, Fo throw new CustomException(ErrorCode.CANNOT_CHANGE_FORM_STATE); } - System.out.println(passenger.getDeviceToken()); - - if (formUpdateDto.getFormState() == FormState.ACCEPT) { - String title = "카풀 매칭에 성공했어요!"; - String body = "운전자 정보를 확인해 주세요🚘"; - pushNotificationClient.sendNotification(passenger.getNickname(), passenger.getDeviceToken(), title, body); - } - else if (formUpdateDto.getFormState() == FormState.REJECT) { - String title = "카풀 매칭에 실패했어요."; - String body = "다른 카풀을 찾아볼까요?💁🏻‍♀️"; - pushNotificationClient.sendNotification(passenger.getNickname(), passenger.getDeviceToken(), title, body); - } + form.changeFormState(formUpdateDto, passenger); return formMapper.toDetailDto( formRepository.saveAndFlush(form), diff --git a/src/main/java/com/fullcar/carpool/domain/carpool/Carpool.java b/src/main/java/com/fullcar/carpool/domain/carpool/Carpool.java index 3e2bfae..faf8748 100644 --- a/src/main/java/com/fullcar/carpool/domain/carpool/Carpool.java +++ b/src/main/java/com/fullcar/carpool/domain/carpool/Carpool.java @@ -58,4 +58,8 @@ public class Carpool { public boolean isMyCarpool(MemberId memberId) { return this.getDriver().getMemberId().getId().equals(memberId.getId()); } + + public void close() { + this.carpoolState = CarpoolState.CLOSE; + } } diff --git a/src/main/java/com/fullcar/carpool/domain/form/Form.java b/src/main/java/com/fullcar/carpool/domain/form/Form.java index a7ce401..5e80331 100644 --- a/src/main/java/com/fullcar/carpool/domain/form/Form.java +++ b/src/main/java/com/fullcar/carpool/domain/form/Form.java @@ -1,13 +1,17 @@ package com.fullcar.carpool.domain.form; import com.fullcar.carpool.domain.carpool.CarpoolId; +import com.fullcar.carpool.domain.form.event.FormStateChangedEvent; +import com.fullcar.carpool.infra.dto.NotificationDto; import com.fullcar.carpool.presentation.form.dto.request.FormUpdateDto; import com.fullcar.core.exception.CustomException; import com.fullcar.core.response.ErrorCode; +import com.fullcar.member.domain.member.Member; import jakarta.persistence.*; import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.domain.AbstractAggregateRoot; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; @@ -19,8 +23,7 @@ @AllArgsConstructor(access = AccessLevel.PROTECTED) @EntityListeners(AuditingEntityListener.class) @Table(name = "form") -public class Form { - private static final String REJECT_MESSAGE = "카풀 매칭에 실패했어요. 다른 카풀을 찾아보세요!"; +public class Form extends AbstractAggregateRoot { @EmbeddedId private FormId formId; @@ -60,32 +63,50 @@ public class Form { @LastModifiedDate private LocalDateTime updatedAt; - public void changeFormState(FormUpdateDto formUpdateDto) { + public void changeFormState(FormUpdateDto formUpdateDto, Member passenger) { FormState formState = formUpdateDto.getFormState(); if (formState == FormState.ACCEPT) { - this.accept(formUpdateDto.getContact(), formUpdateDto.getToPassenger()); + this.accept(formUpdateDto.getContact(), formUpdateDto.getToPassenger(), passenger); } else if (formState == FormState.REJECT) { - this.reject(); + this.reject(passenger); } else { throw new CustomException(ErrorCode.INVALID_FORM_STATE); } } - public void accept(String contact, String toPassenger) { + public void accept(String contact, String toPassenger, Member passenger) { this.formState = FormState.ACCEPT; this.resultMessage = ResultMessage.builder() .contact(contact) .toPassenger(toPassenger) .build(); + + registerEvent(new FormStateChangedEvent( + NotificationDto.builder() + .nickName(passenger.getNickname()) + .deviceToken(passenger.getDeviceToken()) + .title(FormMessage.ACCEPT_TITLE.getMessage()) + .body(FormMessage.ACCEPT_BODY.getMessage()) + .build() + )); } - public void reject() { + public void reject(Member passenger) { this.formState = FormState.REJECT; this.resultMessage = ResultMessage.builder() - .toPassenger(REJECT_MESSAGE) + .toPassenger(FormMessage.REJECT_MESSAGE.getMessage()) .build(); + + registerEvent(new FormStateChangedEvent( + NotificationDto.builder() + .nickName(passenger.getNickname()) + .deviceToken(passenger.getDeviceToken()) + .title(FormMessage.REJECT_TITLE.getMessage()) + .body(FormMessage.REJECT_BODY.getMessage()) + .build() + )); } } diff --git a/src/main/java/com/fullcar/carpool/domain/form/FormMessage.java b/src/main/java/com/fullcar/carpool/domain/form/FormMessage.java new file mode 100644 index 0000000..8d434f4 --- /dev/null +++ b/src/main/java/com/fullcar/carpool/domain/form/FormMessage.java @@ -0,0 +1,18 @@ +package com.fullcar.carpool.domain.form; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum FormMessage { + REJECT_MESSAGE("카풀 매칭에 실패했어요. 다른 카풀을 찾아보세요!"), + REQUEST_TITLE("탑승 요청이 들어왔어요!"), + REQUEST_BODY("탑승자 정보를 확인하고 승인해 주세요🚘"), + ACCEPT_TITLE("카풀 매칭에 성공했어요!"), + ACCEPT_BODY("운전자 정보를 확인해 주세요🚘"), + REJECT_TITLE("카풀 매칭에 실패했어요."), + REJECT_BODY("다른 카풀을 찾아볼까요?💁🏻‍♀️"); + + private final String message; +} diff --git a/src/main/java/com/fullcar/carpool/domain/form/FormRepository.java b/src/main/java/com/fullcar/carpool/domain/form/FormRepository.java index 30ed7e9..961d808 100644 --- a/src/main/java/com/fullcar/carpool/domain/form/FormRepository.java +++ b/src/main/java/com/fullcar/carpool/domain/form/FormRepository.java @@ -23,6 +23,8 @@ List findReceivedForm( @Param("memberId") Long memberId ); + List findAllByCarpoolIdAndIsDeleted(CarpoolId carpoolId, boolean isDeleted); + default Form findByFormIdAndIsDeletedOrThrow(FormId formId, boolean isDeleted) { return findByFormIdAndIsDeleted(formId, isDeleted).orElseThrow( () -> new CustomException(ErrorCode.NOT_EXIST_FORM) diff --git a/src/main/java/com/fullcar/carpool/domain/form/event/FormStateChangedEvent.java b/src/main/java/com/fullcar/carpool/domain/form/event/FormStateChangedEvent.java new file mode 100644 index 0000000..c74212c --- /dev/null +++ b/src/main/java/com/fullcar/carpool/domain/form/event/FormStateChangedEvent.java @@ -0,0 +1,11 @@ +package com.fullcar.carpool.domain.form.event; + +import com.fullcar.carpool.infra.dto.NotificationDto; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class FormStateChangedEvent { + private NotificationDto notificationDto; +} diff --git a/src/main/java/com/fullcar/carpool/domain/service/NotificationService.java b/src/main/java/com/fullcar/carpool/domain/service/NotificationService.java index dbece40..a35014f 100644 --- a/src/main/java/com/fullcar/carpool/domain/service/NotificationService.java +++ b/src/main/java/com/fullcar/carpool/domain/service/NotificationService.java @@ -1,8 +1,9 @@ package com.fullcar.carpool.domain.service; +import com.fullcar.carpool.infra.dto.NotificationDto; import org.springframework.transaction.annotation.Transactional; @Transactional public interface NotificationService { - void sendNotification(String nickname, String deviceToken, String title, String body); + void sendNotification(NotificationDto notificationDto); } diff --git a/src/main/java/com/fullcar/carpool/infra/NotificationClient.java b/src/main/java/com/fullcar/carpool/infra/NotificationClient.java index 9ea77a3..ce071d9 100644 --- a/src/main/java/com/fullcar/carpool/infra/NotificationClient.java +++ b/src/main/java/com/fullcar/carpool/infra/NotificationClient.java @@ -1,6 +1,7 @@ package com.fullcar.carpool.infra; import com.fullcar.carpool.domain.service.NotificationService; +import com.fullcar.carpool.infra.dto.NotificationDto; import com.fullcar.core.exception.CustomException; import com.fullcar.core.response.ErrorCode; import com.google.firebase.messaging.FirebaseMessaging; @@ -17,14 +18,14 @@ public class NotificationClient implements NotificationService { private final FirebaseMessaging firebaseMessaging; @Override - public void sendNotification(String nickname, String deviceToken, String title, String body) { + public void sendNotification(NotificationDto notificationDto) { Notification notification = Notification.builder() - .setTitle(nickname + "님! " + title) - .setBody(body) + .setTitle(notificationDto.getNickName() + "님! " + notificationDto.getTitle()) + .setBody(notificationDto.getBody()) .build(); Message message = Message.builder() - .setToken(deviceToken) + .setToken(notificationDto.getDeviceToken()) .setNotification(notification) .build(); diff --git a/src/main/java/com/fullcar/carpool/infra/dto/NotificationDto.java b/src/main/java/com/fullcar/carpool/infra/dto/NotificationDto.java new file mode 100644 index 0000000..ca5853a --- /dev/null +++ b/src/main/java/com/fullcar/carpool/infra/dto/NotificationDto.java @@ -0,0 +1,17 @@ +package com.fullcar.carpool.infra.dto; + +import lombok.*; + +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class NotificationDto { + private String nickName; + + private String deviceToken; + + private String title; + + private String body; +} diff --git a/src/main/java/com/fullcar/carpool/infra/event/NotificationEvent.java b/src/main/java/com/fullcar/carpool/infra/event/NotificationEvent.java new file mode 100644 index 0000000..f474517 --- /dev/null +++ b/src/main/java/com/fullcar/carpool/infra/event/NotificationEvent.java @@ -0,0 +1,11 @@ +package com.fullcar.carpool.infra.event; + +import com.fullcar.carpool.infra.dto.NotificationDto; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class NotificationEvent { + private NotificationDto notificationDto; +} diff --git a/src/main/java/com/fullcar/carpool/infra/event/NotificationEventListener.java b/src/main/java/com/fullcar/carpool/infra/event/NotificationEventListener.java new file mode 100644 index 0000000..d5e2572 --- /dev/null +++ b/src/main/java/com/fullcar/carpool/infra/event/NotificationEventListener.java @@ -0,0 +1,26 @@ +package com.fullcar.carpool.infra.event; + +import com.fullcar.carpool.domain.form.event.FormStateChangedEvent; +import com.fullcar.carpool.domain.service.NotificationService; +import com.fullcar.carpool.infra.dto.NotificationDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +@EnableAsync +public class NotificationEventListener { + private final NotificationService notificationService; + + @Async + @EventListener + public void sendNotification(FormStateChangedEvent formStateChangedEvent) { + notificationService.sendNotification(formStateChangedEvent.getNotificationDto()); + log.info("Notification send"); + } +} diff --git a/src/main/java/com/fullcar/carpool/presentation/carpool/CarpoolController.java b/src/main/java/com/fullcar/carpool/presentation/carpool/CarpoolController.java index c702838..ca24a43 100644 --- a/src/main/java/com/fullcar/carpool/presentation/carpool/CarpoolController.java +++ b/src/main/java/com/fullcar/carpool/presentation/carpool/CarpoolController.java @@ -92,4 +92,20 @@ public ApiResponse> getMyCarpools( carpoolService.getMyCarpoolList(member) ); } + + @Operation(summary = "카풀 마감 API") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "수정 성공") + }) + @PatchMapping("/carpools/{carpoolId}") + public ApiResponse patchCarpool( + @Parameter(hidden = true) + @CurrentMember Member member, + @PathVariable Long carpoolId + ) { + return ApiResponse.success( + SuccessCode.UPDATE_SUCCESS, + carpoolService.closeCarpool(member, new CarpoolId(carpoolId)) + ); + } } diff --git a/src/main/java/com/fullcar/core/response/ErrorCode.java b/src/main/java/com/fullcar/core/response/ErrorCode.java index f10d5c4..5b1f0fc 100644 --- a/src/main/java/com/fullcar/core/response/ErrorCode.java +++ b/src/main/java/com/fullcar/core/response/ErrorCode.java @@ -15,10 +15,12 @@ public enum ErrorCode { FAILED_TO_GENERATE_PUBLIC_KEY(BAD_REQUEST, "애플 공개키 생성 중 문제 발생"), EMAIL_ADDRESS_IN_BLACKLIST(BAD_REQUEST, "블랙리스트에 있는 이메일 주소입니다."), CANNOT_SEND_TO_OWN_CARPOOL(BAD_REQUEST, "자기자신의 카풀에는 신청할 수 없습니다."), + CANNOT_SEND_TO_CLOSED_CARPOOL(BAD_REQUEST, "마감된 카풀에는 신청할 수 없습니다."), DUPLICATED_FORM(BAD_REQUEST, "이미 요청을 보낸 카풀입니다."), DUPLICATED_NICKNAME(BAD_REQUEST, "중복된 닉네임 입니다."), EXISTED_CAR_IN_MEMBER(BAD_REQUEST, "이미 차량이 등록되었습니다."), CANNOT_CHANGE_FORM_STATE(BAD_REQUEST, "카풀에 등록된 운전자만 신청서 수락/거절을 할 수 있습니다."), + CANNOT_CLOSE_CARPOOL(BAD_REQUEST, "삭제 권한이 없습니다."), INVALID_FORM_STATE(BAD_REQUEST, "유효하지 않은 신청서 상태입니다."), EXISTED_CODE_IN_MAIL(BAD_REQUEST, "이미 인증번호를 보냈습니다."), NOT_MATCHED_CODE(BAD_REQUEST, "인증번호가 일치하지 않습니다."),