diff --git a/src/main/java/com/somemore/recruitboard/domain/RecruitBoard.java b/src/main/java/com/somemore/recruitboard/domain/RecruitBoard.java index 0b3115f3f..6d0c91f37 100644 --- a/src/main/java/com/somemore/recruitboard/domain/RecruitBoard.java +++ b/src/main/java/com/somemore/recruitboard/domain/RecruitBoard.java @@ -15,6 +15,7 @@ import jakarta.persistence.Id; import jakarta.persistence.Lob; import jakarta.persistence.Table; +import java.time.LocalDateTime; import java.time.LocalTime; import java.util.UUID; import lombok.Builder; @@ -84,6 +85,13 @@ public void updateWith(String region) { recruitmentInfo.updateWith(region); } + public void changeRecruitStatus(RecruitStatus newStatus, LocalDateTime currentDateTime) { + validateStatusChange(newStatus); + validateChangeDeadline(currentDateTime); + + this.recruitStatus = newStatus; + } + private void updateRecruitmentInfo(RecruitBoardUpdateRequestDto dto) { recruitmentInfo.updateWith( dto.recruitmentCount(), @@ -93,4 +101,21 @@ private void updateRecruitmentInfo(RecruitBoardUpdateRequestDto dto) { dto.admitted() ); } + + private void validateStatusChange(RecruitStatus newStatus) { + if (newStatus.isChangeable()) { + return; + } + throw new IllegalArgumentException("상태는 '모집중' 또는 '마감'으로만 변경할 수 있습니다."); + } + + private void validateChangeDeadline(LocalDateTime currentDateTime) { + LocalDateTime volunteerStartDateTime = recruitmentInfo.getVolunteerStartDateTime(); + LocalDateTime deadline = volunteerStartDateTime.toLocalDate().atStartOfDay(); + + if (!currentDateTime.isBefore(deadline)) { + throw new IllegalStateException("봉사 시작 일시 자정 전까지만 상태를 변경할 수 있습니다."); + } + } + } diff --git a/src/main/java/com/somemore/recruitboard/domain/RecruitStatus.java b/src/main/java/com/somemore/recruitboard/domain/RecruitStatus.java index a10eee42d..844cff46b 100644 --- a/src/main/java/com/somemore/recruitboard/domain/RecruitStatus.java +++ b/src/main/java/com/somemore/recruitboard/domain/RecruitStatus.java @@ -6,11 +6,11 @@ @Getter @RequiredArgsConstructor public enum RecruitStatus { - RECRUITING("모집중"), - CLOSED("마감"), - COMPLETED("종료"), + RECRUITING("모집중", true), + CLOSED("마감", true), + COMPLETED("종료", false), ; private final String text; - + private final boolean changeable; } diff --git a/src/main/java/com/somemore/recruitboard/service/command/DeleteRecruitBoardService.java b/src/main/java/com/somemore/recruitboard/service/command/DeleteRecruitBoardService.java new file mode 100644 index 000000000..d1f1e9a66 --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/service/command/DeleteRecruitBoardService.java @@ -0,0 +1,42 @@ +package com.somemore.recruitboard.service.command; + +import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_RECRUIT_BOARD; +import static com.somemore.global.exception.ExceptionMessage.UNAUTHORIZED_RECRUIT_BOARD; + +import com.somemore.global.exception.BadRequestException; +import com.somemore.global.exception.ExceptionMessage; +import com.somemore.recruitboard.domain.RecruitBoard; +import com.somemore.recruitboard.repository.RecruitBoardRepository; +import com.somemore.recruitboard.usecase.command.DeleteRecruitBoardUseCase; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional +@Service +public class DeleteRecruitBoardService implements DeleteRecruitBoardUseCase { + + private final RecruitBoardRepository recruitBoardRepository; + + @Override + public void deleteRecruitBoard(UUID centerId, Long recruitBoardId) { + RecruitBoard recruitBoard = recruitBoardRepository.findById(recruitBoardId).orElseThrow( + () -> new BadRequestException(NOT_EXISTS_RECRUIT_BOARD.getMessage()) + ); + + validateWriter(recruitBoard, centerId); + + recruitBoard.markAsDeleted(); + recruitBoardRepository.save(recruitBoard); + } + + private void validateWriter(RecruitBoard recruitBoard, UUID centerId) { + if (recruitBoard.isWriter(centerId)) { + return; + } + + throw new BadRequestException(UNAUTHORIZED_RECRUIT_BOARD.getMessage()); + } +} diff --git a/src/main/java/com/somemore/recruitboard/service/command/UpdateRecruitBoardService.java b/src/main/java/com/somemore/recruitboard/service/command/UpdateRecruitBoardService.java index 1abaf0c02..fc0e6d96f 100644 --- a/src/main/java/com/somemore/recruitboard/service/command/UpdateRecruitBoardService.java +++ b/src/main/java/com/somemore/recruitboard/service/command/UpdateRecruitBoardService.java @@ -6,11 +6,12 @@ import com.somemore.global.exception.BadRequestException; import com.somemore.location.usecase.command.UpdateLocationUseCase; import com.somemore.recruitboard.domain.RecruitBoard; +import com.somemore.recruitboard.domain.RecruitStatus; import com.somemore.recruitboard.dto.request.RecruitBoardLocationUpdateRequestDto; import com.somemore.recruitboard.dto.request.RecruitBoardUpdateRequestDto; import com.somemore.recruitboard.repository.RecruitBoardRepository; import com.somemore.recruitboard.usecase.command.UpdateRecruitBoardUseCase; -import com.somemore.recruitboard.usecase.query.RecruitBoardQueryUseCase; +import java.time.LocalDateTime; import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -23,7 +24,6 @@ public class UpdateRecruitBoardService implements UpdateRecruitBoardUseCase { private final RecruitBoardRepository recruitBoardRepository; - private final RecruitBoardQueryUseCase recruitBoardQueryUseCase; private final UpdateLocationUseCase updateLocationUseCase; @Override @@ -33,9 +33,7 @@ public void updateRecruitBoard( UUID centerId, String imgUrl) { - RecruitBoard recruitBoard = recruitBoardQueryUseCase.findById(recruitBoardId).orElseThrow( - () -> new BadRequestException(NOT_EXISTS_RECRUIT_BOARD.getMessage()) - ); + RecruitBoard recruitBoard = getRecruitBoard(recruitBoardId); validateWriter(recruitBoard, centerId); recruitBoard.updateWith(requestDto, imgUrl); @@ -46,9 +44,7 @@ public void updateRecruitBoard( public void updateRecruitBoardLocation(RecruitBoardLocationUpdateRequestDto requestDto, Long recruitBoardId, UUID centerId) { - RecruitBoard recruitBoard = recruitBoardQueryUseCase.findById(recruitBoardId).orElseThrow( - () -> new BadRequestException(NOT_EXISTS_RECRUIT_BOARD.getMessage()) - ); + RecruitBoard recruitBoard = getRecruitBoard(recruitBoardId); validateWriter(recruitBoard, centerId); updateLocationUseCase.updateLocation(requestDto.toLocationUpdateRequestDto(), @@ -58,6 +54,22 @@ public void updateRecruitBoardLocation(RecruitBoardLocationUpdateRequestDto requ recruitBoardRepository.save(recruitBoard); } + @Override + public void updateRecruitBoardStatus(RecruitStatus status, Long recruitBoardId, UUID centerId, + LocalDateTime currentDateTime) { + RecruitBoard recruitBoard = getRecruitBoard(recruitBoardId); + validateWriter(recruitBoard, centerId); + + recruitBoard.changeRecruitStatus(status, currentDateTime); + recruitBoardRepository.save(recruitBoard); + } + + private RecruitBoard getRecruitBoard(Long recruitBoardId) { + return recruitBoardRepository.findById(recruitBoardId).orElseThrow( + () -> new BadRequestException(NOT_EXISTS_RECRUIT_BOARD.getMessage()) + ); + } + private void validateWriter(RecruitBoard recruitBoard, UUID centerId) { if (recruitBoard.isWriter(centerId)) { return; diff --git a/src/main/java/com/somemore/recruitboard/usecase/command/DeleteRecruitBoardUseCase.java b/src/main/java/com/somemore/recruitboard/usecase/command/DeleteRecruitBoardUseCase.java new file mode 100644 index 000000000..91882f2d3 --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/usecase/command/DeleteRecruitBoardUseCase.java @@ -0,0 +1,8 @@ +package com.somemore.recruitboard.usecase.command; + +import java.util.UUID; + +public interface DeleteRecruitBoardUseCase { + + void deleteRecruitBoard(UUID centerId, Long recruitBoardId); +} diff --git a/src/main/java/com/somemore/recruitboard/usecase/command/UpdateRecruitBoardUseCase.java b/src/main/java/com/somemore/recruitboard/usecase/command/UpdateRecruitBoardUseCase.java index 304e5ae8b..c96457f67 100644 --- a/src/main/java/com/somemore/recruitboard/usecase/command/UpdateRecruitBoardUseCase.java +++ b/src/main/java/com/somemore/recruitboard/usecase/command/UpdateRecruitBoardUseCase.java @@ -1,22 +1,31 @@ package com.somemore.recruitboard.usecase.command; +import com.somemore.recruitboard.domain.RecruitStatus; import com.somemore.recruitboard.dto.request.RecruitBoardLocationUpdateRequestDto; import com.somemore.recruitboard.dto.request.RecruitBoardUpdateRequestDto; +import java.time.LocalDateTime; import java.util.UUID; public interface UpdateRecruitBoardUseCase { - public void updateRecruitBoard( + void updateRecruitBoard( RecruitBoardUpdateRequestDto requestDto, Long recruitBoardId, UUID centerId, String imgUrl ); - public void updateRecruitBoardLocation( + void updateRecruitBoardLocation( RecruitBoardLocationUpdateRequestDto requestDto, Long recruitBoardId, UUID centerId ); + void updateRecruitBoardStatus( + RecruitStatus status, + Long recruitBoardId, + UUID centerId, + LocalDateTime currentDateTime + ); + } diff --git a/src/test/java/com/somemore/common/fixture/LocalDateTimeFixture.java b/src/test/java/com/somemore/common/fixture/LocalDateTimeFixture.java index 6bf9370e5..a68affa76 100644 --- a/src/test/java/com/somemore/common/fixture/LocalDateTimeFixture.java +++ b/src/test/java/com/somemore/common/fixture/LocalDateTimeFixture.java @@ -17,4 +17,9 @@ public static LocalDateTime createUpdateStartDateTime() { return LocalDateTime.of(2024, 11, 25, 16, 0); } + public static LocalDateTime createCurrentDateTime() { + // 2024-11-24 T:18:00:00 + return LocalDateTime.of(2024, 11, 24, 18, 0); + } + } diff --git a/src/test/java/com/somemore/recruitboard/domain/RecruitBoardTest.java b/src/test/java/com/somemore/recruitboard/domain/RecruitBoardTest.java index 4decdd19b..2ac18c467 100644 --- a/src/test/java/com/somemore/recruitboard/domain/RecruitBoardTest.java +++ b/src/test/java/com/somemore/recruitboard/domain/RecruitBoardTest.java @@ -1,10 +1,14 @@ package com.somemore.recruitboard.domain; +import static com.somemore.common.fixture.LocalDateTimeFixture.createCurrentDateTime; import static com.somemore.common.fixture.LocalDateTimeFixture.createStartDateTime; import static com.somemore.common.fixture.LocalDateTimeFixture.createUpdateStartDateTime; +import static com.somemore.recruitboard.domain.RecruitStatus.CLOSED; +import static com.somemore.recruitboard.domain.RecruitStatus.COMPLETED; import static com.somemore.recruitboard.domain.RecruitStatus.RECRUITING; import static com.somemore.recruitboard.domain.VolunteerType.OTHER; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.somemore.recruitboard.dto.request.RecruitBoardUpdateRequestDto; import java.time.LocalDateTime; @@ -12,6 +16,8 @@ import java.util.UUID; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class RecruitBoardTest { @@ -120,6 +126,70 @@ void isNotWriterWithWrongCenterId() { assertThat(isWriter).isFalse(); } + @DisplayName("모집글 상태를 모집중에서 모집 마감으로 변경할 수 있다") + @Test + void changeStatusFromRecruitingToClose() { + // given + UUID centerId = UUID.randomUUID(); + RecruitBoard recruitBoard = createRecruitBoard(centerId); + RecruitStatus newStatus = CLOSED; + LocalDateTime currentDateTime = createCurrentDateTime(); + + // when + recruitBoard.changeRecruitStatus(newStatus, currentDateTime); + + // then + assertThat(recruitBoard.getRecruitStatus()).isEqualTo(newStatus); + } + + @DisplayName("모집글 상태를 모집마감에서 모집중으로 변경할 수 있다") + @Test + void changeStatusFromCloseToRecruiting() { + // given + UUID centerId = UUID.randomUUID(); + RecruitBoard recruitBoard = createRecruitBoard(centerId); + LocalDateTime currentDateTime = createCurrentDateTime(); + recruitBoard.changeRecruitStatus(CLOSED, currentDateTime); + RecruitStatus newStatus = RECRUITING; + + // when + recruitBoard.changeRecruitStatus(newStatus, currentDateTime); + + // then + assertThat(recruitBoard.getRecruitStatus()).isEqualTo(newStatus); + } + + @DisplayName("모집글 상태는 마감으로 변경할 수 없다") + @Test + void changeStatusWhenInvalidStatus() { + // given + UUID centerId = UUID.randomUUID(); + RecruitBoard recruitBoard = createRecruitBoard(centerId); + LocalDateTime currentDateTime = createCurrentDateTime(); + + // when & then + assertThatThrownBy(() -> recruitBoard.changeRecruitStatus(COMPLETED, currentDateTime)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("봉사 시작일 자정 이후 모집 상태를 변경할 경우 에러가 발생한다") + @ParameterizedTest + @ValueSource(longs = {0, 1}) + void changeStatusWhenDeadLineAfter(Long secondsOffset) { + // given + UUID centerId = UUID.randomUUID(); + RecruitBoard recruitBoard = createRecruitBoard(centerId); + LocalDateTime deadLineDateTime = recruitBoard.getRecruitmentInfo() + .getVolunteerStartDateTime().toLocalDate().atStartOfDay(); + LocalDateTime currentDateTime = deadLineDateTime.plusSeconds(secondsOffset); + + // when + // then + assertThatThrownBy( + () -> recruitBoard.changeRecruitStatus(CLOSED, currentDateTime) + ).isInstanceOf(IllegalStateException.class); + + } private static RecruitBoard createRecruitBoard(UUID centerId) { LocalDateTime startDateTime = createStartDateTime(); @@ -149,4 +219,5 @@ private static RecruitBoard createRecruitBoard(UUID centerId, LocalDateTime star .recruitmentInfo(recruitmentInfo) .build(); } + } diff --git a/src/test/java/com/somemore/recruitboard/service/command/DeleteRecruitBoardServiceTest.java b/src/test/java/com/somemore/recruitboard/service/command/DeleteRecruitBoardServiceTest.java new file mode 100644 index 000000000..563026e53 --- /dev/null +++ b/src/test/java/com/somemore/recruitboard/service/command/DeleteRecruitBoardServiceTest.java @@ -0,0 +1,96 @@ +package com.somemore.recruitboard.service.command; + +import static com.somemore.common.fixture.LocalDateTimeFixture.createStartDateTime; +import static com.somemore.recruitboard.domain.VolunteerType.OTHER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.somemore.IntegrationTestSupport; +import com.somemore.global.exception.BadRequestException; +import com.somemore.recruitboard.domain.RecruitBoard; +import com.somemore.recruitboard.domain.RecruitmentInfo; +import com.somemore.recruitboard.repository.RecruitBoardRepository; +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class DeleteRecruitBoardServiceTest extends IntegrationTestSupport { + + @Autowired + private DeleteRecruitBoardService deleteRecruitBoardService; + + @Autowired + private RecruitBoardRepository recruitBoardRepository; + + private RecruitBoard recruitBoard; + + @BeforeEach + void setUp() { + recruitBoard = createRecruitBoard(); + recruitBoardRepository.saveAndFlush(recruitBoard); + } + + @AfterEach + void tearDown() { + recruitBoardRepository.deleteAllInBatch(); + } + + @DisplayName("봉사 모집글 식별값으로 모집글을 삭제할 수 있다") + @Test + void deleteRecruitBoard() { + // given + UUID centerId = recruitBoard.getCenterId(); + Long recruitBoardId = recruitBoard.getId(); + + // when + deleteRecruitBoardService.deleteRecruitBoard(centerId, recruitBoardId); + + // then + Optional findBoard = recruitBoardRepository.findById(recruitBoardId); + + assertThat(findBoard).isEmpty(); + } + + @DisplayName("모집글 삭제시 작성자가 아니면 에러가 발생한다") + @Test + void deleteRecruitBoardWithWrongCenterId() { + // given + UUID wrongCenterId = UUID.randomUUID(); + Long recruitBoardId = recruitBoard.getId(); + + // when + // then + assertThatThrownBy( + () -> deleteRecruitBoardService.deleteRecruitBoard(wrongCenterId, recruitBoardId) + ).isInstanceOf(BadRequestException.class); + } + + private static RecruitBoard createRecruitBoard() { + + LocalDateTime startDateTime = createStartDateTime(); + LocalDateTime endDateTime = startDateTime.plusHours(1); + + RecruitmentInfo recruitmentInfo = RecruitmentInfo.builder() + .region("경기") + .recruitmentCount(1) + .volunteerStartDateTime(startDateTime) + .volunteerEndDateTime(endDateTime) + .volunteerType(OTHER) + .admitted(true) + .build(); + + return RecruitBoard.builder() + .centerId(UUID.randomUUID()) + .locationId(1L) + .title("봉사모집제목") + .content("봉사모집내용") + .imgUrl("https://image.domain.com/links") + .recruitmentInfo(recruitmentInfo) + .build(); + } +} diff --git a/src/test/java/com/somemore/recruitboard/service/command/UpdateRecruitBoardServiceTest.java b/src/test/java/com/somemore/recruitboard/service/command/UpdateRecruitBoardServiceTest.java index 81b95a277..ab7b0f3ba 100644 --- a/src/test/java/com/somemore/recruitboard/service/command/UpdateRecruitBoardServiceTest.java +++ b/src/test/java/com/somemore/recruitboard/service/command/UpdateRecruitBoardServiceTest.java @@ -1,5 +1,6 @@ package com.somemore.recruitboard.service.command; +import static com.somemore.common.fixture.LocalDateTimeFixture.createCurrentDateTime; import static com.somemore.common.fixture.LocalDateTimeFixture.createStartDateTime; import static com.somemore.common.fixture.LocalDateTimeFixture.createUpdateStartDateTime; import static com.somemore.recruitboard.domain.VolunteerType.ADMINISTRATIVE_SUPPORT; @@ -11,6 +12,7 @@ import com.somemore.location.domain.Location; import com.somemore.location.repository.LocationRepository; import com.somemore.recruitboard.domain.RecruitBoard; +import com.somemore.recruitboard.domain.RecruitStatus; import com.somemore.recruitboard.domain.RecruitmentInfo; import com.somemore.recruitboard.dto.request.RecruitBoardLocationUpdateRequestDto; import com.somemore.recruitboard.dto.request.RecruitBoardUpdateRequestDto; @@ -37,12 +39,11 @@ class UpdateRecruitBoardServiceTest extends IntegrationTestSupport { private LocationRepository locationRepository; private RecruitBoard recruitBoard; - private Location location; private UUID centerId; @BeforeEach void setUp() { - location = createLocation(); + Location location = createLocation(); locationRepository.saveAndFlush(location); centerId = UUID.randomUUID(); recruitBoard = createRecruitBoard(centerId, location.getId()); @@ -150,6 +151,23 @@ void updateRecruitBoardWhenCenterIdIsWrong() { } + @DisplayName("봉사 모집글 상태를 변경할 수 있다") + @Test + void updateRecruitBoardStatus() { + // given + Long recruitBoardId = recruitBoard.getId(); + RecruitStatus newStatus = RecruitStatus.CLOSED; + LocalDateTime currentDateTime = createCurrentDateTime(); + + // when + updateRecruitBoardService.updateRecruitBoardStatus(newStatus, recruitBoardId, centerId, + currentDateTime); + + // then + RecruitBoard findBoard = recruitBoardRepository.findById(recruitBoardId).orElseThrow(); + assertThat(findBoard.getRecruitStatus()).isEqualTo(newStatus); + } + private static RecruitBoard createRecruitBoard(UUID centerId, Long locationId) { LocalDateTime startDateTime = createStartDateTime(); LocalDateTime endDateTime = startDateTime.plusHours(1);