Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ public enum NotificationTargetType {
SCHEDULE,
PROJECT,
VIDEO,
RECRUITMENT
RECRUITMENT,
NOTICE,
PROJECT_FILE
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

public enum NotificationType {
SCHEDULE_ASSIGNED,
PROJECT_INVITED,
PROJECT_JOINED,
VIDEO_FEEDBACK_COMMENTED,
RECRUITMENT_APPLIED,
SCHEDULE_CREATED,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,70 +184,91 @@ public void createScheduleAssignedNotifications(
List<Users> recipients,
Long writerId
) {
createNotifications(NotificationCreateCommand.builder()
.recipientIds(recipients.stream()
.map(Users::getId)
.toList())
.projectId(project != null ? project.getId() : null)
.type(NotificationType.SCHEDULE_ASSIGNED)
.title(createScheduleAssignedTitle(project != null ? project.getTitle() : null))
.content(createScheduleAssignedContent(scheduleTitle))
.targetType(NotificationTargetType.SCHEDULE)
.targetId(scheduleId)
.excludeUserId(writerId)
.build());
validateRequiredId(scheduleId);
validateRequiredId(writerId);
validateRequiredText(scheduleTitle);
if (recipients == null) {
throw new BaseException(CommonErrorCode.BAD_REQUEST);
}

Project notificationProject = getProjectOrNull(project != null ? project.getId() : null);
String title = createScheduleAssignedTitle(project != null ? project.getTitle() : null);
String targetType = getTargetTypeName(NotificationTargetType.SCHEDULE);
Set<Long> recipientIds = new HashSet<>();

List<Notification> notifications = recipients.stream()
.filter(Objects::nonNull)
.filter(recipient -> recipientIds.add(recipient.getId()))
.filter(recipient -> !Objects.equals(recipient.getId(), writerId))
.map(recipient -> Notification.create(
Comment on lines +187 to +203

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

작성자 제외를 보장하도록 작성자 ID를 검증하세요.

writerId 또는 actorUserId가 null이면 제외 로직이 적용되지 않습니다. 이 경우 작성자가 자신의 알림을 수신합니다.

  • src/main/java/com/slatto/domain/notification/service/NotificationService.java#L187-L200: writerIdvalidateRequiredId로 검증하세요.
  • src/main/java/com/slatto/domain/notification/service/NotificationService.java#L253-L267: actorUserIdvalidateRequiredId로 검증하세요.
  • src/main/java/com/slatto/domain/notification/service/NotificationService.java#L407-L421: actorUserIdvalidateRequiredId로 검증하세요.
  • src/main/java/com/slatto/domain/notification/service/NotificationService.java#L437-L450: actorUserIdvalidateRequiredId로 검증하세요.
📍 Affects 1 file
  • src/main/java/com/slatto/domain/notification/service/NotificationService.java#L187-L200 (this comment)
  • src/main/java/com/slatto/domain/notification/service/NotificationService.java#L253-L267
  • src/main/java/com/slatto/domain/notification/service/NotificationService.java#L407-L421
  • src/main/java/com/slatto/domain/notification/service/NotificationService.java#L437-L450
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/slatto/domain/notification/service/NotificationService.java`
around lines 187 - 200, 작성자 제외 로직에 사용되는 ID가 null이 되지 않도록 검증을 추가하세요.
NotificationService.java의 187-200행에서는 writerId를 validateRequiredId로 검증하고,
253-267행·407-421행·437-450행에서는 각각 actorUserId를 validateRequiredId로 검증하세요.

recipient,
notificationProject,
NotificationType.SCHEDULE_ASSIGNED,
title,
createScheduleAssignedContent(scheduleTitle, recipient.getNickname()),
targetType,
scheduleId
))
.toList();

notificationRepository.saveAll(notifications);
Comment on lines +199 to +214

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

중복 수신자를 제거하세요.

recipients에 동일 사용자가 두 번 있으면 알림도 두 건 저장됩니다. 이 경로는 getRecipientIds의 중복 제거를 사용하지 않습니다. Notification.create 전에 수신자 ID를 기준으로 중복을 제거하세요.

수정 예시
+        Set<Long> uniqueRecipientIds = new HashSet<>();
         List<Notification> notifications = recipients.stream()
             .filter(Objects::nonNull)
+            .filter(recipient -> recipient.getId() != null)
+            .filter(recipient -> uniqueRecipientIds.add(recipient.getId()))
             .filter(recipient -> !Objects.equals(recipient.getId(), writerId))
             .map(recipient -> Notification.create(
📝 Committable suggestion

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

Suggested change
List<Notification> notifications = recipients.stream()
.filter(Objects::nonNull)
.filter(recipient -> !Objects.equals(recipient.getId(), writerId))
.map(recipient -> Notification.create(
recipient,
notificationProject,
NotificationType.SCHEDULE_ASSIGNED,
title,
createScheduleAssignedContent(scheduleTitle, recipient.getNickname()),
targetType,
scheduleId
))
.toList();
notificationRepository.saveAll(notifications);
Set<Long> uniqueRecipientIds = new HashSet<>();
List<Notification> notifications = recipients.stream()
.filter(Objects::nonNull)
.filter(recipient -> recipient.getId() != null)
.filter(recipient -> uniqueRecipientIds.add(recipient.getId()))
.filter(recipient -> !Objects.equals(recipient.getId(), writerId))
.map(recipient -> Notification.create(
recipient,
notificationProject,
NotificationType.SCHEDULE_ASSIGNED,
title,
createScheduleAssignedContent(scheduleTitle, recipient.getNickname()),
targetType,
scheduleId
))
.toList();
notificationRepository.saveAll(notifications);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/slatto/domain/notification/service/NotificationService.java`
around lines 197 - 211, Update the recipient stream in the notification creation
flow to remove duplicate recipients by recipient ID before invoking
Notification.create. Preserve the existing null filtering, writer exclusion,
notification construction, and saveAll behavior for unique recipients.

}

/**
* 프로젝트 초대 알림을 생성한다.
* 클릭 대상은 프로젝트 상세 화면이다.
*
* @deprecated 알림 문구는 알림 도메인에서 관리하므로 projectTitle을 받는 메서드를 사용한다.
* 프로젝트 합류 알림을 프로젝트 참여자에게 생성한다.
* 합류자 본인도 수신 대상에 포함할 수 있다.
*/
@Deprecated
@Transactional
public void createProjectInvitationNotification(
Long recipientId,
public void createProjectJoinedNotifications(
Long projectId,
String content
String projectTitle,
String joinerName,
List<Long> recipientIds
) {
validateRequiredId(recipientId);
validateRequiredId(projectId);
validateRequiredText(projectTitle);
validateRequiredText(joinerName);

createNotification(NotificationCreateCommand.builder()
.recipientIds(List.of(recipientId))
createNotifications(NotificationCreateCommand.builder()
.recipientIds(recipientIds)
.projectId(projectId)
.type(NotificationType.PROJECT_INVITED)
.title(createFallbackTitle(NotificationType.PROJECT_INVITED))
.content(content)
.type(NotificationType.PROJECT_JOINED)
.title(createProjectJoinedTitle(projectTitle))
.content(createProjectJoinedContent(joinerName))
.targetType(NotificationTargetType.PROJECT)
.targetId(projectId)
.build());
}

/**
* 프로젝트 초대 알림 문구를 정책에 맞춰 생성한다.
* 프로젝트에 새 일정이 등록되었을 때 프로젝트 참여자에게 알림을 생성한다.
*/
@Transactional
public void createProjectInvitationNotification(
Long recipientId,
public void createScheduleCreatedNotifications(
Long projectId,
Long scheduleId,
String projectTitle,
String inviterName
String scheduleTitle,
String creatorName,
List<Long> recipientIds,
Long actorUserId
) {
validateRequiredId(projectId);
validateRequiredId(recipientId);
validateRequiredId(scheduleId);
validateRequiredId(actorUserId);
validateRequiredText(projectTitle);
validateRequiredText(inviterName);
validateRequiredText(scheduleTitle);
validateRequiredText(creatorName);

createNotification(NotificationCreateCommand.builder()
.recipientIds(List.of(recipientId))
createNotifications(NotificationCreateCommand.builder()
.recipientIds(recipientIds)
.projectId(projectId)
.type(NotificationType.PROJECT_INVITED)
.title(createProjectInvitationTitle(projectTitle))
.content(createProjectInvitationContent(projectTitle, inviterName))
.targetType(NotificationTargetType.PROJECT)
.targetId(projectId)
.type(NotificationType.SCHEDULE_CREATED)
.title(createScheduleCreatedTitle(projectTitle))
.content(createScheduleCreatedContent(scheduleTitle, creatorName))
.targetType(NotificationTargetType.SCHEDULE)
.targetId(scheduleId)
.excludeUserId(actorUserId)
.build());
}

Expand Down Expand Up @@ -320,7 +341,7 @@ public void createVideoFeedbackCommentedNotifications(
* 새로운 지원자 발생 알림을 생성하거나 기존 미읽음 알림을 갱신한다.
* 동일 공고 기준으로 그룹핑하므로 targetId는 recruitmentId를 사용한다.
*
* @deprecated 알림 문구는 알림 도메인에서 관리하므로 recruitmentTitle과 applicantName을 받는 메서드를 사용한다.
* @deprecated 알림 문구는 알림 도메인에서 관리하므로 projectTitle, recruitmentTitle, applicantName을 받는 메서드를 사용한다.
*/
@Deprecated
@Transactional
Expand Down Expand Up @@ -349,18 +370,20 @@ public void createRecruitmentAppliedNotification(
public void createRecruitmentAppliedNotification(
Long recipientId,
Long recruitmentId,
String projectTitle,
String recruitmentTitle,
String applicantName
) {
validateRequiredId(recipientId);
validateRequiredId(recruitmentId);
validateRequiredText(projectTitle);
validateRequiredText(recruitmentTitle);
validateRequiredText(applicantName);

NotificationCreateCommand command = NotificationCreateCommand.builder()
.recipientIds(List.of(recipientId))
.type(NotificationType.RECRUITMENT_APPLIED)
.title(createRecruitmentAppliedTitle(recruitmentTitle))
.title(createRecruitmentAppliedTitle(projectTitle))
.content(createRecruitmentAppliedContent(recruitmentTitle, applicantName, 1))
.targetType(NotificationTargetType.RECRUITMENT)
.targetId(recruitmentId)
Expand All @@ -372,6 +395,88 @@ public void createRecruitmentAppliedNotification(
);
}

/**
* 공고 프로젝트명을 알 수 없는 기존 호출부에서 사용하는 지원자 알림 생성 메서드다.
* 프로젝트명을 전달할 수 있는 경우에는 5개 인자 메서드를 사용한다.
*/
@Transactional
public void createRecruitmentAppliedNotification(
Long recipientId,
Long recruitmentId,
String recruitmentTitle,
String applicantName
) {
createRecruitmentAppliedNotification(
recipientId,
recruitmentId,
recruitmentTitle,
recruitmentTitle,
applicantName
);
}

/**
* 프로젝트 공지가 등록되었을 때 프로젝트 참여자에게 알림을 생성한다.
*/
@Transactional
public void createNoticeCreatedNotifications(
Long projectId,
Long noticeId,
String projectTitle,
String noticeTitle,
String creatorName,
List<Long> recipientIds,
Long actorUserId
) {
validateRequiredId(projectId);
validateRequiredId(noticeId);
validateRequiredId(actorUserId);
validateRequiredText(projectTitle);
validateRequiredText(noticeTitle);
validateRequiredText(creatorName);

createNotifications(NotificationCreateCommand.builder()
.recipientIds(recipientIds)
.projectId(projectId)
.type(NotificationType.NOTICE_CREATED)
.title(createNoticeCreatedTitle(projectTitle))
.content(createNoticeCreatedContent(noticeTitle, creatorName))
.targetType(NotificationTargetType.NOTICE)
.targetId(noticeId)
.excludeUserId(actorUserId)
.build());
}

/**
* 프로젝트 파일 등록 알림을 생성한다.
*/
@Transactional
public void createFileUploadedNotifications(
Long projectId,
String projectTitle,
String fileName,
String uploaderName,
List<Long> recipientIds,
Long actorUserId
) {
validateRequiredId(projectId);
validateRequiredId(actorUserId);
validateRequiredText(projectTitle);
validateRequiredText(fileName);
validateRequiredText(uploaderName);

createNotifications(NotificationCreateCommand.builder()
.recipientIds(recipientIds)
.projectId(projectId)
.type(NotificationType.FILE_UPLOADED)
.title(createFileUploadedTitle(projectTitle))
.content(createFileUploadedContent(fileName, uploaderName))
.targetType(NotificationTargetType.PROJECT_FILE)
.targetId(projectId)
.excludeUserId(actorUserId)
.build());
}

private void validateActiveUser(Long currentUserId) {
if (!userRepository.existsByIdAndDeletedAtIsNull(currentUserId)) {
throw new BaseException(CommonErrorCode.NOT_FOUND);
Expand Down Expand Up @@ -555,20 +660,28 @@ private NotificationListResponse.NotificationSummary toSummary(Notification noti
.build();
}

private String createScheduleAssignedContent(String scheduleTitle) {
return "'" + scheduleTitle + "' 일정 담당자로 지정되었습니다.";
private String createScheduleAssignedContent(String scheduleTitle, String assigneeName) {
return assigneeName + "님이 [" + scheduleTitle + "] 담당자로 지정되었어요";
}
Comment on lines +663 to 665

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

담당자 지정 문구를 수동형으로 수정하세요.

assigneeName은 지정한 사용자가 아니라 지정된 사용자입니다. 현재 문구는 담당자가 지정 행위를 수행한 것으로 표시합니다. "지정되었어요"를 사용하세요.

수정 예시
-        return assigneeName + "님이 [" + scheduleTitle + "] 담당자로 지정했어요";
+        return assigneeName + "님이 [" + scheduleTitle + "] 담당자로 지정되었어요";
📝 Committable suggestion

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

Suggested change
private String createScheduleAssignedContent(String scheduleTitle, String assigneeName) {
return assigneeName + "님이 [" + scheduleTitle + "] 담당자로 지정했어요";
}
private String createScheduleAssignedContent(String scheduleTitle, String assigneeName) {
return assigneeName + "님이 [" + scheduleTitle + "] 담당자로 지정되었어요";
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/slatto/domain/notification/service/NotificationService.java`
around lines 637 - 639, createScheduleAssignedContent 메서드의 알림 문구를 수동형으로 수정하세요.
assigneeName이 지정된 사용자로 표시되도록 현재 “담당자로 지정했어요” 표현을 “담당자로 지정되었어요”로 변경하고, 나머지 조합 형식은
유지하세요.


private String createScheduleAssignedTitle(String projectTitle) {
return joinTitle(projectTitle, "일정 담당자 지정");
}

private String createProjectInvitationTitle(String projectTitle) {
return joinTitle(projectTitle, "프로젝트 초대");
private String createProjectJoinedTitle(String projectTitle) {
return joinTitle(projectTitle, "합류");
}

private String createProjectInvitationContent(String projectTitle, String inviterName) {
return inviterName + "님이 [" + projectTitle + "] 프로젝트에 초대했어요";
private String createProjectJoinedContent(String joinerName) {
return joinerName + "님이 프로젝트에 합류했어요";
}

private String createScheduleCreatedTitle(String projectTitle) {
return joinTitle(projectTitle, "새 일정");
}

private String createScheduleCreatedContent(String scheduleTitle, String creatorName) {
return creatorName + "님이 [" + scheduleTitle + "] 일정을 등록했어요";
}

private String createVideoFeedbackCommentedTitle(String projectTitle) {
Expand All @@ -587,8 +700,8 @@ private String createVideoFeedbackCommentedContent(
return "[" + videoTitle + "]에 새로운 피드백 " + groupCount + "건이 등록되었어요";
}

private String createRecruitmentAppliedTitle(String recruitmentTitle) {
return joinTitle(recruitmentTitle, "새로운 지원자");
private String createRecruitmentAppliedTitle(String projectTitle) {
return joinTitle(projectTitle, "새로운 지원자");
}

private String createRecruitmentAppliedContent(
Expand All @@ -603,10 +716,26 @@ private String createRecruitmentAppliedContent(
return "[" + recruitmentTitle + "]에 새로운 지원자 " + groupCount + "명이 지원했어요";
}

private String createNoticeCreatedTitle(String projectTitle) {
return joinTitle(projectTitle, "새 공지");
}

private String createNoticeCreatedContent(String noticeTitle, String creatorName) {
return creatorName + "님이 새 공지를 등록했어요: " + noticeTitle;
}

private String createFileUploadedTitle(String projectTitle) {
return joinTitle(projectTitle, "새 파일");
}

private String createFileUploadedContent(String fileName, String uploaderName) {
return uploaderName + "님이 [" + fileName + "] 파일을 등록했어요";
}

private String createFallbackTitle(NotificationType type) {
return switch (type) {
case SCHEDULE_ASSIGNED -> "일정 담당자 지정";
case PROJECT_INVITED -> "프로젝트 초대";
case PROJECT_JOINED -> "합류";
case VIDEO_FEEDBACK_COMMENTED -> "새로운 피드백";
case RECRUITMENT_APPLIED -> "새로운 지원자";
case SCHEDULE_CREATED -> "새 일정";
Expand Down
Loading