Skip to content

Commit 9325da1

Browse files
authored
feat: 모집글 스케쥴링 (#297)
* refactor: RecruitBoard 리팩토링 - RecruitStatus 기본 값 제거, 생성자 파라미터 추가 - 검증 로직 제거, 비지니스 로직 추가 * test(recruit-board): RecruitBoard 리팩토링 테스트 - RecruitStatus 기본 값 제거, 생성자 파라미터 추가 - 검증 로직 제거, 비지니스 로직 추가 * refactor(recruit-board): RecruitBoard 수정 검증 리팩토링 - 봉사 시작/종료 일시 수정시, 생성일 하루 이후부터 가능하도록 검증 * test(recruit-board): RecruitBoard 수정 검증 리팩토링 테스트 - 봉사 시작/종료 일시 수정시, 생성일 하루 이후부터 가능하도록 검증 * refactor(recruit-board): RecruitBoard 수정 검증 리팩토링 - 모집글 수정시 시간에 따른 검증 일괄 추가 - 봉사 시작 일시 전날까지 수정 가능하도록 검증 * test(recruit-board): RecruitBoard 수정 검증 리팩토링 테스트 - 모집글 수정시 시간에 따른 검증 일괄 추가 - 봉사 시작 일시 전날까지 수정 가능하도록 검증 * refactor(recruit-board): RecruitBoard 수정 검증 변경에 따른 리팩토링 * test(recruit-board): RecruitBoard 수정 검증 변경에 따른 리팩토링 테스트 * chore: retry 종속성 추가 * feat(recruit-board): 모집글 상태 스케쥴링 - 봉사 시작일 기준 모집완료(CLOSED) 변경 - 봉사 종료일 기준 완료(COMPLETED) 변경 * test(recruit-board): 모집글 상태 스케쥴링 테스트 - 봉사 시작일 기준 모집완료(CLOSED) 변경 - 봉사 종료일 기준 완료(COMPLETED) 변경 * refactor(recruit-board): sonarqube 이슈 해결 * fix(recruit-board): sonarqube 이슈 해결 * fix(recruit-board): sonarqube 이슈 해결 * refactor(recruit-board): 스케쥴러 retryable 리팩토링 - 최대 시도, 주기 명시적으로 변경 * fix(recruit-board): 오타 수정 * refactor(recruit-board): 스케쥴링 작업 중 에러 발생시 로깅 처리 * feat: TimeConfig 추가 * refactor(recruit-board): 시간 검증 로직 변경 - Service 계층에서 Clock 사용 * test(recruit-board): 시간 검증 로직 변경 테스트 - Service 계층에서 Clock 사용 * refactor(recruit-board): 스케쥴링 로직 리팩토링 - between()으로 변경 * test(recruit-board): 스케쥴링 로직 리팩토링 테스트 - between()으로 변경 * refactor(recruit-board): 스케줄 시간 상수화 * refactor(recruit-board): sonar qube 이슈 해결 * refactor(recruit-board): 메서드명 명확하게 변경
1 parent 01cdddf commit 9325da1

24 files changed

+944
-457
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ dependencies {
6565
compileOnly 'org.projectlombok:lombok'
6666
annotationProcessor 'org.projectlombok:lombok'
6767
implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.6.0'
68+
implementation("org.springframework.retry:spring-retry")
6869

6970
// Monitoring
7071
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-actuator', version: '3.3.3'

src/main/java/com/somemore/domains/recruitboard/controller/RecruitBoardCommandApiController.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.somemore.domains.recruitboard.controller;
22

33

4+
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
5+
46
import com.somemore.domains.recruitboard.dto.request.RecruitBoardCreateRequestDto;
57
import com.somemore.domains.recruitboard.dto.request.RecruitBoardLocationUpdateRequestDto;
68
import com.somemore.domains.recruitboard.dto.request.RecruitBoardStatusUpdateRequestDto;
@@ -15,16 +17,20 @@
1517
import io.swagger.v3.oas.annotations.Operation;
1618
import io.swagger.v3.oas.annotations.tags.Tag;
1719
import jakarta.validation.Valid;
20+
import java.util.UUID;
1821
import lombok.RequiredArgsConstructor;
1922
import org.springframework.security.access.annotation.Secured;
20-
import org.springframework.web.bind.annotation.*;
23+
import org.springframework.web.bind.annotation.DeleteMapping;
24+
import org.springframework.web.bind.annotation.PatchMapping;
25+
import org.springframework.web.bind.annotation.PathVariable;
26+
import org.springframework.web.bind.annotation.PostMapping;
27+
import org.springframework.web.bind.annotation.PutMapping;
28+
import org.springframework.web.bind.annotation.RequestBody;
29+
import org.springframework.web.bind.annotation.RequestMapping;
30+
import org.springframework.web.bind.annotation.RequestPart;
31+
import org.springframework.web.bind.annotation.RestController;
2132
import org.springframework.web.multipart.MultipartFile;
2233

23-
import java.time.LocalDateTime;
24-
import java.util.UUID;
25-
26-
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
27-
2834
@Tag(name = "Recruit Board Command API", description = "봉사 활동 모집글 생성 수정 삭제 API")
2935
@RequiredArgsConstructor
3036
@RequestMapping("/api")
@@ -44,7 +50,6 @@ public ApiResponse<Long> createRecruitBoard(
4450
@Valid @RequestPart("data") RecruitBoardCreateRequestDto requestDto,
4551
@RequestPart(value = "img_file", required = false) MultipartFile image
4652
) {
47-
4853
String imgUrl = imageUploadUseCase.uploadImage(new ImageUploadRequestDto(image));
4954
return ApiResponse.ok(
5055
201,
@@ -64,7 +69,6 @@ public ApiResponse<String> updateRecruitBoard(
6469
) {
6570
String imgUrl = imageUploadUseCase.uploadImage(new ImageUploadRequestDto(image));
6671
updateRecruitBoardUseCase.updateRecruitBoard(requestDto, id, userId, imgUrl);
67-
6872
return ApiResponse.ok("봉사 활동 모집글 수정 성공");
6973
}
7074

@@ -76,7 +80,6 @@ public ApiResponse<String> updateRecruitBoardLocation(
7680
@PathVariable Long id,
7781
@Valid @RequestBody RecruitBoardLocationUpdateRequestDto requestDto
7882
) {
79-
8083
updateRecruitBoardUseCase.updateRecruitBoardLocation(requestDto, id, userId);
8184
return ApiResponse.ok("봉사 활동 모집글 위치 수정 성공");
8285
}
@@ -89,9 +92,7 @@ public ApiResponse<String> updateRecruitBoardStatus(
8992
@PathVariable Long id,
9093
@RequestBody RecruitBoardStatusUpdateRequestDto requestDto
9194
) {
92-
LocalDateTime now = LocalDateTime.now();
93-
updateRecruitBoardUseCase.updateRecruitBoardStatus(requestDto.status(), id, userId, now);
94-
95+
updateRecruitBoardUseCase.updateRecruitBoardStatus(requestDto.status(), id, userId);
9596
return ApiResponse.ok("봉사 활동 모집글 상태 수정 성공");
9697
}
9798

@@ -105,5 +106,4 @@ public ApiResponse<String> deleteRecruitBoard(
105106
deleteRecruitBoardUseCase.deleteRecruitBoard(userId, id);
106107
return ApiResponse.ok("봉사 활동 모집글 삭제 성공");
107108
}
108-
109109
}

src/main/java/com/somemore/domains/recruitboard/domain/RecruitBoard.java

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
package com.somemore.domains.recruitboard.domain;
22

3+
import static jakarta.persistence.EnumType.STRING;
4+
import static jakarta.persistence.GenerationType.IDENTITY;
5+
import static lombok.AccessLevel.PROTECTED;
6+
37
import com.somemore.domains.recruitboard.dto.request.RecruitBoardUpdateRequestDto;
48
import com.somemore.global.common.entity.BaseEntity;
5-
import jakarta.persistence.*;
9+
import jakarta.persistence.Column;
10+
import jakarta.persistence.Embedded;
11+
import jakarta.persistence.Entity;
12+
import jakarta.persistence.Enumerated;
13+
import jakarta.persistence.GeneratedValue;
14+
import jakarta.persistence.Id;
15+
import jakarta.persistence.Lob;
16+
import jakarta.persistence.Table;
17+
import java.time.LocalDateTime;
18+
import java.util.UUID;
619
import lombok.Builder;
720
import lombok.Getter;
821
import lombok.NoArgsConstructor;
922

10-
import java.time.LocalDateTime;
11-
import java.util.UUID;
12-
13-
import static jakarta.persistence.EnumType.STRING;
14-
import static jakarta.persistence.GenerationType.IDENTITY;
15-
import static lombok.AccessLevel.PROTECTED;
16-
1723
@Getter
1824
@NoArgsConstructor(access = PROTECTED)
1925
@Entity
@@ -42,19 +48,20 @@ public class RecruitBoard extends BaseEntity {
4248

4349
@Enumerated(value = STRING)
4450
@Column(name = "recruit_status", nullable = false, length = 20)
45-
private RecruitStatus recruitStatus = RecruitStatus.RECRUITING;
51+
private RecruitStatus recruitStatus;
4652

4753
@Column(name = "img_url", nullable = false)
4854
private String imgUrl;
4955

5056
@Builder
5157
public RecruitBoard(UUID centerId, Long locationId, String title, String content,
52-
RecruitmentInfo recruitmentInfo, String imgUrl) {
58+
RecruitmentInfo recruitmentInfo, RecruitStatus status, String imgUrl) {
5359
this.centerId = centerId;
5460
this.locationId = locationId;
5561
this.title = title;
5662
this.content = content;
5763
this.recruitmentInfo = recruitmentInfo;
64+
this.recruitStatus = status;
5865
this.imgUrl = imgUrl;
5966
}
6067

@@ -73,17 +80,23 @@ public void updateWith(String region) {
7380
recruitmentInfo.updateWith(region);
7481
}
7582

76-
public void changeRecruitStatus(RecruitStatus newStatus, LocalDateTime currentDateTime) {
77-
validateStatusChange(newStatus);
78-
validateChangeDeadline(currentDateTime);
79-
80-
this.recruitStatus = newStatus;
83+
public void updateRecruitStatus(RecruitStatus status) {
84+
this.recruitStatus = status;
8185
}
8286

83-
public boolean isRecruitOpen() {
87+
public boolean isRecruiting() {
8488
return this.recruitStatus == RecruitStatus.RECRUITING;
8589
}
8690

91+
public boolean isCompleted() {
92+
return this.recruitStatus == RecruitStatus.COMPLETED;
93+
}
94+
95+
public boolean isUpdatable(LocalDateTime current) {
96+
LocalDateTime deadline = this.recruitmentInfo.getVolunteerStartDateTime().toLocalDate().atStartOfDay();
97+
return current.isBefore(deadline);
98+
}
99+
87100
private void updateRecruitmentInfo(RecruitBoardUpdateRequestDto dto) {
88101
recruitmentInfo.updateWith(
89102
dto.region(),
@@ -96,24 +109,4 @@ private void updateRecruitmentInfo(RecruitBoardUpdateRequestDto dto) {
96109
);
97110
}
98111

99-
public boolean isCompleted() {
100-
return this.recruitStatus == RecruitStatus.COMPLETED;
101-
}
102-
103-
private void validateStatusChange(RecruitStatus newStatus) {
104-
if (newStatus.isChangeable()) {
105-
return;
106-
}
107-
throw new IllegalArgumentException("상태는 '모집중' 또는 '마감'으로만 변경할 수 있습니다.");
108-
}
109-
110-
private void validateChangeDeadline(LocalDateTime currentDateTime) {
111-
LocalDateTime volunteerStartDateTime = recruitmentInfo.getVolunteerStartDateTime();
112-
LocalDateTime deadline = volunteerStartDateTime.toLocalDate().atStartOfDay();
113-
114-
if (!currentDateTime.isBefore(deadline)) {
115-
throw new IllegalStateException("봉사 시작 일시 자정 전까지만 상태를 변경할 수 있습니다.");
116-
}
117-
}
118-
119112
}

src/main/java/com/somemore/domains/recruitboard/dto/request/RecruitBoardCreateRequestDto.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.somemore.domains.recruitboard.dto.request;
22

3+
import static com.somemore.domains.recruitboard.domain.RecruitStatus.RECRUITING;
4+
35
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
46
import com.fasterxml.jackson.databind.annotation.JsonNaming;
57
import com.somemore.domains.location.dto.request.LocationCreateRequestDto;
@@ -10,10 +12,9 @@
1012
import jakarta.validation.constraints.Future;
1113
import jakarta.validation.constraints.NotBlank;
1214
import jakarta.validation.constraints.NotNull;
13-
import lombok.Builder;
14-
1515
import java.time.LocalDateTime;
1616
import java.util.UUID;
17+
import lombok.Builder;
1718

1819
@JsonNaming(SnakeCaseStrategy.class)
1920
@Builder
@@ -69,6 +70,7 @@ public RecruitBoard toEntity(UUID centerId, Long locationId, String imgUrl) {
6970
.content(content)
7071
.imgUrl(imgUrl)
7172
.recruitmentInfo(recruitmentInfo)
73+
.status(RECRUITING)
7274
.build();
7375
}
7476
}

src/main/java/com/somemore/domains/recruitboard/dto/request/RecruitBoardUpdateRequestDto.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@
44
import com.fasterxml.jackson.databind.annotation.JsonNaming;
55
import com.somemore.domains.recruitboard.domain.VolunteerCategory;
66
import io.swagger.v3.oas.annotations.media.Schema;
7-
import jakarta.validation.constraints.Future;
87
import jakarta.validation.constraints.NotBlank;
98
import jakarta.validation.constraints.NotNull;
10-
import lombok.Builder;
11-
129
import java.time.LocalDateTime;
10+
import lombok.Builder;
1311

1412
@JsonNaming(SnakeCaseStrategy.class)
1513
@Builder
@@ -28,11 +26,9 @@ public record RecruitBoardUpdateRequestDto(
2826
Integer recruitmentCount,
2927
@Schema(description = "봉사 시작 일시", example = "2024-12-20T10:00:00", type = "string")
3028
@NotNull(message = "봉사 시작 일시는 필수 값입니다.")
31-
@Future(message = "봉사 시작 일시는 내일부터 가능합니다.")
3229
LocalDateTime volunteerStartDateTime,
3330
@Schema(description = "봉사 종료 일시", example = "2024-12-20T12:00:00", type = "string")
3431
@NotNull(message = "봉사 종료 일시는 필수 값입니다.")
35-
@Future(message = "봉사 종료 일시는 내일부터 가능합니다.")
3632
LocalDateTime volunteerEndDateTime,
3733
@Schema(description = "봉사 시간", example = "2")
3834
@NotNull(message = "봉사 시간는 필수 값입니다.")

src/main/java/com/somemore/domains/recruitboard/repository/RecruitBoardRepository.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package com.somemore.domains.recruitboard.repository;
22

3+
import com.somemore.domains.recruitboard.domain.RecruitBoard;
34
import com.somemore.domains.recruitboard.dto.condition.RecruitBoardNearByCondition;
45
import com.somemore.domains.recruitboard.dto.condition.RecruitBoardSearchCondition;
56
import com.somemore.domains.recruitboard.repository.mapper.RecruitBoardDetail;
67
import com.somemore.domains.recruitboard.repository.mapper.RecruitBoardWithCenter;
78
import com.somemore.domains.recruitboard.repository.mapper.RecruitBoardWithLocation;
8-
import com.somemore.domains.recruitboard.domain.RecruitBoard;
9-
9+
import java.time.LocalDateTime;
1010
import java.util.List;
1111
import java.util.Optional;
1212
import java.util.UUID;
@@ -26,17 +26,20 @@ public interface RecruitBoardRepository {
2626

2727
Page<RecruitBoardDetail> findAllNearby(RecruitBoardNearByCondition condition);
2828

29-
// Page<RecruitBoardDetail> findAllNearbyWithKeyword(RecruitBoardNearByCondition condition);
30-
31-
3229
Page<RecruitBoard> findAllByCenterId(UUID centerId, RecruitBoardSearchCondition condition);
3330

3431
List<Long> findNotCompletedIdsByCenterId(UUID centerId);
3532

3633
List<RecruitBoard> findAllByIds(List<Long> ids);
3734

35+
List<RecruitBoard> findAll();
36+
37+
long updateStatusToClosedForDateRange(LocalDateTime startTime, LocalDateTime endTime);
38+
39+
long updateStatusToCompletedForDateRange(LocalDateTime startTime, LocalDateTime endTime);
40+
41+
// Page<RecruitBoardDetail> findAllNearbyWithKeyword(RecruitBoardNearByCondition condition);
3842
// Page<RecruitBoardWithCenter> findByRecruitBoardsContaining(RecruitBoardSearchCondition condition);
3943
// void saveDocuments(List<RecruitBoard> recruitBoards);
40-
List<RecruitBoard> findAll();
4144
// void deleteDocument(Long id);
4245
}

0 commit comments

Comments
 (0)