diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 00000000..fdb52da9 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,42 @@ +name: Build Docker Image + +on: + push: + branches: + - main + +permissions: + contents: read + packages: write + +concurrency: + group: docker-image-main + cancel-in-progress: false + +jobs: + docker: + name: Build and Push Docker Image + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and Push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + ghcr.io/slat-to/slat-to-be:${{ github.sha }} + ghcr.io/slat-to/slat-to-be:latest \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6eb0cf64..3ad76768 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ out/ ### Local Environment ### .env .env.* +.env.docker !.env.example application-local.yml application-dev.yml diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 00000000..4be978e8 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,12 @@ +services: + slatto: + image: ghcr.io/slat-to/slat-to-be:${IMAGE_TAG} + container_name: slatto-prod + + env_file: + - /home/ubuntu/SLAT-TO-BE/.env.prod + + ports: + - "8081:8080" + + restart: unless-stopped \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..635c9d66 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +services: + slatto: + build: . + + env_file: + - .env.docker + + ports: + - "8080:8080" \ No newline at end of file diff --git a/src/main/java/com/slatto/domain/feedback/service/FeedbackDetailService.java b/src/main/java/com/slatto/domain/feedback/service/FeedbackDetailService.java index 396e7a1b..9fbe33ce 100644 --- a/src/main/java/com/slatto/domain/feedback/service/FeedbackDetailService.java +++ b/src/main/java/com/slatto/domain/feedback/service/FeedbackDetailService.java @@ -7,6 +7,7 @@ import com.slatto.domain.feedback.dto.request.FeedbackDetailRequest.ReplyUpdateReqDTO; import com.slatto.domain.feedback.dto.response.FeedbackDetailResponse.ReplyUpdateResDTO; import com.slatto.domain.project.repository.ProjectMemberRepository; +import com.slatto.domain.notification.service.ActivityLogService; import com.slatto.domain.feedback.dto.request.FeedbackDetailRequest.ReplyStatusReqDTO; import com.slatto.domain.feedback.dto.response.FeedbackDetailResponse.ReplyStatusResDTO; import org.springframework.data.domain.PageRequest; @@ -38,6 +39,7 @@ public class FeedbackDetailService { private final GuestRepository guestRepository; private final FeedbackDetailConverter feedbackDetailConverter; private final ProjectMemberRepository projectMemberRepository; + private final ActivityLogService activityLogService; private static final int DEFAULT_PAGE_SIZE = 10; private static final int MAX_PAGE_SIZE = 50; // 페이지 크기 상한 @@ -69,6 +71,22 @@ public ReplyCreateResDTO createReply(Long feedbackId, Long userId, ReplyCreateRe FeedbackDetail reply = feedbackDetailConverter.toFeedbackDetail(feedback, user, guest, req); FeedbackDetail saved = feedbackDetailRepository.save(reply); + if (user != null) { + activityLogService.createVideoFeedbackCommentedLog( + feedback.getVideo().getProject().getId(), + user.getId(), + feedback.getVideo().getId(), + feedback.getVideo().getTitle() + ); + } else { + activityLogService.createGuestVideoFeedbackCommentedLog( + feedback.getVideo().getProject().getId(), + guest, + feedback.getVideo().getId(), + feedback.getVideo().getTitle() + ); + } + return feedbackDetailConverter.toCreateResponse(saved); } @@ -224,4 +242,4 @@ public ReplyStatusResDTO changeReplyStatus(Long replyId, Long userId, ReplyStatu return feedbackDetailConverter.toStatusResponse(reply); } -} \ No newline at end of file +} diff --git a/src/main/java/com/slatto/domain/feedback/service/FeedbackService.java b/src/main/java/com/slatto/domain/feedback/service/FeedbackService.java index 53278538..9c99b126 100644 --- a/src/main/java/com/slatto/domain/feedback/service/FeedbackService.java +++ b/src/main/java/com/slatto/domain/feedback/service/FeedbackService.java @@ -9,6 +9,7 @@ import com.slatto.domain.feedback.dto.request.FeedbackRequest.FeedbackStatusReqDTO; import com.slatto.domain.feedback.dto.response.FeedbackResponse.FeedbackStatusResDTO; import com.slatto.domain.feedback.repository.FeedbackDetailRepository; +import com.slatto.domain.notification.service.ActivityLogService; import com.slatto.domain.project.repository.ProjectMemberRepository; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -45,6 +46,7 @@ public class FeedbackService { private final ObjectProvider entityManagerProvider; private final ProjectMemberRepository projectMemberRepository; private final FeedbackDetailRepository feedbackDetailRepository; + private final ActivityLogService activityLogService; private static final int DEFAULT_PAGE_SIZE = 10; private static final int MAX_PAGE_SIZE = 50; // 페이지 크기 상한 @@ -80,6 +82,22 @@ public FeedbackCreateResDTO createFeedback(Long videoId, Long userId, FeedbackCr Feedback feedback = feedbackConverter.toFeedback(video, user, guest, req); Feedback saved = feedbackRepository.save(feedback); + if (user != null) { + activityLogService.createVideoFeedbackCommentedLog( + video.getProject().getId(), + user.getId(), + video.getId(), + video.getTitle() + ); + } else { + activityLogService.createGuestVideoFeedbackCommentedLog( + video.getProject().getId(), + guest, + video.getId(), + video.getTitle() + ); + } + return feedbackConverter.toCreateResponse(saved); } @@ -278,4 +296,4 @@ public FeedbackStatusResDTO changeFeedbackStatus(Long feedbackId, Long userId, F return feedbackConverter.toStatusResponse(feedback); } -} \ No newline at end of file +} diff --git a/src/main/java/com/slatto/domain/notification/entity/ActivityLog.java b/src/main/java/com/slatto/domain/notification/entity/ActivityLog.java index e5e12da2..143e6cea 100644 --- a/src/main/java/com/slatto/domain/notification/entity/ActivityLog.java +++ b/src/main/java/com/slatto/domain/notification/entity/ActivityLog.java @@ -1,9 +1,11 @@ package com.slatto.domain.notification.entity; import com.slatto.domain.common.entity.BaseEntity; -import com.slatto.domain.project.entity.Project; import com.slatto.domain.notification.enums.ActorType; +import com.slatto.domain.notification.enums.ActivityLogTargetType; import com.slatto.domain.notification.enums.ActivityLogType; +import com.slatto.domain.notification.model.ActivityActor; +import com.slatto.domain.project.entity.Project; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; @@ -52,40 +54,35 @@ public class ActivityLog extends BaseEntity { private ActivityLog( Project project, - Long actorUserId, - Long actorGuestId, - ActorType actorType, + ActivityActor actor, ActivityLogType type, String content, - String targetType, + ActivityLogTargetType targetType, Long targetId, String groupKey ) { this.project = project; - this.actorUserId = actorUserId; - this.actorGuestId = actorGuestId; - this.actorType = actorType; + this.actorUserId = actor.actorUserId(); + this.actorGuestId = actor.actorGuestId(); + this.actorType = actor.actorType(); this.type = type; this.content = content; - this.targetType = targetType; + this.targetType = targetType != null ? targetType.name() : null; this.targetId = targetId; this.groupKey = groupKey; } public static ActivityLog create( Project project, - Long actorUserId, - ActorType actorType, + ActivityActor actor, ActivityLogType type, String content, - String targetType, + ActivityLogTargetType targetType, Long targetId ) { return new ActivityLog( project, - actorUserId, - null, - actorType, + actor, type, content, targetType, @@ -93,4 +90,5 @@ public static ActivityLog create( null ); } + } diff --git a/src/main/java/com/slatto/domain/notification/model/ActivityActor.java b/src/main/java/com/slatto/domain/notification/model/ActivityActor.java new file mode 100644 index 00000000..6ef164ba --- /dev/null +++ b/src/main/java/com/slatto/domain/notification/model/ActivityActor.java @@ -0,0 +1,63 @@ +package com.slatto.domain.notification.model; + +import com.slatto.domain.notification.enums.ActorType; + +public record ActivityActor( + ActorType actorType, + Long actorUserId, + Long actorGuestId, + String displayName +) { + + public ActivityActor { + if (actorType == null) { + throw new IllegalArgumentException("최근활동 행위자 타입은 필수입니다."); + } + if (displayName == null || displayName.isBlank()) { + throw new IllegalArgumentException("최근활동 행위자 이름은 필수입니다."); + } + + validateActorIds(actorType, actorUserId, actorGuestId); + } + + public static ActivityActor user(Long userId, String displayName) { + return new ActivityActor(ActorType.USER, userId, null, displayName); + } + + public static ActivityActor clientReviewer(Long guestId, String displayName) { + return new ActivityActor(ActorType.CLIENT_REVIEWER, null, guestId, displayName); + } + + public static ActivityActor system(String displayName) { + return new ActivityActor(ActorType.SYSTEM, null, null, displayName); + } + + private static void validateActorIds(ActorType actorType, Long actorUserId, Long actorGuestId) { + switch (actorType) { + case USER -> { + requirePositiveId(actorUserId, "사용자"); + requireNull(actorGuestId, "회원 행위자는 게스트 ID를 가질 수 없습니다."); + } + case CLIENT_REVIEWER -> { + requireNull(actorUserId, "게스트 행위자는 사용자 ID를 가질 수 없습니다."); + requirePositiveId(actorGuestId, "게스트"); + } + case SYSTEM -> { + requireNull(actorUserId, "시스템 행위자는 사용자 ID를 가질 수 없습니다."); + requireNull(actorGuestId, "시스템 행위자는 게스트 ID를 가질 수 없습니다."); + } + } + } + + private static void requirePositiveId(Long id, String actorName) { + if (id == null || id <= 0) { + throw new IllegalArgumentException(actorName + " ID는 양수여야 합니다."); + } + } + + private static void requireNull(Long id, String message) { + if (id != null) { + throw new IllegalArgumentException(message); + } + } +} diff --git a/src/main/java/com/slatto/domain/notification/service/ActivityLogService.java b/src/main/java/com/slatto/domain/notification/service/ActivityLogService.java index 7d32e772..a2da37de 100644 --- a/src/main/java/com/slatto/domain/notification/service/ActivityLogService.java +++ b/src/main/java/com/slatto/domain/notification/service/ActivityLogService.java @@ -3,10 +3,11 @@ import com.slatto.domain.notification.entity.ActivityLog; import com.slatto.domain.notification.enums.ActivityLogTargetType; import com.slatto.domain.notification.enums.ActivityLogType; -import com.slatto.domain.notification.enums.ActorType; +import com.slatto.domain.notification.model.ActivityActor; import com.slatto.domain.notification.repository.ActivityLogRepository; import com.slatto.domain.project.entity.Project; import com.slatto.domain.project.repository.ProjectRepository; +import com.slatto.domain.sharelink.entity.Guest; import com.slatto.domain.user.entity.Users; import com.slatto.domain.user.repository.UserRepository; import com.slatto.global.exception.BaseException; @@ -23,6 +24,7 @@ public class ActivityLogService { private final ActivityLogRepository activityLogRepository; private final ProjectRepository projectRepository; private final UserRepository userRepository; + private final ActivityMessageFactory activityMessageFactory; /** * 프로젝트에 새 참여자가 합류했을 때 최근활동을 저장한다. @@ -35,7 +37,7 @@ public void createProjectMemberJoinedLog(Long projectId, Long actorUserId) { projectId, actor, ActivityLogType.PROJECT_MEMBER_JOINED, - createProjectMemberJoinedContent(actor.getNickname()), + activityMessageFactory.projectMemberJoined(actor.getNickname()), ActivityLogTargetType.PROJECT, projectId ); @@ -59,7 +61,7 @@ public void createProjectStatusChangedLog( projectId, actor, ActivityLogType.PROJECT_STATUS_CHANGED, - createProjectStatusChangedContent(actor.getNickname(), previousStatus, changedStatus), + activityMessageFactory.projectStatusChanged(actor.getNickname(), previousStatus, changedStatus), ActivityLogTargetType.PROJECT, projectId ); @@ -76,7 +78,7 @@ public void createProjectUpdatedLog(Long projectId, Long actorUserId) { projectId, actor, ActivityLogType.PROJECT_UPDATED, - createProjectUpdatedContent(actor.getNickname()), + activityMessageFactory.projectUpdated(actor.getNickname()), ActivityLogTargetType.PROJECT, projectId ); @@ -100,7 +102,7 @@ public void createScheduleCreatedLog( projectId, actor, ActivityLogType.SCHEDULE_CREATED, - createScheduleCreatedContent(actor.getNickname(), scheduleTitle), + activityMessageFactory.scheduleCreated(actor.getNickname(), scheduleTitle), ActivityLogTargetType.SCHEDULE, scheduleId ); @@ -124,7 +126,7 @@ public void createScheduleUpdatedLog( projectId, actor, ActivityLogType.SCHEDULE_UPDATED, - createScheduleUpdatedContent(actor.getNickname(), scheduleTitle), + activityMessageFactory.scheduleUpdated(actor.getNickname(), scheduleTitle), ActivityLogTargetType.SCHEDULE, scheduleId ); @@ -146,7 +148,7 @@ public void createNoticeCreatedLog( projectId, actor, ActivityLogType.NOTICE_CREATED, - createNoticeCreatedContent(actor.getNickname()), + activityMessageFactory.noticeCreated(actor.getNickname()), ActivityLogTargetType.NOTICE, noticeId ); @@ -170,7 +172,7 @@ public void createFileUploadedLog( projectId, actor, ActivityLogType.FILE_UPLOADED, - createFileUploadedContent(actor.getNickname(), fileName), + activityMessageFactory.fileUploaded(actor.getNickname(), fileName), ActivityLogTargetType.FILE, fileId ); @@ -194,7 +196,32 @@ public void createVideoFeedbackCommentedLog( projectId, actor, ActivityLogType.VIDEO_FEEDBACK_COMMENTED, - createVideoFeedbackCommentedContent(actor.getNickname(), videoTitle), + activityMessageFactory.videoFeedbackCommented(actor.getNickname(), videoTitle), + ActivityLogTargetType.VIDEO, + videoId + ); + } + + /** + * 공유 링크 게스트가 영상 피드백을 남겼을 때 최근활동을 저장한다. + * 피드백 도메인의 실제 호출 연결은 후속 작업에서 추가한다. + */ + @Transactional + public void createGuestVideoFeedbackCommentedLog( + Long projectId, + Guest guest, + Long videoId, + String videoTitle + ) { + validateRequiredGuest(guest); + validateRequiredId(videoId); + validateRequiredText(videoTitle); + + createActivityLog( + projectId, + ActivityActor.clientReviewer(guest.getId(), guest.getName()), + ActivityLogType.VIDEO_FEEDBACK_COMMENTED, + activityMessageFactory.videoFeedbackCommented(guest.getName(), videoTitle), ActivityLogTargetType.VIDEO, videoId ); @@ -207,56 +234,37 @@ private void createUserActivityLog( String content, ActivityLogTargetType targetType, Long targetId + ) { + createActivityLog( + projectId, + ActivityActor.user(actor.getId(), actor.getNickname()), + type, + content, + targetType, + targetId + ); + } + + private void createActivityLog( + Long projectId, + ActivityActor actor, + ActivityLogType type, + String content, + ActivityLogTargetType targetType, + Long targetId ) { Project project = getActiveProject(projectId); activityLogRepository.save(ActivityLog.create( project, - actor.getId(), - ActorType.USER, + actor, type, content, - getTargetTypeName(targetType), + targetType, targetId )); } - private String createProjectMemberJoinedContent(String actorName) { - return actorName + "님이 프로젝트에 합류했어요"; - } - - private String createProjectStatusChangedContent( - String actorName, - String previousStatus, - String changedStatus - ) { - return actorName + "님이 프로젝트 단계를 '" + previousStatus + "'에서 '" + changedStatus + "'으로 변경했어요"; - } - - private String createProjectUpdatedContent(String actorName) { - return actorName + "님이 프로젝트 정보를 수정했어요"; - } - - private String createScheduleCreatedContent(String actorName, String scheduleTitle) { - return actorName + "님이 [" + scheduleTitle + "] 일정을 등록했어요"; - } - - private String createScheduleUpdatedContent(String actorName, String scheduleTitle) { - return actorName + "님이 [" + scheduleTitle + "] 일정을 수정했어요"; - } - - private String createNoticeCreatedContent(String actorName) { - return actorName + "님이 새 공지를 등록했어요"; - } - - private String createFileUploadedContent(String actorName, String fileName) { - return actorName + "님이 [" + fileName + "] 파일을 등록했어요"; - } - - private String createVideoFeedbackCommentedContent(String actorName, String videoTitle) { - return actorName + "님이 [" + videoTitle + "]에 피드백을 남겼어요"; - } - private Project getActiveProject(Long projectId) { validateRequiredId(projectId); @@ -283,7 +291,10 @@ private void validateRequiredText(String text) { } } - private String getTargetTypeName(ActivityLogTargetType targetType) { - return targetType != null ? targetType.name() : null; + private void validateRequiredGuest(Guest guest) { + if (guest == null) { + throw new BaseException(CommonErrorCode.BAD_REQUEST); + } } + } diff --git a/src/main/java/com/slatto/domain/notification/service/ActivityMessageFactory.java b/src/main/java/com/slatto/domain/notification/service/ActivityMessageFactory.java new file mode 100644 index 00000000..5588be2f --- /dev/null +++ b/src/main/java/com/slatto/domain/notification/service/ActivityMessageFactory.java @@ -0,0 +1,39 @@ +package com.slatto.domain.notification.service; + +import org.springframework.stereotype.Component; + +@Component +public class ActivityMessageFactory { + + public String projectMemberJoined(String actorName) { + return actorName + "님이 프로젝트에 합류했어요"; + } + + public String projectStatusChanged(String actorName, String previousStatus, String changedStatus) { + return actorName + "님이 프로젝트 단계를 '" + previousStatus + "'에서 '" + changedStatus + "'으로 변경했어요"; + } + + public String projectUpdated(String actorName) { + return actorName + "님이 프로젝트 정보를 수정했어요"; + } + + public String scheduleCreated(String actorName, String scheduleTitle) { + return actorName + "님이 [" + scheduleTitle + "] 일정을 등록했어요"; + } + + public String scheduleUpdated(String actorName, String scheduleTitle) { + return actorName + "님이 [" + scheduleTitle + "] 일정을 수정했어요"; + } + + public String noticeCreated(String actorName) { + return actorName + "님이 새 공지를 등록했어요"; + } + + public String fileUploaded(String actorName, String fileName) { + return actorName + "님이 [" + fileName + "] 파일을 등록했어요"; + } + + public String videoFeedbackCommented(String actorName, String videoTitle) { + return actorName + "님이 [" + videoTitle + "]에 피드백을 남겼어요"; + } +} 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 a2248c45..a756da95 100644 --- a/src/main/java/com/slatto/domain/project/service/ProjectFileService.java +++ b/src/main/java/com/slatto/domain/project/service/ProjectFileService.java @@ -1,5 +1,6 @@ package com.slatto.domain.project.service; +import com.slatto.domain.notification.service.ActivityLogService; import com.slatto.domain.project.dto.ProjectFileDownloadResponse; import com.slatto.domain.project.dto.ProjectFileResponse; import com.slatto.domain.project.dto.ProjectFileListResponse; @@ -53,6 +54,7 @@ public class ProjectFileService { private final ProjectFileRepository projectFileRepository; private final ProjectAccessValidator projectAccessValidator; private final StorageService storageService; + private final ActivityLogService activityLogService; public ProjectFileListResponse getProjectFiles( Long projectId, @@ -120,6 +122,7 @@ public ProjectFileResponse uploadProjectFile( ); ProjectFile savedFile = projectFileRepository.save(projectFile); + activityLogService.createFileUploadedLog(projectId, currentUserId, savedFile.getId(), savedFile.getFileName()); return toResponse(savedFile); } 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 068a5b0a..618879a9 100644 --- a/src/main/java/com/slatto/domain/project/service/ProjectInvitationService.java +++ b/src/main/java/com/slatto/domain/project/service/ProjectInvitationService.java @@ -14,6 +14,7 @@ import com.slatto.domain.project.repository.ProjectInvitationRepository; 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.user.entity.Users; import com.slatto.domain.user.enums.RoleName; import com.slatto.domain.user.repository.UserRepository; @@ -48,6 +49,7 @@ public class ProjectInvitationService { private final UserRepository userRepository; private final ProjectAccessValidator projectAccessValidator; private final ProjectInvitationProperties projectInvitationProperties; + private final ActivityLogService activityLogService; private final SecureRandom secureRandom = new SecureRandom(); @Transactional @@ -114,6 +116,7 @@ public ProjectInvitationAcceptResponse acceptInvitation( .toList(); saveProjectRoles(projectMember, roleNames); projectInvitation.accept(accepter); + activityLogService.createProjectMemberJoinedLog(project.getId(), accepter.getId()); return ProjectInvitationAcceptResponse.builder() .projectId(project.getId()) 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 9779c82a..0c0e0aff 100644 --- a/src/main/java/com/slatto/domain/project/service/ProjectNoticeService.java +++ b/src/main/java/com/slatto/domain/project/service/ProjectNoticeService.java @@ -1,5 +1,6 @@ package com.slatto.domain.project.service; +import com.slatto.domain.notification.service.ActivityLogService; import com.slatto.domain.project.dto.ProjectNoticeCreateRequest; import com.slatto.domain.project.dto.ProjectNoticeListResponse; import com.slatto.domain.project.dto.ProjectNoticeReadResponse; @@ -35,6 +36,7 @@ public class ProjectNoticeService { private final ProjectNoticeRepository projectNoticeRepository; private final ProjectNoticeReadRepository projectNoticeReadRepository; private final ProjectAccessValidator projectAccessValidator; + private final ActivityLogService activityLogService; public ProjectNoticeListResponse getProjectNotices( Long projectId, @@ -92,6 +94,7 @@ public ProjectNoticeResponse createProjectNotice( request.getContent() ); ProjectNotice savedNotice = projectNoticeRepository.save(projectNotice); + activityLogService.createNoticeCreatedLog(projectId, currentUserId, savedNotice.getId()); return toResponse(savedNotice, false); } diff --git a/src/main/java/com/slatto/domain/project/service/ProjectService.java b/src/main/java/com/slatto/domain/project/service/ProjectService.java index a1f86034..e925687e 100644 --- a/src/main/java/com/slatto/domain/project/service/ProjectService.java +++ b/src/main/java/com/slatto/domain/project/service/ProjectService.java @@ -17,6 +17,7 @@ import com.slatto.domain.project.repository.ProjectPinRepository; import com.slatto.domain.project.repository.ProjectRepository; import com.slatto.domain.project.repository.ProjectUserRoleRepository; +import com.slatto.domain.notification.service.ActivityLogService; import com.slatto.domain.user.entity.Users; import com.slatto.domain.user.enums.RoleName; import com.slatto.domain.user.repository.UserRepository; @@ -52,6 +53,7 @@ public class ProjectService { private final VideoRepository videoRepository; private final ProjectConverter projectConverter; private final ProjectAccessValidator projectAccessValidator; + private final ActivityLogService activityLogService; @Transactional public ProjectResponse createProject(Long ownerUserId, ProjectCreateRequest request) { @@ -167,6 +169,7 @@ public ProjectResponse updateProject( ) { Project project = projectAccessValidator.getProjectOrThrow(projectId); projectAccessValidator.getCurrentAdminOrThrow(projectId, currentUserId); + ProjectStatus previousStatus = project.getStatus(); project.updateInfo( request.getTitle(), @@ -182,6 +185,17 @@ public ProjectResponse updateProject( project.changeStatus(request.getStatus()); } + activityLogService.createProjectUpdatedLog(projectId, currentUserId); + + if (request.getStatus() != null && previousStatus != request.getStatus()) { + activityLogService.createProjectStatusChangedLog( + projectId, + currentUserId, + getProjectStatusLabel(previousStatus), + getProjectStatusLabel(request.getStatus()) + ); + } + return projectConverter.toResponse(project); } @@ -211,6 +225,15 @@ private void validateProjectCreationQuota(Long ownerUserId) { } } + private String getProjectStatusLabel(ProjectStatus status) { + return switch (status) { + case PREPARING -> "준비중"; + case EDITING -> "편집중"; + case REVIEWING -> "검토중"; + case COMPLETED -> "완료"; + }; + } + private void saveProjectRoles(ProjectMember projectMember, List roleNames) { if (roleNames == null || roleNames.isEmpty()) { return; 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 196b262b..a3ab6539 100644 --- a/src/main/java/com/slatto/domain/schedule/service/ScheduleService.java +++ b/src/main/java/com/slatto/domain/schedule/service/ScheduleService.java @@ -1,5 +1,6 @@ package com.slatto.domain.schedule.service; +import com.slatto.domain.notification.service.ActivityLogService; import com.slatto.domain.notification.service.NotificationService; import com.slatto.domain.project.entity.Project; import com.slatto.domain.project.entity.ProjectMember; @@ -51,6 +52,7 @@ public class ScheduleService { private final ProjectAccessValidator projectAccessValidator; private final ScheduleConverter scheduleConverter; private final NotificationService notificationService; + private final ActivityLogService activityLogService; @Transactional public SchedulePrivateMemoResponse upsertPrivateMemo( @@ -208,6 +210,14 @@ public ScheduleResponse createSchedule(Long currentUserId, ScheduleCreateRequest participants, writer.getId() ); + if (savedSchedule.isProjectSchedule()) { + activityLogService.createScheduleCreatedLog( + project.getId(), + currentUserId, + savedSchedule.getId(), + savedSchedule.getTitle() + ); + } return scheduleConverter.toResponse(savedSchedule); } @@ -249,6 +259,14 @@ public ScheduleResponse updateSchedule( currentUserId ); } + if (schedule.isProjectSchedule()) { + activityLogService.createScheduleUpdatedLog( + schedule.getProject().getId(), + currentUserId, + schedule.getId(), + schedule.getTitle() + ); + } return scheduleConverter.toResponse(schedule); } diff --git a/src/test/java/com/slatto/domain/feedback/service/FeedbackActivityLogConnectionTest.java b/src/test/java/com/slatto/domain/feedback/service/FeedbackActivityLogConnectionTest.java new file mode 100644 index 00000000..956a9485 --- /dev/null +++ b/src/test/java/com/slatto/domain/feedback/service/FeedbackActivityLogConnectionTest.java @@ -0,0 +1,183 @@ +package com.slatto.domain.feedback.service; + +import com.slatto.domain.feedback.converter.FeedbackConverter; +import com.slatto.domain.feedback.converter.FeedbackDetailConverter; +import com.slatto.domain.feedback.dto.request.FeedbackDetailRequest.ReplyCreateReqDTO; +import com.slatto.domain.feedback.dto.request.FeedbackRequest.FeedbackCreateReqDTO; +import com.slatto.domain.feedback.entity.Feedback; +import com.slatto.domain.feedback.entity.FeedbackDetail; +import com.slatto.domain.feedback.repository.FeedbackDetailRepository; +import com.slatto.domain.feedback.repository.FeedbackRepository; +import com.slatto.domain.notification.service.ActivityLogService; +import com.slatto.domain.project.entity.Project; +import com.slatto.domain.project.repository.ProjectMemberRepository; +import com.slatto.domain.sharelink.entity.Guest; +import com.slatto.domain.sharelink.entity.ShareLink; +import com.slatto.domain.sharelink.repository.GuestRepository; +import com.slatto.domain.user.entity.Users; +import com.slatto.domain.user.repository.UserRepository; +import com.slatto.domain.video.entity.Video; +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.ObjectProvider; + +import java.util.Optional; +import java.util.stream.Stream; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class FeedbackActivityLogConnectionTest { + + @Mock private FeedbackRepository feedbackRepository; + @Mock private FeedbackDetailRepository feedbackDetailRepository; + @Mock private UserRepository userRepository; + @Mock private GuestRepository guestRepository; + @Mock private FeedbackConverter feedbackConverter; + @Mock private FeedbackDetailConverter feedbackDetailConverter; + @Mock private ObjectProvider entityManagerProvider; + @Mock private EntityManager entityManager; + @Mock private TypedQuery