diff --git a/src/main/java/com/somemore/location/domain/Location.java b/src/main/java/com/somemore/location/domain/Location.java index 3ac082619..7229778f0 100644 --- a/src/main/java/com/somemore/location/domain/Location.java +++ b/src/main/java/com/somemore/location/domain/Location.java @@ -4,12 +4,14 @@ import static lombok.AccessLevel.PROTECTED; import com.somemore.global.common.BaseEntity; +import com.somemore.location.dto.request.LocationUpdateRequestDto; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.Table; import java.math.BigDecimal; +import java.math.RoundingMode; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -36,7 +38,13 @@ public class Location extends BaseEntity { @Builder public Location(String address, BigDecimal latitude, BigDecimal longitude) { this.address = address; - this.latitude = latitude; - this.longitude = longitude; + this.latitude = latitude.setScale(8, RoundingMode.HALF_UP); + this.longitude = longitude.setScale(8, RoundingMode.HALF_UP); + } + + public void updateWith(LocationUpdateRequestDto requestDto) { + this.address = requestDto.address(); + this.latitude = requestDto.latitude().setScale(8, RoundingMode.HALF_UP); + this.longitude = requestDto.longitude().setScale(8, RoundingMode.HALF_UP); } } \ No newline at end of file diff --git a/src/main/java/com/somemore/location/dto/request/LocationUpdateRequestDto.java b/src/main/java/com/somemore/location/dto/request/LocationUpdateRequestDto.java new file mode 100644 index 000000000..1fe898e0f --- /dev/null +++ b/src/main/java/com/somemore/location/dto/request/LocationUpdateRequestDto.java @@ -0,0 +1,31 @@ +package com.somemore.location.dto.request; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; +import lombok.Builder; + +@JsonNaming(SnakeCaseStrategy.class) +@Builder +public record LocationUpdateRequestDto( + @Schema(description = "도로명 주소", example = "서울특별시 서초구 반포대로 45, 4층(서초동, 명정빌딩)") + @NotBlank(message = "주소는 필수 입력 값입니다.") + String address, + @Schema(description = "주소에 해당하는 위도 정보", example = "37.4845373748015") + @NotNull(message = "위도는 필수 입력 값입니다.") + @DecimalMin(value = "33", message = "위도는 33도 이상이어야 합니다.") + @DecimalMax(value = "39", message = "위도는 38도 이하이어야 합니다.") + BigDecimal latitude, + @Schema(description = "주소에 해당하는 경도 정보", example = "127.010842267696") + @NotNull(message = "경도는 필수 입력 값입니다.") + @DecimalMin(value = "124", message = "경도는 124도 이상이어야 합니다.") + @DecimalMax(value = "132", message = "경도는 132도 이하이어야 합니다.") + BigDecimal longitude +) { + +} diff --git a/src/main/java/com/somemore/location/repository/LocationRepository.java b/src/main/java/com/somemore/location/repository/LocationRepository.java index 602211c7a..b2a5681a7 100644 --- a/src/main/java/com/somemore/location/repository/LocationRepository.java +++ b/src/main/java/com/somemore/location/repository/LocationRepository.java @@ -7,6 +7,8 @@ public interface LocationRepository { Location save(Location location); + Location saveAndFlush(Location location); + Optional findById(Long id); void deleteAllInBatch(); diff --git a/src/main/java/com/somemore/location/repository/LocationRepositoryImpl.java b/src/main/java/com/somemore/location/repository/LocationRepositoryImpl.java index 35104da0d..06b7d940e 100644 --- a/src/main/java/com/somemore/location/repository/LocationRepositoryImpl.java +++ b/src/main/java/com/somemore/location/repository/LocationRepositoryImpl.java @@ -18,6 +18,11 @@ public Location save(Location location) { return locationJpaRepository.save(location); } + @Override + public Location saveAndFlush(Location location) { + return locationJpaRepository.saveAndFlush(location); + } + @Override public Optional findById(Long id) { return locationJpaRepository.findById(id); diff --git a/src/main/java/com/somemore/location/service/command/UpdateLocationService.java b/src/main/java/com/somemore/location/service/command/UpdateLocationService.java new file mode 100644 index 000000000..deec6d0c8 --- /dev/null +++ b/src/main/java/com/somemore/location/service/command/UpdateLocationService.java @@ -0,0 +1,26 @@ +package com.somemore.location.service.command; + +import com.somemore.location.domain.Location; +import com.somemore.location.dto.request.LocationUpdateRequestDto; +import com.somemore.location.repository.LocationRepository; +import com.somemore.location.usecase.command.UpdateLocationUseCase; +import com.somemore.location.usecase.query.LocationQueryUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional +@Service +public class UpdateLocationService implements UpdateLocationUseCase { + + private final LocationQueryUseCase locationQueryUseCase; + private final LocationRepository locationRepository; + + @Override + public void updateLocation(LocationUpdateRequestDto requestDto, Long locationId) { + Location location = locationQueryUseCase.findByIdOrThrow(locationId); + location.updateWith(requestDto); + locationRepository.save(location); + } +} diff --git a/src/main/java/com/somemore/location/service/query/LocationQueryService.java b/src/main/java/com/somemore/location/service/query/LocationQueryService.java new file mode 100644 index 000000000..1b9c291ca --- /dev/null +++ b/src/main/java/com/somemore/location/service/query/LocationQueryService.java @@ -0,0 +1,30 @@ +package com.somemore.location.service.query; + +import com.somemore.global.exception.BadRequestException; +import com.somemore.location.domain.Location; +import com.somemore.location.repository.LocationRepository; +import com.somemore.location.usecase.query.LocationQueryUseCase; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class LocationQueryService implements LocationQueryUseCase { + + private final LocationRepository locationRepository; + + @Override + public Optional findById(Long id) { + return locationRepository.findById(id); + } + + @Override + public Location findByIdOrThrow(Long id) { + return locationRepository.findById(id).orElseThrow( + () -> new BadRequestException("존재하지 않는 위치입니다.") + ); + } +} diff --git a/src/main/java/com/somemore/location/usecase/command/UpdateLocationUseCase.java b/src/main/java/com/somemore/location/usecase/command/UpdateLocationUseCase.java new file mode 100644 index 000000000..b1e776b8b --- /dev/null +++ b/src/main/java/com/somemore/location/usecase/command/UpdateLocationUseCase.java @@ -0,0 +1,8 @@ +package com.somemore.location.usecase.command; + +import com.somemore.location.dto.request.LocationUpdateRequestDto; + +public interface UpdateLocationUseCase { + + void updateLocation(LocationUpdateRequestDto requestDto, Long locationId); +} diff --git a/src/main/java/com/somemore/location/usecase/query/LocationQueryUseCase.java b/src/main/java/com/somemore/location/usecase/query/LocationQueryUseCase.java new file mode 100644 index 000000000..5aec218ce --- /dev/null +++ b/src/main/java/com/somemore/location/usecase/query/LocationQueryUseCase.java @@ -0,0 +1,12 @@ +package com.somemore.location.usecase.query; + +import com.somemore.location.domain.Location; +import java.util.Optional; + +public interface LocationQueryUseCase { + + Optional findById(Long id); + + Location findByIdOrThrow(Long id); + +} diff --git a/src/main/java/com/somemore/recruitboard/domain/RecruitBoard.java b/src/main/java/com/somemore/recruitboard/domain/RecruitBoard.java index 14b3dad61..ee25afbf0 100644 --- a/src/main/java/com/somemore/recruitboard/domain/RecruitBoard.java +++ b/src/main/java/com/somemore/recruitboard/domain/RecruitBoard.java @@ -6,15 +6,15 @@ import static lombok.AccessLevel.PROTECTED; import com.somemore.global.common.BaseEntity; +import com.somemore.recruitboard.dto.request.RecruitBoardUpdateRequestDto; import jakarta.persistence.Column; +import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.Lob; import jakarta.persistence.Table; -import java.time.Duration; -import java.time.LocalDateTime; import java.time.LocalTime; import java.util.UUID; import lombok.Builder; @@ -44,64 +44,58 @@ public class RecruitBoard extends BaseEntity { @Column(name = "content", nullable = false) private String content; - @Column(name = "region", nullable = false) - private String region; - - @Column(name = "recruitment_count", nullable = false) - private Integer recruitmentCount; - - @Column(name = "img_url", nullable = false) - private String imgUrl; + @Embedded + private VolunteerInfo volunteerInfo; @Enumerated(value = STRING) @Column(name = "recruit_status", nullable = false, length = 20) private RecruitStatus recruitStatus = RECRUITING; - @Column(name = "volunteer_start_date_time", nullable = false) - private LocalDateTime volunteerStartDateTime; - - @Column(name = "volunteer_end_date_time", nullable = false) - private LocalDateTime volunteerEndDateTime; - - @Enumerated(value = STRING) - @Column(name = "volunteer_type", nullable = false, length = 30) - private VolunteerType volunteerType; - - @Column(name = "admitted", nullable = false) - private Boolean admitted; + @Column(name = "img_url", nullable = false) + private String imgUrl; @Builder - public RecruitBoard(UUID centerId, Long locationId, String title, String content, String region, - Integer recruitmentCount, String imgUrl, LocalDateTime volunteerStartDateTime, - LocalDateTime volunteerEndDateTime, VolunteerType volunteerType, Boolean admitted) { - - validateVolunteerDateTime(volunteerStartDateTime, volunteerEndDateTime); - + public RecruitBoard(UUID centerId, Long locationId, String title, String content, + VolunteerInfo volunteerInfo, String imgUrl) { this.centerId = centerId; this.locationId = locationId; this.title = title; this.content = content; - this.region = region; - this.recruitmentCount = recruitmentCount; + this.volunteerInfo = volunteerInfo; this.imgUrl = imgUrl; - this.volunteerStartDateTime = volunteerStartDateTime; - this.volunteerEndDateTime = volunteerEndDateTime; - this.volunteerType = volunteerType; - this.admitted = admitted; } - public LocalTime calculateVolunteerTime() { - Duration duration = Duration.between(volunteerStartDateTime, volunteerEndDateTime); + public LocalTime getVolunteerHours() { + return volunteerInfo.calculateVolunteerTime(); + } + + public boolean isWriter(UUID centerId) { + return this.centerId.equals(centerId); + } + + public boolean isNotWriter(UUID centerId) { + return !isWriter(centerId); + } - long hours = duration.toHours(); - long minutes = duration.toMinutes() % 60; + public void updateWith(RecruitBoardUpdateRequestDto dto, String imgUrl) { + updateVolunteerInfo(dto); + this.title = dto.title(); + this.content = dto.content(); + this.imgUrl = imgUrl; + } - return LocalTime.of((int) hours, (int) minutes); + public void updateWith(String region) { + volunteerInfo.updateWith(region); } - private void validateVolunteerDateTime(LocalDateTime startDateTime, LocalDateTime endDateTime) { - if (endDateTime.isEqual(startDateTime) || endDateTime.isBefore(startDateTime)) { - throw new IllegalArgumentException("종료 시간은 시작 시간보다 이후여야 합니다."); - } + private void updateVolunteerInfo(RecruitBoardUpdateRequestDto dto) { + volunteerInfo.updateWith( + dto.recruitmentCount(), + dto.volunteerType(), + dto.volunteerStartDateTime(), + dto.volunteerEndDateTime(), + dto.admitted() + ); } + } \ No newline at end of file diff --git a/src/main/java/com/somemore/recruitboard/domain/VolunteerInfo.java b/src/main/java/com/somemore/recruitboard/domain/VolunteerInfo.java new file mode 100644 index 000000000..fdcd8336a --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/domain/VolunteerInfo.java @@ -0,0 +1,86 @@ +package com.somemore.recruitboard.domain; + +import static jakarta.persistence.EnumType.STRING; +import static lombok.AccessLevel.PROTECTED; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Enumerated; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.LocalTime; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class VolunteerInfo { + + @Column(name = "region", nullable = false) + private String region; + + @Column(name = "recruitment_count", nullable = false) + private Integer recruitmentCount; + + @Column(name = "volunteer_start_date_time", nullable = false) + private LocalDateTime volunteerStartDateTime; + + @Column(name = "volunteer_end_date_time", nullable = false) + private LocalDateTime volunteerEndDateTime; + + @Enumerated(value = STRING) + @Column(name = "volunteer_type", nullable = false, length = 30) + private VolunteerType volunteerType; + + @Column(name = "admitted", nullable = false) + private Boolean admitted; + + @Builder + public VolunteerInfo(String region, Integer recruitmentCount, + LocalDateTime volunteerStartDateTime, LocalDateTime volunteerEndDateTime, + VolunteerType volunteerType, Boolean admitted) { + + validateVolunteerDateTime(volunteerStartDateTime, volunteerEndDateTime); + + this.region = region; + this.recruitmentCount = recruitmentCount; + this.volunteerStartDateTime = volunteerStartDateTime; + this.volunteerEndDateTime = volunteerEndDateTime; + this.volunteerType = volunteerType; + this.admitted = admitted; + } + + public LocalTime calculateVolunteerTime() { + Duration duration = Duration.between(volunteerStartDateTime, volunteerEndDateTime); + + long hours = duration.toHours(); + long minutes = duration.toMinutes() % 60; + + return LocalTime.of((int) hours, (int) minutes); + } + + public void updateWith(Integer recruitmentCount, VolunteerType volunteerType, + LocalDateTime volunteerStartDateTime, LocalDateTime volunteerEndDateTime, + Boolean admitted) { + + validateVolunteerDateTime(volunteerStartDateTime, volunteerEndDateTime); + + this.recruitmentCount = recruitmentCount; + this.volunteerType = volunteerType; + this.volunteerStartDateTime = volunteerStartDateTime; + this.volunteerEndDateTime = volunteerEndDateTime; + this.admitted = admitted; + } + + public void updateWith(String region) { + this.region = region; + } + + private void validateVolunteerDateTime(LocalDateTime startDateTime, LocalDateTime endDateTime) { + if (endDateTime.isEqual(startDateTime) || endDateTime.isBefore(startDateTime)) { + throw new IllegalArgumentException("종료 시간은 시작 시간보다 이후여야 합니다."); + } + } +} diff --git a/src/main/java/com/somemore/recruitboard/dto/request/RecruitBoardCreateRequestDto.java b/src/main/java/com/somemore/recruitboard/dto/request/RecruitBoardCreateRequestDto.java index e047c5886..6d02406cd 100644 --- a/src/main/java/com/somemore/recruitboard/dto/request/RecruitBoardCreateRequestDto.java +++ b/src/main/java/com/somemore/recruitboard/dto/request/RecruitBoardCreateRequestDto.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.somemore.location.dto.request.LocationCreateRequestDto; import com.somemore.recruitboard.domain.RecruitBoard; +import com.somemore.recruitboard.domain.VolunteerInfo; import com.somemore.recruitboard.domain.VolunteerType; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; @@ -44,18 +45,22 @@ public record RecruitBoardCreateRequestDto( ) { public RecruitBoard toEntity(UUID centerId, Long locationId, String imgUrl) { + VolunteerInfo volunteerInfo = VolunteerInfo.builder() + .region("경기") + .recruitmentCount(recruitmentCount) + .volunteerStartDateTime(volunteerStartDateTime) + .volunteerEndDateTime(volunteerEndDateTime) + .volunteerType(volunteerType) + .admitted(admitted) + .build(); + return RecruitBoard.builder() .centerId(centerId) .locationId(locationId) .title(title) .content(content) - .region(region) - .recruitmentCount(recruitmentCount) .imgUrl(imgUrl) - .volunteerStartDateTime(volunteerStartDateTime) - .volunteerEndDateTime(volunteerEndDateTime) - .volunteerType(volunteerType) - .admitted(admitted) + .volunteerInfo(volunteerInfo) .build(); } } diff --git a/src/main/java/com/somemore/recruitboard/dto/request/RecruitBoardLocationUpdateRequestDto.java b/src/main/java/com/somemore/recruitboard/dto/request/RecruitBoardLocationUpdateRequestDto.java new file mode 100644 index 000000000..e2a5d0515 --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/dto/request/RecruitBoardLocationUpdateRequestDto.java @@ -0,0 +1,42 @@ +package com.somemore.recruitboard.dto.request; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.somemore.location.dto.request.LocationUpdateRequestDto; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; +import lombok.Builder; + +@JsonNaming(SnakeCaseStrategy.class) +@Builder +public record RecruitBoardLocationUpdateRequestDto( + @Schema(description = "지역", example = "서울특별시") + @NotBlank(message = "지역은 필수 입력 값입니다.") + String region, + @Schema(description = "도로명 주소", example = "서울특별시 서초구 반포대로 45, 4층(서초동, 명정빌딩)") + @NotBlank(message = "주소는 필수 입력 값입니다.") + String address, + @Schema(description = "주소에 해당하는 위도 정보", example = "37.4845373748015") + @NotNull(message = "위도는 필수 입력 값입니다.") + @DecimalMin(value = "33", message = "위도는 33도 이상이어야 합니다.") + @DecimalMax(value = "39", message = "위도는 38도 이하이어야 합니다.") + BigDecimal latitude, + @Schema(description = "주소에 해당하는 경도 정보", example = "127.010842267696") + @NotNull(message = "경도는 필수 입력 값입니다.") + @DecimalMin(value = "124", message = "경도는 124도 이상이어야 합니다.") + @DecimalMax(value = "132", message = "경도는 132도 이하이어야 합니다.") + BigDecimal longitude +) { + + public LocationUpdateRequestDto toLocationUpdateRequestDto() { + return LocationUpdateRequestDto.builder() + .address(address) + .latitude(latitude) + .longitude(longitude) + .build(); + } +} diff --git a/src/main/java/com/somemore/recruitboard/dto/request/RecruitBoardUpdateRequestDto.java b/src/main/java/com/somemore/recruitboard/dto/request/RecruitBoardUpdateRequestDto.java new file mode 100644 index 000000000..f1592b5a2 --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/dto/request/RecruitBoardUpdateRequestDto.java @@ -0,0 +1,38 @@ +package com.somemore.recruitboard.dto.request; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.somemore.recruitboard.domain.VolunteerType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import lombok.Builder; + +@JsonNaming(SnakeCaseStrategy.class) +@Builder +public record RecruitBoardUpdateRequestDto( + @Schema(description = "봉사 모집글 제목", example = "서울 청계천 환경 미화 봉사 모집") + @NotBlank(message = "모집글 제목은 필수 값입니다.") + String title, + @Schema(description = "봉사 모집글 내용", example = "서울 청계천 주변 환경 미화 봉사 모집합니다.
") + @NotBlank(message = "모집글 내용은 필수 값입니다.") + String content, + @Schema(description = "예상 모집 인원", example = "4") + @NotNull(message = "예상 모집 인원은 필수 값입니다.") + Integer recruitmentCount, + @Schema(description = "봉사 시작 일시", example = "2024-11-20T10:00:00") + @NotNull(message = "봉사 시작 일시는 필수 값입니다.") + LocalDateTime volunteerStartDateTime, + @Schema(description = "봉사 종료 일시", example = "2024-11-20T12:00:00") + @NotNull(message = "봉사 종료 일시는 필수 값입니다.") + LocalDateTime volunteerEndDateTime, + @Schema(description = "봉사 활동 유형", example = "ENVIRONMENTAL_PROTECTION") + @NotNull(message = "봉사 활동 유형은 필수 값입니다.") + VolunteerType volunteerType, + @Schema(description = "봉사 시간 인정 여부", example = "true") + @NotNull(message = "시간 인정 여부는 필수 값입니다.") + Boolean admitted +) { + +} diff --git a/src/main/java/com/somemore/recruitboard/repository/RecruitBoardJpaRepository.java b/src/main/java/com/somemore/recruitboard/repository/RecruitBoardJpaRepository.java new file mode 100644 index 000000000..1380d9b80 --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/repository/RecruitBoardJpaRepository.java @@ -0,0 +1,8 @@ +package com.somemore.recruitboard.repository; + +import com.somemore.recruitboard.domain.RecruitBoard; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RecruitBoardJpaRepository extends JpaRepository { + +} diff --git a/src/main/java/com/somemore/recruitboard/repository/RecruitBoardRepository.java b/src/main/java/com/somemore/recruitboard/repository/RecruitBoardRepository.java index 8544032b0..ffc340b6c 100644 --- a/src/main/java/com/somemore/recruitboard/repository/RecruitBoardRepository.java +++ b/src/main/java/com/somemore/recruitboard/repository/RecruitBoardRepository.java @@ -1,8 +1,16 @@ package com.somemore.recruitboard.repository; import com.somemore.recruitboard.domain.RecruitBoard; -import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; -public interface RecruitBoardRepository extends JpaRepository { +public interface RecruitBoardRepository { + + RecruitBoard save(RecruitBoard recruitBoard); + + RecruitBoard saveAndFlush(RecruitBoard recruitBoard); + + Optional findById(Long id); + + void deleteAllInBatch(); } diff --git a/src/main/java/com/somemore/recruitboard/repository/RecruitBoardRepositoryImpl.java b/src/main/java/com/somemore/recruitboard/repository/RecruitBoardRepositoryImpl.java new file mode 100644 index 000000000..3ac769016 --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/repository/RecruitBoardRepositoryImpl.java @@ -0,0 +1,37 @@ +package com.somemore.recruitboard.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.somemore.recruitboard.domain.RecruitBoard; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class RecruitBoardRepositoryImpl implements RecruitBoardRepository { + + private final RecruitBoardJpaRepository recruitBoardJpaRepository; + private final JPAQueryFactory queryFactory; + + @Override + public RecruitBoard save(RecruitBoard recruitBoard) { + return recruitBoardJpaRepository.save(recruitBoard); + } + + @Override + public RecruitBoard saveAndFlush(RecruitBoard recruitBoard) { + return recruitBoardJpaRepository.saveAndFlush(recruitBoard); + } + + @Override + public Optional findById(Long id) { + return recruitBoardJpaRepository.findById(id); + } + + @Override + public void deleteAllInBatch() { + recruitBoardJpaRepository.deleteAllInBatch(); + } + + +} diff --git a/src/main/java/com/somemore/recruitboard/service/command/UpdateRecruitBoardService.java b/src/main/java/com/somemore/recruitboard/service/command/UpdateRecruitBoardService.java new file mode 100644 index 000000000..0b90de6e2 --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/service/command/UpdateRecruitBoardService.java @@ -0,0 +1,60 @@ +package com.somemore.recruitboard.service.command; + +import com.somemore.global.exception.BadRequestException; +import com.somemore.location.usecase.command.UpdateLocationUseCase; +import com.somemore.recruitboard.domain.RecruitBoard; +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.RecruitQueryUseCase; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + + +@RequiredArgsConstructor +@Transactional +@Service +public class UpdateRecruitBoardService implements UpdateRecruitBoardUseCase { + + private final RecruitBoardRepository recruitBoardRepository; + private final RecruitQueryUseCase recruitQueryUseCase; + private final UpdateLocationUseCase updateLocationUseCase; + + @Override + public void updateRecruitBoard( + RecruitBoardUpdateRequestDto requestDto, + Long recruitBoardId, + UUID centerId, + String imgUrl) { + + RecruitBoard recruitBoard = recruitQueryUseCase.findByIdOrThrow(recruitBoardId); + validateWriter(recruitBoard, centerId); + recruitBoard.updateWith(requestDto, imgUrl); + + recruitBoardRepository.save(recruitBoard); + } + + @Override + public void updateRecruitBoardLocation(RecruitBoardLocationUpdateRequestDto requestDto, + Long recruitBoardId, UUID centerId) { + + RecruitBoard recruitBoard = recruitQueryUseCase.findByIdOrThrow(recruitBoardId); + validateWriter(recruitBoard, centerId); + + updateLocationUseCase.updateLocation(requestDto.toLocationUpdateRequestDto(), + recruitBoard.getLocationId()); + + recruitBoard.updateWith(requestDto.region()); + recruitBoardRepository.save(recruitBoard); + } + + private void validateWriter(RecruitBoard recruitBoard, UUID centerId) { + if (recruitBoard.isNotWriter(centerId)) { + throw new BadRequestException("자신이 작성한 봉사 모집글만 수정할 수 있습니다."); + } + + } +} diff --git a/src/main/java/com/somemore/recruitboard/service/query/RecruitQueryService.java b/src/main/java/com/somemore/recruitboard/service/query/RecruitQueryService.java new file mode 100644 index 000000000..bd9942ba2 --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/service/query/RecruitQueryService.java @@ -0,0 +1,31 @@ +package com.somemore.recruitboard.service.query; + +import com.somemore.global.exception.BadRequestException; +import com.somemore.recruitboard.domain.RecruitBoard; +import com.somemore.recruitboard.repository.RecruitBoardRepository; +import com.somemore.recruitboard.usecase.query.RecruitQueryUseCase; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class RecruitQueryService implements RecruitQueryUseCase { + + private final RecruitBoardRepository recruitBoardRepository; + + @Override + public Optional findById(Long id) { + return recruitBoardRepository.findById(id); + } + + @Override + public RecruitBoard findByIdOrThrow(Long id) { + return recruitBoardRepository.findById(id).orElseThrow( + () -> new BadRequestException("존재하지 않는 봉사 모집 활동입니다.") + ); + } + +} diff --git a/src/main/java/com/somemore/recruitboard/usecase/command/UpdateRecruitBoardUseCase.java b/src/main/java/com/somemore/recruitboard/usecase/command/UpdateRecruitBoardUseCase.java new file mode 100644 index 000000000..304e5ae8b --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/usecase/command/UpdateRecruitBoardUseCase.java @@ -0,0 +1,22 @@ +package com.somemore.recruitboard.usecase.command; + +import com.somemore.recruitboard.dto.request.RecruitBoardLocationUpdateRequestDto; +import com.somemore.recruitboard.dto.request.RecruitBoardUpdateRequestDto; +import java.util.UUID; + +public interface UpdateRecruitBoardUseCase { + + public void updateRecruitBoard( + RecruitBoardUpdateRequestDto requestDto, + Long recruitBoardId, + UUID centerId, + String imgUrl + ); + + public void updateRecruitBoardLocation( + RecruitBoardLocationUpdateRequestDto requestDto, + Long recruitBoardId, + UUID centerId + ); + +} diff --git a/src/main/java/com/somemore/recruitboard/usecase/query/RecruitQueryUseCase.java b/src/main/java/com/somemore/recruitboard/usecase/query/RecruitQueryUseCase.java new file mode 100644 index 000000000..4655dae0b --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/usecase/query/RecruitQueryUseCase.java @@ -0,0 +1,12 @@ +package com.somemore.recruitboard.usecase.query; + +import com.somemore.recruitboard.domain.RecruitBoard; +import java.util.Optional; + +public interface RecruitQueryUseCase { + + Optional findById(Long id); + + RecruitBoard findByIdOrThrow(Long id); + +} diff --git a/src/test/java/com/somemore/location/domain/LocationTest.java b/src/test/java/com/somemore/location/domain/LocationTest.java new file mode 100644 index 000000000..4f5de223f --- /dev/null +++ b/src/test/java/com/somemore/location/domain/LocationTest.java @@ -0,0 +1,61 @@ +package com.somemore.location.domain; + +import static java.math.RoundingMode.HALF_UP; +import static org.assertj.core.api.Assertions.assertThat; + +import com.somemore.location.dto.request.LocationUpdateRequestDto; +import java.math.BigDecimal; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class LocationTest { + + @DisplayName("Location 생성시 소수점이 올바르게 반올림된다.") + @Test + void createLocation() { + // given + String address = "서울특별시 강남구"; + BigDecimal latitude = new BigDecimal("37.123456789"); // 소수점 초과값 + BigDecimal longitude = new BigDecimal("127.987654321"); // 소수점 초과값 + + // when + Location location = Location.builder() + .address(address) + .latitude(latitude) + .longitude(longitude) + .build(); + + // then + assertThat(location.getAddress()).isEqualTo(address); + assertThat(location.getLatitude()).isEqualTo(latitude.setScale(8, HALF_UP)); + assertThat(location.getLongitude()).isEqualTo(longitude.setScale(8, HALF_UP)); + } + + @DisplayName("위치를 업데이트 할 수 있다.") + @Test + void updateLocationWithDto() { + // given + Location location = Location.builder() + .address("서울특별시 서초구 반포대로 45, 4층(서초동, 명정빌딩)") + .longitude(BigDecimal.valueOf(37.4845373748015)) + .latitude(BigDecimal.valueOf(127.010842267696)) + .build(); + + LocationUpdateRequestDto requestDto = LocationUpdateRequestDto.builder() + .address("업데이트 주소") + .longitude(BigDecimal.valueOf(37.333333333333)) + .latitude(BigDecimal.valueOf(127.00000000000)) + .build(); + + // when + location.updateWith(requestDto); + + // then + assertThat(location.getAddress()).isEqualTo(requestDto.address()); + assertThat(location.getLongitude() + .compareTo(requestDto.longitude().setScale(8, HALF_UP))).isZero(); + assertThat(location.getLatitude() + .compareTo(requestDto.latitude().setScale(8, HALF_UP))).isZero(); + } + +} \ No newline at end of file diff --git a/src/test/java/com/somemore/location/repository/LocationRepositoryTest.java b/src/test/java/com/somemore/location/repository/LocationRepositoryTest.java index 372b7a3c5..b5c29b1f2 100644 --- a/src/test/java/com/somemore/location/repository/LocationRepositoryTest.java +++ b/src/test/java/com/somemore/location/repository/LocationRepositoryTest.java @@ -1,11 +1,11 @@ package com.somemore.location.repository; +import static java.math.RoundingMode.HALF_UP; import static org.assertj.core.api.Assertions.assertThat; import com.somemore.IntegrationTestSupport; import com.somemore.location.domain.Location; import java.math.BigDecimal; -import java.math.RoundingMode; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -28,9 +28,9 @@ void testLocationPrecisionWithRound() { Location locationWithRound = Location.builder() .address("서울특별시 서초구 반포대로 45, 4층(서초동, 명정빌딩)") .latitude( - new BigDecimal("37.484537379").setScale(8, RoundingMode.HALF_UP)) // 9자리 반올림 + new BigDecimal("37.484537379").setScale(8, HALF_UP)) // 9자리 반올림 .longitude( - new BigDecimal("127.010842349").setScale(8, RoundingMode.HALF_UP)) // 9자리 반올림 + new BigDecimal("127.010842349").setScale(8, HALF_UP)) // 9자리 반올림 .build(); // when @@ -49,9 +49,9 @@ void testLocationPrecisionWithLargeValues() { Location locationWithLargeValues = Location.builder() .address("서울특별시 서초구 반포대로 45, 4층(서초동, 명정빌딩)") .latitude( - new BigDecimal("89.999999999").setScale(8, RoundingMode.HALF_UP)) // 9자리 + new BigDecimal("89.999999999").setScale(8, HALF_UP)) // 9자리 .longitude( - new BigDecimal("179.999999999").setScale(8, RoundingMode.HALF_UP)) // 9자리 + new BigDecimal("179.999999999").setScale(8, HALF_UP)) // 9자리 .build(); // when diff --git a/src/test/java/com/somemore/location/service/command/UpdateLocationServiceTest.java b/src/test/java/com/somemore/location/service/command/UpdateLocationServiceTest.java new file mode 100644 index 000000000..d3fca6421 --- /dev/null +++ b/src/test/java/com/somemore/location/service/command/UpdateLocationServiceTest.java @@ -0,0 +1,63 @@ +package com.somemore.location.service.command; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.somemore.IntegrationTestSupport; +import com.somemore.location.domain.Location; +import com.somemore.location.dto.request.LocationUpdateRequestDto; +import com.somemore.location.repository.LocationRepository; +import java.math.BigDecimal; +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 UpdateLocationServiceTest extends IntegrationTestSupport { + + @Autowired + private UpdateLocationService updateLocationService; + + @Autowired + private LocationRepository locationRepository; + + private Location location; + + @BeforeEach + void setUp() { + location = Location.builder() + .address("주소주소") + .latitude(BigDecimal.valueOf(37.00000)) + .longitude(BigDecimal.valueOf(127.00000)) + .build(); + + locationRepository.saveAndFlush(location); + } + + @AfterEach + void tearDown() { + locationRepository.deleteAllInBatch(); + } + + @DisplayName("위치를 업데이트하면 저장소에 반영된다.") + @Test + void updateLocationWithDto() { + // given + BigDecimal latitude = BigDecimal.valueOf(37.111111); + BigDecimal longitude = BigDecimal.valueOf(127.11111); + LocationUpdateRequestDto dto = LocationUpdateRequestDto.builder() + .address("새로새로") + .latitude(latitude) + .longitude(longitude) + .build(); + + // when + updateLocationService.updateLocation(dto, location.getId()); + + // then + Location updateLocation = locationRepository.findById(location.getId()).orElseThrow(); + assertThat(updateLocation.getAddress()).isEqualTo(dto.address()); + assertThat(updateLocation.getLatitude().compareTo(dto.latitude())).isZero(); + assertThat(updateLocation.getLongitude().compareTo(dto.longitude())).isZero(); + } +} \ No newline at end of file diff --git a/src/test/java/com/somemore/location/service/query/LocationQueryServiceTest.java b/src/test/java/com/somemore/location/service/query/LocationQueryServiceTest.java new file mode 100644 index 000000000..61b56a877 --- /dev/null +++ b/src/test/java/com/somemore/location/service/query/LocationQueryServiceTest.java @@ -0,0 +1,96 @@ +package com.somemore.location.service.query; + +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.location.domain.Location; +import com.somemore.location.repository.LocationRepository; +import java.math.BigDecimal; +import java.util.Optional; +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 LocationQueryServiceTest extends IntegrationTestSupport { + + @Autowired + private LocationQueryService locationQueryService; + + @Autowired + private LocationRepository locationRepository; + + private Location location; + + @BeforeEach + void setUp() { + location = Location.builder() + .address("123") + .latitude(BigDecimal.valueOf(37.0)) + .longitude(BigDecimal.valueOf(127.0)) + .build(); + locationRepository.saveAndFlush(location); + } + + @AfterEach + void tearDown() { + locationRepository.deleteAllInBatch(); + } + + @DisplayName("존재하는 ID가 주어지면 Location 엔티티를 조회할 수 있다") + @Test + void findByIdWithExistsId() { + // given + Long id = location.getId(); + + // when + Optional findLocation = locationQueryService.findById(id); + + // then + assertThat(findLocation).isPresent(); + } + + @DisplayName("존재하지 않는 ID가 주어지면 빈 Optional 반환한다.") + @Test + void findByIdWithDoesNotExistId() { + // given + Long wrongId = 999L; + + // when + Optional findLocation = locationQueryService.findById(wrongId); + + // then + assertThat(findLocation).isEmpty(); + } + + @DisplayName("ID로 Location 조회할 수 있다") + @Test + void findByIdOrThrowWithExistsId() { + // given + Long id = location.getId(); + + // when + Location findLocation = locationQueryService.findByIdOrThrow(id); + + // then + assertThat(findLocation.getId()).isEqualTo(id); + } + + @DisplayName("존재하지 않는 ID로 Location 조회하면 에러가 발생한다") + @Test + void findByIdOrThrowWithDoesNotExistId() { + // given + Long wrongId = 999L; + + // when + // then + assertThatThrownBy( + () -> locationQueryService.findByIdOrThrow(wrongId) + ).isInstanceOf(BadRequestException.class); + + } + +} \ No newline at end of file diff --git a/src/test/java/com/somemore/recruitboard/domain/RecruitBoardTest.java b/src/test/java/com/somemore/recruitboard/domain/RecruitBoardTest.java index 3ef77e328..c6d04598c 100644 --- a/src/test/java/com/somemore/recruitboard/domain/RecruitBoardTest.java +++ b/src/test/java/com/somemore/recruitboard/domain/RecruitBoardTest.java @@ -3,15 +3,13 @@ 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; import java.time.LocalTime; 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 { @@ -19,79 +17,133 @@ class RecruitBoardTest { @Test void createRecruitBoardWithDefaultStatus() { // given - RecruitBoard board = RecruitBoard.builder() - .centerId(UUID.randomUUID()) - .locationId(1L) - .title("봉사모집제목") - .content("봉사모집내용") - .region("경기") - .recruitmentCount(10) - .imgUrl("https://image.domain.com/links") - .volunteerStartDateTime(LocalDateTime.now()) - .volunteerEndDateTime(LocalDateTime.now().plusHours(1)) - .volunteerType(OTHER) - .admitted(true) - .build(); + UUID centerId = UUID.randomUUID(); + RecruitBoard board = createRecruitBoard(centerId); // when RecruitStatus recruitStatus = board.getRecruitStatus(); // then + assertThat(board.getCenterId()).isEqualTo(centerId); assertThat(recruitStatus).isEqualTo(RECRUITING); } - @DisplayName("봉사 종료 시간이 시작 시간과 같거나 빠르면, 봉사 모집글 생성 시 에러가 발생한다") - @ParameterizedTest - @ValueSource(longs = {0, -1}) - void createRecruitBoardWithInValidVolunteerTime(long secondsOffset) { - // given - LocalDateTime now = LocalDateTime.now(); - LocalDateTime endDateTime = now.plusSeconds(secondsOffset); - - // when & then - assertThatThrownBy( - () -> RecruitBoard.builder() - .centerId(UUID.randomUUID()) - .locationId(1L) - .title("봉사모집제목") - .content("봉사모집내용") - .region("경기") - .recruitmentCount(10) - .imgUrl("https://image.domain.com/links") - .volunteerStartDateTime(now) - .volunteerEndDateTime(endDateTime) - .volunteerType(VolunteerType.OTHER) - .admitted(true) - .build() - ).isInstanceOf(IllegalArgumentException.class); - } - @DisplayName("봉사 시간을 계산할 수 있다") @Test void testCalculateVolunteerTime() { // given int hours = 3; + UUID centerId = UUID.randomUUID(); LocalDateTime startDateTime = LocalDateTime.now(); LocalDateTime endDateTime = startDateTime.plusHours(hours); - RecruitBoard board = RecruitBoard.builder() - .centerId(UUID.randomUUID()) - .locationId(1L) - .title("봉사모집제목") - .content("봉사모집내용") - .region("경기") + RecruitBoard board = createRecruitBoard(centerId, startDateTime, endDateTime); + + // when + LocalTime volunteerTime = board.getVolunteerHours(); + + // then + assertThat(volunteerTime).isEqualTo(LocalTime.of(hours, 0)); + } + + @DisplayName("봉사 모집글을 업데이트 할 수 있다") + @Test + void updateRecruitBoard() { + // given + UUID centerId = UUID.randomUUID(); + RecruitBoard board = createRecruitBoard(centerId); + String imgUrl = "https://image.domain.com/updates"; + + RecruitBoardUpdateRequestDto dto = RecruitBoardUpdateRequestDto.builder() + .title("봉사 모집글 작성 수정") + .content("봉사 하실분을 모집합니다. 수정
") .recruitmentCount(10) - .imgUrl("https://image.domain.com/links") + .volunteerStartDateTime(LocalDateTime.now()) + .volunteerEndDateTime(LocalDateTime.now().plusHours(2)) + .volunteerType(OTHER) + .admitted(true).build(); + + // when + board.updateWith(dto, imgUrl); + + // then + assertThat(board.getTitle()).isEqualTo(dto.title()); + assertThat(board.getContent()).isEqualTo(dto.content()); + assertThat(board.getImgUrl()).isEqualTo(imgUrl); + } + + @DisplayName("봉사 활동 지역을 수정할 수 있다.") + @Test + void updateWithRegion() { + // given + UUID centerId = UUID.randomUUID(); + RecruitBoard board = createRecruitBoard(centerId); + String updateRegion = "새로운지역"; + + // when + board.updateWith(updateRegion); + + // then + VolunteerInfo volunteerInfo = board.getVolunteerInfo(); + assertThat(volunteerInfo.getRegion()).isEqualTo(updateRegion); + } + + @DisplayName("올바른 기관 식별 값이 주어지면 작성자인지 확인할 수 있다") + @Test + void isWriterWithCorrectCenterId() { + // given + UUID centerId = UUID.randomUUID(); + RecruitBoard recruitBoard = createRecruitBoard(centerId); + + // when + boolean isWriter = recruitBoard.isWriter(centerId); + + // then + assertThat(isWriter).isTrue(); + } + + @DisplayName("잘못된 기관 식별 값이 주어지면 잘못된 작성자인 확인할 수있다.") + @Test + void isNotWriterWithWrongCenterId() { + UUID centerId = UUID.randomUUID(); + UUID wrongId = UUID.randomUUID(); + RecruitBoard recruitBoard = createRecruitBoard(centerId); + + // when + boolean isNotWriter = recruitBoard.isNotWriter(wrongId); + + // then + assertThat(isNotWriter).isTrue(); + } + + + private static RecruitBoard createRecruitBoard(UUID centerId) { + LocalDateTime startDateTime = LocalDateTime.now(); + LocalDateTime endDateTime = startDateTime.plusHours(1); + + return createRecruitBoard(centerId, startDateTime, endDateTime); + } + + private static RecruitBoard createRecruitBoard(UUID centerId, LocalDateTime startDateTime, + LocalDateTime endDateTime) { + + VolunteerInfo volunteerInfo = VolunteerInfo.builder() + .region("경기") + .recruitmentCount(1) .volunteerStartDateTime(startDateTime) .volunteerEndDateTime(endDateTime) .volunteerType(OTHER) .admitted(true) .build(); - // when - LocalTime volunteerTime = board.calculateVolunteerTime(); - - // then - assertThat(volunteerTime).isEqualTo(LocalTime.of(hours, 0)); + return RecruitBoard.builder() + .centerId(centerId) + .locationId(1L) + .title("봉사모집제목") + .content("봉사모집내용") + .imgUrl("https://image.domain.com/links") + .volunteerInfo(volunteerInfo) + .build(); } + } \ No newline at end of file diff --git a/src/test/java/com/somemore/recruitboard/domain/VolunteerInfoTest.java b/src/test/java/com/somemore/recruitboard/domain/VolunteerInfoTest.java new file mode 100644 index 000000000..d19014972 --- /dev/null +++ b/src/test/java/com/somemore/recruitboard/domain/VolunteerInfoTest.java @@ -0,0 +1,120 @@ +package com.somemore.recruitboard.domain; + +import static com.somemore.recruitboard.domain.VolunteerType.ADMINISTRATIVE_SUPPORT; +import static com.somemore.recruitboard.domain.VolunteerType.SAFETY_PREVENTION; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.LocalDateTime; +import java.time.LocalTime; +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 VolunteerInfoTest { + + @DisplayName("봉사 종료 시간이 시작 시간과 같거나 빠르면, 봉사 모집글 생성 시 에러가 발생한다") + @ParameterizedTest + @ValueSource(longs = {0, -1}) + void createRecruitBoardWithInValidVolunteerTime(long secondsOffset) { + // given + LocalDateTime now = LocalDateTime.now(); + LocalDateTime endDateTime = now.plusSeconds(secondsOffset); + + // when & then + assertThatThrownBy( + () -> createVolunteerInfo(now, endDateTime) + ).isInstanceOf(IllegalArgumentException.class); + + } + + @DisplayName("봉사 시간을 계산할 수 있다") + @Test + void testCalculateVolunteerTime() { + // given + int hours = 3; + LocalDateTime startDateTime = LocalDateTime.now(); + LocalDateTime endDateTime = startDateTime.plusHours(hours); + + VolunteerInfo volunteerInfo = createVolunteerInfo(startDateTime, endDateTime); + // when + LocalTime volunteerTime = volunteerInfo.calculateVolunteerTime(); + + // then + assertThat(volunteerTime).isEqualTo(LocalTime.of(hours, 0)); + } + + @DisplayName("봉사 활동 정보를 업데이트 할 수 있다") + @Test + void updateVolunteerInfo() { + // given + VolunteerInfo volunteerInfo = createVolunteerInfo(); + + Integer count = 2; + VolunteerType volunteerType = SAFETY_PREVENTION; + LocalDateTime startDateTime = LocalDateTime.now(); + LocalDateTime endDateTime = startDateTime.plusHours(2); + Boolean admitted = false; + + // when + volunteerInfo.updateWith(count, volunteerType, startDateTime, + endDateTime, admitted); + + // then + assertThat(volunteerInfo.getRecruitmentCount()).isEqualTo(count); + assertThat(volunteerInfo.getVolunteerType()).isEqualTo(volunteerType); + assertThat(volunteerInfo.getVolunteerStartDateTime().compareTo(startDateTime)).isZero(); + assertThat(volunteerInfo.getVolunteerEndDateTime().compareTo(endDateTime)).isZero(); + assertThat(volunteerInfo.getAdmitted()).isEqualTo(admitted); + } + + @DisplayName("봉사활동 지역 정보를 업데이트할 수 있다") + @Test + void updateVolunteerInfoWithRegion() { + // given + VolunteerInfo volunteerInfo = createVolunteerInfo(); + String updateRegion = "새로운지역"; + + // when + volunteerInfo.updateWith(updateRegion); + + // then + assertThat(volunteerInfo.getRegion()).isEqualTo(updateRegion); + } + + @DisplayName("봉사 종료 시간이 시작 시간과 같거나 빠르면, 봉사 모집글을 업데이트시 에러가 발생한다") + @ParameterizedTest + @ValueSource(longs = {0, -1}) + void updateRecruitBoardWithInValidVolunteerTime(long secondsOffset) { + // given + LocalDateTime startDateTime = LocalDateTime.now(); + LocalDateTime endDateTime = startDateTime.plusSeconds(secondsOffset); + + VolunteerInfo volunteerInfo = createVolunteerInfo(); + + // when & then + assertThatThrownBy( + () -> volunteerInfo.updateWith(3, ADMINISTRATIVE_SUPPORT, startDateTime, endDateTime, + false) + ).isInstanceOf(IllegalArgumentException.class); + + } + + private static VolunteerInfo createVolunteerInfo(LocalDateTime startDateTime, + LocalDateTime endDateTime) { + return VolunteerInfo.builder() + .region("경기") + .recruitmentCount(1) + .volunteerStartDateTime(startDateTime) + .volunteerEndDateTime(endDateTime) + .volunteerType(VolunteerType.OTHER) + .admitted(true) + .build(); + } + + private static VolunteerInfo createVolunteerInfo() { + return createVolunteerInfo(LocalDateTime.now(), LocalDateTime.now().plusHours(1)); + } + +} diff --git a/src/test/java/com/somemore/recruitboard/service/command/UpdateRecruitBoardServiceTest.java b/src/test/java/com/somemore/recruitboard/service/command/UpdateRecruitBoardServiceTest.java new file mode 100644 index 000000000..826978c50 --- /dev/null +++ b/src/test/java/com/somemore/recruitboard/service/command/UpdateRecruitBoardServiceTest.java @@ -0,0 +1,189 @@ +package com.somemore.recruitboard.service.command; + +import static com.somemore.recruitboard.domain.VolunteerType.ADMINISTRATIVE_SUPPORT; +import static com.somemore.recruitboard.domain.VolunteerType.OTHER; +import static org.assertj.core.api.Assertions.assertThat; + +import com.somemore.IntegrationTestSupport; +import com.somemore.global.exception.BadRequestException; +import com.somemore.location.domain.Location; +import com.somemore.location.repository.LocationRepository; +import com.somemore.recruitboard.domain.RecruitBoard; +import com.somemore.recruitboard.domain.VolunteerInfo; +import com.somemore.recruitboard.dto.request.RecruitBoardLocationUpdateRequestDto; +import com.somemore.recruitboard.dto.request.RecruitBoardUpdateRequestDto; +import com.somemore.recruitboard.repository.RecruitBoardRepository; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; +import org.assertj.core.api.Assertions; +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 UpdateRecruitBoardServiceTest extends IntegrationTestSupport { + + @Autowired + private UpdateRecruitBoardService updateRecruitBoardService; + + @Autowired + private RecruitBoardRepository recruitBoardRepository; + + @Autowired + private LocationRepository locationRepository; + + private RecruitBoard recruitBoard; + private Location location; + private UUID centerId; + + @BeforeEach + void setUp() { + location = createLocation(); + locationRepository.saveAndFlush(location); + centerId = UUID.randomUUID(); + recruitBoard = createRecruitBoard(centerId, location.getId()); + recruitBoardRepository.saveAndFlush(recruitBoard); + + } + + + @AfterEach + void tearDown() { + recruitBoardRepository.deleteAllInBatch(); + locationRepository.deleteAllInBatch(); + } + + @DisplayName("봉사 모집글의 데이터를 업데이트하면 저장소에 반영된다.") + @Test + void updateRecruitBoard() { + // given + LocalDateTime newStartDateTime = LocalDateTime.now(); + LocalDateTime newEndDateTime = newStartDateTime.plusHours(3); + String newImgUrl = "https://image.domain.com/updates"; + RecruitBoardUpdateRequestDto dto = RecruitBoardUpdateRequestDto.builder() + .title("업데이트 제목") + .content("업데이트 내용") + .recruitmentCount(1111) + .volunteerStartDateTime(newStartDateTime) + .volunteerEndDateTime(newEndDateTime) + .volunteerType(ADMINISTRATIVE_SUPPORT) + .admitted(false) + .build(); + + // when + updateRecruitBoardService.updateRecruitBoard(dto, recruitBoard.getId(), centerId, + newImgUrl); + + // then + RecruitBoard updatedRecruitBoard = recruitBoardRepository.findById(recruitBoard.getId()) + .orElseThrow(); + + assertThat(updatedRecruitBoard.getTitle()).isEqualTo(dto.title()); + assertThat(updatedRecruitBoard.getContent()).isEqualTo(dto.content()); + assertThat(updatedRecruitBoard.getImgUrl()).isEqualTo(newImgUrl); + + VolunteerInfo volunteerInfo = updatedRecruitBoard.getVolunteerInfo(); + assertThat(volunteerInfo.getRecruitmentCount()).isEqualTo(dto.recruitmentCount()); + assertThat(volunteerInfo.getVolunteerStartDateTime() + .compareTo(dto.volunteerStartDateTime())).isZero(); + assertThat(volunteerInfo.getVolunteerEndDateTime() + .compareTo(dto.volunteerEndDateTime())).isZero(); + assertThat(volunteerInfo.getVolunteerType()).isEqualTo(dto.volunteerType()); + assertThat(volunteerInfo.getAdmitted()).isEqualTo(dto.admitted()); + } + + @DisplayName("봉사 모집글 위치를 수정할 수 있다") + @Test + void updateRecruitBoardLocation() { + // given + RecruitBoardLocationUpdateRequestDto dto = RecruitBoardLocationUpdateRequestDto.builder() + .region("새로새로지역지역") + .address("새로새로주소주소") + .latitude(BigDecimal.valueOf(37.2222222)) + .longitude(BigDecimal.valueOf(127.2222222)) + .build(); + + // when + updateRecruitBoardService.updateRecruitBoardLocation(dto, recruitBoard.getId(), centerId); + + // then + RecruitBoard updateRecruitBoard = recruitBoardRepository.findById(recruitBoard.getId()) + .orElseThrow(); + Location updateLocation = locationRepository.findById(recruitBoard.getLocationId()) + .orElseThrow(); + + assertThat(updateRecruitBoard.getVolunteerInfo().getRegion()).isEqualTo(dto.region()); + assertThat(updateLocation.getAddress()).isEqualTo(dto.address()); + assertThat(updateLocation.getLongitude().compareTo(dto.longitude())).isZero(); + assertThat(updateLocation.getLatitude().compareTo(dto.latitude())).isZero(); + } + + @DisplayName("봉사 모집글은 작성자만 수정할 수 있다") + @Test + void updateRecruitBoardWhenCenterIdIsWrong() { + // given + UUID wrongCenterId = UUID.randomUUID(); + LocalDateTime newStartDateTime = LocalDateTime.now(); + LocalDateTime newEndDateTime = newStartDateTime.plusHours(3); + String newImgUrl = "https://image.domain.com/updates"; + RecruitBoardUpdateRequestDto dto = RecruitBoardUpdateRequestDto.builder() + .title("업데이트 제목") + .content("업데이트 내용") + .recruitmentCount(1111) + .volunteerStartDateTime(newStartDateTime) + .volunteerEndDateTime(newEndDateTime) + .volunteerType(ADMINISTRATIVE_SUPPORT) + .admitted(false) + .build(); + + // when + // then + Assertions.assertThatThrownBy( + () -> updateRecruitBoardService.updateRecruitBoard(dto, recruitBoard.getId(), + wrongCenterId, + newImgUrl) + ).isInstanceOf(BadRequestException.class); + + } + + + private static RecruitBoard createRecruitBoard(UUID centerId, Long locationId) { + LocalDateTime startDateTime = LocalDateTime.now(); + LocalDateTime endDateTime = startDateTime.plusHours(1); + + return createRecruitBoard(centerId, locationId, startDateTime, endDateTime); + } + + private static RecruitBoard createRecruitBoard(UUID centerId, Long locationId, + LocalDateTime startDateTime, + LocalDateTime endDateTime) { + + VolunteerInfo volunteerInfo = VolunteerInfo.builder() + .region("경기") + .recruitmentCount(1) + .volunteerStartDateTime(startDateTime) + .volunteerEndDateTime(endDateTime) + .volunteerType(OTHER) + .admitted(true) + .build(); + + return RecruitBoard.builder() + .centerId(centerId) + .locationId(locationId) + .title("봉사모집제목") + .content("봉사모집내용") + .imgUrl("https://image.domain.com/links") + .volunteerInfo(volunteerInfo) + .build(); + } + + private static Location createLocation() { + return Location.builder() + .address("주소주소") + .longitude(BigDecimal.valueOf(37.11111)) + .latitude(BigDecimal.valueOf(127.11111)) + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/com/somemore/recruitboard/service/query/RecruitQueryServiceTest.java b/src/test/java/com/somemore/recruitboard/service/query/RecruitQueryServiceTest.java new file mode 100644 index 000000000..077adca3c --- /dev/null +++ b/src/test/java/com/somemore/recruitboard/service/query/RecruitQueryServiceTest.java @@ -0,0 +1,118 @@ +package com.somemore.recruitboard.service.query; + +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.VolunteerInfo; +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 RecruitQueryServiceTest extends IntegrationTestSupport { + + @Autowired + private RecruitQueryService recruitQueryService; + + @Autowired + private RecruitBoardRepository recruitBoardRepository; + + private RecruitBoard recruitBoard; + + @BeforeEach + void setUp() { + recruitBoard = createRecruitBoard(); + recruitBoardRepository.saveAndFlush(recruitBoard); + } + + @AfterEach + void tearDown() { + recruitBoardRepository.deleteAllInBatch(); + } + + @DisplayName("존재하는 ID가 주어지면 RecruitBoard 엔티티를 조회할 수 있다") + @Test + void findByIdWithExistsId() { + // given + Long id = recruitBoard.getId(); + + // when + Optional findBoard = recruitQueryService.findById(id); + + // then + assertThat(findBoard).isPresent(); + } + + @DisplayName("존재하지 않는 ID가 주어지면 빈 Optional 반환한다.") + @Test + void findByIdWithDoesNotExistId() { + // given + Long wrongId = 999L; + + // when + Optional findBoard = recruitQueryService.findById(wrongId); + + // then + assertThat(findBoard).isEmpty(); + } + + @DisplayName("ID로 RecruitBoard 조회할 수 있다") + @Test + void findByIdOrThrowWithExistsId() { + // given + Long id = recruitBoard.getId(); + + // when + RecruitBoard findBoard = recruitQueryService.findByIdOrThrow(id); + + // then + assertThat(findBoard.getId()).isEqualTo(id); + } + + @DisplayName("존재하지 않는 ID로 RecruitBoard 조회하면 에러가 발생한다") + @Test + void findByIdOrThrowWithDoesNotExistId() { + // given + Long wrongId = 999L; + + // when + // then + assertThatThrownBy( + () -> recruitQueryService.findByIdOrThrow(wrongId) + ).isInstanceOf(BadRequestException.class); + + } + + private static RecruitBoard createRecruitBoard() { + + LocalDateTime startDateTime = LocalDateTime.now(); + LocalDateTime endDateTime = startDateTime.plusHours(1); + + VolunteerInfo volunteerInfo = VolunteerInfo.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") + .volunteerInfo(volunteerInfo) + .build(); + } +} \ No newline at end of file