-
Notifications
You must be signed in to change notification settings - Fork 0
Refactor/#73 관리자가 유저에게 보상 지급 시 유저에게 알림이 가도록 구현 #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Walkthrough보상 부여 시 이벤트 기반 아키텍처를 도입합니다. 사용자에게 보상을 부여할 때 RewardGrantedEvent를 발행하고, RewardPushListener가 이를 구독하여 알림 저장 및 Firebase 푸시 메시지 전송을 수행합니다. Changes
Sequence DiagramsequenceDiagram
participant ManagerService
participant ApplicationEventPublisher
participant RewardPushListener
participant NotificationService
participant FirebaseCloudMessageService
ManagerService->>ManagerService: grantRewardToUser()
ManagerService->>ManagerService: 보상 저장 및 사용자 상태 업데이트
ManagerService->>ApplicationEventPublisher: publishEvent(RewardGrantedEvent)
ApplicationEventPublisher->>RewardPushListener: handleRewardGrantedEvent() [비동기]
RewardPushListener->>NotificationService: saveRewardGrantedNotification()
NotificationService->>NotificationService: 사용자 조회 및 검증
NotificationService->>NotificationService: REWARD_GRANTED 타입 알림 생성 및 저장
RewardPushListener->>RewardPushListener: 토픽명 생성 및 로깅
RewardPushListener->>FirebaseCloudMessageService: publishMessage()
FirebaseCloudMessageService->>FirebaseCloudMessageService: REWARD_GRANTED 페이로드 포함 FCM 전송
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧹 Recent nitpick comments
📜 Recent review detailsConfiguration used: Organization UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (1)
🧰 Additional context used🧬 Code graph analysis (1)src/main/java/com/campus/campus/domain/notification/application/service/NotificationService.java (1)
🔇 Additional comments (1)
✏️ Tip: You can disable this entire section by setting 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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In
`@src/main/java/com/campus/campus/domain/notification/application/dto/NotificationListResponse.java`:
- Around line 5-9: Remove the unused DTO class NotificationListResponse: delete
the NotificationListResponse record and any import or reference to it, since
CursorResponse<NotificationResponse> is already used by NotificationController
and NotificationService; ensure there are no lingering imports, tests, or
compilation references to NotificationListResponse after deletion and run a
build to confirm clean compile.
In
`@src/main/java/com/campus/campus/domain/notification/application/exception/ErrorCode.java`:
- Around line 14-16: Update the numeric codes for the notification domain enum
entries in ErrorCode so they no longer collide with the review domain: change
NOTIFICATION_NOT_FOUND from 2801 to 2901, NOTIFICATION_ACCESS_DENIED from 2802
to 2902, and INVALID_NOTIFICATION_IDS from 2803 to 2903 in the ErrorCode enum
definition (the constants NOTIFICATION_NOT_FOUND, NOTIFICATION_ACCESS_DENIED,
INVALID_NOTIFICATION_IDS).
🧹 Nitpick comments (11)
src/main/java/com/campus/campus/domain/manager/application/dto/request/RewardGrantedEvent.java (1)
1-10: LGTM!이벤트 record 클래스가 간결하게 잘 구현되었습니다.
@Builder패턴을 사용하여 가독성 좋은 객체 생성을 지원합니다.참고 (선택사항): 이 클래스는 Spring Application Event로 사용되므로,
dto/request패키지보다는event패키지에 위치시키는 것이 의미상 더 명확할 수 있습니다.src/main/java/com/campus/campus/domain/notification/util/TimeFormatter.java (1)
16-19: 미래 시간 입력에 대한 방어 로직 고려
dateTime이 미래 시간인 경우duration.getSeconds()가 음수가 되어 예상치 못한 결과가 반환될 수 있습니다. 현재 사용 용도상 문제가 되지 않을 수 있지만, 방어적 처리를 추가하면 더 안전합니다.♻️ 미래 시간 처리 제안
LocalDateTime now = LocalDateTime.now(); Duration duration = Duration.between(dateTime, now); long seconds = duration.getSeconds(); + if (seconds < 0) { + return "방금 전"; // 또는 적절한 기본값 + }src/main/java/com/campus/campus/domain/notification/domain/entity/Notification.java (1)
23-69: LGTM!JPA 엔티티가 잘 설계되었습니다.
FetchType.LAZY,EnumType.STRING사용, protected no-args 생성자 등 JPA 모범 사례를 잘 따르고 있습니다.운영 관점 권고 (선택사항): 알림 목록 조회 시
user_id컬럼에 대한 인덱스가 필요할 수 있습니다. 데이터 증가에 따라 조회 성능 최적화를 위해 DB 인덱스 추가를 고려해 주세요.`@Table`(indexes = `@Index`(name = "idx_notification_user_id", columnList = "user_id"))src/main/java/com/campus/campus/domain/notification/application/dto/CursorResponse.java (1)
5-9: LGTM! 일관성 관련 참고 사항제네릭 record를 활용한 깔끔한 페이지네이션 응답 구현입니다.
NextCursor로 커서를 캡슐화하여 API 응답 구조가 명확합니다.참고로,
CursorPageReviewResponse(review 도메인)는nextCursorCreatedAt,nextCursorId를 별도 필드로 사용하는 반면, 이 구현은NextCursor객체로 래핑합니다. 향후 전체 코드베이스에서 커서 응답 패턴을 통일하는 것을 고려해 볼 수 있습니다.src/main/java/com/campus/campus/domain/manager/application/service/ManagerService.java (1)
134-134: 하드코딩된 문자열 상수 추출 고려
"스탬프 보상"문자열이 하드코딩되어 있습니다. 향후 유지보수성을 위해 상수로 추출하는 것을 고려해 볼 수 있습니다.♻️ 제안된 리팩토링
+ private static final String STAMP_REWARD_NAME = "스탬프 보상"; + `@Transactional` public void grantRewardToUser(Long userId, RewardRequest rewardRequest) { // ... - eventPublisher.publishEvent(managerMapper.createRewardGrantedEvent(userId, "스탬프 보상")); + eventPublisher.publishEvent(managerMapper.createRewardGrantedEvent(userId, STAMP_REWARD_NAME)); }src/main/java/com/campus/campus/domain/manager/application/service/RewardPushListener.java (1)
27-47: DB 저장과 FCM 전송 간 에러 처리 고려 필요현재
saveRewardGrantedNotification실패 시에도 FCM 전송이 시도되고, 반대로 FCM 전송 실패 시에도 알림은 이미 DB에 저장된 상태가 됩니다. 부분 실패 시나리오에 대한 처리가 필요한지 검토해 주세요.♻️ 에러 처리 추가 제안
`@Async` `@TransactionalEventListener`(phase = TransactionPhase.AFTER_COMMIT) public void handleRewardGrantedEvent(RewardGrantedEvent rewardGrantedEvent) { String title = "보상 지급 알림"; String body = "스탬프 보상이 지급되었습니다. 보상함을 확인해주세요."; - notificationService.saveRewardGrantedNotification(rewardGrantedEvent.userId(), title, body); + try { + notificationService.saveRewardGrantedNotification(rewardGrantedEvent.userId(), title, body); + } catch (Exception e) { + log.error("[PUSH] Failed to save reward notification. userId={}", rewardGrantedEvent.userId(), e); + return; + } String userTopic = "user_" + rewardGrantedEvent.userId(); log.info("[PUSH] Reward granted. topic={}, userId={}", userTopic, rewardGrantedEvent.userId()); - firebaseCloudMessageService.sendToTopic( - userTopic, - title, - body, - Map.of( - DATA_KEY_TYPE, DATA_TYPE_REWARD_GRANTED - ) - ); + try { + firebaseCloudMessageService.sendToTopic( + userTopic, + title, + body, + Map.of( + DATA_KEY_TYPE, DATA_TYPE_REWARD_GRANTED + ) + ); + } catch (Exception e) { + log.error("[PUSH] Failed to send FCM. topic={}, userId={}", userTopic, rewardGrantedEvent.userId(), e); + } }src/main/java/com/campus/campus/domain/notification/presentation/NotificationResponseCode.java (2)
10-12: 다른 ResponseCode enum과의 일관성을 위해@AllArgsConstructor사용 권장
EmailVerificationResponseCode,StampResponseCode등 다른 ResponseCode enum들은@AllArgsConstructor를 사용하고 있습니다. 코드베이스 전반의 일관성을 위해 동일한 어노테이션 사용을 권장합니다.♻️ 어노테이션 변경 제안
`@Getter` -@RequiredArgsConstructor +@AllArgsConstructor public enum NotificationResponseCode implements ResponseCodeInterface {
14-19: 메시지 포맷 일관성 검토일부 메시지는 "~성공"으로 끝나고,
NOTIFICATION_UNREAD_EXISTS_SUCCESS는 "~성공했습니다."로 끝납니다. 일관된 메시지 포맷을 사용하면 좋겠습니다.src/main/java/com/campus/campus/domain/notification/presentation/NotificationController.java (1)
24-29: 미사용@Slf4j어노테이션
@Slf4j어노테이션이 선언되어 있지만 컨트롤러 내에서 로깅을 사용하지 않습니다. 제거하거나, 필요시 로깅을 추가해 주세요.♻️ 미사용 어노테이션 제거
-@Slf4j `@RestController` `@RequestMapping`("/notifications") `@RequiredArgsConstructor` `@Tag`(name = "알림", description = "알림 관련 API") public class NotificationController {src/main/java/com/campus/campus/domain/notification/application/service/NotificationService.java (2)
38-69: 읽기 작업에@Transactional(readOnly = true)누락
getNotificationsByCursor메서드는 데이터베이스에서 데이터를 조회하는 읽기 작업이지만@Transactional어노테이션이 없습니다. 다른 읽기 메서드인hasUnread에는@Transactional(readOnly = true)가 적용되어 있으므로 일관성을 위해 추가하는 것이 좋습니다.♻️ 제안하는 수정 사항
+ `@Transactional`(readOnly = true) public CursorResponse<NotificationResponse> getNotificationsByCursor( Long userId, LocalDateTime cursorCreatedAt, Long cursorId, int limit ) {
120-132: 토픽 파싱 시 예외 처리 부재
topic.split("_")의 결과가 예상과 다를 경우(예:_가 없거나, ID가 숫자가 아닐 경우)ArrayIndexOutOfBoundsException또는NumberFormatException이 발생할 수 있습니다. 비동기 이벤트 처리에서 이러한 예외는 디버깅을 어렵게 만들 수 있습니다.♻️ 방어적 코드 추가 제안
private List<User> findUsersByTopic(String topic) { + if (topic == null || !topic.contains("_")) { + log.warn("Invalid topic format: {}", topic); + return List.of(); + } String[] parts = topic.split("_"); + if (parts.length < 2) { + log.warn("Invalid topic format: {}", topic); + return List.of(); + } + String scope = parts[0]; - Long scopeId = Long.valueOf(parts[1]); + Long scopeId; + try { + scopeId = Long.valueOf(parts[1]); + } catch (NumberFormatException e) { + log.warn("Invalid scopeId in topic: {}", topic); + return List.of(); + } return switch (scope) { case "major" -> userRepository.findAllByMajor_MajorIdAndDeletedAtIsNull(scopeId); case "college" -> userRepository.findAllByCollege_CollegeIdAndDeletedAtIsNull(scopeId); case "school" -> userRepository.findAllBySchool_SchoolIdAndDeletedAtIsNull(scopeId); default -> List.of(); }; }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (22)
src/main/java/com/campus/campus/domain/council/domain/entity/CouncilType.javasrc/main/java/com/campus/campus/domain/councilpost/application/CouncilPostPushListener.javasrc/main/java/com/campus/campus/domain/manager/application/dto/request/RewardGrantedEvent.javasrc/main/java/com/campus/campus/domain/manager/application/mapper/ManagerMapper.javasrc/main/java/com/campus/campus/domain/manager/application/service/ManagerService.javasrc/main/java/com/campus/campus/domain/manager/application/service/RewardPushListener.javasrc/main/java/com/campus/campus/domain/notification/application/dto/CursorResponse.javasrc/main/java/com/campus/campus/domain/notification/application/dto/NextCursor.javasrc/main/java/com/campus/campus/domain/notification/application/dto/NotificationListResponse.javasrc/main/java/com/campus/campus/domain/notification/application/dto/NotificationResponse.javasrc/main/java/com/campus/campus/domain/notification/application/exception/ErrorCode.javasrc/main/java/com/campus/campus/domain/notification/application/exception/NotificationAccessDeniedException.javasrc/main/java/com/campus/campus/domain/notification/application/exception/NotificationNotFoundException.javasrc/main/java/com/campus/campus/domain/notification/application/mapper/NotificationMapper.javasrc/main/java/com/campus/campus/domain/notification/application/service/NotificationService.javasrc/main/java/com/campus/campus/domain/notification/domain/entity/Notification.javasrc/main/java/com/campus/campus/domain/notification/domain/entity/NotificationType.javasrc/main/java/com/campus/campus/domain/notification/domain/repository/NotificationRepository.javasrc/main/java/com/campus/campus/domain/notification/presentation/NotificationController.javasrc/main/java/com/campus/campus/domain/notification/presentation/NotificationResponseCode.javasrc/main/java/com/campus/campus/domain/notification/util/TimeFormatter.javasrc/main/java/com/campus/campus/domain/user/domain/repository/UserRepository.java
🧰 Additional context used
🧬 Code graph analysis (10)
src/main/java/com/campus/campus/domain/manager/application/service/ManagerService.java (2)
src/main/java/com/campus/campus/domain/manager/presentation/ManagerController.java (2)
RestController(29-94)PostMapping(83-93)src/main/java/com/campus/campus/domain/manager/domain/entity/Manager.java (1)
Entity(15-35)
src/main/java/com/campus/campus/domain/notification/application/dto/NextCursor.java (2)
src/main/java/com/campus/campus/domain/review/application/dto/response/CursorPageReviewResponse.java (1)
Getter(8-16)src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java (1)
toCursorReviewResponse(80-88)
src/main/java/com/campus/campus/domain/notification/application/mapper/NotificationMapper.java (1)
src/main/java/com/campus/campus/domain/notification/util/TimeFormatter.java (1)
Component(8-53)
src/main/java/com/campus/campus/domain/manager/application/service/RewardPushListener.java (5)
src/main/java/com/campus/campus/domain/notification/application/service/NotificationService.java (1)
Slf4j(29-133)src/main/java/com/campus/campus/domain/stamp/domain/entity/Reward.java (1)
Entity(21-39)src/main/java/com/campus/campus/domain/stamp/domain/repository/RewardRepository.java (1)
RewardRepository(10-12)src/main/java/com/campus/campus/domain/stamp/application/dto/response/RewardResponse.java (1)
RewardResponse(7-17)src/main/java/com/campus/campus/domain/manager/presentation/ManagerController.java (1)
PostMapping(83-93)
src/main/java/com/campus/campus/domain/notification/application/exception/ErrorCode.java (6)
src/main/java/com/campus/campus/domain/mail/application/exception/ErrorCode.java (1)
Getter(10-23)src/main/java/com/campus/campus/domain/council/application/exception/ErrorCode.java (1)
Getter(10-28)src/main/java/com/campus/campus/global/common/exception/ErrorCodeInterface.java (1)
ErrorCodeInterface(5-11)src/main/java/com/campus/campus/domain/councilpost/application/exception/ErrorCode.java (1)
Getter(10-38)src/main/java/com/campus/campus/global/firebase/exception/ErrorCode.java (1)
Getter(10-20)src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java (1)
Getter(10-21)
src/main/java/com/campus/campus/domain/notification/application/dto/CursorResponse.java (2)
src/main/java/com/campus/campus/domain/review/application/dto/response/CursorPageReviewResponse.java (1)
Getter(8-16)src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java (2)
toCursorReviewResponse(80-88)toEmptyCursorReviewResponse(38-45)
src/main/java/com/campus/campus/domain/notification/application/service/NotificationService.java (3)
src/main/java/com/campus/campus/domain/notification/application/exception/NotificationAccessDeniedException.java (1)
NotificationAccessDeniedException(5-9)src/main/java/com/campus/campus/domain/notification/application/exception/NotificationNotFoundException.java (1)
NotificationNotFoundException(5-9)src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)
src/main/java/com/campus/campus/domain/notification/application/exception/NotificationAccessDeniedException.java (1)
src/main/java/com/campus/campus/domain/councilpost/application/exception/PostAccessDeniedException.java (2)
PostAccessDeniedException(5-9)PostAccessDeniedException(6-8)
src/main/java/com/campus/campus/domain/notification/domain/repository/NotificationRepository.java (1)
src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java (1)
ReviewRepository(15-44)
src/main/java/com/campus/campus/domain/notification/util/TimeFormatter.java (1)
src/main/java/com/campus/campus/domain/notification/application/mapper/NotificationMapper.java (1)
Component(13-41)
⏰ 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 (21)
src/main/java/com/campus/campus/domain/council/domain/entity/CouncilType.java (1)
1-49: 포매팅 변경 사항 승인이 파일의 변경 사항은 메서드 선언부의 포매팅만 조정한 것으로, 로직에는 영향이 없습니다.
@Override어노테이션과 메서드 시그니처를 별도의 줄로 분리하고 공백 줄을 추가한 것이 일관되게 적용되어 있습니다.다만, 이 파일(
CouncilType.java)은 학생회 타입별 접근 제어 및 토픽 생성을 담당하는 enum으로, PR의 주요 목적인 "관리자가 유저에게 보상 지급 시 알림 전송" 기능과는 직접적인 연관이 없어 보입니다. 코드베이스 작업 중 부수적으로 정리된 것으로 판단됩니다.src/main/java/com/campus/campus/domain/manager/application/mapper/ManagerMapper.java (1)
74-79: LGTM!기존 mapper 패턴과 일관성 있게
createRewardGrantedEvent메서드가 잘 구현되었습니다.src/main/java/com/campus/campus/domain/notification/domain/entity/NotificationType.java (1)
1-7: LGTM!알림 타입 enum이 간결하게 정의되었습니다.
REWARD_GRANTED타입이 이번 PR의 보상 지급 알림 기능을 위해 적절히 추가되었습니다.src/main/java/com/campus/campus/domain/user/domain/repository/UserRepository.java (1)
45-51: LGTM!새로 추가된 쿼리 메서드들이 Spring Data JPA 네이밍 컨벤션을 잘 따르고 있으며, 기존 soft delete 패턴(
DeletedAtIsNull)과 일관성 있게 구현되었습니다.src/main/java/com/campus/campus/domain/notification/application/exception/ErrorCode.java (1)
10-21: 구현 패턴 일관성 확인
ErrorCodeInterface구현 및 Lombok 어노테이션 사용이 다른 도메인의 ErrorCode 패턴과 일관성 있게 구현되었습니다.src/main/java/com/campus/campus/domain/notification/application/exception/NotificationAccessDeniedException.java (1)
5-9: LGTM!기존
PostAccessDeniedException패턴과 일관성 있게 구현되었습니다.src/main/java/com/campus/campus/domain/notification/application/exception/NotificationNotFoundException.java (1)
5-9: LGTM!표준 예외 패턴을 따르며,
NotificationAccessDeniedException과 일관성 있게 구현되었습니다.src/main/java/com/campus/campus/domain/notification/domain/repository/NotificationRepository.java (3)
16-16: LGTM!첫 페이지 조회용 메서드로, Spring Data JPA 쿼리 메서드 네이밍 컨벤션을 올바르게 따르고 있습니다.
34-34: LGTM!읽지 않은 알림 존재 여부 확인 메서드가 Spring Data JPA 네이밍 컨벤션을 따르고 있습니다.
18-32: 커서 기반 페이지네이션 설계 확인됨 - 단, 디자인 패턴 일관성 검토 권장서비스 레이어에서
isFirst플래그를 통해 첫 페이지는findByUserOrderByCreatedAtDescIdDesc를, 이후 페이지는findNextByCursor를 올바르게 분리하여 사용하고 있습니다 (NotificationService 52-54줄).구현 자체는 의도된 설계이며 정상적으로 동작합니다. 다만
ReviewRepository는cursorCreatedAt IS NULL조건으로 단일 메서드에서 첫/이후 페이지를 모두 처리하는 반면, 여기서는 메서드를 분리한 점이 차이입니다. 두 접근 방식 모두 기능적으로 유효하나, 동일 패턴의 적용으로 일관성을 높이는 것을 검토하세요.src/main/java/com/campus/campus/domain/notification/application/dto/NextCursor.java (1)
5-8: LGTM!커서 기반 페이지네이션을 위한 간결한 record 구현입니다.
LocalDateTime을 사용하여 타입 안전성을 확보했고,Long을 사용하여 마지막 페이지에서 null 커서를 표현할 수 있습니다.src/main/java/com/campus/campus/domain/councilpost/application/CouncilPostPushListener.java (1)
41-52: 알림 저장과 Firebase 전송 간 에러 처리 검토 필요
saveCouncilPostCreated호출 후firebaseCloudMessageService.sendToTopic호출 시, 두 작업 중 하나가 실패하면 불일치가 발생할 수 있습니다:
- 알림 저장 성공 → Firebase 전송 실패: DB에 알림은 저장되었으나 실제 푸시는 미전송
- 알림 저장 실패 시 예외가 발생하면 Firebase 전송도 스킵됨 (이 경우는 괜찮음)
현재 구조상 Firebase 전송 실패 시에도 알림이 DB에 남아 있어 사용자가 알림함에서 확인할 수 있으므로 허용 가능한 동작일 수 있습니다. 의도된 동작인지 확인해 주세요.
src/main/java/com/campus/campus/domain/manager/application/service/ManagerService.java (1)
123-135: LGTM! 이벤트 발행 패턴이 올바르게 구현되었습니다.
@Transactional메서드 내에서 이벤트를 발행하고, 리스너에서@TransactionalEventListener(AFTER_COMMIT)를 사용하면 트랜잭션 커밋 후에만 알림이 전송됩니다. 트랜잭션이 롤백되면 이벤트도 처리되지 않아 데이터 일관성이 보장됩니다.src/main/java/com/campus/campus/domain/notification/application/dto/NotificationResponse.java (1)
7-28: LGTM!DTO record 구조가 적절하고, Swagger 문서화 어노테이션이 잘 작성되어 있습니다.
createdAt을 상대 시간 문자열로 처리하는 설계가 클라이언트 친화적입니다.src/main/java/com/campus/campus/domain/notification/application/mapper/NotificationMapper.java (1)
13-41: LGTM!Mapper 구현이 깔끔합니다.
toResponse와createNotification메서드가 단일 책임 원칙을 잘 따르고 있으며,TimeFormatter를 통한 상대 시간 포맷팅 처리가 적절합니다.src/main/java/com/campus/campus/domain/notification/presentation/NotificationController.java (2)
33-50: LGTM!커서 기반 페이지네이션 구현이 적절합니다.
@DateTimeFormat을 통한 ISO 날짜 파싱과 optional 파라미터 처리가 잘 되어 있습니다.
52-78: LGTM!
markAsRead와hasUnread엔드포인트가 명확하게 구현되어 있습니다. PATCH 메서드 사용이 적절하고, 응답 코드 처리가 일관성 있습니다.src/main/java/com/campus/campus/domain/notification/application/service/NotificationService.java (4)
71-84: LGTM!사용자 검증, 알림 존재 확인, 소유권 확인이 모두 적절하게 구현되어 있습니다.
@Transactional어노테이션도 올바르게 적용되어 있습니다.
86-102: LGTM!
REQUIRES_NEW전파를 사용하여 이벤트 리스너에서 독립적인 트랜잭션으로 알림을 저장하는 것이 적절합니다. 배치 저장을 통해 여러 사용자에게 효율적으로 알림을 생성합니다.
104-113: LGTM!PR 목표에 맞게 보상 지급 알림 저장 기능이 구현되었습니다.
findByIdAndDeletedAtIsNull을 사용하여 논리적으로 삭제된 사용자를 제외하고,REQUIRES_NEW트랜잭션 전파를 통해 이벤트 리스너에서 독립적으로 처리됩니다.
115-118: LGTM!읽지 않은 알림 존재 여부를 효율적으로 확인하는 간단한 구현입니다.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
...ain/java/com/campus/campus/domain/notification/application/dto/NotificationListResponse.java
Outdated
Show resolved
Hide resolved
| NOTIFICATION_NOT_FOUND(2801, HttpStatus.NOT_FOUND, "알림을 찾을 수 없습니다."), | ||
| NOTIFICATION_ACCESS_DENIED(2802, HttpStatus.FORBIDDEN, "해당 알림에 접근할 권한이 없습니다."), | ||
| INVALID_NOTIFICATION_IDS(2803, HttpStatus.BAD_REQUEST, "유효하지 않은 알림 ID 목록입니다."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -name "ErrorCode.java" -type f | head -20Repository: our-campUS/our-campUS-BE
Length of output: 1225
🏁 Script executed:
#!/bin/bash
# Extract error codes from all ErrorCode.java files and check for collisions
declare -A code_map
for file in $(find . -name "ErrorCode.java" -type f); do
domain=$(echo "$file" | sed -E 's|.*domain/([^/]+)/.*|\1|; s|.*/(global|firebase|jwt|oci)/.*|\1|')
# Extract all error code patterns: NAME(CODE, ...)
codes=$(rg -o '\b\w+\((\d+),' "$file" | cut -d'(' -f2 | cut -d',' -f1)
if [ ! -z "$codes" ]; then
echo "=== Domain: $domain (File: $file) ==="
echo "$codes" | while read code; do
if [ ! -z "$code" ]; then
key="$code"
if [ ! -z "${code_map[$key]}" ]; then
echo " COLLISION DETECTED: Code $code also in ${code_map[$key]}"
else
code_map[$key]="$domain"
fi
echo " $code"
fi
done
echo ""
fi
done
# Check for duplicates across all files
echo "=== Checking for duplicate codes across all domains ==="
rg -h -o '\b\w+\((\d+),' . --type=java -g '**/exception/ErrorCode.java' | cut -d'(' -f2 | cut -d',' -f1 | sort | uniq -dRepository: our-campUS/our-campUS-BE
Length of output: 2916
🏁 Script executed:
# Get details on error code 2801 in review domain
cat -n ./src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java | grep -E "(2800|2801)"Repository: our-campUS/our-campUS-BE
Length of output: 221
🏁 Script executed:
# Get details on error code 2801 in notification domain
cat -n ./src/main/java/com/campus/campus/domain/notification/application/exception/ErrorCode.java | grep -A2 -B2 "2801"Repository: our-campUS/our-campUS-BE
Length of output: 390
🏁 Script executed:
# Verify mail and councilpost collision
cat -n ./src/main/java/com/campus/campus/domain/mail/application/exception/ErrorCode.java | head -15
cat -n ./src/main/java/com/campus/campus/domain/councilpost/application/exception/ErrorCode.java | head -20Repository: our-campUS/our-campUS-BE
Length of output: 1619
알림 도메인의 에러 코드 변경 필요
NOTIFICATION_NOT_FOUND(2801)이 review 도메인의 NOT_REVIEW_WRITER(2801)과 중복되고 있습니다. 에러 코드는 도메인 간에 고유해야 로깅 및 디버깅 시 혼란을 방지할 수 있습니다.
notification 도메인의 에러 코드를 다음과 같이 변경하세요:
NOTIFICATION_NOT_FOUND(2901, ...)NOTIFICATION_ACCESS_DENIED(2902, ...)INVALID_NOTIFICATION_IDS(2903, ...)
🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/notification/application/exception/ErrorCode.java`
around lines 14 - 16, Update the numeric codes for the notification domain enum
entries in ErrorCode so they no longer collide with the review domain: change
NOTIFICATION_NOT_FOUND from 2801 to 2901, NOTIFICATION_ACCESS_DENIED from 2802
to 2902, and INVALID_NOTIFICATION_IDS from 2803 to 2903 in the ErrorCode enum
definition (the constants NOTIFICATION_NOT_FOUND, NOTIFICATION_ACCESS_DENIED,
INVALID_NOTIFICATION_IDS).
| long weeks = days / 7; | ||
| if (weeks < 4) { | ||
| return weeks + "주 전"; | ||
| } | ||
|
|
||
| long months = days / 30; | ||
| if (months < 12) { | ||
| return months + "개월 전"; | ||
| } | ||
|
|
||
| long years = days / 365; | ||
| return years + "년 전"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
경계 조건에서 "0개월 전" 또는 "0년 전"이 반환될 수 있습니다.
주(weeks)와 월(months) 계산 로직에 불일치가 있습니다:
days = 28일 때:weeks = 4(조건weeks < 4통과 못함),months = 28/30 = 0→ "0개월 전" 반환days = 360일 때:months = 12(조건months < 12통과 못함),years = 360/365 = 0→ "0년 전" 반환
🐛 경계 조건 수정 제안
long weeks = days / 7;
- if (weeks < 4) {
+ if (days < 30) {
return weeks + "주 전";
}
long months = days / 30;
- if (months < 12) {
+ if (days < 365) {
return months + "개월 전";
}
long years = days / 365;
return years + "년 전";📝 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.
| long weeks = days / 7; | |
| if (weeks < 4) { | |
| return weeks + "주 전"; | |
| } | |
| long months = days / 30; | |
| if (months < 12) { | |
| return months + "개월 전"; | |
| } | |
| long years = days / 365; | |
| return years + "년 전"; | |
| long weeks = days / 7; | |
| if (days < 30) { | |
| return weeks + "주 전"; | |
| } | |
| long months = days / 30; | |
| if (days < 365) { | |
| return months + "개월 전"; | |
| } | |
| long years = days / 365; | |
| return years + "년 전"; |
🔀 변경 내용
✅ 작업 항목
📸 스크린샷 (선택)
보상 지급 시 알림 전송
알림함에 보상 지급 알림 저장
📎 참고 이슈
관련 이슈 번호 #72
Summary by CodeRabbit
릴리스 노트
✏️ Tip: You can customize this high-level summary in your review settings.