Skip to content

Conversation

@chominju02
Copy link
Contributor

@chominju02 chominju02 commented Jul 18, 2025

✨ 구현한 기능

  • multi-insert 를 위한 BulkRepository 구현
  • 중복 신청 확인 기능 구현
  • 신청 학교 조건에 맞는 학교 존재 여부 확인 기능 구현

📢 논의하고 싶은 내용

🎸 기타

Summary by CodeRabbit

  • New Features

    • Introduced exam-centric application flow with exam applications, subjects, and lunch selections.
    • Added endpoints and services for managing exams and lunches, including area-based queries.
    • Added DTOs and response formats for exam applications, exam tickets, and lunch data.
    • Added batch insertion support for exam applications and subjects.
  • Improvements

    • Enhanced error handling with new error codes for exam and lunch-related operations.
    • Simplified file attachment handling and standardized pre-signed URL generation without expiration parameters.
    • Replaced school quota caching with exam quota caching for improved capacity management.
  • Removals

    • Removed all school-based application models, services, controllers, DTOs, and repositories.
    • Deprecated legacy admin, school, and application school modules and related API documentation.
    • Removed database initialization and legacy projections related to schools and applications.
  • Bug Fixes

    • Strengthened duplicate application validation by exam IDs.
    • Added explicit error handling for missing applications, exams, lunches, and subjects.
  • Chores

    • Added H2 database dependency for runtime environment support.

@coderabbitai
Copy link

coderabbitai bot commented Jul 18, 2025

Walkthrough

This change refactors the application domain from a school-centric model to an exam-centric one. It removes all school-related services, repositories, entities, and DTOs, introducing new exam, exam application, and lunch domain structures. Controllers, services, and error handling were updated to support the new exam-based logic, with batch operations and detailed error codes added.

Changes

Files/Groups Change Summary
src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java Refactored: School-based application logic replaced with exam-centric model, new validation, batch saves, error handling.
src/main/java/life/mosu/mosuserver/application/exam/ExamService.java, ExamApplicationService.java, LunchService.java, LunchEventService.java Added: Exam, exam application, subject, and lunch services, entities, repositories, DTOs, and controllers.
src/main/java/life/mosu/mosuserver/domain/exam/*, examapplication/*, lunch/* Added: New domain entities and repositories for exams, exam applications, exam subjects, exam tickets, and lunches.
src/main/java/life/mosu/mosuserver/domain/application/ApplicationJpaRepository.java Added: Method for existence check by user and exam IDs.
src/main/java/life/mosu/mosuserver/domain/application/ExamTicketImageJpaEntity.java Renamed: AdmissionTicketImage to ExamTicketImage entity, updated table/column names.
src/main/java/life/mosu/mosuserver/domain/application/ExamTicketImageJpaRepository.java Added: Repository for ExamTicketImage entity.
src/main/java/life/mosu/mosuserver/domain/school/*, applicationschool/* Removed: All school-related entities, repositories, projections, DTOs, and services.
src/main/java/life/mosu/mosuserver/presentation/application/dto/* Refactored: Application DTOs to use exam-centric fields, added new exam DTOs, removed school DTOs.
src/main/java/life/mosu/mosuserver/presentation/exam/*, examapplication/*, lunch/* Added: Controllers and DTOs for new exam, exam application, and lunch domains.
src/main/java/life/mosu/mosuserver/application/notify/NotifyVariableFactory.java Refactored: Notification logic to use exam-based projections and repositories, removed application school notifications.
src/main/java/life/mosu/mosuserver/infra/storage/application/S3Service.java Changed: Pre-signed URL expiration now uses S3Properties internally, method signature simplified.
src/main/java/life/mosu/mosuserver/application/*AttachmentService.java Simplified: File attachment creation and pre-signed URL logic, removed explicit expiration handling.
src/main/java/life/mosu/mosuserver/global/runner/ApplicationSchoolPreWarmRunner.java Changed: Uses ExamQuotaCacheManager instead of SchoolQuotaCacheManager.
src/main/java/life/mosu/mosuserver/application/exam/ExamQuotaCacheManager.java Renamed and refactored: SchoolQuotaCacheManager → ExamQuotaCacheManager, adapted caching to exam domain.
src/main/java/life/mosu/mosuserver/presentation/payment/dto/PreparePaymentRequest.java Simplified: Removed item list, kept only applicationId.
src/main/java/life/mosu/mosuserver/presentation/admin/*, applicationSchool/*, school/*, refund/*, DatabaseInitializer.java Commented out: Admin controller/service, application school controller/service, school controller/service, refund query, and database initializer.
build.gradle Added: H2 database as a runtime-only dependency.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ApplicationController
    participant ApplicationService
    participant ExamApplicationService
    participant ExamJpaRepository
    participant ExamApplicationJpaRepository
    participant ExamSubjectJpaRepository
    participant LunchJpaRepository

    User->>ApplicationController: Submit ApplicationRequest
    ApplicationController->>ApplicationService: apply(userId, request)
    ApplicationService->>ExamJpaRepository: Validate exam IDs (duplicates)
    ApplicationService->>ExamApplicationService: Register exam applications
    ExamApplicationService->>ExamApplicationJpaRepository: Save exam applications
    ExamApplicationService->>ExamSubjectJpaRepository: Save exam subjects
    ApplicationService->>LunchJpaRepository: (if needed) Save lunch info
    ApplicationService-->>ApplicationController: Return ApplicationResponse
    ApplicationController-->>User: ApplicationResponse
Loading

Possibly related PRs

Suggested labels

우선순위 중

Suggested reviewers

  • wlgns12370
  • jbh010204

Poem

In the garden of code, where the exams now bloom,
The schools have retired, swept away with a broom.
Subjects and lunches, new tickets in tow,
Rabbits hop swiftly where old logic did grow.
With batch saves and checks, and a fresh data stream,
The exam-centric future is more than a dream!
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@chominju02 chominju02 self-assigned this Jul 18, 2025
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.

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!

This pull request significantly refactors the ApplicationService to improve the efficiency and robustness of the application submission process. The core changes involve introducing bulk data insertion capabilities for application schools and their subjects, enhancing validation logic to prevent duplicate applications, and streamlining the process of verifying school existence based on application criteria.

Highlights

  • Bulk Data Insertion: Implemented a new BulkRepository to perform efficient multi-inserts for ApplicationSchoolJpaEntity and associated ExamSubject entities, replacing individual save operations. This significantly improves performance for applications involving multiple schools.
  • Enhanced Duplicate Application Prevention: Added comprehensive validation checks within the apply method to prevent users from submitting duplicate applications for the same school on the same exam date and to catch identical school requests within a single application payload.
  • Streamlined School Validation: Refactored the school lookup process to fetch all requested school entities in a single query using SchoolQueryRepositoryImpl, improving performance and ensuring all specified schools exist before proceeding with the application.
  • ApplicationService Refactoring: The apply method in ApplicationService has been largely rewritten to leverage the new bulk operations and validation logic, resulting in a more concise, efficient, and robust application submission flow.
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 in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

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 issue 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 is currently in preview and 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 to provide feedback.

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

@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: 3

🧹 Nitpick comments (1)
src/main/java/life/mosu/mosuserver/infra/respository/BulkRepository.java (1)

58-59: Ensure proper enum handling for database storage

Using String.valueOf() for enum conversion might not be the most robust approach. Consider using the enum's name() method or a dedicated converter.

-                    ps.setString(12, String.valueOf(e.getArea()));
-                    ps.setString(13, String.valueOf(e.getLunch()));
+                    ps.setString(12, e.getArea().name());
+                    ps.setString(13, e.getLunch().name());
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a007cb6 and 0445ece.

📒 Files selected for processing (7)
  • src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolJpaRepository.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/school/SchoolQueryRepository.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/school/SchoolQueryRepositoryImpl.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/infra/respository/BulkRepository.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/application/dto/ApplicationRequest.java (2 hunks)
🔇 Additional comments (11)
src/main/java/life/mosu/mosuserver/domain/school/SchoolQueryRepository.java (1)

9-11: LGTM! Returning full entities supports bulk processing

The change to return SchoolJpaEntity objects instead of just IDs is appropriate for the bulk processing approach, allowing the service layer to access full school data without additional queries.

src/main/java/life/mosu/mosuserver/domain/school/SchoolQueryRepositoryImpl.java (1)

18-34: LGTM! Implementation correctly updated to return full entities

The implementation properly aligns with the interface change. The use of selectFrom to fetch complete entities is appropriate for the bulk processing requirements.

src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolJpaRepository.java (1)

41-49: LGTM! Well-designed bulk query methods

The new methods effectively support bulk operations:

  • existsByUserIdAndSchoolIdIn enables efficient duplicate checking across multiple schools
  • findAllWithSubjectsByIdIn properly uses LEFT JOIN FETCH to avoid N+1 queries when loading subjects
src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java (2)

46-46: LGTM! Error code properly supports duplicate validation.

The new APPLICATION_SCHOOL_DUPLICATED error code appropriately uses BAD_REQUEST status for client-side validation failures when duplicate schools are detected within a single request.


94-95: LGTM! Multi-insert error codes properly categorized.

The new error codes for bulk insert operations are correctly categorized as INTERNAL_SERVER_ERROR since they represent server-side failures during database operations.

src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java (6)

3-9: LGTM! Import additions support the bulk operations refactoring.

The new imports are appropriately added to support the bulk processing logic:

  • Triple for efficient condition grouping
  • Map and Function for school entity mapping
  • HashSet for duplicate detection
  • LocalDate for date handling
  • BulkRepository for batch operations

Also applies to: 16-16, 22-22, 28-28, 42-42


51-60: LGTM! Admission ticket logic simplified and more robust.

The refactored admission ticket logic is cleaner and more defensive by checking both filename and S3 key existence before saving. The direct conversion from FileRequest to entity is more straightforward than the previous helper method approach.


62-74: LGTM! Effective duplicate detection within request.

The duplicate detection logic is well-implemented:

  • Uses Triple to create unique condition keys combining school name, area, and exam date
  • Converts to HashSet to detect duplicates by size comparison
  • Throws appropriate error code for duplicate validation

76-85: LGTM! Bulk school existence validation is efficient.

The school existence validation logic is well-designed:

  • Uses bulk query to fetch all schools at once instead of individual queries
  • Validates that all requested schools exist by count comparison
  • Efficiently checks for prior applications using bulk existence check

87-104: LGTM! Efficient mapping strategy for bulk operations.

The school mapping logic is well-implemented:

  • Creates a map using the same Triple key structure for O(1) lookups
  • Efficiently converts requests to entities using the pre-fetched school data
  • Maintains consistency between condition checking and entity creation

106-112: Transactional handling in bulk operations has been validated

  • BulkRepository is annotated with @Transactional (propagation=REQUIRED by default), so it joins the service’s transaction
  • ApplicationService.apply is itself @Transactional, ensuring the entire save-and-fetch sequence is atomic
  • On any exception, the Spring transaction manager will roll back both the JDBC batch and subsequent JPA fetch

No changes are required here.

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import java.util.Set;
import java.util.List;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify duplicate school handling after changing from Set to List

The change from Set to List allows duplicate school entries in the request. Ensure that duplicate school validation is properly handled in the service layer to prevent users from applying to the same school multiple times within a single request.

Also applies to: 22-22


🏁 Script executed:

#!/bin/bash
# Description: Check if duplicate school validation is implemented in the service layer

# Search for duplicate validation logic in ApplicationService
rg -A 10 "ApplicationService" | rg -A 10 "duplicate|distinct|unique" 

# Look for the ApplicationService implementation
ast-grep --pattern 'class ApplicationService {
  $$$
}'

Length of output: 126


Here’s a deeper inspection to locate the service class and any duplicate-school handling logic:

This will confirm whether there’s an ApplicationService (or similarly named) class and if any de-duplication checks exist.


🏁 Script executed:

#!/bin/bash
# Attempt 2: locate and inspect ApplicationService and duplicate logic

echo "1) Finding ApplicationService.java file…"
fd ApplicationService.java

echo "2) Looking for 'class ApplicationService' declarations…"
rg -n "class .*ApplicationService" -C2 src/main/java

echo "3) Listing all *Service.java files…"
fd -e java | grep -i "Service.java"

echo "4) Searching for 'duplicate' and 'distinct' keywords in service layer…"
rg -n "duplicate" -C5 src/main/java
rg -n "distinct" -C5 src/main/java

echo "5) Searching for 'schools' references in service layer…"
rg -n "schools" -C5 src/main/java

Length of output: 23682


Add explicit duplicate‐school validation in ApplicationService.apply
The service currently collects request.schools() into a map—relying on Collectors.toMap to throw on duplicate keys—but offers no clear error or documentation. Introduce a preflight check to reject duplicate entries with a proper BadRequestException.

Locations to update:

  • src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java, inside the apply(Long userId, ApplicationRequest request) method (around line 46–50)

Suggested snippet:

List<ApplicationSchoolRequest> schoolRequests = request.schools();
// reject duplicates early with a clear exception
if (schoolRequests.stream().map(ApplicationSchoolRequest::schoolName).distinct().count()
        < schoolRequests.size()) {
    throw new BadRequestException("Duplicate school entries are not allowed in a single application.");
}

// existing save and mapping logic…
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java
around lines 46 to 50, add an explicit preflight check in the apply(Long userId,
ApplicationRequest request) method to detect duplicate school entries before
processing. Retrieve the list of schools from the request, then compare the
count of distinct school names to the total list size. If duplicates exist,
throw a BadRequestException with a clear message indicating that duplicate
school entries are not allowed. This replaces relying on Collectors.toMap to
throw and provides clearer error handling.

Comment on lines 40 to 97
public List<Long> saveAllApplicationSchoolsWithSubjects(
List<ApplicationSchoolJpaEntity> entities) {

List<Long> generatedIds = jdbcTemplate.execute((ConnectionCallback<List<Long>>) con -> {
try (PreparedStatement ps = con.prepareStatement(
SQL_INSERT_APPLICATION_SCHOOL, Statement.RETURN_GENERATED_KEYS)) {
for (ApplicationSchoolJpaEntity e : entities) {
ps.setDate(1, Date.valueOf(e.getExamDate()));
ps.setLong(2, e.getApplicationId());
ps.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now()));
ps.setLong(4, e.getSchoolId());
ps.setTimestamp(5, Timestamp.valueOf(LocalDateTime.now()));
ps.setLong(6, e.getUserId());
ps.setString(7, e.getSchoolName());
ps.setString(8, e.getAddress().getDetail());
ps.setString(9, e.getExaminationNumber());
ps.setString(10, e.getAddress().getStreet());
ps.setString(11, e.getAddress().getZipcode());
ps.setString(12, String.valueOf(e.getArea()));
ps.setString(13, String.valueOf(e.getLunch()));
ps.addBatch();
}

ps.executeBatch();

List<Long> ids = new ArrayList<>();
try (ResultSet rs = ps.getGeneratedKeys()) {
while (rs.next()) {
ids.add(rs.getLong(1));
}
}
return ids;

} catch (RuntimeException e) {
throw new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_MULTI_INSERT_ERROR);
}
});

List<Object[]> subjectParams = new ArrayList<>();

for (int i = 0; i < entities.size(); i++) {
Long applicationSchoolId = generatedIds.get(i);

for (Subject subj : entities.get(i).getSubjects()) {
subjectParams.add(new Object[]{applicationSchoolId, String.valueOf(subj)});
}
}

try {
if (!subjectParams.isEmpty()) {
jdbcTemplate.batchUpdate(SQL_INSERT_SUBJECT, subjectParams);
}
} catch (RuntimeException e) {
throw new CustomRuntimeException(ErrorCode.EXAM_SUBJECT_MULTI_INSERT_ERROR);
}

return generatedIds;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding batch size limits for large datasets

The current implementation processes all entities in a single batch without size limits. For large datasets, this could cause memory issues or exceed database limits.

Consider processing in chunks:

private static final int BATCH_SIZE = 1000;

public List<Long> saveAllApplicationSchoolsWithSubjects(List<ApplicationSchoolJpaEntity> entities) {
    List<Long> allGeneratedIds = new ArrayList<>();
    
    for (int i = 0; i < entities.size(); i += BATCH_SIZE) {
        List<ApplicationSchoolJpaEntity> batch = entities.subList(i, 
            Math.min(i + BATCH_SIZE, entities.size()));
        List<Long> batchIds = processBatch(batch);
        allGeneratedIds.addAll(batchIds);
    }
    
    return allGeneratedIds;
}
🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/infra/respository/BulkRepository.java
around lines 40 to 97, the current batch insert processes all entities at once,
which can cause memory or database issues with large datasets. Refactor the
method to split the input list into smaller batches (e.g., 1000 items each) and
process each batch separately, collecting and returning all generated IDs after
all batches are processed. Implement a helper method to handle each batch's
insert logic and call it iteratively for each chunk.

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

The code changes refactor the application submission logic for better performance using bulk database operations. The changes replace N+1 query patterns with batch processing. The review includes suggestions for the new BulkRepository to improve exception handling and data consistency.

Comment on lines 43 to 76
List<Long> generatedIds = jdbcTemplate.execute((ConnectionCallback<List<Long>>) con -> {
try (PreparedStatement ps = con.prepareStatement(
SQL_INSERT_APPLICATION_SCHOOL, Statement.RETURN_GENERATED_KEYS)) {
for (ApplicationSchoolJpaEntity e : entities) {
ps.setDate(1, Date.valueOf(e.getExamDate()));
ps.setLong(2, e.getApplicationId());
ps.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now()));
ps.setLong(4, e.getSchoolId());
ps.setTimestamp(5, Timestamp.valueOf(LocalDateTime.now()));
ps.setLong(6, e.getUserId());
ps.setString(7, e.getSchoolName());
ps.setString(8, e.getAddress().getDetail());
ps.setString(9, e.getExaminationNumber());
ps.setString(10, e.getAddress().getStreet());
ps.setString(11, e.getAddress().getZipcode());
ps.setString(12, String.valueOf(e.getArea()));
ps.setString(13, String.valueOf(e.getLunch()));
ps.addBatch();
}

ps.executeBatch();

List<Long> ids = new ArrayList<>();
try (ResultSet rs = ps.getGeneratedKeys()) {
while (rs.next()) {
ids.add(rs.getLong(1));
}
}
return ids;

} catch (RuntimeException e) {
throw new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_MULTI_INSERT_ERROR);
}
});

Choose a reason for hiding this comment

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

high

The try-catch block catches RuntimeException, but methods on PreparedStatement (like executeBatch()) throw SQLException. Catch SQLException to handle database-related errors during the batch insert correctly. Also, use a single timestamp for all records created in the same batch for data consistency.

        List<Long> generatedIds = jdbcTemplate.execute((ConnectionCallback<List<Long>>) con -> {
            final Timestamp now = Timestamp.valueOf(LocalDateTime.now());
            try (PreparedStatement ps = con.prepareStatement(
                    SQL_INSERT_APPLICATION_SCHOOL, Statement.RETURN_GENERATED_KEYS)) {
                for (ApplicationSchoolJpaEntity e : entities) {
                    ps.setDate(1, Date.valueOf(e.getExamDate()));
                    ps.setLong(2, e.getApplicationId());
                    ps.setTimestamp(3, now);
                    ps.setLong(4, e.getSchoolId());
                    ps.setTimestamp(5, now);
                    ps.setLong(6, e.getUserId());
                    ps.setString(7, e.getSchoolName());
                    ps.setString(8, e.getAddress().getDetail());
                    ps.setString(9, e.getExaminationNumber());
                    ps.setString(10, e.getAddress().getStreet());
                    ps.setString(11, e.getAddress().getZipcode());
                    ps.setString(12, String.valueOf(e.getArea()));
                    ps.setString(13, String.valueOf(e.getLunch()));
                    ps.addBatch();
                }

                ps.executeBatch();

                List<Long> ids = new ArrayList<>();
                try (ResultSet rs = ps.getGeneratedKeys()) {
                    while (rs.next()) {
                        ids.add(rs.getLong(1));
                    }
                }
                return ids;

            } catch (java.sql.SQLException e) {
                throw new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_MULTI_INSERT_ERROR);
            }
        });

Comment on lines 92 to 94
} catch (RuntimeException e) {
throw new CustomRuntimeException(ErrorCode.EXAM_SUBJECT_MULTI_INSERT_ERROR);
}

Choose a reason for hiding this comment

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

medium

Catch the more specific DataAccessException instead of RuntimeException for clarity.

Suggested change
} catch (RuntimeException e) {
throw new CustomRuntimeException(ErrorCode.EXAM_SUBJECT_MULTI_INSERT_ERROR);
}
} catch (org.springframework.dao.DataAccessException e) {
throw new CustomRuntimeException(ErrorCode.EXAM_SUBJECT_MULTI_INSERT_ERROR);
}

@chominju02 chominju02 changed the title MOSU-103 refacor: ApplicationService 리팩토링 MOSU-103 refactor: ApplicationService 리팩토링 Jul 18, 2025
…dmission ticket images and application schools

- Deleted `AdmissionTicketImageJpaEntity` and its repository as they are no longer needed.
- Removed `ApplicationSchoolJpaEntity`, `ApplicationSchoolJpaRepository`, and related projections and DTOs.
- Eliminated `AddressJpaVO`, `Area`, `LunchMenu`, and `SchoolJpaEntity` along with their repositories and controllers.
- Cleaned up associated request and response DTOs for application school and school management.
- This cleanup reduces code complexity and improves maintainability by removing obsolete components.
…commented-out from method in ApplicationNotifyRequest
…ify application existence check in PaymentService
…icketResponse, ExamApplicationRequest, and ExamResponse
…ionJpaEntity and update related DTOs and methods
…main and remove unused AreaDetail and AvailableSchoolResponse classes
… management, including ExamTicketImage, Exam, ExamApplication, and ExamSubject
…ent, including CRUD operations and exam association
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: 19

🔭 Outside diff range comments (7)
src/main/java/life/mosu/mosuserver/application/payment/PaymentService.java (3)

86-94: Critical: Application existence validation is completely disabled

The hardcoded true value bypasses all application existence validation, creating a significant data integrity and security risk. Payments could be processed for non-existent applications.

Either implement proper validation using the new exam application repositories or remove this validation entirely if it's no longer needed in the new domain model.

 private void checkApplicationsExist(List<Long> applicationIds) {
-//        boolean existsAll = applicationSchoolJpaRepository.existsAllByIds(applicationIds,
-//                applicationIds.size());
-        boolean existsAll = true;
+        // TODO: Implement validation using ExamApplicationJpaRepository
+        // boolean existsAll = examApplicationRepository.existsAllByIds(applicationIds, applicationIds.size());
+        boolean existsAll = true; // TEMPORARY - REMOVE BEFORE PRODUCTION
     if (!existsAll) {
         log.warn("Application IDs not found: {}", applicationIds);
         throw new RuntimeException("존재하지 않는 신청입니다.");
     }
 }

53-53: Update variable naming to reflect new domain model

The variable applicationSchoolIds should be renamed to reflect the new exam-based domain model, such as examApplicationIds or applicationIds.


86-94: Critical: Application existence validation is completely bypassed.

The hardcoded true value disables all application existence validation, allowing payments to be processed for non-existent applications. This creates a serious data integrity and security vulnerability.

The validation should be properly refactored to use the new exam-based repositories instead of being disabled:

 private void checkApplicationsExist(List<Long> applicationIds) {
-//        boolean existsAll = applicationSchoolJpaRepository.existsAllByIds(applicationIds,
-//                applicationIds.size());
-        boolean existsAll = true;
+        // TODO: Implement with new exam application repository
+        boolean existsAll = examApplicationRepository.existsAllByIds(applicationIds, applicationIds.size());
     if (!existsAll) {
         log.warn("Application IDs not found: {}", applicationIds);
         throw new RuntimeException("존재하지 않는 신청입니다.");
     }
 }
src/main/java/life/mosu/mosuserver/global/initializer/DatabaseInitializer.java (1)

1-381: Remove the entire commented file instead of keeping it.

The entire DatabaseInitializer class is commented out and relates to the deprecated school-centric domain model. Keeping large blocks of commented code creates maintenance burden and confusion.

Consider either:

  1. Delete this file entirely if database initialization is no longer needed
  2. Create a new initializer for the exam-centric domain model if seeding is still required

Leaving 380+ lines of commented code is not a good practice and should be avoided.

src/main/java/life/mosu/mosuserver/infra/storage/application/S3Service.java (1)

42-43: Remove unused preSignedUrlExpirationMinutes from S3Service

The @Value("${aws.s3.presigned-url-expiration-minutes}") private int preSignedUrlExpirationMinutes; field in S3Service is never referenced—expiration is already provided by s3Properties.getPresignedUrlExpirationMinutes(). To eliminate duplication, remove these two lines:

File: src/main/java/life/mosu/mosuserver/infra/storage/application/S3Service.java

-    @Value("${aws.s3.presigned-url-expiration-minutes}")
-    private int preSignedUrlExpirationMinutes;
src/main/java/life/mosu/mosuserver/application/notify/NotifyVariableFactory.java (1)

22-34: Critical: Potential NullPointerException from null returns.

The create method's switch statement still routes to methods that now return null. This will cause NPE when the returned NotificationVariable is used.

Consider either:

  1. Throwing an exception to indicate these notification types are temporarily unsupported
  2. Implementing the new exam-based notification logic
 private NotificationVariable createRefundVariable(Long targetId) {
-        return null;
+        throw new UnsupportedOperationException("Refund notifications are temporarily disabled during migration");
 }
src/main/java/life/mosu/mosuserver/presentation/application/dto/ApplicationSchoolRequest.java (1)

1-48: Remove this commented-out file.

Since the entire file is commented out as part of the school-to-exam model migration, it should be deleted rather than kept as dead code. This improves codebase cleanliness and reduces confusion.

#!/bin/bash
# Description: Check if this file is still referenced anywhere in the codebase

# Search for any references to ApplicationSchoolRequest
rg "ApplicationSchoolRequest" --type java
🧹 Nitpick comments (17)
src/main/java/life/mosu/mosuserver/application/lunch/LunchEventService.java (1)

18-22: Consider adding validation for the lunch creation event.

While the current implementation is clean, consider adding validation to ensure the CreateLunchEvent contains valid data before converting to entities.

 @Transactional
 public void create(CreateLunchEvent request) {
+    if (request == null || request.toEntities().isEmpty()) {
+        throw new IllegalArgumentException("CreateLunchEvent cannot be null or empty");
+    }
     List<LunchJpaEntity> lunch = request.toEntities();
     lunchJpaRepository.saveAll(lunch);
 }
src/main/java/life/mosu/mosuserver/domain/application/ApplicationJpaRepository.java (1)

12-18: JPQL query implementation looks correct for duplicate checking.

The query properly joins the entities and uses appropriate parameter binding. The CASE WHEN pattern for boolean return is standard JPQL practice.

Consider adding an index on the join columns if this query will be frequently executed with large exam ID lists for better performance.

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

7-7: Consider using @service instead of @component.

Since this class provides business logic for exam number generation, @Service would be more semantically appropriate than @Component.

-@Component
+@Service
src/main/java/life/mosu/mosuserver/application/lunch/LunchService.java (2)

16-19: Consider adding @transactional(readOnly = true) for read operations.

Adding read-only transaction annotation can provide performance benefits and clearer intent.

+@Transactional(readOnly = true)
 public List<LunchResponse> getByExamId(Long examId) {
     List<LunchJpaEntity> foundLunch = lunchJpaRepository.findByExamId(examId);
     return LunchResponse.fromList(foundLunch);
 }

17-17: Variable naming could be more precise.

The variable name foundLunch suggests a single entity but contains a list. Consider foundLunches or lunchEntities for clarity.

-List<LunchJpaEntity> foundLunch = lunchJpaRepository.findByExamId(examId);
-return LunchResponse.fromList(foundLunch);
+List<LunchJpaEntity> lunchEntities = lunchJpaRepository.findByExamId(examId);
+return LunchResponse.fromList(lunchEntities);
src/main/java/life/mosu/mosuserver/application/examapplication/ExamApplicationService.java (1)

20-23: Consider adding error handling for batch operations.

While the current implementation is correct, consider adding validation or error handling for edge cases:

  • Empty exam application lists
  • Failures during exam number generation
  • Database constraint violations
 @Transactional
 public List<ExamApplicationJpaEntity> register(RegisterExamApplicationEvent event) {
+    if (event.targetExams().isEmpty()) {
+        return List.of();
+    }
+    
     List<ExamApplicationJpaEntity> examApplicationEntities = event.toEntity();
     examNumberGenerationService.grantTo(examApplicationEntities);
     return examApplicationJpaRepository.saveAll(examApplicationEntities);
 }
src/main/java/life/mosu/mosuserver/domain/examapplication/ExamApplicationJpaEntity.java (1)

18-54: Well-structured entity with some considerations for improvement.

The entity follows good JPA practices with proper annotations and builder pattern. However, consider these improvements:

  1. Input validation: Add validation for non-null required fields in the builder constructor
  2. Exam number assignment: Consider adding validation in grantExamNumber to prevent overwriting existing exam numbers
  3. Static factory method: The create method duplicates the builder pattern functionality - consider removing it for simplicity
 @Builder
 public ExamApplicationJpaEntity(Long applicationId, Long examId, Long lunchId) {
+    if (applicationId == null || examId == null) {
+        throw new IllegalArgumentException("ApplicationId and ExamId cannot be null");
+    }
     this.applicationId = applicationId;
     this.examId = examId;
     this.lunchId = lunchId;
 }

 public void grantExamNumber(String examNumber) {
+    if (this.examNumber != null) {
+        throw new IllegalStateException("Exam number already assigned");
+    }
     this.examNumber = examNumber;
 }
src/main/java/life/mosu/mosuserver/presentation/lunch/LunchController.java (1)

29-31: TODO comment indicates incomplete implementation.

The TODO comment suggests that CRUD operations for lunch management are planned but not yet implemented.

Would you like me to help generate the implementation for lunch CRUD operations or create an issue to track this task?

src/main/java/life/mosu/mosuserver/presentation/exam/dto/ExamRequest.java (1)

12-20: Consider adding validation annotations.

The record fields lack validation annotations which could help catch invalid data early in the request processing pipeline.

 public record ExamRequest(
+        @NotBlank
         String schoolName,
+        @NotBlank
         String areaName,
+        @NotNull @Valid
         AddressRequest address,
+        @NotNull @Future
         LocalDate examDate,
+        @NotNull @Min(1)
         Integer capacity,
+        @NotNull @Future
         LocalDateTime deadlineTime,
+        @NotNull @Valid
         List<ExamLunchRequest> lunch
 ) {
src/main/java/life/mosu/mosuserver/presentation/lunch/dto/CreateLunchEvent.java (1)

15-19: Consider adding null safety checks.

While the current implementation is clean, adding null checks could improve robustness, especially for the stream operations.

 public List<LunchJpaEntity> toEntities() {
+    if (lunchRequests == null) {
+        return List.of();
+    }
     return lunchRequests.stream()
             .map(request -> request.toEntity(examId))
             .toList();
 }
src/main/java/life/mosu/mosuserver/application/notify/dto/ApplicationNotifyRequest.java (1)

19-26: Remove commented code instead of leaving it in the codebase.

The commented factory method creates code clutter and should be completely removed since the school-centric model is being eliminated. Leaving commented code can cause confusion for future developers.

-//    public static ApplicationNotifyRequest from(
-//            ApplicationSchoolNotifyProjection applicationSchool) {
-
-    /// /        return new ApplicationNotifyRequest( /
-    /// applicationSchool.paymentKey(), /                applicationSchool.examDate(), /
-    ///    applicationSchool.schoolName(), /                applicationSchool.lunch() != null ?
-    /// applicationSchool.lunch().getLunchName() : "" /        );
-//    }
src/main/java/life/mosu/mosuserver/application/notify/NotifyVariableFactory.java (1)

47-89: Track the migration of notification logic.

These methods are commented out as part of the school-to-exam model migration. The implementations need to be updated to use the new exam-based entities.

Would you like me to create an issue to track the implementation of exam-based notification logic for these notification types?

src/main/java/life/mosu/mosuserver/application/exam/ExamService.java (1)

16-41: Consider adding service-level documentation.

As this is a new core service in the exam-centric model, adding Javadoc would help other developers understand its purpose and usage.

+/**
+ * Service for managing exam-related operations.
+ * Handles exam registration, retrieval, and triggers lunch creation events.
+ */
 @Service
 @RequiredArgsConstructor
 public class ExamService {
src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java (2)

48-56: Optimize duplicate check for better performance.

The current implementation fetches all exam IDs upfront and then checks for duplicates. Consider using a more efficient approach that stops at the first duplicate found.

-// 중복 신청 검증
-List<Long> examIds = request.examApplication().stream()
-        .map(examApp -> examApp.examId())
-        .toList();
-
-boolean isDuplicate = applicationJpaRepository.existsByUserIdAndExamIds(userId, examIds);
-if (isDuplicate) {
-    throw new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_DUPLICATED);
-}
+// 중복 신청 검증
+Set<Long> examIds = request.examApplication().stream()
+        .map(examApp -> examApp.examId())
+        .collect(Collectors.toSet());
+
+boolean isDuplicate = applicationJpaRepository.existsByUserIdAndExamIds(userId, examIds);
+if (isDuplicate) {
+    throw new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_DUPLICATED);
+}

Also consider if the repository method can be optimized to use EXISTS SQL clause for better performance.


58-86: Consider transaction boundary optimization.

The current implementation performs multiple repository saves sequentially, which could benefit from better transaction management or batch operations for improved performance.

Consider grouping related operations or using the commented-out ExamApplicationBulkRepository for batch inserts:

// Alternative approach using batch operations
@Transactional
public ApplicationResponse apply(Long userId, ApplicationRequest request) {
    // ... duplicate check logic ...
    
    ApplicationJpaEntity application = request.toApplicationJpaEntity(userId);
    ApplicationJpaEntity savedApplication = applicationJpaRepository.save(application);
    Long applicationId = savedApplication.getId();

    // Use batch repository if available
    List<ExamApplicationJpaEntity> examApplicationEntities = examApplicationService.register(
            RegisterExamApplicationEvent.of(request.examApplication(), applicationId)
    );
    
    // Consider batching these operations
    List<ExamApplicationJpaEntity> savedExamApplications = examApplicationJpaRepository.saveAll(examApplicationEntities);
    
    List<ExamSubjectJpaEntity> allExamSubjects = createExamSubjects(savedExamApplications, request.validatedSubjects());
    examSubjectJpaRepository.saveAll(allExamSubjects);
    
    // ... rest of the method
}
src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java (1)

46-46: Update error message to reflect exam-centric model.

The error message mentions "같은 학교" (same school) but this error code is now used for exam duplicate validation. Consider updating the message to be more accurate.

-APPLICATION_SCHOOL_DUPLICATED(HttpStatus.BAD_REQUEST, "동일 일자의 같은 학교를 신청할 수 없습니다."),
+APPLICATION_SCHOOL_DUPLICATED(HttpStatus.BAD_REQUEST, "동일한 시험을 중복으로 신청할 수 없습니다."),

Or consider renaming the error code itself to better reflect the exam-centric model:

-APPLICATION_SCHOOL_DUPLICATED(HttpStatus.BAD_REQUEST, "동일 일자의 같은 학교를 신청할 수 없습니다."),
+EXAM_APPLICATION_DUPLICATED(HttpStatus.BAD_REQUEST, "동일한 시험을 중복으로 신청할 수 없습니다."),
src/main/java/life/mosu/mosuserver/application/payment/PaymentService.java (1)

32-32: Remove commented-out code.

Commented-out code should be removed rather than left in the codebase. Use version control to track removed dependencies.

-    //    private final ApplicationSchoolJpaRepository applicationSchoolJpaRepository;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 483491e and e49fc92.

📒 Files selected for processing (90)
  • src/main/java/life/mosu/mosuserver/application/admin/AdminService.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/application/application/SchoolQuotaCacheManager.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/applicationschool/ApplicationSchoolService.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/application/event/EventAttachmentService.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/exam/ExamService.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/examapplication/ExamApplicationService.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/examapplication/dto/RegisterExamApplicationEvent.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/faq/FaqAttachmentService.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerAttachmentService.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAttachmentService.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/application/lunch/LunchEventService.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/lunch/LunchService.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/notice/NoticeAttachmentService.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/application/notify/NotifyVariableFactory.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/notify/dto/ApplicationNotifyRequest.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/payment/PaymentService.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/application/school/SchoolQuotaCacheManager.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/application/school/SchoolService.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/domain/admin/ApplicationQueryRepositoryImpl.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/admin/RefundQueryRepositoryImpl.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/application/AdmissionTicketImageJpaRepository.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/domain/application/ApplicationJpaEntity.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/domain/application/ApplicationJpaRepository.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/application/ExamTicketImageJpaEntity.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/domain/application/ExamTicketImageJpaRepository.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolJpaEntity.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolJpaRepository.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolNotifyProjection.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/domain/applicationschool/OneWeekNotifyProjection.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/domain/exam/AddressJpaVO.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/domain/exam/Area.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/exam/ExamJpaEntity.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/exam/ExamJpaRepository.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/examapplication/ExamApplicationJpaEntity.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/examapplication/ExamApplicationJpaRepository.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/examapplication/ExamNumberGenerationService.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/examapplication/ExamSubjectJpaEntity.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/examapplication/ExamSubjectJpaRepository.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/lunch/LunchJpaEntity.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/lunch/LunchJpaRepository.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/refund/RefundJpaRepository.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/school/LunchMenu.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/domain/school/SchoolApplicationProjection.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/domain/school/SchoolJpaEntity.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/domain/school/SchoolJpaRepository.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/domain/school/SchoolQueryRepository.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/domain/school/SchoolQueryRepositoryImpl.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/global/initializer/DatabaseInitializer.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/global/runner/ApplicationSchoolPreWarmRunner.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/global/util/FileRequest.java (4 hunks)
  • src/main/java/life/mosu/mosuserver/infra/respository/BulkRepository.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/storage/application/S3Service.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/admin/AdminController.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/admin/dto/ApplicationListResponse.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/admin/dto/ExamTicketResponse.java (3 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/application/ApplicationController.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/application/dto/ApplicationRequest.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/application/dto/ApplicationResponse.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/application/dto/ApplicationSchoolRequest.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/application/dto/ApplicationSchoolResponse.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/application/dto/ExamApplicationRequest.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/application/dto/ExamApplicationResponse.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/application/dto/ExamWithSubjects.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/applicationschool/ApplicationSchoolController.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/applicationschool/ApplicationSchoolControllerDocs.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/applicationschool/dto/RefundRequest.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/applicationschool/dto/SubjectUpdateRequest.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/common/AddressResponse.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/exam/ExamController.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/exam/dto/AddressRequest.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/exam/dto/ExamLunchRequest.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/exam/dto/ExamRequest.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/exam/dto/ExamResponse.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/lunch/LunchController.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/lunch/LunchEventListener.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/lunch/dto/CreateLunchEvent.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/lunch/dto/LunchResponse.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/school/SchoolController.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/school/SchoolControllerDocs.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/school/dto/AreaDetail.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/school/dto/AreaMeta.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/school/dto/AvailableSchoolResponse.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/school/dto/ExamInfo.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/school/dto/LunchInfo.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/school/dto/LunchMenuRequest.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/school/dto/SchoolEditRequest.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/school/dto/SchoolRegistrationRequest.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/school/dto/SchoolResponse.java (0 hunks)
💤 Files with no reviewable changes (30)
  • src/main/java/life/mosu/mosuserver/presentation/school/dto/LunchInfo.java
  • src/main/java/life/mosu/mosuserver/domain/school/SchoolApplicationProjection.java
  • src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolNotifyProjection.java
  • src/main/java/life/mosu/mosuserver/domain/school/SchoolQueryRepository.java
  • src/main/java/life/mosu/mosuserver/domain/school/LunchMenu.java
  • src/main/java/life/mosu/mosuserver/presentation/applicationschool/dto/SubjectUpdateRequest.java
  • src/main/java/life/mosu/mosuserver/presentation/school/dto/ExamInfo.java
  • src/main/java/life/mosu/mosuserver/presentation/school/dto/AvailableSchoolResponse.java
  • src/main/java/life/mosu/mosuserver/presentation/school/SchoolControllerDocs.java
  • src/main/java/life/mosu/mosuserver/presentation/school/dto/AreaMeta.java
  • src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolJpaEntity.java
  • src/main/java/life/mosu/mosuserver/domain/application/AdmissionTicketImageJpaRepository.java
  • src/main/java/life/mosu/mosuserver/domain/school/SchoolQueryRepositoryImpl.java
  • src/main/java/life/mosu/mosuserver/presentation/school/dto/AreaDetail.java
  • src/main/java/life/mosu/mosuserver/domain/school/SchoolJpaRepository.java
  • src/main/java/life/mosu/mosuserver/domain/school/SchoolJpaEntity.java
  • src/main/java/life/mosu/mosuserver/presentation/applicationschool/ApplicationSchoolController.java
  • src/main/java/life/mosu/mosuserver/domain/applicationschool/OneWeekNotifyProjection.java
  • src/main/java/life/mosu/mosuserver/application/school/SchoolQuotaCacheManager.java
  • src/main/java/life/mosu/mosuserver/presentation/applicationschool/ApplicationSchoolControllerDocs.java
  • src/main/java/life/mosu/mosuserver/presentation/school/dto/SchoolRegistrationRequest.java
  • src/main/java/life/mosu/mosuserver/presentation/school/SchoolController.java
  • src/main/java/life/mosu/mosuserver/presentation/school/dto/LunchMenuRequest.java
  • src/main/java/life/mosu/mosuserver/application/applicationschool/ApplicationSchoolService.java
  • src/main/java/life/mosu/mosuserver/presentation/school/dto/SchoolEditRequest.java
  • src/main/java/life/mosu/mosuserver/presentation/application/dto/ApplicationSchoolResponse.java
  • src/main/java/life/mosu/mosuserver/presentation/school/dto/SchoolResponse.java
  • src/main/java/life/mosu/mosuserver/presentation/applicationschool/dto/RefundRequest.java
  • src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolJpaRepository.java
  • src/main/java/life/mosu/mosuserver/application/school/SchoolService.java
✅ Files skipped from review due to trivial changes (22)
  • src/main/java/life/mosu/mosuserver/domain/exam/Area.java
  • src/main/java/life/mosu/mosuserver/presentation/common/AddressResponse.java
  • src/main/java/life/mosu/mosuserver/presentation/application/ApplicationController.java
  • src/main/java/life/mosu/mosuserver/application/admin/AdminService.java
  • src/main/java/life/mosu/mosuserver/presentation/exam/dto/AddressRequest.java
  • src/main/java/life/mosu/mosuserver/domain/application/ApplicationJpaEntity.java
  • src/main/java/life/mosu/mosuserver/domain/exam/AddressJpaVO.java
  • src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerAttachmentService.java
  • src/main/java/life/mosu/mosuserver/presentation/admin/AdminController.java
  • src/main/java/life/mosu/mosuserver/global/runner/ApplicationSchoolPreWarmRunner.java
  • src/main/java/life/mosu/mosuserver/presentation/application/dto/ExamApplicationRequest.java
  • src/main/java/life/mosu/mosuserver/presentation/exam/dto/ExamLunchRequest.java
  • src/main/java/life/mosu/mosuserver/presentation/lunch/LunchEventListener.java
  • src/main/java/life/mosu/mosuserver/domain/examapplication/ExamSubjectJpaEntity.java
  • src/main/java/life/mosu/mosuserver/presentation/lunch/dto/LunchResponse.java
  • src/main/java/life/mosu/mosuserver/presentation/admin/dto/ExamTicketResponse.java
  • src/main/java/life/mosu/mosuserver/presentation/application/dto/ExamApplicationResponse.java
  • src/main/java/life/mosu/mosuserver/domain/exam/ExamJpaRepository.java
  • src/main/java/life/mosu/mosuserver/presentation/application/dto/ExamWithSubjects.java
  • src/main/java/life/mosu/mosuserver/domain/exam/ExamJpaEntity.java
  • src/main/java/life/mosu/mosuserver/domain/lunch/LunchJpaEntity.java
  • src/main/java/life/mosu/mosuserver/presentation/exam/dto/ExamResponse.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/life/mosu/mosuserver/presentation/application/dto/ApplicationRequest.java
🔇 Additional comments (30)
src/main/java/life/mosu/mosuserver/application/application/SchoolQuotaCacheManager.java (1)

1-70: Remove commented-out code entirely.

The entire SchoolQuotaCacheManager class is commented out, indicating it's no longer needed in the new exam-centric architecture. Commented-out code should be removed rather than left in the codebase as it creates maintenance overhead and confusion.

The AI summary describes this as "newly introduced" functionality, but the actual code shows the entire class is commented out, suggesting it's being deprecated as part of the architectural refactoring.

-//package life.mosu.mosuserver.application.school;
-//
-//import java.util.List;
-//import lombok.RequiredArgsConstructor;
-//import org.springframework.data.redis.core.RedisTemplate;
-//import org.springframework.stereotype.Service;
-//
-//@Service
-//@RequiredArgsConstructor
-//public class SchoolQuotaCacheManager {
-//
-//    private static final String REDIS_KEY_SCHOOL_MAX_CAPACITY = "school:max_capacity:";
-//    private static final String REDIS_KEY_SCHOOL_CURRENT_APPLICATIONS = "school:current_applications:";
-//
-//    private final RedisTemplate<String, Long> redisTemplate;
-//    private final SchoolJpaRepository schoolRepository;
-//
-//    public void cacheSchoolMaxCapacities() {
-//        List<SchoolJpaEntity> schools = schoolRepository.findAll();
-//        for (SchoolJpaEntity school : schools) {
-//            addSchoolMaxCapacity(school.getSchoolName(), school.getCapacity());
-//        }
-//    }
-//
-//    public void cacheSchoolCurrentApplicationCounts() {
-//        List<SchoolApplicationProjection> schoolApplications = schoolRepository.countBySchoolNameGroupBy();
-//        for (SchoolApplicationProjection projection : schoolApplications) {
-//            addSchoolCurrentApplicationCount(projection.schoolName(), projection.count());
-//        }
-//    }
-//
-//    public void addSchoolMaxCapacity(String schoolName, Long capacity) {
-//        String key = REDIS_KEY_SCHOOL_MAX_CAPACITY + schoolName;
-//        redisTemplate.opsForValue().set(key, capacity);
-//    }
-//
-//    public void addSchoolCurrentApplicationCount(String schoolName, Long currentCount) {
-//        String key = REDIS_KEY_SCHOOL_CURRENT_APPLICATIONS + schoolName;
-//        redisTemplate.opsForValue().set(key, currentCount);
-//    }
-//
-//
-//    public Long getSchoolApplicationCounts(String schoolName) {
-//        return redisTemplate.opsForValue()
-//                .get(REDIS_KEY_SCHOOL_CURRENT_APPLICATIONS + schoolName);
-//    }
-//
-//    public Long getSchoolCapacities(String schoolName) {
-//        return redisTemplate.opsForValue()
-//                .get(REDIS_KEY_SCHOOL_MAX_CAPACITY + schoolName);
-//    }
-//
-//    public void increaseApplicationCount(String schoolName) {
-//        String key = REDIS_KEY_SCHOOL_CURRENT_APPLICATIONS + schoolName;
-//        redisTemplate.opsForValue().increment(key);
-//    }
-//
-//    public void decreaseApplicationCount(String schoolName) {
-//        String key = REDIS_KEY_SCHOOL_CURRENT_APPLICATIONS + schoolName;
-//        Long currentValue = redisTemplate.opsForValue().get(key);
-//        if (currentValue != null && currentValue > 0) {
-//            redisTemplate.opsForValue().decrement(key);
-//        }
-//    }
-//
-//    public void preloadSchoolData() {
-//        cacheSchoolMaxCapacities();
-//        cacheSchoolCurrentApplicationCounts();
-//    }
-//}

This aligns with the PR objective of refactoring from a school-centric to exam-centric application model.

Likely an incorrect or invalid review comment.

src/main/java/life/mosu/mosuserver/domain/lunch/LunchJpaRepository.java (1)

1-10: LGTM! Clean repository interface following Spring Data JPA conventions.

The implementation correctly follows Spring Data JPA patterns with proper method naming that will automatically generate the appropriate query for finding lunch entities by exam ID.

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

1-11: LGTM! Properly structured repository interface.

The repository correctly implements Spring Data JPA patterns. The findByApplicationId method appropriately returns a List, indicating the one-to-many relationship between applications and exam applications in the new domain model.

src/main/java/life/mosu/mosuserver/application/lunch/LunchEventService.java (1)

12-23: LGTM! Well-structured event-driven service.

The implementation correctly follows Spring patterns with proper transaction management and efficient batch saving using saveAll().

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

1-10: LGTM! Properly designed repository interface.

The repository follows Spring Data JPA conventions correctly. The findByExamApplicationId method appropriately returns a List, supporting the one-to-many relationship between exam applications and their subjects in the new domain model.

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

17-19: Verify if UUID format meets exam number requirements.

UUIDs are globally unique but may not be user-friendly for exam numbers. Consider if exam numbers need to be:

  • Sequential or human-readable
  • Shorter format
  • Following specific patterns

If current format is acceptable, the implementation is correct.

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

19-24: Transaction handling and batch operations look correct.

The @transactional annotation ensures atomicity for the batch registration process. The combination of exam number generation and saveAll operation is efficient for bulk processing.

src/main/java/life/mosu/mosuserver/application/examapplication/dto/RegisterExamApplicationEvent.java (2)

12-20: Factory method implementation is clean and functional.

The stream-based transformation from ExamApplicationRequest to TargetExam is efficient and follows good functional programming practices.


22-30: Entity conversion method is well-structured.

The toEntity method properly maps the event data to JPA entities using the stream API. The entity creation pattern follows good domain modeling practices.

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

7-23: Clean refactoring from school-centric to exam-centric model.

The transition from schools to exams field aligns well with the architectural shift described in the PR objectives. The dual factory methods provide good flexibility for different use cases.

src/main/java/life/mosu/mosuserver/presentation/lunch/dto/CreateLunchEvent.java (1)

6-34: Well-structured event DTO with good separation of concerns.

The nested record structure and conversion methods are clean and follow good practices. The examId parameter passing to the nested entity creation is handled correctly.

src/main/java/life/mosu/mosuserver/domain/application/ExamTicketImageJpaEntity.java (5)

3-8: LGTM: Clean import refactoring

Good practice replacing wildcard imports with explicit Jakarta Persistence imports for better clarity and maintainability.


19-26: LGTM: Consistent entity and table naming

The table name and primary key column have been correctly updated to align with the exam-centric domain model. The naming convention is consistent throughout.


3-8: Good practice: Explicit imports over wildcard imports.

The change from wildcard imports to explicit Jakarta Persistence imports improves code clarity and reduces potential naming conflicts.


19-26: LGTM: Consistent entity refactoring.

The renaming from admission_ticket_image to exam_ticket_image for both table and column names is consistent with the domain model shift from school-centric to exam-centric applications.


33-37: Constructor maintains proper encapsulation.

The constructor properly delegates to the parent File class and maintains the same parameter structure, ensuring backward compatibility within the new domain model.

src/main/java/life/mosu/mosuserver/presentation/admin/dto/ApplicationListResponse.java (2)

67-68: LGTM: Consistent field rename

The field rename from admissionTicket to examTicket with the corresponding type change to ExamTicketResponse aligns perfectly with the domain refactoring from admission-based to exam-based terminology.


68-68: Field rename aligns with domain model refactoring.

The change from admissionTicket to examTicket with type ExamTicketResponse is consistent with the broader refactoring from school-centric to exam-centric model.

src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAttachmentService.java (1)

54-54: LGTM! S3Service method call simplification.

The removal of the explicit duration parameter from getPreSignedUrl aligns well with centralizing expiration configuration within the S3Service. This makes the code cleaner and more maintainable.

src/main/java/life/mosu/mosuserver/infra/storage/application/S3Service.java (1)

102-116: LGTM! Excellent API simplification by centralizing expiration configuration.

The removal of the explicit Duration parameter and internal retrieval from S3Properties makes the API cleaner while centralizing configuration. This is a good design improvement that reduces parameter passing across the codebase.

src/main/java/life/mosu/mosuserver/application/faq/FaqAttachmentService.java (2)

53-53: LGTM!

The removal of the expiration parameter from getPreSignedUrl aligns with the centralized expiration management in S3Service.


32-32: Method signature confirmed
The toFaqAttachmentEntity(Long faqId) method exists as expected.

src/main/java/life/mosu/mosuserver/global/util/FileRequest.java:24
public FaqAttachmentJpaEntity toFaqAttachmentEntity(Long faqId) { … }

No further changes needed.

src/main/java/life/mosu/mosuserver/application/notice/NoticeAttachmentService.java (2)

32-32: LGTM! Good simplification of the lambda expression.

The removal of explicit parameters in the lambda expression improves readability while maintaining functionality. This aligns well with the centralized approach to handling file metadata.


74-74: LGTM! Simplified presigned URL generation.

Moving the expiration duration management to the S3Service centralizes configuration and reduces parameter passing. This is a good architectural improvement.

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

94-104: LGTM! Well-structured error codes for the new domain model.

The new error codes provide comprehensive coverage for the exam-centric refactoring:

  • Multi-insert operation failures are properly handled
  • Exam and exam application not found scenarios are covered
  • Lunch-related errors are included
  • HTTP status codes are appropriate
  • Error messages are clear and user-friendly

These additions align well with the architectural shift from school-centric to exam-centric model.

src/main/java/life/mosu/mosuserver/global/util/FileRequest.java (5)

4-4: Import change aligns with domain refactoring.

The import change from AdmissionTicketImageJpaEntity to ExamTicketImageJpaEntity correctly reflects the domain model shift from school-based to exam-centric applications.


52-59: Method rename correctly reflects domain changes.

The method rename from toAdmissionTicketImageEntity to toExamTicketImageEntity properly aligns with the shift from admission-based to exam-based domain model. The entity creation logic remains consistent.


24-31: Entity conversion methods follow consistent patterns.

All entity conversion methods follow a consistent builder pattern using the record's fileName and s3Key fields directly. The visibility settings are appropriately configured for each entity type:

  • FAQ attachments: PUBLIC (appropriate for public documentation)
  • Inquiry answer attachments: PRIVATE (appropriate for customer service responses)
  • Event attachments: PUBLIC (appropriate for public event materials)

Also applies to: 61-68, 70-77


33-40: Private visibility verified via pre-signed URLs

Inquiry attachments are served with private S3 ACLs but exposed through pre-signed links:

  • In InquiryAttachmentService#createAttachDetailResponse, s3Service.getPreSignedUrl(attachment.getS3Key()) generates access URLs for PRIVATE objects.

No further changes needed.


43-50: NoticeAttachment visibility is consistent with other public attachments

The change to Visibility.PUBLIC for notice attachments matches the pattern used for FAQ and event attachments (also set to PUBLIC), while user-specific attachments (inquiry, exam ticket, inquiry answer) remain PRIVATE. No further action is needed.

• File: src/main/java/life/mosu/mosuserver/global/util/FileRequest.java
toFaqAttachmentEntity (line 28): PUBLIC
toInquiryAttachmentEntity (line 37): PRIVATE
toNoticeAttachmentEntity (line 43): PUBLIC
toExamTicketImageEntity (line 56): PRIVATE
toInquiryAnswerAttachmentEntity (line 65): PRIVATE
toEventAttachmentEntity (line 74): PUBLIC

Comment on lines +7 to +20
// @Query("""
// SELECT new life.mosu.mosuserver.domain.refund.RefundNotifyProjection(
// p.paymentKey,
// a.examDate,
// a.schoolName,
// p.paymentMethod,
// r.reason
// )
// FROM RefundJpaEntity r
// LEFT JOIN PaymentJpaEntity p ON r.applicationSchoolId = p.applicationSchoolId
// LEFT JOIN ApplicationSchoolJpaEntity a ON r.applicationSchoolId = a.id
// WHERE r.applicationSchoolId = :applicationSchoolId
// """)
// Optional<RefundNotifyProjection> findRefundByApplicationSchoolId(Long applicationSchoolId);
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify refund functionality replacement in the new exam-centric model.

The commented-out method findRefundByApplicationSchoolId suggests refund queries need to be adapted for the new exam-based model. Ensure that equivalent functionality exists for refund processing with exam applications.

Consider either:

  1. Implementing a replacement method for exam-based refunds
  2. Removing the commented code if no longer needed
  3. Adding a TODO comment explaining the migration plan

🏁 Script executed:

#!/bin/bash
# Search for any refund-related functionality that might need the commented method
rg -A 5 -B 5 "findRefundByApplicationSchoolId|RefundJpaRepository" --type java

Length of output: 6622


Implement exam-centric refund query and update notification logic

The commented-out findRefundByApplicationSchoolId has not been replaced by an exam-centric alternative. You’ll need to either provide a new repository method based on your exam model or remove the dead code and clearly document the migration plan.

RefundJpaRepository.java

  • Remove or replace the commented JPQL.
  • Add a new method (e.g. findRefundByExamApplicationId(Long examApplicationId)) that joins RefundJpaEntity, PaymentJpaEntity and your exam-application entity to populate RefundNotifyProjection.

NotifyVariableFactory.java

  • Un-comment and update createRefundVariable to call the new repository method.
  • Throw APPLICATION_NOT_FOUND or an appropriate exception if the projection isn’t present, then map it into RefundNotifyRequest.

If you intend to postpone this work, replace the commented block with a clear // TODO: … pointing to the migration ticket.

🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/domain/refund/RefundJpaRepository.java
around lines 7 to 20, the commented-out method findRefundByApplicationSchoolId
is obsolete and needs to be replaced with an exam-centric query. Remove the
commented JPQL and implement a new repository method named
findRefundByExamApplicationId that joins RefundJpaEntity, PaymentJpaEntity, and
the exam-application entity to return RefundNotifyProjection. Also, update
NotifyVariableFactory.java to un-comment and modify createRefundVariable to call
this new method, throwing an APPLICATION_NOT_FOUND exception if no projection is
found, and map the result into RefundNotifyRequest. If this work is deferred,
replace the commented code with a clear TODO referencing the migration ticket.

Comment on lines +16 to +18
List<TargetExam> targetExams = examApplicationRequests.stream()
.map(request -> new TargetExam(request.examId(), request.lunchId()))
.toList();
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding null safety validation.

The factory method should validate input parameters to prevent null pointer exceptions during stream operations.

 public static RegisterExamApplicationEvent of(
         List<ExamApplicationRequest> examApplicationRequests,
         Long applicationId
 ) {
+    if (examApplicationRequests == null || applicationId == null) {
+        throw new IllegalArgumentException("ExamApplicationRequests and applicationId cannot be null");
+    }
+    
     List<TargetExam> targetExams = examApplicationRequests.stream()
             .map(request -> new TargetExam(request.examId(), request.lunchId()))
             .toList();
     return new RegisterExamApplicationEvent(targetExams, applicationId);
 }
📝 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
List<TargetExam> targetExams = examApplicationRequests.stream()
.map(request -> new TargetExam(request.examId(), request.lunchId()))
.toList();
public static RegisterExamApplicationEvent of(
List<ExamApplicationRequest> examApplicationRequests,
Long applicationId
) {
if (examApplicationRequests == null || applicationId == null) {
throw new IllegalArgumentException("ExamApplicationRequests and applicationId cannot be null");
}
List<TargetExam> targetExams = examApplicationRequests.stream()
.map(request -> new TargetExam(request.examId(), request.lunchId()))
.toList();
return new RegisterExamApplicationEvent(targetExams, applicationId);
}
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/examapplication/dto/RegisterExamApplicationEvent.java
around lines 16 to 18, the stream operation on examApplicationRequests lacks
null safety checks. Add validation to ensure examApplicationRequests is not null
before streaming, and consider validating individual elements or their fields to
prevent null pointer exceptions during the map operation.

Comment on lines +22 to +28
@GetMapping()
public ResponseEntity<ApiResponseWrapper<List<LunchResponse>>> getByExamId(
@RequestParam Long examId) {
List<LunchResponse> lunchResponses = lunchService.getByExamId(examId);
return ResponseEntity.ok(
ApiResponseWrapper.success(HttpStatus.OK, "시험에 호환되는 도시락 전체 조회 성공", lunchResponses));
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add input validation for examId parameter.

The controller endpoint is well-structured, but consider adding validation for the examId parameter to handle invalid inputs gracefully.

 @GetMapping()
 public ResponseEntity<ApiResponseWrapper<List<LunchResponse>>> getByExamId(
-        @RequestParam Long examId) {
+        @RequestParam @Min(1) Long examId) {
     List<LunchResponse> lunchResponses = lunchService.getByExamId(examId);
     return ResponseEntity.ok(
             ApiResponseWrapper.success(HttpStatus.OK, "시험에 호환되는 도시락 전체 조회 성공", lunchResponses));
 }

Also consider adding @Valid annotation and proper exception handling for when the examId doesn't exist.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/presentation/lunch/LunchController.java
around lines 22 to 28, add input validation for the examId parameter by checking
if it is null or less than or equal to zero before calling the service. Use
annotations like @RequestParam(required = true) and consider adding @Valid if
applicable. Implement proper exception handling to return meaningful error
responses when examId is invalid or does not exist, ensuring the controller
gracefully handles such cases.

Comment on lines +44 to +50
private Area parseArea(String areaName) {
try {
return Area.from(areaName);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve exception handling in parseArea method.

The current exception handling is too broad and loses important error information. Consider handling specific exceptions and providing more meaningful error messages.

 private Area parseArea(String areaName) {
     try {
         return Area.from(areaName);
-    } catch (Exception e) {
-        throw new RuntimeException(e);
+    } catch (IllegalArgumentException e) {
+        throw new IllegalArgumentException("Invalid area name: " + areaName, e);
+    } catch (Exception e) {
+        throw new IllegalStateException("Failed to parse area name: " + areaName, 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 Area parseArea(String areaName) {
try {
return Area.from(areaName);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private Area parseArea(String areaName) {
try {
return Area.from(areaName);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid area name: " + areaName, e);
} catch (Exception e) {
throw new IllegalStateException("Failed to parse area name: " + areaName, e);
}
}
🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/presentation/exam/dto/ExamRequest.java
between lines 44 and 50, the parseArea method catches a broad Exception and
rethrows it as a generic RuntimeException, which obscures the original error
details. Refine the exception handling by catching specific exceptions that
Area.from might throw, and throw a new exception with a clear, meaningful
message that includes the original exception as the cause to preserve error
context.

public interface ExamTicketImageJpaRepository extends
JpaRepository<ExamTicketImageJpaEntity, Long> {

ExamTicketImageJpaEntity findByApplicationId(Long 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

Consider using Optional for null safety

The method findByApplicationId returns ExamTicketImageJpaEntity directly, which will return null if no record is found. Consider using Optional<ExamTicketImageJpaEntity> for better null safety and explicit handling of missing records.

-    ExamTicketImageJpaEntity findByApplicationId(Long applicationId);
+    Optional<ExamTicketImageJpaEntity> findByApplicationId(Long applicationId);

Don't forget to add the import:

+import java.util.Optional;
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/domain/application/ExamTicketImageJpaRepository.java
at line 8, the method findByApplicationId currently returns
ExamTicketImageJpaEntity directly, which can be null if no record is found.
Change the return type to Optional<ExamTicketImageJpaEntity> to improve null
safety and make the absence of a record explicit. Also, add the necessary import
for java.util.Optional at the top of the file.

🛠️ Refactor suggestion

Consider using Optional return type for better null safety.

The findByApplicationId method should return Optional<ExamTicketImageJpaEntity> to handle cases where no exam ticket image exists for the given application ID, following Spring Data JPA best practices.

-    ExamTicketImageJpaEntity findByApplicationId(Long applicationId);
+    Optional<ExamTicketImageJpaEntity> findByApplicationId(Long applicationId);

Don't forget to add the import:

import java.util.Optional;
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/domain/application/ExamTicketImageJpaRepository.java
at line 8, change the return type of the method findByApplicationId from
ExamTicketImageJpaEntity to Optional<ExamTicketImageJpaEntity> to improve null
safety and follow Spring Data JPA best practices. Also, add the import statement
for java.util.Optional at the top of the file.

Comment on lines +1 to +91
//package life.mosu.mosuserver.infra.respository;
//
//import java.sql.PreparedStatement;
//import java.sql.ResultSet;
//import java.sql.SQLException;
//import java.sql.Statement;
//import java.sql.Timestamp;
//import java.time.LocalDateTime;
//import java.util.ArrayList;
//import java.util.List;
//import life.mosu.mosuserver.domain.application.Subject;
//import life.mosu.mosuserver.domain.examapplication.ExamApplicationJpaEntity;
//import life.mosu.mosuserver.global.exception.CustomRuntimeException;
//import life.mosu.mosuserver.global.exception.ErrorCode;
//import lombok.RequiredArgsConstructor;
//import org.springframework.dao.DataAccessException;
//import org.springframework.jdbc.core.ConnectionCallback;
//import org.springframework.jdbc.core.JdbcTemplate;
//import org.springframework.stereotype.Repository;
//import org.springframework.transaction.annotation.Transactional;
//
//@Repository
//@RequiredArgsConstructor
//public class ExamApplicationBulkRepository {
//
// private static final String SQL_INSERT_EXAM_APPLICATION = """
// INSERT INTO exam_application
// (created_at, updated_at, application_id, user_id,
// examId, examination_number)
// VALUES (?,?,?,?,?,?)
// """;
// private static final String SQL_INSERT_SUBJECT = """
// INSERT INTO exam_subject (exam_application_id, subjects) VALUES (?, ?)
// """;
// private final JdbcTemplate jdbcTemplate;
//
//
// @Transactional
// public List<Long> saveAllExamApplicationsWithSubjects(
// List<ExamApplicationJpaEntity> entities) {
//
// List<Long> generatedIds = jdbcTemplate.execute((ConnectionCallback<List<Long>>) con -> {
// try (PreparedStatement ps = con.prepareStatement(
// SQL_INSERT_EXAM_APPLICATION, Statement.RETURN_GENERATED_KEYS)) {
// for (ExamApplicationJpaEntity e : entities) {
// ps.setTimestamp(1, Timestamp.valueOf(LocalDateTime.now()));
// ps.setTimestamp(2, Timestamp.valueOf(LocalDateTime.now()));
// ps.setLong(3, e.getApplicationId());
// ps.setLong(4, e.getUserId());
// ps.setLong(5, e.examId());
// ps.setString(6, e.getExaminationNumber());
// ps.addBatch();
// }
//
// ps.executeBatch();
//
// List<Long> ids = new ArrayList<>();
// try (ResultSet rs = ps.getGeneratedKeys()) {
// while (rs.next()) {
// ids.add(rs.getLong(1));
// }
// }
// return ids;
//
// } catch (SQLException e) {
// throw new CustomRuntimeException(ErrorCode.EXAM_APPLICATION_MULTI_INSERT_ERROR);
// }
// });
//
// List<Object[]> subjectParams = new ArrayList<>();
//
// for (int i = 0; i < entities.size(); i++) {
// Long applicationSchoolId = generatedIds.get(i);
//
// for (Subject subj : entities.get(i).getSubjects()) {
// subjectParams.add(new Object[]{applicationSchoolId, String.valueOf(subj)});
// }
// }
//
// try {
// if (!subjectParams.isEmpty()) {
// jdbcTemplate.batchUpdate(SQL_INSERT_SUBJECT, subjectParams);
// }
// } catch (DataAccessException e) {
// throw new CustomRuntimeException(ErrorCode.EXAM_SUBJECT_MULTI_INSERT_ERROR);
// }
//
// return generatedIds;
// }
//
//}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove commented-out code or activate the implementation.

The entire ExamApplicationBulkRepository implementation is commented out, making it dead code. Either activate this implementation if it's needed for the exam-centric refactoring or remove it entirely to maintain code cleanliness.

If this implementation is intended for future use, consider:

  1. Activating the code and addressing previous review feedback:

    • The exception handling improvements suggested in past reviews
    • Adding batch size limits for large datasets
    • Using consistent timestamps across batch operations
  2. Removing the commented code if it's no longer needed for the current exam-centric model

The current approach of keeping large blocks of commented code reduces maintainability and can confuse future developers about the intended architecture.

🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/infra/respository/BulkRepository.java
lines 1 to 91, the entire ExamApplicationBulkRepository class is commented out,
resulting in dead code. To fix this, either fully uncomment and activate the
implementation if it is required, ensuring to address any prior review comments
such as improving exception handling, adding batch size limits, and using
consistent timestamps, or remove the entire commented block if it is no longer
needed to keep the codebase clean and maintainable.

Comment on lines +26 to +34
@PostMapping
public ResponseEntity<ApiResponseWrapper<Void>> register(
@RequestBody ExamRequest request
) {
examService.register(request);
return ResponseEntity.ok(
ApiResponseWrapper.success(HttpStatus.CREATED, "시험 등록 성공"));

}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add input validation for the request body.

The register method lacks validation annotations for the ExamRequest parameter. Consider adding @Valid to ensure proper validation of incoming data.

 @PostMapping
 public ResponseEntity<ApiResponseWrapper<Void>> register(
-        @RequestBody ExamRequest request
+        @RequestBody @Valid ExamRequest request
 ) {
📝 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
@PostMapping
public ResponseEntity<ApiResponseWrapper<Void>> register(
@RequestBody ExamRequest request
) {
examService.register(request);
return ResponseEntity.ok(
ApiResponseWrapper.success(HttpStatus.CREATED, "시험 등록 성공"));
}
@PostMapping
public ResponseEntity<ApiResponseWrapper<Void>> register(
@RequestBody @Valid ExamRequest request
) {
examService.register(request);
return ResponseEntity.ok(
ApiResponseWrapper.success(HttpStatus.CREATED, "시험 등록 성공"));
}
🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/presentation/exam/ExamController.java
around lines 26 to 34, the register method's ExamRequest parameter lacks
validation. Add the @Valid annotation before the @RequestBody annotation on the
ExamRequest parameter to enable validation of the incoming request data
according to the constraints defined in the ExamRequest class.

Comment on lines +36 to +43
@GetMapping
public ResponseEntity<ApiResponseWrapper<List<ExamResponse>>> getByArea(
@RequestParam String areaName
) {
List<ExamResponse> response = examService.getByArea(areaName);
return ResponseEntity.ok(
ApiResponseWrapper.success(HttpStatus.OK, "지역별 시험 조회 성공", response));
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve type safety for area parameter.

Using String areaName parameter reduces type safety. Consider using the Area enum directly or add validation to ensure the provided string is a valid area.

 @GetMapping
 public ResponseEntity<ApiResponseWrapper<List<ExamResponse>>> getByArea(
-        @RequestParam String areaName
+        @RequestParam Area area
 ) {
-    List<ExamResponse> response = examService.getByArea(areaName);
+    List<ExamResponse> response = examService.getByArea(area);

Alternatively, if you need to keep the String parameter, add validation:

 @GetMapping
 public ResponseEntity<ApiResponseWrapper<List<ExamResponse>>> getByArea(
-        @RequestParam String areaName
+        @RequestParam @Valid String areaName
 ) {
📝 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
@GetMapping
public ResponseEntity<ApiResponseWrapper<List<ExamResponse>>> getByArea(
@RequestParam String areaName
) {
List<ExamResponse> response = examService.getByArea(areaName);
return ResponseEntity.ok(
ApiResponseWrapper.success(HttpStatus.OK, "지역별 시험 조회 성공", response));
}
@GetMapping
public ResponseEntity<ApiResponseWrapper<List<ExamResponse>>> getByArea(
@RequestParam Area area
) {
List<ExamResponse> response = examService.getByArea(area);
return ResponseEntity.ok(
ApiResponseWrapper.success(HttpStatus.OK, "지역별 시험 조회 성공", response));
}
🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/presentation/exam/ExamController.java
around lines 36 to 43, the getByArea method uses a String parameter for
areaName, which reduces type safety. To fix this, change the parameter type to
the Area enum if possible, so Spring automatically validates and converts the
input. If you must keep it as a String, add explicit validation logic to check
if the provided string matches a valid Area enum value before calling
examService.getByArea, and handle invalid inputs appropriately by returning a
bad request response.

Comment on lines 110 to 141
List<ExamWithSubjects> exams = examApplications.stream()
.map(examApplication -> {
// 시험 정보 조회
ExamJpaEntity exam = examJpaRepository.findById(examApplication.getExamId())
.orElseThrow(() -> new CustomRuntimeException(ErrorCode.EXAM_NOT_FOUND));

// 점심 정보 조회 (점심은 null일 수 있음)
LunchJpaEntity lunch = null;
if (examApplication.getLunchId() != null) {
lunch = lunchJpaRepository.findById(examApplication.getLunchId())
.orElseThrow(() -> new CustomRuntimeException(ErrorCode.LUNCH_NOT_FOUND));
}

// 과목 정보 조회
List<ExamSubjectJpaEntity> examSubjects =
examSubjectJpaRepository.findByExamApplicationId(examApplication.getId());
Set<String> subjects = examSubjects.stream()
.map(examSubject -> examSubject.getSubject().getSubjectName())
.collect(Collectors.toSet());

// ExamWithSubjects 생성
return new ExamWithSubjects(
examApplication.getId(),
exam.getArea().getAreaName(),
exam.getExamDate(),
exam.getSchoolName(),
lunch != null ? lunch.getName() : "신청 안 함",
examApplication.getExamNumber() != null ? examApplication.getExamNumber() : "",
subjects
);
})
.toList();
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Extract complex nested logic for better maintainability.

The getApplications method contains deeply nested logic that could be extracted into separate methods for better readability and testability.

Consider extracting the exam processing logic:

private ExamWithSubjects createExamWithSubjects(ExamApplicationJpaEntity examApplication) {
    ExamJpaEntity exam = examJpaRepository.findById(examApplication.getExamId())
            .orElseThrow(() -> new CustomRuntimeException(ErrorCode.EXAM_NOT_FOUND));
    
    LunchJpaEntity lunch = null;
    if (examApplication.getLunchId() != null) {
        lunch = lunchJpaRepository.findById(examApplication.getLunchId())
                .orElseThrow(() -> new CustomRuntimeException(ErrorCode.LUNCH_NOT_FOUND));
    }
    
    List<ExamSubjectJpaEntity> examSubjects = 
            examSubjectJpaRepository.findByExamApplicationId(examApplication.getId());
    Set<String> subjects = examSubjects.stream()
            .map(examSubject -> examSubject.getSubject().getSubjectName())
            .collect(Collectors.toSet());
    
    return new ExamWithSubjects(
            examApplication.getId(),
            exam.getArea().getAreaName(),
            exam.getExamDate(),
            exam.getSchoolName(),
            lunch != null ? lunch.getName() : "신청 안 함",
            examApplication.getExamNumber() != null ? examApplication.getExamNumber() : "",
            subjects
    );
}

Then simplify the main method:

List<ExamWithSubjects> exams = examApplications.stream()
        .map(this::createExamWithSubjects)
        .toList();
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java
around lines 110 to 141, the nested logic inside the stream mapping for
examApplications is complex and reduces readability. Extract this logic into a
new private method named createExamWithSubjects that takes an
ExamApplicationJpaEntity and returns an ExamWithSubjects by performing the exam,
lunch, and subject retrievals and constructing the object. Then, replace the
inline lambda in the stream with a method reference to this new method to
simplify the main method and improve maintainability and testability.

Comment on lines 102 to 141
List<ExamApplicationJpaEntity> examApplications =
examApplicationJpaRepository.findByApplicationId(application.getId());

if (examApplications.isEmpty()) {
throw new CustomRuntimeException(ErrorCode.EXAM_APPLICATION_NOT_FOUND);
}

// ExamWithSubjects 리스트 생성
List<ExamWithSubjects> exams = examApplications.stream()
.map(examApplication -> {
// 시험 정보 조회
ExamJpaEntity exam = examJpaRepository.findById(examApplication.getExamId())
.orElseThrow(() -> new CustomRuntimeException(ErrorCode.EXAM_NOT_FOUND));

// 점심 정보 조회 (점심은 null일 수 있음)
LunchJpaEntity lunch = null;
if (examApplication.getLunchId() != null) {
lunch = lunchJpaRepository.findById(examApplication.getLunchId())
.orElseThrow(() -> new CustomRuntimeException(ErrorCode.LUNCH_NOT_FOUND));
}

// 과목 정보 조회
List<ExamSubjectJpaEntity> examSubjects =
examSubjectJpaRepository.findByExamApplicationId(examApplication.getId());
Set<String> subjects = examSubjects.stream()
.map(examSubject -> examSubject.getSubject().getSubjectName())
.collect(Collectors.toSet());

// ExamWithSubjects 생성
return new ExamWithSubjects(
examApplication.getId(),
exam.getArea().getAreaName(),
exam.getExamDate(),
exam.getSchoolName(),
lunch != null ? lunch.getName() : "신청 안 함",
examApplication.getExamNumber() != null ? examApplication.getExamNumber() : "",
subjects
);
})
.toList();
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Address potential N+1 query performance issues.

The current implementation may cause N+1 query problems by fetching exam, lunch, and subject data individually for each exam application. Consider using batch fetching or JOIN queries.

Consider optimizing with batch queries:

// Fetch all required data in batches
Set<Long> examIds = examApplications.stream()
        .map(ExamApplicationJpaEntity::getExamId)
        .collect(Collectors.toSet());

Set<Long> lunchIds = examApplications.stream()
        .map(ExamApplicationJpaEntity::getLunchId)
        .filter(Objects::nonNull)
        .collect(Collectors.toSet());

Map<Long, ExamJpaEntity> examMap = examJpaRepository.findAllById(examIds)
        .stream().collect(Collectors.toMap(ExamJpaEntity::getId, Function.identity()));

Map<Long, LunchJpaEntity> lunchMap = lunchJpaRepository.findAllById(lunchIds)
        .stream().collect(Collectors.toMap(LunchJpaEntity::getId, Function.identity()));

// Then use these maps in the stream processing

Verify the current query patterns to confirm if N+1 issues exist:


🏁 Script executed:

#!/bin/bash
# Search for potential N+1 query patterns in repository methods
echo "Checking for repository query methods that might cause N+1 issues..."

# Look for findById calls in loops or streams
rg -A 3 -B 3 "\.findById\(" --type java

# Check for repository methods that should use batch fetching
rg -A 3 -B 3 "findByExamApplicationId|findAllById" --type java

Length of output: 16408


Optimize ApplicationService to Eliminate N+1 Queries

The current stream mapping issues separate lookups for each Exam, Lunch, and Subjects, resulting in N+1 queries. Batch‐fetch all related data up front and then map into your DTO.

• File: src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java
Lines: 102–141
• Add a repository method to fetch subjects in bulk:

interface ExamSubjectJpaRepository extends JpaRepository<ExamSubjectJpaEntity, Long> {
    List<ExamSubjectJpaEntity> findByExamApplicationIdIn(Collection<Long> ids);
}

• Refactor mapping:

// 1. Collect IDs
Set<Long> applicationIds = examApplications.stream()
    .map(ExamApplicationJpaEntity::getId)
    .collect(Collectors.toSet());
Set<Long> examIds = examApplications.stream()
    .map(ExamApplicationJpaEntity::getExamId)
    .collect(Collectors.toSet());
Set<Long> lunchIds = examApplications.stream()
    .map(ExamApplicationJpaEntity::getLunchId)
    .filter(Objects::nonNull)
    .collect(Collectors.toSet());

// 2. Batch‐fetch entities
Map<Long, ExamJpaEntity> examMap = examJpaRepository.findAllById(examIds)
    .stream().collect(Collectors.toMap(ExamJpaEntity::getId, Function.identity()));
Map<Long, LunchJpaEntity> lunchMap = lunchJpaRepository.findAllById(lunchIds)
    .stream().collect(Collectors.toMap(LunchJpaEntity::getId, Function.identity()));
List<ExamSubjectJpaEntity> allSubjects = 
    examSubjectJpaRepository.findByExamApplicationIdIn(applicationIds);
Map<Long, Set<String>> subjectsByApp = allSubjects.stream()
    .collect(Collectors.groupingBy(
        s -> s.getExamApplication().getId(),
        Collectors.mapping(s -> s.getSubject().getSubjectName(), Collectors.toSet())
    ));

// 3. Build DTOs without further queries
List<ExamWithSubjects> exams = examApplications.stream()
    .map(app -> new ExamWithSubjects(
        app.getId(),
        examMap.get(app.getExamId()).getArea().getAreaName(),
        examMap.get(app.getExamId()).getExamDate(),
        examMap.get(app.getExamId()).getSchoolName(),
        Optional.ofNullable(app.getLunchId())
            .map(lunchMap::get).map(LunchJpaEntity::getName).orElse("신청 안 함"),
        Optional.ofNullable(app.getExamNumber()).orElse(""),
        subjectsByApp.getOrDefault(app.getId(), Set.of())
    ))
    .toList();

This removes per‐entity findById and single‐ID subject queries, collapsing them into three database hits.

🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java
lines 102 to 141, the current code performs N+1 queries by fetching Exam, Lunch,
and Subject entities individually inside a stream map operation. To fix this,
add a repository method in ExamSubjectJpaRepository to fetch subjects by a
collection of application IDs. Then, batch-fetch all Exams, Lunches, and
Subjects upfront using findAllById and the new method, storing them in maps
keyed by their IDs. Finally, refactor the stream mapping to build
ExamWithSubjects DTOs by looking up these pre-fetched entities from the maps,
eliminating per-entity queries and reducing database hits to just three batch
queries.

…uotas and remove unused SchoolQuotaCacheManager
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: 9

♻️ Duplicate comments (2)
src/main/java/life/mosu/mosuserver/application/exam/ExamService.java (2)

22-35: Add @transactional to ensure data consistency.

The register method saves an exam, updates cache, and publishes an event. Without transaction management, if any operation fails, the system could be left in an inconsistent state.

+import org.springframework.transaction.annotation.Transactional;

+@Transactional
 public void register(ExamRequest request) {
     ExamJpaEntity exam = request.toEntity();
     ExamJpaEntity savedExam = examJpaRepository.save(exam);
 
     examQuotaCacheManager.addSchoolCurrentApplicationCount(
             savedExam.getSchoolName(),
             0L
     );
     examQuotaCacheManager.addSchoolMaxCapacity(
             savedExam.getSchoolName(),
             savedExam.getCapacity()
     );
     createLunch(request.toCreateLunchEvent(savedExam.getId()));
 }

37-41: Add error handling for invalid area names.

The Area.from() method might throw an exception for invalid area names. Consider adding proper error handling.

 public List<ExamResponse> getByArea(String areaName) {
+    if (areaName == null || areaName.isBlank()) {
+        throw new IllegalArgumentException("Area name cannot be null or empty");
+    }
     Area area = Area.from(areaName);
     List<ExamJpaEntity> foundExams = examJpaRepository.findByArea(area);
     return ExamResponse.fromList(foundExams);
 }
🧹 Nitpick comments (2)
src/main/java/life/mosu/mosuserver/application/exam/ExamQuotaCacheManager.java (1)

16-17: Consider updating Redis key prefixes to reflect the exam-centric model.

The Redis keys still use the "school:" prefix, which may be confusing now that the system has transitioned to an exam-centric model. Consider updating these to be more descriptive of the current domain.

-    private static final String REDIS_KEY_SCHOOL_MAX_CAPACITY = "school:max_capacity:";
-    private static final String REDIS_KEY_SCHOOL_CURRENT_APPLICATIONS = "school:current_applications:";
+    private static final String REDIS_KEY_SCHOOL_MAX_CAPACITY = "exam:school:max_capacity:";
+    private static final String REDIS_KEY_SCHOOL_CURRENT_APPLICATIONS = "exam:school:current_applications:";
src/main/java/life/mosu/mosuserver/domain/exam/ExamJpaRepository.java (1)

36-45: Consider adding database indexes for performance.

The query groups by school name and counts applications, which could be expensive without proper indexing.

Consider adding database indexes on:

  • ExamApplicationJpaEntity.examId (for the JOIN)
  • ExamJpaEntity.schoolName (for the GROUP BY)

This will significantly improve query performance as the number of applications grows.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e49fc92 and 2ef9ad2.

📒 Files selected for processing (14)
  • build.gradle (1 hunks)
  • src/main/java/life/mosu/mosuserver/MosuServerApplication.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/application/exam/ExamQuotaCacheManager.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/exam/ExamService.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/notify/NotifyVariableFactory.java (3 hunks)
  • src/main/java/life/mosu/mosuserver/application/payment/PaymentService.java (3 hunks)
  • src/main/java/life/mosu/mosuserver/domain/exam/ExamInfoProjection.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/exam/ExamJpaRepository.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/exam/SchoolExamCountProjection.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/global/runner/ApplicationSchoolPreWarmRunner.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/admin/AdminController.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/application/dto/ApplicationSchoolRequest.java (0 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/payment/dto/PreparePaymentRequest.java (1 hunks)
💤 Files with no reviewable changes (2)
  • src/main/java/life/mosu/mosuserver/MosuServerApplication.java
  • src/main/java/life/mosu/mosuserver/presentation/application/dto/ApplicationSchoolRequest.java
✅ Files skipped from review due to trivial changes (3)
  • build.gradle
  • src/main/java/life/mosu/mosuserver/domain/exam/SchoolExamCountProjection.java
  • src/main/java/life/mosu/mosuserver/domain/exam/ExamInfoProjection.java
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main/java/life/mosu/mosuserver/presentation/admin/AdminController.java
  • src/main/java/life/mosu/mosuserver/application/payment/PaymentService.java
  • src/main/java/life/mosu/mosuserver/global/runner/ApplicationSchoolPreWarmRunner.java
  • src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java
🔇 Additional comments (2)
src/main/java/life/mosu/mosuserver/presentation/payment/dto/PreparePaymentRequest.java (1)

3-7: LGTM on the architectural simplification.

The simplified record structure aligns well with the exam-centric refactoring described in the PR objectives. Removing the complex items structure and getSize() method reduces complexity and supports the new domain model effectively.

src/main/java/life/mosu/mosuserver/application/exam/ExamQuotaCacheManager.java (1)

42-44: Capacity parameter type is consistent with the domain model

The ExamJpaEntity.capacity field is defined as an Integer (see src/main/java/life/mosu/mosuserver/domain/exam/ExamJpaEntity.java), so changing addSchoolMaxCapacity to accept an Integer matches the domain. The internal conversion to Long is solely for Redis storage, which expects a Long value. No API inconsistency—this review comment can be ignored.

Likely an incorrect or invalid review comment.

Comment on lines 3 to 5
public record PreparePaymentRequest(
@NotEmpty(message = "결제 항목은 비어 있을 수 없습니다.")
List<@Valid Item> items
Long applicationId
) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add validation for the applicationId field.

The applicationId field lacks validation annotations, which could lead to runtime issues if null values are passed. Consider adding appropriate validation.

+import jakarta.validation.constraints.NotNull;
+
 public record PreparePaymentRequest(
+        @NotNull(message = "Application ID cannot be null")
         Long applicationId
 ) {
📝 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
public record PreparePaymentRequest(
@NotEmpty(message = "결제 항목은 비어 있을 수 없습니다.")
List<@Valid Item> items
Long applicationId
) {
import jakarta.validation.constraints.NotNull;
public record PreparePaymentRequest(
@NotNull(message = "Application ID cannot be null")
Long applicationId
) {
}
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/presentation/payment/dto/PreparePaymentRequest.java
around lines 3 to 5, the applicationId field lacks validation annotations. Add a
validation annotation such as @NotNull to the applicationId field to ensure it
is not null when the record is instantiated, preventing potential runtime
issues.

💡 Verification agent

🧩 Analysis chain

Verify that the simplified structure supports the new exam-centric model.

The significant simplification from a complex items structure to a single applicationId aligns with the architectural shift to an exam-centric model. However, ensure that this change doesn't inadvertently remove necessary functionality for handling multiple exam applications or payment scenarios.


🏁 Script executed:

#!/bin/bash
# Description: Verify how the simplified PreparePaymentRequest is used in the payment flow
# and ensure it adequately supports the exam-centric model

# Search for usage of PreparePaymentRequest in payment-related services
echo "=== Checking PreparePaymentRequest usage in payment services ==="
rg -A 10 -B 5 "PreparePaymentRequest" --type java

echo -e "\n=== Checking payment preparation logic ==="
rg -A 15 -B 5 "preparePayment\|PreparePayment" --type java

echo -e "\n=== Checking for any remaining school-based payment logic ==="
rg -A 5 -B 5 "school.*payment\|payment.*school" --type java -i

Length of output: 9258


Implement exam-centric prepare logic in PaymentService

The DTO now only carries applicationId, but the service still ignores it and relies on legacy, commented-out size-based logic. To complete the exam-centric shift:

  • In src/main/java/life/mosu/mosuserver/application/payment/PaymentService.java
    • Replace the commented lines
    // int applicationCount = request.getSize();
    // int totalAmount = DiscountPolicy.calculate(FIXED_QUANTITY, applicationCount);
    with code that:
    1. Loads the application by request.applicationId (e.g., via ApplicationRepository).
    2. Determines participant count or exam details from that entity.
    3. Calculates the correct totalAmount (using your discount policy).
  • Remove or resolve the //TODO: 신청서 필요함. in PaymentWidgetController—ensure the controller and response include any new data needed for the frontend.
  • If the exam-centric model must support batch payments (multiple applications at once), decide whether to extend PreparePaymentRequest (e.g., list of IDs) or handle that at a higher level.

Comment on lines +23 to 30
List<ExamJpaEntity> exams = examJpaRepository.findUpcomingExamInfo(LocalDate.now());
for (ExamJpaEntity exam : exams) {
addSchoolMaxCapacity(
exam.getSchoolName(),
exam.getCapacity()
);
}
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify the new repository method implementations.

The caching logic now depends on new repository methods. Ensure these methods are properly implemented and tested.


🏁 Script executed:

#!/bin/bash
# Description: Verify the new ExamJpaRepository methods are properly defined

# Search for the findUpcomingExamInfo method implementation
ast-grep --pattern 'findUpcomingExamInfo($$$)'

# Search for SchoolExamCountProjection usage
rg -A 5 "SchoolExamCountProjection" --type java

Length of output: 3271


Missing findUpcomingExamInfo in ExamJpaRepository – implementation required

The cache manager now calls

examJpaRepository.findUpcomingExamInfo(LocalDate.now());

but no such method is defined in
src/main/java/life/mosu/mosuserver/domain/exam/ExamJpaRepository.java. You’ll need to:

• Declare the repository method with the correct signature and query, for example:

@Query("SELECT e FROM ExamJpaEntity e WHERE e.scheduledDate >= :date")
List<ExamJpaEntity> findUpcomingExamInfo(LocalDate date);

• Add corresponding unit or integration tests to cover this new method.

Once added and tested, the caching logic will function as expected.

🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/domain/exam/ExamJpaRepository.java, you
need to declare the method findUpcomingExamInfo with the signature
List<ExamJpaEntity> findUpcomingExamInfo(LocalDate date) and annotate it with
@Query("SELECT e FROM ExamJpaEntity e WHERE e.scheduledDate >= :date") to fetch
exams scheduled from the given date onward. After adding this method, create
appropriate unit or integration tests to verify its behavior. This will ensure
the ExamQuotaCacheManager's call to findUpcomingExamInfo(LocalDate.now()) works
correctly.

projection.schoolName());
ExamInfoProjection examInfo = examJpaRepository.findExamInfo(targetId)
.orElseThrow(
() -> new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_NOT_FOUND));
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Update error codes to match the new domain model.

The error code APPLICATION_SCHOOL_NOT_FOUND is inconsistent with the exam-centric model and may be confusing.

-                        () -> new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_NOT_FOUND));
+                        () -> new CustomRuntimeException(ErrorCode.EXAM_NOT_FOUND));

This change should be applied to all three exam notification methods (lines 74, 84, 94).

Also applies to: 84-84, 94-94

🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/notify/NotifyVariableFactory.java
at lines 74, 84, and 94, the error code APPLICATION_SCHOOL_NOT_FOUND is outdated
and inconsistent with the exam-centric domain model. Replace this error code in
all three exam notification methods with a new, appropriate error code that
reflects the exam context to maintain clarity and consistency.

Comment on lines 64 to 69
private NotificationVariable createApplicationVariable(Long targetId) {
ApplicationSchoolNotifyProjection projection = applicationSchoolJpaRepository.findPaymentByApplicationSchoolId(
targetId);
return ApplicationNotifyRequest.from(projection);
// ApplicationSchoolNotifyProjection projection = applicationSchoolJpaRepository.findPaymentByApplicationSchoolId(
// targetId);
// return ApplicationNotifyRequest.from(projection);
return null;
}
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: Application notification functionality is disabled.

Similar to the refund notification, this method returns null which will break the notification system.

 private NotificationVariable createApplicationVariable(Long targetId) {
-//        ApplicationSchoolNotifyProjection projection = applicationSchoolJpaRepository.findPaymentByApplicationSchoolId(
-//                targetId);
-//        return ApplicationNotifyRequest.from(projection);
-        return null;
+        // TODO: Implement exam-based application notification logic
+        throw new UnsupportedOperationException("Application notifications not yet implemented for exam-centric model");
 }
📝 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 NotificationVariable createApplicationVariable(Long targetId) {
ApplicationSchoolNotifyProjection projection = applicationSchoolJpaRepository.findPaymentByApplicationSchoolId(
targetId);
return ApplicationNotifyRequest.from(projection);
// ApplicationSchoolNotifyProjection projection = applicationSchoolJpaRepository.findPaymentByApplicationSchoolId(
// targetId);
// return ApplicationNotifyRequest.from(projection);
return null;
}
private NotificationVariable createApplicationVariable(Long targetId) {
// TODO: Implement exam-based application notification logic
throw new UnsupportedOperationException("Application notifications not yet implemented for exam-centric model");
}
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/notify/NotifyVariableFactory.java
between lines 64 and 69, the createApplicationVariable method currently returns
null, disabling application notification functionality and causing system
failures. To fix this, uncomment and properly implement the logic to fetch the
ApplicationSchoolNotifyProjection using
applicationSchoolJpaRepository.findPaymentByApplicationSchoolId with the
targetId, then convert it to a NotificationVariable by returning
ApplicationNotifyRequest.from(projection). Ensure the repository call and
conversion are correctly handled to restore notification functionality.

Comment on lines 52 to 62
private NotificationVariable createRefundVariable(Long targetId) {
RefundNotifyProjection projection = refundJpaRepository.findRefundByApplicationSchoolId(
targetId)
.orElseThrow(
() -> new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_NOT_FOUND));
return new RefundNotifyRequest(
projection.paymentKey(), projection.examDate(), projection.schoolName(),
projection.paymentMethod().getName(), projection.reason()
);
// RefundNotifyProjection projection = refundJpaRepository.findRefundByApplicationSchoolId(
// targetId)
// .orElseThrow(
// () -> new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_NOT_FOUND));
// return new RefundNotifyRequest(
// projection.paymentKey(), projection.examDate(), projection.schoolName(),
// projection.paymentMethod().getName(), projection.reason()
// );
return null;
}
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: Commented out notification functionality will break the system.

The refund notification method is commented out and returns null, which will likely cause null pointer exceptions in the notification system.

This needs immediate attention. Either:

  1. Implement the new exam-based refund notification logic
  2. Remove the REFUND_SUCCESS case from the switch statement in the create method
  3. Add proper null handling in the notification system
 private NotificationVariable createRefundVariable(Long targetId) {
-//        RefundNotifyProjection projection = refundJpaRepository.findRefundByApplicationSchoolId(
-//                        targetId)
-//                .orElseThrow(
-//                        () -> new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_NOT_FOUND));
-//        return new RefundNotifyRequest(
-//                projection.paymentKey(), projection.examDate(), projection.schoolName(),
-//                projection.paymentMethod().getName(), projection.reason()
-//        );
-        return null;
+        // TODO: Implement exam-based refund notification logic
+        throw new UnsupportedOperationException("Refund notifications not yet implemented for exam-centric model");
 }
📝 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 NotificationVariable createRefundVariable(Long targetId) {
RefundNotifyProjection projection = refundJpaRepository.findRefundByApplicationSchoolId(
targetId)
.orElseThrow(
() -> new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_NOT_FOUND));
return new RefundNotifyRequest(
projection.paymentKey(), projection.examDate(), projection.schoolName(),
projection.paymentMethod().getName(), projection.reason()
);
// RefundNotifyProjection projection = refundJpaRepository.findRefundByApplicationSchoolId(
// targetId)
// .orElseThrow(
// () -> new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_NOT_FOUND));
// return new RefundNotifyRequest(
// projection.paymentKey(), projection.examDate(), projection.schoolName(),
// projection.paymentMethod().getName(), projection.reason()
// );
return null;
}
private NotificationVariable createRefundVariable(Long targetId) {
// TODO: Implement exam-based refund notification logic
throw new UnsupportedOperationException("Refund notifications not yet implemented for exam-centric model");
}
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/notify/NotifyVariableFactory.java
around lines 52 to 62, the createRefundVariable method is commented out and
returns null, which will cause null pointer exceptions in the notification
system. To fix this, either fully implement the refund notification logic based
on the exam data as intended, or if that is not ready, remove the REFUND_SUCCESS
case from the switch statement in the create method to prevent calling this
method. Alternatively, ensure the notification system safely handles null values
returned from this method to avoid runtime errors.

@@ -0,0 +1,47 @@
package life.mosu.mosuserver.domain.exam;

import io.lettuce.core.dynamic.annotation.Param;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix incorrect import for @param annotation.

You're importing @Param from io.lettuce.core.dynamic.annotation.Param instead of the Spring Data JPA annotation.

-import io.lettuce.core.dynamic.annotation.Param;
+import org.springframework.data.repository.query.Param;
📝 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
import io.lettuce.core.dynamic.annotation.Param;
-import io.lettuce.core.dynamic.annotation.Param;
+import org.springframework.data.repository.query.Param;
🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/domain/exam/ExamJpaRepository.java at line
3, the import statement for the @Param annotation is incorrect; it imports from
io.lettuce.core.dynamic.annotation.Param instead of the correct Spring Data JPA
package. Replace this import with
org.springframework.data.repository.query.Param to fix the issue.

Comment on lines +17 to +27
@Query("""
SELECT new life.mosu.mosuserver.domain.exam.ExamInfoProjection(
e.examDate,
ea.examNumber,
e.schoolName
)
FROM ExamApplicationJpaEntity ea
JOIN ExamJpaEntity e ON ea.examId = e.id
WHERE ea.examId = :examId
""")
Optional<ExamInfoProjection> findExamInfo(@Param("examId") Long examId);
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify the JPQL query structure and performance.

The query joins ExamApplicationJpaEntity with ExamJpaEntity but the relationship and performance implications should be verified.


🏁 Script executed:

#!/bin/bash
# Description: Verify the entity relationships and potential performance issues

# Check the ExamApplicationJpaEntity structure and relationships
ast-grep --pattern 'class ExamApplicationJpaEntity {
  $$$
}'

# Check for any @JoinColumn or relationship annotations
rg -A 3 -B 3 "@JoinColumn|@ManyToOne|@OneToMany" --type java src/main/java/life/mosu/mosuserver/domain/exam/

Length of output: 174


🏁 Script executed:

#!/bin/bash
# Locate the entity Java files in the repo
rg -l "class ExamApplicationJpaEntity" -g "*.java"
rg -l "class ExamJpaEntity" -g "*.java"

# Once we have the path to ExamApplicationJpaEntity, check for relationship mappings
rg -n "@ManyToOne|@JoinColumn|ExamJpaEntity" -g "*ExamApplicationJpaEntity.java"

Length of output: 331


🏁 Script executed:

#!/bin/bash
# Display the top of the entity to inspect mappings and fields
sed -n '1,200p' src/main/java/life/mosu/mosuserver/domain/examapplication/ExamApplicationJpaEntity.java

# Specifically search for the examId field and any relationship annotation
rg -n "examId" -g "src/main/java/life/mosu/mosuserver/domain/examapplication/ExamApplicationJpaEntity.java"
rg -n "@ManyToOne|@JoinColumn" -g "src/main/java/life/mosu/mosuserver/domain/examapplication/ExamApplicationJpaEntity.java"

Length of output: 2522


Fix JPQL join on non‐associated entities & optimize foreign‐key indexing

  • JPA JPQL doesn’t support JOIN … ON between unmapped entities. You have two paths:
    • Map the association in ExamApplicationJpaEntity and join on it:
    – Add a @ManyToOne @JoinColumn(name="exam_id") private ExamJpaEntity exam;
    – Change the repository query to
    java @Query(""" SELECT new life.mosu.mosuserver.domain.exam.ExamInfoProjection( e.examDate, ea.examNumber, e.schoolName ) FROM ExamApplicationJpaEntity ea JOIN ea.exam e WHERE e.id = :examId """) Optional<ExamInfoProjection> findExamInfo(@Param("examId") Long examId);
    • Or use an implicit theta-join without ON:

    @Query("""
        SELECT new life.mosu.mosuserver.domain.exam.ExamInfoProjection(
            e.examDate,
            ea.examNumber,
            e.schoolName
        )
        FROM ExamApplicationJpaEntity ea, ExamJpaEntity e
        WHERE ea.examId = e.id
          AND ea.examId = :examId
    """)
    Optional<ExamInfoProjection> findExamInfo(@Param("examId") Long examId);
  • Add a database index on the foreign-key column to speed up the join:

    @Table(
        name = "exam_application",
        indexes = @Index(name = "idx_exam_application_exam_id", columnList = "exam_id")
    )

Affected files:

  • src/main/java/life/mosu/mosuserver/domain/examapplication/ExamApplicationJpaEntity.java
  • src/main/java/life/mosu/mosuserver/domain/exam/ExamJpaRepository.java
🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/domain/exam/ExamJpaRepository.java lines
17 to 27, the JPQL query uses a JOIN ON between unmapped entities, which is
unsupported. Fix this by either mapping a @ManyToOne association in
ExamApplicationJpaEntity to ExamJpaEntity and updating the query to join via the
mapped association, or rewrite the query as a theta-join without ON using a
comma-separated FROM clause and a WHERE condition for the join. Additionally, in
src/main/java/life/mosu/mosuserver/domain/examapplication/ExamApplicationJpaEntity.java,
add a database index annotation on the foreign key column exam_id to optimize
join performance.

Comment on lines +26 to +33
examQuotaCacheManager.addSchoolCurrentApplicationCount(
savedExam.getSchoolName(),
0L
);
examQuotaCacheManager.addSchoolMaxCapacity(
savedExam.getSchoolName(),
savedExam.getCapacity()
);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider moving cache updates to an event listener.

Manual cache updates in the service method create tight coupling and potential consistency issues. Consider handling cache updates through application events.

 public void register(ExamRequest request) {
     ExamJpaEntity exam = request.toEntity();
     ExamJpaEntity savedExam = examJpaRepository.save(exam);

-    examQuotaCacheManager.addSchoolCurrentApplicationCount(
-            savedExam.getSchoolName(),
-            0L
-    );
-    examQuotaCacheManager.addSchoolMaxCapacity(
-            savedExam.getSchoolName(),
-            savedExam.getCapacity()
-    );
+    publisher.publishEvent(new ExamRegisteredEvent(savedExam));
     createLunch(request.toCreateLunchEvent(savedExam.getId()));
 }

This would require creating an ExamRegisteredEvent and corresponding event listener for cache management.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/application/exam/ExamService.java around
lines 26 to 33, the cache updates for school application count and max capacity
are done manually in the service method, causing tight coupling and potential
consistency issues. Refactor by creating an ExamRegisteredEvent that carries the
necessary exam data, then implement an event listener that listens for this
event and performs the cache updates. Remove the direct cache update calls from
the service method and instead publish the event after saving the exam.

…exam ticket retrieval; add ExamApplicationController and UpdateSubjectRequest for handling requests
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: 7

🧹 Nitpick comments (3)
src/main/java/life/mosu/mosuserver/domain/examapplication/ExamSubjectJpaRepository.java (1)

6-11: Consider adding @Modifying annotation to the delete method

While Spring Data JPA will handle the transactional aspect, it's a best practice to add the @Modifying annotation to delete operations for clarity and to ensure proper flush mode handling.

+import org.springframework.data.jpa.repository.Modifying;
+
 public interface ExamSubjectJpaRepository extends JpaRepository<ExamSubjectJpaEntity, Long> {
 
     List<ExamSubjectJpaEntity> findByExamApplicationId(Long examApplicationId);
 
+    @Modifying
     void deleteByExamApplicationId(Long examApplicationId);
 }
src/main/java/life/mosu/mosuserver/application/examapplication/ExamApplicationService.java (1)

63-65: Use method reference for cleaner code

The lambda expression can be replaced with a method reference for better readability.

 List<Subject> subjects = examSubjects.stream()
-        .map(es -> es.getSubject())
+        .map(ExamSubjectJpaEntity::getSubject)
         .toList();
src/main/java/life/mosu/mosuserver/presentation/examapplication/ExamApplicationController.java (1)

18-21: Add validation annotation to the controller

Consider adding @Validated annotation to enable validation of request bodies.

+import org.springframework.validation.annotation.Validated;
+
 @RestController
 @RequestMapping("/exam-application")
 @RequiredArgsConstructor
+@Validated
 public class ExamApplicationController {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ef9ad2 and 8552c3a.

📒 Files selected for processing (7)
  • src/main/java/life/mosu/mosuserver/application/examapplication/ExamApplicationService.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/examapplication/ExamApplicationJpaRepository.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/examapplication/ExamSubjectJpaRepository.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/admin/dto/ExamTicketResponse.java (2 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/examapplication/ExamApplicationController.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/examapplication/dto/UpdateSubjectRequest.java (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/life/mosu/mosuserver/domain/examapplication/ExamApplicationJpaRepository.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/life/mosu/mosuserver/presentation/admin/dto/ExamTicketResponse.java
  • src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java

Comment on lines +46 to +51
@Transactional
public void updateSubjects(Long examApplicationId, UpdateSubjectRequest request) {
examSubjectJpaRepository.deleteByExamApplicationId(examApplicationId);
List<ExamSubjectJpaEntity> examSubjects = request.toEntityList(examApplicationId);
examSubjectJpaRepository.saveAll(examSubjects);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider race condition in subject updates

The current implementation deletes all subjects before saving new ones. This could lead to a race condition where subjects are temporarily missing. Consider using a single transaction with proper locking.

You might want to:

  1. Load existing subjects
  2. Compare and only update what's changed
  3. Or use @Lock annotation for pessimistic locking if concurrent updates are expected
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/examapplication/ExamApplicationService.java
around lines 46 to 51, the current updateSubjects method deletes all subjects
before saving new ones, risking a race condition with temporary data loss. To
fix this, modify the method to first load existing subjects for the given
examApplicationId, compare them with the incoming request data, and update only
the changed subjects instead of deleting all. Additionally, apply a pessimistic
lock using the @Lock annotation on the repository method that loads existing
subjects to prevent concurrent modifications during the update process.

Comment on lines +54 to +59
public ExamTicketResponse getExamTicket(Long userId, Long examApplicationId) {
ExamApplicationJpaEntity examApplication = examApplicationJpaRepository.findById(
examApplicationId)
.orElseThrow(
() -> new CustomRuntimeException(ErrorCode.EXAM_APPLICATION_NOT_FOUND));

Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Add validation to ensure exam application belongs to the user

The method accepts a userId parameter but doesn't validate that the exam application belongs to that user. This could be a security issue allowing users to access other users' exam tickets.


🏁 Script executed:

#!/bin/bash
# Description: Check if ExamApplicationJpaEntity has a userId field or relationship to verify ownership

# Search for ExamApplicationJpaEntity class definition to check for user relationship
ast-grep --pattern 'class ExamApplicationJpaEntity {
  $$$
}'

# Also search for any userId or user-related fields
rg -A 10 "class ExamApplicationJpaEntity" | rg -i "user"

Length of output: 1340


Ensure ExamApplication is Scoped to the Authenticated User

Currently, getExamTicket(Long userId, Long examApplicationId) fetches the application by ID only. Because ExamApplicationJpaEntity has no reference to its owner, you must prevent unauthorized access by:

• Adding a user_id column and mapping (e.g. @manytoone or simple @column) to ExamApplicationJpaEntity
• Updating the repository to expose a lookup by both user and application ID:

Optional<ExamApplicationJpaEntity> 
  findByUserIdAndApplicationId(Long userId, Long applicationId);

• Changing the service call to:

ExamApplicationJpaEntity examApp = 
    examApplicationJpaRepository
        .findByUserIdAndApplicationId(userId, examApplicationId)
        .orElseThrow(() -> 
            new CustomRuntimeException(EXAM_APPLICATION_NOT_FOUND)
        );

These changes enforce that users can only retrieve their own exam tickets.

🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/examapplication/ExamApplicationService.java
around lines 54 to 59, the method getExamTicket fetches ExamApplicationJpaEntity
by examApplicationId only, allowing potential unauthorized access. To fix this,
add a user_id field to ExamApplicationJpaEntity with appropriate mapping, update
the repository to include a method findByUserIdAndApplicationId(Long userId,
Long applicationId), and modify the service method to use this new repository
method to ensure the exam application belongs to the authenticated user before
returning it.

Long applicationId = examApplication.getApplicationId();
ExamTicketImageJpaEntity examTicketImage = examTicketImageJpaRepository.findByApplicationId(
applicationId);
;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove extra semicolon - syntax error

There's an unnecessary semicolon after the statement that will cause a compilation error.

 ExamTicketImageJpaEntity examTicketImage = examTicketImageJpaRepository.findByApplicationId(
         applicationId);
-        ;
📝 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
;
ExamTicketImageJpaEntity examTicketImage = examTicketImageJpaRepository.findByApplicationId(
applicationId);
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/examapplication/ExamApplicationService.java
at line 70, remove the extra semicolon that is standing alone after a statement,
as it causes a syntax error and prevents compilation.

Comment on lines +7 to +9
public record UpdateSubjectRequest(
List<String> subjects
) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding validation annotations

The record should validate that the subjects list is not null or empty to prevent potential NPEs or invalid states.

+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+
 public record UpdateSubjectRequest(
+        @NotNull(message = "Subjects list cannot be null")
+        @NotEmpty(message = "Subjects list cannot be empty")
         List<String> subjects
 ) {
📝 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
public record UpdateSubjectRequest(
List<String> subjects
) {
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
public record UpdateSubjectRequest(
@NotNull(message = "Subjects list cannot be null")
@NotEmpty(message = "Subjects list cannot be empty")
List<String> subjects
) {
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/presentation/examapplication/dto/UpdateSubjectRequest.java
around lines 7 to 9, the subjects list in the record lacks validation, which can
lead to null or empty values causing errors. Add validation annotations such as
@NotNull and @NotEmpty to the subjects field to ensure it is neither null nor
empty, preventing potential null pointer exceptions or invalid states.

Comment on lines +11 to +15
private List<Subject> validatedSubject() {
return subjects.stream()
.map(Subject::valueOf)
.toList();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add validation for invalid subject names

The Subject::valueOf method will throw an IllegalArgumentException if the subject string doesn't match any enum value. This should be handled gracefully with proper error messages.

 private List<Subject> validatedSubject() {
     return subjects.stream()
-            .map(Subject::valueOf)
+            .map(subjectName -> {
+                try {
+                    return Subject.valueOf(subjectName);
+                } catch (IllegalArgumentException e) {
+                    throw new CustomRuntimeException(
+                        ErrorCode.INVALID_SUBJECT_NAME, 
+                        "Invalid subject name: " + subjectName
+                    );
+                }
+            })
             .toList();
 }
📝 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 List<Subject> validatedSubject() {
return subjects.stream()
.map(Subject::valueOf)
.toList();
}
private List<Subject> validatedSubject() {
return subjects.stream()
- .map(Subject::valueOf)
+ .map(subjectName -> {
+ try {
+ return Subject.valueOf(subjectName);
+ } catch (IllegalArgumentException e) {
+ throw new CustomRuntimeException(
+ ErrorCode.INVALID_SUBJECT_NAME,
+ "Invalid subject name: " + subjectName
+ );
+ }
+ })
.toList();
}
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/presentation/examapplication/dto/UpdateSubjectRequest.java
between lines 11 and 15, the validatedSubject method currently calls
Subject::valueOf directly, which throws IllegalArgumentException for invalid
subject names. Modify the method to catch this exception for each subject
string, handle it gracefully by either collecting validation errors or throwing
a custom exception with a clear error message indicating which subject names are
invalid, instead of letting the exception propagate unhandled.

Comment on lines +25 to +32
@PutMapping("{examApplicationId}/subjects")
public ResponseEntity<ApiResponseWrapper<Void>> updateSubjects(
@PathVariable("examApplicationId") Long examApplicationId,
@RequestBody UpdateSubjectRequest request
) {
examApplicationService.updateSubjects(examApplicationId, request);
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "과목 수정을 완료했습니다."));
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add validation annotation to request body

To ensure the validation annotations in UpdateSubjectRequest are triggered, add @Valid annotation.

+import jakarta.validation.Valid;
+
 @PutMapping("{examApplicationId}/subjects")
 public ResponseEntity<ApiResponseWrapper<Void>> updateSubjects(
         @PathVariable("examApplicationId") Long examApplicationId,
-        @RequestBody UpdateSubjectRequest request
+        @RequestBody @Valid UpdateSubjectRequest request
 ) {
📝 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
@PutMapping("{examApplicationId}/subjects")
public ResponseEntity<ApiResponseWrapper<Void>> updateSubjects(
@PathVariable("examApplicationId") Long examApplicationId,
@RequestBody UpdateSubjectRequest request
) {
examApplicationService.updateSubjects(examApplicationId, request);
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "과목 수정을 완료했습니다."));
}
import jakarta.validation.Valid;
@PutMapping("{examApplicationId}/subjects")
public ResponseEntity<ApiResponseWrapper<Void>> updateSubjects(
@PathVariable("examApplicationId") Long examApplicationId,
@RequestBody @Valid UpdateSubjectRequest request
) {
examApplicationService.updateSubjects(examApplicationId, request);
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "과목 수정을 완료했습니다."));
}
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/presentation/examapplication/ExamApplicationController.java
around lines 25 to 32, the updateSubjects method's request body parameter lacks
the @Valid annotation, so validation annotations in UpdateSubjectRequest are not
triggered. Add the @Valid annotation before the @RequestBody annotation on the
request parameter to enable validation processing.

Comment on lines +34 to +43
@GetMapping("{examApplicationId}/exam-ticket")
public ResponseEntity<ApiResponseWrapper<ExamTicketResponse>> getExamTicket(
@RequestParam Long userId,
@PathVariable("examApplicationId") Long examApplicationId
) {
ExamTicketResponse response = examApplicationService.getExamTicket(userId,
examApplicationId);
return ResponseEntity.ok(
ApiResponseWrapper.success(HttpStatus.OK, "수험표 발급을 완료했습니다.", response));
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Security concern: userId as query parameter

Passing userId as a query parameter is unusual and potentially insecure. User identification should typically come from the authenticated session/token rather than being client-provided.

Consider:

  1. Getting userId from the security context (e.g., @AuthenticationPrincipal)
  2. If this is for admin purposes, ensure proper authorization checks are in place

🏁 Script executed:

#!/bin/bash
# Description: Check how authentication is handled in other controllers

# Search for authentication patterns in other controllers
rg -A 5 "@AuthenticationPrincipal|@PreAuthorize|SecurityContext" --glob "**/*Controller.java"

# Check if there's a security configuration
fd -e java SecurityConfig | xargs cat

Length of output: 18924


Enforce authentication and remove client-provided userId

The getExamTicket endpoint currently trusts a client-supplied userId query parameter, and your SecurityConfig permits all requests by default—allowing anyone to fetch any user’s exam ticket. To secure this:

• Remove the @RequestParam Long userId parameter.
• Retrieve the authenticated user’s ID from the security context (e.g. via @AuthenticationPrincipal or SecurityContextHolder).
• Restrict access to authenticated users by adding @PreAuthorize("isAuthenticated()") on the controller or changing authorizeHttpRequests to .anyRequest().authenticated().
• In examApplicationService.getExamTicket(...), verify that the requested examApplicationId belongs to the authenticated user.
• Audit other controllers that accept userId via @RequestParam (e.g. ProfileController, ApplicationController) and apply the same pattern.

Suggested diff for ExamApplicationController.java:

-    @GetMapping("{examApplicationId}/exam-ticket")
-    public ResponseEntity<ApiResponseWrapper<ExamTicketResponse>> getExamTicket(
-            @RequestParam Long userId,
-            @PathVariable("examApplicationId") Long examApplicationId
-    ) {
-        ExamTicketResponse response = examApplicationService.getExamTicket(userId,
-                examApplicationId);
+    @GetMapping("{examApplicationId}/exam-ticket")
+    @PreAuthorize("isAuthenticated()")
+    public ResponseEntity<ApiResponseWrapper<ExamTicketResponse>> getExamTicket(
+            @AuthenticationPrincipal UserPrincipal principal,
+            @PathVariable Long examApplicationId
+    ) {
+        Long userId = principal.getId();
+        ExamTicketResponse response = examApplicationService.getExamTicket(userId, examApplicationId);
         return ResponseEntity.ok(
                 ApiResponseWrapper.success(HttpStatus.OK, "수험표 발급을 완료했습니다.", response));
     }
📝 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
@GetMapping("{examApplicationId}/exam-ticket")
public ResponseEntity<ApiResponseWrapper<ExamTicketResponse>> getExamTicket(
@RequestParam Long userId,
@PathVariable("examApplicationId") Long examApplicationId
) {
ExamTicketResponse response = examApplicationService.getExamTicket(userId,
examApplicationId);
return ResponseEntity.ok(
ApiResponseWrapper.success(HttpStatus.OK, "수험표 발급을 완료했습니다.", response));
}
@GetMapping("{examApplicationId}/exam-ticket")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<ApiResponseWrapper<ExamTicketResponse>> getExamTicket(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long examApplicationId
) {
Long userId = principal.getId();
ExamTicketResponse response = examApplicationService.getExamTicket(userId, examApplicationId);
return ResponseEntity.ok(
ApiResponseWrapper.success(HttpStatus.OK, "수험표 발급을 완료했습니다.", response));
}
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/presentation/examapplication/ExamApplicationController.java
around lines 34 to 43, remove the @RequestParam Long userId parameter from the
getExamTicket method and instead obtain the authenticated user's ID from the
security context using @AuthenticationPrincipal or SecurityContextHolder. Add
@PreAuthorize("isAuthenticated()") to the controller or configure SecurityConfig
to require authentication for all requests. Update
examApplicationService.getExamTicket to verify that the examApplicationId
belongs to the authenticated user. Also, review other controllers like
ProfileController and ApplicationController for similar userId parameters and
apply the same authentication enforcement pattern.

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