From 64451478c7334417ea3d70a65523e9292f38a192 Mon Sep 17 00:00:00 2001 From: chominju02 Date: Mon, 13 Oct 2025 19:04:18 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20timeTable=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetMemberExamTicketInfoProcessor.java | 12 +- .../GetPartnerExamTicketInfoProcessor.java | 13 +- .../timetable/TimeTableService.java | 80 +++++++++ .../processor/GenerateTimeTableProcessor.java | 157 ++++++++++++++++++ .../projection/TimeTableInfoProjection.java | 9 + .../ExamApplicationJpaRepository.java | 37 +++++ .../mosuserver/global/filter/Whitelist.java | 3 +- .../timetable/TimeTableController.java | 60 +++++++ .../timetable/dto/TimeTableFileResponse.java | 8 + .../timetable/dto/TimeTableInfoResponse.java | 26 +++ src/main/resources/static/time-table.pdf | Bin 0 -> 17776 bytes 11 files changed, 395 insertions(+), 10 deletions(-) create mode 100644 src/main/java/life/mosu/mosuserver/application/timetable/TimeTableService.java create mode 100644 src/main/java/life/mosu/mosuserver/application/timetable/processor/GenerateTimeTableProcessor.java create mode 100644 src/main/java/life/mosu/mosuserver/domain/examapplication/projection/TimeTableInfoProjection.java create mode 100644 src/main/java/life/mosu/mosuserver/presentation/timetable/TimeTableController.java create mode 100644 src/main/java/life/mosu/mosuserver/presentation/timetable/dto/TimeTableFileResponse.java create mode 100644 src/main/java/life/mosu/mosuserver/presentation/timetable/dto/TimeTableInfoResponse.java create mode 100644 src/main/resources/static/time-table.pdf diff --git a/src/main/java/life/mosu/mosuserver/application/examticket/processor/GetMemberExamTicketInfoProcessor.java b/src/main/java/life/mosu/mosuserver/application/examticket/processor/GetMemberExamTicketInfoProcessor.java index b5c0dbd1..98b5690f 100644 --- a/src/main/java/life/mosu/mosuserver/application/examticket/processor/GetMemberExamTicketInfoProcessor.java +++ b/src/main/java/life/mosu/mosuserver/application/examticket/processor/GetMemberExamTicketInfoProcessor.java @@ -27,13 +27,17 @@ public class GetMemberExamTicketInfoProcessor implements StepProcessor new CustomRuntimeException(ErrorCode.EXAM_TICKET_INFO_NOT_FOUND)); - List examSubjects = examSubjectJpaRepository.findByExamApplicationId( - examApplicationId); - List subjects = examSubjects.stream() + List subjects = examSubjectJpaRepository.findByExamApplicationId(examApplicationId) + .stream() .map(ExamSubjectJpaEntity::getSubject) .sorted(Comparator.comparingInt(Subject::ordinal)) - .map(Subject::getSubjectName) + .map(subject -> { + if (subject == Subject.SOCIETY_AND_CULTURE) { + return "사회·문화"; + } + return subject.getSubjectName(); + }) .toList(); String examTicketImgUrl = getExamTicketImgUrl(examTicketInfo); diff --git a/src/main/java/life/mosu/mosuserver/application/examticket/processor/GetPartnerExamTicketInfoProcessor.java b/src/main/java/life/mosu/mosuserver/application/examticket/processor/GetPartnerExamTicketInfoProcessor.java index 5c301df4..e6797b1a 100644 --- a/src/main/java/life/mosu/mosuserver/application/examticket/processor/GetPartnerExamTicketInfoProcessor.java +++ b/src/main/java/life/mosu/mosuserver/application/examticket/processor/GetPartnerExamTicketInfoProcessor.java @@ -44,13 +44,16 @@ public ExamTicketIssueResponse process(String orderId) { Long examApplicationId = examTicketInfo.examApplicationId(); - List examSubjects = examSubjectJpaRepository.findByExamApplicationId( - examApplicationId); - - List subjects = examSubjects.stream() + List subjects = examSubjectJpaRepository.findByExamApplicationId(examApplicationId) + .stream() .map(ExamSubjectJpaEntity::getSubject) .sorted(Comparator.comparingInt(Subject::ordinal)) - .map(Subject::getSubjectName) + .map(subject -> { + if (subject == Subject.SOCIETY_AND_CULTURE) { + return "사회·문화"; + } + return subject.getSubjectName(); + }) .toList(); String examTicketImgUrl = getExamTicketImgUrl(examTicketInfo); diff --git a/src/main/java/life/mosu/mosuserver/application/timetable/TimeTableService.java b/src/main/java/life/mosu/mosuserver/application/timetable/TimeTableService.java new file mode 100644 index 00000000..9cac8359 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/timetable/TimeTableService.java @@ -0,0 +1,80 @@ +package life.mosu.mosuserver.application.timetable; + +import life.mosu.mosuserver.application.timetable.processor.GenerateTimeTableProcessor; +import life.mosu.mosuserver.domain.application.entity.Subject; +import life.mosu.mosuserver.domain.examapplication.entity.ExamSubjectJpaEntity; +import life.mosu.mosuserver.domain.examapplication.repository.ExamApplicationJpaRepository; +import life.mosu.mosuserver.domain.examapplication.repository.ExamSubjectJpaRepository; +import life.mosu.mosuserver.presentation.timetable.dto.TimeTableFileResponse; +import life.mosu.mosuserver.presentation.timetable.dto.TimeTableInfoResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.Comparator; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class TimeTableService { + + private final ExamApplicationJpaRepository examApplicationJpaRepository; + private final ExamSubjectJpaRepository examSubjectJpaRepository; + private final GenerateTimeTableProcessor generateTimeTableProcessor; + + @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) + public TimeTableFileResponse getMemberTimeTables(LocalDate examDate) { + List entries = examApplicationJpaRepository.findMemberTimeTable(examDate) + .stream() + .map(info -> { + Long examApplicationId = info.examApplicationId(); + List subjects = getSubjects(examApplicationId); + + return TimeTableInfoResponse.of( + info.examNumber(), + info.userName(), + subjects, + info.schoolName() + ); + }) + .toList(); + + return generateTimeTableProcessor.process(entries); + } + + @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) + public TimeTableFileResponse getPartnerTimeTables(LocalDate examDate) { + List entries = examApplicationJpaRepository.findPartnerTimeTable(examDate) + .stream() + .map(info -> { + Long examApplicationId = info.examApplicationId(); + List subjects = getSubjects(examApplicationId); + + return TimeTableInfoResponse.of( + info.examNumber(), + info.userName(), + subjects, + info.schoolName() + ); + }) + .toList(); + + return generateTimeTableProcessor.process(entries); + } + + private List getSubjects(Long examApplicationId) { + return examSubjectJpaRepository.findByExamApplicationId(examApplicationId) + .stream() + .map(ExamSubjectJpaEntity::getSubject) + .sorted(Comparator.comparingInt(Subject::ordinal)) + .map(subject -> { + if (subject == Subject.SOCIETY_AND_CULTURE) { + return "사회·문화"; + } + return subject.getSubjectName(); + }) + .toList(); + } +} diff --git a/src/main/java/life/mosu/mosuserver/application/timetable/processor/GenerateTimeTableProcessor.java b/src/main/java/life/mosu/mosuserver/application/timetable/processor/GenerateTimeTableProcessor.java new file mode 100644 index 00000000..eea851ae --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/timetable/processor/GenerateTimeTableProcessor.java @@ -0,0 +1,157 @@ +package life.mosu.mosuserver.application.timetable.processor; + +import jakarta.annotation.PostConstruct; +import life.mosu.mosuserver.global.processor.StepProcessor; +import life.mosu.mosuserver.presentation.timetable.dto.TimeTableFileResponse; +import life.mosu.mosuserver.presentation.timetable.dto.TimeTableInfoResponse; +import lombok.RequiredArgsConstructor; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.font.PDType0Font; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Component; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class GenerateTimeTableProcessor implements StepProcessor, TimeTableFileResponse> { + + private static final String TEMPLATE_CLASSPATH = "static/time-table.pdf"; + private static final String FONT_CLASSPATH = "fonts/NotoSansKR-Regular.ttf"; + + private byte[] templatePdf; + private byte[] fontBytes; + + private static final int FONT_SIZE = 14; + + private static final int ROWS = 3; + private static final int COLS = 2; + private static final int PER_PAGE = ROWS * COLS; + + // 칸 간격 + private static final float CELL_DX = 288f; + private static final float CELL_DY = 264f; + + // 첫 번째 칸 기준 좌표 + private static final float FIRST_EXAM_NUMBER_X = 116f; + private static final float FIRST_EXAM_NUMBER_Y = 771f; + + private static final float FIRST_NAME_X = 116f; + private static final float FIRST_NAME_Y = 752f; + + private static final float FIRST_SUBJECT1_X = 137f; + private static final float FIRST_SUBJECT1_Y = 653f; + + private static final float FIRST_SUBJECT2_X = 137f; + private static final float FIRST_SUBJECT2_Y = 633f; + + private static final float FIRST_SCHOOL_X = 116f; + private static final float FIRST_SCHOOL_Y = 595f; + + @PostConstruct + void init() { + this.templatePdf = readAll(TEMPLATE_CLASSPATH); + this.fontBytes = readAll(FONT_CLASSPATH); + } + + @Override + public TimeTableFileResponse process(List list) { + try (PDDocument templateDoc = Loader.loadPDF(templatePdf); + PDDocument outDoc = new PDDocument(); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + + PDType0Font font = PDType0Font.load(outDoc, new ByteArrayInputStream(fontBytes)); + + int total = list.size(); + int pageCount = (total + PER_PAGE - 1) / PER_PAGE; + + for (int p = 0; p < pageCount; p++) { + PDPage page = outDoc.importPage(templateDoc.getPage(0)); + + try (PDPageContentStream cs = new PDPageContentStream( + outDoc, page, PDPageContentStream.AppendMode.APPEND, true)) { + + for (int slot = 0; slot < PER_PAGE; slot++) { + int idx = p * PER_PAGE + slot; + if (idx >= total) break; + + TimeTableInfoResponse e = list.get(idx); + + int row = slot / COLS; // 0,1,2 + int col = slot % COLS; // 0,1 + + float dx = col * CELL_DX; + float dy = row * CELL_DY; + + // 수험번호 + drawText(cs, font, FONT_SIZE, + FIRST_EXAM_NUMBER_X + dx, + FIRST_EXAM_NUMBER_Y - dy, + e.examNumber()); + + // 성명 + drawText(cs, font, FONT_SIZE, + FIRST_NAME_X + dx, + FIRST_NAME_Y - dy, + e.userName()); + + // 탐구 과목 + String sub1 = (e.subjects() != null && e.subjects().size() > 0) ? nz(e.subjects().get(0)) : ""; + String sub2 = (e.subjects() != null && e.subjects().size() > 1) ? nz(e.subjects().get(1)) : ""; + + drawText(cs, font, FONT_SIZE, + FIRST_SUBJECT1_X + dx, + FIRST_SUBJECT1_Y - dy, + sub1); + + drawText(cs, font, FONT_SIZE, + FIRST_SUBJECT2_X + dx, + FIRST_SUBJECT2_Y - dy, + sub2); + + // 학교명 + drawText(cs, font, FONT_SIZE, + FIRST_SCHOOL_X + dx, + FIRST_SCHOOL_Y - dy, + e.schoolName()); + } + } + } + + outDoc.save(out); + return new TimeTableFileResponse(out.toByteArray(), "time-tables.pdf", "application/pdf"); + + } catch (Exception e) { + throw new RuntimeException("Generate time-table PDF failed", e); + } + } + + private byte[] readAll(String classpath) { + try (InputStream in = new ClassPathResource(classpath).getInputStream()) { + return in.readAllBytes(); + } catch (Exception e) { + throw new RuntimeException("Resource not found: " + classpath, e); + } + } + + private void drawText(PDPageContentStream cs, PDType0Font font, int size, + float x, float y, String text) { + try { + cs.beginText(); + cs.setFont(font, size); + cs.newLineAtOffset(x, y); + cs.showText(text == null ? "" : text); + cs.endText(); + } catch (Exception e) { + throw new RuntimeException("Failed to draw text", e); + } + } + + private static String nz(String s) { return s == null ? "" : s; } +} diff --git a/src/main/java/life/mosu/mosuserver/domain/examapplication/projection/TimeTableInfoProjection.java b/src/main/java/life/mosu/mosuserver/domain/examapplication/projection/TimeTableInfoProjection.java new file mode 100644 index 00000000..f1873df7 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/domain/examapplication/projection/TimeTableInfoProjection.java @@ -0,0 +1,9 @@ +package life.mosu.mosuserver.domain.examapplication.projection; + +public record TimeTableInfoProjection ( + Long examApplicationId, + String examNumber, + String userName, + String schoolName +) { +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/domain/examapplication/repository/ExamApplicationJpaRepository.java b/src/main/java/life/mosu/mosuserver/domain/examapplication/repository/ExamApplicationJpaRepository.java index 06a4982f..dff61bf7 100644 --- a/src/main/java/life/mosu/mosuserver/domain/examapplication/repository/ExamApplicationJpaRepository.java +++ b/src/main/java/life/mosu/mosuserver/domain/examapplication/repository/ExamApplicationJpaRepository.java @@ -9,6 +9,7 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import java.time.LocalDate; import java.util.List; import java.util.Optional; @@ -300,4 +301,40 @@ List findDoneAndSortByTestPaperGroup( """) Optional findMemberExamTicketIssueProjectionByExamApplicationId(@Param("examApplicationId") Long examApplicationId); + @Query(""" + SELECT new life.mosu.mosuserver.domain.examapplication.projection.TimeTableInfoProjection( + ea.id, + ea.examNumber, + pr.userName, + e.schoolName + ) + FROM ExamApplicationJpaEntity ea + LEFT JOIN PaymentJpaEntity p on p.examApplicationId = ea.id + LEFT JOIN ExamJpaEntity e on ea.examId = e.id + LEFT JOIN UserJpaEntity u on ea.userId = u.id + LEFT JOIN ProfileJpaEntity pr on pr.userId = u.id + WHERE p.paymentStatus = 'DONE' + AND e.examDate = :examDate + ORDER BY ea.examNumber + """) + List findMemberTimeTable(@Param("examDate")LocalDate examDate); + + + @Query(""" + SELECT new life.mosu.mosuserver.domain.examapplication.projection.TimeTableInfoProjection( + ea.id, + ea.examNumber, + u.name, + e.schoolName + ) + FROM ExamApplicationJpaEntity ea + JOIN ApplicationJpaEntity a on a.id = ea.applicationId + JOIN UserJpaEntity u on a.userId = u.id + JOIN VirtualAccountLogJpaEntity v on v.applicationId = a.id + JOIN ExamJpaEntity e on ea.examId = e.id + WHERE v.depositStatus = 'DONE' + AND e.examDate = :examDate + ORDER BY ea.examNumber + """) + List findPartnerTimeTable(@Param("examDate")LocalDate examDate); } \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/global/filter/Whitelist.java b/src/main/java/life/mosu/mosuserver/global/filter/Whitelist.java index 0979442e..e74234a0 100644 --- a/src/main/java/life/mosu/mosuserver/global/filter/Whitelist.java +++ b/src/main/java/life/mosu/mosuserver/global/filter/Whitelist.java @@ -54,7 +54,8 @@ public enum Whitelist { APPLICATION_GUEST("/api/v1/applications/guest", WhitelistMethod.ALL), APPLICATION_PAID("/api/v1/applications/schools/paid-count",WhitelistMethod.ALL), EXAM_TICKET_GUEST("/api/v1/exam-ticket", WhitelistMethod.GET), - ADMIN_IMPORT_CSV("/api/v1/admin/applications/import", WhitelistMethod.ALL); + ADMIN_IMPORT_CSV("/api/v1/admin/applications/import", WhitelistMethod.ALL), + TIME_TABLE("/api/v1/time-table", WhitelistMethod.GET); private static final List AUTH_REQUIRED_EXCEPTIONS = List.of( new ExceptionRule("/api/v1/exam-application", WhitelistMethod.GET) diff --git a/src/main/java/life/mosu/mosuserver/presentation/timetable/TimeTableController.java b/src/main/java/life/mosu/mosuserver/presentation/timetable/TimeTableController.java new file mode 100644 index 00000000..33050cc8 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/presentation/timetable/TimeTableController.java @@ -0,0 +1,60 @@ +package life.mosu.mosuserver.presentation.timetable; + +import life.mosu.mosuserver.application.timetable.TimeTableService; +import life.mosu.mosuserver.presentation.timetable.dto.TimeTableFileResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.Normalizer; +import java.time.LocalDate; + +@RestController +@RequestMapping("/time-table") +@RequiredArgsConstructor +public class TimeTableController { + + private final TimeTableService timeTableService; + + @GetMapping(value = "/member", produces = MediaType.APPLICATION_PDF_VALUE) + public ResponseEntity getMemberTimeTables( + @RequestParam(name = "examDate") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate examDate + ) { + TimeTableFileResponse file = timeTableService.getMemberTimeTables(examDate); + return buildPdfResponse(file); + } + + @GetMapping(value = "/partner", produces = MediaType.APPLICATION_PDF_VALUE) + public ResponseEntity getPartnerTimeTables( + @RequestParam(name = "examDate") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate examDate + ) { + TimeTableFileResponse file = timeTableService.getPartnerTimeTables(examDate); + return buildPdfResponse(file); + } + + private ResponseEntity buildPdfResponse(TimeTableFileResponse file) { + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_TYPE, file.contentType()) + .header(HttpHeaders.CONTENT_DISPOSITION, contentDispositionInlineUtf8(file.filename())) + .body(file.bytes()); + } + + private String contentDispositionInlineUtf8(String filename) { + String asciiFallback = Normalizer.normalize(filename, Normalizer.Form.NFKD) + .replaceAll("\\p{M}+", "") + .replaceAll("[^A-Za-z0-9._-]", "_"); + String encoded = URLEncoder.encode(filename, StandardCharsets.UTF_8).replace("+", "%20"); + return "inline; filename=\"" + asciiFallback + "\"; filename*=UTF-8''" + encoded; + } + +} diff --git a/src/main/java/life/mosu/mosuserver/presentation/timetable/dto/TimeTableFileResponse.java b/src/main/java/life/mosu/mosuserver/presentation/timetable/dto/TimeTableFileResponse.java new file mode 100644 index 00000000..ddb6a929 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/presentation/timetable/dto/TimeTableFileResponse.java @@ -0,0 +1,8 @@ +package life.mosu.mosuserver.presentation.timetable.dto; + +public record TimeTableFileResponse ( + byte[] bytes, + String filename, + String contentType +) { +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/presentation/timetable/dto/TimeTableInfoResponse.java b/src/main/java/life/mosu/mosuserver/presentation/timetable/dto/TimeTableInfoResponse.java new file mode 100644 index 00000000..2c545743 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/presentation/timetable/dto/TimeTableInfoResponse.java @@ -0,0 +1,26 @@ +package life.mosu.mosuserver.presentation.timetable.dto; + +import java.util.List; + +public record TimeTableInfoResponse ( + String examNumber, + String userName, + List subjects, + String schoolName +) { + + public static TimeTableInfoResponse of( + String examNumber, + String userName, + List subjects, + String schoolName + ) { + return new TimeTableInfoResponse( + examNumber, + userName, + subjects, + schoolName + ); + } + +} \ No newline at end of file diff --git a/src/main/resources/static/time-table.pdf b/src/main/resources/static/time-table.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0305fdc8cc72c13a0b40c489afacc331d2fea8dc GIT binary patch literal 17776 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 qo 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 Date: Mon, 13 Oct 2025 19:04:43 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=201019=20=EC=88=98=ED=97=98?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=9E=AC=EC=83=9D=EC=84=B1=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infra/cron/job/ExamNumberGenerationJobRound1.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/life/mosu/mosuserver/infra/cron/job/ExamNumberGenerationJobRound1.java b/src/main/java/life/mosu/mosuserver/infra/cron/job/ExamNumberGenerationJobRound1.java index 63f45f98..ef1a3945 100644 --- a/src/main/java/life/mosu/mosuserver/infra/cron/job/ExamNumberGenerationJobRound1.java +++ b/src/main/java/life/mosu/mosuserver/infra/cron/job/ExamNumberGenerationJobRound1.java @@ -11,7 +11,7 @@ import java.time.LocalDate; @Slf4j -@CronJob(cron = "0 0 10 13 10 ?", name = "examNumberGeneratorJob_20251019") +@CronJob(cron = "0 30 19 13 10 ?", name = "examNumberGeneratorJob_20251019") @DisallowConcurrentExecution @RequiredArgsConstructor public class ExamNumberGenerationJobRound1 implements Job {