Skip to content

Feat/notification#70

Closed
ramen0519 wants to merge 14 commits intodevelopfrom
feat/notification
Closed

Feat/notification#70
ramen0519 wants to merge 14 commits intodevelopfrom
feat/notification

Conversation

@ramen0519
Copy link
Copy Markdown
Contributor

@ramen0519 ramen0519 commented Feb 4, 2026

📍 PR 타입 (하나 이상 선택)

  • 기능 추가
  • 버그 수정
  • 의존성, 환경 변수, 빌드 관련 코드 업데이트
  • 기타 사소한 수정

❗️ 관련 이슈 링크

Close #

📌 개요

마이페이지 일일, 주간 알림 메일 전송 기능 구현

🔁 변경 사항

  • 알림 설정 조회, 변경 api 구현
  • 일일 체크리스트 (전날 기준) 쿼리 추가
  • 주간 학습 뉴스 숫자, 퀴즈 풀이 숫자 쿼리 추가
  • 일일 체크리스트 복습 쿼리 수정 ( createdAt 조건 추가 )
  • 일일, 주간 메일 HTML 구현

📸 스크린샷

스크린샷(214) 일일 알림 ( 매일 오전 8시 ) 스크린샷(213) 주간 알림 ( 매주 월요일 오전 8시 )

👀 기타 더 이야기해볼 점

✅ 체크 리스트

  • PR 템플릿에 맞추어 작성했어요.
  • 변경 내용에 대한 테스트를 진행했어요.
  • 프로그램이 정상적으로 동작해요.
  • PR에 적절한 라벨을 선택했어요.
  • 불필요한 코드는 삭제했어요.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 일일/주간 학습 활동 요약 이메일 알림 제공 (HTML 형식 지원)
    • 사용자 알림 설정 조회/수정 기능 추가
  • 변경

    • 저장된 뉴스/용어 검색이 폴더 단위로 동작하도록 변경(검색 시 폴더 지정 필요)
    • 저장 항목 응답 필드 변경: 뉴스 ID 및 카테고리, 용어/설명 필드명 갱신
  • 버그 수정

    • 동일 항목에 대한 폴더별 중복 저장 방지 로직 적용

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 4, 2026

Caution

Review failed

The pull request is closed.

Walkthrough

사용자의 알림 설정 조회/수정 API와 notificationEnabled 필드 추가, 일일·주간 학습 알림 스케줄러·발송 서비스와 HTML 이메일 템플릿 빌더, 관련 저장소 쿼리(활동 집계 및 검색)의 구현을 추가합니다.

Changes

Cohort / File(s) Summary
알림 설정 관리
src/main/java/com/finsight/finsight/domain/mypage/presentation/MypageController.java, src/main/java/com/finsight/finsight/domain/mypage/domain/service/MypageService.java, src/main/java/com/finsight/finsight/domain/mypage/application/dto/request/UpdateNotificationRequest.java, src/main/java/com/finsight/finsight/domain/mypage/application/dto/response/NotificationResponse.java
사용자 알림 설정 조회(GET)·수정(PUT) 엔드포인트와 서비스 로직, 요청/응답 DTO(record) 추가 및 인증 체크 적용.
사용자 엔티티·레포지토리 확장
src/main/java/com/finsight/finsight/domain/user/persistence/entity/UserEntity.java, src/main/java/com/finsight/finsight/domain/user/persistence/repository/UserRepository.java
UserEntity에 notificationEnabled 필드와 업데이트 메서드 추가. notificationEnabled 및 AuthType 기반 사용자 조회 쿼리 추가.
이메일 발송 확장
src/main/java/com/finsight/finsight/domain/auth/domain/service/EmailService.java
단순 텍스트 이메일(sendNotificationEmail) 및 HTML MIME 이메일(sendHtmlEmail) 발송 메서드 추가(예외 포장 포함).
알림 스케줄링 및 서비스
src/main/java/com/finsight/finsight/domain/notification/application/usecase/NotificationScheduler.java, src/main/java/com/finsight/finsight/domain/notification/domain/service/NotificationService.java
매일·주간 크론 스케줄러 추가. 알림 활성 사용자 대상 일별/주별 활동 집계 후 템플릿 생성 및 이메일 발송 로직 구현(개별 실패 시 계속 처리).
이메일 템플릿 생성기
src/main/java/com/finsight/finsight/domain/notification/infrastructure/template/NotificationTemplateBuilder.java
일일·주간 HTML 이메일 템플릿 빌더 추가(상태 배지 생성, 한글 날짜 포맷, 메시지/이모지 분기 포함).
활동 집계 및 쿼리 추가/수정
src/main/java/com/finsight/finsight/domain/quiz/persistence/repository/QuizAttemptRepository.java, src/main/java/com/finsight/finsight/domain/storage/persistence/repository/FolderItemRepository.java, src/main/java/com/finsight/finsight/domain/naver/persistence/repository/UserArticleViewRepository.java
퀴즈/저장항목/기사뷰 기준 날짜 범위 쿼리 추가 및 일부 기존 쿼리 조건 수정(예: existsReviewAttemptToday 제약 추가, 폴더 기반 검색으로 쿼리 시그니처 변경).
저장(News/Term) 서비스·컨트롤러 변경
src/main/java/com/finsight/finsight/domain/storage/domain/service/SavedNewsService.java, src/main/java/com/finsight/finsight/domain/storage/domain/service/SavedTermService.java, src/main/java/com/finsight/finsight/domain/storage/presentation/SavedNewsController.java, src/main/java/com/finsight/finsight/domain/storage/presentation/SavedTermController.java, src/main/java/com/finsight/finsight/domain/storage/application/dto/response/SavedNewsResponse.java, src/main/java/com/finsight/finsight/domain/storage/application/dto/response/SavedTermResponse.java
폴더 단위 중복 검사로 변경, 검색 API에 folderId 파라미터 추가, 응답 DTO 필드(뉴스: articleId→newsId, category 추가 등 / 용어: displayName→term 등) 변경에 따른 매핑 업데이트.
기타 소소한 변경
src/main/java/com/finsight/finsight/domain/auth/presentation/AuthController.java
로그아웃 엔드포인트에 인증 null 체크(unauthorized 예외) 추가.

Sequence Diagram(s)

sequenceDiagram
    participant Scheduler as NotificationScheduler
    participant Service as NotificationService
    participant UserRepo as UserRepository
    participant DataRepos as Data Repositories
    participant Template as NotificationTemplateBuilder
    participant EmailSvc as EmailService
    participant SMTP as Email Provider

    Scheduler->>Service: sendDailyNotifications()
    Service->>UserRepo: findByNotificationEnabledAndAuthType(EMAIL)
    UserRepo-->>Service: List<UserEntity>

    loop 각 사용자 처리
        Service->>DataRepos: 집계 쿼리(뉴스 저장, 퀴즈 시도/복습, 기사 뷰) for user
        DataRepos-->>Service: flags / counts
        Service->>Template: buildDailyEmail(flags)
        Template-->>Service: HTML content
        Service->>EmailSvc: sendHtmlEmail(email, subject, html)
        EmailSvc->>SMTP: 전송 요청
        SMTP-->>EmailSvc: 전송 결과
    end
Loading
sequenceDiagram
    participant Client as Client
    participant Controller as MypageController
    participant Service as MypageService
    participant Repo as UserRepository
    participant Entity as UserEntity

    Client->>Controller: GET /api/mypage/me/notification
    Controller->>Service: getNotificationSetting(userId)
    Service->>Repo: findById(userId)
    Repo-->>Entity: UserEntity
    Entity-->>Service: notificationEnabled
    Service-->>Controller: NotificationResponse
    Controller-->>Client: DataResponse<NotificationResponse>

    Client->>Controller: PUT /api/mypage/me/notification (UpdateNotificationRequest)
    Controller->>Service: updateNotificationSetting(userId, request)
    Service->>Repo: findById(userId)
    Repo-->>Entity: UserEntity
    Service->>Entity: updateNotificationEnabled(enabled)
    Service-->>Controller: void
    Controller-->>Client: DataResponse<Void>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Feat/#57 #64 — overlapping changes: repository 쿼리 추가 및 UserEntity 확장(활동/뷰 추적) 관련.
  • Feat/storage #55 — storage/folder 도메인, FolderItemRepository·FolderService·SavedNews/SavedTerm 관련 변경과 연관.

Suggested labels

✨ feature

Suggested reviewers

  • kkshyun
  • kingmingyu

📝 상세 검토 포인트

좋은 점:

  • 알림 설정 API와 스케줄러/서비스 역할 분리가 명확하고, 사용자별 실패를 무시하고 계속 처리하는 설계는 실무에 적합합니다.
  • 이메일 템플릿과 발송 로직을 분리해 테스트·유지보수하기 수월합니다.

주의할 사항:

  1. NotificationTemplateBuilder의 대형 HTML 문자열과 날짜/이모지 로직은 유지보수 어려움이 있으니 템플릿 외부화(파일/템플릿 엔진) 또는 상수 분리 고려를 권장합니다.
  2. NotificationService#getEmailFromUser()에서 null 안전성 및 중복 인증소스 처리(우선순위 보장)를 재검토하세요(예외 케이스 및 로그 남김 권장).
  3. QuizAttemptRepository의 existsReviewAttemptToday에 추가된 createdAt 비교 조건이 기존 통계/알고리즘에 영향 줄 수 있으니 관련 테스트 케이스 확인 필요합니다.
  4. UserRepository의 findByNotificationEnabledAndAuthType 쿼리는 join으로 중복 레코드가 생길 수 있어 필요 시 DISTINCT 또는 그룹핑 적용 검토 바랍니다.
  5. EmailService.sendHtmlEmail에서 MessagingException을 RuntimeException으로 포장하는 전략은 호출자에서 적절히 처리되는지(재시도/로깅) 확인하세요.
🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive PR 제목 '기능 추가'는 너무 모호해서 이 PR의 핵심 변경사항인 일일/주간 알림 메일 전송 기능을 명확히 전달하지 못합니다. '일일/주간 알림 메일 전송 기능 추가' 또는 '마이페이지 학습 알림 메일 자동화' 같은 구체적인 제목으로 변경해주세요.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed PR 설명이 템플릿 구조를 잘 따르고 있으며, 기능 개요, 변경사항, 스크린샷, 체크리스트가 모두 작성되어 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/notification

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

❤️ Share

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

Copy link
Copy Markdown

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

🤖 Fix all issues with AI agents
In
`@src/main/java/com/finsight/finsight/domain/auth/domain/service/EmailService.java`:
- Around line 31-40: The sendNotificationEmail(String email, String subject,
String content) method in EmailService is unused and should be removed to reduce
dead code; search the codebase to confirm there are no callers, then delete the
sendNotificationEmail method and any now-unused imports or JavaDoc related to
it, leaving sendVerificationEmail and sendHtmlEmail untouched; if you expect to
reuse plaintext emailing later instead mark it `@Deprecated` and add a TODO
comment instead of deleting.

In
`@src/main/java/com/finsight/finsight/domain/notification/domain/service/NotificationService.java`:
- Around line 37-62: In NotificationService.sendDailyNotifications, avoid the
empty catch: log the exception and contextual info (at least user.getUserId()
and/or email) when handling failures for each user so errors aren't swallowed;
update the catch block to call the class logger (e.g., log.warn or log.error)
with a clear message including userId/email and the exception to aid debugging,
while keeping the loop behavior to continue to next user; reference methods
getEmailFromUser, templateBuilder.buildDailyEmail, and
emailService.sendHtmlEmail to indicate where failures should be logged.
- Around line 68-93: In sendWeeklyNotifications (NotificationService) the catch
block currently swallows exceptions; update it to log failures with context and
the exception (e.g., include user.getUserId() and the resolved email from
getEmailFromUser(user)) using the class logger before continuing so individual
user failures are recorded; keep the existing behavior of continuing to next
user after logging.
- Line 39: The current query used by UserRepository's
findByNotificationEnabledAndAuthType returns UserEntity without fetching
userAuths, causing N+1 queries when NotificationService calls
getEmailFromUser(user) which accesses user.getUserAuths(); update the repository
query to use a FETCH JOIN to eagerly load u.userAuths (i.e., LEFT JOIN FETCH
u.userAuths ua) while keeping the same authType filter so the users and their
userAuths are loaded in one query and the N+1 problem is eliminated.

In
`@src/main/java/com/finsight/finsight/domain/notification/infrastructure/template/NotificationTemplateBuilder.java`:
- Around line 23-32: The current else branch in NotificationTemplateBuilder
produces a "saved news awaiting quiz" message for any case not covered by the
first two branches, which is incorrect when isNewsSaved is false but
isQuizSolved or isQuizReviewed is true; update the branching to explicitly
handle the remaining combinations instead of a catch-all else: after the
existing checks for (!isNewsSaved && !isQuizSolved && !isQuizReviewed) and
(isNewsSaved && isQuizSolved && isQuizReviewed), add explicit branches such as
(isNewsSaved) to keep the "saved news awaiting quiz" message, (isQuizSolved) to
produce a message acknowledging a completed quiz but no saved news, and
(isQuizReviewed) to produce a message for only review—set the appropriate
message and emoji values in each branch so message/emoji reflect the actual
booleans isNewsSaved, isQuizSolved, isQuizReviewed.

In
`@src/main/java/com/finsight/finsight/domain/user/persistence/entity/UserEntity.java`:
- Around line 71-72: In updateNotificationEnabled(Boolean enabled) in
UserEntity, guard against null before assigning to the non-nullable field
notificationEnabled; if enabled is null, set notificationEnabled to a safe
default (e.g., false) or throw an IllegalArgumentException — implement a
null-check at the start of the method and assign either enabled.booleanValue()
or the chosen default to this.notificationEnabled to avoid nullable=false
constraint violations at runtime.
- Around line 55-57: The UserEntity field notificationEnabled is declared
non-nullable but existing DB rows aren’t handled; add a proper migration or
config: create a Flyway/Liquibase migration that adds the notification_enabled
column with a default value (e.g., true) and backfills existing rows, then alter
the column to NOT NULL (or add it NOT NULL with DEFAULT then remove default), or
if you rely on JPA DDL only, ensure spring.jpa.hibernate.ddl-auto is set
appropriately for your environment (validate in prod) and do an explicit
backfill migration instead of relying on `@Builder.Default`; locate
UserEntity.notificationEnabled and implement the migration/backfill and
configuration change accordingly.

In
`@src/main/java/com/finsight/finsight/domain/user/persistence/repository/UserRepository.java`:
- Around line 18-27: The JPQL query in
UserRepository.findByNotificationEnabledAndAuthType selects UserEntity u with a
JOIN on u.userAuths which can return duplicate UserEntity rows when a user has
multiple matching userAuths; update the query to return unique users (e.g.,
change the projection to SELECT DISTINCT u) so the method
findByNotificationEnabledAndAuthType (and downstream callers like
NotificationService.sendDailyNotifications/sendWeeklyNotifications) receive each
UserEntity only once.
🧹 Nitpick comments (3)
src/main/java/com/finsight/finsight/domain/notification/infrastructure/template/NotificationTemplateBuilder.java (1)

13-14: TODO 주석 확인했어요.

프론트 배포 후 실제 URL로 변경이 필요하다는 점 잘 기록해두셨네요. 이 부분은 배포 환경별로 다를 수 있으니, 추후 application.yml에서 설정값으로 주입받는 방식(@Value)으로 변경하면 환경별 관리가 더 편해질 거예요.

이 작업을 위한 이슈를 열어드릴까요?

src/main/java/com/finsight/finsight/domain/auth/domain/service/EmailService.java (1)

45-58: HTML 이메일 발송 구현이 잘 되었어요!

MimeMessageHelper를 사용한 HTML 이메일 발송이 올바르게 구현되었고, UTF-8 인코딩도 적절히 설정되어 있네요.

한 가지 개선 제안: 현재 RuntimeException으로 감싸고 있는데, 커스텀 예외를 사용하면 상위 레이어에서 더 명확하게 처리할 수 있어요. 다만 NotificationService에서 이미 try-catch로 처리하고 있으니, 현재 구조에서는 크게 문제되지 않습니다.

♻️ 커스텀 예외 적용 예시 (선택사항)
// EmailSendException.java 생성
public class EmailSendException extends RuntimeException {
    public EmailSendException(String message, Throwable cause) {
        super(message, cause);
    }
}

// EmailService.java에서 사용
} catch (MessagingException e) {
    throw new EmailSendException("이메일 발송 실패: " + email, e);
}
src/main/java/com/finsight/finsight/domain/notification/domain/service/NotificationService.java (1)

44-61: 유저가 많아지면 성능 이슈가 발생할 수 있어요.

현재 구조는 유저별로 3개의 쿼리를 실행합니다 (existsByUserIdAndItemTypeAndSavedAtBetween, existsNewAttemptBetween, existsReviewAttemptBetween). 유저가 1000명이면 최소 3000개의 쿼리가 실행되고, 이메일 발송까지 포함하면 상당한 시간이 소요될 수 있어요.

당장은 문제없겠지만, 스케일업 시 아래 방안들을 고려해볼 수 있어요:

  • 배치 쿼리로 모든 유저의 활동 현황을 한 번에 조회
  • 비동기 이메일 발송 (@Async 또는 메시지 큐 활용)

성능 최적화 방안에 대한 이슈를 열어드릴까요?

Comment on lines +31 to +40
/**
* 학습 알림 이메일 발송 (평문)
*/
public void sendNotificationEmail(String email, String subject, String content) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(email);
message.setSubject(subject);
message.setText(content);
mailSender.send(message);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for all usages of sendNotificationEmail method

echo "=== Searching for sendNotificationEmail usage ==="
rg -n "sendNotificationEmail" --type=java

echo ""
echo "=== Checking EmailService.java location and content ==="
fd "EmailService.java" --type f | head -5

echo ""
echo "=== Checking NotificationService implementation ==="
fd "NotificationService.java" --type f | head -5

Repository: IT-Cotato/12th-FinSight-BE

Length of output: 576


🏁 Script executed:

#!/bin/bash
# Get NotificationService to see which email methods it uses
echo "=== NotificationService.java (checking email method calls) ==="
cat "src/main/java/com/finsight/finsight/domain/notification/domain/service/NotificationService.java"

echo ""
echo "=== EmailService.java (full file for context) ==="
cat "src/main/java/com/finsight/finsight/domain/auth/domain/service/EmailService.java"

Repository: IT-Cotato/12th-FinSight-BE

Length of output: 6648


sendNotificationEmail 메서드는 현재 사용되지 않고 있습니다.

코드베이스 전체를 확인한 결과, sendNotificationEmail은 정의만 되어 있고 어디서도 호출되지 않습니다. NotificationServicesendDailyNotifications()sendWeeklyNotifications() 메서드 모두 sendHtmlEmail()만 사용 중이에요.

불필요한 메서드는 코드 복잡도를 높이고 유지보수를 어렵게 하므로, 향후 사용 계획이 없다면 제거하는 것이 좋습니다. 참고로 sendVerificationEmail()sendHtmlEmail()은 명확한 용도가 있으니 그대로 유지하면 됩니다.

🤖 Prompt for AI Agents
In
`@src/main/java/com/finsight/finsight/domain/auth/domain/service/EmailService.java`
around lines 31 - 40, The sendNotificationEmail(String email, String subject,
String content) method in EmailService is unused and should be removed to reduce
dead code; search the codebase to confirm there are no callers, then delete the
sendNotificationEmail method and any now-unused imports or JavaDoc related to
it, leaving sendVerificationEmail and sendHtmlEmail untouched; if you expect to
reuse plaintext emailing later instead mark it `@Deprecated` and add a TODO
comment instead of deleting.

Comment on lines +37 to +62
@Transactional(readOnly = true)
public void sendDailyNotifications() {
List<UserEntity> users = userRepository.findByNotificationEnabledAndAuthType(AuthType.EMAIL);

LocalDateTime yesterdayStart = LocalDate.now().minusDays(1).atStartOfDay();
LocalDateTime yesterdayEnd = LocalDate.now().atStartOfDay();

for (UserEntity user : users) {
try {
String email = getEmailFromUser(user);
if (email == null) continue;

boolean isNewsSaved = folderItemRepository.existsByUserIdAndItemTypeAndSavedAtBetween(
user.getUserId(), FolderType.NEWS, yesterdayStart, yesterdayEnd);
boolean isQuizSolved = quizAttemptRepository.existsNewAttemptBetween(
user.getUserId(), yesterdayStart, yesterdayEnd);
boolean isQuizReviewed = quizAttemptRepository.existsReviewAttemptBetween(
user.getUserId(), yesterdayStart, yesterdayEnd);

String htmlContent = templateBuilder.buildDailyEmail(isNewsSaved, isQuizSolved, isQuizReviewed);
emailService.sendHtmlEmail(email, "[FinSight] 오늘의 학습 알림", htmlContent);
} catch (Exception e) {
// 개별 유저 실패해도 다음 유저 진행
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

빈 catch 블록에 로깅을 추가해주세요.

개별 유저 실패 시 다음 유저로 진행하는 로직은 좋지만, 예외를 완전히 삼켜버리면 문제 진단이 어려워져요. 최소한 경고 로그라도 남겨두면 운영 시 디버깅에 큰 도움이 됩니다.

🐛 로깅 추가 제안
+import lombok.extern.slf4j.Slf4j;

+@Slf4j
 `@Service`
 `@RequiredArgsConstructor`
 public class NotificationService {
     // ...
             } catch (Exception e) {
-                // 개별 유저 실패해도 다음 유저 진행
+                log.warn("일일 알림 발송 실패 - userId: {}, error: {}", user.getUserId(), e.getMessage());
             }
🤖 Prompt for AI Agents
In
`@src/main/java/com/finsight/finsight/domain/notification/domain/service/NotificationService.java`
around lines 37 - 62, In NotificationService.sendDailyNotifications, avoid the
empty catch: log the exception and contextual info (at least user.getUserId()
and/or email) when handling failures for each user so errors aren't swallowed;
update the catch block to call the class logger (e.g., log.warn or log.error)
with a clear message including userId/email and the exception to aid debugging,
while keeping the loop behavior to continue to next user; reference methods
getEmailFromUser, templateBuilder.buildDailyEmail, and
emailService.sendHtmlEmail to indicate where failures should be logged.

*/
@Transactional(readOnly = true)
public void sendDailyNotifications() {
List<UserEntity> users = userRepository.findByNotificationEnabledAndAuthType(AuthType.EMAIL);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1. findByNotificationEnabledAndAuthType 메서드 정의 위치 찾기
echo "=== 메서드 정의 위치 검색 ==="
rg -n "findByNotificationEnabledAndAuthType" --type=java

echo ""
echo "=== UserRepository 인터페이스 확인 ==="
fd "UserRepository" --type=java -exec cat -n {} \;

echo ""
echo "=== NotificationService 파일 확인 ==="
fd "NotificationService" --type=java -path "*notification*" -exec cat -n {} \;

Repository: IT-Cotato/12th-FinSight-BE

Length of output: 1112


🏁 Script executed:

#!/bin/bash
# UserRepository 파일 내용 확인
echo "=== UserRepository.java 확인 ==="
cat -n src/main/java/com/finsight/finsight/domain/user/persistence/repository/UserRepository.java

echo ""
echo "=== NotificationService.java 확인 ==="
cat -n src/main/java/com/finsight/finsight/domain/notification/domain/service/NotificationService.java

echo ""
echo "=== UserEntity의 notificationEnabled 필드 확인 ==="
find . -name "UserEntity.java" -type f -exec cat -n {} \;

echo ""
echo "=== AuthType enum 확인 ==="
find . -name "AuthType.java" -type f -exec cat -n {} \;

Repository: IT-Cotato/12th-FinSight-BE

Length of output: 10237


N+1 쿼리 최적화가 필요해요.

메서드 자체는 잘 정의되어 있습니다! UserRepository에서 명시적인 @Query로 구현되어 있고, notificationEnabled 필드도 UserEntity에 존재하네요.

다만 성능상 개선할 부분이 있습니다. 현재 쿼리는 JOIN u.userAuths ua로 필터링하지만, SELECT u로만 반환하고 있어요. 이러면 userAuths가 lazy loading이 되는데, 반복문에서 각 user마다 getEmailFromUser(user) 호출 시 user.getUserAuths() 접근으로 인해 추가 쿼리가 발생합니다 (N+1 문제).

수정 방법
`@Query`("""
    SELECT u FROM UserEntity u
    LEFT JOIN FETCH u.userAuths ua
    WHERE u.notificationEnabled = true
    AND ua.authType = :authType
""")
List<UserEntity> findByNotificationEnabledAndAuthType(`@Param`("authType") AuthType authType);

FETCH JOIN을 추가하면 한 번의 쿼리로 userAuths까지 함께 로드되어 N+1 문제를 방지할 수 있습니다.

🤖 Prompt for AI Agents
In
`@src/main/java/com/finsight/finsight/domain/notification/domain/service/NotificationService.java`
at line 39, The current query used by UserRepository's
findByNotificationEnabledAndAuthType returns UserEntity without fetching
userAuths, causing N+1 queries when NotificationService calls
getEmailFromUser(user) which accesses user.getUserAuths(); update the repository
query to use a FETCH JOIN to eagerly load u.userAuths (i.e., LEFT JOIN FETCH
u.userAuths ua) while keeping the same authType filter so the users and their
userAuths are loaded in one query and the N+1 problem is eliminated.

Comment on lines +68 to +93
@Transactional(readOnly = true)
public void sendWeeklyNotifications() {
List<UserEntity> users = userRepository.findByNotificationEnabledAndAuthType(AuthType.EMAIL);

LocalDateTime lastWeekStart = LocalDate.now().minusWeeks(1).with(DayOfWeek.MONDAY).atStartOfDay();
LocalDateTime lastWeekEnd = LocalDate.now().with(DayOfWeek.MONDAY).atStartOfDay();

for (UserEntity user : users) {
try {
String email = getEmailFromUser(user);
if (email == null) continue;

Long quizCount = quizAttemptRepository.countByUserIdAndCreatedAtBetween(
user.getUserId(), lastWeekStart, lastWeekEnd);
Long newsCount = userArticleViewRepository.countByUserIdAndViewedAtBetween(
user.getUserId(), lastWeekStart, lastWeekEnd);

String htmlContent = templateBuilder.buildWeeklyEmail(
quizCount != null ? quizCount : 0,
newsCount != null ? newsCount : 0);
emailService.sendHtmlEmail(email, "[FinSight] 주간 학습 리포트", htmlContent);
} catch (Exception e) {
// 개별 유저 실패해도 다음 유저 진행
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

주간 알림도 동일한 패턴이네요.

일일 알림과 같은 구조라 동일한 개선 포인트(로깅, 성능)가 적용됩니다. catch 블록에 로깅 추가 부탁드려요.

🐛 로깅 추가
             } catch (Exception e) {
-                // 개별 유저 실패해도 다음 유저 진행
+                log.warn("주간 알림 발송 실패 - userId: {}, error: {}", user.getUserId(), e.getMessage());
             }
📝 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
@Transactional(readOnly = true)
public void sendWeeklyNotifications() {
List<UserEntity> users = userRepository.findByNotificationEnabledAndAuthType(AuthType.EMAIL);
LocalDateTime lastWeekStart = LocalDate.now().minusWeeks(1).with(DayOfWeek.MONDAY).atStartOfDay();
LocalDateTime lastWeekEnd = LocalDate.now().with(DayOfWeek.MONDAY).atStartOfDay();
for (UserEntity user : users) {
try {
String email = getEmailFromUser(user);
if (email == null) continue;
Long quizCount = quizAttemptRepository.countByUserIdAndCreatedAtBetween(
user.getUserId(), lastWeekStart, lastWeekEnd);
Long newsCount = userArticleViewRepository.countByUserIdAndViewedAtBetween(
user.getUserId(), lastWeekStart, lastWeekEnd);
String htmlContent = templateBuilder.buildWeeklyEmail(
quizCount != null ? quizCount : 0,
newsCount != null ? newsCount : 0);
emailService.sendHtmlEmail(email, "[FinSight] 주간 학습 리포트", htmlContent);
} catch (Exception e) {
// 개별 유저 실패해도 다음 유저 진행
}
}
}
`@Transactional`(readOnly = true)
public void sendWeeklyNotifications() {
List<UserEntity> users = userRepository.findByNotificationEnabledAndAuthType(AuthType.EMAIL);
LocalDateTime lastWeekStart = LocalDate.now().minusWeeks(1).with(DayOfWeek.MONDAY).atStartOfDay();
LocalDateTime lastWeekEnd = LocalDate.now().with(DayOfWeek.MONDAY).atStartOfDay();
for (UserEntity user : users) {
try {
String email = getEmailFromUser(user);
if (email == null) continue;
Long quizCount = quizAttemptRepository.countByUserIdAndCreatedAtBetween(
user.getUserId(), lastWeekStart, lastWeekEnd);
Long newsCount = userArticleViewRepository.countByUserIdAndViewedAtBetween(
user.getUserId(), lastWeekStart, lastWeekEnd);
String htmlContent = templateBuilder.buildWeeklyEmail(
quizCount != null ? quizCount : 0,
newsCount != null ? newsCount : 0);
emailService.sendHtmlEmail(email, "[FinSight] 주간 학습 리포트", htmlContent);
} catch (Exception e) {
log.warn("주간 알림 발송 실패 - userId: {}, error: {}", user.getUserId(), e.getMessage());
}
}
}
🤖 Prompt for AI Agents
In
`@src/main/java/com/finsight/finsight/domain/notification/domain/service/NotificationService.java`
around lines 68 - 93, In sendWeeklyNotifications (NotificationService) the catch
block currently swallows exceptions; update it to log failures with context and
the exception (e.g., include user.getUserId() and the resolved email from
getEmailFromUser(user)) using the class logger before continuing so individual
user failures are recorded; keep the existing behavior of continuing to next
user after logging.

Comment on lines +23 to +32
if (!isNewsSaved && !isQuizSolved && !isQuizReviewed) {
message = "어제는 기록된 학습이 없었어요.<br>오늘은 뉴스 1개 저장하고, 퀴즈 한 번만 풀어 볼까요?";
emoji = "💪";
} else if (isNewsSaved && isQuizSolved && isQuizReviewed) {
message = "어제 뉴스와 퀴즈 모두 잘 챙기셨어요.<br>오늘도 가볍게 뉴스 1개부터 이어가 볼까요?";
emoji = "🎉";
} else {
message = "어제 저장한 뉴스가 아직 퀴즈를 기다리고 있어요.<br>오늘은 퀴즈 한 번만 이어서 풀어 볼까요?";
emoji = "💪";
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

일일 메시지 분기 로직이 일부 케이스를 정확히 반영하지 못할 수 있어요.

현재 else 분기의 메시지는 "어제 저장한 뉴스가 아직 퀴즈를 기다리고 있어요"인데, 이 메시지는 isNewsSaved=true인 경우에만 적절해요.

하지만 실제로 else 분기는 다음과 같은 경우도 포함됩니다:

  • 퀴즈만 풀고 뉴스는 저장 안 한 경우 (false, true, false)
  • 복습만 한 경우 (false, false, true)

이런 경우에 "저장한 뉴스가 아직 퀴즈를 기다리고 있어요"라는 메시지가 사실과 다를 수 있어요.

💡 개선 제안
-        } else {
-            message = "어제 저장한 뉴스가 아직 퀴즈를 기다리고 있어요.<br>오늘은 퀴즈 한 번만 이어서 풀어 볼까요?";
-            emoji = "💪";
+        } else if (isNewsSaved && !isQuizSolved) {
+            message = "어제 저장한 뉴스가 아직 퀴즈를 기다리고 있어요.<br>오늘은 퀴즈 한 번만 이어서 풀어 볼까요?";
+            emoji = "💪";
+        } else {
+            message = "어제도 꾸준히 학습하셨네요!<br>오늘도 뉴스 1개부터 이어가 볼까요?";
+            emoji = "👍";
         }
📝 Committable suggestion

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

Suggested change
if (!isNewsSaved && !isQuizSolved && !isQuizReviewed) {
message = "어제는 기록된 학습이 없었어요.<br>오늘은 뉴스 1개 저장하고, 퀴즈 한 번만 풀어 볼까요?";
emoji = "💪";
} else if (isNewsSaved && isQuizSolved && isQuizReviewed) {
message = "어제 뉴스와 퀴즈 모두 잘 챙기셨어요.<br>오늘도 가볍게 뉴스 1개부터 이어가 볼까요?";
emoji = "🎉";
} else {
message = "어제 저장한 뉴스가 아직 퀴즈를 기다리고 있어요.<br>오늘은 퀴즈 한 번만 이어서 풀어 볼까요?";
emoji = "💪";
}
if (!isNewsSaved && !isQuizSolved && !isQuizReviewed) {
message = "어제는 기록된 학습이 없었어요.<br>오늘은 뉴스 1개 저장하고, 퀴즈 한 번만 풀어 볼까요?";
emoji = "💪";
} else if (isNewsSaved && isQuizSolved && isQuizReviewed) {
message = "어제 뉴스와 퀴즈 모두 잘 챙기셨어요.<br>오늘도 가볍게 뉴스 1개부터 이어가 볼까요?";
emoji = "🎉";
} else if (isNewsSaved && !isQuizSolved) {
message = "어제 저장한 뉴스가 아직 퀴즈를 기다리고 있어요.<br>오늘은 퀴즈 한 번만 이어서 풀어 볼까요?";
emoji = "💪";
} else {
message = "어제도 꾸준히 학습하셨네요!<br>오늘도 뉴스 1개부터 이어가 볼까요?";
emoji = "👍";
}
🤖 Prompt for AI Agents
In
`@src/main/java/com/finsight/finsight/domain/notification/infrastructure/template/NotificationTemplateBuilder.java`
around lines 23 - 32, The current else branch in NotificationTemplateBuilder
produces a "saved news awaiting quiz" message for any case not covered by the
first two branches, which is incorrect when isNewsSaved is false but
isQuizSolved or isQuizReviewed is true; update the branching to explicitly
handle the remaining combinations instead of a catch-all else: after the
existing checks for (!isNewsSaved && !isQuizSolved && !isQuizReviewed) and
(isNewsSaved && isQuizSolved && isQuizReviewed), add explicit branches such as
(isNewsSaved) to keep the "saved news awaiting quiz" message, (isQuizSolved) to
produce a message acknowledging a completed quiz but no saved news, and
(isQuizReviewed) to produce a message for only review—set the appropriate
message and emoji values in each branch so message/emoji reflect the actual
booleans isNewsSaved, isQuizSolved, isQuizReviewed.

Comment on lines +55 to +57
@Builder.Default
@Column(name = "notification_enabled", nullable = false)
private Boolean notificationEnabled = true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 마이그레이션 관련 디렉토리 구조 확인
find . -type d \( -name "migration" -o -name "migrations" -o -name "flyway" -o -name "liquibase" -o -name "db" \) 2>/dev/null | head -20

Repository: IT-Cotato/12th-FinSight-BE

Length of output: 52


🏁 Script executed:

#!/bin/bash
# notification_enabled 관련 모든 참조 확인
rg -i "notification_enabled" --type java --type sql -n

Repository: IT-Cotato/12th-FinSight-BE

Length of output: 216


🏁 Script executed:

#!/bin/bash
# 리포지토리의 파일 구조 확인
git ls-files | grep -E "(migration|flyway|liquibase|sql)" | head -30

Repository: IT-Cotato/12th-FinSight-BE

Length of output: 52


🏁 Script executed:

#!/bin/bash
# application.properties/yml 파일 검색 및 JPA DDL 설정 확인
find . -type f \( -name "application*.properties" -o -name "application*.yml" \) 2>/dev/null | xargs grep -l "jpa\|hibernate\|ddl" 2>/dev/null | head -10

Repository: IT-Cotato/12th-FinSight-BE

Length of output: 52


🏁 Script executed:

#!/bin/bash
# application.properties/yml 전체 내용 확인
find . -type f \( -name "application.properties" -o -name "application.yml" -o -name "application-*.properties" -o -name "application-*.yml" \) 2>/dev/null

Repository: IT-Cotato/12th-FinSight-BE

Length of output: 52


🏁 Script executed:

#!/bin/bash
# UserEntity 파일 전체 내용 확인 - 버전 정보 및 Entity 클래스 명 확인
cat src/main/java/com/finsight/finsight/domain/user/persistence/entity/UserEntity.java

Repository: IT-Cotato/12th-FinSight-BE

Length of output: 2844


🏁 Script executed:

#!/bin/bash
# SQL 파일이 있는지 전체적으로 확인
find . -type f -name "*.sql" 2>/dev/null

Repository: IT-Cotato/12th-FinSight-BE

Length of output: 52


기존 테이블에 NOT NULL 컬럼 추가 시 마이그레이션 전략 확인 필요합니다.

users 테이블에 notification_enabled 컬럼을 nullable=false로 추가하는데, 저장소에 마이그레이션 파일(Flyway/Liquibase)이나 JPA DDL 설정이 보이지 않습니다.

다음 중 하나를 확인해 주세요:

  • Flyway/Liquibase 마이그레이션 SQL 파일이 있는지
  • application.ymlspring.jpa.hibernate.ddl-auto 설정값 (production에서는 validate여야 함)
  • 기존 데이터 행들에 대한 백필 처리

JPA의 @Builder.Default와 Java 기본값(true)은 신규 Entity 생성 시만 작동하므로, 기존 데이터베이스 행들의 컬럼값 처리가 명시적으로 필요합니다. 마이그레이션 없이 배포하면 스키마 변경 실패로 인한 배포 중단이 발생할 수 있습니다.

🤖 Prompt for AI Agents
In
`@src/main/java/com/finsight/finsight/domain/user/persistence/entity/UserEntity.java`
around lines 55 - 57, The UserEntity field notificationEnabled is declared
non-nullable but existing DB rows aren’t handled; add a proper migration or
config: create a Flyway/Liquibase migration that adds the notification_enabled
column with a default value (e.g., true) and backfills existing rows, then alter
the column to NOT NULL (or add it NOT NULL with DEFAULT then remove default), or
if you rely on JPA DDL only, ensure spring.jpa.hibernate.ddl-auto is set
appropriately for your environment (validate in prod) and do an explicit
backfill migration instead of relying on `@Builder.Default`; locate
UserEntity.notificationEnabled and implement the migration/backfill and
configuration change accordingly.

Comment on lines +71 to +72
public void updateNotificationEnabled(Boolean enabled) {
this.notificationEnabled = enabled;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

null 입력 방어가 필요합니다.

nullable = false 컬럼인데 setter에서 null을 그대로 넣으면 런타임에 제약 위반이 날 수 있어요. 간단히 방어해 두는 게 안전합니다.

✅ 제안 수정
 public void updateNotificationEnabled(Boolean enabled) {
-    this.notificationEnabled = enabled;
+    if (enabled == null) {
+        throw new IllegalArgumentException("notificationEnabled는 null일 수 없습니다.");
+    }
+    this.notificationEnabled = enabled;
 }
📝 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 void updateNotificationEnabled(Boolean enabled) {
this.notificationEnabled = enabled;
public void updateNotificationEnabled(Boolean enabled) {
if (enabled == null) {
throw new IllegalArgumentException("notificationEnabled는 null일 수 없습니다.");
}
this.notificationEnabled = enabled;
}
🤖 Prompt for AI Agents
In
`@src/main/java/com/finsight/finsight/domain/user/persistence/entity/UserEntity.java`
around lines 71 - 72, In updateNotificationEnabled(Boolean enabled) in
UserEntity, guard against null before assigning to the non-nullable field
notificationEnabled; if enabled is null, set notificationEnabled to a safe
default (e.g., false) or throw an IllegalArgumentException — implement a
null-check at the start of the method and assign either enabled.booleanValue()
or the chosen default to this.notificationEnabled to avoid nullable=false
constraint violations at runtime.

Comment on lines +18 to +27
/**
* 알림 활성화된 EMAIL 유저 목록 조회
*/
@Query("""
SELECT u FROM UserEntity u
JOIN u.userAuths ua
WHERE u.notificationEnabled = true
AND ua.authType = :authType
""")
List<UserEntity> findByNotificationEnabledAndAuthType(@Param("authType") AuthType authType);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find UserEntity definition
find src/main/java -name "UserEntity.java" -o -name "UserAuths.java" | head -20

Repository: IT-Cotato/12th-FinSight-BE

Length of output: 152


🏁 Script executed:

#!/bin/bash
# List files in user domain to understand structure
find src/main/java/com/finsight/finsight/domain/user -type f -name "*.java" | head -30

Repository: IT-Cotato/12th-FinSight-BE

Length of output: 583


🏁 Script executed:

#!/bin/bash
# Read UserEntity to see userAuths relationship
cat -n src/main/java/com/finsight/finsight/domain/user/persistence/entity/UserEntity.java

Repository: IT-Cotato/12th-FinSight-BE

Length of output: 3579


🏁 Script executed:

#!/bin/bash
# Read UserAuthEntity structure
cat -n src/main/java/com/finsight/finsight/domain/user/persistence/entity/UserAuthEntity.java

Repository: IT-Cotato/12th-FinSight-BE

Length of output: 3063


🏁 Script executed:

#!/bin/bash
# Search for usages of findByNotificationEnabledAndAuthType
rg "findByNotificationEnabledAndAuthType" src/

Repository: IT-Cotato/12th-FinSight-BE

Length of output: 653


🏁 Script executed:

#!/bin/bash
# Check for other similar queries with DISTINCT or JOIN patterns
rg "SELECT.*FROM.*JOIN.*ua" src/ --type java -A 3 -B 1

Repository: IT-Cotato/12th-FinSight-BE

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Read NotificationService to see how the query result is used
cat -n src/main/java/com/finsight/finsight/domain/notification/domain/service/NotificationService.java

Repository: IT-Cotato/12th-FinSight-BE

Length of output: 5337


JOIN 없이 중복 제거해야 합니다.

같은 사용자가 여러 EMAIL 인증 정보를 가지면 JOIN으로 중복 행이 반환되어, 알림이 같은 사용자에게 여러 번 발송돼요. 실제로 NotificationServicesendDailyNotifications(), sendWeeklyNotifications()에서 루프를 돌면서 중복된 사용자에게 이메일을 반복 발송하게 됩니다. DISTINCT를 추가하면 이 문제를 깔끔하게 해결할 수 있습니다.

✅ 제안 수정
 `@Query`("""
-    SELECT u FROM UserEntity u
+    SELECT DISTINCT u FROM UserEntity u
     JOIN u.userAuths ua
     WHERE u.notificationEnabled = true
     AND ua.authType = :authType
 """)
 List<UserEntity> findByNotificationEnabledAndAuthType(`@Param`("authType") AuthType authType);
🤖 Prompt for AI Agents
In
`@src/main/java/com/finsight/finsight/domain/user/persistence/repository/UserRepository.java`
around lines 18 - 27, The JPQL query in
UserRepository.findByNotificationEnabledAndAuthType selects UserEntity u with a
JOIN on u.userAuths which can return duplicate UserEntity rows when a user has
multiple matching userAuths; update the query to return unique users (e.g.,
change the projection to SELECT DISTINCT u) so the method
findByNotificationEnabledAndAuthType (and downstream callers like
NotificationService.sendDailyNotifications/sendWeeklyNotifications) receive each
UserEntity only once.

@ramen0519 ramen0519 closed this Feb 7, 2026
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.

1 participant