Skip to content

Conversation

@chominju02
Copy link
Contributor

@chominju02 chominju02 commented Jul 16, 2025

✨ 구현한 기능

  • 알림톡 발송 message request 생성을 하는 stategy 구현
  • webClient 의존성 추가
  • Application 리팩토링을 위한 임시 구현 로직 추가
  • NotifyMessage 관련 dto들 추가
  • 알림톡 메세지 추가
  • NotifyService, NotifyPublisher, NotifyEventListener 구현
  • NotifyStatus, EventStatus 구현

📢 논의하고 싶은 내용

🎸 기타

Summary by CodeRabbit

  • New Features

    • Introduced a comprehensive notification system supporting various user events, including application completion, exam reminders, sign-up, inquiry answers, and refunds.
    • Added localized Korean notification templates for multiple scenarios.
    • Integrated support for scheduled and personalized messaging via AlimTalk and SMS, with dynamic content and action buttons.
    • Enhanced internationalization and message resource management.
    • Added reactive and asynchronous event handling for notifications.
  • Improvements

    • Updated configuration to support new notification features and message sources.
    • Standardized user phone numbers in test data for consistency.
  • Bug Fixes

    • None.
  • Documentation

    • Added new message resource files with detailed notification templates in Korean.

@chominju02 chominju02 self-assigned this Jul 16, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jul 16, 2025

Walkthrough

This update introduces a comprehensive notification system, including event-driven notification strategies, DTOs, and infrastructure for sending notifications via external services. It adds new repository interfaces and implementations for batch and projection queries, message templating with localization, and configuration for WebClient and message sources. Several new domain projections and DTOs support notification scenarios for applications, exams, inquiries, and refunds.

Changes

Files/Groups Change Summary
build.gradle Added spring-boot-starter-webflux dependency.
src/main/resources/application.yml, messages_ko.properties Added message source config, notification service properties, and Korean notification templates.
src/main/java/life/mosu/mosuserver/global/config/MessageSourceConfig.java, WebClientConfig.java Added configuration for message sources and WebClient.
src/main/java/life/mosu/mosuserver/global/annotation/ReactiveEventListener.java Added composed annotation for async event listeners.
src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java Added notification-related error codes.
src/main/java/life/mosu/mosuserver/global/initializer/DatabaseInitializer.java Set all dummy user phone numbers to a fixed value.
src/main/java/life/mosu/mosuserver/application/notify/NotifyService.java Added new notification service handling event-driven notifications.
src/main/java/life/mosu/mosuserver/presentation/notify/NotifyEventListener.java Added async event listener for notifications.
src/main/java/life/mosu/mosuserver/infra/notify/NotifyClientAdapter.java, LunaSoftNotifier.java, NotifyEventPublisher.java, NotifyProperties.java Introduced notification client adapter, LunaSoft notifier, event publisher, and properties.
src/main/java/life/mosu/mosuserver/infra/notify/NotifyEventTemplateGenerator.java Added message templating and notification request generation.
src/main/java/life/mosu/mosuserver/infra/notify/constant/NotifyConstants.java Added notification URL and template ID constants.
src/main/java/life/mosu/mosuserver/infra/notify/dto/… Added DTOs, enums, and records for notification events, requests, statuses, button URLs, and variable mapping for all notification types.
src/main/java/life/mosu/mosuserver/infra/notify/strategy/… Added notification strategies for application, exam reminders, inquiry answers, refunds, and sign-up events.
src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolJpaRepository.java Added projection queries for notification use cases.
src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolNotifyProjection.java, OneWeekNotifyProjection.java Added new projection records for notification data.
src/main/java/life/mosu/mosuserver/domain/refund/RefundJpaRepository.java, RefundNotifyProjection.java Added refund notification projection and repository method.
src/main/java/life/mosu/mosuserver/domain/school/SchoolQueryRepository.java, SchoolQueryRepositoryImpl.java Added custom repository for batch school queries.
src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java Added dependency on SchoolQueryRepositoryImpl, enhanced inline documentation, and added (commented) refactored batch logic.
src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java Added dependencies and commented notification logic for inquiry answers.
src/main/java/life/mosu/mosuserver/infra/respository/BulkRepository.java Added repository for bulk saving application schools (logic not fully implemented).

Sequence Diagram(s)

sequenceDiagram
    participant EventSource as Event Source
    participant NotifyEventListener
    participant NotifyService
    participant ApplicationContext
    participant ProfileRepository
    participant NotifyStrategy
    participant NotifyClientAdapter

    EventSource->>NotifyEventListener: Publish NotifyEvent
    NotifyEventListener->>NotifyService: notify(event)
    NotifyService->>ApplicationContext: getNotifyStrategy(event.status)
    ApplicationContext-->>NotifyService: NotifyStrategy
    NotifyService->>ProfileRepository: findByUserId(event.userId)
    ProfileRepository-->>NotifyService: Profile (with phone number)
    NotifyService->>NotifyStrategy: apply(phoneNumber, event)
    NotifyStrategy-->>NotifyService: NotifyEventRequest
    NotifyService->>NotifyClientAdapter: send(NotifyEventRequest)
    NotifyClientAdapter-->>NotifyService: (async send)
Loading

Possibly related PRs

  • mosu-dev/mosu-server#64: Refactored ApplicationService.apply with batch school condition queries, directly related to the new batch query and notification logic introduced here.

Suggested labels

우선순위 중

Suggested reviewers

  • chominju02
  • wlgns12370
  • jbh010204

Poem

A rabbit hops with news to send,
Notifications now ascend!
With templates, buttons, and a ping,
Reminders, refunds, all take wing.
Korean messages, WebClient too—
Hopping fast to message you!
🐇📱✨

✨ Finishing Touches
  • 📝 Generate Docstrings

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

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 introduces a new Alimtalk notification feature, enabling the system to send automated messages for various user interactions and important events. It establishes an event-driven notification framework, integrates with an external messaging API using WebClient, and lays the groundwork for future performance optimizations in application data handling.

Highlights

  • Alimtalk (KakaoTalk Notification) Integration: I've implemented a comprehensive Alimtalk notification system, allowing the application to send various types of messages such as application confirmations, exam reminders (1-week, 3-day, 1-day before), refund confirmations, inquiry answer notifications, and sign-up welcome messages. This is achieved through a new event-driven architecture and a strategy pattern for message generation.
  • Asynchronous Event Handling: A new @ReactiveEventListener annotation has been introduced, combining @Async and @EventListener, to ensure that notification events are processed asynchronously, preventing blocking operations and improving application responsiveness.
  • WebClient and External API Communication: The spring-boot-starter-webflux dependency was added, and WebClient is now configured to handle external API calls to the Alimtalk service, facilitating efficient and non-blocking communication for sending notifications.
  • Application Service Refactoring (Initial Stage): I've included a commented-out section in ApplicationService that outlines a refactored approach for handling school applications using a new SchoolQueryRepository with QueryDSL. This indicates a future direction for optimizing data retrieval, though the bulk save functionality is not yet fully implemented.
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

@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

This pull request introduces a notification feature using Alimtalk. The implementation leverages the Strategy pattern for handling different notification types and employs asynchronous event handling. I've identified a few areas for improvement, including a critical bug in a repository query that will cause runtime failures. I've also noted some opportunities to improve error handling, enhance maintainability by cleaning up work-in-progress code, and boost efficiency by using projections for database queries. Addressing these points will significantly increase the robustness and quality of this new feature.

Comment on lines +8 to +18
@Query("""
SELECT
p.paymentKey AS paymentKey,
a.examDate AS examDate,
p.paymentMethod AS paymentMethod,
r.reason AS 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
""")

Choose a reason for hiding this comment

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

critical

The query for findRefundByApplicationSchoolId is missing a.schoolName in the SELECT clause. The RefundNotifyProjection record expects this field, and its absence will cause a mapping error at runtime. Please add a.schoolName AS schoolName to the projection.

    @Query("""
                SELECT
                    p.paymentKey AS paymentKey,
                    a.examDate AS examDate,
                    a.schoolName AS schoolName,
                    p.paymentMethod AS paymentMethod,
                    r.reason AS 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
            """)

inquiryEntity.updateStatusToComplete();

ProfileJpaEntity profile = profileJpaRepository.findByUserId(inquiryEntity.getUserId())
.orElseThrow(() -> new RuntimeException(""));

Choose a reason for hiding this comment

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

high

Throwing a generic RuntimeException with an empty message is not informative. It's better to use the existing CustomRuntimeException with a specific ErrorCode to provide more context. Consider creating and using an appropriate error code (e.g., PROFILE_NOT_FOUND in ErrorCode.java) to represent this specific failure case.

Suggested change
.orElseThrow(() -> new RuntimeException(""));
.orElseThrow(() -> new CustomRuntimeException(ErrorCode.PROFILE_NOT_FOUND));

RefundNotifyProjection projection = refundJpaRepository.findRefundByApplicationSchoolId(
event.id());

//TODO: 가격 추가

Choose a reason for hiding this comment

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

high

There's a TODO here to add the price, and the RefundNotifyVariablesDto has the amount field commented out. However, the message template in messages_ko.properties uses a #{refundAmount} placeholder. This will lead to an incomplete message being sent to the user. Please implement the logic to fetch and pass the refund amount to ensure the notification is correct.

Comment on lines 31 to 33
ApplicationSchoolJpaEntity applicationSchool = applicationSchoolJpaRepository.findById(
event.id()).orElseThrow(
() -> new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_NOT_FOUND));

Choose a reason for hiding this comment

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

medium

Fetching the entire ApplicationSchoolJpaEntity object when only a few fields are needed is inefficient and can lead to performance issues. Consider creating a dedicated projection to fetch only the required fields (examDate, examinationNumber, schoolName). This would also make the code's intent clearer.

This feedback also applies to Exam3DayBeforeNotifyStrategy.


private final MessageSource messageSource;
private final NotifyEventTemplate template;
private final InquiryJpaRepository inquiryJpaRepository;

Choose a reason for hiding this comment

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

medium

The InquiryJpaRepository dependency is injected but not used within this class. Please remove it to keep the code clean and avoid unnecessary dependencies.

Comment on lines +29 to +30
String alimTalkContent = messageSource.getMessage("notify.signup.complete.alimtalk", null,
Locale.KOREA);

Choose a reason for hiding this comment

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

medium

This strategy uses MessageSource directly to get the message content, while other strategies use the NotifyEventTemplate which encapsulates MessageSource logic. For consistency across strategies, it would be better to use template.getProcessedMessage(...) here as well, even if there are no variables to replace.

Suggested change
String alimTalkContent = messageSource.getMessage("notify.signup.complete.alimtalk", null,
Locale.KOREA);
String alimTalkContent = template.getProcessedMessage("notify.signup.complete.alimtalk",
java.util.Collections.emptyMap());

Comment on lines +17 to +23
public void saveAllApplicationSchools(List<ApplicationSchoolJpaEntity> applicationSchools) {
String sql = """
INSERT INTO application_school
(user_id, application_id, school_id, school_name, area, exam_date)
VALUES (?, ?, ?, ?, ?, ?)
""";
}

Choose a reason for hiding this comment

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

medium

The saveAllApplicationSchools method is incomplete. It defines an SQL string but doesn't execute any database operation with JdbcTemplate. This is currently dead code. Please either complete the implementation (e.g., using jdbcTemplate.batchUpdate) or remove this method if it's not intended to be used yet.

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: 22

🧹 Nitpick comments (28)
src/main/java/life/mosu/mosuserver/domain/refund/RefundNotifyProjection.java (1)

6-13: Remove TODO or create a tracked issue

The inline // TODO: 환불 금액 추가 will be easy to forget. Convert to a tracked issue or implement the field now to avoid another projection change (which triggers recompilation & migration work).

Example:

-        PaymentMethod paymentMethod,
-        String reason
-        // TODO: 환불 금액 추가
+        PaymentMethod paymentMethod,
+        String reason,
+        BigDecimal refundAmount // new field
src/main/java/life/mosu/mosuserver/presentation/notify/dto/NotifyButtonUrls.java (1)

14-16: Consider using List.of() for better performance.

While the current implementation works correctly, List.of() provides better performance than Arrays.asList() for creating immutable lists in modern Java.

public static NotifyButtonUrls of(NotifyButtonUrl... btns) {
-    return new NotifyButtonUrls(Arrays.asList(btns));
+    return new NotifyButtonUrls(List.of(btns));
}
src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventSuccessMessageDto.java (1)

16-27: Remove redundant static factory method.

The create() method is unnecessary since Java records automatically provide a constructor with the same signature. This adds unnecessary code complexity.

-public static NotifyEventSuccessMessageDto create(
-        Integer no,
-        String telNum,
-        String msgContent,
-        String smsContent,
-        String useSms,
-        NotifyButtonUrls btnUrls
-) {
-    return new NotifyEventSuccessMessageDto(
-            no, telNum, msgContent, smsContent, useSms, btnUrls
-    );
-}

Simply use the record constructor directly:

var dto = new NotifyEventSuccessMessageDto(no, telNum, msgContent, smsContent, useSms, btnUrls);
src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventRemindMessageDto.java (1)

17-33: Remove redundant static factory method.

Similar to NotifyEventSuccessMessageDto, this static factory method is unnecessary since Java records provide constructors automatically.

-public static NotifyEventRemindMessageDto create(
-        Integer no,
-        String telNum,
-        LocalDateTime reserveTime,
-        String msgContent,
-        String smsContent,
-        NotifyButtonUrls btnUrls
-) {
-    return new NotifyEventRemindMessageDto(
-            no,
-            telNum,
-            reserveTime,
-            msgContent,
-            smsContent,
-            btnUrls
-    );
-}
src/main/resources/application.yml (1)

84-84: Fix missing newline at end of file.

Add a newline character at the end of the file to comply with YAML formatting standards.

kakao:
  channel-id: ${KAKAO_CHANNEL_ID}
+
src/main/java/life/mosu/mosuserver/presentation/notify/dto/NotifyEventRequest.java (1)

8-11: Ensure consistent naming conventions in JSON properties.

The JSON property names use inconsistent naming conventions:

  • userId uses camelCase
  • api_key and template_id use snake_case

Consider standardizing to either camelCase or snake_case:

public record NotifyEventRequest(
-       @JsonProperty("userId") String userId,
-       @JsonProperty("api_key") String apiKey,
-       @JsonProperty("template_id") Integer templateId,
+       @JsonProperty("user_id") String userId,
+       @JsonProperty("api_key") String apiKey,
+       @JsonProperty("template_id") Integer templateId,
        @JsonProperty("messages") List<Map<String, Object>> messages
) {

Or alternatively, use camelCase throughout:

public record NotifyEventRequest(
        @JsonProperty("userId") String userId,
-       @JsonProperty("api_key") String apiKey,
-       @JsonProperty("template_id") Integer templateId,
+       @JsonProperty("apiKey") String apiKey,
+       @JsonProperty("templateId") Integer templateId,
        @JsonProperty("messages") List<Map<String, Object>> messages
) {
src/main/java/life/mosu/mosuserver/domain/school/SchoolQueryRepository.java (1)

7-12: Consider adding method documentation and parameter naming.

The interface would benefit from documentation explaining the method's purpose and parameter structure.

Add JavaDoc documentation:

public interface SchoolQueryRepository {

+   /**
+    * Finds school IDs that match the given conditions.
+    * 
+    * @param conditions List of triples containing (schoolName, area, examDate)
+    * @return List of school IDs matching the conditions
+    */
    List<Long> findSchoolsByConditions(
            List<Triple<String, Area, LocalDate>> conditions);

Also consider using a more descriptive parameter name:

    List<Long> findSchoolsByConditions(
-           List<Triple<String, Area, LocalDate>> conditions);
+           List<Triple<String, Area, LocalDate>> schoolConditions);
src/main/java/life/mosu/mosuserver/infra/notify/dto/Exam3DayBeforeNotifyVariablesDto.java (1)

6-10: Consider creating a base interface for notification variable DTOs.

Since multiple DTOs follow the same pattern with a toMap() method, consider creating a common interface to ensure consistency across the notification system.

Create a base interface:

package life.mosu.mosuserver.infra.notify.dto;

import java.util.Map;

public interface NotifyVariablesDto {
    Map<String, String> toMap();
}

Then implement it in your DTOs:

-public record Exam3DayBeforeNotifyVariablesDto(
+public record Exam3DayBeforeNotifyVariablesDto(
        LocalDate examDate,
        String examinationNumber,
        String schoolName
-) {
+) implements NotifyVariablesDto {

This would provide better type safety and consistency across all notification variable DTOs.

src/main/java/life/mosu/mosuserver/infra/notify/dto/Exam1WeekBeforeNotifyVariablesDto.java (1)

12-18: Consider date formatting and null safety in toMap() method.

The implementation is functionally correct, but consider these improvements:

  1. examDate.toString() produces ISO format (YYYY-MM-DD) which might not be user-friendly
  2. No null checks could cause NPE if any field is null

Consider this enhanced version:

    public Map<String, String> toMap() {
+       if (examDate == null || paymentKey == null || schoolName == null) {
+           throw new IllegalStateException("All fields must be non-null");
+       }
        return Map.of(
-               "examDate", examDate.toString(),
+               "examDate", examDate.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일")),
                "paymentKey", paymentKey,
                "schoolName", schoolName
        );
    }
src/main/java/life/mosu/mosuserver/infra/notify/dto/Exam1DayBeforeNotifyVariablesDto.java (1)

12-18: Same date formatting and null safety concerns as Exam1WeekBeforeNotifyVariablesDto.

This DTO has identical issues to the previous one regarding date formatting and null safety.

Consider applying the same improvements as suggested for Exam1WeekBeforeNotifyVariablesDto:

    public Map<String, String> toMap() {
+       if (examDate == null || examinationNumber == null || schoolName == null) {
+           throw new IllegalStateException("All fields must be non-null");
+       }
        return Map.of(
-               "examDate", examDate.toString(),
+               "examDate", examDate.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일")),
                "examinationNumber", examinationNumber,
                "schoolName", schoolName
        );
    }

Additionally, consider creating a common interface or abstract class for notification DTOs to ensure consistency:

public interface NotifyVariablesDto {
    Map<String, String> toMap();
}
src/main/java/life/mosu/mosuserver/global/config/MessageSourceConfig.java (1)

11-17: Standard MessageSource configuration looks good.

The implementation follows Spring best practices for internationalization. The configuration is correct and appropriate for Korean message support.

Optional enhancement - consider adding cache seconds for better performance:

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("classpath:messages");
        messageSource.setDefaultEncoding("UTF-8");
+       messageSource.setCacheSeconds(60); // Cache for 1 minute
        return messageSource;
    }
src/main/java/life/mosu/mosuserver/infra/notify/dto/RefundNotifyVariablesDto.java (1)

15-24: Consider null safety for the toMap method.

The current implementation doesn't handle potential null values. Consider adding null checks or using a more robust approach to handle null fields.

public Map<String, String> toMap() {
-    return Map.of(
-            "paymentKey", paymentKey,
-            "examDate", examDate.toString(),
-            "schoolName", schoolName,
-            "paymentMethod", paymentMethod,
-            "reason", reason
-    );
+    Map<String, String> map = new HashMap<>();
+    if (paymentKey != null) map.put("paymentKey", paymentKey);
+    if (examDate != null) map.put("examDate", examDate.toString());
+    if (schoolName != null) map.put("schoolName", schoolName);
+    if (paymentMethod != null) map.put("paymentMethod", paymentMethod);
+    if (reason != null) map.put("reason", reason);
+    return Map.copyOf(map);
}

Alternatively, if null values are not expected, consider adding @NonNull annotations to the record fields.

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

22-28: Add null safety for individual condition elements.

The method doesn't validate that the Triple elements are not null, which could cause issues in the query building.

for (Triple<String, Area, LocalDate> cond : conditions) {
+    if (cond.getLeft() == null || cond.getMiddle() == null || cond.getRight() == null) {
+        continue; // Skip invalid conditions
+    }
    builder.or(
            QSchoolJpaEntity.schoolJpaEntity.schoolName.eq(cond.getLeft())
                    .and(QSchoolJpaEntity.schoolJpaEntity.area.eq(cond.getMiddle()))
                    .and(QSchoolJpaEntity.schoolJpaEntity.examDate.eq(cond.getRight()))
    );
}

30-35: Consider performance implications for large condition sets.

For very large condition lists, this approach might result in performance issues due to the large number of OR conditions. Consider alternative approaches for bulk operations.

For better performance with large datasets, consider:

  1. Using IN clauses with composite keys if the database supports it
  2. Implementing pagination for large result sets
  3. Adding database indexes on the combination of (schoolName, area, examDate)
  4. Using EXISTS subqueries for better performance in some cases

You might also want to add logging or monitoring to track query performance:

+    log.debug("Querying schools with {} conditions", conditions.size());
    return queryFactory
            .select(QSchoolJpaEntity.schoolJpaEntity.id)
            .from(QSchoolJpaEntity.schoolJpaEntity)
            .where(builder)
            .fetch();
src/main/java/life/mosu/mosuserver/infra/notify/service/NotifyService.java (1)

36-38: Consider the implications of fire-and-forget pattern.

The subscribe() call makes this a fire-and-forget operation. While this might be appropriate for notifications, consider if you need to handle completion or errors differently.

For better error handling and monitoring, consider:

        .doOnTerminate(() ->
                log.info("알림톡 전송 완료: {}", request))
-        .subscribe();
+        .subscribe(
+                response -> log.debug("알림톡 전송 성공: {}", response),
+                error -> {
+                    log.error("알림톡 전송 실패: {}", error.getMessage(), error);
+                    // Consider adding metrics or alerting here
+                }
+        );

Or if you need to handle the result:

-        .subscribe();
+        .subscribe(
+                response -> handleSuccess(response, request),
+                error -> handleError(error, request)
+        );
src/main/java/life/mosu/mosuserver/infra/notify/strategy/InquiryAnswerNotifyStrategy.java (1)

28-47: Consider optimizing and verify SMS content handling.

The implementation is functionally correct, but has a few considerations:

  1. SMS content is empty: Line 45 sets SMS content to an empty string. Verify this is intentional.
  2. Template processing: The alimtalk template processing looks correct.
  3. Button URL configuration: Using the same URL for both button text and URL might be confusing.

Consider this improvement for the button URL:

-        NotifyButtonUrls btnUrls = NotifyButtonUrls.of(
-                NotifyButtonUrl.of(INQUIRY, INQUIRY)
-        );
+        NotifyButtonUrls btnUrls = NotifyButtonUrls.of(
+                NotifyButtonUrl.of("문의 확인", INQUIRY)
+        );

Verify that empty SMS content is the intended behavior for inquiry answer notifications.

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

11-11: Remove commented-out code or implement it properly.

The commented-out method existsByUserIdAndSchoolIds should either be removed if not needed or implemented if it's part of the planned refactoring mentioned in ApplicationService.java.


24-30: Consider JOIN consistency and null handling.

The query uses LEFT JOIN which allows for null payment keys, but this might not be the intended behavior for notification purposes. Consider whether an INNER JOIN would be more appropriate if payments are required for notifications.

-    @Query("""
-                SELECT p.paymentKey, a.examDate, a.schoolName, a.lunch
-                FROM ApplicationSchoolJpaEntity a
-                LEFT JOIN PaymentJpaEntity p ON a.id = p.applicationSchoolId
-                WHERE a.id = :applicationSchoolId
-            """)
+    @Query("""
+                SELECT p.paymentKey, a.examDate, a.schoolName, a.lunch
+                FROM ApplicationSchoolJpaEntity a
+                INNER JOIN PaymentJpaEntity p ON a.id = p.applicationSchoolId
+                WHERE a.id = :applicationSchoolId
+            """)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/SignUpNotifyStrategy.java (2)

29-30: Consider externalizing the locale configuration.

The hardcoded Locale.KOREA should be extracted to a configuration property or constant to improve maintainability and internationalization support.

-        String alimTalkContent = messageSource.getMessage("notify.signup.complete.alimtalk", null,
-                Locale.KOREA);
+        String alimTalkContent = messageSource.getMessage("notify.signup.complete.alimtalk", null,
+                Locale.getDefault());

32-34: Verify button URL appropriateness.

Using HOMEPAGE for both the URL and button text might not provide the best user experience. Consider using a more descriptive button text.

-        NotifyButtonUrls btnUrls = NotifyButtonUrls.of(
-                NotifyButtonUrl.of(HOMEPAGE, HOMEPAGE)
-        );
+        NotifyButtonUrls btnUrls = NotifyButtonUrls.of(
+                NotifyButtonUrl.of("홈페이지", HOMEPAGE)
+        );
src/main/java/life/mosu/mosuserver/infra/notify/strategy/RefundNotifyStrategy.java (1)

30-30: Address the TODO comment.

The TODO comment indicates that refund amount should be added to the notification. This is crucial information for users and should be prioritized.

Would you like me to help implement the refund amount field in the projection and DTO, or should this be tracked as a separate issue?

src/main/java/life/mosu/mosuserver/presentation/notify/dto/NotifyStatus.java (1)

19-20: Consider adding getter methods for better encapsulation.

While Lombok's @RequiredArgsConstructor creates the constructor, consider adding explicit getter methods for better API clarity and potential future enhancements.

+    public EventStatus getStatus() {
+        return status;
+    }
+
+    public String getMessage() {
+        return message;
+    }
src/main/java/life/mosu/mosuserver/infra/notify/NotifyEventTemplate.java (2)

25-31: Improve variable substitution performance and add null safety checks.

The current implementation uses StringUtils.replace in a loop which can be inefficient with many variables. Additionally, there's no null safety for the variables map or validation that all placeholders are replaced.

Consider this improved implementation:

 public String getProcessedMessage(String code, Map<String, String> variables) {
     String message = messageSource.getMessage(code, null, Locale.KOREA);
+    if (variables == null || variables.isEmpty()) {
+        return message;
+    }
+    
     for (Map.Entry<String, String> entry : variables.entrySet()) {
+        if (entry.getKey() == null || entry.getValue() == null) {
+            continue;
+        }
         message = StringUtils.replace(message, "#{" + entry.getKey() + "}", entry.getValue());
     }
     return message;
 }

26-26: Consider making locale configurable instead of hardcoded.

The locale is hardcoded to Locale.KOREA. Consider making this configurable or parameterizing it to support internationalization.

-String message = messageSource.getMessage(code, null, Locale.KOREA);
+String message = messageSource.getMessage(code, null, Locale.getDefault());

Or inject a configurable locale property.

src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam3DayBeforeNotifyStrategy.java (1)

46-48: Consider making notification scheduling time configurable.

The hardcoded 8:00 AM scheduling time should be configurable through application properties to allow for operational flexibility.

+@Value("${notification.reminder.time.hour:8}")
+private int reminderHour;
+@Value("${notification.reminder.time.minute:0}")
+private int reminderMinute;

 LocalDateTime reserveTime = applicationSchool.getExamDate()
         .minusDays(3)
-        .atTime(8, 0);
+        .atTime(reminderHour, reminderMinute);
src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1DayBeforeNotifyStrategy.java (1)

46-48: Same scheduling time configurability issue as other strategies.

The hardcoded 8:00 AM scheduling time should be configurable. This issue appears across multiple exam reminder strategies.

Apply the same configuration approach as suggested for Exam3DayBeforeNotifyStrategy.

src/main/java/life/mosu/mosuserver/infra/notify/strategy/ApplicationNotifyStrategy.java (1)

31-33: Consider adding null safety for projection fields.

The projection fields (paymentKey(), examDate(), schoolName(), lunch().getLunchName()) should be validated for null values before use, especially the nested lunch().getLunchName() call.

+if (projection == null) {
+    throw new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_NOT_FOUND);
+}
+
 ApplicationNotifyVariablesDto dto = new ApplicationNotifyVariablesDto(
         projection.paymentKey(), projection.examDate(), projection.schoolName(),
-        projection.lunch().getLunchName());
+        projection.lunch() != null ? projection.lunch().getLunchName() : "");
src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1WeekBeforeNotifyStrategy.java (1)

42-44: Apply same scheduling time configurability as other strategies.

The hardcoded 8:00 AM scheduling time should be configurable, consistent with the other exam reminder strategies.

Apply the same configuration approach as suggested for the other exam reminder strategies.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 351518b and 32009b9.

📒 Files selected for processing (45)
  • build.gradle (1 hunks)
  • src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java (3 hunks)
  • src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java (3 hunks)
  • src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolJpaRepository.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolNotifyProjection.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/applicationschool/OneWeekNotifyProjection.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/refund/RefundJpaRepository.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/domain/refund/RefundNotifyProjection.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 (1 hunks)
  • src/main/java/life/mosu/mosuserver/global/annotation/ReactiveEventListener.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/global/config/MessageSourceConfig.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/global/config/WebClientConfig.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/NotifyEvent.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/NotifyEventPublisher.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/NotifyEventTemplate.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/constant/NotifyConstants.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/ApplicationNotifyVariablesDto.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/Exam1DayBeforeNotifyVariablesDto.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/Exam1WeekBeforeNotifyVariablesDto.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/Exam3DayBeforeNotifyVariablesDto.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/InquiryAnswerNotifyVariablesDto.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventRemindMessageDto.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventSuccessMessageDto.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/RefundNotifyVariablesDto.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/service/NotifyService.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/ApplicationNotifyStrategy.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1DayBeforeNotifyStrategy.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1WeekBeforeNotifyStrategy.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam3DayBeforeNotifyStrategy.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/InquiryAnswerNotifyStrategy.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/NotifyStrategy.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/NotifyStrategyMap.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/RefundNotifyStrategy.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/SignUpNotifyStrategy.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/respository/BulkRepository.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/notify/NotifyEventListener.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/notify/dto/EventStatus.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/notify/dto/NotifyButtonUrl.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/notify/dto/NotifyButtonUrls.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/notify/dto/NotifyEventRequest.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/notify/dto/NotifyStatus.java (1 hunks)
  • src/main/resources/application.yml (2 hunks)
  • src/main/resources/messages_ko.properties (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (8)
src/main/java/life/mosu/mosuserver/presentation/notify/NotifyEventListener.java (1)
src/main/java/life/mosu/mosuserver/infra/notify/NotifyEventPublisher.java (1)
  • Component (7-16)
src/main/java/life/mosu/mosuserver/domain/school/SchoolQueryRepositoryImpl.java (1)
src/main/java/life/mosu/mosuserver/infra/respository/BulkRepository.java (1)
  • Repository (10-25)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/SignUpNotifyStrategy.java (2)
src/main/java/life/mosu/mosuserver/infra/notify/constant/NotifyConstants.java (1)
  • NotifyConstants (3-23)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/InquiryAnswerNotifyStrategy.java (1)
  • Component (20-51)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam3DayBeforeNotifyStrategy.java (4)
src/main/java/life/mosu/mosuserver/infra/notify/constant/NotifyConstants.java (1)
  • NotifyConstants (3-23)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1DayBeforeNotifyStrategy.java (1)
  • Component (22-62)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1WeekBeforeNotifyStrategy.java (1)
  • Component (20-57)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/ApplicationNotifyStrategy.java (1)
  • Component (19-53)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/ApplicationNotifyStrategy.java (3)
src/main/java/life/mosu/mosuserver/infra/notify/constant/NotifyConstants.java (1)
  • NotifyConstants (3-23)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/SignUpNotifyStrategy.java (1)
  • Component (18-42)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/RefundNotifyStrategy.java (1)
  • Component (18-55)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1DayBeforeNotifyStrategy.java (3)
src/main/java/life/mosu/mosuserver/infra/notify/constant/NotifyConstants.java (1)
  • NotifyConstants (3-23)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1WeekBeforeNotifyStrategy.java (1)
  • Component (20-57)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam3DayBeforeNotifyStrategy.java (1)
  • Component (22-64)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1WeekBeforeNotifyStrategy.java (2)
src/main/java/life/mosu/mosuserver/infra/notify/constant/NotifyConstants.java (1)
  • NotifyConstants (3-23)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1DayBeforeNotifyStrategy.java (1)
  • Component (22-62)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/InquiryAnswerNotifyStrategy.java (4)
src/main/java/life/mosu/mosuserver/infra/notify/constant/NotifyConstants.java (1)
  • NotifyConstants (3-23)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/ApplicationNotifyStrategy.java (1)
  • Component (19-53)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/SignUpNotifyStrategy.java (1)
  • Component (18-42)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/RefundNotifyStrategy.java (1)
  • Component (18-55)
🪛 YAMLlint (1.37.1)
src/main/resources/application.yml

[error] 84-84: no new line character at the end of file

(new-line-at-end-of-file)

🔇 Additional comments (26)
build.gradle (1)

88-90: Beware of dual‐stack servlet & reactive starters

Adding spring-boot-starter-webflux while keeping spring-boot-starter-web pulls in both the servlet (Tomcat) and reactive (Netty) stacks.
Spring Boot will default to servlet and silently skip Netty auto-configuration, so the extra dependency:

  1. Bloats the artifact size.
  2. Creates the risk of classpath conflicts (e.g., WebFilter vs Filter, WebExceptionHandler vs ExceptionHandlerExceptionResolver).
  3. Provides little benefit if you only need WebClient, because WebClient can be obtained from spring-boot-starter-webflux alone.

Consider one of the following:

-    implementation 'org.springframework.boot:spring-boot-starter-web'
-    ...
-    implementation 'org.springframework.boot:spring-boot-starter-webflux'
+    // Option A – servlet stack only, use RestTemplate or bring `spring-webflux` as *compileOnly* for WebClient
+    implementation 'org.springframework.boot:spring-boot-starter-web'
+    compileOnly 'org.springframework:spring-webflux'
+
+    // Option B – go fully reactive (preferred if you plan to use WebFlux controllers)
+    // implementation 'org.springframework.boot:spring-boot-starter-webflux'

Pick one stack to avoid surprises at runtime.

Run ./gradlew dependencies | grep "spring-boot-starter-web" to confirm duplicate transitive trees.

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

6-11: Confirm projection aliases match your JPQL

Spring projection mapping is name-based. Ensure the JPQL in ApplicationSchoolJpaRepository#findPaymentByApplicationSchoolId aliases columns exactly to:

paymentKey, examDate, schoolName, lunch

Otherwise Spring will fail to bind the record constructor at runtime.

src/main/java/life/mosu/mosuserver/presentation/notify/dto/NotifyButtonUrls.java (1)

18-20: Clean implementation using streams.

The method properly delegates to NotifyButtonUrl.toMap() and uses streams effectively for conversion.

src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventSuccessMessageDto.java (1)

30-39: Proper serialization implementation.

The toMap() method correctly handles field name mapping and nested object conversion for external API integration.

src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventRemindMessageDto.java (1)

42-42: Clarify hardcoded SMS usage value.

The hardcoded "use_sms": "1" differs from NotifyEventSuccessMessageDto which has a configurable useSms field. This inconsistency suggests different SMS behavior for reminders.

Please verify if this hardcoded value is intentional for reminder messages or if it should be configurable like success messages.

src/main/java/life/mosu/mosuserver/presentation/notify/dto/EventStatus.java (1)

3-5: Clean enum implementation.

The enum provides clear, descriptive values for event status tracking and follows Java naming conventions.

src/main/resources/application.yml (2)

49-51: Proper internationalization configuration.

The Spring messages configuration correctly sets up the message source for the notification templates.


77-84: Good security practice with environment variables.

The configuration properly externalizes sensitive information like API keys and user credentials using environment variables.

src/main/java/life/mosu/mosuserver/infra/notify/strategy/NotifyStrategy.java (1)

6-9: LGTM! Clean strategy interface design.

The interface follows the strategy pattern effectively, providing a clear contract for different notification strategies. The method signature is well-defined and the package structure is logical.

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

5-11: LGTM! Well-designed projection record.

The record effectively encapsulates the necessary data for one-week exam notifications. The field types are appropriate and the immutable nature of records is perfect for data projections.

src/main/java/life/mosu/mosuserver/infra/notify/dto/InquiryAnswerNotifyVariablesDto.java (1)

5-14: LGTM! Effective DTO design for notification templating.

The record provides a clean abstraction for inquiry answer notification variables. The toMap() method is well-implemented and the key mapping is consistent with the domain context.

src/main/java/life/mosu/mosuserver/infra/notify/NotifyEvent.java (1)

5-15: LGTM! Clean event record design.

The record effectively encapsulates notification event data with appropriate field types. The static factory method follows established patterns and maintains consistency with the codebase conventions.

src/main/java/life/mosu/mosuserver/infra/notify/NotifyEventPublisher.java (1)

7-16: LGTM! Well-structured event publisher component.

The class effectively wraps Spring's ApplicationEventPublisher, providing a clean abstraction for notification event publishing. The use of dependency injection and the single responsibility principle is commendable.

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

86-90: LGTM! Well-structured error codes for notification system.

The new error codes follow the existing naming conventions and provide clear Korean messages. The HTTP status codes are appropriate for the error scenarios.

src/main/java/life/mosu/mosuserver/domain/refund/RefundJpaRepository.java (1)

8-19: Add missing schoolName to query and verify single‐result expectation

The custom JPQL projection omits the schoolName field declared in RefundNotifyProjection, and there’s no uniqueness constraint on applicationSchoolId in RefundJpaEntity, so this method may return multiple rows and throw an exception.

• File to update: src/main/java/life/mosu/mosuserver/domain/refund/RefundJpaRepository.java (lines 8–19)
• Projection interface exists: RefundNotifyProjection defines String getSchoolName()
• Entity check: applicationSchoolId has no unique = true or @UniqueConstraint

Apply this diff to include the missing field:

    @Query("""
                SELECT
                    p.paymentKey AS paymentKey,
                    a.examDate AS examDate,
+                   a.schoolName AS schoolName,
                    p.paymentMethod AS paymentMethod,
                    r.reason AS 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
            """)
    RefundNotifyProjection findRefundByApplicationSchoolId(Long applicationSchoolId);

Next steps:

  • Confirm in your domain that each applicationSchoolId can have at most one refund.
  • If multiple refunds per application are possible, change the return type to List<RefundNotifyProjection> or add a dialect-specific limit clause.
src/main/java/life/mosu/mosuserver/infra/notify/strategy/InquiryAnswerNotifyStrategy.java (1)

22-26: LGTM: Clean strategy implementation with proper dependency injection.

The class follows the strategy pattern correctly with appropriate Spring annotations and constructor injection.

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

7-9: LGTM: Proper dependency injection for notification functionality.

The new dependencies are correctly added to support the notification publishing feature.

Also applies to: 12-13, 18-18, 31-33

src/main/java/life/mosu/mosuserver/infra/notify/constant/NotifyConstants.java (1)

3-23: LGTM: Well-structured constants class with proper design.

The class follows Java best practices for utility classes:

  • Final class with private constructor
  • Constants are logically grouped (URLs vs Template IDs)
  • Consistent naming conventions
  • Proper public static final declarations
src/main/java/life/mosu/mosuserver/infra/notify/strategy/NotifyStrategyMap.java (1)

10-42: Excellent strategy pattern implementation with optimal performance.

This implementation demonstrates several best practices:

  1. Performance optimization: Uses EnumMap for optimal enum key performance
  2. Comprehensive coverage: Maps all notification statuses to their corresponding strategies
  3. Proper error handling: Throws appropriate custom exception when strategy not found
  4. Clean design: Constructor injection ensures all strategies are available at initialization

The strategy pattern is well-implemented and provides a clean, maintainable way to handle different notification types.

src/main/resources/messages_ko.properties (1)

1-132: LGTM: Comprehensive Korean message templates with proper formatting.

The message templates are well-structured with:

  1. Consistent placeholder syntax: Uses #{variable} format throughout
  2. Proper line continuation: Backslash continuation for long messages
  3. Unicode encoding: Proper Korean character encoding
  4. Complete coverage: Templates for all notification scenarios (exam, signup, inquiry, refund)
  5. Channel-specific formatting: Separate templates for alimtalk and SMS where appropriate

The templates provide clear, informative content that would be helpful for users receiving notifications.

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

32-38: LGTM - Consistent JOIN usage.

The INNER JOIN usage here is consistent with the expected behavior where payment is required for the one-week notification.

src/main/java/life/mosu/mosuserver/infra/notify/strategy/RefundNotifyStrategy.java (1)

36-41: LGTM - Dual content creation is appropriate.

Creating both AlimTalk and SMS content is good practice for notification fallback scenarios.

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

47-74: LGTM - Improved code documentation.

The inline comments significantly improve code readability and make the application process clearer for future maintainers.

src/main/java/life/mosu/mosuserver/presentation/notify/dto/NotifyStatus.java (1)

8-17: Well-structured enum with clear categorization.

The enum is well-organized with logical grouping of SUCCESS and INFO statuses. The descriptive messages are appropriate and provide clear context for each notification type.

src/main/java/life/mosu/mosuserver/infra/notify/NotifyEventTemplate.java (1)

33-57: LGTM - Template creation methods are well-structured.

The template creation methods for success and reminder notifications are clean and follow a consistent pattern. The use of builder pattern with DTOs promotes maintainability.

src/main/java/life/mosu/mosuserver/infra/notify/strategy/ApplicationNotifyStrategy.java (1)

42-45: LGTM - Button URL configuration is appropriate for application notifications.

The button URLs (WARNING and MYPAGE) are appropriate for application completion notifications, providing users with relevant next steps.

Comment on lines +10 to +14
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Async
@EventListener
public @interface ReactiveEventListener {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Annotation name & meta-configuration are misleading

The composed annotation wires @Async + @EventListener, but nothing here is “reactive” in the Project-Reactor sense. This may confuse future maintainers who’ll expect a Flux/Mono contract.

Suggestions:

-@Target(ElementType.METHOD)
-@Retention(RetentionPolicy.RUNTIME)
-@Async
-@EventListener
-public @interface ReactiveEventListener {
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Async
+@EventListener
+public @interface AsyncEventListener { // clearer intent
 }

• Add @Documented for Javadoc visibility.
• Rename to AsyncEventListener (or similar) unless you truly require Reactor types.
Remember to @EnableAsync in a configuration class; otherwise the async executor is never activated.

📝 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
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Async
@EventListener
public @interface ReactiveEventListener {
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Async
@EventListener
public @interface AsyncEventListener { // clearer intent
}
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/global/annotation/ReactiveEventListener.java
around lines 10 to 14, the annotation name and meta-configuration are misleading
because it combines @Async and @EventListener but does not involve any
Project-Reactor reactive types like Flux or Mono. Rename the annotation to
AsyncEventListener to better reflect its behavior, add the @Documented
annotation for Javadoc visibility, and ensure that @EnableAsync is present in a
configuration class to activate the async executor.

Comment on lines +11 to +16
@Bean
public WebClient webClient() {
return WebClient.builder()
.defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.build();
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Set sensible timeouts & use header constants

A bare WebClient without timeouts risks hanging threads under network issues.

-        return WebClient.builder()
-                .defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
-                .build();
+        return WebClient.builder()
+                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+                .clientConnector(new ReactorClientHttpConnector(
+                        HttpClient.create()
+                                  .responseTimeout(Duration.ofSeconds(5))
+                                  .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
+                ))
+                .build();

• Use HttpHeaders.CONTENT_TYPE constant.
• Configure connect & read timeouts (values adjustable).
This prevents resource leaks and speeds up failure recovery.

📝 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
@Bean
public WebClient webClient() {
return WebClient.builder()
.defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.build();
}
@Bean
public WebClient webClient() {
return WebClient.builder()
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create()
.responseTimeout(Duration.ofSeconds(5))
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
))
.build();
}
🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/global/config/WebClientConfig.java around
lines 11 to 16, the WebClient bean is created without any timeout settings and
uses a hardcoded string for the Content-Type header. To fix this, replace the
hardcoded "Content-Type" string with the HttpHeaders.CONTENT_TYPE constant, and
configure sensible connect and read timeouts on the WebClient builder using
appropriate timeout values to prevent hanging threads and improve failure
handling.

Comment on lines +13 to +20
public Map<String, String> toMap() {
return Map.of(
"paymentKey", paymentKey,
"examDate", examDate.toString(),
"schoolName", schoolName,
"lunch", lunch
);
}
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 date formatting and null safety in toMap() method.

The current implementation has two potential issues:

  1. LocalDate.toString() uses ISO format (yyyy-MM-dd) which might not be user-friendly for notifications
  2. Map.of() will throw NullPointerException if any parameter is null

Consider this safer implementation:

public Map<String, String> toMap() {
    return Map.of(
-           "paymentKey", paymentKey,
-           "examDate", examDate.toString(),
-           "schoolName", schoolName,
-           "lunch", lunch
+           "paymentKey", Objects.requireNonNullElse(paymentKey, ""),
+           "examDate", examDate.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일")),
+           "schoolName", Objects.requireNonNullElse(schoolName, ""),
+           "lunch", Objects.requireNonNullElse(lunch, "")
    );
}

Add these imports:

+import java.time.format.DateTimeFormatter;
+import java.util.Objects;
📝 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 Map<String, String> toMap() {
return Map.of(
"paymentKey", paymentKey,
"examDate", examDate.toString(),
"schoolName", schoolName,
"lunch", lunch
);
}
// Add at the top of the file:
import java.time.format.DateTimeFormatter;
import java.util.Objects;
public Map<String, String> toMap() {
return Map.of(
"paymentKey", Objects.requireNonNullElse(paymentKey, ""),
"examDate", examDate.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일")),
"schoolName", Objects.requireNonNullElse(schoolName, ""),
"lunch", Objects.requireNonNullElse(lunch, "")
);
}
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/infra/notify/dto/ApplicationNotifyVariablesDto.java
around lines 13 to 20, the toMap() method uses LocalDate.toString() which
outputs ISO format and may not be user-friendly, and Map.of() which throws
NullPointerException if any value is null. To fix this, format examDate using a
user-friendly date formatter like DateTimeFormatter.ofPattern with a suitable
pattern, and replace Map.of() with a map construction approach that handles null
values safely, such as using a HashMap and conditionally putting entries or
using Map.ofEntries with Objects.toString(value, "") to avoid nulls.

Comment on lines 7 to 12
public record NotifyEventRequest(
@JsonProperty("userId") String userId,
@JsonProperty("api_key") String apiKey,
@JsonProperty("template_id") Integer templateId,
@JsonProperty("messages") List<Map<String, Object>> messages
) {
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 for required fields.

The record lacks validation which could lead to runtime errors when processing notification requests.

Add validation annotations:

+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;

public record NotifyEventRequest(
-       @JsonProperty("userId") String userId,
-       @JsonProperty("api_key") String apiKey,
-       @JsonProperty("template_id") Integer templateId,
-       @JsonProperty("messages") List<Map<String, Object>> messages
+       @JsonProperty("userId") @NotBlank String userId,
+       @JsonProperty("api_key") @NotBlank String apiKey,
+       @JsonProperty("template_id") @NotNull @Positive Integer templateId,
+       @JsonProperty("messages") @NotNull List<Map<String, Object>> messages
) {
📝 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 NotifyEventRequest(
@JsonProperty("userId") String userId,
@JsonProperty("api_key") String apiKey,
@JsonProperty("template_id") Integer templateId,
@JsonProperty("messages") List<Map<String, Object>> messages
) {
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
public record NotifyEventRequest(
@JsonProperty("userId") @NotBlank String userId,
@JsonProperty("api_key") @NotBlank String apiKey,
@JsonProperty("template_id") @NotNull @Positive Integer templateId,
@JsonProperty("messages") @NotNull List<Map<String, Object>> messages
) {
}
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/presentation/notify/dto/NotifyEventRequest.java
lines 7 to 12, the record fields lack validation annotations which may cause
runtime errors. Add appropriate validation annotations such as @NotNull or
@NotEmpty to required fields like userId, apiKey, templateId, and messages to
ensure they are present and valid when processing notification requests.

Comment on lines +12 to +18
public Map<String, String> toMap() {
return Map.of(
"examDate", examDate.toString(),
"examinationNumber", examinationNumber,
"schoolName", schoolName
);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Apply consistent date formatting and null safety (same as ApplicationNotifyVariablesDto).

This DTO has the same issues as ApplicationNotifyVariablesDto:

  1. LocalDate.toString() uses ISO format which might not be user-friendly
  2. Map.of() will throw NullPointerException if any parameter is null

For consistency across the notification system, apply the same fix:

public Map<String, String> toMap() {
    return Map.of(
-           "examDate", examDate.toString(),
-           "examinationNumber", examinationNumber,
-           "schoolName", schoolName
+           "examDate", examDate.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일")),
+           "examinationNumber", Objects.requireNonNullElse(examinationNumber, ""),
+           "schoolName", Objects.requireNonNullElse(schoolName, "")
    );
}

Add these imports:

+import java.time.format.DateTimeFormatter;
+import java.util.Objects;
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/infra/notify/dto/Exam3DayBeforeNotifyVariablesDto.java
around lines 12 to 18, replace the direct use of examDate.toString() with a
formatted date string using a DateTimeFormatter consistent with
ApplicationNotifyVariablesDto. Also, modify the toMap method to handle potential
null values safely, avoiding NullPointerException from Map.of by using a map
builder or a method that supports null values. Add necessary imports for
DateTimeFormatter and null checks as done in ApplicationNotifyVariablesDto.

Comment on lines 29 to 63
@Override
public NotifyEventRequest create(NotifyEvent event) {
ApplicationSchoolJpaEntity applicationSchool = applicationSchoolJpaRepository.findById(
event.id()).orElseThrow(
() -> new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_NOT_FOUND));

Exam3DayBeforeNotifyVariablesDto dto = new Exam3DayBeforeNotifyVariablesDto(
applicationSchool.getExamDate(), applicationSchool.getExaminationNumber(),
applicationSchool.getSchoolName());

String alimTalkContent = template.getProcessedMessage(
"notify.exam.threeday.reminder.alimtalk",
dto.toMap());

String smsContent = template.getProcessedMessage("notify.exam.threeday.reminder.sms",
dto.toMap());

LocalDateTime reserveTime = applicationSchool.getExamDate()
.minusDays(3)
.atTime(8, 0);

NotifyButtonUrls btnUrls = NotifyButtonUrls.of(
NotifyButtonUrl.of(INQUIRY, INQUIRY),
NotifyButtonUrl.of(MYPAGE, MYPAGE)
);

NotifyEventRemindMessageDto eventMessage = NotifyEventRemindMessageDto.create(1,
event.telNum(), reserveTime,
alimTalkContent,
smsContent, btnUrls);

return template.getNotifyEventRemindTemplate(EXAM_3DAY_BEFORE_TEMPLATE_ID, eventMessage);


}
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 exception handling and consider extracting common notification logic.

The strategy follows a consistent pattern but has some areas for improvement:

  1. Exception handling: The AI summary mentions NOTICE_NOT_FOUND error code, but the code uses APPLICATION_SCHOOL_NOT_FOUND. Please verify this is the correct error code.

  2. Code duplication: This strategy shares significant structure with other exam reminder strategies. Consider extracting common logic into a base class or utility method.

  3. Configuration: The hardcoded scheduling time (8:00 AM) should be configurable.

Let me verify the correct error code usage:


🏁 Script executed:

#!/bin/bash
# Description: Check for the correct error code usage across notification strategies

# Search for NOTICE_NOT_FOUND error code usage
rg -A 2 -B 2 "NOTICE_NOT_FOUND"

# Search for APPLICATION_SCHOOL_NOT_FOUND error code usage in notify strategies
rg -A 2 -B 2 "APPLICATION_SCHOOL_NOT_FOUND" --type java --glob "**/notify/strategy/**"

Length of output: 2676


Refactor common notification logic & externalize scheduling time

The error code usage (APPLICATION_SCHOOL_NOT_FOUND) is correct—no change needed. However, the following improvements are still recommended:

  • Extract shared behavior from each *DayBeforeNotifyStrategy into a common base class or utility:
    • Exam1DayBeforeNotifyStrategy.java
    • Exam3DayBeforeNotifyStrategy.java
    (and any other “XDayBeforeNotifyStrategy” implementations)
  • Make the hard-coded reminder time (.minusDays(3).atTime(8, 0)) configurable rather than fixed in code.
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam3DayBeforeNotifyStrategy.java
lines 29 to 63, refactor by extracting the common notification creation logic
shared with other *DayBeforeNotifyStrategy classes into a common base class or
utility method to avoid duplication. Additionally, replace the hard-coded
reminder time calculation (.minusDays(3).atTime(8, 0)) with a configurable
parameter, such as reading the number of days before and reminder time from
configuration or constructor parameters, to make the scheduling time flexible
and maintainable.

Comment on lines 30 to 61
public NotifyEventRequest create(NotifyEvent event) {
ApplicationSchoolJpaEntity applicationSchool = applicationSchoolJpaRepository.findById(
event.id()).orElseThrow(
() -> new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_NOT_FOUND));

Exam1DayBeforeNotifyVariablesDto dto = new Exam1DayBeforeNotifyVariablesDto(
applicationSchool.getExamDate(), applicationSchool.getExaminationNumber(),
applicationSchool.getSchoolName());

String alimTalkContent = template.getProcessedMessage(
"notify.exam.oneday.reminder.alimtalk",
dto.toMap());

String smsContent = template.getProcessedMessage("notify.exam.oneday.reminder.sms",
dto.toMap());

LocalDateTime reserveTime = applicationSchool.getExamDate()
.minusDays(1)
.atTime(8, 0);

NotifyButtonUrls btnUrls = NotifyButtonUrls.of(
NotifyButtonUrl.of(INQUIRY, INQUIRY),
NotifyButtonUrl.of(MYPAGE, MYPAGE)
);

NotifyEventRemindMessageDto eventMessage = NotifyEventRemindMessageDto.create(1,
event.telNum(), reserveTime,
alimTalkContent,
smsContent, btnUrls);

return template.getNotifyEventRemindTemplate(EXAM_1DAY_BEFORE_TEMPLATE_ID, eventMessage);
}
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 common exam reminder logic to reduce code duplication.

This strategy is nearly identical to Exam3DayBeforeNotifyStrategy except for the number of days (1 vs 3) and message template keys. Consider extracting common logic into a base class or utility method.

Example approach:

+public abstract class BaseExamReminderStrategy implements NotifyStrategy {
+    protected final NotifyEventTemplate template;
+    protected final ApplicationSchoolJpaRepository applicationSchoolJpaRepository;
+    
+    protected NotifyEventRequest createExamReminder(NotifyEvent event, int daysBefore, 
+            String alimTalkTemplate, String smsTemplate, Integer templateId) {
+        // Common logic here
+    }
+}

-@Component
-@RequiredArgsConstructor
-public class Exam1DayBeforeNotifyStrategy implements NotifyStrategy {
+public class Exam1DayBeforeNotifyStrategy extends BaseExamReminderStrategy {
+    @Override
+    public NotifyEventRequest create(NotifyEvent event) {
+        return createExamReminder(event, 1, 
+            "notify.exam.oneday.reminder.alimtalk",
+            "notify.exam.oneday.reminder.sms",
+            EXAM_1DAY_BEFORE_TEMPLATE_ID);
+    }
+}

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

🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1DayBeforeNotifyStrategy.java
lines 30 to 61, the create method duplicates logic found in
Exam3DayBeforeNotifyStrategy with only differences in days offset and template
keys. Refactor by extracting the shared logic into a base class or utility
method that accepts parameters for the number of days before the exam and the
template keys. Then have Exam1DayBeforeNotifyStrategy and
Exam3DayBeforeNotifyStrategy call this common method with their specific values
to reduce code duplication and improve maintainability.

Comment on lines 27 to 51
public NotifyEventRequest create(NotifyEvent event) {
ApplicationSchoolNotifyProjection projection = applicationSchoolJpaRepository.findPaymentByApplicationSchoolId(
event.id());

ApplicationNotifyVariablesDto dto = new ApplicationNotifyVariablesDto(
projection.paymentKey(), projection.examDate(), projection.schoolName(),
projection.lunch().getLunchName());

String alimTalkContent = template.getProcessedMessage(
"notify.exam.application.complete.alimtalk",
dto.toMap());

String smsContent = template.getProcessedMessage("notify.exam.application.complete.sms",
dto.toMap());

NotifyButtonUrls btnUrls = NotifyButtonUrls.of(
NotifyButtonUrl.of(WARNING, WARNING),
NotifyButtonUrl.of(MYPAGE, MYPAGE)
);

NotifyEventSuccessMessageDto eventMessage = NotifyEventSuccessMessageDto.create(1,
event.telNum(), alimTalkContent,
smsContent, "1", btnUrls);
return template.getNotifyEventSuccessTemplate(APPLICATION_TEMPLATE_ID, eventMessage);
}
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 null safety for repository projection and verify projection method behavior.

The strategy uses a repository projection method that may return null if the application school is not found. Unlike the exam reminder strategies, this doesn't use orElseThrow() for null handling.

Let me verify the projection method behavior:


🏁 Script executed:

#!/bin/bash
# Description: Check how the projection method handles missing data

# Search for the projection method implementation
ast-grep --pattern 'findPaymentByApplicationSchoolId($_) {
  $$$
}'

# Search for other projection method usages to see error handling patterns
rg -A 5 -B 2 "findPaymentByApplicationSchoolId" --type java

Length of output: 2414


Ensure projection result is non-null before use

The JPA query method

ApplicationSchoolNotifyProjection findPaymentByApplicationSchoolId(Long applicationSchoolId);

returns null if no record is found. In create(...), dereferencing a null projection will cause an NPE. To match other strategies’ error handling, add a null check or throw immediately after fetching the projection:

• File: src/main/java/life/mosu/mosuserver/infra/notify/strategy/ApplicationNotifyStrategy.java

    ApplicationSchoolNotifyProjection projection =
-         applicationSchoolJpaRepository.findPaymentByApplicationSchoolId(event.id());
+         applicationSchoolJpaRepository.findPaymentByApplicationSchoolId(event.id());
+     Objects.requireNonNull(projection,
+         "No application school found for id " + event.id());

Alternatively, update the repository signature to return

Optional<ApplicationSchoolNotifyProjection>
findPaymentByApplicationSchoolId(Long id);

and then use .orElseThrow(...) here.

🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/infra/notify/strategy/ApplicationNotifyStrategy.java
between lines 27 and 51, the projection fetched by
findPaymentByApplicationSchoolId can be null, leading to a NullPointerException
when accessed. Fix this by adding a null check after fetching the projection and
throw an appropriate exception if it is null, or alternatively, modify the
repository method to return an Optional and use orElseThrow here to handle the
absence of data safely.

Comment on lines 28 to 56
public NotifyEventRequest create(NotifyEvent event) {
OneWeekNotifyProjection projection = applicationSchoolJpaRepository.findOneWeekNotifyByApplicationSchoolId(
event.id());

Exam1WeekBeforeNotifyVariablesDto dto = new Exam1WeekBeforeNotifyVariablesDto(
projection.examDate(), projection.paymentKey(), projection.schoolName());

String alimTalkContent = template.getProcessedMessage(
"notify.exam.oneweek.reminder.alimtalk",
dto.toMap());

String smsContent = template.getProcessedMessage("notify.exam.oneweek.reminder.sms",
dto.toMap());

LocalDateTime reserveTime = projection.examDate()
.minusDays(7)
.atTime(8, 0);

NotifyButtonUrls btnUrls = NotifyButtonUrls.of(
NotifyButtonUrl.of(INQUIRY, INQUIRY)
);

NotifyEventRemindMessageDto eventMessage = NotifyEventRemindMessageDto.create(1,
event.telNum(), reserveTime,
alimTalkContent,
smsContent, btnUrls);

return template.getNotifyEventRemindTemplate(EXAM_1WEEK_BEFORE_TEMPLATE_ID, eventMessage);
}
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 null safety for projection result and ensure consistency with other strategies.

This strategy uses a projection method that may return null, but doesn't include null checking like the other exam reminder strategies. Additionally, it only includes one button URL while others include two.

 OneWeekNotifyProjection projection = applicationSchoolJpaRepository.findOneWeekNotifyByApplicationSchoolId(
         event.id());
+if (projection == null) {
+    throw new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_NOT_FOUND);
+}

Consider adding the MYPAGE button for consistency with other exam reminders:

 NotifyButtonUrls btnUrls = NotifyButtonUrls.of(
-        NotifyButtonUrl.of(INQUIRY, INQUIRY)
+        NotifyButtonUrl.of(INQUIRY, INQUIRY),
+        NotifyButtonUrl.of(MYPAGE, MYPAGE)
 );
📝 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 NotifyEventRequest create(NotifyEvent event) {
OneWeekNotifyProjection projection = applicationSchoolJpaRepository.findOneWeekNotifyByApplicationSchoolId(
event.id());
Exam1WeekBeforeNotifyVariablesDto dto = new Exam1WeekBeforeNotifyVariablesDto(
projection.examDate(), projection.paymentKey(), projection.schoolName());
String alimTalkContent = template.getProcessedMessage(
"notify.exam.oneweek.reminder.alimtalk",
dto.toMap());
String smsContent = template.getProcessedMessage("notify.exam.oneweek.reminder.sms",
dto.toMap());
LocalDateTime reserveTime = projection.examDate()
.minusDays(7)
.atTime(8, 0);
NotifyButtonUrls btnUrls = NotifyButtonUrls.of(
NotifyButtonUrl.of(INQUIRY, INQUIRY)
);
NotifyEventRemindMessageDto eventMessage = NotifyEventRemindMessageDto.create(1,
event.telNum(), reserveTime,
alimTalkContent,
smsContent, btnUrls);
return template.getNotifyEventRemindTemplate(EXAM_1WEEK_BEFORE_TEMPLATE_ID, eventMessage);
}
public NotifyEventRequest create(NotifyEvent event) {
OneWeekNotifyProjection projection = applicationSchoolJpaRepository.findOneWeekNotifyByApplicationSchoolId(
event.id());
if (projection == null) {
throw new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_NOT_FOUND);
}
Exam1WeekBeforeNotifyVariablesDto dto = new Exam1WeekBeforeNotifyVariablesDto(
projection.examDate(), projection.paymentKey(), projection.schoolName());
String alimTalkContent = template.getProcessedMessage(
"notify.exam.oneweek.reminder.alimtalk",
dto.toMap());
String smsContent = template.getProcessedMessage("notify.exam.oneweek.reminder.sms",
dto.toMap());
LocalDateTime reserveTime = projection.examDate()
.minusDays(7)
.atTime(8, 0);
NotifyButtonUrls btnUrls = NotifyButtonUrls.of(
NotifyButtonUrl.of(INQUIRY, INQUIRY),
NotifyButtonUrl.of(MYPAGE, MYPAGE)
);
NotifyEventRemindMessageDto eventMessage = NotifyEventRemindMessageDto.create(1,
event.telNum(), reserveTime,
alimTalkContent,
smsContent, btnUrls);
return template.getNotifyEventRemindTemplate(EXAM_1WEEK_BEFORE_TEMPLATE_ID, eventMessage);
}
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1WeekBeforeNotifyStrategy.java
between lines 28 and 56, add a null check after retrieving the projection from
applicationSchoolJpaRepository to handle cases where it may return null,
returning null or an appropriate fallback if so. Also, update the
NotifyButtonUrls to include both the INQUIRY and MYPAGE buttons to maintain
consistency with other exam reminder strategies.

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: 5

♻️ Duplicate comments (4)
src/main/java/life/mosu/mosuserver/infra/notify/strategy/SignUpNotifyStrategy.java (1)

27-28: Use template generator for consistency with other strategies.

This strategy uses MessageSource directly while other strategies use NotifyEventTemplateGenerator. For consistency across the codebase, consider using the template generator pattern.

-        String alimTalkContent = messageSource.getMessage("notify.signup.complete.alimtalk", null,
-                Locale.KOREA);
+        String alimTalkContent = template.getProcessedMessage("notify.signup.complete.alimtalk",
+                java.util.Collections.emptyMap());
src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1DayBeforeNotifyStrategy.java (2)

31-37: Create a projection to fetch only required fields

Fetching the entire entity when only examDate, examinationNumber, and schoolName are needed is inefficient.


22-62: Extract common exam reminder logic to reduce duplication

This strategy shares significant logic with Exam3DayBeforeNotifyStrategy and likely Exam1WeekBeforeNotifyStrategy. The only differences are the number of days and template keys.

src/main/java/life/mosu/mosuserver/infra/notify/strategy/ApplicationNotifyStrategy.java (1)

28-33: Add null check for projection to prevent NPE

The repository method findPaymentByApplicationSchoolId can return null if no record is found. Dereferencing it without a null check will cause a NullPointerException.

🧹 Nitpick comments (3)
src/main/java/life/mosu/mosuserver/infra/notify/NotifyProperties.java (1)

6-11: Consider using @ConfigurationProperties for better type safety and validation.

The current approach using @Value annotations works but has limitations. Consider using Spring Boot's @ConfigurationProperties for better type safety, validation, and IDE support.

+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+import jakarta.validation.constraints.NotBlank;

-@Component
-public class NotifyProperties {
-    @Value("${alimtalk.api.base-url}") String alimTalkUrl;
-    @Value("${alimtalk.user-id}") String userId;
-    @Value("${alimtalk.api-key}") String apiKey;
-}
+@ConfigurationProperties(prefix = "alimtalk")
+@Validated
+@Component
+public class NotifyProperties {
+    @NotBlank
+    private String apiBaseUrl;
+    @NotBlank
+    private String userId;
+    @NotBlank
+    private String apiKey;
+    
+    // Getters and setters
+    public String getApiBaseUrl() { return apiBaseUrl; }
+    public void setApiBaseUrl(String apiBaseUrl) { this.apiBaseUrl = apiBaseUrl; }
+    public String getUserId() { return userId; }
+    public void setUserId(String userId) { this.userId = userId; }
+    public String getApiKey() { return apiKey; }
+    public void setApiKey(String apiKey) { this.apiKey = apiKey; }
+}
src/main/java/life/mosu/mosuserver/infra/notify/LunaSoftNotifier.java (1)

32-42: Consider adding timeout and error handling improvements.

The reactive chain looks good but could benefit from timeout configuration and better error handling for production use.

        webClient.post()
                .uri(properties.alimTalkUrl)
                .bodyValue(lunaRequest)
                .retrieve()
                .bodyToMono(String.class)
+               .timeout(Duration.ofSeconds(30))
                .publishOn(Schedulers.boundedElastic())
                .doOnSuccess(response -> log.info("알림톡 응답: {}", response))
                .doOnError(error -> log.error("알림톡 전송 실패", error))
                .doOnTerminate(() -> log.info("알림톡 전송 완료: {}", request))
+               .onErrorComplete()
                .subscribe();

The onErrorComplete() ensures that errors don't propagate and crash the application, and timeout() prevents hanging requests.

src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1DayBeforeNotifyStrategy.java (1)

46-48: Consider making notification time configurable

The notification time is hardcoded to 8:00 AM. This might need to be configurable based on business requirements or user preferences.

+@Value("${notify.exam.reminder.hour:8}")
+private int reminderHour;
+
+@Value("${notify.exam.reminder.minute:0}")
+private int reminderMinute;

 LocalDateTime reserveTime = applicationSchool.getExamDate()
         .minusDays(1)
-        .atTime(8, 0);
+        .atTime(reminderHour, reminderMinute);
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 32009b9 and aa28d41.

📒 Files selected for processing (26)
  • src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java (3 hunks)
  • src/main/java/life/mosu/mosuserver/application/notify/NotifyService.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/global/initializer/DatabaseInitializer.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/LunaSoftNotifier.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/NotifyClientAdapter.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/NotifyEventPublisher.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/NotifyEventTemplateGenerator.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/NotifyProperties.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/constant/NotifyConstants.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/EventStatus.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyButtonUrl.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyButtonUrls.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEvent.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventRemindMessageDto.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventRequest.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventSuccessMessageDto.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyStatus.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/ApplicationNotifyStrategy.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1DayBeforeNotifyStrategy.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1WeekBeforeNotifyStrategy.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam3DayBeforeNotifyStrategy.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/InquiryAnswerNotifyStrategy.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/NotifyStrategy.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/RefundNotifyStrategy.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/SignUpNotifyStrategy.java (1 hunks)
  • src/main/java/life/mosu/mosuserver/presentation/notify/NotifyEventListener.java (1 hunks)
✅ Files skipped from review due to trivial changes (8)
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/EventStatus.java
  • src/main/java/life/mosu/mosuserver/infra/notify/NotifyClientAdapter.java
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventRequest.java
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyButtonUrls.java
  • src/main/java/life/mosu/mosuserver/presentation/notify/NotifyEventListener.java
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyButtonUrl.java
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEvent.java
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyStatus.java
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/main/java/life/mosu/mosuserver/infra/notify/NotifyEventPublisher.java
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/NotifyStrategy.java
  • src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/InquiryAnswerNotifyStrategy.java
  • src/main/java/life/mosu/mosuserver/infra/notify/constant/NotifyConstants.java
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/RefundNotifyStrategy.java
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam3DayBeforeNotifyStrategy.java
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventSuccessMessageDto.java
  • src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1WeekBeforeNotifyStrategy.java
  • src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventRemindMessageDto.java
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/main/java/life/mosu/mosuserver/infra/notify/LunaSoftNotifier.java (1)
src/main/java/life/mosu/mosuserver/infra/notify/NotifyProperties.java (1)
  • Component (6-11)
src/main/java/life/mosu/mosuserver/infra/notify/NotifyProperties.java (1)
src/main/java/life/mosu/mosuserver/infra/notify/LunaSoftNotifier.java (1)
  • Component (13-65)
🔇 Additional comments (4)
src/main/java/life/mosu/mosuserver/global/initializer/DatabaseInitializer.java (1)

110-110: LGTM! Fixed phone number for notification testing support.

The change to use a fixed phone number instead of dynamically generated ones makes sense for testing the new notification system infrastructure. This ensures consistent test data for the 알림톡 feature implementation.

src/main/java/life/mosu/mosuserver/infra/notify/LunaSoftNotifier.java (1)

53-64: LGTM! Well-structured record with proper JSON mapping.

The LunaNotifyRequest record is well-designed with proper JSON property mapping and a clean factory method.

src/main/java/life/mosu/mosuserver/infra/notify/strategy/SignUpNotifyStrategy.java (1)

34-38: LGTM! Clean implementation of the notification strategy.

The strategy implementation follows the pattern correctly and properly constructs the notification request with button URLs and message content.

src/main/java/life/mosu/mosuserver/infra/notify/strategy/ApplicationNotifyStrategy.java (1)

42-45: Button URLs are correctly configured with actual URL constants
The WARNING, MYPAGE, HOMEPAGE, and INQUIRY constants are defined in NotifyConstants.java as full URLs (e.g., "https://www.mosuedu.com/warning"). The call

NotifyButtonUrl.of(WARNING, WARNING)

passes the same URL for both PC and mobile views, which is intentional and valid. You can ignore the earlier concern about using labels in place of URLs.

Likely an incorrect or invalid review comment.

Comment on lines +8 to +10
@Value("${alimtalk.api.base-url}") String alimTalkUrl;
@Value("${alimtalk.user-id}") String userId;
@Value("${alimtalk.api-key}") String apiKey;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make fields private and add proper encapsulation.

The fields should be private with getter methods to follow proper encapsulation principles.

-    @Value("${alimtalk.api.base-url}") String alimTalkUrl;
-    @Value("${alimtalk.user-id}") String userId;
-    @Value("${alimtalk.api-key}") String apiKey;
+    @Value("${alimtalk.api.base-url}") private String alimTalkUrl;
+    @Value("${alimtalk.user-id}") private String userId;
+    @Value("${alimtalk.api-key}") private String apiKey;
+    
+    public String getAlimTalkUrl() { return alimTalkUrl; }
+    public String getUserId() { return userId; }
+    public String getApiKey() { return apiKey; }
📝 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
@Value("${alimtalk.api.base-url}") String alimTalkUrl;
@Value("${alimtalk.user-id}") String userId;
@Value("${alimtalk.api-key}") String apiKey;
@Value("${alimtalk.api.base-url}") private String alimTalkUrl;
@Value("${alimtalk.user-id}") private String userId;
@Value("${alimtalk.api-key}") private String apiKey;
public String getAlimTalkUrl() {
return alimTalkUrl;
}
public String getUserId() {
return userId;
}
public String getApiKey() {
return apiKey;
}
🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/infra/notify/NotifyProperties.java lines 8
to 10, the fields alimTalkUrl, userId, and apiKey are package-private but should
be made private to enforce encapsulation. Change the field access modifiers to
private and add public getter methods for each field to allow controlled access
while keeping the fields encapsulated.


ProfileJpaEntity profile = profileJpaRepository.findByUserId(event.targetId())
.orElseThrow(() -> new CustomRuntimeException(ErrorCode.PROFILE_NOT_FOUND));
String parsedPhoneNumber = profile.getPhoneNumber().replaceAll("-", "");
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 for phone number processing.

The current implementation assumes the phone number always contains hyphens and doesn't validate the format. Consider adding validation.

-        String parsedPhoneNumber = profile.getPhoneNumber().replaceAll("-", "");
+        String phoneNumber = profile.getPhoneNumber();
+        if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
+            throw new CustomRuntimeException(ErrorCode.INVALID_PHONE_NUMBER);
+        }
+        String parsedPhoneNumber = phoneNumber.replaceAll("-", "");
📝 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
String parsedPhoneNumber = profile.getPhoneNumber().replaceAll("-", "");
String phoneNumber = profile.getPhoneNumber();
if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
throw new CustomRuntimeException(ErrorCode.INVALID_PHONE_NUMBER);
}
String parsedPhoneNumber = phoneNumber.replaceAll("-", "");
🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/application/notify/NotifyService.java at
line 30, the code removes hyphens from the phone number without validating its
format. Add validation before processing the phone number to ensure it matches
the expected pattern (e.g., digits with optional hyphens). If the format is
invalid, handle it appropriately, such as throwing an exception or returning an
error, to prevent processing incorrect phone numbers.

Comment on lines +37 to +39
private NotifyStrategy getNotifyStrategy(String strategyName) {
return (NotifyStrategy) applicationContext.getBean(strategyName);
}
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 error handling for missing strategy beans.

The current implementation will throw a NoSuchBeanDefinitionException if the strategy bean is not found. Consider adding proper error handling.

    private NotifyStrategy getNotifyStrategy(String strategyName) {
-        return (NotifyStrategy) applicationContext.getBean(strategyName);
+        try {
+            return (NotifyStrategy) applicationContext.getBean(strategyName);
+        } catch (NoSuchBeanDefinitionException e) {
+            throw new CustomRuntimeException(ErrorCode.STRATEGY_NOT_FOUND);
+        }
    }
📝 Committable suggestion

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

Suggested change
private NotifyStrategy getNotifyStrategy(String strategyName) {
return (NotifyStrategy) applicationContext.getBean(strategyName);
}
private NotifyStrategy getNotifyStrategy(String strategyName) {
try {
return (NotifyStrategy) applicationContext.getBean(strategyName);
} catch (NoSuchBeanDefinitionException e) {
throw new CustomRuntimeException(ErrorCode.STRATEGY_NOT_FOUND);
}
}
🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/application/notify/NotifyService.java
around lines 37 to 39, the method getNotifyStrategy currently retrieves a bean
by name without handling the case where the bean does not exist, which throws
NoSuchBeanDefinitionException. Modify the method to catch this exception and
handle it gracefully, such as by logging an error and returning null or throwing
a custom exception with a clear message, to improve robustness.

Comment on lines +28 to +48
public NotifyEventRequest getNotifyEventSuccessTemplate(
Integer templateId,
NotifyEventSuccessMessageDto message
) {

return new NotifyEventRequest(
templateId,
List.of(message.toMap())
);
}

public NotifyEventRequest getNotifyEventRemindTemplate(
Integer templateId,
NotifyEventRemindMessageDto message
) {

return new NotifyEventRequest(
templateId,
List.of(message.toMap())
);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor duplicate code in template methods

Both methods have identical logic except for the parameter type. Consider using a common interface or generic method.

+public interface NotifyMessage {
+    Map<String, Object> toMap();
+}

-public NotifyEventRequest getNotifyEventSuccessTemplate(
-        Integer templateId,
-        NotifyEventSuccessMessageDto message
-) {
-
-    return new NotifyEventRequest(
-            templateId,
-            List.of(message.toMap())
-    );
-}
-
-public NotifyEventRequest getNotifyEventRemindTemplate(
-        Integer templateId,
-        NotifyEventRemindMessageDto message
-) {
-
-    return new NotifyEventRequest(
-            templateId,
-            List.of(message.toMap())
-    );
-}
+public <T extends NotifyMessage> NotifyEventRequest getNotifyEventTemplate(
+        Integer templateId,
+        T message
+) {
+    if (templateId == null || message == null) {
+        throw new IllegalArgumentException("Template ID and message must not be null");
+    }
+    
+    return new NotifyEventRequest(
+            templateId,
+            List.of(message.toMap())
+    );
+}

Then update NotifyEventSuccessMessageDto and NotifyEventRemindMessageDto to implement the NotifyMessage interface.

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

🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/infra/notify/NotifyEventTemplateGenerator.java
between lines 28 and 48, both getNotifyEventSuccessTemplate and
getNotifyEventRemindTemplate methods contain duplicate code differing only by
parameter type. To fix this, create a common interface NotifyMessage with a
toMap method, have NotifyEventSuccessMessageDto and NotifyEventRemindMessageDto
implement this interface, then refactor these methods into a single generic
method that accepts a NotifyMessage parameter and returns the NotifyEventRequest
accordingly.

Comment on lines +20 to +26
public String getProcessedMessage(String code, Map<String, String> variables) {
String message = messageSource.getMessage(code, null, Locale.KOREA);
for (Map.Entry<String, String> entry : variables.entrySet()) {
message = StringUtils.replace(message, "#{" + entry.getKey() + "}", entry.getValue());
}
return message;
}
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 error handling and make locale configurable

The method has several issues:

  1. No error handling if the message code doesn't exist
  2. Hardcoded Locale.KOREA limits internationalization
  3. Multiple string replacements in a loop can be inefficient
-public String getProcessedMessage(String code, Map<String, String> variables) {
-    String message = messageSource.getMessage(code, null, Locale.KOREA);
-    for (Map.Entry<String, String> entry : variables.entrySet()) {
-        message = StringUtils.replace(message, "#{" + entry.getKey() + "}", entry.getValue());
-    }
-    return message;
+public String getProcessedMessage(String code, Map<String, String> variables, Locale locale) {
+    if (code == null || variables == null) {
+        throw new IllegalArgumentException("Code and variables must not be null");
+    }
+    
+    String message;
+    try {
+        message = messageSource.getMessage(code, null, locale != null ? locale : Locale.KOREA);
+    } catch (NoSuchMessageException e) {
+        throw new CustomRuntimeException(ErrorCode.MESSAGE_NOT_FOUND, "Message not found for code: " + code);
+    }
+    
+    // Use StringBuilder for better performance with multiple replacements
+    StringBuilder result = new StringBuilder(message);
+    for (Map.Entry<String, String> entry : variables.entrySet()) {
+        String placeholder = "#{" + entry.getKey() + "}";
+        String value = entry.getValue() != null ? entry.getValue() : "";
+        int index = 0;
+        while ((index = result.indexOf(placeholder, index)) != -1) {
+            result.replace(index, index + placeholder.length(), value);
+            index += value.length();
+        }
+    }
+    return result.toString();
+}

You'll also need to update the calling code to pass the locale parameter.

📝 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 String getProcessedMessage(String code, Map<String, String> variables) {
String message = messageSource.getMessage(code, null, Locale.KOREA);
for (Map.Entry<String, String> entry : variables.entrySet()) {
message = StringUtils.replace(message, "#{" + entry.getKey() + "}", entry.getValue());
}
return message;
}
public String getProcessedMessage(String code, Map<String, String> variables, Locale locale) {
if (code == null || variables == null) {
throw new IllegalArgumentException("Code and variables must not be null");
}
String message;
try {
message = messageSource.getMessage(code, null, locale != null ? locale : Locale.KOREA);
} catch (NoSuchMessageException e) {
throw new CustomRuntimeException(ErrorCode.MESSAGE_NOT_FOUND, "Message not found for code: " + code);
}
// Use StringBuilder for better performance with multiple replacements
StringBuilder result = new StringBuilder(message);
for (Map.Entry<String, String> entry : variables.entrySet()) {
String placeholder = "#{" + entry.getKey() + "}";
String value = entry.getValue() != null ? entry.getValue() : "";
int index = 0;
while ((index = result.indexOf(placeholder, index)) != -1) {
result.replace(index, index + placeholder.length(), value);
index += value.length();
}
}
return result.toString();
}
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/infra/notify/NotifyEventTemplateGenerator.java
around lines 20 to 26, the getProcessedMessage method lacks error handling for
missing message codes, uses a hardcoded Locale.KOREA, and performs inefficient
string replacements in a loop. To fix this, add a Locale parameter to the method
to make it configurable, handle exceptions or check for missing messages when
retrieving the message from messageSource, and optimize the replacement logic by
using a more efficient approach such as a single pass replacement or a
StringBuilder. Also, update all calling code to pass the appropriate Locale
argument.

Copy link
Contributor

@wlgns12370 wlgns12370 left a comment

Choose a reason for hiding this comment

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

알림 디자인 패턴 매우 만족스럽네요! 메시지는 인코딩해서 저장해두는 것까지 좋습니다!

Comment on lines 48 to 67
admissionTicketImageJpaRepository.save(
createAdmissionTicketImageIfPresent(request.admissionTicket(), applicationId));

for (ApplicationSchoolRequest schoolRequest : schoolRequests) {
//해당 학교가 존재하는 학교인지
Long schoolId = schoolJpaRepository.findBySchoolNameAndAreaAndExamDate(
schoolRequest.schoolName(),
schoolRequest.validatedArea(schoolRequest.area()),
schoolRequest.examDate())
.orElseThrow(() -> new CustomRuntimeException(ErrorCode.SCHOOL_NOT_FOUND))
.getId();

//해당 학교를 이미 신청하였는지
if (applicationSchoolJpaRepository.existsByUserIdAndSchoolId(userId, schoolId)) {
throw new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_ALREADY_APPLIED);
}

//해당 학교를 찾을 수 있는지
SchoolJpaEntity school = schoolJpaRepository.findById(schoolId)
.orElseThrow(() -> new CustomRuntimeException(ErrorCode.SCHOOL_NOT_FOUND));
Copy link
Contributor

Choose a reason for hiding this comment

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

학교 존재 확인과 기존의 신청이 있는지 검증을 하고 수험표 저장 로직을 수행해야 할 것 같은데 혹시 다른 의도가 있는 건가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

수정 중에 있습니다! 확인했어요

@polyglot-k polyglot-k merged commit 8d2f310 into develop Jul 17, 2025
2 checks passed
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.

3 participants