Skip to content

[Feature] 알림 기능 구현#265

Merged
yooooonshine merged 4 commits intodevelopfrom
feature/243-implement-notification
Nov 16, 2025
Merged

[Feature] 알림 기능 구현#265
yooooonshine merged 4 commits intodevelopfrom
feature/243-implement-notification

Conversation

@yooooonshine
Copy link
Contributor

@yooooonshine yooooonshine commented Nov 16, 2025

개요

작업사항

  • 알림 목록 조회 API 구현(목록 조회시 자동으로 읽음 처리)
  • 안 읽은 알림 수 조회 API 구현
  • 알림 생성 기능 구현(코인 전송, 팔로우, 파생 포스트 생성, 포스트 구매)

Summary by CodeRabbit

릴리스 노트

  • New Features
    • 알림 시스템 추가: 팔로우, 포스트 구매, 리뷰, 코인 수신 등 주요 활동에 대한 실시간 알림 제공
    • 알림 조회 및 미읽음 개수 확인 기능 추가
    • 페이지네이션을 통한 알림 목록 조회 기능 추가
    • 읽은 알림 자동 정리 기능 추가

@yooooonshine yooooonshine linked an issue Nov 16, 2025 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Nov 16, 2025

Walkthrough

알림 기능을 구현하고 기존 서비스에 통합합니다. 금전 거래, 팔로우, 게시글 구매 및 리뷰 등의 이벤트에서 알림을 생성하고, 만료된 알림을 정리하는 스케줄러를 추가합니다. 여러 인프라 클래스를 공통 패키지에서 인프라 패키지로 이동시킵니다.

Changes

코호트 / 파일 변경 요약
알림 설정 및 프로퍼티
src/main/java/hanium/modic/backend/common/property/property/NotificationProperties.java, src/main/java/hanium/modic/backend/common/property/config/PropertyConfig.java
NotificationProperties 클래스를 추가하여 알림 읽음 보존 기간을 설정 가능하게 함. 기본값은 30일이며, PropertyConfig에 @EnableConfigurationProperties로 등록.
알림 엔티티 및 저장소
src/main/java/hanium/modic/backend/domain/notification/entity/NotificationEntity.java, src/main/java/hanium/modic/backend/domain/notification/repository/NotificationRepository.java
알림 저장을 위한 JPA 엔티티와 커스텀 쿼리 메서드(활성 알림 조회, 개수 집계, 만료 알림 삭제)를 포함한 저장소 추가.
알림 Enum 타입
src/main/java/hanium/modic/backend/domain/notification/enums/NotificationType.java, src/main/java/hanium/modic/backend/domain/notification/enums/NotificationStatus.java
여섯 가지 알림 유형(COIN_RECEIVED, POST_PURCHASED_BY_COIN 등)과 UNREAD/READ 상태 정의. 각 타입은 제목/본문 생성 메서드 구현.
알림 DTO
src/main/java/hanium/modic/backend/domain/notification/dto/NotificationPayload.java, src/main/java/hanium/modic/backend/domain/notification/dto/GetNotificationsResponse.java, src/main/java/hanium/modic/backend/domain/notification/dto/GetUnreadCountResponse.java
알림 페이로드를 위한 레코드(빌더 패턴 포함), 알림 조회 응답, 미읽음 개수 응답 추가.
알림 서비스
src/main/java/hanium/modic/backend/domain/notification/service/NotificationService.java
알림 조회, 미읽음 개수 카운트, 알림 생성, 만료 알림 정리 메서드 포함. JSON 직렬화/역직렬화 및 읽음 상태 관리 지원.
알림 컨트롤러
src/main/java/hanium/modic/backend/web/notification/controller/NotificationController.java
/api/notifications 엔드포인트: GET으로 페이지 기반 알림 조회(기본 페이지 크기 20), /api/notifications/unread-count로 미읽음 개수 조회.
알림 컨트롤러 DTO
src/main/java/hanium/modic/backend/web/notification/dto/response/NotificationUnreadCountResponse.java
미읽음 개수 응답용 레코드.
스케줄러 인프라
src/main/java/hanium/modic/backend/common/scheduler/DailyScheduler.java, src/main/java/hanium/modic/backend/common/scheduler/config/SchedulingConfig.java
매일 자정에 실행되는 스케줄러로 만료된 알림을 정리. SchedulingConfig에서 @EnableScheduling 활성화.
기존 서비스에 알림 통합
src/main/java/hanium/modic/backend/domain/follow/service/FollowService.java, src/main/java/hanium/modic/backend/domain/user/service/UserCoinService.java, src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java, src/main/java/hanium/modic/backend/domain/postReview/service/PostReviewService.java
각 서비스에서 관련 이벤트(팔로우, 코인 수령, 게시글 구매, 리뷰) 발생 시 알림 생성 로직 추가.
패키지 이동: AMQP
src/main/java/hanium/modic/backend/infra/amqp/config/RabbitMqConfig.java, src/main/java/hanium/modic/backend/infra/amqp/service/MessageQueueService.java
RabbitMqConfig 및 MessageQueueService의 패키지를 common.amqp에서 infra.amqp로 변경.
패키지 이동: Redis
src/main/java/hanium/modic/backend/infra/redis/config/RedisConfig.java, src/main/java/hanium/modic/backend/infra/redis/config/RedissonConfig.java, src/main/java/hanium/modic/backend/infra/redis/distributedLock/*
Redis 관련 클래스(RedisConfig, RedissonConfig, LockManager, LockOptions, AopForTransaction, DistributionLockExecutor)의 패키지를 common.redis에서 infra.redis로 변경.
패키지 이동: SSE
src/main/java/hanium/modic/backend/infra/sse/service/EmitterService.java
EmitterService의 패키지를 common.sse에서 infra.sse로 변경.
패키지 이동 영향: 도메인 레이어
src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/AiImageCreatedListener.java, src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/AiImageCreatedDlqListener.java, src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/DlqListener.java, src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiResponseSseService.java, src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiServerService.java, src/main/java/hanium/modic/backend/domain/postLike/service/PostLikeService.java, src/main/java/hanium/modic/backend/domain/ticket/service/TicketService.java, src/main/java/hanium/modic/backend/domain/user/service/UserVoteStreakService.java, src/main/java/hanium/modic/backend/domain/vote/listener/SimilarityCheckListener.java, src/main/java/hanium/modic/backend/domain/vote/service/AiSimilarityRequestService.java, src/main/java/hanium/modic/backend/domain/vote/service/VoteCompletionRewardService.java, src/main/java/hanium/modic/backend/domain/vote/service/VotingService.java
이동된 클래스의 import 경로를 common에서 infra로 업데이트.
테스트 업데이트
src/test/java/hanium/modic/backend/domain/ai/service/AiImagePermissionServiceTest.java, src/test/java/hanium/modic/backend/domain/follow/service/FollowMockingServiceTest.java, src/test/java/hanium/modic/backend/domain/user/service/UserVoteStreakServiceTest.java, src/test/java/hanium/modic/backend/domain/vote/service/AiSimilarityRequestServiceTest.java, src/test/java/hanium/modic/backend/domain/vote/service/VoteCompletionRewardServiceTest.java, src/test/java/hanium/modic/backend/domain/vote/service/VotingServiceTest.java
이동된 클래스의 import 경로 업데이트 및 알림 서비스 모킹 추가.

Sequence Diagram

sequenceDiagram
    participant User
    participant Service as 서비스<br/>(Follow/Coin/Review 등)
    participant NS as NotificationService
    participant NR as NotificationRepository
    participant Scheduler as DailyScheduler
    
    User->>Service: 이벤트 발생<br/>(팔로우/코인이체/리뷰)
    activate Service
    Service->>Service: 핵심 비즈니스 로직 실행
    Service->>NS: createNotification<br/>(recipientId, type, payload)
    deactivate Service
    
    activate NS
    NS->>NS: generateTitle/Body<br/>(NotificationType 기반)
    NS->>NS: 페이로드 JSON 직렬화
    NS->>NR: save(NotificationEntity)
    NR-->>NS: 저장 완료
    deactivate NS
    
    User->>NS: GET /api/notifications?page=0
    activate NS
    NS->>NR: findActiveNotifications<br/>(userId, now, pageable)
    NR-->>NS: 활성 알림 페이지 반환
    NS->>NS: 페이로드 역직렬화<br/>& 상태 READ로 변경<br/>(readAt + retention)
    NS-->>User: GetNotificationsResponse 페이지 반환
    deactivate NS
    
    User->>NS: GET /api/notifications/unread-count
    activate NS
    NS->>NR: countActiveByRecipientIdAndStatus<br/>(userId, UNREAD, now)
    NR-->>NS: 미읽음 개수
    NS-->>User: GetUnreadCountResponse 반환
    deactivate NS
    
    Note over Scheduler: 매일 자정 0시 0분 0초
    activate Scheduler
    Scheduler->>NS: cleanupExpiredNotifications()
    NS->>NR: deleteByStatusAndExpiresAtBefore<br/>(READ, now)
    NR-->>NS: 만료 알림 삭제 완료
    deactivate Scheduler
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

상세 검토 영역

  • 알림 도메인 전체 (새 기능): NotificationEntity, NotificationRepository, NotificationService의 비즈니스 로직 및 데이터베이스 쿼리 검증 필요. JSON 직렬화/역직렬화 오류 처리, 만료 로직 정확성 확인.
  • 다중 서비스 통합: FollowService, UserCoinService, AiImagePermissionService, PostReviewService에서 알림 생성 로직이 올바르게 트리거되는지, 기존 비즈니스 로직을 훼손하지 않는지 확인.
  • 패키지 이동 영향도: common → infra로 이동한 12개 클래스 및 이를 참조하는 약 20개 파일의 import 경로 변경이 누락되지 않았는지 검증.
  • NotificationController 검증: 페이징 파라미터 기본값, 유효성 검사 제약 조건(page ≥ 0, size 1-50) 확인.
  • DailyScheduler 및 SchedulingConfig: @Scheduled 크론 표현식(0 0 0 * * *) 정확성 및 @EnableScheduling 활성화 확인.

Possibly related PRs

Poem

🐰 알림의 숲을 헤치며 가는 길,

코인이 반짝이고 팔로우가 피어나니!

매일 밤 자정에 정리하는 손길,

사용자의 마음에 닿는 알림이 되려,

리모델링한 집도 이제 제 자리 찾네. 🌙✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.09% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 알림 기능 구현이라는 주요 변경사항을 명확하게 설명합니다.
Description check ✅ Passed PR 설명이 필수 섹션(개요, 작업사항)을 모두 포함하고 구체적인 작업 내용을 상세히 기술합니다.
Linked Issues check ✅ Passed PR의 코드 변경사항이 #243 이슈의 요구사항을 완전히 충족합니다. 알림 생성, 조회 등 전반적인 알림 기능이 구현되었습니다.
Out of Scope Changes check ✅ Passed common에서 infra로의 패키지 이동은 리팩토링으로 관련된 범위 내 변경이며, 모든 변경사항이 알림 기능과 관련된 범위 내입니다.
✨ 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 feature/243-implement-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

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

🧹 Nitpick comments (8)
src/main/java/hanium/modic/backend/domain/notification/dto/NotificationPayload.java (1)

6-6: 불필요한 주석 제거 필요

프로덕션 코드에서는 // 추가됨과 같은 임시 주석을 제거해야 합니다.

-	String senderEmail,   // 추가됨
+	String senderEmail,
src/main/java/hanium/modic/backend/common/scheduler/DailyScheduler.java (2)

16-19: 타임존 설정을 명시하는 것을 권장합니다.

현재 cron 표현식은 서버의 기본 타임존을 사용합니다. 배포 환경에 따라 예상치 못한 시간에 실행될 수 있으므로, zone 속성으로 명시적인 타임존 지정을 고려해보세요.

다음과 같이 타임존을 명시할 수 있습니다:

-	@Scheduled(cron = "0 0 0 * * *")
+	@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
 	public void runEveryMidnight() {
 		notificationService.cleanupExpiredNotifications();
 	}

17-19: 메서드 접근 제어자를 제한하는 것을 고려하세요.

스케줄러 메서드는 Spring에서만 호출되므로 public 대신 package-private으로 변경할 수 있습니다. 단, 외부에서 수동 호출이 필요한 경우 현재 상태를 유지하세요.

-	public void runEveryMidnight() {
+	void runEveryMidnight() {
 		notificationService.cleanupExpiredNotifications();
 	}
src/main/java/hanium/modic/backend/domain/follow/service/FollowService.java (1)

18-20: 팔로우 시 알림 생성 연동은 자연스럽지만, 중복 알림 여부를 한 번 더 점검하면 좋겠습니다.

  • FOLLOW 분기에서 followRepository.insertFollowIfExist(me.getId(), target.getId()) 호출 결과와 무관하게 항상 notificationService.createNotification(...) 이 호출되고 있습니다.
    만약 insertFollowIfExist 가 “이미 팔로우 중이면 아무 것도 하지 않는” 형태라면, 같은 사용자가 반복해서 팔로우 요청을 보낼 때 대상 유저에게 중복 알림이 쌓일 수 있습니다.
    이 경우, 저장 여부(또는 영향 row 수)를 반환하도록 리포지토리 메서드를 확장한 뒤 “실제로 새 팔로우가 생성된 경우에만” 알림을 발송하는 쪽이 UX 상 더 자연해 보입니다.

  • 그 외에는 팔로우 대상(targetId)를 수신자, me 를 액터로 두고 NotificationType.FOLLOWEDNotificationPayload 를 구성한 부분이 도메인 관점에서 직관적입니다.

Also applies to: 33-41, 62-68

src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java (1)

54-64: 추가 쿼리 최적화를 고려해보세요.

현재 구현은 알림 생성을 위해 UserEntity를 다시 조회하고 있습니다(Lines 54-55). NotificationPayload.builder가 userId, name, email을 모두 요구하는 것으로 보이는데, 만약 알림 생성 시 userId만으로도 충분하다면 불필요한 DB 조회를 줄일 수 있습니다.

다만, 현재 방식이 트랜잭션 격리나 데이터 일관성을 위한 의도적인 설계일 수 있으므로 선택적으로 고려해주세요.

src/main/java/hanium/modic/backend/domain/notification/enums/NotificationType.java (1)

14-15: 문자열 생성 방식 개선을 고려해보세요.

현재 문자열 연결 연산자(+)를 사용하고 있는데, 가독성과 유지보수성 향상을 위해 String.format()이나 텍스트 블록 사용을 고려해보세요.

예시:

 @Override
 public String generateBody(NotificationPayload payload) {
-    return sender(payload) + "님이 " + payload.amount() + " 코인을 보내셨습니다.";
+    return String.format("%s님이 %d 코인을 보내셨습니다.", sender(payload), payload.amount());
 }

다만 현재 구현도 충분히 읽기 쉬우므로 선택적으로 적용하시면 됩니다.

Also applies to: 26-30, 42-45, 57-57

src/main/java/hanium/modic/backend/web/notification/controller/NotificationController.java (1)

30-30: 생성자 접근 제한자를 검토하세요.

@RequiredArgsConstructor(access = AccessLevel.PROTECTED)를 사용하고 있는데, 컨트롤러의 생성자는 일반적으로 public이거나 접근 제한자를 지정하지 않습니다. Spring이 빈을 생성할 때 protected 생성자도 접근할 수 있지만, 관례상 명시적인 이유가 없다면 기본값을 사용하는 것이 좋습니다.

-@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
+@RequiredArgsConstructor
src/main/java/hanium/modic/backend/domain/notification/service/NotificationService.java (1)

118-126: 직렬화 예외 처리를 개선하세요.

Line 124에서 일반적인 RuntimeException을 던지고 있습니다. 더 구체적인 예외 타입을 사용하거나 커스텀 예외를 정의하는 것이 좋습니다.

 private String serializePayload(NotificationPayload payload) {
 	try {
 		return objectMapper.writeValueAsString(payload);
 	} catch (Exception e) {
-		throw new RuntimeException("Failed to serialize notification payload", e);
+		throw new AppException(ErrorCode.NOTIFICATION_SERIALIZATION_FAILED, e);
 	}
 }

참고: 이를 위해서는 ErrorCode에 적절한 에러 코드를 추가해야 합니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0a5c4c and 3a479e7.

📒 Files selected for processing (45)
  • src/main/java/hanium/modic/backend/common/property/config/PropertyConfig.java (2 hunks)
  • src/main/java/hanium/modic/backend/common/property/property/NotificationProperties.java (1 hunks)
  • src/main/java/hanium/modic/backend/common/scheduler/DailyScheduler.java (1 hunks)
  • src/main/java/hanium/modic/backend/common/scheduler/config/SchedulingConfig.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java (4 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/AiImageCreatedDlqListener.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/AiImageCreatedListener.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/DlqListener.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiResponseSseService.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiServerService.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/follow/service/FollowService.java (3 hunks)
  • src/main/java/hanium/modic/backend/domain/notification/dto/GetNotificationsResponse.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/notification/dto/GetUnreadCountResponse.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/notification/dto/NotificationPayload.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/notification/entity/NotificationEntity.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/notification/enums/NotificationStatus.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/notification/enums/NotificationType.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/notification/repository/NotificationRepository.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/notification/service/NotificationService.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/postLike/service/PostLikeService.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/postReview/service/PostReviewService.java (3 hunks)
  • src/main/java/hanium/modic/backend/domain/ticket/service/TicketService.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/user/service/UserCoinService.java (4 hunks)
  • src/main/java/hanium/modic/backend/domain/user/service/UserVoteStreakService.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/vote/listener/SimilarityCheckListener.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/vote/service/AiSimilarityRequestService.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/vote/service/VoteCompletionRewardService.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/vote/service/VotingService.java (1 hunks)
  • src/main/java/hanium/modic/backend/infra/amqp/config/RabbitMqConfig.java (1 hunks)
  • src/main/java/hanium/modic/backend/infra/amqp/service/MessageQueueService.java (1 hunks)
  • src/main/java/hanium/modic/backend/infra/redis/config/RedisConfig.java (1 hunks)
  • src/main/java/hanium/modic/backend/infra/redis/config/RedissonConfig.java (1 hunks)
  • src/main/java/hanium/modic/backend/infra/redis/distributedLock/AopForTransaction.java (1 hunks)
  • src/main/java/hanium/modic/backend/infra/redis/distributedLock/DistributionLockExecutor.java (1 hunks)
  • src/main/java/hanium/modic/backend/infra/redis/distributedLock/LockManager.java (1 hunks)
  • src/main/java/hanium/modic/backend/infra/redis/distributedLock/LockOptions.java (1 hunks)
  • src/main/java/hanium/modic/backend/infra/sse/service/EmitterService.java (1 hunks)
  • src/main/java/hanium/modic/backend/web/notification/controller/NotificationController.java (1 hunks)
  • src/main/java/hanium/modic/backend/web/notification/dto/response/NotificationUnreadCountResponse.java (1 hunks)
  • src/test/java/hanium/modic/backend/domain/ai/service/AiImagePermissionServiceTest.java (4 hunks)
  • src/test/java/hanium/modic/backend/domain/follow/service/FollowMockingServiceTest.java (3 hunks)
  • src/test/java/hanium/modic/backend/domain/user/service/UserVoteStreakServiceTest.java (1 hunks)
  • src/test/java/hanium/modic/backend/domain/vote/service/AiSimilarityRequestServiceTest.java (1 hunks)
  • src/test/java/hanium/modic/backend/domain/vote/service/VoteCompletionRewardServiceTest.java (1 hunks)
  • src/test/java/hanium/modic/backend/domain/vote/service/VotingServiceTest.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/hanium/modic/backend/common/scheduler/config/SchedulingConfig.java (1)
src/main/java/hanium/modic/backend/common/property/config/PropertyConfig.java (1)
  • Configuration (20-36)
src/main/java/hanium/modic/backend/domain/notification/entity/NotificationEntity.java (1)
src/main/java/hanium/modic/backend/common/property/property/NotificationProperties.java (1)
  • Getter (10-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (48)
src/main/java/hanium/modic/backend/domain/ticket/service/TicketService.java (1)

10-10: 패키지 리팩토링 변경사항 확인 완료

인프라 관련 클래스들을 common 패키지에서 infra 패키지로 이동시킨 변경사항이 적절하게 반영되었습니다.

src/main/java/hanium/modic/backend/domain/notification/dto/NotificationPayload.java (1)

3-70: 레코드와 빌더 패턴 구현 확인 완료

필수 필드(senderId, senderNickname, senderEmail)와 선택적 필드를 적절히 구분한 빌더 패턴이 잘 구현되어 있습니다. empty() 팩토리 메서드도 기본값 처리에 유용합니다.

src/main/java/hanium/modic/backend/infra/sse/service/EmitterService.java (1)

1-1: 패키지 리팩토링 변경사항 확인 완료

SSE 서비스를 인프라 패키지로 이동시킨 변경사항이 적절합니다.

src/main/java/hanium/modic/backend/domain/vote/service/VoteCompletionRewardService.java (1)

11-11: 패키지 리팩토링 변경사항 확인 완료

LockManager 임포트 경로가 새로운 인프라 패키지 구조에 맞게 업데이트되었습니다.

src/test/java/hanium/modic/backend/domain/user/service/UserVoteStreakServiceTest.java (1)

19-19: 테스트 코드 업데이트 확인 완료

테스트에서 사용하는 LockManager 임포트 경로가 새로운 패키지 구조에 맞게 업데이트되었습니다.

src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/AiImageCreatedListener.java (1)

3-3: 패키지 리팩토링 변경사항 확인 완료

RabbitMQ 설정 클래스의 임포트 경로가 인프라 패키지로 적절히 업데이트되었습니다.

src/main/java/hanium/modic/backend/domain/vote/service/VotingService.java (1)

14-14: 패키지 리팩토링 변경사항 확인 완료

분산 락 매니저의 임포트 경로가 새로운 인프라 패키지 구조에 맞게 업데이트되었습니다.

src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiServerService.java (1)

15-15: 패키지 리팩토링 변경사항 확인 완료

메시지 큐 서비스의 임포트 경로가 인프라 패키지로 적절히 업데이트되었습니다.

src/test/java/hanium/modic/backend/domain/vote/service/VoteCompletionRewardServiceTest.java (1)

19-19: 패키지 리팩토링이 올바르게 적용되었습니다.

LockManager의 import 경로가 common.redis.distributedLock에서 infra.redis.distributedLock으로 정상적으로 변경되었습니다. 테스트 로직은 영향을 받지 않습니다.

src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/AiImageCreatedDlqListener.java (1)

3-3: 패키지 리팩토링이 올바르게 적용되었습니다.

RabbitMqConfig의 static import 경로가 common.amqp.config에서 infra.amqp.config로 정상적으로 변경되었습니다.

src/main/java/hanium/modic/backend/domain/postLike/service/PostLikeService.java (1)

15-15: 패키지 리팩토링이 올바르게 적용되었습니다.

LockManager의 import 경로가 common.redis.distributedLock에서 infra.redis.distributedLock으로 정상적으로 변경되었습니다.

src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/DlqListener.java (1)

3-3: 패키지 리팩토링이 올바르게 적용되었습니다.

RabbitMqConfig의 static import 경로가 common.amqp.config에서 infra.amqp.config로 정상적으로 변경되었습니다.

src/test/java/hanium/modic/backend/domain/vote/service/AiSimilarityRequestServiceTest.java (1)

3-3: 패키지 리팩토링이 올바르게 적용되었습니다.

RabbitMqConfig의 static import 경로가 common.amqp.config에서 infra.amqp.config로 정상적으로 변경되었습니다.

src/main/java/hanium/modic/backend/domain/vote/listener/SimilarityCheckListener.java (1)

4-4: 패키지 리팩토링이 올바르게 적용되었습니다.

RabbitMqConfig의 static import 경로가 common.amqp.config에서 infra.amqp.config로 정상적으로 변경되었습니다.

src/test/java/hanium/modic/backend/domain/vote/service/VotingServiceTest.java (1)

14-14: 패키지 리팩토링이 올바르게 적용되었습니다.

LockManager의 import 경로가 common.redis.distributedLock에서 infra.redis.distributedLock으로 정상적으로 변경되었습니다.

src/main/java/hanium/modic/backend/domain/user/service/UserVoteStreakService.java (1)

7-7: 패키지 리팩토링이 올바르게 적용되었습니다.

인프라 관련 클래스를 infra 패키지로 이동하는 것은 적절한 구조 개선입니다.

src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiResponseSseService.java (1)

11-11: SSE 서비스 임포트 경로 업데이트가 적절합니다.

인프라 계층 분리에 따른 일관된 패키지 구조 개선입니다.

src/main/java/hanium/modic/backend/infra/redis/distributedLock/LockManager.java (1)

1-1: 인프라 패키지로의 이동이 적절합니다.

분산 락 매니저는 인프라 계층에 위치하는 것이 올바른 아키텍처 구조입니다.

src/main/java/hanium/modic/backend/domain/vote/service/AiSimilarityRequestService.java (1)

3-3: RabbitMQ 설정 임포트 경로 변경이 올바릅니다.

AMQP 설정의 인프라 패키지 이동에 맞춰 정적 임포트가 적절히 업데이트되었습니다.

src/main/java/hanium/modic/backend/infra/redis/distributedLock/AopForTransaction.java (1)

1-1: 트랜잭션 AOP 컴포넌트의 패키지 이동이 적절합니다.

분산 락 관련 유틸리티 클래스들이 일관되게 인프라 패키지로 이동되었습니다.

src/main/java/hanium/modic/backend/infra/redis/distributedLock/LockOptions.java (1)

1-1: 락 옵션 클래스의 패키지 이동이 일관됩니다.

분산 락 관련 모든 컴포넌트가 인프라 패키지로 일관되게 재구성되었습니다.

src/main/java/hanium/modic/backend/common/scheduler/config/SchedulingConfig.java (1)

1-9: 스케줄링 설정이 올바르게 추가되었습니다.

알림 기능 구현의 일환으로 스케줄링 지원을 활성화하는 것은 적절합니다. 만료된 알림 정리 등의 주기적 작업에 필요한 설정입니다.

src/main/java/hanium/modic/backend/infra/redis/config/RedissonConfig.java (1)

1-1: Redisson 설정의 패키지 이동이 적절합니다.

Redis 관련 설정과 인프라 컴포넌트들이 일관되게 infra.redis 패키지로 정리되었습니다.

src/main/java/hanium/modic/backend/infra/redis/config/RedisConfig.java (1)

1-1: Redis 설정 클래스를 infra 레이어로 이동한 구조 정리가 적절합니다.

commoninfra.redis.config 로 패키지를 옮겨 인프라 설정 클래스들의 위치가 일관되게 정리된 점이 유지보수 관점에서 좋습니다.

src/main/java/hanium/modic/backend/domain/notification/enums/NotificationStatus.java (1)

1-6: 알림 상태 값을 READ/UNREAD 두 가지로 단순하게 분리한 설계가 명확합니다.

현재 요구사항(읽음/안 읽음)에는 충분히 대응하고, 추후 상태가 늘어나더라도 상수만 추가하면 되도록 확장 여지도 열려 있어서 괜찮습니다.

src/main/java/hanium/modic/backend/common/property/config/PropertyConfig.java (1)

10-10: Notification 관련 설정을 공통 PropertyConfig 에 통합한 방향이 좋습니다.

NotificationProperties@EnableConfigurationProperties 목록에 추가해서 notification.* 설정을 타입 세이프하게 주입할 수 있게 한 점이 구성 관리 측면에서 깔끔합니다.

Also applies to: 32-33

src/main/java/hanium/modic/backend/infra/amqp/config/RabbitMqConfig.java (1)

1-1: RabbitMQ 설정 클래스를 infra.amqp 패키지로 이동한 것이 구조적으로 자연스럽습니다.

Redis 등 다른 인프라 설정 클래스들과 동일한 infra 네이밍으로 맞춰서 레이어링이 더 명확해졌습니다.

src/main/java/hanium/modic/backend/domain/notification/dto/GetUnreadCountResponse.java (1)

1-6: 읽지 않은 알림 개수를 별도 응답 DTO 로 분리한 구성이 명확합니다.

도메인 서비스에서 계산한 unreadCount 를 이 DTO를 통해 그대로 노출하므로, 이후 필드가 늘어나더라도 API 스펙 관리가 수월해질 것 같습니다.

src/main/java/hanium/modic/backend/infra/redis/distributedLock/DistributionLockExecutor.java (1)

1-1: 패키지 구조 개선이 적절합니다.

인프라 관련 코드를 common에서 infra 패키지로 이동한 것은 적절한 구조 개선입니다.

src/main/java/hanium/modic/backend/infra/amqp/service/MessageQueueService.java (1)

1-3: LGTM!

패키지 리팩토링이 일관되게 적용되었습니다.

src/test/java/hanium/modic/backend/domain/follow/service/FollowMockingServiceTest.java (1)

49-49: 테스트 모킹이 적절히 업데이트되었습니다.

프로덕션 코드의 NotificationService 의존성 추가에 맞춰 테스트도 올바르게 수정되었습니다.

Also applies to: 78-78

src/main/java/hanium/modic/backend/web/notification/dto/response/NotificationUnreadCountResponse.java (1)

5-11: LGTM!

간단한 DTO 구조로 문제없습니다.

src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java (1)

81-91: 알림 생성 로직이 일관되게 구현되었습니다.

티켓 구매 시에도 동일한 패턴으로 알림을 생성하고 있어 좋습니다.

src/main/java/hanium/modic/backend/domain/postReview/service/PostReviewService.java (1)

80-88: 알림 기능이 적절히 통합되었습니다.

리뷰 생성 후 게시글 작성자에게 알림을 보내는 로직이 올바르게 구현되었습니다. user 파라미터를 활용하여 추가 DB 조회 없이 알림을 생성하는 점도 효율적입니다.

src/main/java/hanium/modic/backend/domain/notification/enums/NotificationType.java (2)

91-96: 널 안전성 처리가 적절합니다.

sender() 메서드에서 이메일이 없는 경우를 처리하고 있어 좋습니다.


56-57: 리뷰 내용 출력 시 보안을 고려하세요.

Line 57에서 payload.reviewContent()를 직접 메시지에 포함하고 있습니다. 만약 이 알림이 웹에서 렌더링될 경우 XSS 공격 가능성이 있으므로, 프레젠테이션 레이어에서 적절한 이스케이핑 처리가 되는지 확인해주세요.

src/main/java/hanium/modic/backend/domain/notification/dto/GetNotificationsResponse.java (1)

19-29: 이 리뷰 코멘트는 제거 가능합니다.

getCreateAt() 메서드는 정상적입니다. NotificationEntityBaseEntity를 상속하며, BaseEntity@CreatedDate 필드명이 의도적으로 createAt으로 정의되어 있고 데이터베이스 컬럼명은 create_at입니다. 이 네이밍은 코드베이스 전체에서 일관되게 사용되고 있습니다.

src/main/java/hanium/modic/backend/domain/notification/repository/NotificationRepository.java (1)

15-44: LGTM! 리포지토리 쿼리 메서드가 잘 구현되었습니다.

JPQL 쿼리가 올바르게 작성되었으며, 활성 알림 필터링 로직(expiresAt이 NULL이거나 미래)이 일관되게 적용되었습니다. 벌크 삭제를 위한 @Modifying 어노테이션 사용도 적절합니다.

src/main/java/hanium/modic/backend/domain/notification/entity/NotificationEntity.java (2)

59-78: LGTM! 엔티티 생성자가 잘 구현되었습니다.

status 필드에 대한 null 안전성 처리(Line 72)가 적절하며, 기본값으로 UNREAD를 설정하는 것이 합리적입니다.


84-91: LGTM! 읽음 처리 로직이 잘 구현되었습니다.

이미 읽은 알림에 대한 중복 처리를 방지하는 가드 조건(Line 85-87)이 적절하며, retention 기간에 따른 만료 시간 설정 로직(Line 90)도 올바릅니다.

src/main/java/hanium/modic/backend/domain/user/service/UserCoinService.java (1)

52-59: 수신자 존재 확인 로직이 잘 추가되었습니다.

자기 자신에게 송금하는 것을 방지하고(Line 53-55), 수신자가 존재하는지 검증하는 로직(Line 58-59)이 적절합니다.

src/main/java/hanium/modic/backend/web/notification/controller/NotificationController.java (2)

37-49: LGTM! 안 읽은 알림 수 조회 엔드포인트가 잘 구현되었습니다.

인증된 사용자 확인, 서비스 호출, 응답 변환이 적절하게 처리되었습니다.


80-87: LGTM! 페이지네이션 파라미터 검증이 잘 구현되었습니다.

페이지 번호와 크기에 대한 검증 제약 조건이 적절합니다(page >= 0, size 1-50).

src/main/java/hanium/modic/backend/domain/notification/service/NotificationService.java (3)

128-141: LGTM! 역직렬화 에러 처리가 잘 구현되었습니다.

null/빈 문자열 체크(Line 131-133)와 역직렬화 실패 시 빈 페이로드 반환(Line 139)이 적절합니다. 실패 시 로깅도 포함되어 있어 디버깅에 유용합니다.


40-60: LGTM! 알림 조회 및 읽음 처리 로직이 잘 구현되었습니다.

페이지네이션, 필터링, 읽음 처리가 트랜잭션 내에서 적절하게 처리되었습니다.


34-34: 리뷰 코멘트는 부정확합니다 - 현재 코드가 올바릅니다.

검증 결과, BaseEntity의 실제 필드명은 createAt이며, NotificationService.java의 Line 34에서 사용 중인 "createAt"이 정확히 일치합니다. 리뷰 코멘트에서 지적한 "createdAt"은 BaseEntity에 존재하지 않습니다.

Likely an incorrect or invalid review comment.

src/test/java/hanium/modic/backend/domain/ai/service/AiImagePermissionServiceTest.java (2)

55-72: LGTM! 알림 서비스 통합을 위한 테스트 업데이트가 잘 되었습니다.

새로운 의존성인 NotificationServiceUserEntityRepository에 대한 Mock 설정(Line 55-59, 71-72)이 적절하게 추가되었습니다. 테스트가 새로운 알림 생성 흐름을 올바르게 반영하고 있습니다.


137-138: LGTM! 티켓 구매 테스트의 Mock 설정이 잘 업데이트되었습니다.

코인 구매 테스트와 동일하게 알림 관련 Mock 설정이 추가되어 일관성이 유지되고 있습니다.

Comment on lines +10 to +11
@Getter
@Setter
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

중복된 getter/setter를 제거하세요.

Lombok의 @Getter@Setter 어노테이션을 사용하면서 Line 20-26에서 명시적으로 getter/setter를 다시 정의하고 있어 중복입니다.

다음 중 하나를 선택하세요:

  1. Lombok 어노테이션을 유지하고 Line 20-26의 명시적 메서드를 제거
  2. Lombok 어노테이션을 제거하고 명시적 메서드만 유지

권장 방안: Lombok 어노테이션 유지

 @Getter
 @Setter
 @ConfigurationProperties(prefix = "notification")
 public class NotificationProperties {
 
 	/**
 	 * 읽은 알림을 얼마 동안 유지할지(분 단위)
 	 */
 	private long readRetentionMinutes = Duration.ofDays(30).toMinutes(); // 기본값: 30일
 
-	public long getReadRetentionMinutes() {
-		return readRetentionMinutes;
-	}
-
-	public void setReadRetentionMinutes(long readRetentionMinutes) {
-		this.readRetentionMinutes = readRetentionMinutes;
-	}
-
 	public Duration getReadRetentionDuration() {
 		return Duration.ofMinutes(readRetentionMinutes);
 	}
 }
📝 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
@Getter
@Setter
@Getter
@Setter
@ConfigurationProperties(prefix = "notification")
public class NotificationProperties {
/**
* 읽은 알림을 얼마 동안 유지할지( 단위)
*/
private long readRetentionMinutes = Duration.ofDays(30).toMinutes(); // 기본값: 30일
public Duration getReadRetentionDuration() {
return Duration.ofMinutes(readRetentionMinutes);
}
}
🤖 Prompt for AI Agents
In
src/main/java/hanium/modic/backend/common/property/property/NotificationProperties.java
around lines 10-11 and 20-26, you have duplicated getters/setters: Lombok
@Getter/@Setter are applied at the class level while explicit getter and setter
methods are defined on lines 20-26; remove the explicit methods (lines 20-26)
and keep the Lombok annotations so the compiler-generated accessors are used,
ensuring imports for Lombok remain and no other references rely on the removed
method bodies.

Comment on lines +72 to +79
// 알림
notificationService.createNotification(
toUserId,
NotificationType.COIN_RECEIVED,
NotificationPayload.builder(fromUserId, toUser.getName(), toUser.getEmail())
.amount(coin)
.build()
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

알림 페이로드에 이메일 포함 여부를 검토하세요.

Line 76에서 toUser.getEmail()을 알림 페이로드에 포함하고 있습니다. 이메일은 민감한 개인정보(PII)이므로 다음을 검토하세요:

  • 알림 페이로드에 이메일이 반드시 필요한지 확인
  • 필요하다면, 페이로드가 로그에 기록되지 않도록 보장
  • 불필요하다면, 이메일을 페이로드에서 제거

다음 스크립트로 NotificationPayload가 로그에 기록되는지 확인하세요:


🏁 Script executed:

#!/bin/bash
# NotificationPayload가 로그에 기록되는지 확인
rg -n "log\." --type=java -A 2 -B 2 | rg -i "payload|notificationpayload"

Length of output: 467


추가 검증이 필요합니다. 알림 페이로드의 로깅 범위와 이메일의 필요성을 확인하겠습니다.


🏁 Script executed:

#!/bin/bash
# 1. 모든 로깅 문 확인 (notification, payload 관련)
echo "=== All logging with notification/payload ==="
rg -n "log\.(debug|info|warn|error)" --type=java -B 2 -A 2 | rg -i "notification|payload" -B 2 -A 2

echo ""
echo "=== NotificationPayload class definition ==="
fd -t f "NotificationPayload.java" --exec cat {}

echo ""
echo "=== Email usage in notification context ==="
rg -n "getEmail\|setEmail" --type=java -B 3 -A 3 | head -50

Length of output: 2526


🏁 Script executed:

#!/bin/bash
# NotificationPayload가 로그에 노출되는 다른 경로 확인
echo "=== All places where NotificationPayload is handled ==="
rg -n "NotificationPayload|notification.*Payload" --type=java -B 1 -A 1 | grep -E "(\.builder|\.build|\.toString|log\.|return)" -B 1 -A 1 | head -80

Length of output: 6624


이메일의 실제 필요성과 다른 로깅 경로를 확인하겠습니다.


🏁 Script executed:

#!/bin/bash
# sender() 메서드 구현 확인 및 이메일 사용 여부
echo "=== NotificationType sender() method ==="
rg -n "private.*sender\|sender.*payload" --type=java -A 5 -B 1

echo ""
echo "=== Check if senderEmail is actually used anywhere ==="
rg -n "senderEmail\|\.senderEmail" --type=java -B 2 -A 2

echo ""
echo "=== Check for toString() or serialization of payload ==="
rg -n "toString\(\)|@ToString|ObjectMapper|writeValueAsString|asJson" --type=java -B 2 -A 2 | grep -i "payload\|notification" -B 2 -A 2

Length of output: 2790


🏁 Script executed:

#!/bin/bash
# NotificationEntity와 payload 저장 방식 확인
echo "=== NotificationEntity structure ==="
fd -t f "NotificationEntity.java" --exec cat {}

echo ""
echo "=== How payload is stored and retrieved ==="
rg -n "payload.*save\|save.*payload\|setPayload" --type=java -B 3 -A 3

Length of output: 2620


추가 검증을 통해 문제를 확인했습니다.


🏁 Script executed:

#!/bin/bash
# sender() 메서드가 실제로 NotificationType에 있는지 더 자세히 확인
echo "=== NotificationType full content ==="
fd -t f "NotificationType.java" --exec cat {}

Length of output: 2432


검증 완료. 최종 분석 결과를 정리하겠습니다.

확인 결과, sender() 메서드가 NotificationType.java에 존재하며, 이메일이 실제로 알림 메시지에 사용됩니다 ("nickname(email)" 형식으로 표시). 하지만 PII 로깅 문제는 여전히 유효합니다.

이메일 필드 제거 필요 - 알림 페이로드의 이메일이 오류 로그에 노출됩니다.

NotificationService.java 라인 138에서 역직렬화 실패 시 페이로드 전체가 로그에 기록됩니다:

log.warn("Failed to deserialize notification payload: {}", payload, exception);

이는 다음 문제를 야기합니다:

  • NotificationPayload는 record이므로 자동으로 toString()이 모든 필드(senderEmail 포함)를 출력
  • 예외 발생 시 민감한 이메일 정보가 운영 환경 로그에 노출
  • 이메일은 NotificationType의 sender() 메서드에서 알림 메시지에 포함되므로, 데이터베이스에 평문 JSON으로 저장될 때도 노출

필요한 조치:

  • UserCoinService.java 라인 76의 .builder(fromUserId, toUser.getName(), toUser.getEmail()) 호출에서 이메일 제거 또는
  • NotificationService.java 라인 138의 로깅 시 페이로드를 마스킹/필터링하여 이메일 제외
🤖 Prompt for AI Agents
In src/main/java/hanium/modic/backend/domain/user/service/UserCoinService.java
around lines 72-79, the NotificationPayload is being built with the recipient's
email which leads to PII (email) being stored and potentially logged; remove the
email argument from NotificationPayload.builder (i.e., stop passing
toUser.getEmail()) so the payload does not include email, and if the
builder/signature does not have an overload, add a builder/constructor variant
or adjust NotificationPayload to omit the email field; after change, run
compilation and update any callers/tests and ensure NotificationService
logging/masking still prevents accidental PII exposure.

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