Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.example.solidconnection.admin.controller;

import com.example.solidconnection.admin.dto.MentorApplicationSearchCondition;
import com.example.solidconnection.admin.dto.MentorApplicationSearchResponse;
import com.example.solidconnection.admin.service.AdminMentorApplicationService;
import com.example.solidconnection.common.response.PageResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RequestMapping("/admin/mentor-applications")
@RestController
@Slf4j
public class AdminMentorApplicationController {
private final AdminMentorApplicationService adminMentorApplicationService;

@GetMapping
public ResponseEntity<PageResponse<MentorApplicationSearchResponse>> searchMentorApplications(
@Valid @ModelAttribute MentorApplicationSearchCondition mentorApplicationSearchCondition,
Pageable pageable
) {
Page<MentorApplicationSearchResponse> page = adminMentorApplicationService.searchMentorApplications(
mentorApplicationSearchCondition,
pageable
);

return ResponseEntity.ok(PageResponse.of(page));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.solidconnection.admin.dto;

import com.example.solidconnection.mentor.domain.MentorApplicationStatus;
import java.time.ZonedDateTime;

public record MentorApplicationResponse(
long id,
String region,
String country,
String university,
String mentorProofUrl,
MentorApplicationStatus mentorApplicationStatus,
String rejectedReason,
ZonedDateTime createdAt,
ZonedDateTime approvedAt
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.solidconnection.admin.dto;

import com.example.solidconnection.mentor.domain.MentorApplicationStatus;
import java.time.LocalDate;

public record MentorApplicationSearchCondition(
MentorApplicationStatus mentorApplicationStatus,
String keyword,
LocalDate createdAt
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.solidconnection.admin.dto;

public record MentorApplicationSearchResponse(
SiteUserResponse siteUserResponse,
MentorApplicationResponse mentorApplicationResponse
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.example.solidconnection.admin.service;

import com.example.solidconnection.admin.dto.MentorApplicationSearchCondition;
import com.example.solidconnection.admin.dto.MentorApplicationSearchResponse;
import com.example.solidconnection.mentor.repository.MentorApplicationRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class AdminMentorApplicationService {

private final MentorApplicationRepository mentorApplicationRepository;

@Transactional(readOnly = true)
public Page<MentorApplicationSearchResponse> searchMentorApplications(
MentorApplicationSearchCondition mentorApplicationSearchCondition,
Pageable pageable
) {
return mentorApplicationRepository.searchMentorApplications(mentorApplicationSearchCondition, pageable);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
Expand Down Expand Up @@ -66,6 +67,9 @@ public class MentorApplication extends BaseEntity {
@Enumerated(EnumType.STRING)
private MentorApplicationStatus mentorApplicationStatus;

@Column
private ZonedDateTime approvedAt;

private static final Set<ExchangeStatus> ALLOWED =
Collections.unmodifiableSet(EnumSet.of(ExchangeStatus.STUDYING_ABROAD, ExchangeStatus.AFTER_EXCHANGE));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

import com.example.solidconnection.mentor.domain.MentorApplication;
import com.example.solidconnection.mentor.domain.MentorApplicationStatus;
import com.example.solidconnection.mentor.repository.custom.MentorApplicationFilterRepository;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MentorApplicationRepository extends JpaRepository<MentorApplication, Long> {
public interface MentorApplicationRepository extends JpaRepository<MentorApplication, Long> , MentorApplicationFilterRepository {

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.solidconnection.mentor.repository.custom;

import com.example.solidconnection.admin.dto.MentorApplicationSearchCondition;
import com.example.solidconnection.admin.dto.MentorApplicationSearchResponse;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface MentorApplicationFilterRepository {

Page<MentorApplicationSearchResponse> searchMentorApplications(MentorApplicationSearchCondition mentorApplicationSearchCondition, Pageable pageable);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package com.example.solidconnection.mentor.repository.custom;

import static com.example.solidconnection.location.country.domain.QCountry.country;
import static com.example.solidconnection.location.region.domain.QRegion.region;
import static com.example.solidconnection.mentor.domain.QMentorApplication.mentorApplication;
import static com.example.solidconnection.siteuser.domain.QSiteUser.siteUser;
import static com.example.solidconnection.university.domain.QUniversity.university;
import static org.springframework.util.StringUtils.hasText;

import com.example.solidconnection.admin.dto.MentorApplicationResponse;
import com.example.solidconnection.admin.dto.MentorApplicationSearchCondition;
import com.example.solidconnection.admin.dto.MentorApplicationSearchResponse;
import com.example.solidconnection.admin.dto.SiteUserResponse;
import com.example.solidconnection.mentor.domain.MentorApplicationStatus;
import com.querydsl.core.types.ConstructorExpression;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;

@Repository
public class MentorApplicationFilterRepositoryImpl implements MentorApplicationFilterRepository {

private static final ZoneId SYSTEM_ZONE_ID = ZoneId.systemDefault();

private static final ConstructorExpression<SiteUserResponse> SITE_USER_RESPONSE_PROJECTION =
Projections.constructor(
SiteUserResponse.class,
siteUser.id,
siteUser.nickname,
siteUser.profileImageUrl
);

private static final ConstructorExpression<MentorApplicationResponse> MENTOR_APPLICATION_RESPONSE_PROJECTION =
Projections.constructor(
MentorApplicationResponse.class,
mentorApplication.id,
region.koreanName,
country.koreanName,
university.koreanName,
mentorApplication.mentorProofUrl,
mentorApplication.mentorApplicationStatus,
mentorApplication.rejectedReason,
mentorApplication.createdAt,
mentorApplication.approvedAt
);

private static final ConstructorExpression<MentorApplicationSearchResponse> MENTOR_APPLICATION_SEARCH_RESPONSE_PROJECTION =
Projections.constructor(
MentorApplicationSearchResponse.class,
SITE_USER_RESPONSE_PROJECTION,
MENTOR_APPLICATION_RESPONSE_PROJECTION
);

private final JPAQueryFactory queryFactory;

@Autowired
public MentorApplicationFilterRepositoryImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}

@Override
public Page<MentorApplicationSearchResponse> searchMentorApplications(MentorApplicationSearchCondition condition, Pageable pageable) {
List<MentorApplicationSearchResponse> content = queryFactory
.select(MENTOR_APPLICATION_SEARCH_RESPONSE_PROJECTION)
.from(mentorApplication)
.join(siteUser).on(mentorApplication.siteUserId.eq(siteUser.id))
.leftJoin(university).on(mentorApplication.universityId.eq(university.id))
.leftJoin(region).on(university.region.eq(region))
.leftJoin(country).on(university.country.eq(country))
.where(
verifyMentorStatusEq(condition.mentorApplicationStatus()),
keywordContains(condition.keyword()),
createdAtEq(condition.createdAt())
)
.orderBy(mentorApplication.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();

Long totalCount = createCountQuery(condition).fetchOne();

return new PageImpl<>(content, pageable, totalCount != null ? totalCount : 0L);
}

private JPAQuery<Long> createCountQuery(MentorApplicationSearchCondition condition) {
JPAQuery<Long> query = queryFactory
.select(mentorApplication.count())
.from(mentorApplication);

String keyword = condition.keyword();

if (hasText(keyword)) {
query.join(siteUser).on(mentorApplication.siteUserId.eq(siteUser.id))
.leftJoin(university).on(mentorApplication.universityId.eq(university.id))
.leftJoin(region).on(university.region.eq(region))
.leftJoin(country).on(university.country.eq(country));
}

return query.where(
verifyMentorStatusEq(condition.mentorApplicationStatus()),
keywordContains(condition.keyword()),
createdAtEq(condition.createdAt())
);
Comment on lines +110 to +114
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수로 나눠져 있어서 깔끔하네요!
조건이 더 복잡해지면 BooleanBuilder를 사용하는 것도 좋을 것 같습니다

}

private BooleanExpression verifyMentorStatusEq(MentorApplicationStatus status) {
return status != null ? mentorApplication.mentorApplicationStatus.eq(status) : null;
}

private BooleanExpression keywordContains(String keyword) {
if (!hasText(keyword)) {
return null;
}

return siteUser.nickname.containsIgnoreCase(keyword)
.or(university.koreanName.containsIgnoreCase(keyword))
.or(region.koreanName.containsIgnoreCase(keyword))
.or(country.koreanName.containsIgnoreCase(keyword));
Comment on lines +126 to +129
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개인적인 생각으로는 일단 컬럼별 우선순위 없이 현재 방식을 유지해도 될 것 같습니다!

}

private BooleanExpression createdAtEq(LocalDate createdAt) {
if (createdAt == null) {
return null;
}

LocalDateTime startOfDay = createdAt.atStartOfDay();
LocalDateTime endOfDay = createdAt.plusDays(1).atStartOfDay().minusNanos(1);

return mentorApplication.createdAt.between(
startOfDay.atZone(SYSTEM_ZONE_ID),
endOfDay.atZone(SYSTEM_ZONE_ID)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ALTER TABLE mentor_application
ADD COLUMN approved_at DATETIME(6);

UPDATE mentor_application
SET approved_at = NOW()
WHERE mentor_application_status = 'APPROVED'
AND approved_at IS NULL;
Loading
Loading