Skip to content

Commit d052325

Browse files
authored
feat: 어드민 멘토 승격 요청 페이징 조회 기능 추가 (#576)
* feat: 어드민 멘토 지원서 페이징 조회 기능 추가 * feat: mentor/repository 패키지에 custom 패키지 추가 - custom 패키지에 페이징 조회를 책임지는 MentorApplicationFilterRepository 추가 - MentorApplicationSearchCondition 에서 넘긴 keyword 기반으로 닉네임, 권역, 나라, 학교명으로 필터링 검색 기능 추가 - MentorApplicationSearchCondition 에서 넘긴 mentorApplicationStatus 기반으로 승인, 거절, 진행중 으로 필터링 기능 추가 * test: 어드민 멘토 지원서 페이징 조회 테스트 추가 * feat: MentorApplication 엔티티에 approved_at 필드 추가 flyway 스크립트 작성 * fix: 파일 끝에 개행 추가 * refactor: 페이징 조회 시 count 쿼리에 불필요한 조인 막기 * fix: 코드래빗 리뷰 적용 * fix: flyway V39 스크립트 파일명 수정 * test: 테스트 코드 오류 수정, 검증 추가   * test: 기대하는 값이랑 다른 테스트 응답을 수정합니다
1 parent 0e9d476 commit d052325

11 files changed

Lines changed: 482 additions & 1 deletion
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.example.solidconnection.admin.controller;
2+
3+
import com.example.solidconnection.admin.dto.MentorApplicationSearchCondition;
4+
import com.example.solidconnection.admin.dto.MentorApplicationSearchResponse;
5+
import com.example.solidconnection.admin.service.AdminMentorApplicationService;
6+
import com.example.solidconnection.common.response.PageResponse;
7+
import jakarta.validation.Valid;
8+
import lombok.RequiredArgsConstructor;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.data.domain.Page;
11+
import org.springframework.data.domain.Pageable;
12+
import org.springframework.http.ResponseEntity;
13+
import org.springframework.web.bind.annotation.GetMapping;
14+
import org.springframework.web.bind.annotation.ModelAttribute;
15+
import org.springframework.web.bind.annotation.RequestMapping;
16+
import org.springframework.web.bind.annotation.RestController;
17+
18+
@RequiredArgsConstructor
19+
@RequestMapping("/admin/mentor-applications")
20+
@RestController
21+
@Slf4j
22+
public class AdminMentorApplicationController {
23+
private final AdminMentorApplicationService adminMentorApplicationService;
24+
25+
@GetMapping
26+
public ResponseEntity<PageResponse<MentorApplicationSearchResponse>> searchMentorApplications(
27+
@Valid @ModelAttribute MentorApplicationSearchCondition mentorApplicationSearchCondition,
28+
Pageable pageable
29+
) {
30+
Page<MentorApplicationSearchResponse> page = adminMentorApplicationService.searchMentorApplications(
31+
mentorApplicationSearchCondition,
32+
pageable
33+
);
34+
35+
return ResponseEntity.ok(PageResponse.of(page));
36+
}
37+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.example.solidconnection.admin.dto;
2+
3+
import com.example.solidconnection.mentor.domain.MentorApplicationStatus;
4+
import java.time.ZonedDateTime;
5+
6+
public record MentorApplicationResponse(
7+
long id,
8+
String region,
9+
String country,
10+
String university,
11+
String mentorProofUrl,
12+
MentorApplicationStatus mentorApplicationStatus,
13+
String rejectedReason,
14+
ZonedDateTime createdAt,
15+
ZonedDateTime approvedAt
16+
) {
17+
18+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.example.solidconnection.admin.dto;
2+
3+
import com.example.solidconnection.mentor.domain.MentorApplicationStatus;
4+
import java.time.LocalDate;
5+
6+
public record MentorApplicationSearchCondition(
7+
MentorApplicationStatus mentorApplicationStatus,
8+
String keyword,
9+
LocalDate createdAt
10+
) {
11+
12+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.example.solidconnection.admin.dto;
2+
3+
public record MentorApplicationSearchResponse(
4+
SiteUserResponse siteUserResponse,
5+
MentorApplicationResponse mentorApplicationResponse
6+
) {
7+
8+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.example.solidconnection.admin.service;
2+
3+
import com.example.solidconnection.admin.dto.MentorApplicationSearchCondition;
4+
import com.example.solidconnection.admin.dto.MentorApplicationSearchResponse;
5+
import com.example.solidconnection.mentor.repository.MentorApplicationRepository;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.data.domain.Page;
8+
import org.springframework.data.domain.Pageable;
9+
import org.springframework.stereotype.Service;
10+
import org.springframework.transaction.annotation.Transactional;
11+
12+
@RequiredArgsConstructor
13+
@Service
14+
public class AdminMentorApplicationService {
15+
16+
private final MentorApplicationRepository mentorApplicationRepository;
17+
18+
@Transactional(readOnly = true)
19+
public Page<MentorApplicationSearchResponse> searchMentorApplications(
20+
MentorApplicationSearchCondition mentorApplicationSearchCondition,
21+
Pageable pageable
22+
) {
23+
return mentorApplicationRepository.searchMentorApplications(mentorApplicationSearchCondition, pageable);
24+
}
25+
}

src/main/java/com/example/solidconnection/mentor/domain/MentorApplication.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import jakarta.persistence.GeneratedValue;
1212
import jakarta.persistence.GenerationType;
1313
import jakarta.persistence.Id;
14+
import java.time.ZonedDateTime;
1415
import java.util.Collections;
1516
import java.util.EnumSet;
1617
import java.util.Set;
@@ -66,6 +67,9 @@ public class MentorApplication extends BaseEntity {
6667
@Enumerated(EnumType.STRING)
6768
private MentorApplicationStatus mentorApplicationStatus;
6869

70+
@Column
71+
private ZonedDateTime approvedAt;
72+
6973
private static final Set<ExchangeStatus> ALLOWED =
7074
Collections.unmodifiableSet(EnumSet.of(ExchangeStatus.STUDYING_ABROAD, ExchangeStatus.AFTER_EXCHANGE));
7175

src/main/java/com/example/solidconnection/mentor/repository/MentorApplicationRepository.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
import com.example.solidconnection.mentor.domain.MentorApplication;
44
import com.example.solidconnection.mentor.domain.MentorApplicationStatus;
5+
import com.example.solidconnection.mentor.repository.custom.MentorApplicationFilterRepository;
56
import java.util.List;
67
import java.util.Optional;
78
import org.springframework.data.jpa.repository.JpaRepository;
89

9-
public interface MentorApplicationRepository extends JpaRepository<MentorApplication, Long> {
10+
public interface MentorApplicationRepository extends JpaRepository<MentorApplication, Long> , MentorApplicationFilterRepository {
1011

1112
boolean existsBySiteUserIdAndMentorApplicationStatusIn(long siteUserId, List<MentorApplicationStatus> mentorApplicationStatuses);
1213

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.example.solidconnection.mentor.repository.custom;
2+
3+
import com.example.solidconnection.admin.dto.MentorApplicationSearchCondition;
4+
import com.example.solidconnection.admin.dto.MentorApplicationSearchResponse;
5+
import org.springframework.data.domain.Page;
6+
import org.springframework.data.domain.Pageable;
7+
8+
public interface MentorApplicationFilterRepository {
9+
10+
Page<MentorApplicationSearchResponse> searchMentorApplications(MentorApplicationSearchCondition mentorApplicationSearchCondition, Pageable pageable);
11+
12+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package com.example.solidconnection.mentor.repository.custom;
2+
3+
import static com.example.solidconnection.location.country.domain.QCountry.country;
4+
import static com.example.solidconnection.location.region.domain.QRegion.region;
5+
import static com.example.solidconnection.mentor.domain.QMentorApplication.mentorApplication;
6+
import static com.example.solidconnection.siteuser.domain.QSiteUser.siteUser;
7+
import static com.example.solidconnection.university.domain.QUniversity.university;
8+
import static org.springframework.util.StringUtils.hasText;
9+
10+
import com.example.solidconnection.admin.dto.MentorApplicationResponse;
11+
import com.example.solidconnection.admin.dto.MentorApplicationSearchCondition;
12+
import com.example.solidconnection.admin.dto.MentorApplicationSearchResponse;
13+
import com.example.solidconnection.admin.dto.SiteUserResponse;
14+
import com.example.solidconnection.mentor.domain.MentorApplicationStatus;
15+
import com.querydsl.core.types.ConstructorExpression;
16+
import com.querydsl.core.types.Projections;
17+
import com.querydsl.core.types.dsl.BooleanExpression;
18+
import com.querydsl.jpa.impl.JPAQuery;
19+
import com.querydsl.jpa.impl.JPAQueryFactory;
20+
import jakarta.persistence.EntityManager;
21+
import java.time.LocalDate;
22+
import java.time.LocalDateTime;
23+
import java.time.ZoneId;
24+
import java.util.List;
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
import org.springframework.data.domain.Page;
27+
import org.springframework.data.domain.PageImpl;
28+
import org.springframework.data.domain.Pageable;
29+
import org.springframework.stereotype.Repository;
30+
31+
@Repository
32+
public class MentorApplicationFilterRepositoryImpl implements MentorApplicationFilterRepository {
33+
34+
private static final ZoneId SYSTEM_ZONE_ID = ZoneId.systemDefault();
35+
36+
private static final ConstructorExpression<SiteUserResponse> SITE_USER_RESPONSE_PROJECTION =
37+
Projections.constructor(
38+
SiteUserResponse.class,
39+
siteUser.id,
40+
siteUser.nickname,
41+
siteUser.profileImageUrl
42+
);
43+
44+
private static final ConstructorExpression<MentorApplicationResponse> MENTOR_APPLICATION_RESPONSE_PROJECTION =
45+
Projections.constructor(
46+
MentorApplicationResponse.class,
47+
mentorApplication.id,
48+
region.koreanName,
49+
country.koreanName,
50+
university.koreanName,
51+
mentorApplication.mentorProofUrl,
52+
mentorApplication.mentorApplicationStatus,
53+
mentorApplication.rejectedReason,
54+
mentorApplication.createdAt,
55+
mentorApplication.approvedAt
56+
);
57+
58+
private static final ConstructorExpression<MentorApplicationSearchResponse> MENTOR_APPLICATION_SEARCH_RESPONSE_PROJECTION =
59+
Projections.constructor(
60+
MentorApplicationSearchResponse.class,
61+
SITE_USER_RESPONSE_PROJECTION,
62+
MENTOR_APPLICATION_RESPONSE_PROJECTION
63+
);
64+
65+
private final JPAQueryFactory queryFactory;
66+
67+
@Autowired
68+
public MentorApplicationFilterRepositoryImpl(EntityManager em) {
69+
this.queryFactory = new JPAQueryFactory(em);
70+
}
71+
72+
@Override
73+
public Page<MentorApplicationSearchResponse> searchMentorApplications(MentorApplicationSearchCondition condition, Pageable pageable) {
74+
List<MentorApplicationSearchResponse> content = queryFactory
75+
.select(MENTOR_APPLICATION_SEARCH_RESPONSE_PROJECTION)
76+
.from(mentorApplication)
77+
.join(siteUser).on(mentorApplication.siteUserId.eq(siteUser.id))
78+
.leftJoin(university).on(mentorApplication.universityId.eq(university.id))
79+
.leftJoin(region).on(university.region.eq(region))
80+
.leftJoin(country).on(university.country.eq(country))
81+
.where(
82+
verifyMentorStatusEq(condition.mentorApplicationStatus()),
83+
keywordContains(condition.keyword()),
84+
createdAtEq(condition.createdAt())
85+
)
86+
.orderBy(mentorApplication.createdAt.desc())
87+
.offset(pageable.getOffset())
88+
.limit(pageable.getPageSize())
89+
.fetch();
90+
91+
Long totalCount = createCountQuery(condition).fetchOne();
92+
93+
return new PageImpl<>(content, pageable, totalCount != null ? totalCount : 0L);
94+
}
95+
96+
private JPAQuery<Long> createCountQuery(MentorApplicationSearchCondition condition) {
97+
JPAQuery<Long> query = queryFactory
98+
.select(mentorApplication.count())
99+
.from(mentorApplication);
100+
101+
String keyword = condition.keyword();
102+
103+
if (hasText(keyword)) {
104+
query.join(siteUser).on(mentorApplication.siteUserId.eq(siteUser.id))
105+
.leftJoin(university).on(mentorApplication.universityId.eq(university.id))
106+
.leftJoin(region).on(university.region.eq(region))
107+
.leftJoin(country).on(university.country.eq(country));
108+
}
109+
110+
return query.where(
111+
verifyMentorStatusEq(condition.mentorApplicationStatus()),
112+
keywordContains(condition.keyword()),
113+
createdAtEq(condition.createdAt())
114+
);
115+
}
116+
117+
private BooleanExpression verifyMentorStatusEq(MentorApplicationStatus status) {
118+
return status != null ? mentorApplication.mentorApplicationStatus.eq(status) : null;
119+
}
120+
121+
private BooleanExpression keywordContains(String keyword) {
122+
if (!hasText(keyword)) {
123+
return null;
124+
}
125+
126+
return siteUser.nickname.containsIgnoreCase(keyword)
127+
.or(university.koreanName.containsIgnoreCase(keyword))
128+
.or(region.koreanName.containsIgnoreCase(keyword))
129+
.or(country.koreanName.containsIgnoreCase(keyword));
130+
}
131+
132+
private BooleanExpression createdAtEq(LocalDate createdAt) {
133+
if (createdAt == null) {
134+
return null;
135+
}
136+
137+
LocalDateTime startOfDay = createdAt.atStartOfDay();
138+
LocalDateTime endOfDay = createdAt.plusDays(1).atStartOfDay().minusNanos(1);
139+
140+
return mentorApplication.createdAt.between(
141+
startOfDay.atZone(SYSTEM_ZONE_ID),
142+
endOfDay.atZone(SYSTEM_ZONE_ID)
143+
);
144+
}
145+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
ALTER TABLE mentor_application
2+
ADD COLUMN approved_at DATETIME(6);
3+
4+
UPDATE mentor_application
5+
SET approved_at = NOW()
6+
WHERE mentor_application_status = 'APPROVED'
7+
AND approved_at IS NULL;

0 commit comments

Comments
 (0)