Skip to content

Conversation

@chominju02
Copy link
Contributor

@chominju02 chominju02 commented Oct 12, 2025

✨ 구현한 기능

  • 수험번호 발급 기능 구현
  • 수험표 발급 기능 구현
  • 신청 정보 csv import 하여 신청 정보 등록 기능 구현

📢 논의하고 싶은 내용

🎸 기타

Summary by CodeRabbit

  • New Features

    • Admin CSV import page for bulk guest application uploads with a result summary.
    • Exam ticket download endpoints for partners and members, returning inline PDF files with proper filenames.
    • Automatic exam number generation scheduled for specific exam dates.
    • PDF exam ticket rendering with font support and image fallback.
    • Phone number display improved by removing unwanted prefixes.
  • Security

    • Enabled guest access to exam ticket retrieval.
    • Allowed admin CSV import endpoint in the whitelist.
  • Database

    • Added a field to track whether a test paper has been checked.

@chominju02 chominju02 self-assigned this Oct 12, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 12, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds guest CSV import flow, exam ticket generation (including PDF creation), and exam number generation via cron. Introduces supporting DTOs, processors, repositories, schema migration, whitelisting, and utilities. Removes legacy ExamNumberGenerationService. Updates ApplicationContext mappings and several repository queries. Includes new Thymeleaf admin pages and PDFBox dependencies.

Changes

Cohort / File(s) Summary
Build & Dependencies
build.gradle
Adds PDFBox dependencies (pdfbox, fontbox) for PDF generation.
Admin CSV Import Feature
presentation/admin/AdminApplicationImportController.java, application/admin/AdminApplicationImportService.java, application/admin/dto/ApplicationCsvInfo.java, application/admin/dto/ImportResultDto.java, application/admin/util/CsvReader.java, application/admin/processor/GetApplicationGuestRequestStepProcessor.java, application/admin/processor/ApplyGuestStepProcessor.java, application/admin/processor/ChangeTestPaperCheckedStepProcessor.java, application/admin/processor/RegisterVirtualAccountStepProcessor.java, resources/templates/admin/import.html, resources/templates/admin/import-result.html
Adds admin UI and backend to import guest applications from CSV. Parses rows, builds requests, applies guest applications, optionally marks test paper checked, registers virtual accounts, and summarizes results.
Exam Ticket Generation
presentation/examticket/ExamTicketController.java, application/examticket/ExamTicketService.java, application/examticket/processor/GetPartnerExamTicketInfoProcessor.java, application/examticket/processor/GetMemberExamTicketInfoProcessor.java, application/examticket/processor/GenerateExamTicketProcessor.java, presentation/examticket/dto/ExamTicketFileResponse.java, presentation/examticket/dto/ExamTicketIssueResponse.java, domain/examapplication/projection/ExamTicketIssueProjection.java
Adds endpoints to fetch exam tickets (partner/member). Gathers ticket info, resolves subjects/S3 image, and generates a PDF via PDFBox.
Exam Number Generation (Cron)
application/examapplication/cron/ExamNumberGeneratorExecutor.java, application/examapplication/cron/ExamNumberGeneratorExecutorImpl.java, application/examapplication/code/ExamNumberCode.java, application/examapplication/util/ExamNumberUtil.java, infra/cron/job/ExamNumberGenerationJobRound1.java, infra/cron/job/ExamNumberGenerationJobRound2.java
Introduces cron executor and jobs to generate exam numbers per date, with code maps and formatter. Processes applications by subject groups and assigns formatted exam numbers.
Repository & Entity Enhancements
domain/exam/entity/ExamJpaRepository.java, domain/examapplication/repository/ExamApplicationJpaRepository.java, domain/virtualaccount/VirtualAccountLogJpaRepository.java, domain/examapplication/entity/ExamApplicationJpaEntity.java, resources/db/migration/V4__add_test_paper_checked.sql
Adds exam queries (by date, by date+school), projections for ticket issuing, sorted retrieval for number generation, virtual account log query by orderId+status, and a new isTestPaperChecked column with setter. Adds migration for new column.
Application Context & Services
application/application/ApplicationContext.java, application/examapplication/ExamApplicationService.java, domain/examapplication/service/ExamNumberGenerationService.java
Expands ApplicationContext with exam/subject/payment/refund maps and related fetch/assemble paths. Removes ExamNumberGenerationService and its use from ExamApplicationService.
Presentation DTO touch-ups
presentation/application/dto/ExamApplicationResponse.java, domain/examapplication/projection/ExamApplicationInfoProjection.java, presentation/examapplication/dto/ExamApplicationInfoResponse.java
Adds commented placeholders for test-paper checks; no API change. Minor import adjustments.
Global & Infra
global/filter/Whitelist.java, global/exception/ErrorCode.java, global/util/PhoneNumberUtil.java, infra/config/QuartzAutoRegisterConfig.java, infra/cron/annotation/CronJob.java, infra/cron/job/ArchivingOrchestratorJob.java
Whitelists exam ticket and admin CSV import paths; adds EXAM_TICKET_INFO_NOT_FOUND; adds phone prefix remover; import cleanups; annotates CronJob with @component.
Misc
infra/notify/LunaSoftNotifier.java, domain/application/entity/ApplicationJpaEntity.java
Disables actual webClient call in notifier (commented out). Consolidates imports with wildcards; no behavior change.

Sequence Diagram(s)

sequenceDiagram
  actor Admin
  participant C as AdminApplicationImportController
  participant S as AdminApplicationImportService
  participant R as CsvReader
  participant P1 as GetApplicationGuestRequestStepProcessor
  participant P2 as ApplyGuestStepProcessor
  participant P3 as ChangeTestPaperCheckedStepProcessor
  participant P4 as RegisterVirtualAccountStepProcessor

  Admin->>C: POST /admin/applications/import/guests (file)
  C->>S: importGuestApplications(file)
  S->>R: read(file)
  R-->>S: List<ApplicationCsvInfo>
  loop For each row
    S->>P1: process(csvInfo)
    P1-->>S: ApplicationGuestRequest
    S->>P2: process(request)
    P2-->>S: applicationId
    alt csvInfo.isTestPaperChecked
      S->>P3: process(applicationId)
      P3-->>S: examApplicationIds
    end
    S->>P4: process(applicationId)
    P4-->>S: virtualAccountLogId
  end
  S-->>C: ImportResultDto
  C-->>Admin: View "admin/import-result"
Loading
sequenceDiagram
  actor Client
  participant EC as ExamTicketController
  participant ES as ExamTicketService
  participant IP as InfoProcessor (Partner/Member)
  participant REP as Repositories/S3
  participant GP as GenerateExamTicketProcessor
  participant PDF as PDFBox

  Client->>EC: GET /exam-ticket/(partner|member)/...
  EC->>ES: get...ExamTicket(...)
  ES->>IP: process(key)
  IP->>REP: fetch projection, subjects, image URL
  REP-->>IP: Data
  IP-->>ES: ExamTicketIssueResponse
  ES->>GP: process(issueResponse)
  GP->>PDF: load template/font/image, draw text/image
  PDF-->>GP: bytes
  GP-->>ES: ExamTicketFileResponse
  ES-->>EC: ExamTicketFileResponse
  EC-->>Client: PDF bytes + headers
Loading
sequenceDiagram
  participant Quartz as Quartz Scheduler
  participant J as ExamNumberGenerationJob(R1/R2)
  participant EX as ExamNumberGeneratorExecutorImpl
  participant ER as ExamJpaRepository
  participant AR as ExamApplicationJpaRepository
  participant EC as ExamNumberCode/Util
  participant EA as ExamApplication Entities

  Quartz->>J: Trigger execute()
  J->>EX: generate(examDate)
  EX->>ER: findAllByExamDate(examDate)
  ER-->>EX: exams
  loop per exam
    EX->>AR: findDoneAndSortByTestPaperGroup(examId, groups)
    AR-->>EX: applications
    EX->>EC: getRound/Area/School codes
    EC-->>EX: codes
    loop assign numbers
      EX->>EC: formatExamNumber(...)
      EC-->>EX: examNumber
      EX->>EA: grantExamNumber(examNumber)
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

Suggested reviewers

  • wlgns12370
  • jbh010204
  • polyglot-k

Poem

I nibbled the CSV, row by row,
Stamped numbers in a moonlit glow.
Tickets flutter—PDF wings!
Subjects dance, the school bell rings.
Cron clocks chime; I hop on cue—
Import, generate, and print anew. 🐇📄✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly summarizes the primary features implemented—exam number issuance and admission ticket generation—in a concise, single sentence that directly reflects the main changes without extraneous details.
Description Check ✅ Passed The description follows the repository template exactly, including the required “✨ 구현한 기능”, “📢 논의하고 싶은 내용”, and “🎸 기타” sections, and clearly lists all implemented features such as exam number issuance, ticket generation, and CSV import.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/ticket

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @chominju02, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 시험 신청 관리 시스템에 중요한 기능들을 도입합니다. 응시자에게 고유한 수험번호를 자동으로 부여하고, 개인화된 수험표를 PDF 형태로 발급할 수 있도록 하여 사용자 경험을 향상시킵니다. 또한, 관리자가 CSV 파일을 통해 대량의 게스트 신청 데이터를 효율적으로 등록할 수 있는 기능을 제공하여 운영 편의성을 크게 높였습니다. 이로써 시험 신청부터 수험표 발급까지의 전반적인 프로세스가 더욱 자동화되고 체계화되었습니다.

Highlights

  • 수험번호 발급 기능 구현: 시험 응시자에게 고유한 수험번호를 생성하고 부여하는 기능이 추가되었습니다. 이 기능은 시험일, 지역, 학교 코드 및 개인 코드를 조합하여 수험번호를 생성하며, Quartz 스케줄러를 통해 특정 시험일에 맞춰 자동으로 수험번호를 부여합니다.
  • 수험표 발급 기능 구현: 응시자의 수험표를 PDF 형식으로 생성하여 제공하는 기능이 추가되었습니다. 이 기능은 수험표 템플릿에 응시자 정보(사진, 이름, 생년월일, 수험번호, 학교명, 과목 등)를 동적으로 삽입하여 PDF 파일을 생성하며, 회원 및 파트너(가상계좌 주문번호)를 통해 조회할 수 있습니다.
  • 신청 정보 CSV 일괄 등록 기능 구현: 관리자가 CSV 파일을 업로드하여 게스트 신청 정보를 일괄적으로 등록할 수 있는 기능이 추가되었습니다. CSV 파일의 각 행을 파싱하여 신청 정보를 시스템에 등록하고, 시험지 확인 여부 및 가상계좌 등록 처리까지 자동화합니다. 처리 결과는 웹 페이지를 통해 요약 보고됩니다.
  • PDFBox 라이브러리 추가: 수험표 PDF 생성을 위해 org.apache.pdfbox:pdfboxorg.apache.pdfbox:fontbox 라이브러리가 build.gradle에 추가되었습니다.
  • 시험 신청 엔티티에 시험지 확인 여부 필드 추가: ExamApplicationJpaEntityisTestPaperChecked 필드가 추가되어 시험지 확인 여부를 관리할 수 있게 되었습니다. 이는 CSV 일괄 등록 시에도 활용됩니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

이 PR은 수험번호 및 수험표 발급 기능과 CSV를 통한 신청 정보 일괄 등록 기능을 구현합니다. 전반적으로 기능 구현은 잘 이루어졌지만, 몇 가지 개선점을 제안합니다. CSV 가져오기 기능의 오류 처리 로직을 개선하여 실패 시에도 사용자에게 명확한 결과를 보여주도록 수정이 필요합니다. 또한, NullPointerException 발생 가능성이 있는 부분을 방어하고, 일부 하드코딩된 값들을 상수로 추출하여 코드의 안정성과 유지보수성을 높이는 것이 좋겠습니다. 알림 전송 로직이 주석 처리되어 있는데, 의도된 것인지 확인이 필요해 보입니다. 마지막으로 사용하지 않는 코드와 주석 처리된 코드를 정리하여 코드베이스를 깔끔하게 유지하는 것을 권장합니다.


LocalDate examDate = csvInfo.examDate();
String schoolName = csvInfo.examSchool();
ExamJpaEntity exam = examJpaRepository.findByExamDateAndSchoolName(examDate, schoolName);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

examJpaRepository.findByExamDateAndSchoolName(examDate, schoolName) 메서드가 null을 반환할 경우, 다음 줄에서 exam.getId()를 호출할 때 NullPointerException이 발생할 수 있습니다. 데이터베이스에 해당 시험 정보가 없는 경우를 대비하여 null 체크 로직을 추가해야 합니다.

        ExamJpaEntity exam = examJpaRepository.findByExamDateAndSchoolName(examDate, schoolName);
        if (exam == null) {
            throw new IllegalArgumentException("해당 시험 정보가 존재하지 않습니다. examDate=" + examDate + ", schoolName=" + schoolName);
        }

Comment on lines 24 to 36
public void send(LunaNotifyEventRequest request) {
LunaNotifyRequest lunaRequest = createLunaNotifyRequest(request);

webClient.post()
.uri(properties.getApi().getBaseUrl())
.bodyValue(lunaRequest)
.retrieve()
.bodyToMono(String.class)
.publishOn(Schedulers.boundedElastic())
.doOnSuccess(response -> log.debug("알림톡 응답 성공"))
.doOnError(error -> log.error("알림톡 전송 실패", error))
.subscribe();
// LunaNotifyRequest lunaRequest = createLunaNotifyRequest(request);
//
// webClient.post()
// .uri(properties.getApi().getBaseUrl())
// .bodyValue(lunaRequest)
// .retrieve()
// .bodyToMono(String.class)
// .publishOn(Schedulers.boundedElastic())
// .doOnSuccess(response -> log.debug("알림톡 응답 성공"))
// .doOnError(error -> log.error("알림톡 전송 실패", error))
// .subscribe();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

send 메서드의 내용 전체가 주석 처리되어 있어 알림톡 전송 기능이 비활성화된 것으로 보입니다. 개발 또는 테스트 목적으로 의도된 변경이라면 괜찮지만, 만약 실수로 주석 처리된 것이라면 프로덕션 환경에서 알림이 발송되지 않는 문제가 발생할 수 있습니다. 이 변경이 의도된 것인지 확인해주세요.

Comment on lines +30 to +52
@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 일괄 처리 중 오류 발생");
}
}

int fail = total - success;
log.info("CSV Import 완료 - 총 {}건, 성공 {}, 실패 {}", total, success, fail);
return new ImportResultDto(total, success, fail);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

importGuestApplications 메서드의 오류 처리 방식에 개선이 필요해 보입니다. 현재 구조에서는 CSV 처리 중 오류가 발생하면 RuntimeException을 발생시켜 트랜잭션을 롤백합니다. 이는 데이터 정합성 측면에서는 좋은 방법이지만, 예외가 컨트롤러까지 전파되어 사용자에게는 일반적인 오류 페이지만 보이게 됩니다. import-result.html에 구현된 실패 결과 화면을 사용자에게 보여주려면 서비스 계층에서 예외를 던지는 대신, 트랜잭션 롤백을 수동으로 설정하고 실패 정보를 담은 ImportResultDto를 반환하는 방식이 더 적합합니다.

또한, Spring 환경에서는 jakarta.transaction.Transactional보다 Spring이 제공하는 @Transactional 어노테이션을 사용하는 것이 일관성 및 기능 활용(TransactionAspectSupport 등) 측면에서 더 좋습니다.

    @Transactional
    public ImportResultDto importGuestApplications(MultipartFile file) {
        List<ApplicationCsvInfo> rows = csvReader.read(file);

        int total = rows.size();

        int lineNo = 1;
        for (ApplicationCsvInfo row : rows) {
            lineNo++;
            try {
                processGuestRow(row);
            } catch (Exception e) {
                log.error("게스트 신청 CSV 행 {} 처리 실패: {}", lineNo, e.getMessage(), e);
                TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                log.info("CSV Import 실패로 인해 롤백됩니다.");
                return new ImportResultDto(total, 0, total);
            }
        }

        log.info("CSV Import 완료 - 총 {}건, 성공 {}, 실패 {}", total, total, 0);
        return new ImportResultDto(total, total, 0);
    }

Comment on lines +15 to +21
return String.format(
"%d%02d%02d%04d",
roundCode,
areaCode,
schoolCode,
personalCode
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

String.format에 사용된 포맷 문자열 "%d%02d%02d%04d"가 매직 스트링으로 사용되고 있습니다. 이 문자열을 상수로 추출하면 코드의 의도를 더 명확하게 하고 유지보수성을 높일 수 있습니다.

        final String EXAM_NUMBER_FORMAT = "%d%02d%02d%04d";
        return String.format(
                EXAM_NUMBER_FORMAT,
                roundCode,
                areaCode,
                schoolCode,
                personalCode
        );

exam.getExamDate(),
subjects,
lunchName
// examApp.getIsTestPaperChecked()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

주석 처리된 코드가 남아있습니다. 사용하지 않는 코드는 가독성을 위해 제거하는 것이 좋습니다.

String schoolName,
AddressJpaVO address,
Boolean isLunchChecked,
// Boolean isTeacherChecked,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

주석 처리된 코드가 남아있습니다. 사용하지 않는 코드는 가독성을 위해 제거하는 것이 좋습니다.

Comment on lines 1 to 9
package life.mosu.mosuserver.global.support;

import java.time.LocalDate;

public interface NumberGeneratorWithDate {

String generate(LocalDate examDate);

}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

새로 추가된 NumberGeneratorWithDate 인터페이스가 현재 PR의 변경사항 내에서 사용되지 않는 것으로 보입니다. 만약 향후 사용될 예정이 아니라면, 사용하지 않는 코드는 혼란을 줄 수 있으므로 제거하는 것이 좋습니다.

Comment on lines +36 to +38
if (values.length < 11) {
throw new IllegalArgumentException("CSV 행이 필수 컬럼 수(11개)보다 부족합니다. 실제 컬럼 수: " + values.length);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

of(String[] values) 메서드에서 CSV 컬럼 수를 나타내는 11이 하드코딩되어 있습니다. 이 값을 private static final int REQUIRED_COLUMN_COUNT = 11;과 같이 상수로 추출하면 코드의 가독성과 유지보수성이 향상됩니다.

LocalDate examDate,
Set<String> subjects,
String lunchName
// Boolean isTestPaperChecked

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

주석 처리된 코드가 남아있습니다. 사용하지 않는 코드는 가독성을 위해 제거하는 것이 좋습니다.

AddressResponse address,
Set<String> subjects,
String lunchName,
// Boolean isTestPaperChecked,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

주석 처리된 코드가 남아있습니다. 사용하지 않는 코드는 가독성을 위해 제거하는 것이 좋습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 35

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/life/mosu/mosuserver/application/examapplication/ExamApplicationService.java (1)

43-47: Critical: Register method doesn't persist entities.

The register method is marked @Transactional but only converts the event to entities without saving them to the database. This will result in exam applications not being persisted.

Apply this diff to save the entities:

 @Transactional
 public List<ExamApplicationJpaEntity> register(RegisterExamApplicationEvent event) {
     List<ExamApplicationJpaEntity> examApplicationEntities = event.toEntity();
-    return examApplicationEntities;
+    return examApplicationJpaRepository.saveAll(examApplicationEntities);
 }
🧹 Nitpick comments (26)
src/main/java/life/mosu/mosuserver/domain/examapplication/projection/ExamApplicationInfoProjection.java (1)

16-16: Remove the commented-out field.

Leaving dead/commented code in a projection bloats the API surface with noise. If the field is not part of the contract anymore, please drop the comment outright; otherwise, reintroduce it properly.

src/main/java/life/mosu/mosuserver/presentation/examapplication/dto/ExamApplicationInfoResponse.java (1)

16-17: Remove lingering commented-out field wiring.

The commented isTestPaperChecked field and factory wiring add noise and invite drift. Either bring the field back with full support or drop the commented lines altogether to keep the DTO clean.

Also applies to: 30-31, 43-44

src/main/java/life/mosu/mosuserver/infra/config/QuartzAutoRegisterConfig.java (1)

6-6: Consider using explicit imports instead of wildcard.

The wildcard import org.quartz.* makes it less clear which specific Quartz classes are being used. Explicit imports improve code readability and help avoid potential naming conflicts.

Apply this diff to use explicit imports:

-import org.quartz.*;
+import org.quartz.CronScheduleBuilder;
+import org.quartz.Job;
+import org.quartz.JobBuilder;
+import org.quartz.JobDetail;
+import org.quartz.Trigger;
+import org.quartz.TriggerBuilder;
src/main/java/life/mosu/mosuserver/global/util/PhoneNumberUtil.java (1)

27-32: LGTM! Consider adding documentation.

The implementation correctly removes the prefix characters (G/g/U/u) added by formatGuestPhoneNumber and formatPhoneNumber. The case-insensitive regex pattern and null handling are appropriate for a utility method likely used in data processing pipelines.

Note: This method returns null for null input, whereas formatPhoneNumberWithHyphen throws an exception. This difference appears intentional for flexibility in data pipelines, but ensure this behavior aligns with how the method is used in the CSV import feature.

Consider adding JavaDoc to document the method's purpose and behavior:

+    /**
+     * Removes the prefix (G/g/U/u) from a phone number.
+     * 
+     * @param phoneNumber the phone number with prefix, or null
+     * @return the phone number without prefix, or null if input is null
+     */
     public static String removePrefix(String phoneNumber) {
         if (phoneNumber == null) {
             return null;
         }
         return phoneNumber.replaceFirst("^[GgUu]", "");
     }
src/main/java/life/mosu/mosuserver/application/admin/processor/ApplyGuestStepProcessor.java (1)

25-26: Reconsider exception type for "not found" after creation.

IllegalArgumentException typically indicates invalid input. However, if an application cannot be found immediately after creation, this suggests a data consistency or concurrency issue, not invalid input.

Consider using IllegalStateException or a custom exception to better convey the nature of the problem:

-        .orElseThrow(() -> new IllegalArgumentException("신청을 찾을 수 없습니다. id=" + applicationId));
+        .orElseThrow(() -> new IllegalStateException("방금 생성된 신청을 찾을 수 없습니다. id=" + applicationId));

This makes the exceptional condition clearer for debugging and monitoring.

src/main/java/life/mosu/mosuserver/application/examapplication/code/ExamNumberCode.java (5)

15-19: Consider externalizing hardcoded exam dates.

The roundCodeMap contains hardcoded exam dates that will require code changes for future exam rounds. Consider:

  1. Loading these mappings from application.yml or a database table for easier updates without redeployment.
  2. Adding validation to ensure exam dates are in the future when the application starts.

Example configuration approach in application.yml:

exam:
  rounds:
    - date: 2025-10-19
      code: 1
    - date: 2025-10-26
      code: 2
    - date: 2025-11-02
      code: 3

Then inject via @ConfigurationProperties:

@Component
@ConfigurationProperties(prefix = "exam")
@Getter
@Setter
public class ExamNumberCode {
    private List<RoundMapping> rounds;
    private Map<Area, Integer> areas;
    private Map<String, Integer> schools;
    
    public Integer getRoundCode(LocalDate examDate) {
        return rounds.stream()
            .filter(r -> r.getDate().equals(examDate))
            .findFirst()
            .map(RoundMapping::getCode)
            .orElseThrow(() -> new IllegalArgumentException(
                "해당 시험일에 해당하는 코드가 존재하지 않습니다"));
    }
    
    @Getter
    @Setter
    public static class RoundMapping {
        private LocalDate date;
        private Integer code;
    }
}

28-36: Prefer database or configuration for school codes.

The schoolCodeMap hardcodes school names and codes. As the number of schools grows beyond 10 entries, Map.of() will fail (it supports maximum 10 entries). Consider:

  1. Storing school codes in a database table for dynamic management.
  2. Using @ConfigurationProperties with a Map<String, Integer> (no entry limit).
  3. Providing an admin interface to manage school codes without code deployments.

12-12: Reconsider exposing internal maps via @Getter.

The @Getter annotation exposes the internal maps publicly, which violates encapsulation. While Map.of() returns immutable maps, exposing them still reveals internal implementation details.

Remove @Getter and rely solely on the lookup methods for access:

 @Component
-@Getter
 public class ExamNumberCode {

38-54: Improve error messages for production observability.

The error messages are in Korean and lack context about the invalid input, making debugging harder in production logs.

Apply this pattern to improve observability:

 public Integer getRoundCode(LocalDate examDate) {
     return Optional.ofNullable(roundCodeMap.get(examDate))
             .orElseThrow(() -> new IllegalArgumentException(
-                    "해당 시험일에 해당하는 코드가 존재하지 않습니다"));
+                    String.format("No round code found for exam date: %s. Available dates: %s",
+                        examDate, roundCodeMap.keySet())));
 }
 
 public Integer getAreaCode(Area area) {
     return Optional.ofNullable(areaCodeMap.get(area))
             .orElseThrow(() -> new IllegalArgumentException(
-                    "해당 지역에 해당하는 코드가 존재하지 않습니다"));
+                    String.format("No area code found for area: %s. Available areas: %s",
+                        area, areaCodeMap.keySet())));
 }
 
 public Integer getSchoolCode(String schoolName) {
     return Optional.ofNullable(schoolCodeMap.get(schoolName))
             .orElseThrow(() -> new IllegalArgumentException(
-                    "해당 학교명에 해당하는 코드가 존재하지 않습니다"));
+                    String.format("No school code found for school: '%s'. Available schools: %s",
+                        schoolName, schoolCodeMap.keySet())));
 }

11-13: Add startup validation for exam number code mappings.

Without validation, invalid or incomplete mappings won't be detected until runtime when an exam number is generated, potentially causing failures during critical operations.

Add a @PostConstruct validation method:

+import jakarta.annotation.PostConstruct;
+import java.util.Arrays;
+import java.util.EnumSet;
+
 @Component
 @Getter
 public class ExamNumberCode {
 
     private final Map<LocalDate, Integer> roundCodeMap = Map.of(
             LocalDate.of(2025, 10, 19), 1,
             LocalDate.of(2025, 10, 26), 2,
             LocalDate.of(2025, 11, 2), 3
     );
 
     private final Map<Area, Integer> areaCodeMap = Map.of(
             Area.DAECHI, 1,
             Area.MOKDONG, 2,
             Area.NOWON, 3,
             Area.DAEGU, 4
     );
 
     private final Map<String, Integer> schoolCodeMap = Map.of(
             "대치중학교", 7,
             "개원중학교", 6,
             "문래중학교", 5,
             "목운중학교", 4,
             "신서중학교", 3,
             "온곡중학교", 2,
             "노변중학교", 1
     );
+
+    @PostConstruct
+    public void validate() {
+        // Validate all Area enums have mappings
+        EnumSet<Area> unmappedAreas = EnumSet.complementOf(
+            EnumSet.copyOf(areaCodeMap.keySet()));
+        if (!unmappedAreas.isEmpty()) {
+            throw new IllegalStateException(
+                "Missing area code mappings for: " + unmappedAreas);
+        }
+        
+        // Validate round codes are not empty
+        if (roundCodeMap.isEmpty()) {
+            throw new IllegalStateException("Round code map cannot be empty");
+        }
+        
+        // Validate school codes are not empty
+        if (schoolCodeMap.isEmpty()) {
+            throw new IllegalStateException("School code map cannot be empty");
+        }
+        
+        log.info("ExamNumberCode validation passed. Rounds: {}, Areas: {}, Schools: {}",
+            roundCodeMap.size(), areaCodeMap.size(), schoolCodeMap.size());
+    }
src/main/resources/db/migration/V4__add_test_paper_checked.sql (2)

1-2: Consider NOT NULL constraint for boolean flag.

The test_paper_checked column is defined as NULL with a DEFAULT b'0'. This creates ambiguity:

  • If NULL is allowed, what does it represent vs. b'0' (false)?
  • For a boolean flag, typically NOT NULL is preferred to avoid tri-state logic.

Unless there's a specific business need for NULL to represent "unknown" or "not applicable", apply this diff:

 ALTER TABLE exam_application
-    ADD COLUMN test_paper_checked BIT(1) NULL DEFAULT b'0';
+    ADD COLUMN test_paper_checked BIT(1) NOT NULL DEFAULT b'0';

Also, ensure the file ends with a newline to comply with POSIX standards and avoid git diff warnings.


1-2: LGTM! Migration adds test_paper_checked column correctly.

The migration appropriately adds a nullable BIT(1) column with a default value of b'0' (false), ensuring backward compatibility with existing records.

Minor: Consider adding a newline at the end of the file for consistency with Unix conventions.

src/main/java/life/mosu/mosuserver/domain/virtualaccount/VirtualAccountLogJpaRepository.java (1)

11-12: Add composite index on (order_id, deposit_status) to virtual_account_log
Create a new migration under src/main/resources/db/migration that adds

CREATE INDEX idx_va_log_order_deposit 
  ON virtual_account_log(order_id, deposit_status);
src/main/java/life/mosu/mosuserver/application/admin/processor/RegisterVirtualAccountStepProcessor.java (1)

18-25: Document the null parameters and auto-success behavior.

The processor creates a VirtualAccountLogJpaEntity with several null fields (bankCode, accountNumber, accountOwner, depositName) and immediately marks the deposit as successful without actual payment verification. This pattern suggests these are pre-approved guest applications where payment is handled separately from the virtual account flow.

Consider adding documentation to clarify:

  • Why these fields are null (e.g., guest applications don't require actual virtual accounts)
  • Why deposit is auto-marked successful (e.g., payment verification happens through a different channel for CSV imports)

Example documentation:

+    /**
+     * Registers a virtual account log entry for admin-imported guest applications.
+     * For guest imports via CSV, payment is verified externally, so we create a 
+     * placeholder log entry with deposit pre-marked as successful.
+     * 
+     * @param applicationId the application ID to register
+     * @return the saved virtual account log ID
+     */
     @Override
     public Long process(Long applicationId) {
src/main/java/life/mosu/mosuserver/domain/examapplication/entity/ExamApplicationJpaEntity.java (3)

3-3: Consider using explicit imports instead of wildcard.

Wildcard imports can lead to namespace pollution and make it harder to track dependencies. While this is a minor style issue, explicit imports improve code clarity.


70-72: Consider renaming the setter for clarity.

The method setTestPaperChecked() acts as a flag-setter with no parameters, which is unconventional. While the implementation is correct for marking test papers as checked, a more descriptive name like markTestPaperAsChecked() or checkTestPaper() would better convey the intent.

Example refactor:

-    public void setTestPaperChecked() {
+    public void markTestPaperAsChecked() {
         this.isTestPaperChecked = Boolean.TRUE;
     }

3-3: Consider avoiding wildcard imports.

The wildcard import jakarta.persistence.* was introduced, replacing the previous individual imports. While this is not a critical issue, explicit imports are generally preferred because they:

  • Make dependencies clearer
  • Avoid naming conflicts
  • Help IDEs and tools with refactoring

If the project's coding guidelines permit wildcard imports, you can safely ignore this suggestion.

src/main/resources/templates/admin/import-result.html (2)

145-154: Remove or implement the commented-out error details section.

Lines 145-154 contain commented-out code for displaying detailed error messages. If error detail display is planned for a future iteration, consider removing this dead code to keep the template clean, or uncomment and implement it if the functionality is needed now.


145-154: Remove commented-out error details section or add TODO.

The error messages section is commented out (lines 145-154). If this feature is planned for future implementation, add a TODO comment explaining what's needed. Otherwise, remove the dead code to keep the template clean.

Example TODO:

+        <!-- TODO: Implement detailed error message display when error tracking is added to ImportResultDto -->
         <!--        <h4>오류 상세 내용 (롤백을 유발한 오류):</h4>-->
src/main/java/life/mosu/mosuserver/presentation/admin/AdminApplicationImportController.java (1)

26-31: Add file validation for CSV uploads.

The importGuests method accepts a MultipartFile without validation. Consider adding checks for:

  • File size limits (prevent DoS via large files)
  • File type/MIME type (ensure it's a CSV)
  • File name validation (prevent path traversal)

Example validation:

     @PostMapping("/guests")
     public String importGuests(@RequestParam("file") MultipartFile file, Model model) {
+        // Validate file
+        if (file.isEmpty()) {
+            model.addAttribute("error", "파일을 선택해주세요.");
+            return "admin/import";
+        }
+        
+        // Check file size (e.g., 10MB limit)
+        if (file.getSize() > 10 * 1024 * 1024) {
+            model.addAttribute("error", "파일 크기는 10MB를 초과할 수 없습니다.");
+            return "admin/import";
+        }
+        
+        // Validate content type
+        String contentType = file.getContentType();
+        if (contentType == null || !contentType.equals("text/csv")) {
+            model.addAttribute("error", "CSV 파일만 업로드 가능합니다.");
+            return "admin/import";
+        }
+
         ImportResultDto result = adminApplicationImportService.importGuestApplications(file);

Note: File size can also be configured globally via spring.servlet.multipart.max-file-size in application properties.

src/main/java/life/mosu/mosuserver/application/admin/util/CsvReader.java (1)

43-45: Consider validating CSV structure.

The reader splits each line by comma and passes the raw array to ApplicationCsvInfo.of(values), but doesn't validate the expected number of columns. If a row has too few or too many columns, the error will only surface when ApplicationCsvInfo.of() tries to access array indices.

Consider adding a column count check:

+                private static final int EXPECTED_COLUMN_COUNT = 8; // Update based on actual CSV structure
+
                 try {
                     String[] values = line.split(CSV_DELIMITER, -1);
+                    if (values.length != EXPECTED_COLUMN_COUNT) {
+                        throw new IllegalArgumentException(
+                            String.format("잘못된 열 수: 예상 %d개, 실제 %d개", EXPECTED_COLUMN_COUNT, values.length)
+                        );
+                    }
                     results.add(ApplicationCsvInfo.of(values));
src/main/java/life/mosu/mosuserver/application/examticket/processor/GenerateExamTicketProcessor.java (1)

66-66: Consider caching the loaded font to improve performance.

Loading the font on every request via PDType0Font.load(doc, new ByteArrayInputStream(fontBytes)) may incur unnecessary overhead. While the byte array is cached, PDFBox still parses and loads the font structure each time.

Consider caching the loaded font at the class level if PDFBox supports font reuse across documents, or measure whether the current approach meets your performance requirements.

src/main/java/life/mosu/mosuserver/domain/examapplication/repository/ExamApplicationJpaRepository.java (1)

247-278: Complex query: verify performance and index coverage.

This query performs multiple LEFT JOINs, aggregation, and a multi-level ORDER BY with randomization. Consider these points:

  1. Performance: The GROUP BY with aggregation and random sorting may be slow on large datasets. Ensure appropriate indexes exist on:

    • ExamApplicationJpaEntity.examId
    • PaymentJpaEntity.examApplicationId and paymentStatus
    • VirtualAccountLogJpaEntity.applicationId and depositStatus
    • ExamSubjectJpaEntity.examApplicationId
  2. Random sorting: function('rand', 1) is database-specific (MySQL/H2). If you migrate to PostgreSQL, you'll need random() instead. Consider documenting this dependency or using a dialect-agnostic approach if portability matters.

  3. Correctness: Verify that GROUP BY ea.id, ea.isTestPaperChecked is intentional and that other aggregated columns are correctly handled.

Run an EXPLAIN query to verify index usage and performance, especially if you expect thousands of exam applications per exam.

src/main/java/life/mosu/mosuserver/infra/cron/job/ExamNumberGenerationJobRound2.java (1)

13-17: Consider parameterizing cron jobs for maintainability.

Both ExamNumberGenerationJobRound1 and ExamNumberGenerationJobRound2 follow identical patterns with only the date and cron expression differing. This duplication creates maintenance burden.

Consider one of these approaches:

  1. Make the exam date configurable via properties and use a single job class
  2. Use a data-driven approach where job configurations are stored in the database or configuration files
  3. At minimum, extract common logic into a base class or shared method

This would reduce duplication and make it easier to add future exam rounds.

src/main/java/life/mosu/mosuserver/application/admin/dto/ApplicationCsvInfo.java (2)

62-68: Boolean parsing silently treats invalid values as false.

Boolean.parseBoolean(trimmedValue) returns false for any non-"true" string (case-insensitive). This means CSV values like "yes", "1", "Y", or typos will silently become false.

If your CSV uses strict "true"/"false" values, the current behavior is acceptable. However, if the CSV might contain variations or you want to catch data quality issues, consider more explicit parsing:

private static Boolean parseBoolean(String bool) {
    if (bool == null || bool.isBlank()) {
        return false;
    }
    String trimmedValue = bool.trim().toLowerCase();
    return switch (trimmedValue) {
        case "true", "1", "yes", "y" -> true;
        case "false", "0", "no", "n" -> false;
        default -> throw new IllegalArgumentException(
            "잘못된 boolean 값: '" + bool + "', 예상값: true/false");
    };
}

Or document that only "true" (case-insensitive) is accepted and other values default to false.


35-52: Enforce exact column count or document extra columns
The factory requires ≥11 fields but drops any extras without notice; either validate values.length == 11 in ApplicationCsvInfo.of or update its Javadoc to state that additional columns are intentionally ignored.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 33eeee6 and 10f9d2f.

⛔ Files ignored due to path filters (8)
  • src/main/resources/fonts/NanumGothic-Regular.ttf is excluded by !**/*.ttf
  • src/main/resources/fonts/NotoSansKR-Black.ttf is excluded by !**/*.ttf
  • src/main/resources/fonts/NotoSansKR-Light.ttf is excluded by !**/*.ttf
  • src/main/resources/fonts/NotoSansKR-Medium.ttf is excluded by !**/*.ttf
  • src/main/resources/fonts/NotoSansKR-Regular.ttf is excluded by !**/*.ttf
  • src/main/resources/fonts/NotoSansKR-Thin.ttf is excluded by !**/*.ttf
  • src/main/resources/static/default-profile.png is excluded by !**/*.png
  • src/main/resources/static/exam-ticket.pdf is excluded by !**/*.pdf
📒 Files selected for processing (46)
  • build.gradle (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/admin/AdminApplicationImportService.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/admin/dto/ApplicationCsvInfo.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/admin/dto/ImportResultDto.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/admin/processor/ApplyGuestStepProcessor.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/admin/processor/ChangeTestPaperCheckedStepProcessor.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/admin/processor/GetApplicationGuestRequestStepProcessor.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/admin/processor/RegisterVirtualAccountStepProcessor.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/admin/util/CsvReader.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/application/ApplicationContext.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/application/examapplication/ExamApplicationService.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/application/examapplication/code/ExamNumberCode.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/examapplication/cron/ExamNumberGeneratorExecutor.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/examapplication/cron/ExamNumberGeneratorExecutorImpl.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/examapplication/util/ExamNumberUtil.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/examticket/ExamTicketService.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/examticket/processor/GenerateExamTicketProcessor.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/examticket/processor/GetMemberExamTicketInfoProcessor.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/examticket/processor/GetPartnerExamTicketInfoProcessor.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/application/entity/ApplicationJpaEntity.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/exam/entity/ExamJpaRepository.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/examapplication/entity/ExamApplicationJpaEntity.java (3 hunks)
  • src/main/java/life/mosu/mosuserver/domain/examapplication/projection/ExamApplicationInfoProjection.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/examapplication/projection/ExamTicketIssueProjection.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/examapplication/repository/ExamApplicationJpaRepository.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/domain/examapplication/service/ExamNumberGenerationService.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/domain/virtualaccount/VirtualAccountLogJpaRepository.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/global/filter/Whitelist.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/global/support/NumberGeneratorWithDate.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/global/util/PhoneNumberUtil.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/config/QuartzAutoRegisterConfig.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/infra/cron/annotation/CronJob.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/cron/job/ArchivingOrchestratorJob.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/cron/job/ExamNumberGenerationJobRound1.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/cron/job/ExamNumberGenerationJobRound2.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/LunaSoftNotifier.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/admin/AdminApplicationImportController.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/application/dto/ExamApplicationResponse.java (3 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/examapplication/dto/ExamApplicationInfoResponse.java (4 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/examticket/ExamTicketController.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/examticket/dto/ExamTicketFileResponse.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/examticket/dto/ExamTicketIssueResponse.java (1 hunks)
  • src/main/resources/db/migration/V4__add_test_paper_checked.sql (1 hunks)
  • src/main/resources/templates/admin/import-result.html (1 hunks)
  • src/main/resources/templates/admin/import.html (1 hunks)
💤 Files with no reviewable changes (1)
  • src/main/java/life/mosu/mosuserver/domain/examapplication/service/ExamNumberGenerationService.java
🧰 Additional context used
🧬 Code graph analysis (11)
src/main/java/life/mosu/mosuserver/domain/examapplication/projection/ExamTicketIssueProjection.java (1)
src/main/java/life/mosu/mosuserver/domain/examapplication/projection/ExamTicketInfoProjection.java (1)
  • ExamTicketInfoProjection (5-14)
src/main/java/life/mosu/mosuserver/application/admin/AdminApplicationImportService.java (1)
src/main/java/life/mosu/mosuserver/application/admin/util/CsvReader.java (1)
  • Slf4j (14-59)
src/main/java/life/mosu/mosuserver/application/examticket/processor/GetMemberExamTicketInfoProcessor.java (2)
src/main/java/life/mosu/mosuserver/application/examticket/processor/GenerateExamTicketProcessor.java (1)
  • Component (30-193)
src/main/java/life/mosu/mosuserver/application/examticket/processor/GetPartnerExamTicketInfoProcessor.java (1)
  • Component (23-72)
src/main/java/life/mosu/mosuserver/application/admin/processor/GetApplicationGuestRequestStepProcessor.java (1)
src/main/java/life/mosu/mosuserver/application/admin/processor/ApplyGuestStepProcessor.java (1)
  • Component (13-31)
src/main/java/life/mosu/mosuserver/infra/cron/job/ExamNumberGenerationJobRound2.java (2)
src/main/java/life/mosu/mosuserver/application/examapplication/cron/ExamNumberGeneratorExecutorImpl.java (1)
  • Slf4j (19-82)
src/main/java/life/mosu/mosuserver/infra/cron/job/ExamNumberGenerationJobRound1.java (1)
  • Slf4j (13-32)
src/main/java/life/mosu/mosuserver/application/admin/processor/ApplyGuestStepProcessor.java (1)
src/main/java/life/mosu/mosuserver/application/admin/processor/GetApplicationGuestRequestStepProcessor.java (1)
  • Component (16-49)
src/main/java/life/mosu/mosuserver/application/examticket/processor/GenerateExamTicketProcessor.java (2)
src/main/java/life/mosu/mosuserver/application/examticket/processor/GetMemberExamTicketInfoProcessor.java (1)
  • Component (19-54)
src/main/java/life/mosu/mosuserver/application/examticket/processor/GetPartnerExamTicketInfoProcessor.java (1)
  • Component (23-72)
src/main/java/life/mosu/mosuserver/application/examticket/processor/GetPartnerExamTicketInfoProcessor.java (2)
src/main/java/life/mosu/mosuserver/application/examticket/processor/GenerateExamTicketProcessor.java (1)
  • Component (30-193)
src/main/java/life/mosu/mosuserver/application/examticket/processor/GetMemberExamTicketInfoProcessor.java (1)
  • Component (19-54)
src/main/java/life/mosu/mosuserver/application/admin/util/CsvReader.java (2)
src/main/java/life/mosu/mosuserver/application/admin/AdminApplicationImportService.java (1)
  • Slf4j (19-66)
src/main/java/life/mosu/mosuserver/application/admin/processor/GetApplicationGuestRequestStepProcessor.java (1)
  • Component (16-49)
src/main/java/life/mosu/mosuserver/application/examapplication/cron/ExamNumberGeneratorExecutorImpl.java (2)
src/main/java/life/mosu/mosuserver/infra/cron/job/ExamNumberGenerationJobRound1.java (1)
  • Slf4j (13-32)
src/main/java/life/mosu/mosuserver/infra/cron/job/ExamNumberGenerationJobRound2.java (1)
  • Slf4j (13-33)
src/main/java/life/mosu/mosuserver/infra/cron/job/ExamNumberGenerationJobRound1.java (2)
src/main/java/life/mosu/mosuserver/application/examapplication/cron/ExamNumberGeneratorExecutorImpl.java (1)
  • Slf4j (19-82)
src/main/java/life/mosu/mosuserver/infra/cron/job/ExamNumberGenerationJobRound2.java (1)
  • Slf4j (13-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-and-deploy
  • GitHub Check: build-and-test
🔇 Additional comments (37)
src/main/java/life/mosu/mosuserver/infra/cron/job/ArchivingOrchestratorJob.java (1)

11-12: Import shuffle is fine.

No functional impact; import grouping still consistent.

src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java (1)

67-67: LGTM! Error code addition is well-structured.

The new EXAM_TICKET_INFO_NOT_FOUND error code is correctly configured with:

  • Appropriate HttpStatus.NOT_FOUND for a missing resource scenario
  • Clear Korean error message consistent with other codes
  • CriticalLevel.LOW suitable for a user-facing "not found" condition
  • Logical placement within the exam ticket errors section
src/main/java/life/mosu/mosuserver/infra/config/QuartzAutoRegisterConfig.java (1)

15-17: LGTM!

The explicit imports for java.util classes are clear and appropriate.

src/main/java/life/mosu/mosuserver/domain/application/entity/ApplicationJpaEntity.java (1)

3-3: Import consolidation applied.

The wildcard import consolidates multiple jakarta.persistence imports into a single statement with no functional impact.

src/main/java/life/mosu/mosuserver/application/admin/processor/ApplyGuestStepProcessor.java (1)

20-30: Transactions correctly configured: importGuestApplications is annotated @Transactional, covering both process() and its applyByGuest call, ensuring atomicity of the create-fetch-update sequence.

src/main/java/life/mosu/mosuserver/application/examapplication/ExamApplicationService.java (3)

26-30: LGTM!

The added imports are necessary and correctly support the time-related operations in validateExamTicketOpenDate and collection operations in getApplication.


44-47: Confirm cron-based exam number generation and edge-case coverage
The register() method no longer assigns exam numbers; ExamNumberGenerationJobRound1/2 invoke ExamNumberGeneratorExecutorImpl.generate(examDate) to grant numbers. Verify these jobs are scheduled at the correct times for each exam date and that any applications created after the initial run (e.g., late registrations) still receive exam numbers (via reruns or fallback logic).


44-47: Cron-based exam number generation is configured
ExamNumberGeneratorExecutorImpl is annotated @CronTarget and scheduled by ExamNumberGenerationJobRound1/ExamNumberGenerationJobRound2 via @cronjob, ensuring all registered applications receive exam numbers.

src/main/java/life/mosu/mosuserver/application/examapplication/code/ExamNumberCode.java (1)

38-54: LGTM with reservation!

The lookup methods correctly use Optional.ofNullable() and throw meaningful exceptions. The Korean error messages are appropriate for the target users.

However, be aware that IllegalArgumentException is an unchecked exception. Ensure callers handle these exceptions appropriately or consider using a custom checked exception for better error tracking.

src/main/java/life/mosu/mosuserver/presentation/examticket/dto/ExamTicketFileResponse.java (3)

1-8: LGTM!

The record design is clean and appropriate for carrying PDF file data. The three fields (bytes, filename, contentType) are sufficient for the exam ticket file response use case.

Note: Java records don't perform defensive copying of arrays by default. If the bytes array is mutable and shared, consider documenting that callers should not modify the array after passing it to this record, or implement a custom constructor with defensive copying if needed.


1-8: LGTM!

Clean and simple DTO record for exam ticket file responses. The structure is appropriate for returning PDF content with metadata.


3-8: LGTM! Clean DTO for exam ticket file response.

The record appropriately models the PDF response with byte content, filename, and content type. The structure is clear and suitable for returning generated exam ticket PDFs.

src/main/java/life/mosu/mosuserver/application/examapplication/cron/ExamNumberGeneratorExecutor.java (3)

1-10: LGTM!

The interface design is clean and follows single-responsibility principle:

  • Extends CronJobExecutor to integrate with the cron infrastructure.
  • Single method generate(LocalDate examDate) provides a clear contract for exam number generation.
  • Parameterizing by examDate allows different cron jobs to target specific exam rounds.

1-10: LGTM!

Clean interface design for exam number generation. The contract is simple and focused, extending CronJobExecutor appropriately for scheduled execution.


6-10: LGTM! Clean interface design for exam number generation.

The interface appropriately extends CronJobExecutor and defines a clear contract for exam-date-specific number generation. The LocalDate parameter allows flexible scheduling for different exam rounds.

src/main/java/life/mosu/mosuserver/global/filter/Whitelist.java (2)

8-10: LGTM!

The import additions are necessary for the enhanced whitelist logic and are correctly used in the static methods below.


56-56: LGTM! Guest exam ticket access is appropriately restricted.

The EXAM_TICKET_GUEST whitelist entry properly restricts access to GET requests only, which is appropriate for guest users retrieving their exam tickets.

src/main/java/life/mosu/mosuserver/domain/examapplication/entity/ExamApplicationJpaEntity.java (3)

37-51: Ensure isTestPaperChecked is handled consistently in the builder.

The new isTestPaperChecked field has a default value of Boolean.FALSE, but it's not included as a parameter in the builder (lines 40-46). This means entities created via the builder will have this field set to FALSE by default, which aligns with the field initialization. However, for clarity and consistency, consider whether this field should be explicitly included in the builder pattern or if the current approach is intentional.

If the field should remain excluded from the builder (because it's only set via setTestPaperChecked() after creation), consider adding a comment to document this design decision.


37-38: Field initialization pattern is correct for nullable boolean flag.

Using Boolean (boxed type) with explicit Boolean.FALSE initialization is appropriate here since:

  • The field represents an optional state that may not be set initially
  • The default value prevents NPE issues
  • JPA/Hibernate handles boxed Boolean types correctly for database mapping

Note: The builder (lines 40-46) doesn't include isTestPaperChecked as a parameter, so all new instances correctly default to FALSE.


70-72: One-way setter design is intentional but inflexible.

The setTestPaperChecked() method only allows setting the flag to TRUE with no way to reset it to FALSE. This is a valid design choice for an immutable state transition (e.g., once a test paper is checked, it stays checked), but it limits flexibility.

If there's ever a need to "un-check" a test paper (e.g., for corrections or admin overrides), this design will require modification. Consider documenting this one-way behavior in a comment or the method's Javadoc if it's a critical business rule.

src/main/java/life/mosu/mosuserver/application/admin/processor/ChangeTestPaperCheckedStepProcessor.java (4)

23-26: Verify that throwing an exception for empty results is the desired behavior.

The processor throws EXAM_APPLICATION_NOT_FOUND when no exam applications exist for the given applicationId. In the context of CSV import with transaction rollback, this ensures data consistency. However, consider whether this is the intended behavior if:

  • An application was created but no exam applications were associated yet
  • An application was deleted after CSV processing started

If these are valid edge cases, consider logging a warning and returning an empty list instead of throwing an exception.


28-32: LGTM! JPA dirty checking will persist the changes.

The use of forEach to mutate entities followed by collecting IDs is a clean pattern. The changes will be automatically persisted by JPA dirty checking within the transaction, so no explicit save call is needed.


23-26: Reconsider the exception for missing exam applications.

The code throws EXAM_APPLICATION_NOT_FOUND when no exam applications exist for the given applicationId. This might be overly strict depending on the business logic:

  • If it's valid for an application to exist without exam applications (e.g., incomplete signup), throwing an exception would prevent CSV import from completing.
  • If exam applications are mandatory, then this check is appropriate.

Please verify: Is it always an error when an applicationId has no associated exam applications during CSV import? If not, consider logging a warning instead of throwing an exception:

 List<ExamApplicationJpaEntity> examApplications = examApplicationJpaRepository.findByApplicationId(applicationId);
 if (examApplications.isEmpty()) {
-    throw new CustomRuntimeException(ErrorCode.EXAM_APPLICATION_NOT_FOUND);
+    log.warn("No exam applications found for applicationId: {}", applicationId);
+    return Collections.emptyList();
 }

28-32: JPA dirty checking will persist changes; code is correct.

The code mutates entities via setTestPaperChecked() without an explicit save call. This is correct within a @Transactional context (managed by the calling service), as JPA's dirty checking will automatically persist the changes when the transaction commits.

Optional: For Java 16+, you could simplify the stream using method reference and toList():

 return examApplications.stream()
         .map(ExamApplicationJpaEntity::getId)
-        .collect(Collectors.toList());
+        .toList();
src/main/java/life/mosu/mosuserver/domain/exam/entity/ExamJpaRepository.java (2)

66-66: LGTM! Returning List is appropriate for multi-result queries.

The method findAllByExamDate correctly returns a List<ExamJpaEntity>, which can be empty if no exams are found for the given date. This is the standard Spring Data JPA pattern for multi-result queries.


66-66: LGTM: findAllByExamDate signature is safe.

Returning List<ExamJpaEntity> is correct—Spring Data JPA will return an empty list (never null) if no records match.

src/main/java/life/mosu/mosuserver/application/examapplication/util/ExamNumberUtil.java (1)

8-22: LGTM! Clean utility method with proper validation.

The exam number formatting logic is well-implemented:

  • Validates all required parameters are non-null (lines 11-13)
  • Uses clear format string with appropriate digit padding (lines 15-21)
  • Throws descriptive exception for invalid input
src/main/java/life/mosu/mosuserver/application/admin/util/CsvReader.java (1)

28-29: LGTM! Proper encoding and CSV parsing.

The implementation correctly:

  • Uses StandardCharsets.UTF_8 for reading (line 29)
  • Uses split(CSV_DELIMITER, -1) to preserve trailing empty strings (line 44), which is essential for CSV with optional trailing columns

Also applies to: 44-44

src/main/java/life/mosu/mosuserver/application/examticket/processor/GenerateExamTicketProcessor.java (2)

51-56: LGTM: Resource initialization at startup.

Loading PDF template, font, and default profile at initialization is an effective pattern for reusing resources across invocations. The approach reduces I/O overhead on each request.


187-192: Verify character spacing logic for edge cases.

The formatName method applies different spacing for 2-character names versus others. Ensure this handles edge cases correctly:

  • Empty string returns "" (line 188 check)
  • Single-character names get 2-space spacing
  • Names with 3+ characters get 2-space spacing

If this formatting is based on specific PDF layout requirements, consider adding a comment explaining the spacing rationale.

src/main/java/life/mosu/mosuserver/domain/examapplication/repository/ExamApplicationJpaRepository.java (2)

226-245: LGTM: Well-structured query with proper joins.

The query retrieves exam ticket information with explicit LEFT JOINs and returns an Optional projection. The join structure is appropriate for the use case.


280-301: LGTM: Consistent query pattern with payment status filter.

The query follows the same pattern as the partner projection query, retrieving exam ticket information with appropriate joins and filtering by payment status.

src/main/java/life/mosu/mosuserver/infra/cron/job/ExamNumberGenerationJobRound2.java (1)

14-22: Incorrect mismatch alert: the cron schedule intentionally runs six days before the exam (matches Round1).

Cron for Round1 is 0 0 9 13 10 ? vs exam date 2025-10-19 (6 days before) and Round2 is 0 0 6 20 10 ? vs exam date 2025-10-26 (6 days before). No changes required.

Likely an incorrect or invalid review comment.

src/main/java/life/mosu/mosuserver/domain/virtualaccount/VirtualAccountLogJpaRepository.java (2)

11-12: LGTM!

Standard Spring Data JPA query method following naming conventions. The addition of depositStatus parameter allows filtering by both order ID and deposit status, which aligns with the admin import workflow requirements.


11-12: LGTM! Repository method follows Spring Data JPA conventions.

The findByOrderIdAndDepositStatus method appropriately extends the repository's query capabilities, allowing filtering by both order ID and deposit status. The method signature follows Spring Data JPA naming conventions and returns Optional for null-safety.

src/main/java/life/mosu/mosuserver/application/admin/processor/RegisterVirtualAccountStepProcessor.java (1)

17-25: Validate simulated deposit logic and null handling

  • Creating a “successful” deposit log without an actual transaction risks data integrity and audit discrepancies.
  • Guard against null or blank orderId from orderIdGenerator.generate().
  • Avoid passing multiple nulls into VirtualAccountLogJpaEntity.create(...) unless downstream consumers explicitly handle them.

Confirm whether marking deposits as successful here is intentional (e.g., for prepaid/guest CSV flows). If so, introduce a flag/status to distinguish simulated from real payments and document the context; otherwise, add proper validation and error handling.

src/main/java/life/mosu/mosuserver/presentation/application/dto/ExamApplicationResponse.java (1)

15-15: Complete or remove isTestPaperChecked in ExamApplicationResponse DTO.

  • In src/main/java/.../ExamApplicationResponse.java (lines 15, 27, 38), either uncomment the field in the record, constructor, and factory method—mapping it from ExamApplicationJpaEntity.isTestPaperChecked—or delete all commented references.
  • Confirm whether testPaperChecked is intended to be exposed in this response.

Comment on lines +37 to +46
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 일괄 처리 중 오류 발생");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not abort the entire import on the first row error.

The catch block rethrows a RuntimeException on the first failing row, so the loop stops immediately, the transaction rolls back, and the summary logic (including the failure count) is never reached. As a result, a single bad row prevents all other valid applications from being registered—defeating the purpose of batch import and the success/fail statistics you’re computing.

Handle the exception per row (log it, continue, and let the failure counter increase) instead of propagating it so that the rest of the CSV can still be processed. For example:

-            } catch (Exception e) {
-                log.error("게스트 신청 CSV 행 {} 처리 실패: {}", lineNo, e.getMessage(), e);
-                throw new RuntimeException("CSV 일괄 처리 중 오류 발생");
-            }
+            } catch (Exception e) {
+                log.error("게스트 신청 CSV 행 {} 처리 실패: {}", lineNo, e.getMessage(), e);
+            }

This keeps the loop running, allowing fail = total - success to reflect actual results while leaving successful rows committed.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 일괄 처리 중 오류 발생");
}
int lineNo = 1;
for (ApplicationCsvInfo row : rows) {
lineNo++;
try {
processGuestRow(row);
success++;
} catch (Exception e) {
log.error("게스트 신청 CSV 행 {} 처리 실패: {}", lineNo, e.getMessage(), e);
}
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/admin/AdminApplicationImportService.java
around lines 37 to 46, the catch currently rethrows a RuntimeException which
aborts the whole import and prevents the failure/success summary from being
produced; instead remove the rethrow, log the full exception, increment a
per-row failure counter (or keep track of failedRows), and continue the loop so
remaining rows are processed; if transactions currently wrap the entire import,
ensure each row is processed in a way that a single row failure does not roll
back successful rows (e.g., use a separate transactional boundary for
processGuestRow or catch and swallow exceptions per row), and preserve existing
success/fail summary logic so fail = total - success (or use an explicit fail++
when catching).

Comment on lines +54 to +60
private static LocalDate parseDate(String dateStr) {
try {
return LocalDate.parse(dateStr.trim());
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("날짜 형식이 올바르지 않습니다");
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Improve error message to include the invalid value.

The error message "날짜 형식이 올바르지 않습니다" (Date format is incorrect) doesn't indicate which value failed or what format is expected. This makes debugging difficult for users.

Apply this diff to improve the error message:

 private static LocalDate parseDate(String dateStr) {
     try {
         return LocalDate.parse(dateStr.trim());
     } catch (DateTimeParseException e) {
-        throw new IllegalArgumentException("날짜 형식이 올바르지 않습니다");
+        throw new IllegalArgumentException(
+            "날짜 형식이 올바르지 않습니다. 입력값: '" + dateStr + "', 예상 형식: yyyy-MM-dd", e);
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static LocalDate parseDate(String dateStr) {
try {
return LocalDate.parse(dateStr.trim());
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("날짜 형식이 올바르지 않습니다");
}
}
private static LocalDate parseDate(String dateStr) {
try {
return LocalDate.parse(dateStr.trim());
} catch (DateTimeParseException e) {
throw new IllegalArgumentException(
"날짜 형식이 올바르지 않습니다. 입력값: '" + dateStr + "', 예상 형식: yyyy-MM-dd", e);
}
}
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/admin/dto/ApplicationCsvInfo.java
around lines 54 to 60, the parseDate method throws a generic
IllegalArgumentException without showing the offending value or expected format;
update the catch block to include the trimmed input value and the expected
format (e.g., ISO yyyy-MM-dd) in the exception message (optionally appending the
original DateTimeParseException message) so callers know which value failed and
why.

Comment on lines +22 to +26
CreateApplicationResponse response = applicationService.applyByGuest(request);

Long applicationId = response.applicationId();
ApplicationJpaEntity application = applicationJpaRepository.findById(applicationId)
.orElseThrow(() -> new IllegalArgumentException("신청을 찾을 수 없습니다. id=" + applicationId));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Inefficient fetch immediately after creation.

The code fetches the application entity from the database immediately after creating it via applyByGuest(). This introduces unnecessary database round-trips and latency.

Consider one of the following approaches:

  1. Preferred: Modify applyByGuest() to return the created entity or allow setting the status during creation, eliminating the need for a separate fetch and update.

  2. Alternative: If the fetch-and-update pattern is required for architectural reasons (e.g., audit logging, event triggering in changeStatus()), add a comment explaining why the fetch is necessary.

For approach 1, you would refactor the service method to accept the status as a parameter or return the entity directly:

ApplicationJpaEntity application = applicationService.applyByGuestAndReturn(request, ApplicationStatus.APPROVED);
return application.getId();

This reduces database operations and improves performance for bulk imports.

🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/admin/processor/ApplyGuestStepProcessor.java
around lines 22 to 26, the code does an unnecessary DB fetch right after calling
applicationService.applyByGuest(), causing extra round-trips; either change
applyByGuest() to return the created ApplicationJpaEntity (or accept a desired
status so it can be set on creation) and use that returned entity directly, or,
if fetch is required, add a concise comment explaining why the separate
findById() is necessary (e.g., for audit/event reasons) so future readers
understand the intentional extra query.

ApplicationJpaEntity application = applicationJpaRepository.findById(applicationId)
.orElseThrow(() -> new IllegalArgumentException("신청을 찾을 수 없습니다. id=" + applicationId));

application.changeStatus(ApplicationStatus.APPROVED);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Status change is not persisted to the database.

Line 28 calls changeStatus() on the entity, but the modified entity is never saved back to the repository. The status change will be lost when the transaction commits (or the persistence context is cleared).

Apply this diff to persist the status change:

 application.changeStatus(ApplicationStatus.APPROVED);
+applicationJpaRepository.save(application);
 return applicationId;

Alternatively, if changeStatus() is designed to work within a managed transaction, ensure this method is annotated with @Transactional at the class or method level.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
application.changeStatus(ApplicationStatus.APPROVED);
application.changeStatus(ApplicationStatus.APPROVED);
applicationJpaRepository.save(application);
return applicationId;
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/admin/processor/ApplyGuestStepProcessor.java
around line 28, the code calls
application.changeStatus(ApplicationStatus.APPROVED) but does not persist the
updated entity; either call the appropriate repository.save(application) (or
repository.saveAndFlush(application) if immediate DB write is required) after
the change to persist the new status, or ensure the surrounding method/class is
annotated with @Transactional so the persistence context will automatically
flush the status change before commit.

Comment on lines +28 to +33
ExamJpaEntity exam = examJpaRepository.findByExamDateAndSchoolName(examDate, schoolName);

ExamApplicationRequest examApplicationRequest = new ExamApplicationRequest(
exam.getId(),
csvInfo.isLunchChecked()
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against missing exam lookups.

findByExamDateAndSchoolName returns null when no exam matches the CSV row. We immediately dereference exam.getId(), so the CSV import blows up with an NPE instead of a controlled failure. Please fail fast with a domain exception (e.g., CustomRuntimeException(ErrorCode.EXAM_NOT_FOUND) or similar) before building the request. A minimal fix is to wrap the lookup in Optional.ofNullable(...).orElseThrow(...).

+import life.mosu.mosuserver.global.exception.CustomRuntimeException;
+import life.mosu.mosuserver.global.exception.ErrorCode;
+import java.util.Optional;
@@
-        ExamJpaEntity exam = examJpaRepository.findByExamDateAndSchoolName(examDate, schoolName);
+        ExamJpaEntity exam = Optional.ofNullable(
+                examJpaRepository.findByExamDateAndSchoolName(examDate, schoolName))
+                .orElseThrow(() -> new CustomRuntimeException(ErrorCode.EXAM_NOT_FOUND));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ExamJpaEntity exam = examJpaRepository.findByExamDateAndSchoolName(examDate, schoolName);
ExamApplicationRequest examApplicationRequest = new ExamApplicationRequest(
exam.getId(),
csvInfo.isLunchChecked()
);
// at the top of the file, alongside the other imports
import life.mosu.mosuserver.global.exception.CustomRuntimeException;
import life.mosu.mosuserver.global.exception.ErrorCode;
import java.util.Optional;
// around lines 28–33 in GetApplicationGuestRequestStepProcessor.java
ExamJpaEntity exam = Optional.ofNullable(
examJpaRepository.findByExamDateAndSchoolName(examDate, schoolName))
.orElseThrow(() -> new CustomRuntimeException(ErrorCode.EXAM_NOT_FOUND));
ExamApplicationRequest examApplicationRequest = new ExamApplicationRequest(
exam.getId(),
csvInfo.isLunchChecked()
);
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/admin/processor/GetApplicationGuestRequestStepProcessor.java
around lines 28 to 33, the code dereferences exam returned from
examJpaRepository.findByExamDateAndSchoolName(...) which can be null; wrap the
lookup with Optional.ofNullable(...).orElseThrow(...) and throw a domain
exception such as new CustomRuntimeException(ErrorCode.EXAM_NOT_FOUND) (or the
project’s equivalent) so the import fails fast with a controlled error before
constructing ExamApplicationRequest.

LocalDate examDate,
Set<String> subjects,
String lunchName
// Boolean isTestPaperChecked
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove or uncomment isTestPaperChecked field.

The isTestPaperChecked field is commented out in three locations. Given that:

  1. Migration V4__add_test_paper_checked.sql adds the test_paper_checked column to the database.
  2. The field appears to be part of the feature implementation.

Either:

  • Uncomment the field if it's part of this PR's functionality and wire it through from the entity.
  • Remove the comments entirely if the feature is deferred to a future PR.

Leaving commented code creates maintenance confusion and suggests incomplete implementation.

If the field should be active, apply this diff:

         LocalDate examDate,
         Set<String> subjects,
-        String lunchName
-//        Boolean isTestPaperChecked
+        String lunchName,
+        Boolean isTestPaperChecked
 ) {

     public static ExamApplicationResponse of(
             ...
             LocalDate examDate,
             Set<String> subjects,
-            String lunchName
-//            Boolean isTestPaperChecked
+            String lunchName,
+            Boolean isTestPaperChecked
     ) {
         return new ExamApplicationResponse(
                 ...
                 subjects,
-                lunchName
-//                isTestPaperChecked
+                lunchName,
+                isTestPaperChecked
         );
     }

Also applies to: 27-27, 38-38

🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/presentation/application/dto/ExamApplicationResponse.java
around lines 15, 27 and 38, the isTestPaperChecked field is left commented out;
either remove the commented lines entirely if this feature is deferred, or if it
should be active in this PR, uncomment the field declarations and accessors and
map it from the domain/entity (add a getter/setter or include in constructor and
set from the entity's getTestPaperChecked() value), update
equals/hashCode/toString if applicable, and ensure any JSON serialization
annotations are present so the DTO reflects the new test_paper_checked column.

🛠️ Refactor suggestion | 🟠 Major

Remove commented code.

The commented isTestPaperChecked references should be removed rather than left in the codebase. Commented code creates confusion and technical debt.

Options:

  1. If the feature is incomplete: Remove the comments entirely and track the work in an issue
  2. If ready to implement: Uncomment and ensure the field is properly populated from the entity

Apply this diff to remove the comments:

     Set<String> subjects,
     String lunchName
-//        Boolean isTestPaperChecked
 ) {

     public static ExamApplicationResponse of(
             String lunchName
-//            Boolean isTestPaperChecked
     ) {
         return new ExamApplicationResponse(
                 lunchName
-//                isTestPaperChecked
         );

Also applies to: 27-27, 38-38

🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/presentation/application/dto/ExamApplicationResponse.java
around lines 15, 27 and 38, remove the commented-out Boolean isTestPaperChecked
lines (do not leave commented code in the file). If the feature is incomplete,
delete those comment lines and open a ticket to track adding the field later; if
the feature should exist, instead uncomment the field and ensure it is declared,
mapped from the entity, and used consistently across
constructors/getters/setters.

Comment on lines +1 to +2
ALTER TABLE exam_application
ADD COLUMN test_paper_checked BIT(1) NULL DEFAULT b'0'; No newline at end of file
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Column should be NOT NULL with default value.

The test_paper_checked column is defined as NULL but has a DEFAULT b'0'. Since every row should have a boolean value (true/false), the column should be NOT NULL.

Apply this diff:

 ALTER TABLE exam_application
-    ADD COLUMN test_paper_checked BIT(1) NULL DEFAULT b'0';
+    ADD COLUMN test_paper_checked BIT(1) NOT NULL DEFAULT b'0';

Note: BIT(1) is MySQL/MariaDB-specific. If the project needs to support other databases, consider using BOOLEAN or TINYINT(1) for better portability.

🤖 Prompt for AI Agents
In src/main/resources/db/migration/V4__add_test_paper_checked.sql around lines 1
to 2, the new column is created as NULL with a default of b'0'; change the
column to NOT NULL with the same default so every row always has a boolean
value. Update the ALTER TABLE statement to declare test_paper_checked as NOT
NULL DEFAULT b'0' (or switch to a more portable type like BOOLEAN/TINYINT(1) if
cross-DB compatibility is required).

Comment on lines +62 to +72
/*.failure-message {*/
/* color: #ef4444; !* Red *!*/
/* font-weight: bold;*/
/* font-size: 18px;*/
/* margin-top: 20px;*/
/* padding: 10px;*/
/* border: 1px solid #fee2e2;*/
/* background-color: #fef2f2;*/
/* border-radius: 8px;*/
/* text-align: center;*/
/*}*/
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Uncomment the CSS for failure-message or use an alternative class.

The .failure-message class is used at line 141 to style the failure banner, but the corresponding CSS is commented out at lines 62-72. This will result in unstyled text when import failures occur.

Apply this fix:

-        /*.failure-message {*/
-        /*    color: #ef4444; !* Red *!*/
-        /*    font-weight: bold;*/
-        /*    font-size: 18px;*/
-        /*    margin-top: 20px;*/
-        /*    padding: 10px;*/
-        /*    border: 1px solid #fee2e2;*/
-        /*    background-color: #fef2f2;*/
-        /*    border-radius: 8px;*/
-        /*    text-align: center;*/
-        /*}*/
+        .failure-message {
+            color: #ef4444; /* Red */
+            font-weight: bold;
+            font-size: 18px;
+            margin-top: 20px;
+            padding: 10px;
+            border: 1px solid #fee2e2;
+            background-color: #fef2f2;
+            border-radius: 8px;
+            text-align: center;
+        }

Also applies to: 141-143

🤖 Prompt for AI Agents
In src/main/resources/templates/admin/import-result.html around lines 62-72 (and
also relevant to usage at lines 141-143), the .failure-message CSS block is
commented out so the failure banner at lines 141-143 will be unstyled; restore
or reintroduce the CSS by uncommenting the .failure-message block (or moving
equivalent styles into an existing stylesheet) so it defines color, font-weight,
font-size, margin, padding, border, background-color, border-radius and
text-align for the failure banner; ensure the restored rules are valid CSS
(remove comment markers and any template-comment artifacts) and reload the
template reference so the element at 141-143 receives the expected styles.

⚠️ Potential issue | 🟡 Minor

Remove commented-out CSS or document its purpose.

The .failure-message CSS block is commented out but the class is referenced on line 141. This creates an inconsistency where the failure banner will be unstyled.

If this styling is not needed yet, remove the reference on line 141. If it's planned for future use, add a TODO comment explaining when/why it should be enabled.

Apply this diff to remove the dead code:

-        /*.failure-message {*/
-        /*    color: #ef4444; !* Red *!*/
-        /*    font-weight: bold;*/
-        /*    font-size: 18px;*/
-        /*    margin-top: 20px;*/
-        /*    padding: 10px;*/
-        /*    border: 1px solid #fee2e2;*/
-        /*    background-color: #fef2f2;*/
-        /*    border-radius: 8px;*/
-        /*    text-align: center;*/
-        /*}*/

Or uncomment and complete the failure message styling if it's needed.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/*.failure-message {*/
/* color: #ef4444; !* Red *!*/
/* font-weight: bold;*/
/* font-size: 18px;*/
/* margin-top: 20px;*/
/* padding: 10px;*/
/* border: 1px solid #fee2e2;*/
/* background-color: #fef2f2;*/
/* border-radius: 8px;*/
/* text-align: center;*/
/*}*/
🤖 Prompt for AI Agents
In src/main/resources/templates/admin/import-result.html around lines 62 to 72
(the commented .failure-message CSS) and note the class is referenced at line
141, the file contains dead/commented-out styling which leaves the failure
banner unstyled; either remove the HTML reference to .failure-message at line
141 if the style is not required, or restore the CSS by uncommenting and
finishing the block so the class is styled, or keep the CSS commented but add a
clear TODO comment above the commented block describing when/why it should be
enabled; make whichever change keeps code consistent (no unused class
references) and update commit message accordingly.

</div>

<div th:if="${result.totalFailure == 0}" class="success-message">
✅ 데이터 오류가 발견되지 않아 **총 <span th:text="${result.totalSuccess}">100</span>건의 데이터가 모두 성공적으로 반영되었습니다.**
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix incorrect bold text syntax in HTML.

Lines 137 and 142 use **text** markdown syntax inside HTML, which won't render as bold. Use proper HTML tags instead.

Apply this diff:

-        ✅ 데이터 오류가 발견되지 않아 **총 <span th:text="${result.totalSuccess}">100</span>건의 데이터가 모두 성공적으로 반영되었습니다.**
+        ✅ 데이터 오류가 발견되지 않아 <strong>총 <span th:text="${result.totalSuccess}">100</span>건의 데이터가 모두 성공적으로 반영되었습니다.</strong>
-            ❌ 데이터 오류가 발견되어 **전체 (<span th:text="${result.totalProcessed}">100</span>건) 작업이 롤백되었습니다.**
+            ❌ 데이터 오류가 발견되어 <strong>전체 (<span th:text="${result.totalProcessed}">100</span>건) 작업이 롤백되었습니다.</strong>

Also applies to: 142-142

🤖 Prompt for AI Agents
In src/main/resources/templates/admin/import-result.html around lines 137 and
142, the text uses markdown-style bold (**...**) which will not render in HTML;
replace the **bolded** markdown with proper HTML tags (e.g., wrap the intended
bold portions in <strong>...</strong> or <b>...</b>), preserving the existing
<span th:text="${result.totalSuccess}">100</span> interpolation and any
surrounding text/spacing.

Comment on lines +136 to +144
<form th:action="@{/admin/applications/import/guests}" method="post" enctype="multipart/form-data">
<label for="guest-file">
CSV 파일 선택 (.csv):
</label>
<input type="file" id="guest-file" name="file" accept=".csv" required />

<button type="submit" class="btn-guest">
비회원 데이터 업로드 시작
</button>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add CSRF hidden field to enable POST

Spring Security’s default CSRF protection will reject this multipart POST unless the _csrf token is submitted, so the admin upload will fail. Please include the hidden token field.

         <form th:action="@{/admin/applications/import/guests}" method="post" enctype="multipart/form-data">
+            <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
             <label for="guest-file">
🤖 Prompt for AI Agents
In src/main/resources/templates/admin/import.html around lines 136 to 144, the
multipart POST form is missing the Spring Security CSRF token field so the
upload will be rejected; add a hidden input inside the form that binds to the
CSRF parameter and token (using Thymeleaf expressions: set the input's name to
${_csrf.parameterName} and its value to ${_csrf.token}) so the _csrf token is
submitted with the POST.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 10f9d2f and 7b9a961.

📒 Files selected for processing (1)
  • src/main/java/life/mosu/mosuserver/application/examapplication/cron/ExamNumberGeneratorExecutorImpl.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/life/mosu/mosuserver/application/examapplication/cron/ExamNumberGeneratorExecutorImpl.java (2)
src/main/java/life/mosu/mosuserver/infra/cron/job/ExamNumberGenerationJobRound1.java (1)
  • Slf4j (13-32)
src/main/java/life/mosu/mosuserver/infra/cron/job/ExamNumberGenerationJobRound2.java (1)
  • Slf4j (13-33)

Comment on lines +78 to +89
if (!gapInserted && Boolean.FALSE.equals(examApplication.getIsTestPaperChecked())) {
personalCode += GAP_BETWEEN_CHECKED_AND_UNCHECKED;
gapInserted = true;
}

personalCode++;

String examNumber = ExamNumberUtil.formatExamNumber(
roundCode, areaCode, schoolCode, personalCode);

examApplication.grantExamNumber(examNumber);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix gap handling when no checked papers exist.

If every application has isTestPaperChecked == FALSE, the very first iteration hits this branch, adds the gap, and assigns personal code 26 to the first candidate. That wastes numbers 1–25 even though there are no “checked” applications ahead of them. Guard the gap insertion with personalCode > 0 (or an equivalent flag) so it only triggers after at least one checked application has been numbered.

-                if (!gapInserted && Boolean.FALSE.equals(examApplication.getIsTestPaperChecked())) {
+                if (!gapInserted
+                        && Boolean.FALSE.equals(examApplication.getIsTestPaperChecked())
+                        && personalCode > 0) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!gapInserted && Boolean.FALSE.equals(examApplication.getIsTestPaperChecked())) {
personalCode += GAP_BETWEEN_CHECKED_AND_UNCHECKED;
gapInserted = true;
}
personalCode++;
String examNumber = ExamNumberUtil.formatExamNumber(
roundCode, areaCode, schoolCode, personalCode);
examApplication.grantExamNumber(examNumber);
}
if (!gapInserted
&& Boolean.FALSE.equals(examApplication.getIsTestPaperChecked())
&& personalCode > 0) {
personalCode += GAP_BETWEEN_CHECKED_AND_UNCHECKED;
gapInserted = true;
}
personalCode++;
String examNumber = ExamNumberUtil.formatExamNumber(
roundCode, areaCode, schoolCode, personalCode);
examApplication.grantExamNumber(examNumber);
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/examapplication/cron/ExamNumberGeneratorExecutorImpl.java
around lines 78 to 89, the gap insertion branch currently runs even when no
checked papers have been numbered, causing the first candidate to receive
personalCode 26; update the condition so the gap is only inserted after at least
one personal code has been assigned (e.g., change the if to require personalCode
> 0 in addition to !gapInserted and isTestPaperChecked == FALSE, or
alternatively set gapInserted only when you assign the first checked
application) so numbering 1–25 are not skipped when no checked applications
precede them.

@chominju02 chominju02 merged commit d7a85bd into develop Oct 12, 2025
3 checks passed
@chominju02 chominju02 deleted the feat/ticket branch October 12, 2025 17:35
@coderabbitai coderabbitai bot mentioned this pull request Oct 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants