From e910814eaefdab09e87e0bbf7055364fee447ff9 Mon Sep 17 00:00:00 2001 From: guingguing Date: Thu, 6 Aug 2026 14:49:04 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=ED=95=A9=EB=A5=98=20=EC=95=8C=EB=A6=BC=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ProjectInvitationService.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main/java/com/slatto/domain/project/service/ProjectInvitationService.java b/src/main/java/com/slatto/domain/project/service/ProjectInvitationService.java index 618879a9..ff8edf5b 100644 --- a/src/main/java/com/slatto/domain/project/service/ProjectInvitationService.java +++ b/src/main/java/com/slatto/domain/project/service/ProjectInvitationService.java @@ -15,6 +15,7 @@ import com.slatto.domain.project.repository.ProjectMemberRepository; import com.slatto.domain.project.repository.ProjectUserRoleRepository; import com.slatto.domain.notification.service.ActivityLogService; +import com.slatto.domain.notification.service.NotificationService; import com.slatto.domain.user.entity.Users; import com.slatto.domain.user.enums.RoleName; import com.slatto.domain.user.repository.UserRepository; @@ -50,6 +51,7 @@ public class ProjectInvitationService { private final ProjectAccessValidator projectAccessValidator; private final ProjectInvitationProperties projectInvitationProperties; private final ActivityLogService activityLogService; + private final NotificationService notificationService; private final SecureRandom secureRandom = new SecureRandom(); @Transactional @@ -117,6 +119,12 @@ public ProjectInvitationAcceptResponse acceptInvitation( saveProjectRoles(projectMember, roleNames); projectInvitation.accept(accepter); activityLogService.createProjectMemberJoinedLog(project.getId(), accepter.getId()); + notificationService.createProjectJoinedNotifications( + project.getId(), + project.getTitle(), + accepter.getNickname(), + getActiveProjectMemberUserIds(project.getId()) + ); return ProjectInvitationAcceptResponse.builder() .projectId(project.getId()) @@ -134,6 +142,14 @@ private void saveProjectRoles(ProjectMember projectMember, List roleNa projectUserRoleRepository.saveAll(projectUserRoles); } + private List getActiveProjectMemberUserIds(Long projectId) { + return projectMemberRepository.findAllActiveMembersByProjectId(projectId) + .stream() + .map(ProjectMember::getUser) + .map(Users::getId) + .toList(); + } + private void validateAcceptableInvitation(ProjectInvitation projectInvitation) { if (projectInvitation.isAccepted()) { throw new BaseException(ProjectErrorCode.PROJECT_INVITATION_ALREADY_ACCEPTED); From e6617ce2c82830448f79ba7e82edbfb8b69b49c7 Mon Sep 17 00:00:00 2001 From: young Date: Thu, 6 Aug 2026 14:50:52 +0900 Subject: [PATCH 02/10] =?UTF-8?q?fix:=20=ED=94=BC=EB=93=9C=EB=B0=B1=20?= =?UTF-8?q?=EB=B9=A8=EA=B0=84=EC=A0=90=20=EC=9D=BD=EC=9D=8C=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20(#131)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/NotificationRepository.java | 20 +++++++++++++++++++ .../service/NotificationService.java | 14 +++++++++++++ .../domain/video/service/VideoService.java | 3 +++ 3 files changed, 37 insertions(+) diff --git a/src/main/java/com/slatto/domain/notification/repository/NotificationRepository.java b/src/main/java/com/slatto/domain/notification/repository/NotificationRepository.java index 5bbe6304..ea0bbf98 100644 --- a/src/main/java/com/slatto/domain/notification/repository/NotificationRepository.java +++ b/src/main/java/com/slatto/domain/notification/repository/NotificationRepository.java @@ -103,6 +103,26 @@ int markAsReadByIdAndUserId( @Param("readAt") LocalDateTime readAt ); + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + update Notification n + set n.isRead = true, + n.readAt = :readAt + where n.user.id = :userId + and n.type = :type + and n.targetType = :targetType + and n.targetId = :videoId + and n.isRead = false + and n.deletedAt is null + """) + int markVideoFeedbackNotificationsAsRead( + @Param("userId") Long userId, + @Param("type") NotificationType type, + @Param("targetType") String targetType, + @Param("videoId") Long videoId, + @Param("readAt") LocalDateTime readAt + ); + // 동일 대상의 미읽음 그룹 알림을 잠금 조회해 누적 개수와 문구를 같은 엔티티 상태 기준으로 갱신한다. @Lock(LockModeType.PESSIMISTIC_WRITE) @Query(""" diff --git a/src/main/java/com/slatto/domain/notification/service/NotificationService.java b/src/main/java/com/slatto/domain/notification/service/NotificationService.java index eb96b58b..b6ad1ebc 100644 --- a/src/main/java/com/slatto/domain/notification/service/NotificationService.java +++ b/src/main/java/com/slatto/domain/notification/service/NotificationService.java @@ -102,6 +102,20 @@ public void markAllNotificationsAsRead(Long currentUserId) { notificationRepository.markAllAsReadByUserId(currentUserId, LocalDateTime.now()); } + @Transactional + public void markVideoFeedbackNotificationsAsRead(Long userId, Long videoId) { + validateActiveUser(userId); + validateRequiredId(videoId); + + notificationRepository.markVideoFeedbackNotificationsAsRead( + userId, + NotificationType.VIDEO_FEEDBACK_COMMENTED, + NotificationTargetType.VIDEO.name(), + videoId, + LocalDateTime.now() + ); + } + /** * 영상 목록의 미읽은 피드백 누적 개수를 한 번에 조회한다. */ diff --git a/src/main/java/com/slatto/domain/video/service/VideoService.java b/src/main/java/com/slatto/domain/video/service/VideoService.java index 0e150c3c..a35139be 100644 --- a/src/main/java/com/slatto/domain/video/service/VideoService.java +++ b/src/main/java/com/slatto/domain/video/service/VideoService.java @@ -59,6 +59,7 @@ public class VideoService { private final YoutubeUrlParser youtubeUrlParser; private final YoutubeApiClient youtubeApiClient; + @Transactional public VideoDetailResDTO getVideo(Long memberId, Long projectId, Long videoId) { if (!projectAccessRepository.projectExistsById(projectId)) { throw new BaseException(CommonErrorCode.NOT_FOUND); @@ -75,6 +76,8 @@ public VideoDetailResDTO getVideo(Long memberId, Long projectId, Long videoId) { projectAccessRepository.findProjectRoleNames(projectId) ); + notificationService.markVideoFeedbackNotificationsAsRead(memberId, videoId); + return VideoDetailResDTO.from(video, bookmarked, projectTags); } From f35532e0c7a7a660b99a4c5097bd1c4a05e0db80 Mon Sep 17 00:00:00 2001 From: guingguing Date: Thu, 6 Aug 2026 14:57:27 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=9D=BC=EC=A0=95=20=EB=93=B1=EB=A1=9D=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/service/ScheduleService.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/java/com/slatto/domain/schedule/service/ScheduleService.java b/src/main/java/com/slatto/domain/schedule/service/ScheduleService.java index a3ab6539..bddba54b 100644 --- a/src/main/java/com/slatto/domain/schedule/service/ScheduleService.java +++ b/src/main/java/com/slatto/domain/schedule/service/ScheduleService.java @@ -211,6 +211,15 @@ public ScheduleResponse createSchedule(Long currentUserId, ScheduleCreateRequest writer.getId() ); if (savedSchedule.isProjectSchedule()) { + notificationService.createScheduleCreatedNotifications( + project.getId(), + savedSchedule.getId(), + project.getTitle(), + savedSchedule.getTitle(), + writer.getNickname(), + getActiveProjectMemberUserIds(project.getId()), + writer.getId() + ); activityLogService.createScheduleCreatedLog( project.getId(), currentUserId, @@ -377,6 +386,14 @@ private List getProjectParticipantUsers(Long projectId, List partic .toList(); } + private List getActiveProjectMemberUserIds(Long projectId) { + return projectMemberRepository.findAllActiveMembersByProjectId(projectId) + .stream() + .map(ProjectMember::getUser) + .map(Users::getId) + .toList(); + } + private Users getActiveUser(Long userId) { return userRepository.findByIdAndDeletedAtIsNull(userId) .orElseThrow(() -> new BaseException(CommonErrorCode.NOT_FOUND)); From a2c20171cf4b0261153f8bf5559412b57d2c02c5 Mon Sep 17 00:00:00 2001 From: guingguing Date: Thu, 6 Aug 2026 15:30:14 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20=ED=8C=8C=EC=9D=BC=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EC=95=8C=EB=A6=BC=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/service/ProjectFileService.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/com/slatto/domain/project/service/ProjectFileService.java b/src/main/java/com/slatto/domain/project/service/ProjectFileService.java index a756da95..09369d9b 100644 --- a/src/main/java/com/slatto/domain/project/service/ProjectFileService.java +++ b/src/main/java/com/slatto/domain/project/service/ProjectFileService.java @@ -1,6 +1,7 @@ package com.slatto.domain.project.service; import com.slatto.domain.notification.service.ActivityLogService; +import com.slatto.domain.notification.service.NotificationService; import com.slatto.domain.project.dto.ProjectFileDownloadResponse; import com.slatto.domain.project.dto.ProjectFileResponse; import com.slatto.domain.project.dto.ProjectFileListResponse; @@ -12,6 +13,7 @@ import com.slatto.domain.project.entity.ProjectMember; import com.slatto.domain.project.exception.ProjectErrorCode; import com.slatto.domain.project.repository.ProjectFileRepository; +import com.slatto.domain.project.repository.ProjectMemberRepository; import com.slatto.domain.user.entity.Users; import com.slatto.global.exception.BaseException; import com.slatto.global.response.code.CommonErrorCode; @@ -53,8 +55,10 @@ public class ProjectFileService { private final ProjectFileRepository projectFileRepository; private final ProjectAccessValidator projectAccessValidator; + private final ProjectMemberRepository projectMemberRepository; private final StorageService storageService; private final ActivityLogService activityLogService; + private final NotificationService notificationService; public ProjectFileListResponse getProjectFiles( Long projectId, @@ -123,6 +127,14 @@ public ProjectFileResponse uploadProjectFile( ProjectFile savedFile = projectFileRepository.save(projectFile); activityLogService.createFileUploadedLog(projectId, currentUserId, savedFile.getId(), savedFile.getFileName()); + notificationService.createFileUploadedNotifications( + projectId, + project.getTitle(), + savedFile.getFileName(), + currentMember.getUser().getNickname(), + getActiveProjectMemberUserIds(projectId), + currentUserId + ); return toResponse(savedFile); } @@ -321,6 +333,14 @@ private int normalizePageSize(int size) { return Math.min(size, MAX_PAGE_SIZE); } + private List getActiveProjectMemberUserIds(Long projectId) { + return projectMemberRepository.findAllActiveMembersByProjectId(projectId) + .stream() + .map(ProjectMember::getUser) + .map(Users::getId) + .toList(); + } + private ProjectFileResponse toResponse(ProjectFile projectFile) { Users uploader = projectFile.getUploader(); From 0fb3e878dd7f28e854e0496a30fb0a8161f155cd Mon Sep 17 00:00:00 2001 From: guingguing Date: Thu, 6 Aug 2026 15:34:42 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat:=20=EA=B3=B5=EC=A7=80=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EC=95=8C=EB=A6=BC=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/service/ProjectNoticeService.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/main/java/com/slatto/domain/project/service/ProjectNoticeService.java b/src/main/java/com/slatto/domain/project/service/ProjectNoticeService.java index 0c0e0aff..79469abe 100644 --- a/src/main/java/com/slatto/domain/project/service/ProjectNoticeService.java +++ b/src/main/java/com/slatto/domain/project/service/ProjectNoticeService.java @@ -1,6 +1,7 @@ package com.slatto.domain.project.service; import com.slatto.domain.notification.service.ActivityLogService; +import com.slatto.domain.notification.service.NotificationService; import com.slatto.domain.project.dto.ProjectNoticeCreateRequest; import com.slatto.domain.project.dto.ProjectNoticeListResponse; import com.slatto.domain.project.dto.ProjectNoticeReadResponse; @@ -11,6 +12,7 @@ import com.slatto.domain.project.entity.ProjectNotice; import com.slatto.domain.project.entity.ProjectNoticeRead; import com.slatto.domain.project.exception.ProjectErrorCode; +import com.slatto.domain.project.repository.ProjectMemberRepository; import com.slatto.domain.project.repository.ProjectNoticeReadRepository; import com.slatto.domain.project.repository.ProjectNoticeRepository; import com.slatto.domain.user.entity.Users; @@ -35,8 +37,10 @@ public class ProjectNoticeService { private final ProjectNoticeRepository projectNoticeRepository; private final ProjectNoticeReadRepository projectNoticeReadRepository; + private final ProjectMemberRepository projectMemberRepository; private final ProjectAccessValidator projectAccessValidator; private final ActivityLogService activityLogService; + private final NotificationService notificationService; public ProjectNoticeListResponse getProjectNotices( Long projectId, @@ -95,6 +99,15 @@ public ProjectNoticeResponse createProjectNotice( ); ProjectNotice savedNotice = projectNoticeRepository.save(projectNotice); activityLogService.createNoticeCreatedLog(projectId, currentUserId, savedNotice.getId()); + notificationService.createNoticeCreatedNotifications( + projectId, + savedNotice.getId(), + project.getTitle(), + savedNotice.getTitle(), + currentMember.getUser().getNickname(), + getActiveProjectMemberUserIds(projectId), + currentUserId + ); return toResponse(savedNotice, false); } @@ -202,6 +215,14 @@ private Map getReadByNoticeId(Long userId, List pr )); } + private List getActiveProjectMemberUserIds(Long projectId) { + return projectMemberRepository.findAllActiveMembersByProjectId(projectId) + .stream() + .map(ProjectMember::getUser) + .map(Users::getId) + .toList(); + } + private int normalizePageSize(int size) { if (size <= 0) { return DEFAULT_PAGE_SIZE; From bb5a66452814bf23b155275ba919fa572d834977 Mon Sep 17 00:00:00 2001 From: guingguing Date: Thu, 6 Aug 2026 16:01:42 +0900 Subject: [PATCH 06/10] =?UTF-8?q?test:=20=EA=B3=B5=EC=A7=80=20=EC=B5=9C?= =?UTF-8?q?=EA=B7=BC=ED=99=9C=EB=8F=99=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EB=B3=B4?= =?UTF-8?q?=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ProjectNoticeActivityFlowIntegrationTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/java/com/slatto/domain/notification/service/ProjectNoticeActivityFlowIntegrationTest.java b/src/test/java/com/slatto/domain/notification/service/ProjectNoticeActivityFlowIntegrationTest.java index 55d41eb8..42b2e8d0 100644 --- a/src/test/java/com/slatto/domain/notification/service/ProjectNoticeActivityFlowIntegrationTest.java +++ b/src/test/java/com/slatto/domain/notification/service/ProjectNoticeActivityFlowIntegrationTest.java @@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import java.time.LocalDate; @@ -56,6 +57,9 @@ class ProjectNoticeActivityFlowIntegrationTest { @Autowired private ProjectActivityReadRepository projectActivityReadRepository; + @MockBean + private NotificationService notificationService; + @Autowired private UserRepository userRepository; From d2029f44b5f2f9258c8b5b895b308924447dae7b Mon Sep 17 00:00:00 2001 From: guingguing Date: Thu, 6 Aug 2026 18:38:35 +0900 Subject: [PATCH 07/10] =?UTF-8?q?fix:=20=EA=B0=9C=EC=9D=B8=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EB=AC=B8=EA=B5=AC=20=EC=A0=95=EC=B1=85=EC=84=9C=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/NotificationService.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/slatto/domain/notification/service/NotificationService.java b/src/main/java/com/slatto/domain/notification/service/NotificationService.java index eb96b58b..3dcdd13f 100644 --- a/src/main/java/com/slatto/domain/notification/service/NotificationService.java +++ b/src/main/java/com/slatto/domain/notification/service/NotificationService.java @@ -669,7 +669,7 @@ private NotificationListResponse.NotificationSummary toSummary(Notification noti } private String createScheduleAssignedContent(String scheduleTitle, String assigneeName) { - return assigneeName + "님이 [" + scheduleTitle + "] 담당자로 지정되었어요"; + return assigneeName + "님이 [" + scheduleTitle + "] 담당자로 지명되었어요."; } private String createScheduleAssignedTitle(String projectTitle) { @@ -681,7 +681,7 @@ private String createProjectJoinedTitle(String projectTitle) { } private String createProjectJoinedContent(String joinerName) { - return joinerName + "님이 프로젝트에 합류했어요"; + return joinerName + "님이 프로젝트에 합류했어요."; } private String createScheduleCreatedTitle(String projectTitle) { @@ -689,7 +689,7 @@ private String createScheduleCreatedTitle(String projectTitle) { } private String createScheduleCreatedContent(String scheduleTitle, String creatorName) { - return creatorName + "님이 [" + scheduleTitle + "] 일정을 등록했어요"; + return creatorName + "님이 [" + scheduleTitle + "] 일정을 등록했어요."; } private String createVideoFeedbackCommentedTitle(String projectTitle) { @@ -702,10 +702,10 @@ private String createVideoFeedbackCommentedContent( int groupCount ) { if (groupCount <= 1) { - return commenterName + "님이 [" + videoTitle + "]에 새로운 피드백을 남겼어요"; + return commenterName + "님이 [" + videoTitle + "]에 새로운 피드백을 남겼어요."; } - return "[" + videoTitle + "]에 새로운 피드백 " + groupCount + "건이 등록되었어요"; + return "[" + videoTitle + "]에 새로운 피드백 " + groupCount + "건이 등록되었어요."; } private String createRecruitmentAppliedTitle(String projectTitle) { @@ -718,10 +718,10 @@ private String createRecruitmentAppliedContent( int groupCount ) { if (groupCount <= 1) { - return applicantName + "님이 [" + recruitmentTitle + "]에 지원했어요"; + return applicantName + "님이 [" + recruitmentTitle + "]에 지원했어요."; } - return "[" + recruitmentTitle + "]에 새로운 지원자 " + groupCount + "명이 지원했어요"; + return "[" + recruitmentTitle + "]에 새로운 지원자 " + groupCount + "명이 지원했어요."; } private String createNoticeCreatedTitle(String projectTitle) { @@ -729,7 +729,7 @@ private String createNoticeCreatedTitle(String projectTitle) { } private String createNoticeCreatedContent(String noticeTitle, String creatorName) { - return creatorName + "님이 새 공지를 등록했어요: " + noticeTitle; + return creatorName + "님이 새 공지를 등록했어요: " + noticeTitle + "."; } private String createFileUploadedTitle(String projectTitle) { @@ -737,7 +737,7 @@ private String createFileUploadedTitle(String projectTitle) { } private String createFileUploadedContent(String fileName, String uploaderName) { - return uploaderName + "님이 [" + fileName + "] 파일을 등록했어요"; + return uploaderName + "님이 [" + fileName + "] 파일을 등록했어요."; } private String createFallbackTitle(NotificationType type) { From ccaee95d8420acdff2e18185d00f33e721e50b5e Mon Sep 17 00:00:00 2001 From: guingguing Date: Thu, 6 Aug 2026 19:48:26 +0900 Subject: [PATCH 08/10] =?UTF-8?q?fix:=20=ED=8C=8C=EC=9D=BC=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EB=8C=80=EC=83=81=20ID=EB=A5=BC=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20ID=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/notification/service/NotificationService.java | 4 +++- .../com/slatto/domain/project/service/ProjectFileService.java | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/slatto/domain/notification/service/NotificationService.java b/src/main/java/com/slatto/domain/notification/service/NotificationService.java index 3dcdd13f..3da29e8d 100644 --- a/src/main/java/com/slatto/domain/notification/service/NotificationService.java +++ b/src/main/java/com/slatto/domain/notification/service/NotificationService.java @@ -461,6 +461,7 @@ public void createNoticeCreatedNotifications( @Transactional public void createFileUploadedNotifications( Long projectId, + Long fileId, String projectTitle, String fileName, String uploaderName, @@ -468,6 +469,7 @@ public void createFileUploadedNotifications( Long actorUserId ) { validateRequiredId(projectId); + validateRequiredId(fileId); validateRequiredId(actorUserId); validateRequiredText(projectTitle); validateRequiredText(fileName); @@ -480,7 +482,7 @@ public void createFileUploadedNotifications( .title(createFileUploadedTitle(projectTitle)) .content(createFileUploadedContent(fileName, uploaderName)) .targetType(NotificationTargetType.PROJECT_FILE) - .targetId(projectId) + .targetId(fileId) .excludeUserId(actorUserId) .build()); } diff --git a/src/main/java/com/slatto/domain/project/service/ProjectFileService.java b/src/main/java/com/slatto/domain/project/service/ProjectFileService.java index 09369d9b..2e316dcf 100644 --- a/src/main/java/com/slatto/domain/project/service/ProjectFileService.java +++ b/src/main/java/com/slatto/domain/project/service/ProjectFileService.java @@ -129,6 +129,7 @@ public ProjectFileResponse uploadProjectFile( activityLogService.createFileUploadedLog(projectId, currentUserId, savedFile.getId(), savedFile.getFileName()); notificationService.createFileUploadedNotifications( projectId, + savedFile.getId(), project.getTitle(), savedFile.getFileName(), currentMember.getUser().getNickname(), From c0b68ac00ba0fb2ae44a3d0a5674ee435c3558fa Mon Sep 17 00:00:00 2001 From: young Date: Thu, 6 Aug 2026 20:14:09 +0900 Subject: [PATCH 09/10] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=88=98=EC=A0=95=20(#131)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/notification/repository/NotificationRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/slatto/domain/notification/repository/NotificationRepository.java b/src/main/java/com/slatto/domain/notification/repository/NotificationRepository.java index ea0bbf98..dd2c7267 100644 --- a/src/main/java/com/slatto/domain/notification/repository/NotificationRepository.java +++ b/src/main/java/com/slatto/domain/notification/repository/NotificationRepository.java @@ -103,7 +103,7 @@ int markAsReadByIdAndUserId( @Param("readAt") LocalDateTime readAt ); - @Modifying(clearAutomatically = true, flushAutomatically = true) + @Modifying(flushAutomatically = true) @Query(""" update Notification n set n.isRead = true, From bc1379c95e2b191a834d1698e9964d779a6a9239 Mon Sep 17 00:00:00 2001 From: chazy-d Date: Thu, 6 Aug 2026 20:15:29 +0900 Subject: [PATCH 10/10] =?UTF-8?q?fix:=20project=5Ffile=20=EB=A0=88?= =?UTF-8?q?=EA=B1=B0=EC=8B=9C=20=EC=BB=AC=EB=9F=BC=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=EB=A1=9C=20=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 파일 업로드가 운영에서 전건 500으로 실패했다. JpaSystemException: could not execute statement [Field 'file_url' doesn't have a default value] at ProjectFileService.uploadProjectFile(ProjectFileService.java:124) a833792 에서 ProjectFile 의 file_url 을 storage_key 로, isPinned(boolean) 을 pinnedAt(nullable) 로 바꾸면서 대응 마이그레이션을 만들지 않았다. 운영 DB 는 ddl-auto=update 로 생성돼 신규 컬럼만 추가되고 구 컬럼 file_url, is_pinned 가 NOT NULL / DEFAULT 없음 상태로 남았다. 현재 INSERT 문은 두 컬럼을 포함하지 않으므로 모든 INSERT 가 거부된다. ddl-auto=validate 는 엔티티에 있는데 DB 에 없는 컬럼만 검사하고 그 반대는 보지 않아 이 상태를 잡아내지 못했다. 운영 DB 전체 컬럼을 엔티티와 대조해 같은 유형의 고아 컬럼이 project_file 두 건뿐임을 확인했다. file_url 만 제거하면 다음 업로드에서 is_pinned 로 동일하게 실패하므로 함께 제거한다. 삭제 시점 기준 project_file 은 0건이라 데이터 손실이 없다. V011 은 d5191f6 에서 쓰였다가 f1e99f0 에서 삭제된 이력이 있어 재사용하지 않는다. --- ...V012__project_file_drop_legacy_columns.sql | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/main/resources/db/migration/V012__project_file_drop_legacy_columns.sql diff --git a/src/main/resources/db/migration/V012__project_file_drop_legacy_columns.sql b/src/main/resources/db/migration/V012__project_file_drop_legacy_columns.sql new file mode 100644 index 00000000..935cca86 --- /dev/null +++ b/src/main/resources/db/migration/V012__project_file_drop_legacy_columns.sql @@ -0,0 +1,26 @@ +-- project_file 레거시 컬럼 제거 (file_url, is_pinned) +-- +-- 배경: +-- a833792(2026-07-23) "프로젝트 파일 도메인 모델 정리"에서 ProjectFile 엔티티의 +-- file_url -> storage_key, isPinned(boolean) -> pinnedAt(nullable) 으로 모델을 바꿨으나 +-- 대응 마이그레이션이 없었다. 운영 DB는 ddl-auto=update로 생성돼 신규 컬럼만 추가되고 +-- 구 컬럼 두 개가 NOT NULL / DEFAULT 없음 상태로 남았다. +-- 현재 INSERT문은 두 컬럼을 포함하지 않으므로 파일 업로드가 전건 실패한다. +-- Field 'file_url' doesn't have a default value +-- ddl-auto=validate는 "엔티티에 있는데 DB에 없는 컬럼"만 검사하고 그 반대는 보지 않아 +-- 이 상태를 잡아내지 못했다. +-- +-- 안전성: +-- 삭제 시점 기준 project_file 행 수 0건이므로 데이터 손실 없음. +-- +-- 주의: +-- MySQL 8.4에는 DROP COLUMN IF EXISTS가 없다(MariaDB 전용 문법). 조건 없이 삭제한다. +-- 따라서 두 컬럼이 존재하는 DB에서만 성공한다. 운영 DB에서 직접 DROP을 먼저 실행하면 +-- 이 마이그레이션이 ERROR 1091로 실패해 이후 배포가 모두 막히므로 절대 병행하지 말 것. +-- +-- 버전 번호: +-- V011은 d5191f6에서 activity_log 인덱스용으로 쓰였다가 f1e99f0에서 삭제된 이력이 있어 +-- 재사용하지 않는다. 번호를 비워도 Flyway 동작에는 영향이 없다. +ALTER TABLE project_file + DROP COLUMN file_url, + DROP COLUMN is_pinned;