-
Notifications
You must be signed in to change notification settings - Fork 1
prod: 수험번호 및 수험표 #382
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
prod: 수험번호 및 수험표 #382
Changes from all commits
b12ea0d
9cf32a0
75f9183
53b3ec2
0168370
32cc885
10f9d2f
fa08f36
7b9a961
d7a85bd
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,66 @@ | ||
| package life.mosu.mosuserver.application.admin; | ||
|
|
||
| import jakarta.transaction.Transactional; | ||
| import life.mosu.mosuserver.application.admin.dto.ApplicationCsvInfo; | ||
| import life.mosu.mosuserver.application.admin.dto.ImportResultDto; | ||
| import life.mosu.mosuserver.application.admin.processor.ApplyGuestStepProcessor; | ||
| import life.mosu.mosuserver.application.admin.processor.ChangeTestPaperCheckedStepProcessor; | ||
| import life.mosu.mosuserver.application.admin.processor.GetApplicationGuestRequestStepProcessor; | ||
| import life.mosu.mosuserver.application.admin.processor.RegisterVirtualAccountStepProcessor; | ||
| import life.mosu.mosuserver.application.admin.util.CsvReader; | ||
| import life.mosu.mosuserver.presentation.application.dto.ApplicationGuestRequest; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @Slf4j | ||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class AdminApplicationImportService { | ||
|
|
||
| private final GetApplicationGuestRequestStepProcessor getApplicationGuestRequestStepProcessor; | ||
| private final ApplyGuestStepProcessor applyGuestStepProcessor; | ||
| private final ChangeTestPaperCheckedStepProcessor changeTestPaperCheckedStepProcessor; | ||
| private final RegisterVirtualAccountStepProcessor registerVirtualAccountStepProcessor; | ||
| private final CsvReader csvReader; | ||
|
|
||
| @Transactional | ||
| public ImportResultDto importGuestApplications(MultipartFile file) { | ||
| List<ApplicationCsvInfo> rows = csvReader.read(file); | ||
|
|
||
| int total = rows.size(); | ||
| int success = 0; | ||
|
|
||
| int lineNo = 1; | ||
| for (ApplicationCsvInfo row : rows) { | ||
| lineNo++; | ||
| try { | ||
| processGuestRow(row); | ||
| success++; | ||
| } catch (Exception e) { | ||
| log.error("게스트 신청 CSV 행 {} 처리 실패: {}", lineNo, e.getMessage(), e); | ||
| throw new RuntimeException("CSV 일괄 처리 중 오류 발생"); | ||
|
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. |
||
| } | ||
| } | ||
|
|
||
| int fail = total - success; | ||
| log.info("CSV Import 완료 - 총 {}건, 성공 {}, 실패 {}", total, success, fail); | ||
| return new ImportResultDto(total, success, fail); | ||
| } | ||
|
|
||
|
|
||
| private void processGuestRow(ApplicationCsvInfo csvInfo) { | ||
| ApplicationGuestRequest applicationGuestRequest = getApplicationGuestRequestStepProcessor.process(csvInfo); | ||
|
|
||
| Long applicationId = applyGuestStepProcessor.process(applicationGuestRequest); | ||
|
|
||
| if (Boolean.TRUE.equals(csvInfo.isTestPaperChecked())) { | ||
| changeTestPaperCheckedStepProcessor.process(applicationId); | ||
| } | ||
|
|
||
| registerVirtualAccountStepProcessor.process(applicationId); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package life.mosu.mosuserver.application.admin.dto; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.format.DateTimeParseException; | ||
|
|
||
| public record ApplicationCsvInfo( | ||
| String createdAt, | ||
| String name, | ||
| String gender, | ||
| LocalDate birth, | ||
| String phoneNumber, | ||
| LocalDate examDate, | ||
| String examSchool, | ||
| Boolean isLunchChecked, | ||
| Boolean isTestPaperChecked, | ||
| String subject1, | ||
| String subject2 | ||
| ) { | ||
| public static ApplicationCsvInfo of( | ||
| String createdAt, | ||
| String name, | ||
| String gender, | ||
| LocalDate birth, | ||
| String phoneNumber, | ||
| LocalDate examDate, | ||
| String examSchool, | ||
| Boolean isLunchChecked, | ||
| Boolean isTestPaperChecked, | ||
| String subject1, | ||
| String subject2 | ||
| ) { | ||
| return new ApplicationCsvInfo(createdAt, name, gender, birth, phoneNumber, examDate, examSchool, isLunchChecked, isTestPaperChecked, subject1, subject2); | ||
| } | ||
|
|
||
| public static ApplicationCsvInfo of(String[] values) { | ||
| if (values.length < 11) { | ||
|
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. |
||
| throw new IllegalArgumentException("CSV 행이 필수 컬럼 수(11개)보다 부족합니다. 실제 컬럼 수: " + values.length); | ||
| } | ||
| return new ApplicationCsvInfo( | ||
| values[0], // createdAt | ||
| values[1], // name | ||
| values[2], // gender | ||
| parseDate(values[3]), // birth | ||
| values[4], // phoneNumber | ||
| parseDate(values[5]), // examDate | ||
| values[6], // examSchool | ||
| parseBoolean(values[7]), // isLunchChecked | ||
| parseBoolean(values[8]), // isTestPaperChecked | ||
| values[9], // subject1 | ||
| values[10] // subject2 | ||
| ); | ||
| } | ||
|
|
||
| private static LocalDate parseDate(String dateStr) { | ||
| try { | ||
| return LocalDate.parse(dateStr.trim()); | ||
| } catch (DateTimeParseException e) { | ||
| throw new IllegalArgumentException("날짜 형식이 올바르지 않습니다"); | ||
|
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 static Boolean parseBoolean(String bool) { | ||
| if (bool == null || bool.isBlank()) { | ||
| return false; | ||
| } | ||
| String trimmedValue = bool.trim(); | ||
| return Boolean.parseBoolean(trimmedValue); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package life.mosu.mosuserver.application.admin.dto; | ||
|
|
||
| public record ImportResultDto( | ||
| int totalProcessed, | ||
| int totalSuccess, | ||
| int totalFailure | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package life.mosu.mosuserver.application.admin.processor; | ||
|
|
||
| import life.mosu.mosuserver.application.application.ApplicationService; | ||
| import life.mosu.mosuserver.domain.application.entity.ApplicationJpaEntity; | ||
| import life.mosu.mosuserver.domain.application.entity.ApplicationStatus; | ||
| import life.mosu.mosuserver.domain.application.repository.ApplicationJpaRepository; | ||
| import life.mosu.mosuserver.global.processor.StepProcessor; | ||
| import life.mosu.mosuserver.presentation.application.dto.ApplicationGuestRequest; | ||
| import life.mosu.mosuserver.presentation.application.dto.CreateApplicationResponse; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class ApplyGuestStepProcessor implements StepProcessor<ApplicationGuestRequest, Long> { | ||
|
|
||
| private final ApplicationService applicationService; | ||
| private final ApplicationJpaRepository applicationJpaRepository; | ||
|
|
||
| @Override | ||
| public Long process(ApplicationGuestRequest request) { | ||
| CreateApplicationResponse response = applicationService.applyByGuest(request); | ||
|
|
||
| Long applicationId = response.applicationId(); | ||
| ApplicationJpaEntity application = applicationJpaRepository.findById(applicationId) | ||
| .orElseThrow(() -> new IllegalArgumentException("신청을 찾을 수 없습니다. id=" + applicationId)); | ||
|
|
||
| application.changeStatus(ApplicationStatus.APPROVED); | ||
| return applicationId; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| package life.mosu.mosuserver.application.admin.processor; | ||
|
|
||
| import life.mosu.mosuserver.domain.examapplication.entity.ExamApplicationJpaEntity; | ||
| import life.mosu.mosuserver.domain.examapplication.repository.ExamApplicationJpaRepository; | ||
| import life.mosu.mosuserver.global.exception.CustomRuntimeException; | ||
| import life.mosu.mosuserver.global.exception.ErrorCode; | ||
| import life.mosu.mosuserver.global.processor.StepProcessor; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.util.List; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class ChangeTestPaperCheckedStepProcessor implements StepProcessor<Long, List<Long>> { | ||
|
|
||
| private final ExamApplicationJpaRepository examApplicationJpaRepository; | ||
|
|
||
| @Override | ||
| public List<Long> process(Long applicationId) { | ||
|
|
||
| List<ExamApplicationJpaEntity> examApplications = examApplicationJpaRepository.findByApplicationId(applicationId); | ||
| if (examApplications.isEmpty()) { | ||
| throw new CustomRuntimeException(ErrorCode.EXAM_APPLICATION_NOT_FOUND); | ||
| } | ||
|
|
||
| examApplications.forEach(ExamApplicationJpaEntity::setTestPaperChecked); | ||
|
|
||
| return examApplications.stream() | ||
| .map(ExamApplicationJpaEntity::getId) | ||
| .collect(Collectors.toList()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package life.mosu.mosuserver.application.admin.processor; | ||
|
|
||
| import life.mosu.mosuserver.application.admin.dto.ApplicationCsvInfo; | ||
| import life.mosu.mosuserver.domain.exam.entity.ExamJpaEntity; | ||
| import life.mosu.mosuserver.domain.exam.entity.ExamJpaRepository; | ||
| import life.mosu.mosuserver.global.processor.StepProcessor; | ||
| import life.mosu.mosuserver.presentation.application.dto.ApplicationGuestRequest; | ||
| import life.mosu.mosuserver.presentation.application.dto.ExamApplicationRequest; | ||
| import life.mosu.mosuserver.presentation.common.FileRequest; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.Set; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class GetApplicationGuestRequestStepProcessor implements StepProcessor<ApplicationCsvInfo, ApplicationGuestRequest> { | ||
|
|
||
| private final ExamJpaRepository examJpaRepository; | ||
| private static final String ORG_NAME = "모수사전예약"; | ||
|
|
||
| @Override | ||
| public ApplicationGuestRequest process(ApplicationCsvInfo csvInfo) { | ||
|
|
||
| LocalDate examDate = csvInfo.examDate(); | ||
| String schoolName = csvInfo.examSchool(); | ||
| ExamJpaEntity exam = examJpaRepository.findByExamDateAndSchoolName(examDate, schoolName); | ||
|
|
||
| ExamApplicationRequest examApplicationRequest = new ExamApplicationRequest( | ||
| exam.getId(), | ||
| csvInfo.isLunchChecked() | ||
| ); | ||
| FileRequest admissionTicket = new FileRequest("", ""); | ||
| Set<String> subjects = Set.of(csvInfo.subject1(), csvInfo.subject2()); | ||
|
|
||
| return new ApplicationGuestRequest( | ||
| ORG_NAME, | ||
| csvInfo.gender(), | ||
| csvInfo.name(), | ||
| csvInfo.birth(), | ||
| csvInfo.phoneNumber(), | ||
| examApplicationRequest, | ||
| subjects, | ||
| admissionTicket | ||
| ); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package life.mosu.mosuserver.application.admin.processor; | ||
|
|
||
| import life.mosu.mosuserver.application.virtualaccount.VirtualAccountOrderIdGenerator; | ||
| import life.mosu.mosuserver.domain.virtualaccount.VirtualAccountLogJpaEntity; | ||
| import life.mosu.mosuserver.domain.virtualaccount.VirtualAccountLogJpaRepository; | ||
| import life.mosu.mosuserver.global.processor.StepProcessor; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class RegisterVirtualAccountStepProcessor implements StepProcessor<Long, Long> { | ||
|
|
||
| private final VirtualAccountLogJpaRepository virtualAccountLogJpaRepository; | ||
| private final VirtualAccountOrderIdGenerator orderIdGenerator; | ||
|
|
||
| @Override | ||
| public Long process(Long applicationId) { | ||
| String orderId = orderIdGenerator.generate(); | ||
| VirtualAccountLogJpaEntity virtualAccountLog = VirtualAccountLogJpaEntity.create(applicationId, orderId, null, null, null, null); | ||
| virtualAccountLog.setDepositSuccess(); | ||
| VirtualAccountLogJpaEntity saved = virtualAccountLogJpaRepository.save(virtualAccountLog); | ||
|
|
||
| return saved.getId(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package life.mosu.mosuserver.application.admin.util; | ||
|
|
||
| import life.mosu.mosuserver.application.admin.dto.ApplicationCsvInfo; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
|
|
||
| import java.io.BufferedReader; | ||
| import java.io.InputStreamReader; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| @Slf4j | ||
| @Component | ||
| public class CsvReader { | ||
|
|
||
| private static final String CSV_DELIMITER = ","; | ||
|
|
||
| public List<ApplicationCsvInfo> read(MultipartFile file) { | ||
| List<ApplicationCsvInfo> results = new ArrayList<>(); | ||
|
|
||
| if (file == null || file.isEmpty()) { | ||
| log.warn("업로드된 파일이 비어있습니다."); | ||
| return results; | ||
| } | ||
|
|
||
| try (BufferedReader reader = | ||
| new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) { | ||
|
|
||
| String line; | ||
| boolean isHeader = true; | ||
| int lineNumber = 0; | ||
|
|
||
| while ((line = reader.readLine()) != null) { | ||
| lineNumber++; | ||
| if (line.isBlank()) continue; | ||
| if (isHeader) { | ||
| isHeader = false; | ||
| continue; | ||
| } | ||
|
|
||
| try { | ||
| String[] values = line.split(CSV_DELIMITER, -1); | ||
|
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. |
||
| results.add(ApplicationCsvInfo.of(values)); | ||
| } catch (Exception e) { | ||
| log.error("CSV 파싱 실패 (Line {}): {}", lineNumber, e.getMessage()); | ||
| throw new RuntimeException("CSV 파싱 오류"); | ||
|
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. |
||
|
|
||
| } | ||
| } | ||
| } catch (Exception e) { | ||
| log.error("파일 읽기 실패: {}", e.getMessage(), e); | ||
| throw new RuntimeException("CSV 읽기 오류"); | ||
| } | ||
|
|
||
| return results; | ||
| } | ||
| } | ||
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.
현재 구현은 CSV 처리 중 첫 번째 오류가 발생하면 전체 작업을 중단하고 롤백합니다. 이는 데이터 일관성을 보장하는 안전한 방법이지만, CSV 파일에 여러 오류가 있을 경우 사용자가 한 번에 하나의 오류만 보고받게 되어 불편할 수 있습니다. 모든 행을 검사하고 모든 오류를 한 번에 보고하는 방식을 고려해볼 수 있습니다. 예를 들어, 1) 모든 행에 대한 유효성 검사를 먼저 수행하고, 2) 유효성 검사를 통과하면 모든 행을 데이터베이스에 저장하는 2단계 처리 방식을 사용할 수 있습니다. 이렇게 하면 트랜잭션의 원자성을 유지하면서 사용자 경험을 개선할 수 있습니다.