-
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
Changes from all commits
4947174
28a97e7
4f66034
ed318cb
bbde8fc
5613500
06a0492
File filter
Filter by extension
Conversations
Jump to
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
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
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. 기존 승인 데이터의 실제 승인 시점이 손실됩니다. 4-7번 라인의 UPDATE 구문이 모든 기존 APPROVED 레코드에 대해 더 나은 접근 방법:
방식 1을 적용한 diff: UPDATE mentor_application
-SET approved_at = NOW()
+SET approved_at = created_at
WHERE mentor_application_status = 'APPROVED'
AND approved_at IS NULL;참고: Line 7의 🤖 Prompt for AI Agents
Contributor
Author
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. @coderabbitai 현재 서비스에서 승인 된 멘토 지원서는 존재할 수 없다는 가정이면? 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. @sukangpunch 좋은 지적입니다! 만약 현재 프로덕션/스테이징 환경에 해당 가정이 맞다면 현재 마이그레이션 스크립트는 안전합니다. 다만 확실성을 위해: 1. 이 가정을 확인해주실 수 있나요?
2. (선택사항) 명확성을 위해 주석 추가를 고려해보시겠어요? -- Note: As of this migration, no APPROVED mentor applications exist in production/staging
-- This UPDATE serves as a defensive measure for any edge cases
UPDATE mentor_application
SET approved_at = NOW()
WHERE mentor_application_status = 'APPROVED'
AND approved_at IS NULL;가정이 맞다면 현재 코드 그대로 진행하셔도 괜찮습니다! 👍 🧠 Learnings used |
||
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를 사용하는 것도 좋을 것 같습니다