Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 8
feat: 어드민 멘토 승격 요청 페이징 조회 기능 추가#576
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
494717428a97e74f66034ed318cbbbde8fc561350006a0492File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| 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 |
|---|---|---|
| @@ -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()) | ||
| ); | ||
| } | ||
| 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
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
Comment on lines
+1
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 마이그레이션 파일명에 오타가 있습니다. 파일명이 다음과 같이 파일명을 수정해주세요: -V39__add_approved_at_mentro_application.sql+V39__add_approved_at_mentor_application.sql
🤖 Prompt for AI Agents
Comment on lines
+4
to
+7
| ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
함수로 나눠져 있어서 깔끔하네요!
조건이 더 복잡해지면 BooleanBuilder를 사용하는 것도 좋을 것 같습니다