Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c6e64cd
feat: 피드백/답글 생성 시 알림 호출 연동 (#96)
Kohseoyoung Aug 2, 2026
cca00e3
feat: 개인 알림 타입 확장
guingguing Aug 5, 2026
f6e4d08
feat: 프로젝트 합류 일정 공지 파일 알림 생성 메서드 추가
guingguing Aug 5, 2026
1ca9bbf
fix: 일정 담당자 지정 알림 문구 수정
guingguing Aug 5, 2026
54e9956
fix: 지원자 알림 제목 생성 기준 수정
guingguing Aug 5, 2026
ffa532e
fix: 파일 등록 알림 그룹핑 제거
guingguing Aug 5, 2026
8928c04
refactor: 프로젝트 초대 알림 제거
guingguing Aug 5, 2026
7127b2d
fix: 지원자 알림 생성 메서드 호환성 추가
guingguing Aug 5, 2026
1e0b108
Merge branch 'develop' into feature/96-feedback-notification
Kohseoyoung Aug 5, 2026
8a93ad7
fix: 프로젝트 목록 최근 활동 시각 정합성 보완
chazy-d Aug 5, 2026
086d0a7
feat: 프로필 이미지 업로드 API 추가
chazy-d Aug 5, 2026
242c255
fix: 일정 담당자 알림 중복 수신자 제거
guingguing Aug 5, 2026
e08bc5e
fix: 알림 문구 조합을 알림 도메인에 위임 (#96)
Kohseoyoung Aug 5, 2026
999b6fd
fix: 일정 담당자 알림 생성 문구 수정
guingguing Aug 5, 2026
78762a7
feat: 영상 목록 hasUnreadFeedback 구현 (#121)
young0206 Aug 5, 2026
e377f89
fix: 게스트 피드백 알림 행위자 ID null 허용
guingguing Aug 5, 2026
ff1f138
fix: 코드 리뷰 수정 (#121)
young0206 Aug 5, 2026
bebbfb7
build: Flyway 마이그레이션 자동화 추가
chazy-d Aug 5, 2026
75cd92a
refactor: 마이그레이션 버전 체계 정리
chazy-d Aug 5, 2026
aaca6d1
Merge pull request #120 from SLAT-TO/feature/111-notification-create-…
guingguing Aug 5, 2026
e8b9c95
fix: 프로필 이미지 업로드 예외 정리 및 multipart 제한 동기화
chazy-d Aug 5, 2026
38b58c3
fix: 중복 활성 지원 마이그레이션 정리
chazy-d Aug 5, 2026
dfddb7b
fix: activity_log 타입 마이그레이션 정규화
chazy-d Aug 5, 2026
1c0eab1
Merge pull request #124 from SLAT-TO/feat/121-unread
young0206 Aug 5, 2026
8691dc7
Merge pull request #123 from SLAT-TO/feature/96-feedback-notification
Kohseoyoung Aug 6, 2026
31b8cc9
Merge pull request #125 from SLAT-TO/fix/project-card-profile-image
chazy-d Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ AWS_SECRET_ACCESS_KEY=<aws-secret-access-key>

# S3 bucket
CLOUD_AWS_S3_BUCKET=<cloud-aws-s3-bucket>

# 프로필 이미지 등 공개 파일 URL을 조합할 CDN 또는 공개 S3 base URL
# 예: https://cdn.slatto.cloud
CLOUD_AWS_S3_PUBLIC_BASE_URL=<cloud-aws-s3-public-base-url>
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.16'
implementation 'me.paulschwarz:spring-dotenv:4.0.0'
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import com.slatto.domain.sharelink.entity.ShareLink;
import com.slatto.domain.sharelink.exception.ShareLinkErrorCode;
import com.slatto.domain.sharelink.repository.GuestRepository;
import com.slatto.domain.notification.service.NotificationService;
import com.slatto.domain.video.entity.Video;
import com.slatto.domain.user.entity.Users;
import com.slatto.domain.user.repository.UserRepository;
import com.slatto.global.exception.BaseException;
Expand All @@ -39,6 +41,7 @@ public class FeedbackDetailService {
private final GuestRepository guestRepository;
private final FeedbackDetailConverter feedbackDetailConverter;
private final ProjectMemberRepository projectMemberRepository;
private final NotificationService notificationService;
private final ActivityLogService activityLogService;

private static final int DEFAULT_PAGE_SIZE = 10;
Expand Down Expand Up @@ -71,6 +74,12 @@ public ReplyCreateResDTO createReply(Long feedbackId, Long userId, ReplyCreateRe
FeedbackDetail reply = feedbackDetailConverter.toFeedbackDetail(feedback, user, guest, req);
FeedbackDetail saved = feedbackDetailRepository.save(reply);

// 5. 프로젝트 멤버에게 답글 알림 발송 (작성자 본인은 actorUserId로 제외)
// 알림 문구 조합용 작성자명 — 회원이면 유저명, 게스트면 게스트명
String commenterName = (user != null) ? user.getNickname() : guest.getName();
sendReplyNotification(feedback.getVideo(), userId, commenterName);

// 6. 최근 활동 로그 기록 (회원/게스트 구분)
if (user != null) {
activityLogService.createVideoFeedbackCommentedLog(
feedback.getVideo().getProject().getId(),
Expand All @@ -90,6 +99,29 @@ public ReplyCreateResDTO createReply(Long feedbackId, Long userId, ReplyCreateRe
return feedbackDetailConverter.toCreateResponse(saved);
}

// 답글 생성 시 프로젝트 멤버에게 알림 발송
// 문구 조합/저장/그룹핑/작성자 제외는 알림 도메인이 처리하므로 재료(영상명·작성자명)만 준비해 호출한다.
// 원 피드백의 영상 기준으로 그룹핑되므로 targetId는 videoId가 사용된다.
private void sendReplyNotification(Video video, Long actorUserId, String commenterName) {
Long projectId = video.getProject().getId();

// 프로젝트 활성 멤버 전체를 수신자로 (작성자 제외는 actorUserId로 알림 도메인이 처리)
List<Long> recipientIds = projectMemberRepository
.findAllActiveMembersByProjectId(projectId)
.stream()
.map(pm -> pm.getUser().getId())
.toList();

notificationService.createVideoFeedbackCommentedNotifications(
projectId,
video.getId(),
video.getTitle(), // 영상명 → 알림 도메인이 문구 조합에 사용
commenterName, // 작성자명 → 알림 도메인이 문구 조합에 사용
recipientIds,
actorUserId // 게스트면 null → 제외 대상 없음
);
}

private void validateWriter(Long userId, Long guestId) {
boolean hasUser = (userId != null);
boolean hasGuest = (guestId != null);
Expand Down Expand Up @@ -242,4 +274,4 @@ public ReplyStatusResDTO changeReplyStatus(Long replyId, Long userId, ReplyStatu

return feedbackDetailConverter.toStatusResponse(reply);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.slatto.domain.project.repository.ProjectMemberRepository;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import com.slatto.domain.notification.service.NotificationService;

import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -46,6 +47,7 @@ public class FeedbackService {
private final ObjectProvider<EntityManager> entityManagerProvider;
private final ProjectMemberRepository projectMemberRepository;
private final FeedbackDetailRepository feedbackDetailRepository;
private final NotificationService notificationService;
private final ActivityLogService activityLogService;

private static final int DEFAULT_PAGE_SIZE = 10;
Expand Down Expand Up @@ -82,6 +84,12 @@ public FeedbackCreateResDTO createFeedback(Long videoId, Long userId, FeedbackCr
Feedback feedback = feedbackConverter.toFeedback(video, user, guest, req);
Feedback saved = feedbackRepository.save(feedback);

// 5. 프로젝트 멤버에게 피드백 알림 발송 (작성자 본인은 actorUserId로 제외)
// 알림 문구 조합용 작성자명 — 회원이면 유저명, 게스트면 게스트명
String commenterName = (user != null) ? user.getNickname() : guest.getName();
sendFeedbackNotification(video, userId, commenterName);

// 6. 최근 활동 로그 기록 (회원/게스트 구분)
if (user != null) {
activityLogService.createVideoFeedbackCommentedLog(
video.getProject().getId(),
Expand All @@ -101,6 +109,28 @@ public FeedbackCreateResDTO createFeedback(Long videoId, Long userId, FeedbackCr
return feedbackConverter.toCreateResponse(saved);
}

// 피드백/답글 생성 시 프로젝트 멤버에게 알림을 보낸다.
// 문구 조합/저장/그룹핑/작성자 제외는 알림 도메인이 처리하므로 재료(영상명·작성자명)만 준비해 호출한다.
private void sendFeedbackNotification(Video video, Long actorUserId, String commenterName) {
Long projectId = video.getProject().getId();

// 프로젝트 활성 멤버 전체를 수신자로 (작성자 제외는 actorUserId로 알림 도메인이 처리)
List<Long> recipientIds = projectMemberRepository
.findAllActiveMembersByProjectId(projectId)
.stream()
.map(pm -> pm.getUser().getId())
.toList();

notificationService.createVideoFeedbackCommentedNotifications(
projectId,
video.getId(),
video.getTitle(), // 영상명 → 알림 도메인이 문구 조합에 사용
commenterName, // 작성자명 → 알림 도메인이 문구 조합에 사용
recipientIds,
actorUserId // 게스트면 null → 제외 대상 없음
);
}

@Transactional
public FeedbackUpdateResDTO updateFeedback(Long feedbackId, Long userId, FeedbackUpdateReqDTO req) {

Expand Down Expand Up @@ -296,4 +326,5 @@ public FeedbackStatusResDTO changeFeedbackStatus(Long feedbackId, Long userId, F

return feedbackConverter.toStatusResponse(feedback);
}
}

}
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 @@ -31,4 +31,14 @@ List<ActivityLog> findRecentActivitiesByCursor(
);

Optional<ActivityLog> findByIdAndProjectId(Long activityId, Long projectId);

@Query("""
select al.project.id as projectId, max(al.createdAt) as lastActivityAt
from ActivityLog al
where al.project.id in :projectIds
group by al.project.id
""")
List<ProjectLatestActivityProjection> findLatestActivityAtByProjectIds(
@Param("projectIds") List<Long> projectIds
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,22 +124,22 @@ List<Notification> findUnreadGroupedNotificationsForUpdate(
Pageable pageable
);

// 미읽음 그룹 알림이 없으면 누적 개수는 0이다.
@Query("""
select coalesce(max(n.groupCount), 0)
select n.targetId, coalesce(max(n.groupCount), 0)
from Notification n
where n.user.id = :userId
and n.type = :type
and n.targetType = :targetType
and n.targetId = :targetId
and n.targetId in :targetIds
and n.isRead = false
and n.deletedAt is null
group by n.targetId
""")
int findUnreadGroupedNotificationCount(
List<Object[]> findUnreadGroupedNotificationCounts(
@Param("userId") Long userId,
@Param("type") NotificationType type,
@Param("targetType") String targetType,
@Param("targetId") Long targetId
@Param("targetIds") List<Long> targetIds
);

@Modifying(clearAutomatically = true, flushAutomatically = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.slatto.domain.notification.repository;

import java.time.LocalDateTime;

public interface ProjectLatestActivityProjection {

Long getProjectId();

LocalDateTime getLastActivityAt();
}
Loading
Loading