Skip to content

Commit e517b99

Browse files
authored
feat: 봉사 지원 조회 기능 (#145)
* feat(volunteer-apply): 지원자 통계 기능 * test(volunteer-apply): 지원자 통계 기능 테스트 * feat(volunteer-apply): 지원자 통계 기능 API * refactor(volunteer-apply): requestDto 패키지 위치 변경 * feat(volunteer-apply): 봉사 지원 단건 조회 기능 * test(volunteer-apply): 봉사 지원 단건 조회 테스트 * feat(volunteer-apply): 봉사 지원 단건 조회 API * test(volunteer-apply): 봉사 지원 단건 조회 API 테스트 * feat(volunteer): 봉사 지원자 정보 조회 기능 - 봉사 지원자 정보에 노출해야는 데이터들이 volunteer, detail 혼재 - mapper VolunteerSimpleInfo 정의 * test(volunteer): 봉사 지원자 정보 조회 기능 테스트 - 봉사 지원자 정보에 노출해야는 데이터들이 volunteer, detail 혼재 - mapper VolunteerSimpleInfo 정의 * feat(volunteer-apply): 봉사 지원 리스트 응답 DTO 및 동적 쿼리 DTO * feat(volunteer-apply): 봉사 지원 리스트 조회 기능 - **순환 참조로 인한 파사드 도입** - VolunteerApplyQueryFacade** * test(volunteer-apply): 봉사 지원 리스트 조회 기능 테스트 - **순환 참조로 인한 파사드 도입** - VolunteerApplyQueryFacade** * feat(volunteer-apply): 봉사 지원 리스트 조회 기능 API * test(volunteer-apply): 봉사 지원 리스트 조회 기능 API 테스트 * feat(recruit-board): 봉사 모집글 조회 기능 추가 - 지원 리스트 정보에 필요한 기능 추가 * test(recruit-board): 봉사 모집글 조회 기능 추가 테스트 - 지원 리스트 정보에 필요한 기능 추가 * feat(volunteer-apply): 봉사 지원 리스트 응답 DTO * feat(volunteer-apply): 특정 봉사 지원 리스트 조회 기능 * test(volunteer-apply): 특정 봉사 지원 리스트 조회 기능 테스트 * feat(volunteer-apply): 특정 봉사 지원 리스트 조회 기능 API * test(volunteer-apply): 특정 봉사 지원 리스트 조회 기능 API 테스트 * fix(volunteer-apply): @transactional 없던 오류 수정 * refactor(volunteer-apply): VolunteerInfo 객체만 가져오도록 수정 - 객체를 가져오고 현 서비스에서 응답 DTO로 감싸주도록 변경 * chore: sonar qube 이슈 수정 * refactor(volunteer-apply): facade 패키기로 이동 * refactor(volunteer-apply): facade 에서 레포지토리가 아닌 UseCase를 의존하도록 변경 * test(volunteer-apply): facade 에서 레포지토리가 아닌 UseCase를 의존하도록 변경에 따른 테스트
1 parent 030cd64 commit e517b99

File tree

40 files changed

+1400
-130
lines changed

40 files changed

+1400
-130
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package com.somemore.facade.volunteerapply;
2+
3+
import static com.somemore.global.exception.ExceptionMessage.UNAUTHORIZED_RECRUIT_BOARD;
4+
5+
import com.somemore.global.exception.BadRequestException;
6+
import com.somemore.recruitboard.domain.RecruitBoard;
7+
import com.somemore.recruitboard.usecase.query.RecruitBoardQueryUseCase;
8+
import com.somemore.volunteer.repository.mapper.VolunteerSimpleInfo;
9+
import com.somemore.volunteer.usecase.VolunteerQueryUseCase;
10+
import com.somemore.volunteerapply.domain.VolunteerApply;
11+
import com.somemore.volunteerapply.dto.condition.VolunteerApplySearchCondition;
12+
import com.somemore.volunteerapply.dto.response.VolunteerApplyRecruitInfoResponseDto;
13+
import com.somemore.volunteerapply.dto.response.VolunteerApplyVolunteerInfoResponseDto;
14+
import com.somemore.volunteerapply.usecase.VolunteerApplyQueryUseCase;
15+
import java.util.List;
16+
import java.util.Map;
17+
import java.util.UUID;
18+
import java.util.stream.Collectors;
19+
import lombok.RequiredArgsConstructor;
20+
import org.springframework.data.domain.Page;
21+
import org.springframework.stereotype.Service;
22+
import org.springframework.transaction.annotation.Transactional;
23+
24+
@RequiredArgsConstructor
25+
@Transactional(readOnly = true)
26+
@Service
27+
public class VolunteerApplyQueryFacadeService implements VolunteerApplyQueryFacadeUseCase {
28+
29+
private final VolunteerApplyQueryUseCase volunteerApplyQueryUseCase;
30+
private final RecruitBoardQueryUseCase recruitBoardQueryUseCase;
31+
private final VolunteerQueryUseCase volunteerQueryUseCase;
32+
33+
@Override
34+
public Page<VolunteerApplyVolunteerInfoResponseDto> getVolunteerAppliesByRecruitIdAndCenterId(
35+
Long recruitId, UUID centerId, VolunteerApplySearchCondition condition) {
36+
validateAuthorization(recruitId, centerId);
37+
38+
Page<VolunteerApply> applies = volunteerApplyQueryUseCase.getAllByRecruitId(recruitId,
39+
condition);
40+
41+
Map<UUID, VolunteerSimpleInfo> volunteerMap = getVolunteerInfoMap(
42+
applies);
43+
44+
return applies.map(
45+
apply -> VolunteerApplyVolunteerInfoResponseDto.of(
46+
apply,
47+
volunteerMap.getOrDefault(apply.getVolunteerId(), null)
48+
));
49+
}
50+
51+
@Override
52+
public Page<VolunteerApplyRecruitInfoResponseDto> getVolunteerAppliesByVolunteerId(
53+
UUID volunteerId, VolunteerApplySearchCondition condition) {
54+
55+
Page<VolunteerApply> applies = volunteerApplyQueryUseCase.getAllByVolunteerId(volunteerId,
56+
condition);
57+
58+
Map<Long, RecruitBoard> boardMap = getRecruitBoardMap(applies);
59+
60+
return applies.map(
61+
apply -> VolunteerApplyRecruitInfoResponseDto.of(
62+
apply,
63+
boardMap.getOrDefault(apply.getRecruitBoardId(), null)
64+
));
65+
}
66+
67+
private void validateAuthorization(Long recruitId, UUID centerId) {
68+
RecruitBoard recruitBoard = recruitBoardQueryUseCase.getById(recruitId);
69+
if (recruitBoard.isWriter(centerId)) {
70+
return;
71+
}
72+
73+
throw new BadRequestException(UNAUTHORIZED_RECRUIT_BOARD);
74+
}
75+
76+
private Map<Long, RecruitBoard> getRecruitBoardMap(Page<VolunteerApply> applies) {
77+
List<Long> boardIds = applies.getContent().stream().map(VolunteerApply::getRecruitBoardId)
78+
.toList();
79+
List<RecruitBoard> boards = recruitBoardQueryUseCase.getAllByIds(boardIds);
80+
81+
return boards.stream()
82+
.collect(Collectors.toMap(RecruitBoard::getId,
83+
board -> board));
84+
}
85+
86+
private Map<UUID, VolunteerSimpleInfo> getVolunteerInfoMap(Page<VolunteerApply> applies) {
87+
List<UUID> volunteerIds = applies.getContent().stream().map(VolunteerApply::getVolunteerId)
88+
.toList();
89+
90+
List<VolunteerSimpleInfo> volunteers = volunteerQueryUseCase.getVolunteerSimpleInfosByIds(
91+
volunteerIds);
92+
93+
return volunteers.stream()
94+
.collect(Collectors.toMap(VolunteerSimpleInfo::id,
95+
volunteer -> volunteer));
96+
}
97+
98+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.somemore.facade.volunteerapply;
2+
3+
import com.somemore.volunteerapply.dto.condition.VolunteerApplySearchCondition;
4+
import com.somemore.volunteerapply.dto.response.VolunteerApplyRecruitInfoResponseDto;
5+
import com.somemore.volunteerapply.dto.response.VolunteerApplyVolunteerInfoResponseDto;
6+
import java.util.UUID;
7+
import org.springframework.data.domain.Page;
8+
9+
public interface VolunteerApplyQueryFacadeUseCase {
10+
11+
Page<VolunteerApplyVolunteerInfoResponseDto> getVolunteerAppliesByRecruitIdAndCenterId(
12+
Long recruitId,
13+
UUID centerId, VolunteerApplySearchCondition condition);
14+
15+
Page<VolunteerApplyRecruitInfoResponseDto> getVolunteerAppliesByVolunteerId(UUID volunteerId,
16+
VolunteerApplySearchCondition condition);
17+
18+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.somemore.recruitboard.dto.response;
2+
3+
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
4+
import com.fasterxml.jackson.databind.annotation.JsonNaming;
5+
import com.somemore.recruitboard.domain.RecruitBoard;
6+
import io.swagger.v3.oas.annotations.media.Schema;
7+
import lombok.Builder;
8+
9+
@JsonNaming(SnakeCaseStrategy.class)
10+
@Builder
11+
public record RecruitBoardOverViewResponseDto(
12+
@Schema(description = "모집글 아이디", example = "1")
13+
Long id,
14+
@Schema(description = "모집글 제목", example = "서울시 도서관 봉사 활동 모집")
15+
String title
16+
) {
17+
18+
public static RecruitBoardOverViewResponseDto from(RecruitBoard recruitBoard) {
19+
return RecruitBoardOverViewResponseDto.builder()
20+
.id(recruitBoard.getId())
21+
.title(recruitBoard.getTitle())
22+
.build();
23+
}
24+
}

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
package com.somemore.recruitboard.repository;
22

33
import com.somemore.recruitboard.domain.RecruitBoard;
4+
import com.somemore.recruitboard.dto.condition.RecruitBoardNearByCondition;
5+
import com.somemore.recruitboard.dto.condition.RecruitBoardSearchCondition;
46
import com.somemore.recruitboard.repository.mapper.RecruitBoardDetail;
57
import com.somemore.recruitboard.repository.mapper.RecruitBoardWithCenter;
68
import com.somemore.recruitboard.repository.mapper.RecruitBoardWithLocation;
7-
import com.somemore.recruitboard.dto.condition.RecruitBoardNearByCondition;
8-
import com.somemore.recruitboard.dto.condition.RecruitBoardSearchCondition;
9-
109
import java.util.List;
11-
1210
import java.util.Optional;
1311
import java.util.UUID;
14-
1512
import org.springframework.data.domain.Page;
1613

1714
public interface RecruitBoardRepository {
@@ -31,4 +28,6 @@ public interface RecruitBoardRepository {
3128
Page<RecruitBoard> findAllByCenterId(UUID centerId, RecruitBoardSearchCondition condition);
3229

3330
List<Long> findNotCompletedIdsByCenterId(UUID centerId);
31+
32+
List<RecruitBoard> findAllByIds(List<Long> ids);
3433
}

src/main/java/com/somemore/recruitboard/repository/RecruitBoardRepositoryImpl.java

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,16 @@
1414
import com.somemore.location.utils.GeoUtils;
1515
import com.somemore.recruitboard.domain.QRecruitBoard;
1616
import com.somemore.recruitboard.domain.RecruitBoard;
17-
18-
import java.util.List;
19-
2017
import com.somemore.recruitboard.domain.RecruitStatus;
2118
import com.somemore.recruitboard.domain.VolunteerCategory;
19+
import com.somemore.recruitboard.dto.condition.RecruitBoardNearByCondition;
20+
import com.somemore.recruitboard.dto.condition.RecruitBoardSearchCondition;
2221
import com.somemore.recruitboard.repository.mapper.RecruitBoardDetail;
2322
import com.somemore.recruitboard.repository.mapper.RecruitBoardWithCenter;
2423
import com.somemore.recruitboard.repository.mapper.RecruitBoardWithLocation;
25-
import com.somemore.recruitboard.dto.condition.RecruitBoardNearByCondition;
26-
import com.somemore.recruitboard.dto.condition.RecruitBoardSearchCondition;
27-
24+
import java.util.List;
2825
import java.util.Optional;
2926
import java.util.UUID;
30-
3127
import lombok.RequiredArgsConstructor;
3228
import org.apache.commons.lang3.StringUtils;
3329
import org.springframework.data.domain.Page;
@@ -80,6 +76,17 @@ public List<Long> findNotCompletedIdsByCenterId(UUID centerId) {
8076
.fetch();
8177
}
8278

79+
@Override
80+
public List<RecruitBoard> findAllByIds(List<Long> ids) {
81+
BooleanExpression exp = recruitBoard.id.in(ids)
82+
.and(isNotCompleted());
83+
84+
return queryFactory
85+
.selectFrom(recruitBoard)
86+
.where(exp)
87+
.fetch();
88+
}
89+
8390
@Override
8491
public Optional<RecruitBoardWithLocation> findWithLocationById(Long id) {
8592
QRecruitBoard recruitBoard = QRecruitBoard.recruitBoard;
@@ -160,7 +167,7 @@ public Page<RecruitBoardDetail> findAllNearby(RecruitBoardNearByCondition condit
160167

161168
@Override
162169
public Page<RecruitBoard> findAllByCenterId(UUID centerId,
163-
RecruitBoardSearchCondition condition) {
170+
RecruitBoardSearchCondition condition) {
164171
QRecruitBoard recruitBoard = QRecruitBoard.recruitBoard;
165172

166173
Pageable pageable = condition.pageable();

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.somemore.recruitboard.service.query;
22

3+
import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_RECRUIT_BOARD;
4+
35
import com.somemore.center.usecase.query.CenterQueryUseCase;
46
import com.somemore.global.exception.BadRequestException;
57
import com.somemore.recruitboard.domain.RecruitBoard;
@@ -14,16 +16,13 @@
1416
import com.somemore.recruitboard.repository.mapper.RecruitBoardWithCenter;
1517
import com.somemore.recruitboard.repository.mapper.RecruitBoardWithLocation;
1618
import com.somemore.recruitboard.usecase.query.RecruitBoardQueryUseCase;
19+
import java.util.List;
20+
import java.util.UUID;
1721
import lombok.RequiredArgsConstructor;
1822
import org.springframework.data.domain.Page;
1923
import org.springframework.stereotype.Service;
2024
import org.springframework.transaction.annotation.Transactional;
2125

22-
import java.util.List;
23-
import java.util.UUID;
24-
25-
import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_RECRUIT_BOARD;
26-
2726
@RequiredArgsConstructor
2827
@Transactional(readOnly = true)
2928
@Service
@@ -70,7 +69,7 @@ public Page<RecruitBoardDetailResponseDto> getRecruitBoardsNearby(
7069

7170
@Override
7271
public Page<RecruitBoardResponseDto> getRecruitBoardsByCenterId(UUID centerId,
73-
RecruitBoardSearchCondition condition) {
72+
RecruitBoardSearchCondition condition) {
7473
centerQueryUseCase.validateCenterExists(centerId);
7574

7675
Page<RecruitBoard> boards = recruitBoardRepository.findAllByCenterId(centerId, condition);
@@ -82,4 +81,9 @@ public List<Long> getNotCompletedIdsByCenterIds(UUID centerId) {
8281
return recruitBoardRepository.findNotCompletedIdsByCenterId(centerId);
8382
}
8483

84+
@Override
85+
public List<RecruitBoard> getAllByIds(List<Long> ids) {
86+
return recruitBoardRepository.findAllByIds(ids);
87+
}
88+
8589
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ Page<RecruitBoardResponseDto> getRecruitBoardsByCenterId(UUID centerId,
2929

3030
List<Long> getNotCompletedIdsByCenterIds(UUID centerId);
3131

32+
List<RecruitBoard> getAllByIds(List<Long> ids);
3233
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.somemore.volunteer.dto.response;
2+
3+
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
4+
import com.fasterxml.jackson.databind.annotation.JsonNaming;
5+
import com.somemore.volunteer.repository.mapper.VolunteerSimpleInfo;
6+
import io.swagger.v3.oas.annotations.media.Schema;
7+
import java.util.UUID;
8+
import lombok.Builder;
9+
10+
@Schema(description = "봉사자 간단 정보 응답 DTO")
11+
@JsonNaming(SnakeCaseStrategy.class)
12+
@Builder
13+
public record VolunteerSimpleInfoResponseDto(
14+
@Schema(description = "봉사자 ID", example = "f5a8779a-bcc9-4fc5-b8a1-7b2a383054a9")
15+
UUID id,
16+
@Schema(description = "봉사자 이름", example = "홍길동")
17+
String name,
18+
@Schema(description = "봉사자 닉네임", example = "gil-dong")
19+
String nickname,
20+
@Schema(description = "봉사자 이메일", example = "[email protected]")
21+
String email,
22+
@Schema(description = "봉사자 이미지 URL", example = "https://example.com/images/hong.jpg")
23+
String imgUrl
24+
) {
25+
26+
public static VolunteerSimpleInfoResponseDto from(VolunteerSimpleInfo volunteerSimpleInfo) {
27+
return VolunteerSimpleInfoResponseDto.builder()
28+
.id(volunteerSimpleInfo.id())
29+
.name(volunteerSimpleInfo.name())
30+
.nickname(volunteerSimpleInfo.nickname())
31+
.email(volunteerSimpleInfo.email())
32+
.imgUrl(volunteerSimpleInfo.imgUrl())
33+
.build();
34+
}
35+
}

src/main/java/com/somemore/volunteer/repository/VolunteerRepository.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
import com.somemore.volunteer.domain.Volunteer;
44
import com.somemore.volunteer.repository.mapper.VolunteerOverviewForRankingByHours;
5-
import org.springframework.stereotype.Repository;
6-
5+
import com.somemore.volunteer.repository.mapper.VolunteerSimpleInfo;
76
import java.util.List;
87
import java.util.Optional;
98
import java.util.UUID;
9+
import org.springframework.stereotype.Repository;
1010

1111
@Repository
1212
public interface VolunteerRepository {
@@ -24,4 +24,6 @@ public interface VolunteerRepository {
2424
void deleteAllInBatch();
2525

2626
List<Volunteer> findAllByIds(List<UUID> volunteerIds);
27+
28+
List<VolunteerSimpleInfo> findSimpleInfoByIds(List<UUID> ids);
2729
}

src/main/java/com/somemore/volunteer/repository/VolunteerRepositoryImpl.java

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
import com.querydsl.core.types.dsl.BooleanExpression;
66
import com.querydsl.jpa.impl.JPAQueryFactory;
77
import com.somemore.volunteer.domain.QVolunteer;
8+
import com.somemore.volunteer.domain.QVolunteerDetail;
89
import com.somemore.volunteer.domain.Volunteer;
910
import com.somemore.volunteer.repository.mapper.VolunteerOverviewForRankingByHours;
10-
import lombok.RequiredArgsConstructor;
11-
import org.springframework.stereotype.Repository;
12-
11+
import com.somemore.volunteer.repository.mapper.VolunteerSimpleInfo;
1312
import java.util.List;
1413
import java.util.Optional;
1514
import java.util.UUID;
15+
import lombok.RequiredArgsConstructor;
16+
import org.springframework.stereotype.Repository;
1617

1718
@RequiredArgsConstructor
1819
@Repository
@@ -22,6 +23,7 @@ public class VolunteerRepositoryImpl implements VolunteerRepository {
2223
private final JPAQueryFactory queryFactory;
2324

2425
private static final QVolunteer volunteer = QVolunteer.volunteer;
26+
private static final QVolunteerDetail volunteerDetail = QVolunteerDetail.volunteerDetail;
2527

2628
@Override
2729
public Volunteer save(Volunteer volunteer) {
@@ -72,6 +74,25 @@ public List<Volunteer> findAllByIds(List<UUID> volunteerIds) {
7274
return volunteerJpaRepository.findAllByIdInAndDeletedFalse(volunteerIds);
7375
}
7476

77+
@Override
78+
public List<VolunteerSimpleInfo> findSimpleInfoByIds(List<UUID> ids) {
79+
BooleanExpression exp = volunteer.id.in(ids)
80+
.and(isNotDeleted());
81+
82+
return queryFactory
83+
.select(Projections.constructor(VolunteerSimpleInfo.class,
84+
volunteer.id,
85+
volunteerDetail.name,
86+
volunteer.nickname,
87+
volunteerDetail.email,
88+
volunteer.imgUrl,
89+
volunteer.tier))
90+
.from(volunteer)
91+
.join(volunteerDetail).on(volunteer.id.eq(volunteerDetail.volunteerId))
92+
.where(exp)
93+
.fetch();
94+
}
95+
7596
private Optional<Volunteer> findOne(BooleanExpression condition) {
7697

7798
return Optional.ofNullable(

0 commit comments

Comments
 (0)