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 1001439a..0aae1552 100644 --- a/src/main/java/com/slatto/domain/feedback/service/FeedbackDetailService.java +++ b/src/main/java/com/slatto/domain/feedback/service/FeedbackDetailService.java @@ -65,6 +65,8 @@ public ReplyCreateResDTO createReply(Long feedbackId, Long userId, ReplyCreateRe if (userId != null) { user = userRepository.findByIdAndDeletedAtIsNull(userId) .orElseThrow(() -> new BaseException(CommonErrorCode.NOT_FOUND)); + // 회원이 원 피드백 영상의 프로젝트 멤버인지 검증 + validateMemberAccess(userId, feedback.getVideo().getProject().getId()); } else { // 게스트: 원 피드백의 영상에 접근할 자격이 있는지 검증 후 Guest 확보 guest = validateGuestAccess(req.guestId(), feedback.getVideo().getId()); @@ -130,6 +132,16 @@ private void validateWriter(Long userId, Long guestId) { } } + // 회원이 해당 프로젝트의 활성 멤버인지 검증 + // 게스트의 validateGuestAccess와 대칭 — 회원은 프로젝트 멤버 자격으로 접근 인가 + private void validateMemberAccess(Long userId, Long projectId) { + boolean isMember = projectMemberRepository + .existsByProjectIdAndUserIdAndLeftAtIsNull(projectId, userId); + if (!isMember) { + throw new BaseException(CommonErrorCode.FORBIDDEN); + } + } + // 게스트가 해당 영상에 접근할 자격이 있는지 검증하고, 검증된 Guest를 반환 // Guest → ShareLink → Video 체인으로 소유 여부 확인 private Guest validateGuestAccess(Long guestId, Long videoId) { @@ -159,9 +171,11 @@ public ReplyListResDTO getReplyList(Long feedbackId, Long userId, Long guestId, .filter(f -> f.getDeletedAt() == null) .orElseThrow(() -> new BaseException(CommonErrorCode.NOT_FOUND)); - // 2. 게스트가 조회하는 경우 원 피드백의 영상에 접근 자격이 있는지 검증 - // 회원이 아니면 guestId 필수 — 익명(둘 다 null) 조회 차단 - if (userId == null) { + // 2. 접근 검증 — 회원은 프로젝트 멤버, 게스트는 공유링크 소유 + // 회원도 게스트도 아니면(둘 다 null) 익명 조회 차단 + if (userId != null) { + validateMemberAccess(userId, feedback.getVideo().getProject().getId()); + } else { if (guestId == null) { throw new BaseException(ShareLinkErrorCode.GUEST_ACCESS_DENIED); } @@ -204,8 +218,10 @@ public ReplyUpdateResDTO updateReply(Long replyId, Long userId, ReplyUpdateReqDT // 2. 작성자 검증 validateWriter(userId, req.guestId()); - // 3. 게스트면 이 답글의 영상에 접근 자격이 있는지 검증 (답글 → 피드백 → 영상) - if (userId == null) { + // 3. 접근 검증 — 회원은 프로젝트 멤버, 게스트는 공유링크 소유 (답글 → 피드백 → 영상) + if (userId != null) { + validateMemberAccess(userId, reply.getFeedback().getVideo().getProject().getId()); + } else { validateGuestAccess(req.guestId(), reply.getFeedback().getVideo().getId()); } @@ -234,8 +250,10 @@ public void deleteReply(Long replyId, Long userId, Long guestId) { // 2. 작성자 검증 validateWriter(userId, guestId); - // 3. 게스트면 이 답글의 영상에 접근 자격이 있는지 검증 (답글 → 피드백 → 영상) - if (userId == null) { + // 3. 접근 검증 — 회원은 프로젝트 멤버, 게스트는 공유링크 소유 (답글 → 피드백 → 영상) + if (userId != null) { + validateMemberAccess(userId, reply.getFeedback().getVideo().getProject().getId()); + } else { validateGuestAccess(guestId, reply.getFeedback().getVideo().getId()); } 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 f09e8639..b9bc572e 100644 --- a/src/main/java/com/slatto/domain/feedback/service/FeedbackService.java +++ b/src/main/java/com/slatto/domain/feedback/service/FeedbackService.java @@ -75,6 +75,8 @@ public FeedbackCreateResDTO createFeedback(Long videoId, Long userId, FeedbackCr if (userId != null) { user = userRepository.findByIdAndDeletedAtIsNull(userId) .orElseThrow(() -> new BaseException(CommonErrorCode.NOT_FOUND)); + // 회원이 이 영상의 프로젝트 멤버인지 검증 + validateMemberAccess(userId, video.getProject().getId()); } else { // 게스트: 이 영상에 접근할 자격이 있는지 검증 후 Guest 확보 guest = validateGuestAccess(req.guestId(), videoId); @@ -142,8 +144,10 @@ public FeedbackUpdateResDTO updateFeedback(Long feedbackId, Long userId, Feedbac // 2. 작성자 검증 validateWriter(userId, req.guestId()); - // 3. 게스트면 이 피드백의 영상에 접근 자격이 있는지 검증 - if (userId == null) { + // 3. 접근 검증 — 회원은 프로젝트 멤버, 게스트는 공유링크 소유 + if (userId != null) { + validateMemberAccess(userId, feedback.getVideo().getProject().getId()); + } else { validateGuestAccess(req.guestId(), feedback.getVideo().getId()); } @@ -170,6 +174,16 @@ private void validateWriter(Long userId, Long guestId) { } } + // 회원이 해당 프로젝트의 활성 멤버인지 검증 + // 게스트의 validateGuestAccess와 대칭 — 회원은 프로젝트 멤버 자격으로 접근 인가 + private void validateMemberAccess(Long userId, Long projectId) { + boolean isMember = projectMemberRepository + .existsByProjectIdAndUserIdAndLeftAtIsNull(projectId, userId); + if (!isMember) { + throw new BaseException(CommonErrorCode.FORBIDDEN); + } + } + // 게스트가 해당 영상에 접근할 자격이 있는지 검증하고, 검증된 Guest를 반환 // Guest → ShareLink → Video 체인으로 소유 여부 확인 private Guest validateGuestAccess(Long guestId, Long videoId) { @@ -202,8 +216,10 @@ public void deleteFeedback(Long feedbackId, Long userId, Long guestId) { // 2. 작성자 검증 validateWriter(userId, guestId); - // 3. 게스트면 이 피드백의 영상에 접근 자격이 있는지 검증 - if (userId == null) { + // 3. 접근 검증 — 회원은 프로젝트 멤버, 게스트는 공유링크 소유 + if (userId != null) { + validateMemberAccess(userId, feedback.getVideo().getProject().getId()); + } else { validateGuestAccess(guestId, feedback.getVideo().getId()); } @@ -230,9 +246,16 @@ select count(v) from Video v where v.id = :videoId throw new BaseException(CommonErrorCode.NOT_FOUND); } - // 2. 게스트가 조회하는 경우 이 영상에 접근 자격이 있는지 검증 - // 회원이 아니면 guestId 필수 — 익명(둘 다 null) 조회 차단 - if (userId == null) { + // 2. 접근 검증 — 회원은 프로젝트 멤버, 게스트는 공유링크 소유 + // 회원도 게스트도 아니면(둘 다 null) 익명 조회 차단 + if (userId != null) { + Long projectId = entityManagerProvider.getObject().createQuery(""" + select v.project.id from Video v where v.id = :videoId + """, Long.class) + .setParameter("videoId", videoId) + .getSingleResult(); + validateMemberAccess(userId, projectId); + } else { if (guestId == null) { throw new BaseException(ShareLinkErrorCode.GUEST_ACCESS_DENIED); } diff --git a/src/test/java/com/slatto/domain/feedback/service/FeedbackActivityLogConnectionTest.java b/src/test/java/com/slatto/domain/feedback/service/FeedbackActivityLogConnectionTest.java index cc072da1..aa1035ce 100644 --- a/src/test/java/com/slatto/domain/feedback/service/FeedbackActivityLogConnectionTest.java +++ b/src/test/java/com/slatto/domain/feedback/service/FeedbackActivityLogConnectionTest.java @@ -89,6 +89,8 @@ void setUp() { stubVideoLookup(video); given(userRepository.findByIdAndDeletedAtIsNull(1L)).willReturn(Optional.of(user)); + // 정상 회원은 프로젝트 멤버 — 멤버 검증 통과하도록 stub + given(projectMemberRepository.existsByProjectIdAndUserIdAndLeftAtIsNull(101L, 1L)).willReturn(true); given(feedbackConverter.toFeedback(eq(video), eq(user), eq(null), any())).willReturn(feedback); given(feedbackRepository.save(feedback)).willReturn(feedback); @@ -130,6 +132,8 @@ void setUp() { given(feedback.getDeletedAt()).willReturn(null); given(feedback.getVideo()).willReturn(video); given(userRepository.findByIdAndDeletedAtIsNull(1L)).willReturn(Optional.of(user)); + // 정상 회원은 프로젝트 멤버 — 멤버 검증 통과하도록 stub + given(projectMemberRepository.existsByProjectIdAndUserIdAndLeftAtIsNull(101L, 1L)).willReturn(true); given(feedbackDetailConverter.toFeedbackDetail(eq(feedback), eq(user), eq(null), any())).willReturn(reply); given(feedbackDetailRepository.save(reply)).willReturn(reply); @@ -184,4 +188,4 @@ private Users user(Long userId) { given(user.getId()).willReturn(userId); return user; } -} +} \ No newline at end of file