-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/time table #385
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
Feat/time table #385
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(); | ||
| }) | ||
|
Comment on lines
+51
to
+56
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. |
||
| .toList(); | ||
|
|
||
| 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); | ||
|
Comment on lines
+29
to
+44
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. N+1 query problem: consider batch-fetching subjects. For each Recommended solution: Fetch all subjects for the exam applications in a single query using an IN clause: @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public TimeTableFileResponse getMemberTimeTables(LocalDate examDate) {
List<TimeTableInfoProjection> projections = examApplicationJpaRepository.findMemberTimeTable(examDate);
// Batch fetch all subjects
List<Long> examApplicationIds = projections.stream()
.map(TimeTableInfoProjection::examApplicationId)
.toList();
Map<Long, List<String>> subjectsByApplicationId = examSubjectJpaRepository
.findByExamApplicationIdIn(examApplicationIds)
.stream()
.collect(Collectors.groupingBy(
ExamSubjectJpaEntity::getExamApplicationId,
Collectors.mapping(
entity -> {
Subject subject = entity.getSubject();
if (subject == Subject.SOCIETY_AND_CULTURE) {
return "사회·문화";
}
return subject.getSubjectName();
},
Collectors.toList()
)
));
// Sort subjects for each application
subjectsByApplicationId.replaceAll((id, subjects) ->
subjects.stream()
.sorted(/* by original Subject ordinal - you'll need to preserve that info */)
.toList()
);
List<TimeTableInfoResponse> entries = projections.stream()
.map(info -> TimeTableInfoResponse.of(
info.examNumber(),
info.userName(),
subjectsByApplicationId.getOrDefault(info.examApplicationId(), List.of()),
info.schoolName()
))
.toList();
return generateTimeTableProcessor.process(entries);
}Note: You'll need to add a |
||
| } | ||
|
|
||
| @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); | ||
|
Comment on lines
+49
to
+64
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. Apply the same N+1 query fix here. This method has the same N+1 query problem as 🤖 Prompt for AI Agents |
||
| } | ||
|
Comment on lines
+28
to
+65
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.
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public TimeTableFileResponse getMemberTimeTables(LocalDate examDate) {
List<life.mosu.mosuserver.domain.examapplication.projection.TimeTableInfoProjection> infos = examApplicationJpaRepository.findMemberTimeTable(examDate);
return generateTimeTablesFor(infos);
}
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public TimeTableFileResponse getPartnerTimeTables(LocalDate examDate) {
List<life.mosu.mosuserver.domain.examapplication.projection.TimeTableInfoProjection> infos = examApplicationJpaRepository.findPartnerTimeTable(examDate);
return generateTimeTablesFor(infos);
}
private TimeTableFileResponse generateTimeTablesFor(List<life.mosu.mosuserver.domain.examapplication.projection.TimeTableInfoProjection> timeTableInfos) {
List<TimeTableInfoResponse> entries = timeTableInfos.stream()
.map(info -> {
Long examApplicationId = info.examApplicationId();
List<String> subjects = getSubjects(examApplicationId);
return new TimeTableInfoResponse(
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(); | ||
| }) | ||
|
Comment on lines
+72
to
+77
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. |
||
| .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. 🛠️ Refactor suggestion | 🟠 Major Extract duplicated subject mapping logic to eliminate code duplication. The subject mapping logic (fetch → sort by ordinal → map SOCIETY_AND_CULTURE → convert to names) is duplicated in three places:
This violates DRY and makes maintenance harder if the mapping rules change. Consider extracting to a shared utility or helper: @Component
public class SubjectMappingHelper {
private final ExamSubjectJpaRepository examSubjectJpaRepository;
public List<String> getLocalizedSubjectNames(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();
}
}Then inject and use this helper in all three locations: List<String> subjects = subjectMappingHelper.getLocalizedSubjectNames(examApplicationId); |
||
| } | ||
| 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)) { | ||||||||||||||
|
|
||||||||||||||
|
Comment on lines
+77
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. 🧩 Analysis chainVerify PDPageContentStream constructor vs. PDFBox version Using Loader suggests PDFBox 3.x; in 3.x the PDPageContentStream constructors changed (may require a resetContext parameter). Ensure this signature matches your pdfbox version. 🏁 Script executed: #!/bin/bash
# Check declared PDFBox version(s)
rg -nP --type-add 'gradle:*.gradle' --type-add 'maven:*.xml' --type gradle --type maven -C2 '(?i)pdfbox'Length of output: 220 Use correct PDPageContentStream constructor for PDFBox 3.x try (PDPageContentStream cs = new PDPageContentStream(
outDoc, page, PDPageContentStream.AppendMode.APPEND, true, /*resetContext*/ true)) {
…
}🤖 Prompt for AI Agents |
||||||||||||||
| 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)) : ""; | ||||||||||||||
|
Comment on lines
+105
to
+106
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. 과목 문자열을 가져오는 로직이 다소 장황하고, 불필요한 null 체크를 포함하고 있습니다.
Suggested change
|
||||||||||||||
|
|
||||||||||||||
| 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
|
||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+63
to
+133
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. Empty input produces an invalid PDF (no pages) If list is empty, outDoc has 0 pages; saving likely fails. Guard and return a valid PDF (e.g., the template) or raise a domain exception. @Override
public TimeTableFileResponse process(List<TimeTableInfoResponse> list) {
+ if (list == null || list.isEmpty()) {
+ // Option A: return the template as a 1-page PDF
+ return new TimeTableFileResponse(templatePdf, "time-tables.pdf", "application/pdf");
+ // Option B (alternative): throw a domain exception to map to 204/404
+ // throw new NoTimeTableDataException();
+ }
try (PDDocument templateDoc = Loader.loadPDF(templatePdf);
PDDocument outDoc = new PDDocument();
ByteArrayOutputStream out = new ByteArrayOutputStream()) {🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| 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 | ||||||
| WHERE p.paymentStatus = 'DONE' | ||||||
| AND e.examDate = :examDate | ||||||
| ORDER BY ea.examNumber | ||||||
| """) | ||||||
| List<TimeTableInfoProjection> findMemberTimeTable(@Param("examDate")LocalDate examDate); | ||||||
|
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. Add spacing after annotation. Missing space after Apply this diff: - List<TimeTableInfoProjection> findMemberTimeTable(@Param("examDate")LocalDate examDate);
+ List<TimeTableInfoProjection> findMemberTimeTable(@Param("examDate") LocalDate examDate);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
|
|
||||||
|
|
||||||
| @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); | ||||||
|
Comment on lines
+323
to
+339
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. Inconsistent JOIN type between member and partner queries. The
Verify that this difference is intentional. If both queries should handle missing data the same way, standardize the JOIN types. Apply this diff if INNER JOINs are appropriate for both: @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
+ JOIN PaymentJpaEntity p on p.examApplicationId = ea.id
+ JOIN ExamJpaEntity e on ea.examId = e.id
+ JOIN UserJpaEntity u on ea.userId = u.id
+ JOIN ProfileJpaEntity pr on pr.userId = u.id
WHERE p.paymentStatus = 'DONE'
AND e.examDate = :examDate
ORDER BY ea.examNumber
""")
List<TimeTableInfoProjection> findMemberTimeTable(@Param("examDate")LocalDate examDate);Or if LEFT JOINs are correct for both, update the partner query accordingly. |
||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
|
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. 🧩 Analysis chainVerify the security implications of public timetable access. The TIME_TABLE whitelist entry allows unauthenticated GET access to Run the following script to verify what endpoints will be publicly accessible: Additionally, consider if these endpoints should require authentication or at minimum some form of access control (e.g., date-based restrictions, API keys for partners). 🏁 Script executed: #!/bin/bash
# Description: Identify all timetable controller endpoints that will be publicly accessible
# Find TimeTableController and list its endpoints
ast-grep --pattern $'@RestController
$$$
class TimeTableController {
$$$
}'
# Also search for @GetMapping, @PostMapping annotations in TimeTableController
rg -A 5 '@(Get|Post|Put|Delete)Mapping' --type java -g '**/TimeTableController.java'Length of output: 1701 Restrict unauthenticated access to timetable endpoints. The TIME_TABLE whitelist entry (Whitelist.java:58) currently allows unauthenticated GET access to 🤖 Prompt for AI Agents |
||
|
|
||
| private static final List<ExceptionRule> AUTH_REQUIRED_EXCEPTIONS = List.of( | ||
| new ExceptionRule("/api/v1/exam-application", WhitelistMethod.GET) | ||
|
|
||
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등 여러 곳에서 중복되고 있습니다. 코드 중복은 향후 유지보수를 어렵게 만듭니다. 이 로직을Subjectenum 내부에getDisplayName()과 같은 공통 메서드로 추출하여 중앙에서 관리하는 것을 강력히 권장합니다.