-
Notifications
You must be signed in to change notification settings - Fork 1
prod #386
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 #386
Changes from all commits
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 |
|---|---|---|
|
|
@@ -44,13 +44,16 @@ public ExamTicketIssueResponse process(String orderId) { | |
|
|
||
| Long examApplicationId = examTicketInfo.examApplicationId(); | ||
|
|
||
| List<ExamSubjectJpaEntity> examSubjects = examSubjectJpaRepository.findByExamApplicationId( | ||
| examApplicationId); | ||
|
|
||
| List<String> subjects = examSubjects.stream() | ||
| List<String> 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(); | ||
|
Comment on lines
+47
to
57
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. |
||
|
|
||
| String examTicketImgUrl = getExamTicketImgUrl(examTicketInfo); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<TimeTableInfoResponse> entries = examApplicationJpaRepository.findMemberTimeTable(examDate) | ||
| .stream() | ||
| .map(info -> { | ||
| Long examApplicationId = info.examApplicationId(); | ||
| List<String> 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<TimeTableInfoResponse> entries = examApplicationJpaRepository.findPartnerTimeTable(examDate) | ||
| .stream() | ||
| .map(info -> { | ||
| Long examApplicationId = info.examApplicationId(); | ||
| List<String> subjects = getSubjects(examApplicationId); | ||
|
|
||
| return TimeTableInfoResponse.of( | ||
| info.examNumber(), | ||
| info.userName(), | ||
| subjects, | ||
| info.schoolName() | ||
| ); | ||
| }) | ||
| .toList(); | ||
|
|
||
| return generateTimeTableProcessor.process(entries); | ||
| } | ||
|
|
||
| private List<String> 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(); | ||
| } | ||
|
Comment on lines
+67
to
+79
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. |
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<List<TimeTableInfoResponse>, 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<TimeTableInfoResponse> 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); | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+130
to
+132
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.
Suggested change
|
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| 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); | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+138
to
+140
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.
Suggested change
|
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| 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); | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+151
to
+153
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.
Suggested change
|
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| private static String nz(String s) { return s == null ? "" : s; } | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package life.mosu.mosuserver.domain.examapplication.projection; | ||
|
|
||
| public record TimeTableInfoProjection ( | ||
| Long examApplicationId, | ||
| String examNumber, | ||
| String userName, | ||
| String schoolName | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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<ExamApplicationJpaEntity> findDoneAndSortByTestPaperGroup( | |||||||||||||||||
| """) | ||||||||||||||||||
| Optional<ExamTicketIssueProjection> 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 | ||||||||||||||||||
|
Comment on lines
+312
to
+315
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.
Suggested change
|
||||||||||||||||||
| WHERE p.paymentStatus = 'DONE' | ||||||||||||||||||
| AND e.examDate = :examDate | ||||||||||||||||||
| ORDER BY ea.examNumber | ||||||||||||||||||
| """) | ||||||||||||||||||
| List<TimeTableInfoProjection> 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<TimeTableInfoProjection> findPartnerTimeTable(@Param("examDate")LocalDate examDate); | ||||||||||||||||||
| } | ||||||||||||||||||
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.
과목 목록을 가져와 포맷팅하는 이 로직은
GetPartnerExamTicketInfoProcessor와 새로 추가된TimeTableService에도 중복되어 있습니다. 코드 중복을 피하고 유지보수성을 높이기 위해 이 로직을 중앙에서 관리하는 유틸리티 메서드나 서비스로 분리하는 것이 좋습니다.