Skip to content

Commit 7487768

Browse files
authored
feat: 봉사 활동 지원 및 철회 기능 (#128)
* refactor(recruit-board): 조회 기능 수정 * test(recruit-board): 조회 기능 수정에 따른 테스트 * feat(recruit-board): 모집 상태에 대한 도메인 비지니스 로직 메서드 작성 * test(recruit-board): 모집 상태에 대한 도메인 비지니스 로직 메서드 테스트 * test(recruit-board): 모집 상태에 대한 도메인 비지니스 로직 메서드 테스트 * feat(volunteer-apply): 봉사 모집글 지원 기능 * test(volunteer-apply): 봉사 모집글 지원 테스트 * feat(volunteer-apply): 봉사 모집글 지원 API 연결 * test(volunteer-apply): 봉사 모집글 지원 API 연결 테스트 * refactor(volunteer-apply): 클래스 이름 변경 - VolunteerApplyCommand* -> ApplyVolunteerApply* * feat(volunteer-apply): 도메인 로직 작성 * test(volunteer-apply): 도메인 로직 작성 테스트 * feat(volunteer-apply): 봉사 지원 철회 기능 * test(volunteer-apply): 봉사 지원 철회 기능 테스트 * feat(volunteer-apply): 봉사 지원 철회 기능 API * test(volunteer-apply): 봉사 지원 철회 기능 API 테스트 * fix(volunteer-apply): sonar qube 리뷰 반영 * refactor(recruit-board): 도메인 로직 메서드 변경 - isApplicationOpen -> isRecruitOpen * test(recruit-board): 도메인 로직 메서드 변경에 따른 테스트 및 부적절한 테스트 제거
1 parent bda5959 commit 7487768

22 files changed

+618
-95
lines changed

src/main/java/com/somemore/global/exception/ExceptionMessage.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ public enum ExceptionMessage {
2929
REVIEW_ALREADY_EXISTS("이미 작성한 리뷰가 존재합니다."),
3030
REVIEW_RESTRICTED_TO_ATTENDED("리뷰는 참석한 봉사에 한해서만 작성할 수 있습니다."),
3131
NOT_EXISTS_REVIEW("존재하지 않는 리뷰입니다."),
32-
;
32+
RECRUITMENT_NOT_OPEN("현재 모집 진행 중이 아닙니다."),
33+
DUPLICATE_APPLICATION("이미 신청한 봉사 모집 공고입니다."),
34+
UNAUTHORIZED_VOLUNTEER_APPLY("해당 지원에 권한이 없습니다."),
3335

36+
;
3437
private final String message;
3538
}

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public class RecruitBoard extends BaseEntity {
5757

5858
@Builder
5959
public RecruitBoard(UUID centerId, Long locationId, String title, String content,
60-
RecruitmentInfo recruitmentInfo, String imgUrl) {
60+
RecruitmentInfo recruitmentInfo, String imgUrl) {
6161
this.centerId = centerId;
6262
this.locationId = locationId;
6363
this.title = title;
@@ -92,13 +92,17 @@ public void changeRecruitStatus(RecruitStatus newStatus, LocalDateTime currentDa
9292
this.recruitStatus = newStatus;
9393
}
9494

95+
public boolean isRecruitOpen() {
96+
return this.recruitStatus == RECRUITING;
97+
}
98+
9599
private void updateRecruitmentInfo(RecruitBoardUpdateRequestDto dto) {
96100
recruitmentInfo.updateWith(
97-
dto.recruitmentCount(),
98-
dto.volunteerCategory(),
99-
dto.volunteerStartDateTime(),
100-
dto.volunteerEndDateTime(),
101-
dto.admitted()
101+
dto.recruitmentCount(),
102+
dto.volunteerCategory(),
103+
dto.volunteerStartDateTime(),
104+
dto.volunteerEndDateTime(),
105+
dto.admitted()
102106
);
103107
}
104108

src/main/java/com/somemore/recruitboard/service/query/RecruitBoardQueryService.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,15 @@ public class RecruitBoardQueryService implements RecruitBoardQueryUseCase {
3333
private final CenterQueryUseCase centerQueryUseCase;
3434

3535
@Override
36-
public RecruitBoardResponseDto getById(Long id) {
37-
RecruitBoard recruitBoard = getRecruitBoard(id);
36+
public RecruitBoard getById(Long id) {
37+
return recruitBoardRepository.findById(id).orElseThrow(
38+
() -> new BadRequestException(NOT_EXISTS_RECRUIT_BOARD.getMessage())
39+
);
40+
}
41+
42+
@Override
43+
public RecruitBoardResponseDto getRecruitBoardById(Long id) {
44+
RecruitBoard recruitBoard = getById(id);
3845
return RecruitBoardResponseDto.from(recruitBoard);
3946
}
4047

@@ -75,9 +82,4 @@ public List<Long> getNotCompletedIdsByCenterIds(UUID centerId) {
7582
return recruitBoardRepository.findNotCompletedIdsByCenterId(centerId);
7683
}
7784

78-
private RecruitBoard getRecruitBoard(Long id) {
79-
return recruitBoardRepository.findById(id).orElseThrow(
80-
() -> new BadRequestException(NOT_EXISTS_RECRUIT_BOARD.getMessage())
81-
);
82-
}
8385
}

src/main/java/com/somemore/recruitboard/usecase/query/RecruitBoardQueryUseCase.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.somemore.recruitboard.usecase.query;
22

3+
import com.somemore.recruitboard.domain.RecruitBoard;
34
import com.somemore.recruitboard.dto.condition.RecruitBoardNearByCondition;
45
import com.somemore.recruitboard.dto.condition.RecruitBoardSearchCondition;
56
import com.somemore.recruitboard.dto.response.RecruitBoardDetailResponseDto;
@@ -13,7 +14,8 @@
1314

1415
public interface RecruitBoardQueryUseCase {
1516

16-
RecruitBoardResponseDto getById(Long id);
17+
RecruitBoard getById(Long id);
18+
RecruitBoardResponseDto getRecruitBoardById(Long id);
1719

1820
RecruitBoardWithLocationResponseDto getWithLocationById(Long id);
1921

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.somemore.volunteerapply.controller;
2+
3+
import com.somemore.auth.annotation.CurrentUser;
4+
import com.somemore.global.common.response.ApiResponse;
5+
import com.somemore.volunteerapply.dto.VolunteerApplyCreateRequestDto;
6+
import com.somemore.volunteerapply.usecase.ApplyVolunteerApplyUseCase;
7+
import com.somemore.volunteerapply.usecase.WithdrawVolunteerApplyUseCase;
8+
import io.swagger.v3.oas.annotations.Operation;
9+
import io.swagger.v3.oas.annotations.tags.Tag;
10+
import jakarta.validation.Valid;
11+
import java.util.UUID;
12+
import lombok.RequiredArgsConstructor;
13+
import org.springframework.security.access.annotation.Secured;
14+
import org.springframework.web.bind.annotation.DeleteMapping;
15+
import org.springframework.web.bind.annotation.PathVariable;
16+
import org.springframework.web.bind.annotation.PostMapping;
17+
import org.springframework.web.bind.annotation.RequestBody;
18+
import org.springframework.web.bind.annotation.RequestMapping;
19+
import org.springframework.web.bind.annotation.RestController;
20+
21+
@Tag(name = "Volunteer Apply Command API", description = "봉사 활동 지원, 철회 관련 API")
22+
@RequiredArgsConstructor
23+
@RequestMapping("/api")
24+
@RestController
25+
public class VolunteerApplyCommandApiController {
26+
27+
private final ApplyVolunteerApplyUseCase applyVolunteerApplyUseCase;
28+
private final WithdrawVolunteerApplyUseCase withdrawVolunteerApplyUseCase;
29+
30+
@Secured("ROLE_VOLUNTEER")
31+
@Operation(summary = "봉사 활동 지원", description = "봉사 활동에 지원합니다.")
32+
@PostMapping("/volunteer-apply")
33+
public ApiResponse<Long> apply(
34+
@CurrentUser UUID volunteerId,
35+
@Valid @RequestBody VolunteerApplyCreateRequestDto requestDto
36+
) {
37+
return ApiResponse.ok(
38+
201,
39+
applyVolunteerApplyUseCase.apply(requestDto, volunteerId),
40+
"봉사 활동 지원 성공"
41+
);
42+
}
43+
44+
@Secured("ROLE_VOLUNTEER")
45+
@Operation(summary = "봉사 활동 지원 철회", description = "봉사 활동 지원을 철회합니다.")
46+
@DeleteMapping("/volunteer-apply/{id}")
47+
public ApiResponse<String> withdraw(
48+
@CurrentUser UUID volunteerId,
49+
@PathVariable Long id
50+
) {
51+
withdrawVolunteerApplyUseCase.withdraw(id, volunteerId);
52+
return ApiResponse.ok("봉사 활동 지원 철회 성공");
53+
}
54+
55+
}

src/main/java/com/somemore/volunteerapply/domain/VolunteerApply.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ public void changeAttended(Boolean attended) {
6363
this.attended = attended;
6464
}
6565

66+
public boolean isOwnApplication(UUID volunteerId) {
67+
return this.volunteerId.equals(volunteerId);
68+
}
69+
70+
6671
public boolean isVolunteerActivityCompleted() {
6772
return this.attended && this.status == APPROVED;
6873
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.somemore.volunteerapply.dto;
2+
3+
import static com.somemore.volunteerapply.domain.ApplyStatus.WAITING;
4+
5+
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
6+
import com.fasterxml.jackson.databind.annotation.JsonNaming;
7+
import com.somemore.volunteerapply.domain.VolunteerApply;
8+
import io.swagger.v3.oas.annotations.media.Schema;
9+
import jakarta.validation.constraints.NotNull;
10+
import java.util.UUID;
11+
import lombok.Builder;
12+
13+
@JsonNaming(SnakeCaseStrategy.class)
14+
@Builder
15+
public record VolunteerApplyCreateRequestDto(
16+
@Schema(description = "봉사 모집글 아이디", example = "1")
17+
@NotNull(message = "모집글 아이디는 필수 값입니다.")
18+
Long recruitBoardId
19+
) {
20+
21+
public VolunteerApply toEntity(UUID volunteerId) {
22+
return VolunteerApply.builder()
23+
.volunteerId(volunteerId)
24+
.recruitBoardId(recruitBoardId)
25+
.status(WAITING)
26+
.attended(false)
27+
.build();
28+
}
29+
30+
}

src/main/java/com/somemore/volunteerapply/repository/VolunteerApplyRepository.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ public interface VolunteerApplyRepository {
1515

1616
Optional<VolunteerApply> findById(Long id);
1717

18+
Optional<VolunteerApply> findByRecruitIdAndVolunteerId(Long recruitId, UUID volunteerId);
19+
20+
boolean existsByRecruitIdAndVolunteerId(Long recruitId, UUID volunteerId);
21+
1822
List<UUID> findVolunteerIdsByRecruitIds(List<Long> recruitIds);
1923

2024
Page<VolunteerApply> findAllByRecruitId(Long recruitId, Pageable pageable);
2125

22-
Optional<VolunteerApply> findByRecruitIdAndVolunteerId(Long recruitId, UUID volunteerId);
2326
}

src/main/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImpl.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,16 @@ public Optional<VolunteerApply> findByRecruitIdAndVolunteerId(Long recruitId,
8383
return findOne(exp);
8484
}
8585

86+
@Override
87+
public boolean existsByRecruitIdAndVolunteerId(Long recruitId, UUID volunteerId) {
88+
return queryFactory
89+
.selectFrom(volunteerApply)
90+
.where(volunteerApply.recruitBoardId.eq(recruitId)
91+
.and(volunteerApply.volunteerId.eq(volunteerId))
92+
.and(isNotDeleted()))
93+
.fetchFirst() != null;
94+
}
95+
8696
private Long getCount(BooleanExpression exp) {
8797
return queryFactory
8898
.select(volunteerApply.count())
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.somemore.volunteerapply.service;
2+
3+
import static com.somemore.global.exception.ExceptionMessage.DUPLICATE_APPLICATION;
4+
import static com.somemore.global.exception.ExceptionMessage.RECRUITMENT_NOT_OPEN;
5+
6+
import com.somemore.global.exception.BadRequestException;
7+
import com.somemore.recruitboard.domain.RecruitBoard;
8+
import com.somemore.recruitboard.usecase.query.RecruitBoardQueryUseCase;
9+
import com.somemore.volunteerapply.domain.VolunteerApply;
10+
import com.somemore.volunteerapply.dto.VolunteerApplyCreateRequestDto;
11+
import com.somemore.volunteerapply.repository.VolunteerApplyRepository;
12+
import com.somemore.volunteerapply.usecase.ApplyVolunteerApplyUseCase;
13+
import java.util.UUID;
14+
import lombok.RequiredArgsConstructor;
15+
import org.springframework.stereotype.Service;
16+
import org.springframework.transaction.annotation.Transactional;
17+
18+
@RequiredArgsConstructor
19+
@Transactional
20+
@Service
21+
public class ApplyVolunteerApplyService implements ApplyVolunteerApplyUseCase {
22+
23+
private final VolunteerApplyRepository volunteerApplyRepository;
24+
private final RecruitBoardQueryUseCase recruitBoardQueryUseCase;
25+
26+
@Override
27+
public Long apply(VolunteerApplyCreateRequestDto requestDto, UUID volunteerId) {
28+
29+
RecruitBoard board = recruitBoardQueryUseCase.getById(requestDto.recruitBoardId());
30+
validateCanApply(board);
31+
validateDuplicatedApply(volunteerId, board);
32+
33+
VolunteerApply apply = requestDto.toEntity(volunteerId);
34+
volunteerApplyRepository.save(apply);
35+
36+
return apply.getId();
37+
}
38+
39+
private void validateCanApply(RecruitBoard board) {
40+
if (board.isRecruitOpen()) {
41+
return;
42+
}
43+
throw new BadRequestException(RECRUITMENT_NOT_OPEN);
44+
}
45+
46+
private void validateDuplicatedApply(UUID volunteerId, RecruitBoard board) {
47+
boolean isDuplicate = volunteerApplyRepository.existsByRecruitIdAndVolunteerId(board.getId(),
48+
volunteerId);
49+
if (isDuplicate) {
50+
throw new BadRequestException(DUPLICATE_APPLICATION);
51+
}
52+
}
53+
}

0 commit comments

Comments
 (0)