diff --git a/src/main/java/life/mosu/mosuserver/application/admin/AdminService.java b/src/main/java/life/mosu/mosuserver/application/admin/AdminService.java index 879625f1..d47dcddb 100644 --- a/src/main/java/life/mosu/mosuserver/application/admin/AdminService.java +++ b/src/main/java/life/mosu/mosuserver/application/admin/AdminService.java @@ -1,55 +1,55 @@ -//package life.mosu.mosuserver.application.admin; -// -//import java.util.List; -//import life.mosu.mosuserver.domain.admin.ApplicationQueryRepositoryImpl; -//import life.mosu.mosuserver.domain.admin.RefundQueryRepositoryImpl; -//import life.mosu.mosuserver.domain.admin.StudentQueryRepositoryImpl; -//import life.mosu.mosuserver.presentation.admin.dto.ApplicationExcelDto; -//import life.mosu.mosuserver.presentation.admin.dto.ApplicationFilter; -//import life.mosu.mosuserver.presentation.admin.dto.ApplicationListResponse; -//import life.mosu.mosuserver.presentation.admin.dto.RefundListResponse; -//import life.mosu.mosuserver.presentation.admin.dto.SchoolLunchResponse; -//import life.mosu.mosuserver.presentation.admin.dto.StudentExcelDto; -//import life.mosu.mosuserver.presentation.admin.dto.StudentFilter; -//import life.mosu.mosuserver.presentation.admin.dto.StudentListResponse; -//import lombok.RequiredArgsConstructor; -//import lombok.extern.slf4j.Slf4j; -//import org.springframework.data.domain.Page; -//import org.springframework.data.domain.Pageable; -//import org.springframework.stereotype.Service; -// -//@Slf4j -//@Service -//@RequiredArgsConstructor -//public class AdminService { -// -// private final StudentQueryRepositoryImpl studentQueryRepository; -// private final ApplicationQueryRepositoryImpl applicationQueryRepository; -// private final RefundQueryRepositoryImpl refundQueryRepository; -// -// public Page getStudents(StudentFilter filter, Pageable pageable) { -// return studentQueryRepository.searchAllStudents(filter, pageable); -// } -// -// public List getStudentExcelData() { -// return studentQueryRepository.searchAllStudentsForExcel(); -// } -// -// public List getLunchCounts() { -// return applicationQueryRepository.searchAllSchoolLunches(); -// } -// -// public Page getApplications(ApplicationFilter filter, -// Pageable pageable) { -// return applicationQueryRepository.searchAllApplications(filter, pageable); -// } -// -// public List getApplicationExcelData() { -// return applicationQueryRepository.searchAllApplicationsForExcel(); -// } -// -// public Page getRefunds(Pageable pageable) { -// return refundQueryRepository.searchAllRefunds(pageable); -// } -// -//} +package life.mosu.mosuserver.application.admin; + +import java.util.List; +import life.mosu.mosuserver.domain.admin.ApplicationQueryRepositoryImpl; +import life.mosu.mosuserver.domain.admin.RefundQueryRepositoryImpl; +import life.mosu.mosuserver.domain.admin.StudentQueryRepositoryImpl; +import life.mosu.mosuserver.presentation.admin.dto.ApplicationExcelDto; +import life.mosu.mosuserver.presentation.admin.dto.ApplicationFilter; +import life.mosu.mosuserver.presentation.admin.dto.ApplicationListResponse; +import life.mosu.mosuserver.presentation.admin.dto.RefundListResponse; +import life.mosu.mosuserver.presentation.admin.dto.SchoolLunchResponse; +import life.mosu.mosuserver.presentation.admin.dto.StudentExcelDto; +import life.mosu.mosuserver.presentation.admin.dto.StudentFilter; +import life.mosu.mosuserver.presentation.admin.dto.StudentListResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AdminService { + + private final StudentQueryRepositoryImpl studentQueryRepository; + private final ApplicationQueryRepositoryImpl applicationQueryRepository; + private final RefundQueryRepositoryImpl refundQueryRepository; + + public Page getStudents(StudentFilter filter, Pageable pageable) { + return studentQueryRepository.searchAllStudents(filter, pageable); + } + + public List getStudentExcelData() { + return studentQueryRepository.searchAllStudentsForExcel(); + } + + public List getLunchCounts() { + return applicationQueryRepository.searchAllSchoolLunches(); + } + + public Page getApplications(ApplicationFilter filter, + Pageable pageable) { + return applicationQueryRepository.searchAllApplications(filter, pageable); + } + + public List getApplicationExcelData() { + return applicationQueryRepository.searchAllApplicationsForExcel(); + } + + public Page getRefunds(Pageable pageable) { + return refundQueryRepository.searchAllRefunds(pageable); + } + +} diff --git a/src/main/java/life/mosu/mosuserver/application/examapplication/ExamApplicationService.java b/src/main/java/life/mosu/mosuserver/application/examapplication/ExamApplicationService.java index 2edd5e34..97b6ed99 100644 --- a/src/main/java/life/mosu/mosuserver/application/examapplication/ExamApplicationService.java +++ b/src/main/java/life/mosu/mosuserver/application/examapplication/ExamApplicationService.java @@ -1,10 +1,11 @@ package life.mosu.mosuserver.application.examapplication; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import life.mosu.mosuserver.application.examapplication.dto.RegisterExamApplicationEvent; import life.mosu.mosuserver.domain.application.ExamTicketImageJpaEntity; import life.mosu.mosuserver.domain.application.ExamTicketImageJpaRepository; -import life.mosu.mosuserver.domain.application.Subject; import life.mosu.mosuserver.domain.exam.ExamJpaEntity; import life.mosu.mosuserver.domain.exam.ExamJpaRepository; import life.mosu.mosuserver.domain.examapplication.ExamApplicationJpaEntity; @@ -34,8 +35,7 @@ public class ExamApplicationService { private final ProfileJpaRepository profileJpaRepository; private final ExamJpaRepository examJpaRepository; private final S3Service s3Service; - - + @Transactional public List register(RegisterExamApplicationEvent event) { List examApplicationEntities = event.toEntity(); @@ -60,9 +60,9 @@ public ExamTicketResponse getExamTicket(Long userId, Long examApplicationId) { List examSubjects = examSubjectJpaRepository.findByExamApplicationId( examApplicationId); - List subjects = examSubjects.stream() - .map(es -> es.getSubject()) - .toList(); + Set subjects = examSubjects.stream() + .map(es -> es.getSubject().getSubjectName()) + .collect(Collectors.toSet()); Long applicationId = examApplication.getApplicationId(); ExamTicketImageJpaEntity examTicketImage = examTicketImageJpaRepository.findByApplicationId( @@ -78,7 +78,5 @@ public ExamTicketResponse getExamTicket(Long userId, Long examApplicationId) { return ExamTicketResponse.of(examTicketImgUrl, profile.getUserName(), profile.getBirth(), examApplication.getExamNumber(), subjects, exam.getSchoolName()); - - } } diff --git a/src/main/java/life/mosu/mosuserver/application/examapplication/dto/RegisterExamApplicationEvent.java b/src/main/java/life/mosu/mosuserver/application/examapplication/dto/RegisterExamApplicationEvent.java index ae090401..e76a449e 100644 --- a/src/main/java/life/mosu/mosuserver/application/examapplication/dto/RegisterExamApplicationEvent.java +++ b/src/main/java/life/mosu/mosuserver/application/examapplication/dto/RegisterExamApplicationEvent.java @@ -14,7 +14,7 @@ public static RegisterExamApplicationEvent of( Long applicationId ) { List targetExams = examApplicationRequests.stream() - .map(request -> new TargetExam(request.examId(), request.lunchId())) + .map(request -> new TargetExam(request.examId(), request.isLunchChecked())) .toList(); return new RegisterExamApplicationEvent(targetExams, applicationId); } @@ -24,14 +24,14 @@ public List toEntity() { .map(targetExam -> ExamApplicationJpaEntity.create( applicationId, targetExam.examId(), - targetExam.lunchId() + targetExam.isLunchChecked() )) .toList(); } public record TargetExam( Long examId, - Long lunchId + boolean isLunchChecked ) { } diff --git a/src/main/java/life/mosu/mosuserver/application/payment/PaymentFailureHandler.java b/src/main/java/life/mosu/mosuserver/application/payment/PaymentFailureHandler.java index 7e515c21..6f74251c 100644 --- a/src/main/java/life/mosu/mosuserver/application/payment/PaymentFailureHandler.java +++ b/src/main/java/life/mosu/mosuserver/application/payment/PaymentFailureHandler.java @@ -19,7 +19,7 @@ public class PaymentFailureHandler { public void handlePaymentFailure(PaymentEvent event) { List existingPayments = paymentRepository.findByOrderId(event.orderId()); Set existingAppIds = existingPayments.stream() - .map(PaymentJpaEntity::getApplicationSchoolId) + .map(PaymentJpaEntity::getApplicationId) .collect(Collectors.toSet()); List missingAppSchoolIds = event.applicationSchoolIds().stream() diff --git a/src/main/java/life/mosu/mosuserver/domain/admin/ApplicationQueryRepositoryImpl.java b/src/main/java/life/mosu/mosuserver/domain/admin/ApplicationQueryRepositoryImpl.java index c8995a36..b66a7642 100644 --- a/src/main/java/life/mosu/mosuserver/domain/admin/ApplicationQueryRepositoryImpl.java +++ b/src/main/java/life/mosu/mosuserver/domain/admin/ApplicationQueryRepositoryImpl.java @@ -1,251 +1,257 @@ -//package life.mosu.mosuserver.domain.admin; -// -//import com.querydsl.core.Tuple; -//import com.querydsl.core.types.Predicate; -//import com.querydsl.core.types.dsl.EnumPath; -//import com.querydsl.core.types.dsl.Expressions; -//import com.querydsl.jpa.impl.JPAQuery; -//import com.querydsl.jpa.impl.JPAQueryFactory; -//import java.time.format.DateTimeFormatter; -//import java.util.HashSet; -//import java.util.List; -//import java.util.Set; -//import java.util.stream.Collectors; -//import life.mosu.mosuserver.domain.application.Lunch; -//import life.mosu.mosuserver.domain.application.QAdmissionTicketImageJpaEntity; -//import life.mosu.mosuserver.domain.application.QApplicationJpaEntity; -//import life.mosu.mosuserver.domain.application.Subject; -//import life.mosu.mosuserver.domain.applicationschool.QApplicationSchoolJpaEntity; -//import life.mosu.mosuserver.domain.payment.QPaymentJpaEntity; -//import life.mosu.mosuserver.domain.profile.QProfileJpaEntity; -//import life.mosu.mosuserver.domain.school.QSchoolJpaEntity; -//import life.mosu.mosuserver.domain.user.QUserJpaEntity; -//import life.mosu.mosuserver.infra.storage.application.S3Service; -//import life.mosu.mosuserver.presentation.admin.dto.ApplicationExcelDto; -//import life.mosu.mosuserver.presentation.admin.dto.ApplicationFilter; -//import life.mosu.mosuserver.presentation.admin.dto.ApplicationListResponse; -//import life.mosu.mosuserver.presentation.admin.dto.SchoolLunchResponse; -//import life.mosu.mosuserver.presentation.applicationschool.dto.AdmissionTicketResponse; -//import lombok.RequiredArgsConstructor; -//import org.springframework.data.domain.Page; -//import org.springframework.data.domain.PageImpl; -//import org.springframework.data.domain.Pageable; -//import org.springframework.stereotype.Repository; -// -//@Repository -//@RequiredArgsConstructor -//public class ApplicationQueryRepositoryImpl implements ApplicationQueryRepository { -// -// private static final DateTimeFormatter EXCEL_DT_FORMATTER = -// DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); -// -// private final JPAQueryFactory queryFactory; -// private final S3Service s3Service; -// -// private final QProfileJpaEntity profile = QProfileJpaEntity.profileJpaEntity; -// private final QApplicationJpaEntity application = QApplicationJpaEntity.applicationJpaEntity; -// private final QApplicationSchoolJpaEntity applicationSchool = QApplicationSchoolJpaEntity.applicationSchoolJpaEntity; -// private final QPaymentJpaEntity payment = QPaymentJpaEntity.paymentJpaEntity; -// private final QAdmissionTicketImageJpaEntity admissionTicketImage = QAdmissionTicketImageJpaEntity.admissionTicketImageJpaEntity; -// private final QUserJpaEntity user = QUserJpaEntity.userJpaEntity; -// private final QSchoolJpaEntity school = QSchoolJpaEntity.schoolJpaEntity; -// -// @Override -// public Page searchAllApplications(ApplicationFilter filter, -// Pageable pageable) { -// -// JPAQuery query = baseQuery() -// .where( -// buildNameCondition(filter.name()), -// buildPhoneCondition(filter.phone()) -// ) -// .offset(pageable.getOffset()) -// .limit(pageable.getPageSize()); -// -// List content = query.fetch().stream() -// .map(tuple -> { -// Long appSchoolId = tuple.get(applicationSchool.id); -// Set subjects = appSchoolId != null -// ? findSubjectsByApplicationSchoolId(appSchoolId) -// : new HashSet<>(); -// return mapToResponse(tuple, subjects); -// }) -// .toList(); -// -// return new PageImpl<>(content, pageable, content.size()); -// } -// -// @Override -// public List searchAllApplicationsForExcel() { -// JPAQuery query = baseQuery(); -// return query.fetch().stream() -// .map(tuple -> { -// Long appSchoolId = tuple.get(applicationSchool.id); -// Set subjects = appSchoolId != null -// ? findSubjectsByApplicationSchoolId(appSchoolId) -// : new HashSet<>(); -// return mapToExcel(tuple, subjects); -// }) -// .toList(); -// } -// -// @Override -// public List searchAllSchoolLunches() { -// return queryFactory -// .select( -// school.schoolName, -// applicationSchool.lunch.count() -// ) -// .from(applicationSchool) -// .rightJoin(school).on(applicationSchool.schoolId.eq(school.id)) -// .where(applicationSchool.lunch.ne(Lunch.NONE)) -// .groupBy(school.id, school.schoolName) -// .fetch() -// .stream() -// .map(t -> new SchoolLunchResponse( -// t.get(school.schoolName), -// t.get(applicationSchool.lunch.count()) -// )) -// .toList(); -// } -// -// -// private JPAQuery baseQuery() { -// return queryFactory -// .select( -// applicationSchool.id, -// payment.paymentKey, -// applicationSchool.examinationNumber, -// profile.userName, -// profile.gender, -// profile.birth, -// profile.phoneNumber, -// application.guardianPhoneNumber, -// profile.education, -// profile.schoolInfo.schoolName, -// profile.grade, -// applicationSchool.lunch, -// applicationSchool.schoolName, -// applicationSchool.examDate, -// admissionTicketImage.s3Key, -// admissionTicketImage.fileName, -// payment.paymentStatus, -// payment.paymentMethod, -// application.createdAt -// ) -// .from(applicationSchool) -// .leftJoin(application).on(applicationSchool.applicationId.eq(application.id)) -// .leftJoin(payment).on(payment.applicationSchoolId.eq(applicationSchool.id)) -// .leftJoin(user).on(application.userId.eq(user.id)) -// .leftJoin(profile).on(profile.userId.eq(user.id)) -// .leftJoin(admissionTicketImage) -// .on(admissionTicketImage.applicationId.eq(application.id)); -// } -// -// private Predicate buildNameCondition(String name) { -// return (name == null || name.isBlank()) -// ? null -// : profile.userName.contains(name); -// } -// -// private Predicate buildPhoneCondition(String phone) { -// return (phone == null || phone.isBlank()) -// ? null -// : profile.phoneNumber.contains(phone); -// } -// -// private Set findSubjectsByApplicationSchoolId(Long applicationSchoolId) { -// EnumPath subject = Expressions.enumPath(Subject.class, "subject"); -// return new HashSet<>( -// queryFactory -// .select(subject) -// .from(applicationSchool) -// .join(applicationSchool.subjects, subject) -// .where(applicationSchool.id.eq(applicationSchoolId)) -// .fetch() -// ); -// } -// -// private ApplicationListResponse mapToResponse(Tuple tuple, Set subjects) { -// Set subjectNames = subjects.stream() -// .map(Subject::getSubjectName) -// .collect(Collectors.toSet()); -// -// String lunchName = tuple.get(applicationSchool.lunch).getLunchName(); -// -// String s3Key = tuple.get(admissionTicketImage.s3Key); -// String url = getAdmissionTicketImageUrl(s3Key); -// -// AdmissionTicketResponse admissionTicket = AdmissionTicketResponse.of( -// url, -// tuple.get(profile.userName), -// tuple.get(profile.birth), -// tuple.get(applicationSchool.examinationNumber), -// subjectNames, -// tuple.get(applicationSchool.schoolName) -// ); -// -// return new ApplicationListResponse( -// tuple.get(payment.paymentKey), -// tuple.get(applicationSchool.examinationNumber), -// tuple.get(profile.userName), -// tuple.get(profile.gender).getGenderName(), -// tuple.get(profile.birth), -// tuple.get(profile.phoneNumber), -// tuple.get(application.guardianPhoneNumber), -// tuple.get(profile.education).getEducationName(), -// tuple.get(profile.schoolInfo.schoolName), -// tuple.get(profile.grade).getGradeName(), -// lunchName, -// subjectNames, -// tuple.get(applicationSchool.schoolName), -// tuple.get(applicationSchool.examDate), -// tuple.get(admissionTicketImage.fileName), -// tuple.get(payment.paymentStatus), -// tuple.get(payment.paymentMethod), -// tuple.get(application.createdAt), -// admissionTicket -// ); -// } -// -// private ApplicationExcelDto mapToExcel(Tuple tuple, Set subjects) { -// Set subjectNames = subjects.stream() -// .map(Subject::getSubjectName) -// .collect(Collectors.toSet()); -// -// String lunchName = tuple.get(applicationSchool.lunch).getLunchName(); -// String genderName = tuple.get(profile.gender).getGenderName(); -// String gradeName = tuple.get(profile.grade).getGradeName(); -// String educationName = tuple.get(profile.education).getEducationName(); -// String appliedAt = tuple.get(application.createdAt) -// .format(EXCEL_DT_FORMATTER); -// -// return new ApplicationExcelDto( -// tuple.get(payment.paymentKey), -// tuple.get(applicationSchool.examinationNumber), -// tuple.get(profile.userName), -// genderName, -// tuple.get(profile.birth), -// tuple.get(profile.phoneNumber), -// tuple.get(application.guardianPhoneNumber), -// educationName, -// tuple.get(profile.schoolInfo.schoolName), -// gradeName, -// lunchName, -// subjectNames, -// tuple.get(applicationSchool.schoolName), -// tuple.get(applicationSchool.examDate), -// tuple.get(admissionTicketImage.fileName), -// tuple.get(payment.paymentStatus), -// tuple.get(payment.paymentMethod), -// appliedAt -// ); -// } -// -// private String getAdmissionTicketImageUrl(String s3Key) { -// if (s3Key == null || s3Key.isBlank()) { -// return null; -// } -// return s3Service.getPreSignedUrl(s3Key); -// } -//} \ No newline at end of file +package life.mosu.mosuserver.domain.admin; + +import com.querydsl.core.Tuple; +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.EnumPath; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.format.DateTimeFormatter; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import life.mosu.mosuserver.domain.application.QApplicationJpaEntity; +import life.mosu.mosuserver.domain.application.QExamTicketImageJpaEntity; +import life.mosu.mosuserver.domain.application.Subject; +import life.mosu.mosuserver.domain.exam.QExamJpaEntity; +import life.mosu.mosuserver.domain.examapplication.QExamApplicationJpaEntity; +import life.mosu.mosuserver.domain.examapplication.QExamSubjectJpaEntity; +import life.mosu.mosuserver.domain.payment.PaymentStatus; +import life.mosu.mosuserver.domain.payment.QPaymentJpaEntity; +import life.mosu.mosuserver.domain.profile.QProfileJpaEntity; +import life.mosu.mosuserver.domain.user.QUserJpaEntity; +import life.mosu.mosuserver.infra.storage.application.S3Service; +import life.mosu.mosuserver.presentation.admin.dto.ApplicationExcelDto; +import life.mosu.mosuserver.presentation.admin.dto.ApplicationFilter; +import life.mosu.mosuserver.presentation.admin.dto.ApplicationListResponse; +import life.mosu.mosuserver.presentation.admin.dto.ExamTicketResponse; +import life.mosu.mosuserver.presentation.admin.dto.SchoolLunchResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class ApplicationQueryRepositoryImpl implements ApplicationQueryRepository { + + private static final DateTimeFormatter EXCEL_DT_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private final JPAQueryFactory queryFactory; + private final S3Service s3Service; + + private final QProfileJpaEntity profile = QProfileJpaEntity.profileJpaEntity; + private final QApplicationJpaEntity application = QApplicationJpaEntity.applicationJpaEntity; + private final QPaymentJpaEntity payment = QPaymentJpaEntity.paymentJpaEntity; + private final QUserJpaEntity user = QUserJpaEntity.userJpaEntity; + private final QExamJpaEntity exam = QExamJpaEntity.examJpaEntity; + private final QExamApplicationJpaEntity examApplication = QExamApplicationJpaEntity.examApplicationJpaEntity; + private final QExamTicketImageJpaEntity examTicketImage = QExamTicketImageJpaEntity.examTicketImageJpaEntity; + private final QExamSubjectJpaEntity examSubject = QExamSubjectJpaEntity.examSubjectJpaEntity; + + @Override + public Page searchAllApplications(ApplicationFilter filter, + Pageable pageable) { + + JPAQuery query = baseQuery() + .where( + buildNameCondition(filter.name()), + buildPhoneCondition(filter.phone()) + ) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()); + + List content = query.fetch().stream() + .map(tuple -> { + Long appSchoolId = tuple.get(examApplication.id); + Set subjects = appSchoolId != null + ? findSubjectsByExamApplicationId(appSchoolId) + : new HashSet<>(); + return mapToResponse(tuple, subjects); + }) + .toList(); + + return new PageImpl<>(content, pageable, content.size()); + } + + @Override + public List searchAllApplicationsForExcel() { + JPAQuery query = baseQuery(); + return query.fetch().stream() + .map(tuple -> { + Long appSchoolId = tuple.get(examApplication.id); + Set subjects = appSchoolId != null + ? findSubjectsByExamApplicationId(appSchoolId) + : new HashSet<>(); + return mapToExcel(tuple, subjects); + }) + .toList(); + } + + @Override + public List searchAllSchoolLunches() { + return queryFactory + .select( + exam.schoolName, + examApplication.id.count() + ) + .from(examApplication) + .rightJoin(exam).on(examApplication.examId.eq(exam.id)) + .rightJoin(application).on(application.id.eq(examApplication.applicationId)) + .rightJoin(payment).on(application.id.eq(payment.applicationId)) + .where( + examApplication.isLunchChecked.isTrue(), + payment.paymentStatus.eq(PaymentStatus.DONE) + ) + .groupBy(exam.schoolName) + .fetch() + .stream() + .map(t -> new SchoolLunchResponse( + t.get(exam.schoolName), + t.get(examApplication.id.count()) + )) + .toList(); + } + + private JPAQuery baseQuery() { + return queryFactory + .select( + examApplication.id, + payment.paymentKey, + examApplication.examNumber, + profile.userName, + profile.gender, + profile.birth, + profile.phoneNumber, + application.parentPhoneNumber, + profile.education, + profile.schoolInfo.schoolName, + profile.grade, + exam.schoolName, + exam.examDate, + examApplication.isLunchChecked, + examTicketImage.s3Key, + examTicketImage.fileName, + payment.paymentStatus, + payment.paymentMethod, + application.createdAt + ) + .from(examApplication) + .leftJoin(exam).on(examApplication.examId.eq(exam.id)) + .leftJoin(application).on(examApplication.applicationId.eq(application.id)) + .leftJoin(payment).on(payment.applicationId.eq(application.id)) + .leftJoin(user).on(application.userId.eq(user.id)) + .leftJoin(profile).on(profile.userId.eq(user.id)) + .leftJoin(examTicketImage) + .on(examTicketImage.applicationId.eq(application.id)); + } + + private Predicate buildNameCondition(String name) { + return (name == null || name.isBlank()) + ? null + : profile.userName.contains(name); + } + + private Predicate buildPhoneCondition(String phone) { + return (phone == null || phone.isBlank()) + ? null + : profile.phoneNumber.contains(phone); + } + + private Set findSubjectsByExamApplicationId(Long examApplicationId) { + EnumPath subject = Expressions.enumPath(Subject.class, "subject"); + return new HashSet<>( + queryFactory + .select( + examSubject.subject + ) + .from(examSubject) + .where(examSubject.examApplicationId.eq(examApplicationId)) + .fetch() + ); + } + + private ApplicationListResponse mapToResponse(Tuple tuple, Set subjects) { + Set subjectNames = subjects.stream() + .map(Subject::getSubjectName) + .collect(Collectors.toSet()); + + String s3Key = tuple.get(examTicketImage.s3Key); + String url = getAdmissionTicketImageUrl(s3Key); + + ExamTicketResponse examTicketResponse = ExamTicketResponse.of( + url, + tuple.get(profile.userName), + tuple.get(profile.birth), + tuple.get(examApplication.examNumber), + subjectNames, + tuple.get(exam.schoolName) + ); + + return new ApplicationListResponse( + tuple.get(payment.paymentKey), + tuple.get(examApplication.examNumber), + tuple.get(profile.userName), + tuple.get(profile.gender).getGenderName(), + tuple.get(profile.birth), + tuple.get(profile.phoneNumber), + tuple.get(application.parentPhoneNumber), + tuple.get(profile.education).getEducationName(), + tuple.get(profile.schoolInfo.schoolName), + tuple.get(profile.grade).getGradeName(), + tuple.get(examApplication.isLunchChecked), + subjectNames, + tuple.get(exam.schoolName), + tuple.get(exam.examDate), + tuple.get(examTicketImage.fileName), + tuple.get(payment.paymentStatus), + tuple.get(payment.paymentMethod), + tuple.get(application.createdAt), + examTicketResponse + ); + } + + private ApplicationExcelDto mapToExcel(Tuple tuple, Set subjects) { + Set subjectNames = subjects.stream() + .map(Subject::getSubjectName) + .collect(Collectors.toSet()); + + String lunchName = tuple.get(exam.lunchName); + String genderName = tuple.get(profile.gender).getGenderName(); + String gradeName = tuple.get(profile.grade).getGradeName(); + String educationName = tuple.get(profile.education).getEducationName(); + String appliedAt = tuple.get(application.createdAt) + .format(EXCEL_DT_FORMATTER); + + return new ApplicationExcelDto( + tuple.get(payment.paymentKey), + tuple.get(examApplication.examNumber), + tuple.get(profile.userName), + genderName, + tuple.get(profile.birth), + tuple.get(profile.phoneNumber), + tuple.get(application.parentPhoneNumber), + educationName, + tuple.get(profile.schoolInfo.schoolName), + gradeName, + lunchName, + subjectNames, + tuple.get(exam.schoolName), + tuple.get(exam.examDate), + tuple.get(examTicketImage.fileName), + tuple.get(payment.paymentStatus), + tuple.get(payment.paymentMethod), + appliedAt + ); + } + + private String getAdmissionTicketImageUrl(String s3Key) { + if (s3Key == null || s3Key.isBlank()) { + return null; + } + return s3Service.getPreSignedUrl(s3Key); + } +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/domain/admin/RefundQueryRepositoryImpl.java b/src/main/java/life/mosu/mosuserver/domain/admin/RefundQueryRepositoryImpl.java index 540977b2..27451cc6 100644 --- a/src/main/java/life/mosu/mosuserver/domain/admin/RefundQueryRepositoryImpl.java +++ b/src/main/java/life/mosu/mosuserver/domain/admin/RefundQueryRepositoryImpl.java @@ -1,84 +1,82 @@ -//package life.mosu.mosuserver.domain.admin; -// -//import static life.mosu.mosuserver.domain.base.BaseTimeEntity.formatDate; -// -//import com.querydsl.core.Tuple; -//import com.querydsl.jpa.impl.JPAQuery; -//import com.querydsl.jpa.impl.JPAQueryFactory; -//import java.util.List; -//import life.mosu.mosuserver.domain.application.QApplicationJpaEntity; -//import life.mosu.mosuserver.domain.applicationschoolX.QApplicationSchoolJpaEntity; -//import life.mosu.mosuserver.domain.payment.PaymentMethod; -//import life.mosu.mosuserver.domain.payment.QPaymentJpaEntity; -//import life.mosu.mosuserver.domain.profile.QProfileJpaEntity; -//import life.mosu.mosuserver.domain.refund.QRefundJpaEntity; -//import life.mosu.mosuserver.presentation.admin.dto.RefundListResponse; -//import lombok.RequiredArgsConstructor; -//import org.springframework.data.domain.Page; -//import org.springframework.data.domain.PageImpl; -//import org.springframework.data.domain.Pageable; -//import org.springframework.stereotype.Repository; -// -//@Repository -//@RequiredArgsConstructor -//public class RefundQueryRepositoryImpl implements RefundQueryRepository { -// -// private final JPAQueryFactory queryFactory; -// -// QRefundJpaEntity refund = QRefundJpaEntity.refundJpaEntity; -// QApplicationSchoolJpaEntity appSchool = QApplicationSchoolJpaEntity.applicationSchoolJpaEntity; -// QApplicationJpaEntity application = QApplicationJpaEntity.applicationJpaEntity; -// QProfileJpaEntity profile = QProfileJpaEntity.profileJpaEntity; -// QPaymentJpaEntity payment = QPaymentJpaEntity.paymentJpaEntity; -// -// @Override -// public Page searchAllRefunds(Pageable pageable) { -// long total = baseQuery().fetch().size(); -// -// List content = baseQuery() -// .offset(pageable.getOffset()) -// .limit(pageable.getPageSize()) -// .fetch() -// .stream() -// .map(this::mapToResponse) -// .toList(); -// -// return new PageImpl<>(content, pageable, total); -// -// } -// -// private JPAQuery baseQuery() { -// return queryFactory -// .select( -// refund.id, -// appSchool.examinationNumber, -// profile.userName, -// profile.phoneNumber, -// refund.createdAt, -// refund.agreedAt, -// payment.paymentMethod, -// refund.reason -// ) -// .from(refund) -// .leftJoin(appSchool).on(refund.applicationSchoolId.eq(appSchool.id)) -// .leftJoin(application).on(appSchool.applicationId.eq(application.id)) -// .leftJoin(profile).on(profile.userId.eq(application.userId)) -// .leftJoin(payment).on(payment.applicationSchoolId.eq(appSchool.id)); -// } -// -// private RefundListResponse mapToResponse(Tuple tuple) { -// PaymentMethod paymentMethod = tuple.get(payment.paymentMethod); -// return new RefundListResponse( -// tuple.get(refund.id), -// tuple.get(appSchool.examinationNumber), -// tuple.get(profile.userName), -// tuple.get(profile.phoneNumber), -// formatDate(tuple.get(refund.createdAt)), -// formatDate(tuple.get(refund.agreedAt)), -// paymentMethod != null ? paymentMethod.getName() : "N/A", -// tuple.get(refund.reason) -// ); -// } -// -// -//} \ No newline at end of file +package life.mosu.mosuserver.domain.admin; + +import static life.mosu.mosuserver.domain.base.BaseTimeEntity.formatDate; + +import com.querydsl.core.Tuple; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import life.mosu.mosuserver.domain.application.QApplicationJpaEntity; +import life.mosu.mosuserver.domain.examapplication.QExamApplicationJpaEntity; +import life.mosu.mosuserver.domain.payment.PaymentMethod; +import life.mosu.mosuserver.domain.payment.QPaymentJpaEntity; +import life.mosu.mosuserver.domain.profile.QProfileJpaEntity; +import life.mosu.mosuserver.domain.refund.QRefundJpaEntity; +import life.mosu.mosuserver.presentation.admin.dto.RefundListResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class RefundQueryRepositoryImpl implements RefundQueryRepository { + + private final JPAQueryFactory queryFactory; + + private final QRefundJpaEntity refund = QRefundJpaEntity.refundJpaEntity; + private final QExamApplicationJpaEntity examApplication = QExamApplicationJpaEntity.examApplicationJpaEntity; + private final QApplicationJpaEntity application = QApplicationJpaEntity.applicationJpaEntity; + private final QProfileJpaEntity profile = QProfileJpaEntity.profileJpaEntity; + private final QPaymentJpaEntity payment = QPaymentJpaEntity.paymentJpaEntity; + + @Override + public Page searchAllRefunds(Pageable pageable) { + long total = baseQuery().fetch().size(); + + List content = baseQuery() + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch() + .stream() + .map(this::mapToResponse) + .toList(); + + return new PageImpl<>(content, pageable, total); + + } + + private JPAQuery baseQuery() { + return queryFactory + .select( + refund.id, + examApplication.examNumber, + profile.userName, + profile.phoneNumber, + refund.createdAt, + refund.agreedAt, + // payment.paymentMethod, + refund.reason + ) + .from(refund) + .leftJoin(examApplication).on(refund.examApplicationId.eq(examApplication.id)) + .leftJoin(application).on(examApplication.applicationId.eq(application.id)) + .leftJoin(profile).on(profile.userId.eq(application.userId)); + //TODO: payment method join + } + + private RefundListResponse mapToResponse(Tuple tuple) { + PaymentMethod paymentMethod = tuple.get(payment.paymentMethod); + return new RefundListResponse( + tuple.get(refund.id), + tuple.get(examApplication.examNumber), + tuple.get(profile.userName), + tuple.get(profile.phoneNumber), + formatDate(tuple.get(refund.createdAt)), + formatDate(tuple.get(refund.agreedAt)), + paymentMethod != null ? paymentMethod.getName() : "N/A", + tuple.get(refund.reason) + ); + } +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/domain/examapplication/ExamApplicationJpaEntity.java b/src/main/java/life/mosu/mosuserver/domain/examapplication/ExamApplicationJpaEntity.java index dddf42cd..0ffd5cbd 100644 --- a/src/main/java/life/mosu/mosuserver/domain/examapplication/ExamApplicationJpaEntity.java +++ b/src/main/java/life/mosu/mosuserver/domain/examapplication/ExamApplicationJpaEntity.java @@ -27,24 +27,27 @@ public class ExamApplicationJpaEntity { @Column(name = "exam_id") private Long examId; - @Column(name = "lunch_id") - private Long lunchId; + @Column(name = "lunch_checked") + private boolean isLunchChecked; @Column(name = "exam_number") private String examNumber; @Builder - public ExamApplicationJpaEntity(Long applicationId, Long examId, Long lunchId) { + public ExamApplicationJpaEntity(Long applicationId, Long examId, boolean isLunchChecked, + String examNumber) { this.applicationId = applicationId; this.examId = examId; - this.lunchId = lunchId; + this.isLunchChecked = isLunchChecked; + this.examNumber = examNumber; } - public static ExamApplicationJpaEntity create(Long applicationId, Long examId, Long lunchId) { + public static ExamApplicationJpaEntity create(Long applicationId, Long examId, + boolean isLunchChecked) { return ExamApplicationJpaEntity.builder() .applicationId(applicationId) .examId(examId) - .lunchId(lunchId) + .isLunchChecked(isLunchChecked) .build(); } diff --git a/src/main/java/life/mosu/mosuserver/domain/payment/PaymentJpaEntity.java b/src/main/java/life/mosu/mosuserver/domain/payment/PaymentJpaEntity.java index 4dc7b6d0..16ea29e5 100644 --- a/src/main/java/life/mosu/mosuserver/domain/payment/PaymentJpaEntity.java +++ b/src/main/java/life/mosu/mosuserver/domain/payment/PaymentJpaEntity.java @@ -26,8 +26,8 @@ public class PaymentJpaEntity extends BaseTimeEntity { @Column(name = "payment_id") private Long id; - @Column(name = "application_school_id") - private Long applicationSchoolId; + @Column(name = "application_id") + private Long applicationId; @Column(name = "payment_key") private String paymentKey; @@ -48,14 +48,14 @@ public class PaymentJpaEntity extends BaseTimeEntity { @Builder(access = AccessLevel.PRIVATE) private PaymentJpaEntity( - Long applicationSchoolId, + Long applicationId, String paymentKey, String orderId, PaymentAmountVO paymentAmount, PaymentStatus paymentStatus, PaymentMethod paymentMethod ) { - this.applicationSchoolId = applicationSchoolId; + this.applicationId = applicationId; this.paymentKey = paymentKey; this.orderId = orderId; this.paymentAmount = paymentAmount; @@ -64,7 +64,7 @@ private PaymentJpaEntity( } public static PaymentJpaEntity of( - Long applicationSchoolId, + Long applicationId, String paymentKey, String orderId, PaymentStatus paymentStatus, @@ -72,7 +72,7 @@ public static PaymentJpaEntity of( PaymentMethod paymentMethod ) { return PaymentJpaEntity.builder() - .applicationSchoolId(applicationSchoolId) + .applicationId(applicationId) .paymentKey(paymentKey) .orderId(orderId) .paymentStatus(paymentStatus) @@ -82,14 +82,14 @@ public static PaymentJpaEntity of( } public static PaymentJpaEntity ofFailure( - Long applicationSchoolId, + Long applicationId, String orderId, PaymentStatus paymentStatus, Integer totalAmount ) { PaymentAmountVO paymentAmount = PaymentAmountVO.ofFailure(totalAmount); return PaymentJpaEntity.builder() - .applicationSchoolId(applicationSchoolId) + .applicationId(applicationId) .orderId(orderId) .paymentStatus(paymentStatus) .paymentAmount(paymentAmount) diff --git a/src/main/java/life/mosu/mosuserver/domain/refund/RefundJpaEntity.java b/src/main/java/life/mosu/mosuserver/domain/refund/RefundJpaEntity.java index 991cebaa..feeae07e 100644 --- a/src/main/java/life/mosu/mosuserver/domain/refund/RefundJpaEntity.java +++ b/src/main/java/life/mosu/mosuserver/domain/refund/RefundJpaEntity.java @@ -1,14 +1,18 @@ package life.mosu.mosuserver.domain.refund; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDateTime; import life.mosu.mosuserver.domain.base.BaseTimeEntity; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; - @Entity @Getter @Table(name = "refund") @@ -20,10 +24,10 @@ public class RefundJpaEntity extends BaseTimeEntity { @Column(name = "refund_id") private Long id; - @Column(name = "application_school_id") - private Long applicationSchoolId; + @Column(name = "exam_application_id") + private Long examApplicationId; - @Column(name = "refund_reason", nullable = false) + @Column(name = "reason", nullable = false) private String reason; @Column(name = "refund_agreed") @@ -34,15 +38,14 @@ public class RefundJpaEntity extends BaseTimeEntity { @Builder public RefundJpaEntity( - final Long applicationSchoolId, - final String reason, - final Boolean refundAgreed, - final LocalDateTime agreedAt + final Long examApplicationId, + final String reason, + final Boolean refundAgreed, + final LocalDateTime agreedAt ) { - this.applicationSchoolId = applicationSchoolId; + this.examApplicationId = examApplicationId; this.reason = reason; this.refundAgreed = refundAgreed; this.agreedAt = agreedAt; } - } diff --git a/src/main/java/life/mosu/mosuserver/global/initializer/DatabaseInitializer.java b/src/main/java/life/mosu/mosuserver/global/initializer/DatabaseInitializer.java index a62faec4..13889c0f 100644 --- a/src/main/java/life/mosu/mosuserver/global/initializer/DatabaseInitializer.java +++ b/src/main/java/life/mosu/mosuserver/global/initializer/DatabaseInitializer.java @@ -1,380 +1,339 @@ -//package life.mosu.mosuserver.global.initializer; -// -//import jakarta.annotation.PostConstruct; -//import java.time.LocalDate; -//import java.time.LocalDateTime; -//import java.util.ArrayList; -//import java.util.Collections; -//import java.util.HashSet; -//import java.util.List; -//import java.util.Random; -//import java.util.Set; -//import java.util.UUID; -//import life.mosu.mosuserver.domain.application.ApplicationJpaEntity; -//import life.mosu.mosuserver.domain.application.ApplicationJpaRepository; -//import life.mosu.mosuserver.domain.application.Lunch; -//import life.mosu.mosuserver.domain.application.Subject; -//import life.mosu.mosuserver.domain.applicationschool.ApplicationSchoolJpaEntity; -//import life.mosu.mosuserver.domain.applicationschool.ApplicationSchoolJpaRepository; -//import life.mosu.mosuserver.domain.event.DurationJpaVO; -//import life.mosu.mosuserver.domain.event.EventJpaEntity; -//import life.mosu.mosuserver.domain.event.EventJpaRepository; -//import life.mosu.mosuserver.domain.inquiry.InquiryJpaEntity; -//import life.mosu.mosuserver.domain.inquiry.InquiryJpaRepository; -//import life.mosu.mosuserver.domain.inquiryAnswer.InquiryAnswerJpaEntity; -//import life.mosu.mosuserver.domain.inquiryAnswer.InquiryAnswerJpaRepository; -//import life.mosu.mosuserver.domain.notice.NoticeJpaEntity; -//import life.mosu.mosuserver.domain.notice.NoticeJpaRepository; -//import life.mosu.mosuserver.domain.payment.PaymentAmountVO; -//import life.mosu.mosuserver.domain.payment.PaymentJpaEntity; -//import life.mosu.mosuserver.domain.payment.PaymentMethod; -//import life.mosu.mosuserver.domain.payment.PaymentRepository; -//import life.mosu.mosuserver.domain.payment.PaymentStatus; -//import life.mosu.mosuserver.domain.profile.Education; -//import life.mosu.mosuserver.domain.profile.Gender; -//import life.mosu.mosuserver.domain.profile.Grade; -//import life.mosu.mosuserver.domain.profile.ProfileJpaEntity; -//import life.mosu.mosuserver.domain.profile.ProfileJpaRepository; -//import life.mosu.mosuserver.domain.profile.SchoolInfoJpaVO; -//import life.mosu.mosuserver.domain.exam.AddressJpaVO; -//import life.mosu.mosuserver.domain.exam.Area; -//import life.mosu.mosuserver.domain.school.LunchMenu; -//import life.mosu.mosuserver.domain.school.SchoolJpaEntity; -//import life.mosu.mosuserver.domain.school.SchoolJpaRepository; -//import life.mosu.mosuserver.domain.user.UserJpaEntity; -//import life.mosu.mosuserver.domain.user.UserJpaRepository; -//import life.mosu.mosuserver.domain.user.UserRole; -//import lombok.RequiredArgsConstructor; -//import lombok.extern.slf4j.Slf4j; -//import org.springframework.security.crypto.password.PasswordEncoder; -//import org.springframework.stereotype.Component; -// -//@Slf4j -//@Component -//@RequiredArgsConstructor -//public class DatabaseInitializer { -// -// private final UserJpaRepository userRepository; -// private final ProfileJpaRepository profileRepository; -// private final SchoolJpaRepository schoolRepository; -// private final ApplicationJpaRepository applicationRepository; -// private final ApplicationSchoolJpaRepository applicationSchoolRepository; -// private final InquiryJpaRepository inquiryJpaRepository; -// private final NoticeJpaRepository noticeJpaRepository; -// private final InquiryAnswerJpaRepository inquiryAnswerJpaRepository; -// private final EventJpaRepository eventRepository; -// private final PaymentRepository paymentRepository; -// private final PasswordEncoder passwordEncoder; -// -// @PostConstruct -// public void init() { -// if (userRepository.count() > 0 || schoolRepository.count() > 0) { -// log.info("이미 더미 데이터가 존재하여 전체 초기화를 건너뜁니다."); -// return; -// } -// -// log.info("전체 더미 데이터 초기화를 시작합니다..."); -// Random random = new Random(); -// -// List createdUsers = initializeUsersAndProfiles(random); -// List createdSchools = initializeSchools(); -// List createdAppSchools = initializeApplications(createdUsers, -// createdSchools, random); -// initializePayments(createdAppSchools); -// initializeBoardItems(createdUsers, random); -// -// log.info("모든 더미 데이터 초기화가 완료되었습니다."); -// } -// -// private List initializeUsersAndProfiles(Random random) { -// List createdUsers = new ArrayList<>(); -// for (int i = 1; i <= 10; i++) { -// UserJpaEntity user = UserJpaEntity.builder() -// .loginId("user" + i) -// .password(passwordEncoder.encode("password" + i + "!")) -// .gender((i % 2 == 0) ? Gender.MALE : Gender.FEMALE) -// .name((i % 2 == 0) ? "김철수" + i : "이영희" + i) -// .birth(LocalDate.of(1990 + (i % 5), (i % 12) + 1, (i % 28) + 1)) -// .agreedToTermsOfService(true) -// .agreedToPrivacyPolicy(true) -// .agreedToMarketing(random.nextBoolean()) -// .userRole((i == 1) ? UserRole.ROLE_ADMIN : UserRole.ROLE_USER) -// .build(); -// userRepository.save(user); -// createdUsers.add(user); -// -// ProfileJpaEntity profile = ProfileJpaEntity.builder() -// .userId(user.getId()) -// .userName(user.getName()) -// .gender(user.getGender()) -// .birth(user.getBirth()) -// .phoneNumber("010-4870-5466") -// .email("user" + i + "@example.com") -// .education(Education.values()[random.nextInt(Education.values().length)]) -// .schoolInfo(new SchoolInfoJpaVO("모수대학교" + (i % 3 + 1), "123-23", "서울시 모수구 모수동")) -// .grade(Grade.values()[random.nextInt(Grade.values().length)]) -// .build(); -// -// profile.registerRecommenderPhoneNumber((i % 3 == 0) ? "010-1234-5678" : null); -// profileRepository.save(profile); -// } -// log.info("User 및 Profile 데이터 {}건 생성 완료.", createdUsers.size()); -// return createdUsers; -// } -// -// private List initializeSchools() { -// List schools = new ArrayList<>(List.of( -// SchoolJpaEntity.builder() -// .schoolName("대치중학교") -// .area(Area.DAECHI) -// .address(new AddressJpaVO("06234", "서울특별시", "강남구 대치동 987")) -// .examDate(LocalDate.of(2025, 10, 19)) -// .capacity(532L) -// .deadlineTime(LocalDateTime.of(2025, 10, 10, 23, 59)) -// .lunchMenu( -// Set.of( -// LunchMenu.of( -// Lunch.OPTION1, 12000 -// ), -// LunchMenu.of( -// Lunch.OPTION2, 13000 -// ) -// ) -// ) -// .build(), -// SchoolJpaEntity.builder() -// .schoolName("목운중학교") -// .area(Area.MOKDONG) -// .address(new AddressJpaVO("07995", "서울특별시", "양천구 목동서로 369")) -// .examDate(LocalDate.of(2025, 10, 26)) -// .capacity(896L) -// .deadlineTime(LocalDateTime.of(2025, 10, 17, 23, 59)) -// .lunchMenu( -// Set.of( -// LunchMenu.of( -// Lunch.OPTION1, 12000 -// ), -// LunchMenu.of( -// Lunch.OPTION2, 13000 -// ) -// ) -// ) -// .build(), -// SchoolJpaEntity.builder() -// .schoolName("신서중학교") -// .area(Area.MOKDONG) -// .address(new AddressJpaVO("08018", "서울특별시", "양천구 신정로 250")) -// .examDate(LocalDate.of(2025, 11, 2)) -// .capacity(896L) -// .deadlineTime(LocalDateTime.of(2025, 10, 24, 23, 59)) -// .lunchMenu( -// Set.of( -// LunchMenu.of( -// Lunch.OPTION4, 12000 -// ), -// LunchMenu.of( -// Lunch.OPTION2, 13000 -// ) -// ) -// ) -// .build(), -// SchoolJpaEntity.builder() -// .schoolName("개원중학교") -// .area(Area.DAECHI) -// .address(new AddressJpaVO("06327", "서울특별시", "강남구 개포로 619")) -// .examDate(LocalDate.of(2025, 10, 26)) -// .capacity(840L) -// .deadlineTime(LocalDateTime.of(2025, 10, 17, 23, 59)) -// .lunchMenu( -// Set.of( -// LunchMenu.of( -// Lunch.OPTION4, 12000 -// ), -// LunchMenu.of( -// Lunch.OPTION2, 13000 -// ) -// ) -// ) -// .build(), -// SchoolJpaEntity.builder() -// .schoolName("문래중학교") -// .area(Area.MOKDONG) -// .address(new AddressJpaVO("07291", "서울특별시", "영등포구 문래로 195")) -// .examDate(LocalDate.of(2025, 10, 19)) -// .capacity(558L) -// .deadlineTime(LocalDateTime.of(2025, 10, 10, 23, 59)) -// .lunchMenu( -// Set.of( -// LunchMenu.of( -// Lunch.OPTION6, 12000 -// ), -// LunchMenu.of( -// Lunch.OPTION2, 13000 -// ) -// ) -// ) -// .build(), -// SchoolJpaEntity.builder() -// .schoolName("온곡중학교") -// .area(Area.NOWON) -// .address(new AddressJpaVO("01673", "서울특별시", "노원구 덕릉로 70길 99")) -// .examDate(LocalDate.of(2025, 10, 19)) -// .capacity(448L) -// .deadlineTime(LocalDateTime.of(2025, 10, 10, 23, 59)) -// .lunchMenu( -// Set.of( -// LunchMenu.of( -// Lunch.OPTION1, 12000 -// ), -// LunchMenu.of( -// Lunch.OPTION3, 13000 -// ) -// ) -// ) -// .build() -// )); -// schoolRepository.saveAll(schools); -// -// log.info("School 데이터 {}건 생성 완료.", schools.size()); -// return schools; -// } -// -// private List initializeApplications(List users, -// List schools, -// Random random) { -// List createdAppSchools = new ArrayList<>(); -// for (int i = 0; i < users.size(); i++) { -// UserJpaEntity user = users.get(i); -// ApplicationJpaEntity application = applicationRepository.save( -// ApplicationJpaEntity.builder() -// .userId(user.getId()) -// .guardianPhoneNumber("010-9876-" + String.format("%04d", 1000 + i)) -// .agreedToNotices(true) -// .agreedToRefundPolicy(true) -// .build() -// ); -// -// Collections.shuffle(schools); -// int schoolsToApply = random.nextInt(2) + 2; -// -// for (int j = 0; j < Math.min(schoolsToApply, schools.size()); j++) { -// SchoolJpaEntity school = schools.get(j); -// Set subjects = new HashSet<>(); -// subjects.add(Subject.values()[random.nextInt(Subject.values().length)]); -// if (random.nextBoolean()) { -// subjects.add(Subject.values()[random.nextInt(Subject.values().length)]); -// } -// -// ApplicationSchoolJpaEntity appSchool = applicationSchoolRepository.save( -// ApplicationSchoolJpaEntity.builder() -// .userId(user.getId()) -// .applicationId(application.getId()) -// .schoolId(school.getId()) -// .schoolName(school.getSchoolName()) -// .area(school.getArea()) -// .address(school.getAddress()) -// .examDate(LocalDate.of(2025, 10, 20 + i)) -// .lunch(Lunch.values()[random.nextInt(Lunch.values().length)]) -// .examinationNumber( -// String.format("EXAM-%d-%d", user.getId(), school.getId())) -// .subjects(subjects) -// .build() -// ); -// createdAppSchools.add(appSchool); -// } -// } -// log.info("Application 및 ApplicationSchool 데이터 생성 완료."); -// return createdAppSchools; -// } -// -// private void initializePayments(List applicationSchools) { -// List payments = new ArrayList<>(); -// int successCount = applicationSchools.size() / 2; -// -// for (int i = 0; i < applicationSchools.size(); i++) { -// ApplicationSchoolJpaEntity appSchool = applicationSchools.get(i); -// String orderId = "order-" + UUID.randomUUID().toString().substring(0, 8); -// -// if (i < successCount) { -// PaymentAmountVO paymentAmount = PaymentAmountVO.of( -// 12000, -// 10909, -// 1091, -// 12000, -// 0 -// ); -// -// PaymentJpaEntity payment = PaymentJpaEntity.of( -// appSchool.getId(), -// "pkey-" + UUID.randomUUID().toString().substring(0, 12), -// orderId, -// PaymentStatus.DONE, -// paymentAmount, -// PaymentMethod.CARD -// ); -// payments.add(payment); -// } else { -// PaymentJpaEntity payment = PaymentJpaEntity.ofFailure( -// appSchool.getId(), -// orderId, -// PaymentStatus.ABORTED, -// 12000 -// ); -// payments.add(payment); -// } -// } -// paymentRepository.saveAll(payments); -// log.info("Payment 데이터 {}건 생성 완료 (성공: {}, 실패: {}).", payments.size(), successCount, -// payments.size() - successCount); -// } -// -// private void initializeBoardItems(List users, Random random) { -// for (int i = 1; i <= 10; i++) { -// UserJpaEntity author = users.get(random.nextInt(users.size())); -// noticeJpaRepository.save(NoticeJpaEntity.builder() -// .title("공지사항 제목 " + i) -// .content("이것은 " + i + "번째 공지사항의 내용입니다.") -// .userId(author.getId()) -// .author(author.getName()) -// .build()); -// } -// -// List inquiries = new ArrayList<>(); -// for (int i = 1; i <= 10; i++) { -// UserJpaEntity author = users.get(random.nextInt(users.size())); -// inquiries.add(inquiryJpaRepository.save( -// InquiryJpaEntity.builder() -// .title("문의 제목 " + i) -// .content("안녕하세요. " + i + "번째 문의 내용입니다.") -// .userId(author.getId()) -// .author(author.getName()) -// .build()) -// ); -// } -// -// for (int i = 1; i <= 10; i++) { -// InquiryJpaEntity inquiryToAnswer = inquiries.get(random.nextInt(inquiries.size())); -// -// UserJpaEntity answerer = users.get(random.nextInt(users.size())); -// inquiryAnswerJpaRepository.save(InquiryAnswerJpaEntity.builder() -// .title("Re: " + inquiryToAnswer.getTitle()) -// .content("문의하신 내용에 대한 답변입니다. " + i + "번째 답변입니다.") -// .inquiryId(inquiryToAnswer.getId()) -// .userId(answerer.getId()) -// .build()); -// inquiryToAnswer.updateStatusToComplete(); -// inquiryJpaRepository.save(inquiryToAnswer); -// } -// -// for (int i = 1; i <= 10; i++) { -// LocalDate startDate = LocalDate.now().plusDays(i * 2); -// LocalDate endDate = startDate.plusDays(random.nextInt(7) + 3); -// eventRepository.save(EventJpaEntity.builder() -// .title("이벤트 제목 " + i) -// .duration(new DurationJpaVO(startDate, endDate)) -// .eventLink("https://example.com/event/" + i) -// .build()); -// } -// log.info("Board(Notice, Inquiry, Event) 데이터 생성 완료."); -// } -//} +package life.mosu.mosuserver.global.initializer; + +import jakarta.annotation.PostConstruct; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.UUID; +import life.mosu.mosuserver.domain.application.ApplicationJpaEntity; +import life.mosu.mosuserver.domain.application.ApplicationJpaRepository; +import life.mosu.mosuserver.domain.application.Subject; +import life.mosu.mosuserver.domain.event.DurationJpaVO; +import life.mosu.mosuserver.domain.event.EventJpaEntity; +import life.mosu.mosuserver.domain.event.EventJpaRepository; +import life.mosu.mosuserver.domain.exam.AddressJpaVO; +import life.mosu.mosuserver.domain.exam.Area; +import life.mosu.mosuserver.domain.exam.ExamJpaEntity; +import life.mosu.mosuserver.domain.exam.ExamJpaRepository; +import life.mosu.mosuserver.domain.examapplication.ExamApplicationJpaEntity; +import life.mosu.mosuserver.domain.examapplication.ExamApplicationJpaRepository; +import life.mosu.mosuserver.domain.examapplication.ExamSubjectJpaEntity; +import life.mosu.mosuserver.domain.examapplication.ExamSubjectJpaRepository; +import life.mosu.mosuserver.domain.inquiry.InquiryJpaEntity; +import life.mosu.mosuserver.domain.inquiry.InquiryJpaRepository; +import life.mosu.mosuserver.domain.inquiryAnswer.InquiryAnswerJpaEntity; +import life.mosu.mosuserver.domain.inquiryAnswer.InquiryAnswerJpaRepository; +import life.mosu.mosuserver.domain.notice.NoticeJpaEntity; +import life.mosu.mosuserver.domain.notice.NoticeJpaRepository; +import life.mosu.mosuserver.domain.payment.PaymentAmountVO; +import life.mosu.mosuserver.domain.payment.PaymentJpaEntity; +import life.mosu.mosuserver.domain.payment.PaymentMethod; +import life.mosu.mosuserver.domain.payment.PaymentRepository; +import life.mosu.mosuserver.domain.payment.PaymentStatus; +import life.mosu.mosuserver.domain.profile.Education; +import life.mosu.mosuserver.domain.profile.Gender; +import life.mosu.mosuserver.domain.profile.Grade; +import life.mosu.mosuserver.domain.profile.ProfileJpaEntity; +import life.mosu.mosuserver.domain.profile.ProfileJpaRepository; +import life.mosu.mosuserver.domain.profile.SchoolInfoJpaVO; +import life.mosu.mosuserver.domain.user.UserJpaEntity; +import life.mosu.mosuserver.domain.user.UserJpaRepository; +import life.mosu.mosuserver.domain.user.UserRole; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class DatabaseInitializer { + + // 모의고사 1회당 기본 응시료 + private static final int BASE_EXAM_FEE = 50000; + + private final UserJpaRepository userRepository; + private final ProfileJpaRepository profileRepository; + private final ExamJpaRepository examRepository; + private final ApplicationJpaRepository applicationRepository; + private final ExamApplicationJpaRepository examApplicationRepository; + private final ExamSubjectJpaRepository examSubjectRepository; + private final PaymentRepository paymentRepository; + private final NoticeJpaRepository noticeRepository; + private final InquiryJpaRepository inquiryRepository; + private final InquiryAnswerJpaRepository inquiryAnswerRepository; + private final EventJpaRepository eventRepository; + private final PasswordEncoder passwordEncoder; + + @PostConstruct + public void init() { + if (userRepository.count() > 0 || examRepository.count() > 0) { + log.info("이미 더미 데이터가 존재하여 전체 초기화를 건너뜁니다."); + return; + } + + log.info("전체 더미 데이터 초기화를 시작합니다... 🚀"); + Random random = new Random(); + + // 1. 유저 및 프로필 생성 + List users = initializeUsersAndProfiles(random); + + // 2. 시험 정보 생성 + List exams = initializeExams(); + + // 3. 유저별 시험 신청 및 결제 정보 생성 + initializeApplicationsAndPayments(users, exams, random); + + // 4. 게시판 관련 데이터 생성 (공지, 문의, 이벤트) + initializeBoardItems(users, random); + + log.info("✅ 모든 더미 데이터 초기화가 완료되었습니다."); + } + + private List initializeUsersAndProfiles(Random random) { + List createdUsers = new ArrayList<>(); + for (int i = 1; i <= 10; i++) { + UserJpaEntity user = UserJpaEntity.builder() + .loginId("user" + i) + .password(passwordEncoder.encode("password" + i + "!")) + .gender((i % 2 == 0) ? Gender.MALE : Gender.FEMALE) + .name((i % 2 == 0) ? "김모수" + i : "이모수" + i) + .birth(LocalDate.of(2005 + (i % 3), (i % 12) + 1, (i % 28) + 1)) + .agreedToTermsOfService(true) + .agreedToPrivacyPolicy(true) + .agreedToMarketing(random.nextBoolean()) + .userRole((i == 1) ? UserRole.ROLE_ADMIN : UserRole.ROLE_USER) + .build(); + userRepository.save(user); + createdUsers.add(user); + + ProfileJpaEntity profile = ProfileJpaEntity.builder() + .userId(user.getId()) + .userName(user.getName()) + .gender(user.getGender()) + .birth(user.getBirth()) + .phoneNumber("010-1234-" + String.format("%04d", i)) + .email("user" + i + "@mosu.life") + .education(Education.ENROLLED) + .schoolInfo( + new SchoolInfoJpaVO( + "모수고등학교", + "서울시 모수구 모수동 123-45", + "12345" + ) + ) + .grade(Grade.values()[random.nextInt(Grade.values().length)]) + .build(); + profileRepository.save(profile); + } + log.info("👤 User 및 Profile 데이터 {}건 생성 완료.", createdUsers.size()); + return createdUsers; + } + + private List initializeExams() { + // 사용자가 제공한 확정된 시험 정보로 수정 + // 수정 가능한 리스트로 만들기 위해 new ArrayList<>()로 감싸기 + List exams = new ArrayList<>(List.of( + // 대치중학교 + createExam("대치중학교", Area.DAECHI, "06234", "강남구 대치동 987", + LocalDate.of(2025, 10, 19), 532, "제육김치덮밥", 8000), + + // 목운중학교 + createExam("목운중학교", Area.MOKDONG, "07995", "양천구 목동서로 369", + LocalDate.of(2025, 10, 26), 896, "함박스테이크", 9500), + + // 신서중학교 + createExam("신서중학교", Area.MOKDONG, "08018", "양천구 신정로 250", + LocalDate.of(2025, 11, 2), 896, "돈까스카레", 9000), + + // 개원중학교 (10/26 시험) + createExam("개원중학교", Area.DAECHI, "06327", "강남구 개포로 619", + LocalDate.of(2025, 10, 26), 840, "치킨마요덮밥", 8500), + // 개원중학교 (11/2 시험) + createExam("개원중학교", Area.DAECHI, "06327", "강남구 개포로 619", + LocalDate.of(2025, 11, 2), 840, "소불고기덮밥", 9000), + + // 문래중학교 + createExam("문래중학교", Area.MOKDONG, "07291", "영등포구 문래로 195", + LocalDate.of(2025, 10, 19), 558, null, null), // 도시락 미제공 + + // 온곡중학교 (10/19 시험) + createExam("온곡중학교", Area.NOWON, "01673", "노원구 덕릉로 70길 99", + LocalDate.of(2025, 10, 19), 448, "유부초밥&우동", 7500), + // 온곡중학교 (11/2 시험) + createExam("온곡중학교", Area.NOWON, "01673", "노원구 덕릉로 70길 99", + LocalDate.of(2025, 11, 2), 448, "참치김치찌개", 8000) + )); + examRepository.saveAll(exams); + log.info("🏫 Exam 데이터 {}건 생성 완료.", exams.size()); + return exams; + } + + private ExamJpaEntity createExam(String schoolName, Area area, String zipcode, String street, + LocalDate examDate, int capacity, String lunchName, Integer lunchPrice) { + return ExamJpaEntity.builder() + .schoolName(schoolName) + .area(area) + .address(new AddressJpaVO(zipcode, "서울특별시", street)) + .examDate(examDate) + .capacity(capacity) + .deadlineTime(examDate.minusDays(7).atTime(23, 59, 59)) + .lunchName(lunchName) + .lunchPrice(lunchPrice) + .build(); + } + + private void initializeApplicationsAndPayments(List users, + List exams, Random random) { + int successfulPayments = users.size() / 2; + int paymentCounter = 0; + + for (UserJpaEntity user : users) { + // 1. 유저별로 하나의 신청(Application) 묶음 생성 + ApplicationJpaEntity application = applicationRepository.save( + ApplicationJpaEntity.builder() + .userId(user.getId()) + .parentPhoneNumber("010-9876-" + String.format("%04d", user.getId())) + .agreedToNotices(true) + .agreedToRefundPolicy(true) + .build() + ); + + // 2. 해당 신청에 1~3개의 시험 응시(ExamApplication)를 추가 + Collections.shuffle(exams); + int examsToApplyCount = random.nextInt(3) + 1; // 1~3개 시험 신청 + int totalAmount = 0; + List currentExamApplications = new ArrayList<>(); + + for (int j = 0; j < Math.min(examsToApplyCount, exams.size()); j++) { + ExamJpaEntity exam = exams.get(j); + boolean isLunchChecked = (exam.getLunchPrice() != null) && random.nextBoolean(); + + totalAmount += BASE_EXAM_FEE; + + ExamApplicationJpaEntity examApplication = examApplicationRepository.save( + ExamApplicationJpaEntity.builder() + .applicationId(application.getId()) + .examId(exam.getId()) + .examNumber(String.format("MOSU-%d-%d", application.getId(), + exam.getId())) + .isLunchChecked( + ((j % 2 == 0) ? true : false) + ) + .build() + ); + currentExamApplications.add(examApplication); + } + + // 3. 각 시험 응시(ExamApplication)에 대한 선택 과목(ExamSubject) 생성 + for (ExamApplicationJpaEntity examApp : currentExamApplications) { + Set subjects = new HashSet<>(); + int subjectCount = random.nextInt(2) + 1; // 1~2개 과목 선택 + while (subjects.size() < subjectCount) { + subjects.add(Subject.values()[random.nextInt(Subject.values().length)]); + } + subjects.forEach(subject -> + examSubjectRepository.save( + new ExamSubjectJpaEntity(examApp.getId(), subject) + ) + ); + } + + // 4. 생성된 신청(Application) 묶음에 대한 결제(Payment) 정보 생성 + String orderId = "order-" + UUID.randomUUID().toString().substring(0, 12); + if (paymentCounter < successfulPayments) { + // 결제 성공 케이스 + PaymentJpaEntity payment = PaymentJpaEntity.of( + application.getId(), + "pkey-" + UUID.randomUUID().toString().substring(0, 18), + orderId, + PaymentStatus.DONE, + PaymentAmountVO.of( + totalAmount, totalAmount, totalAmount, 0, 0 + ), + PaymentMethod.CARD + ); + paymentRepository.save(payment); + } else { + // 결제 실패 케이스 (취소, 시간 초과 등) + PaymentStatus failureStatus = + random.nextBoolean() ? PaymentStatus.ABORTED : PaymentStatus.EXPIRED; + PaymentJpaEntity payment = PaymentJpaEntity.of( + application.getId(), + "pkey-" + UUID.randomUUID().toString().substring(0, 18), + orderId, + failureStatus, + PaymentAmountVO.of( + totalAmount, totalAmount, totalAmount, 0, 0 + ), + PaymentMethod.CARD + ); + paymentRepository.save(payment); + } + paymentCounter++; + } + log.info("📝 Application, ExamApplication, ExamSubject, Payment 데이터 생성 완료 (성공: {}, 실패: {}).", + successfulPayments, users.size() - successfulPayments); + } + + private void initializeBoardItems(List users, Random random) { + UserJpaEntity admin = users.stream().filter(u -> u.getUserRole() == UserRole.ROLE_ADMIN) + .findFirst().orElse(users.get(0)); + + // 공지사항 생성 + for (int i = 1; i <= 10; i++) { + noticeRepository.save(NoticeJpaEntity.builder() + .title("중요 공지사항 #" + i) + .content("제 " + i + "차 모의고사 관련 안내입니다. 내용을 필히 숙지해주시기 바랍니다.") + .userId(admin.getId()) + .author(admin.getName()) + .build()); + } + + // 문의 및 답변 생성 + List inquiries = new ArrayList<>(); + for (int i = 1; i <= 10; i++) { + UserJpaEntity author = users.get(random.nextInt(users.size())); + InquiryJpaEntity inquiry = inquiryRepository.save( + InquiryJpaEntity.builder() + .title("결제 관련 문의 드립니다 (" + i + ")") + .content("안녕하세요. " + i + "번째 문의 내용입니다. 확인 부탁드립니다.") + .userId(author.getId()) + .author(author.getName()) + .build() + ); + inquiries.add(inquiry); + } + + // 7개의 문의에만 답변 달기 + Collections.shuffle(inquiries); + for (int i = 0; i < 7; i++) { + InquiryJpaEntity inquiryToAnswer = inquiries.get(i); + InquiryAnswerJpaEntity inquiryAnswer = InquiryAnswerJpaEntity.builder() + .title("Re: " + inquiryToAnswer.getTitle()) + .content("문의하신 내용에 대한 답변입니다. 확인 후 추가 문의사항이 있으시면 다시 글 남겨주세요.") + .inquiryId(inquiryToAnswer.getId()) + .userId(admin.getId()) + .build(); + inquiryAnswerRepository.save(inquiryAnswer); + inquiryToAnswer.updateStatusToComplete(); + inquiryRepository.save(inquiryToAnswer); + } + + // 이벤트 생성 + for (int i = 1; i <= 10; i++) { + LocalDate startDate = LocalDate.now().plusDays(i * 5L); + LocalDate endDate = startDate.plusDays(random.nextInt(10) + 5); + eventRepository.save(EventJpaEntity.builder() + .title("여름방학 맞이 특별 이벤트 #" + i) + .duration(new DurationJpaVO(startDate, endDate)) + .eventLink("https://mosu.life/event/" + i) + .build()); + } + log.info("📋 Board(Notice, Inquiry, Event) 데이터 생성 완료."); + } +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/presentation/admin/AdminController.java b/src/main/java/life/mosu/mosuserver/presentation/admin/AdminController.java index 664097e1..a5b0b803 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/admin/AdminController.java +++ b/src/main/java/life/mosu/mosuserver/presentation/admin/AdminController.java @@ -6,11 +6,15 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.List; +import life.mosu.mosuserver.application.admin.AdminService; import life.mosu.mosuserver.global.util.ApiResponseWrapper; +import life.mosu.mosuserver.global.util.excel.SimpleExcelFile; +import life.mosu.mosuserver.presentation.admin.dto.ApplicationExcelDto; import life.mosu.mosuserver.presentation.admin.dto.ApplicationFilter; import life.mosu.mosuserver.presentation.admin.dto.ApplicationListResponse; import life.mosu.mosuserver.presentation.admin.dto.RefundListResponse; import life.mosu.mosuserver.presentation.admin.dto.SchoolLunchResponse; +import life.mosu.mosuserver.presentation.admin.dto.StudentExcelDto; import life.mosu.mosuserver.presentation.admin.dto.StudentFilter; import life.mosu.mosuserver.presentation.admin.dto.StudentListResponse; import lombok.RequiredArgsConstructor; @@ -29,15 +33,16 @@ @RequestMapping("/admin") public class AdminController implements AdminControllerDocs { + private final AdminService adminService; @GetMapping("/students") //@PreAuthorize("isAuthenticated() and hasRole('ADMIN')") public ResponseEntity>> getStudents( @Valid @ModelAttribute StudentFilter filter, - Pageable pageable + @PageableDefault(size = 10) Pageable pageable ) { -// Page result = adminService.getStudents(filter, pageable); - return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "학생 목록 조회 성공", null)); + Page result = adminService.getStudents(filter, pageable); + return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "학생 목록 조회 성공", result)); } @GetMapping("/excel/students") @@ -51,29 +56,29 @@ public void downloadStudentInfo( "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName); -// List data = adminService.getStudentExcelData(); -// SimpleExcelFile excelFile = new SimpleExcelFile<>(data, -// StudentExcelDto.class); + List data = adminService.getStudentExcelData(); + SimpleExcelFile excelFile = new SimpleExcelFile<>(data, + StudentExcelDto.class); -// excelFile.write(response.getOutputStream()); + excelFile.write(response.getOutputStream()); } @GetMapping("/lunches") // @PreAuthorize("isAuthenticated() and hasRole('ADMIN')") public ResponseEntity>> getLunchCounts() { -// List result = adminService.getLunchCounts(); + List result = adminService.getLunchCounts(); return ResponseEntity.ok( - ApiResponseWrapper.success(HttpStatus.OK, "학교별 도시락 신청 수 조회 성공", null)); + ApiResponseWrapper.success(HttpStatus.OK, "학교별 도시락 신청 수 조회 성공", result)); } @GetMapping("/applications") // @PreAuthorize("isAuthenticated() and hasRole('ADMIN')") public ResponseEntity>> getApplications( @Valid @ModelAttribute ApplicationFilter filter, - Pageable pageable + @PageableDefault(size = 10) Pageable pageable ) { -// Page result = adminService.getApplications(filter, pageable); - return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "신청 목록 조회 성공", null)); + Page result = adminService.getApplications(filter, pageable); + return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "신청 목록 조회 성공", result)); } @GetMapping("/excel/applications") @@ -86,11 +91,11 @@ public void downloadApplicationInfo(HttpServletResponse response) throws IOExcep "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName); -// List data = adminService.getApplicationExcelData(); -// SimpleExcelFile excelFile = new SimpleExcelFile<>(data, -// ApplicationExcelDto.class); -// -// excelFile.write(response.getOutputStream()); + List data = adminService.getApplicationExcelData(); + SimpleExcelFile excelFile = new SimpleExcelFile<>(data, + ApplicationExcelDto.class); + + excelFile.write(response.getOutputStream()); } @GetMapping("/refunds") diff --git a/src/main/java/life/mosu/mosuserver/presentation/admin/AdminControllerDocs.java b/src/main/java/life/mosu/mosuserver/presentation/admin/AdminControllerDocs.java index 9d513682..88a40e30 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/admin/AdminControllerDocs.java +++ b/src/main/java/life/mosu/mosuserver/presentation/admin/AdminControllerDocs.java @@ -13,23 +13,25 @@ import life.mosu.mosuserver.global.util.ApiResponseWrapper; import life.mosu.mosuserver.presentation.admin.dto.ApplicationFilter; import life.mosu.mosuserver.presentation.admin.dto.ApplicationListResponse; +import life.mosu.mosuserver.presentation.admin.dto.RefundListResponse; import life.mosu.mosuserver.presentation.admin.dto.SchoolLunchResponse; import life.mosu.mosuserver.presentation.admin.dto.StudentFilter; import life.mosu.mosuserver.presentation.admin.dto.StudentListResponse; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; @Tag(name = "Admin API", description = "관리자용 데이터 조회 및 엑셀 다운로드 API 명세") public interface AdminControllerDocs { - @Operation(summary = "학생 목록 조회", description = "필터 조건에 따른 학생 목록을 페이징하여 조회합니다.") + @Operation(summary = "학생 목록 및 단건 조회", description = "필터 조건에 따라 단건 데이터가 조회됩니다. 전체 목록을 조회하는 필터는 admin/applications?page=0 해당 방식으로 사용하시면 됩니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "학생 목록 조회 성공", content = @Content(schema = @Schema(implementation = StudentListResponse.class))) }) ResponseEntity>> getStudents( - @Parameter(description = "학생 목록 조회 필터") + @Parameter(description = "(단건 조회시 사용)학생 목록 조회 필터") StudentFilter filter, @Parameter(hidden = true) @@ -44,20 +46,20 @@ void downloadStudentInfo( @Parameter(hidden = true) HttpServletResponse response ) throws IOException; - @Operation(summary = "학교별 도시락 신청 수 조회", description = "학교별 도시락 신청 수를 조회합니다.") + @Operation(summary = "학교별 도시락 신청자 수 조회", description = "학교별 도시락 신청자 수를 조회합니다.") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "도시락 신청 수 조회 성공", + @ApiResponse(responseCode = "200", description = "도시락 신청자 수 조회 성공", content = @Content(schema = @Schema(implementation = SchoolLunchResponse.class))) }) ResponseEntity>> getLunchCounts(); - @Operation(summary = "신청 목록 조회", description = "필터 조건에 따른 신청 목록을 페이징하여 조회합니다.") + @Operation(summary = "신청 목록 및 단건 조회", description = "필터 조건에 따라 단건 데이터가 조회됩니다. 전체 목록을 조회하는 필터는 admin/applications?page=0 해당 방식으로 사용하시면 됩니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "신청 목록 조회 성공", content = @Content(schema = @Schema(implementation = ApplicationListResponse.class))) }) ResponseEntity>> getApplications( - @Parameter(description = "신청 목록 조회 필터") + @Parameter(description = "신청자 단건 조회 필터") ApplicationFilter filter, @Parameter(hidden = true) @@ -72,4 +74,11 @@ void downloadApplicationInfo( @Parameter(hidden = true) HttpServletResponse response ) throws IOException; + @Operation(summary = "환불 신청 수 조회 (구현중)", description = "환불 신청 수 조회 입니다") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "환불 신청 수 조회 성공") + }) + public ResponseEntity>> getRefundCounts( + @PageableDefault(size = 15) Pageable pageable + ); } \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/presentation/admin/dto/ApplicationExcelDto.java b/src/main/java/life/mosu/mosuserver/presentation/admin/dto/ApplicationExcelDto.java index e2315f67..4d403d8c 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/admin/dto/ApplicationExcelDto.java +++ b/src/main/java/life/mosu/mosuserver/presentation/admin/dto/ApplicationExcelDto.java @@ -6,83 +6,26 @@ import life.mosu.mosuserver.domain.payment.PaymentMethod; import life.mosu.mosuserver.domain.payment.PaymentStatus; import life.mosu.mosuserver.global.annotation.ExcelColumn; -import lombok.AllArgsConstructor; -import lombok.Getter; @Schema(description = "신청 엑셀 데이터 DTO") -@AllArgsConstructor -@Getter -public class ApplicationExcelDto { +public record ApplicationExcelDto( + @Schema(description = "결제 번호", example = "PAY-20250710-0001") @ExcelColumn(headerName = "결제 번호") String paymentNumber, + @Schema(description = "수험 번호", example = "2025-00001") @ExcelColumn(headerName = "수험 번호") String examinationNumber, + @Schema(description = "수험자 이름", example = "홍길동") @ExcelColumn(headerName = "이름") String name, + @Schema(description = "성별", example = "남자") @ExcelColumn(headerName = "성별") String gender, + @Schema(description = "생년월일", example = "2005-05-10") @ExcelColumn(headerName = "생년월일") LocalDate birth, + @Schema(description = "전화번호", example = "01012345678") @ExcelColumn(headerName = "전화번호") String phoneNumber, + @Schema(description = "보호자 전화번호", example = "01098765432") @ExcelColumn(headerName = "보호자 전화번호") String guardianPhoneNumber, + @Schema(description = "학력", example = "고등학교 재학") @ExcelColumn(headerName = "학력") String educationLevel, + @Schema(description = "학교명", example = "서울고등학교") @ExcelColumn(headerName = "학교명") String schoolName, + @Schema(description = "학년", example = "3학년") @ExcelColumn(headerName = "학년") String grade, + @Schema(description = "도시락 신청 여부", example = "신청함") @ExcelColumn(headerName = "도시락") String lunch, + @Schema(description = "응시 과목 목록", example = "[\"국어\", \"수학\"]") @ExcelColumn(headerName = "응시 과목") Set subjects, + @Schema(description = "시험 학교", example = "서울고등학교") @ExcelColumn(headerName = "시험 학교") String examSchoolName, + @Schema(description = "시험 일자", example = "2025-08-10") @ExcelColumn(headerName = "시험 일자") LocalDate examDate, + @Schema(description = "수험표 이미지 URL", example = "https://s3.amazonaws.com/bucket/admission/2025-00001.jpg") @ExcelColumn(headerName = "수험표 사진") String admissionTicketImage, + @Schema(description = "결제 상태", example = "COMPLETED") @ExcelColumn(headerName = "결제 상태") PaymentStatus paymentStatus, + @Schema(description = "결제 방법", example = "CARD") @ExcelColumn(headerName = "결제 방법") PaymentMethod paymentMethod, + @Schema(description = "신청 일시", example = "2025-07-10 15:30:00") @ExcelColumn(headerName = "신청 일시") String applicationDate) { - @Schema(description = "결제 번호", example = "PAY-20250710-0001") - @ExcelColumn(headerName = "결제 번호") - private final String paymentNumber; - - @Schema(description = "수험 번호", example = "2025-00001") - @ExcelColumn(headerName = "수험 번호") - private final String examinationNumber; - - @Schema(description = "수험자 이름", example = "홍길동") - @ExcelColumn(headerName = "이름") - private final String name; - - @Schema(description = "성별", example = "남자") - @ExcelColumn(headerName = "성별") - private final String gender; - - @Schema(description = "생년월일", example = "2005-05-10") - @ExcelColumn(headerName = "생년월일") - private final LocalDate birth; - - @Schema(description = "전화번호", example = "01012345678") - @ExcelColumn(headerName = "전화번호") - private final String phoneNumber; - - @Schema(description = "보호자 전화번호", example = "01098765432") - @ExcelColumn(headerName = "보호자 전화번호") - private final String guardianPhoneNumber; - - @Schema(description = "학력", example = "고등학교 재학") - @ExcelColumn(headerName = "학력") - private final String educationLevel; - - @Schema(description = "학교명", example = "서울고등학교") - @ExcelColumn(headerName = "학교명") - private final String schoolName; - - @Schema(description = "학년", example = "3학년") - @ExcelColumn(headerName = "학년") - private final String grade; - - @Schema(description = "도시락 신청 여부", example = "신청함") - @ExcelColumn(headerName = "도시락") - private final String lunch; - - @Schema(description = "응시 과목 목록", example = "[\"국어\", \"수학\"]") - @ExcelColumn(headerName = "응시 과목") - private final Set subjects; - - @Schema(description = "시험 학교", example = "서울고등학교") - @ExcelColumn(headerName = "시험 학교") - private final String examSchoolName; - - @Schema(description = "시험 일자", example = "2025-08-10") - @ExcelColumn(headerName = "시험 일자") - private final LocalDate examDate; - - @Schema(description = "수험표 이미지 URL", example = "https://s3.amazonaws.com/bucket/admission/2025-00001.jpg") - @ExcelColumn(headerName = "수험표 사진") - private final String admissionTicketImage; - - @Schema(description = "결제 상태", example = "COMPLETED") - @ExcelColumn(headerName = "결제 상태") - private final PaymentStatus paymentStatus; - - @Schema(description = "결제 방법", example = "CARD") - @ExcelColumn(headerName = "결제 방법") - private final PaymentMethod paymentMethod; - - @Schema(description = "신청 일시", example = "2025-07-10 15:30:00") - @ExcelColumn(headerName = "신청 일시") - private final String applicationDate; } diff --git a/src/main/java/life/mosu/mosuserver/presentation/admin/dto/ApplicationFilter.java b/src/main/java/life/mosu/mosuserver/presentation/admin/dto/ApplicationFilter.java index cd1b68d6..296829d6 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/admin/dto/ApplicationFilter.java +++ b/src/main/java/life/mosu/mosuserver/presentation/admin/dto/ApplicationFilter.java @@ -2,7 +2,6 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDate; -import life.mosu.mosuserver.global.annotation.PhoneNumberPattern; @Schema(description = "신청 목록 필터 DTO") public record ApplicationFilter( @@ -10,8 +9,7 @@ public record ApplicationFilter( @Schema(description = "이름 필터", example = "홍길동") String name, - @Schema(description = "전화번호 필터", example = "01012345678") - @PhoneNumberPattern + @Schema(description = "전화번호 필터", example = "010-1234-5678") String phone, @Schema(description = "신청 일자 필터", example = "2025-07-10") diff --git a/src/main/java/life/mosu/mosuserver/presentation/admin/dto/ApplicationListResponse.java b/src/main/java/life/mosu/mosuserver/presentation/admin/dto/ApplicationListResponse.java index 9024d445..ffb9bb0a 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/admin/dto/ApplicationListResponse.java +++ b/src/main/java/life/mosu/mosuserver/presentation/admin/dto/ApplicationListResponse.java @@ -40,8 +40,8 @@ public record ApplicationListResponse( @Schema(description = "학년", example = "고등학교 1학년") String grade, - @Schema(description = "도시락 신청 여부", example = "NONE") - String lunch, + @Schema(description = "도시락 신청 여부", example = "true, false") + Boolean lunch, @Schema(description = "응시 과목 목록", example = "[\"생활과 윤리\", \"정치와 법\"]") Set subjects, diff --git a/src/main/java/life/mosu/mosuserver/presentation/admin/dto/ExamTicketResponse.java b/src/main/java/life/mosu/mosuserver/presentation/admin/dto/ExamTicketResponse.java index bb119d9c..4c78e977 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/admin/dto/ExamTicketResponse.java +++ b/src/main/java/life/mosu/mosuserver/presentation/admin/dto/ExamTicketResponse.java @@ -2,8 +2,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDate; -import java.util.List; -import life.mosu.mosuserver.domain.application.Subject; +import java.util.Set; public record ExamTicketResponse( @Schema(description = "수험표 이미지 URL", example = "https://s3.amazonaws.com/bucket/admission/2025-00001.jpg") @@ -19,7 +18,7 @@ public record ExamTicketResponse( String examNumber, @Schema(description = "응시 과목 목록", example = "[\"생명과학\", \"지구과학\"]") - List subjects, + Set subjects, @Schema(description = "응시 학교명", example = "대치중학교") String schoolName @@ -30,7 +29,7 @@ public static ExamTicketResponse of( String userName, LocalDate birth, String examNumber, - List subjects, + Set subjects, String schoolName ) { return new ExamTicketResponse( diff --git a/src/main/java/life/mosu/mosuserver/presentation/admin/dto/SchoolLunchResponse.java b/src/main/java/life/mosu/mosuserver/presentation/admin/dto/SchoolLunchResponse.java index ffa9e854..1d9d34b2 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/admin/dto/SchoolLunchResponse.java +++ b/src/main/java/life/mosu/mosuserver/presentation/admin/dto/SchoolLunchResponse.java @@ -13,4 +13,8 @@ public record SchoolLunchResponse( ) { + public static SchoolLunchResponse of(String schoolName, + Long count) { + return new SchoolLunchResponse(schoolName, count); + } } diff --git a/src/main/java/life/mosu/mosuserver/presentation/admin/dto/StudentFilter.java b/src/main/java/life/mosu/mosuserver/presentation/admin/dto/StudentFilter.java index 9bab8d28..cc932ee4 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/admin/dto/StudentFilter.java +++ b/src/main/java/life/mosu/mosuserver/presentation/admin/dto/StudentFilter.java @@ -1,7 +1,6 @@ package life.mosu.mosuserver.presentation.admin.dto; import io.swagger.v3.oas.annotations.media.Schema; -import life.mosu.mosuserver.global.annotation.PhoneNumberPattern; @Schema(description = "학생 목록 필터 DTO") public record StudentFilter( @@ -10,10 +9,9 @@ public record StudentFilter( String name, @Schema(description = "전화번호 필터", example = "01012345678") - @PhoneNumberPattern String phone, - @Schema(description = "정렬 순서 (desc 또는 asc)", example = "desc", defaultValue = "desc") + @Schema(description = "정렬 순서 (개발중)", example = "desc", defaultValue = "desc") String order ) { diff --git a/src/main/java/life/mosu/mosuserver/presentation/application/dto/ExamApplicationRequest.java b/src/main/java/life/mosu/mosuserver/presentation/application/dto/ExamApplicationRequest.java index 826371f4..2518e23a 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/application/dto/ExamApplicationRequest.java +++ b/src/main/java/life/mosu/mosuserver/presentation/application/dto/ExamApplicationRequest.java @@ -4,10 +4,10 @@ public record ExamApplicationRequest( Long examId, - Long lunchId + boolean isLunchChecked ) { public TargetExam toTargetExam() { - return new TargetExam(examId, lunchId); + return new TargetExam(examId, isLunchChecked); } }