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 2e0f01a..63c6946 100644 --- a/src/main/java/com/fullcar/carpool/application/carpool/CarpoolService.java +++ b/src/main/java/com/fullcar/carpool/application/carpool/CarpoolService.java @@ -96,23 +96,26 @@ public CarpoolResponseDto.CarpoolDetailDtO closeCarpool(Member member, CarpoolId } List
forms = formRepository.findAllByCarpoolIdAndIsDeleted(carpoolId, false); - carpool.close(); + carpool.close(forms); - for (Form form: forms) { - if (form.getFormState() == FormState.REQUEST) { - form.reject( - memberRepository.findByIdAndIsDeletedOrThrow( - form.getPassenger().getMemberId(), - false - ) - ); - } + carpoolRepository.saveAndFlush(carpool); - } // TODO: N+1 문제 개선 필요. + return carpoolMapper.toDetailDto(carpool, member, car); + } - carpoolRepository.save(carpool); - formRepository.saveAllAndFlush(forms); + @Transactional + public CarpoolResponseDto deleteCarpool(Member member, CarpoolId carpoolId) { + Carpool carpool = carpoolRepository.findByCarpoolIdAndIsDeletedOrThrow(carpoolId, false); - return carpoolMapper.toDetailDto(carpool, member, car); + if (!carpool.isMyCarpool(member.getId())) { + throw new CustomException(ErrorCode.CANNOT_DELETE_CARPOOL); + } + List forms = formRepository.findAllByCarpoolIdAndIsDeleted(carpoolId, false); + + carpool.delete(forms); + + carpoolRepository.saveAndFlush(carpool); + + return carpoolMapper.toDto(carpool, member); } } diff --git a/src/main/java/com/fullcar/carpool/application/form/CarpoolClosedEventHandler.java b/src/main/java/com/fullcar/carpool/application/form/CarpoolClosedEventHandler.java new file mode 100644 index 0000000..a5b6d9f --- /dev/null +++ b/src/main/java/com/fullcar/carpool/application/form/CarpoolClosedEventHandler.java @@ -0,0 +1,32 @@ +package com.fullcar.carpool.application.form; + + +import com.fullcar.carpool.domain.carpool.event.CarpoolClosedEvent; +import com.fullcar.carpool.domain.form.Form; +import com.fullcar.carpool.domain.form.FormRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CarpoolClosedEventHandler { + private final FormRepository formRepository; + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void rejectForms(CarpoolClosedEvent carpoolClosedEvent) { + log.info("CarpoolClosedEvent received"); + List forms = carpoolClosedEvent.getForms(); + + forms.forEach(Form::reject); + formRepository.saveAllAndFlush(forms); + } +} diff --git a/src/main/java/com/fullcar/carpool/application/form/CarpoolDeletedEventHandler.java b/src/main/java/com/fullcar/carpool/application/form/CarpoolDeletedEventHandler.java new file mode 100644 index 0000000..f491030 --- /dev/null +++ b/src/main/java/com/fullcar/carpool/application/form/CarpoolDeletedEventHandler.java @@ -0,0 +1,37 @@ +package com.fullcar.carpool.application.form; + +import com.fullcar.carpool.domain.carpool.event.CarpoolDeletedEvent; +import com.fullcar.carpool.domain.form.Form; +import com.fullcar.carpool.domain.form.FormRepository; +import com.fullcar.carpool.domain.form.FormState; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CarpoolDeletedEventHandler { + private final FormRepository formRepository; + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void rejectForms(CarpoolDeletedEvent carpoolDeletedEvent) { + log.info("CarpoolDeletedEvent received"); + List forms = carpoolDeletedEvent.getForms(); + + for (Form form: forms) { + if (form.getFormState() == FormState.REQUEST || form.getFormState() == FormState.ACCEPT) { + form.reject(); + } + form.disconnectCarpool(); + } + formRepository.saveAllAndFlush(forms); + } +} diff --git a/src/main/java/com/fullcar/carpool/application/form/FormMapper.java b/src/main/java/com/fullcar/carpool/application/form/FormMapper.java index c9d28c4..7e4cd6f 100644 --- a/src/main/java/com/fullcar/carpool/application/form/FormMapper.java +++ b/src/main/java/com/fullcar/carpool/application/form/FormMapper.java @@ -31,6 +31,15 @@ public FormResponseDto toDto(Form form, Member member) { } public FormResponseDto.FormDetailDto toDetailDto(Form form, Member member) { + Long carpoolId; + + if (form.getCarpoolId() != null) { + carpoolId = form.getCarpoolId().getId(); + } + else { + carpoolId = null; + } + return FormResponseDto.FormDetailDto.builder() .id(form.getFormId().getId()) .pickupLocation(form.getPickupLocation()) @@ -42,7 +51,7 @@ public FormResponseDto.FormDetailDto toDetailDto(Form form, Member member) { .createdAt(form.getCreatedAt()) .content(form.getContent()) .resultMessage(form.getResultMessage()) - .carpoolId(form.getCarpoolId().getId()) + .carpoolId(carpoolId) .build(); } 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 b3d7c2e..f0ff606 100644 --- a/src/main/java/com/fullcar/carpool/application/form/FormService.java +++ b/src/main/java/com/fullcar/carpool/application/form/FormService.java @@ -64,14 +64,11 @@ public FormResponseDto requestForm(Member member, CarpoolId carpoolId, FormReque Form form = formMapper.toEntity(member, carpoolId, formRequestDto); eventPublisher.publishEvent( - new FormStateChangedEvent( - NotificationDto.builder() - .nickName(driver.getNickname()) - .deviceToken(driver.getDeviceToken()) - .title(FormMessage.REQUEST_TITLE.getMessage()) - .body(FormMessage.REQUEST_BODY.getMessage()) - .build() - ) + FormStateChangedEvent.builder() + .memberId(driver.getId()) + .title(FormMessage.REQUEST_TITLE.getMessage()) + .body(FormMessage.REQUEST_BODY.getMessage()) + .build() ); return formMapper.toDto( @@ -114,14 +111,13 @@ public List readReceivedFormList(Member member) { @Transactional public FormResponseDto.FormDetailDto updateForm(Member member, FormId formId, FormUpdateDto formUpdateDto) { Form form = formRepository.findByFormIdAndIsDeletedOrThrow(formId, false); - Member passenger = memberRepository.findByIdAndIsDeletedOrThrow(form.getPassenger().getMemberId(), false); Carpool carpool = carpoolRepository.findByCarpoolIdAndIsDeletedOrThrow(form.getCarpoolId(), false); if (!carpool.isMyCarpool(member.getId())) { throw new CustomException(ErrorCode.CANNOT_CHANGE_FORM_STATE); } - form.changeFormState(formUpdateDto, passenger); + form.changeFormState(formUpdateDto); return formMapper.toDetailDto( formRepository.saveAndFlush(form), diff --git a/src/main/java/com/fullcar/carpool/application/form/FormStateChangedEventHandler.java b/src/main/java/com/fullcar/carpool/application/form/FormStateChangedEventHandler.java new file mode 100644 index 0000000..5e5f939 --- /dev/null +++ b/src/main/java/com/fullcar/carpool/application/form/FormStateChangedEventHandler.java @@ -0,0 +1,37 @@ +package com.fullcar.carpool.application.form; + +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.member.domain.member.MemberRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Slf4j +@Service +@RequiredArgsConstructor +public class FormStateChangedEventHandler { + private final NotificationService notificationService; + private final MemberRepository memberRepository; + + @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void sendNotification(FormStateChangedEvent formStateChangedEvent) { + log.info("FormStateChangedEvent received"); + notificationService.sendNotification( + NotificationDto.builder() + .member( + memberRepository.findByIdAndIsDeletedOrThrow(formStateChangedEvent.getMemberId(), false) + ) + .title(formStateChangedEvent.getTitle()) + .body(formStateChangedEvent.getBody()) + .build() + ); + log.info("Notification send"); + } +} 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 faf8748..fff3c41 100644 --- a/src/main/java/com/fullcar/carpool/domain/carpool/Carpool.java +++ b/src/main/java/com/fullcar/carpool/domain/carpool/Carpool.java @@ -1,13 +1,21 @@ package com.fullcar.carpool.domain.carpool; +import com.fullcar.carpool.domain.carpool.event.CarpoolClosedEvent; +import com.fullcar.carpool.domain.carpool.event.CarpoolDeletedEvent; +import com.fullcar.carpool.domain.form.Form; +import com.fullcar.carpool.domain.form.FormState; +import com.fullcar.core.exception.CustomException; +import com.fullcar.core.response.ErrorCode; import com.fullcar.member.domain.member.MemberId; 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; +import java.util.List; import static lombok.AccessLevel.PROTECTED; @@ -18,7 +26,7 @@ @AllArgsConstructor(access = PROTECTED) @EntityListeners(AuditingEntityListener.class) @Table(name = "carpool") -public class Carpool { +public class Carpool extends AbstractAggregateRoot { @EmbeddedId private CarpoolId carpoolId; @@ -59,7 +67,28 @@ public boolean isMyCarpool(MemberId memberId) { return this.getDriver().getMemberId().getId().equals(memberId.getId()); } - public void close() { + public void close(List forms) { + if (this.carpoolState == CarpoolState.CLOSE) { + throw new CustomException(ErrorCode.ALREADY_CLOSED); + } this.carpoolState = CarpoolState.CLOSE; + registerEvent( + new CarpoolClosedEvent( + forms.stream() + .filter(form -> form.getFormState() == FormState.REQUEST) + .toList() + ) + ); + } + + public void delete(List forms) { + if (this.carpoolState == CarpoolState.OPEN) { + throw new CustomException(ErrorCode.CANNOT_DELETE_OPEN_CARPOOL); + } + + this.isDeleted = true; + registerEvent( + new CarpoolDeletedEvent(forms) + ); } } diff --git a/src/main/java/com/fullcar/carpool/domain/carpool/event/CarpoolClosedEvent.java b/src/main/java/com/fullcar/carpool/domain/carpool/event/CarpoolClosedEvent.java new file mode 100644 index 0000000..00003e7 --- /dev/null +++ b/src/main/java/com/fullcar/carpool/domain/carpool/event/CarpoolClosedEvent.java @@ -0,0 +1,13 @@ +package com.fullcar.carpool.domain.carpool.event; + +import com.fullcar.carpool.domain.form.Form; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class CarpoolClosedEvent { + private List forms; +} diff --git a/src/main/java/com/fullcar/carpool/domain/carpool/event/CarpoolDeletedEvent.java b/src/main/java/com/fullcar/carpool/domain/carpool/event/CarpoolDeletedEvent.java new file mode 100644 index 0000000..3882c3d --- /dev/null +++ b/src/main/java/com/fullcar/carpool/domain/carpool/event/CarpoolDeletedEvent.java @@ -0,0 +1,13 @@ +package com.fullcar.carpool.domain.carpool.event; + +import com.fullcar.carpool.domain.form.Form; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class CarpoolDeletedEvent { + private List forms; +} 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 5e80331..5e69c7c 100644 --- a/src/main/java/com/fullcar/carpool/domain/form/Form.java +++ b/src/main/java/com/fullcar/carpool/domain/form/Form.java @@ -63,50 +63,52 @@ public class Form extends AbstractAggregateRoot { @LastModifiedDate private LocalDateTime updatedAt; - public void changeFormState(FormUpdateDto formUpdateDto, Member passenger) { + public void changeFormState(FormUpdateDto formUpdateDto) { FormState formState = formUpdateDto.getFormState(); if (formState == FormState.ACCEPT) { - this.accept(formUpdateDto.getContact(), formUpdateDto.getToPassenger(), passenger); + this.accept(formUpdateDto.getContact(), formUpdateDto.getToPassenger()); } else if (formState == FormState.REJECT) { - this.reject(passenger); + this.reject(); } else { throw new CustomException(ErrorCode.INVALID_FORM_STATE); } } - public void accept(String contact, String toPassenger, Member passenger) { + public void accept(String contact, String toPassenger) { this.formState = FormState.ACCEPT; this.resultMessage = ResultMessage.builder() .contact(contact) .toPassenger(toPassenger) .build(); - registerEvent(new FormStateChangedEvent( - NotificationDto.builder() - .nickName(passenger.getNickname()) - .deviceToken(passenger.getDeviceToken()) + registerEvent( + FormStateChangedEvent.builder() + .memberId(this.passenger.getMemberId()) .title(FormMessage.ACCEPT_TITLE.getMessage()) .body(FormMessage.ACCEPT_BODY.getMessage()) .build() - )); + ); } - public void reject(Member passenger) { + public void reject() { this.formState = FormState.REJECT; this.resultMessage = ResultMessage.builder() .toPassenger(FormMessage.REJECT_MESSAGE.getMessage()) .build(); - registerEvent(new FormStateChangedEvent( - NotificationDto.builder() - .nickName(passenger.getNickname()) - .deviceToken(passenger.getDeviceToken()) + registerEvent( + FormStateChangedEvent.builder() + .memberId(this.passenger.getMemberId()) .title(FormMessage.REJECT_TITLE.getMessage()) .body(FormMessage.REJECT_BODY.getMessage()) .build() - )); + ); + } + + public void disconnectCarpool() { + this.carpoolId = null; } } 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 index c74212c..7f93fdb 100644 --- a/src/main/java/com/fullcar/carpool/domain/form/event/FormStateChangedEvent.java +++ b/src/main/java/com/fullcar/carpool/domain/form/event/FormStateChangedEvent.java @@ -1,11 +1,21 @@ package com.fullcar.carpool.domain.form.event; import com.fullcar.carpool.infra.dto.NotificationDto; +import com.fullcar.member.domain.member.Member; +import com.fullcar.member.domain.member.MemberId; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter +@Builder +@NoArgsConstructor @AllArgsConstructor public class FormStateChangedEvent { - private NotificationDto notificationDto; + private MemberId memberId; + + private String title; + + private String body; } diff --git a/src/main/java/com/fullcar/carpool/infra/NotificationClient.java b/src/main/java/com/fullcar/carpool/infra/NotificationClient.java index ce071d9..c981c07 100644 --- a/src/main/java/com/fullcar/carpool/infra/NotificationClient.java +++ b/src/main/java/com/fullcar/carpool/infra/NotificationClient.java @@ -20,12 +20,12 @@ public class NotificationClient implements NotificationService { @Override public void sendNotification(NotificationDto notificationDto) { Notification notification = Notification.builder() - .setTitle(notificationDto.getNickName() + "님! " + notificationDto.getTitle()) + .setTitle(notificationDto.getMember().getNickname() + "님! " + notificationDto.getTitle()) .setBody(notificationDto.getBody()) .build(); Message message = Message.builder() - .setToken(notificationDto.getDeviceToken()) + .setToken(notificationDto.getMember().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 index ca5853a..4c23949 100644 --- a/src/main/java/com/fullcar/carpool/infra/dto/NotificationDto.java +++ b/src/main/java/com/fullcar/carpool/infra/dto/NotificationDto.java @@ -1,5 +1,6 @@ package com.fullcar.carpool.infra.dto; +import com.fullcar.member.domain.member.Member; import lombok.*; @Getter @@ -7,9 +8,7 @@ @AllArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED) public class NotificationDto { - private String nickName; - - private String deviceToken; + private Member member; private String title; diff --git a/src/main/java/com/fullcar/carpool/infra/event/NotificationEvent.java b/src/main/java/com/fullcar/carpool/infra/event/NotificationEvent.java deleted file mode 100644 index f474517..0000000 --- a/src/main/java/com/fullcar/carpool/infra/event/NotificationEvent.java +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index d5e2572..0000000 --- a/src/main/java/com/fullcar/carpool/infra/event/NotificationEventListener.java +++ /dev/null @@ -1,26 +0,0 @@ -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 ca24a43..847f453 100644 --- a/src/main/java/com/fullcar/carpool/presentation/carpool/CarpoolController.java +++ b/src/main/java/com/fullcar/carpool/presentation/carpool/CarpoolController.java @@ -108,4 +108,20 @@ public ApiResponse patchCarpool( carpoolService.closeCarpool(member, new CarpoolId(carpoolId)) ); } + + @Operation(summary = "카풀 삭제 API") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "삭제 성공") + }) + @DeleteMapping("/carpools/{carpoolId}") + public ApiResponse deleteCarpool( + @Parameter(hidden = true) + @CurrentMember Member member, + @PathVariable Long carpoolId + ) { + return ApiResponse.success( + SuccessCode.DELETE_SUCCESS, + carpoolService.deleteCarpool(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 0dddfdf..dc833f9 100644 --- a/src/main/java/com/fullcar/core/response/ErrorCode.java +++ b/src/main/java/com/fullcar/core/response/ErrorCode.java @@ -20,9 +20,12 @@ public enum ErrorCode { DUPLICATED_NICKNAME(BAD_REQUEST, "중복된 닉네임 입니다."), EXISTED_CAR_IN_MEMBER(BAD_REQUEST, "이미 차량이 등록되었습니다."), CANNOT_CHANGE_FORM_STATE(BAD_REQUEST, "카풀에 등록된 운전자만 신청서 수락/거절을 할 수 있습니다."), - CANNOT_CLOSE_CARPOOL(BAD_REQUEST, "삭제 권한이 없습니다."), + CANNOT_CLOSE_CARPOOL(BAD_REQUEST, "카풀마감 권한이 없습니다."), + CANNOT_DELETE_CARPOOL(BAD_REQUEST, "카풀삭제 권한이 없습니다."), INVALID_FORM_STATE(BAD_REQUEST, "유효하지 않은 신청서 상태입니다."), NOT_MATCHED_CODE(BAD_REQUEST, "인증번호가 일치하지 않습니다."), + ALREADY_CLOSED(BAD_REQUEST, "이미 마감된 카풀입니다."), + CANNOT_DELETE_OPEN_CARPOOL(BAD_REQUEST, "모집중인 카풀은 삭제할 수 없습니다."), /* 401 UNAUTHORIZED */ UNAUTHORIZED_KAKAO_TOKEN(UNAUTHORIZED, "유효하지 않은 카카오 토큰"), diff --git a/src/main/java/com/fullcar/core/response/SuccessCode.java b/src/main/java/com/fullcar/core/response/SuccessCode.java index 61cc47e..7ecbaa8 100644 --- a/src/main/java/com/fullcar/core/response/SuccessCode.java +++ b/src/main/java/com/fullcar/core/response/SuccessCode.java @@ -22,7 +22,8 @@ public enum SuccessCode { UPDATE_SUCCESS(OK, "수정 성공"), CODE_VERIFICATION_SUCCESS(OK, "인증 성공"), WITHDRAW_SUCCESS(OK, "탈퇴 성공"), - SAVE_DEVICE_TOKEN_SUCCESS(OK, "디바이스 토큰 등록 성공"); + SAVE_DEVICE_TOKEN_SUCCESS(OK, "디바이스 토큰 등록 성공"), + DELETE_SUCCESS(OK, "삭제 성공"); private final HttpStatus status; private final String message;