diff --git a/src/main/java/com/somemore/global/common/BaseEntity.java b/src/main/java/com/somemore/global/common/BaseEntity.java index 8f7aafc2d..4bf05c604 100644 --- a/src/main/java/com/somemore/global/common/BaseEntity.java +++ b/src/main/java/com/somemore/global/common/BaseEntity.java @@ -33,4 +33,8 @@ public void prePersist() { this.deleted = false; } } + + public void markAsDeleted() { + this.deleted = true; + } } diff --git a/src/main/java/com/somemore/global/exception/ExceptionMessage.java b/src/main/java/com/somemore/global/exception/ExceptionMessage.java index 615f84ac0..546c529c5 100644 --- a/src/main/java/com/somemore/global/exception/ExceptionMessage.java +++ b/src/main/java/com/somemore/global/exception/ExceptionMessage.java @@ -8,7 +8,10 @@ @Getter public enum ExceptionMessage { - NOT_EXISTS_CENTER("존재하지 않는 기관 ID 입니다.") + NOT_EXISTS_CENTER("존재하지 않는 기관 ID 입니다."), + NOT_EXISTS_LOCATION("존재하지 않는 위치 ID 입니다."), + NOT_EXISTS_RECRUIT_BOARD("존재하지 않는 봉사 모집글 ID 입니다."), + UNAUTHORIZED_RECRUIT_BOARD("자신이 작성한 봉사 모집글이 아닙니다."), ; private final String message; diff --git a/src/main/java/com/somemore/location/domain/Location.java b/src/main/java/com/somemore/location/domain/Location.java index 3ac082619..d2d474c39 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); } -} \ No newline at end of file + + 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); + } +} 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..6c5185034 --- /dev/null +++ b/src/main/java/com/somemore/location/service/command/UpdateLocationService.java @@ -0,0 +1,30 @@ +package com.somemore.location.service.command; + +import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_LOCATION; + +import com.somemore.global.exception.BadRequestException; +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.findById(locationId) + .orElseThrow(() -> new BadRequestException(NOT_EXISTS_LOCATION.getMessage())); + 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..266811cdf --- /dev/null +++ b/src/main/java/com/somemore/location/service/query/LocationQueryService.java @@ -0,0 +1,23 @@ +package com.somemore.location.service.query; + +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); + } + +} 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..b1e468da0 --- /dev/null +++ b/src/main/java/com/somemore/location/usecase/query/LocationQueryUseCase.java @@ -0,0 +1,10 @@ +package com.somemore.location.usecase.query; + +import com.somemore.location.domain.Location; +import java.util.Optional; + +public interface LocationQueryUseCase { + + Optional findById(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..0b3115f3f 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,53 @@ 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 RecruitmentInfo recruitmentInfo; @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, + RecruitmentInfo recruitmentInfo, String imgUrl) { this.centerId = centerId; this.locationId = locationId; this.title = title; this.content = content; - this.region = region; - this.recruitmentCount = recruitmentCount; + this.recruitmentInfo = recruitmentInfo; 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 recruitmentInfo.calculateVolunteerTime(); + } + + public boolean isWriter(UUID centerId) { + return this.centerId.equals(centerId); + } - long hours = duration.toHours(); - long minutes = duration.toMinutes() % 60; + public void updateWith(RecruitBoardUpdateRequestDto dto, String imgUrl) { + updateRecruitmentInfo(dto); + this.title = dto.title(); + this.content = dto.content(); + this.imgUrl = imgUrl; + } - return LocalTime.of((int) hours, (int) minutes); + public void updateWith(String region) { + recruitmentInfo.updateWith(region); } - private void validateVolunteerDateTime(LocalDateTime startDateTime, LocalDateTime endDateTime) { - if (endDateTime.isEqual(startDateTime) || endDateTime.isBefore(startDateTime)) { - throw new IllegalArgumentException("종료 시간은 시작 시간보다 이후여야 합니다."); - } + private void updateRecruitmentInfo(RecruitBoardUpdateRequestDto dto) { + recruitmentInfo.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/RecruitmentInfo.java b/src/main/java/com/somemore/recruitboard/domain/RecruitmentInfo.java new file mode 100644 index 000000000..1b50bee28 --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/domain/RecruitmentInfo.java @@ -0,0 +1,87 @@ +package com.somemore.recruitboard.domain; + +import static jakarta.persistence.EnumType.STRING; +import static java.time.temporal.ChronoUnit.MINUTES; +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 RecruitmentInfo { + + @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 RecruitmentInfo(String region, Integer recruitmentCount, + LocalDateTime volunteerStartDateTime, LocalDateTime volunteerEndDateTime, + VolunteerType volunteerType, Boolean admitted) { + + validateVolunteerDateTime(volunteerStartDateTime, volunteerEndDateTime); + + this.region = region; + this.recruitmentCount = recruitmentCount; + this.volunteerStartDateTime = volunteerStartDateTime.truncatedTo(MINUTES); + this.volunteerEndDateTime = volunteerEndDateTime.truncatedTo(MINUTES); + 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.truncatedTo(MINUTES); + this.volunteerEndDateTime = volunteerEndDateTime.truncatedTo(MINUTES); + 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..8906c48a4 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.RecruitmentInfo; 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) { - return RecruitBoard.builder() - .centerId(centerId) - .locationId(locationId) - .title(title) - .content(content) + RecruitmentInfo recruitmentInfo = RecruitmentInfo.builder() .region(region) .recruitmentCount(recruitmentCount) - .imgUrl(imgUrl) .volunteerStartDateTime(volunteerStartDateTime) .volunteerEndDateTime(volunteerEndDateTime) .volunteerType(volunteerType) .admitted(admitted) .build(); + + return RecruitBoard.builder() + .centerId(centerId) + .locationId(locationId) + .title(title) + .content(content) + .imgUrl(imgUrl) + .recruitmentInfo(recruitmentInfo) + .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..3db97c1b6 --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/repository/RecruitBoardRepositoryImpl.java @@ -0,0 +1,49 @@ +package com.somemore.recruitboard.repository; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.somemore.recruitboard.domain.QRecruitBoard; +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) { + QRecruitBoard recruitBoard = QRecruitBoard.recruitBoard; + + RecruitBoard result = queryFactory + .selectFrom(recruitBoard) + .where(isNotDeleted().and(recruitBoard.id.eq(id))) + .fetchOne(); + + return Optional.ofNullable(result); + } + + @Override + public void deleteAllInBatch() { + recruitBoardJpaRepository.deleteAllInBatch(); + } + + private BooleanExpression isNotDeleted() { + QRecruitBoard recruitBoard = QRecruitBoard.recruitBoard; + return recruitBoard.deleted.eq(false); + } +} 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..1abaf0c02 --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/service/command/UpdateRecruitBoardService.java @@ -0,0 +1,68 @@ +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.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.RecruitBoardQueryUseCase; +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 RecruitBoardQueryUseCase recruitBoardQueryUseCase; + private final UpdateLocationUseCase updateLocationUseCase; + + @Override + public void updateRecruitBoard( + RecruitBoardUpdateRequestDto requestDto, + Long recruitBoardId, + UUID centerId, + String imgUrl) { + + RecruitBoard recruitBoard = recruitBoardQueryUseCase.findById(recruitBoardId).orElseThrow( + () -> new BadRequestException(NOT_EXISTS_RECRUIT_BOARD.getMessage()) + ); + validateWriter(recruitBoard, centerId); + recruitBoard.updateWith(requestDto, imgUrl); + + recruitBoardRepository.save(recruitBoard); + } + + @Override + public void updateRecruitBoardLocation(RecruitBoardLocationUpdateRequestDto requestDto, + Long recruitBoardId, UUID centerId) { + + RecruitBoard recruitBoard = recruitBoardQueryUseCase.findById(recruitBoardId).orElseThrow( + () -> new BadRequestException(NOT_EXISTS_RECRUIT_BOARD.getMessage()) + ); + 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.isWriter(centerId)) { + return; + } + + throw new BadRequestException(UNAUTHORIZED_RECRUIT_BOARD.getMessage()); + } +} diff --git a/src/main/java/com/somemore/recruitboard/service/query/RecruitBoardQueryService.java b/src/main/java/com/somemore/recruitboard/service/query/RecruitBoardQueryService.java new file mode 100644 index 000000000..e3c9dffd1 --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/service/query/RecruitBoardQueryService.java @@ -0,0 +1,23 @@ +package com.somemore.recruitboard.service.query; + +import com.somemore.recruitboard.domain.RecruitBoard; +import com.somemore.recruitboard.repository.RecruitBoardRepository; +import com.somemore.recruitboard.usecase.query.RecruitBoardQueryUseCase; +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 RecruitBoardQueryService implements RecruitBoardQueryUseCase { + + private final RecruitBoardRepository recruitBoardRepository; + + @Override + public Optional findById(Long id) { + return recruitBoardRepository.findById(id); + } + +} 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/RecruitBoardQueryUseCase.java b/src/main/java/com/somemore/recruitboard/usecase/query/RecruitBoardQueryUseCase.java new file mode 100644 index 000000000..b98576d79 --- /dev/null +++ b/src/main/java/com/somemore/recruitboard/usecase/query/RecruitBoardQueryUseCase.java @@ -0,0 +1,10 @@ +package com.somemore.recruitboard.usecase.query; + +import com.somemore.recruitboard.domain.RecruitBoard; +import java.util.Optional; + +public interface RecruitBoardQueryUseCase { + + Optional findById(Long id); + +} diff --git a/src/test/java/com/somemore/common/fixture/LocalDateTimeFixture.java b/src/test/java/com/somemore/common/fixture/LocalDateTimeFixture.java new file mode 100644 index 000000000..6bf9370e5 --- /dev/null +++ b/src/test/java/com/somemore/common/fixture/LocalDateTimeFixture.java @@ -0,0 +1,20 @@ +package com.somemore.common.fixture; + +import java.time.LocalDateTime; + +public class LocalDateTimeFixture { + + private LocalDateTimeFixture() { + } + + public static LocalDateTime createStartDateTime() { + // 2024-11-25 T:13:00:00 + return LocalDateTime.of(2024, 11, 25, 13, 0); + } + + public static LocalDateTime createUpdateStartDateTime() { + // 2024-11-25 T:16:00:00 + return LocalDateTime.of(2024, 11, 25, 16, 0); + } + +} 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..4d698e052 --- /dev/null +++ b/src/test/java/com/somemore/location/domain/LocationTest.java @@ -0,0 +1,60 @@ +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(); + } +} 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/CreateLocationServiceTest.java b/src/test/java/com/somemore/location/service/command/CreateLocationServiceTest.java index e0d85fd56..8ff85f49c 100644 --- a/src/test/java/com/somemore/location/service/command/CreateLocationServiceTest.java +++ b/src/test/java/com/somemore/location/service/command/CreateLocationServiceTest.java @@ -43,8 +43,5 @@ void createLocationWithCreateRequestDto() { Optional location = locationRepository.findById(locationId); assertThat(location).isPresent(); assertThat(location.get().getId()).isEqualTo(locationId); - } - - -} \ No newline at end of file +} 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..9c815a5a6 --- /dev/null +++ b/src/test/java/com/somemore/location/service/command/UpdateLocationServiceTest.java @@ -0,0 +1,65 @@ +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()) + .isEqualByComparingTo(dto.latitude()); + assertThat(updateLocation.getLongitude()) + .isEqualByComparingTo(dto.longitude()); + } +} 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..389c34418 --- /dev/null +++ b/src/test/java/com/somemore/location/service/query/LocationQueryServiceTest.java @@ -0,0 +1,67 @@ +package com.somemore.location.service.query; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.somemore.IntegrationTestSupport; +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(); + } + +} diff --git a/src/test/java/com/somemore/recruitboard/domain/RecruitBoardTest.java b/src/test/java/com/somemore/recruitboard/domain/RecruitBoardTest.java index 3ef77e328..4decdd19b 100644 --- a/src/test/java/com/somemore/recruitboard/domain/RecruitBoardTest.java +++ b/src/test/java/com/somemore/recruitboard/domain/RecruitBoardTest.java @@ -1,17 +1,17 @@ package com.somemore.recruitboard.domain; +import static com.somemore.common.fixture.LocalDateTimeFixture.createStartDateTime; +import static com.somemore.common.fixture.LocalDateTimeFixture.createUpdateStartDateTime; 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 +19,134 @@ 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; - LocalDateTime startDateTime = LocalDateTime.now(); + UUID centerId = UUID.randomUUID(); + LocalDateTime startDateTime = createStartDateTime(); 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"; + LocalDateTime startDateTime = createUpdateStartDateTime(); + LocalDateTime endDateTime = startDateTime.plusHours(2); + + RecruitBoardUpdateRequestDto dto = RecruitBoardUpdateRequestDto.builder() + .title("봉사 모집글 작성 수정") + .content("봉사 하실분을 모집합니다. 수정
") .recruitmentCount(10) - .imgUrl("https://image.domain.com/links") .volunteerStartDateTime(startDateTime) .volunteerEndDateTime(endDateTime) .volunteerType(OTHER) - .admitted(true) - .build(); + .admitted(true).build(); // when - LocalTime volunteerTime = board.calculateVolunteerTime(); + board.updateWith(dto, imgUrl); // then - assertThat(volunteerTime).isEqualTo(LocalTime.of(hours, 0)); + 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 + RecruitmentInfo recruitmentInfo = board.getRecruitmentInfo(); + assertThat(recruitmentInfo.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 isWriter = recruitBoard.isWriter(wrongId); + + // then + assertThat(isWriter).isFalse(); + } + + + private static RecruitBoard createRecruitBoard(UUID centerId) { + LocalDateTime startDateTime = createStartDateTime(); + LocalDateTime endDateTime = startDateTime.plusHours(1); + + return createRecruitBoard(centerId, startDateTime, endDateTime); + } + + private static RecruitBoard createRecruitBoard(UUID centerId, LocalDateTime startDateTime, + LocalDateTime endDateTime) { + + RecruitmentInfo recruitmentInfo = RecruitmentInfo.builder() + .region("경기") + .recruitmentCount(1) + .volunteerStartDateTime(startDateTime) + .volunteerEndDateTime(endDateTime) + .volunteerType(OTHER) + .admitted(true) + .build(); + + return RecruitBoard.builder() + .centerId(centerId) + .locationId(1L) + .title("봉사모집제목") + .content("봉사모집내용") + .imgUrl("https://image.domain.com/links") + .recruitmentInfo(recruitmentInfo) + .build(); } -} \ No newline at end of file +} diff --git a/src/test/java/com/somemore/recruitboard/domain/RecruitmentInfoTest.java b/src/test/java/com/somemore/recruitboard/domain/RecruitmentInfoTest.java new file mode 100644 index 000000000..df46af4eb --- /dev/null +++ b/src/test/java/com/somemore/recruitboard/domain/RecruitmentInfoTest.java @@ -0,0 +1,125 @@ +package com.somemore.recruitboard.domain; + +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; +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 RecruitmentInfoTest { + + @DisplayName("봉사 종료 시간이 시작 시간과 같거나 빠르면, 봉사 모집글 생성 시 에러가 발생한다") + @ParameterizedTest + @ValueSource(longs = {0, -1}) + void createRecruitBoardWithInValidVolunteerTime(long minutesOffset) { + // given + LocalDateTime now = createStartDateTime(); + LocalDateTime endDateTime = now.plusMinutes(minutesOffset); + + // when & then + assertThatThrownBy( + () -> createRecruitmentInfo(now, endDateTime) + ).isInstanceOf(IllegalArgumentException.class); + + } + + @DisplayName("봉사 시간을 계산할 수 있다") + @Test + void testCalculateVolunteerTime() { + // given + int hours = 3; + LocalDateTime startDateTime = createStartDateTime(); + LocalDateTime endDateTime = startDateTime.plusHours(hours); + + RecruitmentInfo recruitmentInfo = createRecruitmentInfo(startDateTime, endDateTime); + + // when + LocalTime volunteerTime = recruitmentInfo.calculateVolunteerTime(); + + // then + assertThat(volunteerTime).isEqualTo(LocalTime.of(hours, 0)); + } + + @DisplayName("봉사 활동 정보를 업데이트 할 수 있다") + @Test + void updateRecruitmentInfo() { + // given + RecruitmentInfo recruitmentInfo = createRecruitmentInfo(); + + Integer count = 2; + VolunteerType volunteerType = SAFETY_PREVENTION; + LocalDateTime startDateTime = createUpdateStartDateTime(); + LocalDateTime endDateTime = startDateTime.plusHours(2); + Boolean admitted = false; + + // when + recruitmentInfo.updateWith(count, volunteerType, startDateTime, + endDateTime, admitted); + + // then + assertThat(recruitmentInfo.getRecruitmentCount()).isEqualTo(count); + assertThat(recruitmentInfo.getVolunteerType()).isEqualTo(volunteerType); + assertThat(recruitmentInfo.getVolunteerStartDateTime().compareTo(startDateTime)).isZero(); + assertThat(recruitmentInfo.getVolunteerEndDateTime().compareTo(endDateTime)).isZero(); + assertThat(recruitmentInfo.getAdmitted()).isEqualTo(admitted); + } + + @DisplayName("봉사활동 지역 정보를 업데이트할 수 있다") + @Test + void updateRecruitmentInfoWithRegion() { + // given + RecruitmentInfo recruitmentInfo = createRecruitmentInfo(); + String updateRegion = "새로운지역"; + + // when + recruitmentInfo.updateWith(updateRegion); + + // then + assertThat(recruitmentInfo.getRegion()).isEqualTo(updateRegion); + } + + @DisplayName("봉사 종료 시간이 시작 시간과 같거나 빠르면, 봉사 모집글을 업데이트시 에러가 발생한다") + @ParameterizedTest + @ValueSource(longs = {0, -1}) + void updateRecruitBoardWithInValidVolunteerTime(long minutesOffset) { + // given + LocalDateTime startDateTime = createStartDateTime(); + LocalDateTime endDateTime = startDateTime.plusMinutes(minutesOffset); + + RecruitmentInfo recruitmentInfo = createRecruitmentInfo(); + + // when & then + assertThatThrownBy( + () -> recruitmentInfo.updateWith(3, ADMINISTRATIVE_SUPPORT, startDateTime, endDateTime, + false) + ).isInstanceOf(IllegalArgumentException.class); + + } + + private static RecruitmentInfo createRecruitmentInfo(LocalDateTime startDateTime, + LocalDateTime endDateTime) { + return RecruitmentInfo.builder() + .region("경기") + .recruitmentCount(1) + .volunteerStartDateTime(startDateTime) + .volunteerEndDateTime(endDateTime) + .volunteerType(VolunteerType.OTHER) + .admitted(true) + .build(); + } + + private static RecruitmentInfo createRecruitmentInfo() { + LocalDateTime startDateTime = createStartDateTime(); + LocalDateTime endDateTime = startDateTime.plusHours(1); + return createRecruitmentInfo(startDateTime, endDateTime); + } + +} diff --git a/src/test/java/com/somemore/recruitboard/repository/RecruitBoardRepositoryImplTest.java b/src/test/java/com/somemore/recruitboard/repository/RecruitBoardRepositoryImplTest.java new file mode 100644 index 000000000..bc4383d9a --- /dev/null +++ b/src/test/java/com/somemore/recruitboard/repository/RecruitBoardRepositoryImplTest.java @@ -0,0 +1,75 @@ +package com.somemore.recruitboard.repository; + +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 com.somemore.IntegrationTestSupport; +import com.somemore.recruitboard.domain.RecruitBoard; +import com.somemore.recruitboard.domain.RecruitmentInfo; +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 RecruitBoardRepositoryImplTest extends IntegrationTestSupport { + + @Autowired + private RecruitBoardRepositoryImpl recruitBoardRepository; + + private RecruitBoard recruitBoard; + + @BeforeEach + void setUp() { + recruitBoard = createRecruitBoard(); + recruitBoardRepository.saveAndFlush(recruitBoard); + recruitBoard.markAsDeleted(); + recruitBoardRepository.saveAndFlush(recruitBoard); + } + + @AfterEach + void tearDown() { + recruitBoardRepository.deleteAllInBatch(); + } + + @DisplayName("논리 삭제된 데이터를 id로 조회시 빈 Optional 반환된다") + @Test + void findById() { + // given + Long deletedId = recruitBoard.getId(); + + // when + Optional findBoard = recruitBoardRepository.findById(deletedId); + + // then + assertThat(findBoard).isEmpty(); + } + + 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/CreateRecruitBoardServiceTest.java b/src/test/java/com/somemore/recruitboard/service/command/CreateRecruitBoardServiceTest.java index c97271866..894aa05c7 100644 --- a/src/test/java/com/somemore/recruitboard/service/command/CreateRecruitBoardServiceTest.java +++ b/src/test/java/com/somemore/recruitboard/service/command/CreateRecruitBoardServiceTest.java @@ -1,5 +1,6 @@ package com.somemore.recruitboard.service.command; +import static com.somemore.common.fixture.LocalDateTimeFixture.createStartDateTime; import static org.assertj.core.api.Assertions.assertThat; import com.somemore.IntegrationTestSupport; @@ -45,13 +46,16 @@ void createRecruitBoardWithDto() { .longitude(BigDecimal.valueOf(127.010842267696)) .build(); + LocalDateTime startDateTime = createStartDateTime(); + LocalDateTime endDateTime = startDateTime.plusHours(2); + RecruitBoardCreateRequestDto dto = RecruitBoardCreateRequestDto.builder() .title("봉사 모집글 작성") .content("봉사 하실분을 모집합니다.
") .region("지역") .recruitmentCount(10) - .volunteerStartDateTime(LocalDateTime.now()) - .volunteerEndDateTime(LocalDateTime.now().plusHours(2)) + .volunteerStartDateTime(startDateTime) + .volunteerEndDateTime(endDateTime) .volunteerType(VolunteerType.OTHER) .admitted(true) .location(locationDto) 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..81b95a277 --- /dev/null +++ b/src/test/java/com/somemore/recruitboard/service/command/UpdateRecruitBoardServiceTest.java @@ -0,0 +1,190 @@ +package com.somemore.recruitboard.service.command; + +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; +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.RecruitmentInfo; +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 = createUpdateStartDateTime(); + 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); + + RecruitmentInfo recruitmentInfo = updatedRecruitBoard.getRecruitmentInfo(); + assertThat(recruitmentInfo.getRecruitmentCount()).isEqualTo(dto.recruitmentCount()); + assertThat(recruitmentInfo.getVolunteerType()).isEqualTo(dto.volunteerType()); + assertThat(recruitmentInfo.getAdmitted()).isEqualTo(dto.admitted()); + + assertThat(recruitmentInfo.getVolunteerStartDateTime()) + .isEqualToIgnoringNanos(dto.volunteerStartDateTime()); + assertThat(recruitmentInfo.getVolunteerEndDateTime()) + .isEqualToIgnoringNanos(dto.volunteerEndDateTime()); + } + + @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.getRecruitmentInfo().getRegion()).isEqualTo(dto.region()); + assertThat(updateLocation.getAddress()).isEqualTo(dto.address()); + assertThat(updateLocation.getLongitude()) + .isEqualByComparingTo(dto.longitude()); + assertThat(updateLocation.getLatitude()) + .isEqualByComparingTo(dto.latitude()); + } + + @DisplayName("봉사 모집글은 작성자만 수정할 수 있다") + @Test + void updateRecruitBoardWhenCenterIdIsWrong() { + // given + Long id = recruitBoard.getId(); + UUID wrongCenterId = UUID.randomUUID(); + LocalDateTime newStartDateTime = createUpdateStartDateTime(); + 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, id, wrongCenterId, newImgUrl) + ).isInstanceOf(BadRequestException.class); + + } + + private static RecruitBoard createRecruitBoard(UUID centerId, Long locationId) { + LocalDateTime startDateTime = createStartDateTime(); + LocalDateTime endDateTime = startDateTime.plusHours(1); + + return createRecruitBoard(centerId, locationId, startDateTime, endDateTime); + } + + private static RecruitBoard createRecruitBoard(UUID centerId, Long locationId, + LocalDateTime startDateTime, + LocalDateTime endDateTime) { + + RecruitmentInfo recruitmentInfo = RecruitmentInfo.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") + .recruitmentInfo(recruitmentInfo) + .build(); + } + + private static Location createLocation() { + return Location.builder() + .address("주소주소") + .longitude(BigDecimal.valueOf(37.11111)) + .latitude(BigDecimal.valueOf(127.11111)) + .build(); + } +} 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..d001cc182 --- /dev/null +++ b/src/test/java/com/somemore/recruitboard/service/query/RecruitQueryServiceTest.java @@ -0,0 +1,90 @@ +package com.somemore.recruitboard.service.query; + +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 com.somemore.IntegrationTestSupport; +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 RecruitQueryServiceTest extends IntegrationTestSupport { + + @Autowired + private RecruitBoardQueryService 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(); + } + + 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(); + } +}