Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ public class FeedbackController {
public ResponseEntity<ApiResponse<FeedbackCreateResDTO>> createFeedback(
@PathVariable Long videoId,
@AuthenticationPrincipal Long userId,
@RequestHeader(value = "X-Guest-Token", required = false) String guestToken,
@Valid @RequestBody FeedbackCreateReqDTO request
) {
FeedbackCreateResDTO result = feedbackService.createFeedback(videoId, userId, request);

FeedbackCreateResDTO result = feedbackService.createFeedback(videoId, userId, guestToken, request);
return ResponseEntity
.status(HttpStatus.CREATED)
.body(ApiResponse.success(CommonSuccessCode.CREATED, result));
Expand All @@ -46,10 +46,10 @@ public ResponseEntity<ApiResponse<FeedbackCreateResDTO>> createFeedback(
public ResponseEntity<ApiResponse<FeedbackUpdateResDTO>> updateFeedback(
@PathVariable Long feedbackId,
@AuthenticationPrincipal Long userId,
@RequestHeader(value = "X-Guest-Token", required = false) String guestToken,
@Valid @RequestBody FeedbackUpdateReqDTO request
) {
FeedbackUpdateResDTO result = feedbackService.updateFeedback(feedbackId, userId, request);

FeedbackUpdateResDTO result = feedbackService.updateFeedback(feedbackId, userId, guestToken, request);
return ResponseEntity
.ok(ApiResponse.success(CommonSuccessCode.OK, result));
}
Expand All @@ -59,10 +59,10 @@ public ResponseEntity<ApiResponse<FeedbackUpdateResDTO>> updateFeedback(
public ResponseEntity<ApiResponse<Void>> deleteFeedback(
@PathVariable Long feedbackId,
@AuthenticationPrincipal Long userId,
@RequestParam(required = false) Long guestId
@RequestParam(required = false) Long guestId,
@RequestHeader(value = "X-Guest-Token", required = false) String guestToken
) {
feedbackService.deleteFeedback(feedbackId, userId, guestId);

feedbackService.deleteFeedback(feedbackId, userId, guestId, guestToken);
return ResponseEntity
.ok(ApiResponse.success(CommonSuccessCode.OK, null));
}
Expand All @@ -73,11 +73,11 @@ public ResponseEntity<ApiResponse<FeedbackListResDTO>> getFeedbackList(
@PathVariable Long videoId,
@AuthenticationPrincipal Long userId,
@RequestParam(required = false) Long guestId,
@RequestHeader(value = "X-Guest-Token", required = false) String guestToken,
@RequestParam(required = false) String cursor,
@RequestParam(required = false) Integer size
) {
FeedbackListResDTO result = feedbackService.getFeedbackList(videoId, userId, guestId, cursor, size);

FeedbackListResDTO result = feedbackService.getFeedbackList(videoId, userId, guestId, guestToken, cursor, size);
return ResponseEntity
.ok(ApiResponse.success(CommonSuccessCode.OK, result));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ public class FeedbackDetailController {
public ResponseEntity<ApiResponse<ReplyCreateResDTO>> createReply(
@PathVariable Long feedbackId,
@AuthenticationPrincipal Long userId,
@RequestHeader(value = "X-Guest-Token", required = false) String guestToken,
@Valid @RequestBody ReplyCreateReqDTO request
) {
ReplyCreateResDTO result = feedbackDetailService.createReply(feedbackId, userId, request);
ReplyCreateResDTO result = feedbackDetailService.createReply(feedbackId, userId, guestToken, request);

return ResponseEntity
.status(HttpStatus.CREATED)
Expand All @@ -47,10 +48,11 @@ public ResponseEntity<ApiResponse<ReplyListResDTO>> getReplyList(
@PathVariable Long feedbackId,
@AuthenticationPrincipal Long userId,
@RequestParam(required = false) Long guestId,
@RequestHeader(value = "X-Guest-Token", required = false) String guestToken,
@RequestParam(required = false) Long cursor,
@RequestParam(required = false) Integer size
) {
ReplyListResDTO result = feedbackDetailService.getReplyList(feedbackId, userId, guestId, cursor, size);
ReplyListResDTO result = feedbackDetailService.getReplyList(feedbackId, userId, guestId, guestToken, cursor, size);

return ResponseEntity
.ok(ApiResponse.success(CommonSuccessCode.OK, result));
Expand All @@ -61,9 +63,10 @@ public ResponseEntity<ApiResponse<ReplyListResDTO>> getReplyList(
public ResponseEntity<ApiResponse<ReplyUpdateResDTO>> updateReply(
@PathVariable Long replyId,
@AuthenticationPrincipal Long userId,
@RequestHeader(value = "X-Guest-Token", required = false) String guestToken,
@Valid @RequestBody ReplyUpdateReqDTO request
) {
ReplyUpdateResDTO result = feedbackDetailService.updateReply(replyId, userId, request);
ReplyUpdateResDTO result = feedbackDetailService.updateReply(replyId, userId, guestToken, request);

return ResponseEntity
.ok(ApiResponse.success(CommonSuccessCode.OK, result));
Expand All @@ -74,9 +77,10 @@ public ResponseEntity<ApiResponse<ReplyUpdateResDTO>> updateReply(
public ResponseEntity<ApiResponse<Void>> deleteReply(
@PathVariable Long replyId,
@AuthenticationPrincipal Long userId,
@RequestParam(required = false) Long guestId
@RequestParam(required = false) Long guestId,
@RequestHeader(value = "X-Guest-Token", required = false) String guestToken
) {
feedbackDetailService.deleteReply(replyId, userId, guestId);
feedbackDetailService.deleteReply(replyId, userId, guestId, guestToken);

return ResponseEntity
.ok(ApiResponse.success(CommonSuccessCode.OK, null));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ public class FeedbackDetailService {
private final ActivityLogService activityLogService;

private static final int DEFAULT_PAGE_SIZE = 10;
private static final int MAX_PAGE_SIZE = 50; // 페이지 크기 상한
private static final int MAX_PAGE_SIZE = 50;

@Transactional
public ReplyCreateResDTO createReply(Long feedbackId, Long userId, ReplyCreateReqDTO req) {
public ReplyCreateResDTO createReply(Long feedbackId, Long userId, String guestToken, ReplyCreateReqDTO req) {

// 1. 원 피드백 조회 (삭제된 건 제외)
Feedback feedback = feedbackRepository.findById(feedbackId)
Expand All @@ -68,16 +68,15 @@ public ReplyCreateResDTO createReply(Long feedbackId, Long userId, ReplyCreateRe
// 회원이 원 피드백 영상의 프로젝트 멤버인지 검증
validateMemberAccess(userId, feedback.getVideo().getProject().getId());
} else {
// 게스트: 원 피드백의 영상에 접근할 자격이 있는지 검증 후 Guest 확보
guest = validateGuestAccess(req.guestId(), feedback.getVideo().getId());
// 게스트: 토큰 + 원 피드백 영상 소유 검증 후 Guest 확보
guest = validateGuestAccess(req.guestId(), feedback.getVideo().getId(), guestToken);
}

// 4. 저장
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);

Expand All @@ -102,12 +101,9 @@ public ReplyCreateResDTO createReply(Long feedbackId, Long userId, ReplyCreateRe
}

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

// 프로젝트 활성 멤버 전체를 수신자로 (작성자 제외는 actorUserId로 알림 도메인이 처리)
List<Long> recipientIds = projectMemberRepository
.findAllActiveMembersByProjectId(projectId)
.stream()
Expand All @@ -117,10 +113,10 @@ private void sendReplyNotification(Video video, Long actorUserId, String comment
notificationService.createVideoFeedbackCommentedNotifications(
projectId,
video.getId(),
video.getTitle(), // 영상명 → 알림 도메인이 문구 조합에 사용
commenterName, // 작성자명 → 알림 도메인이 문구 조합에 사용
video.getTitle(),
commenterName,
recipientIds,
actorUserId // 게스트면 null → 제외 대상 없음
actorUserId
);
}

Expand All @@ -133,7 +129,6 @@ private void validateWriter(Long userId, Long guestId) {
}

// 회원이 해당 프로젝트의 활성 멤버인지 검증
// 게스트의 validateGuestAccess와 대칭 — 회원은 프로젝트 멤버 자격으로 접근 인가
private void validateMemberAccess(Long userId, Long projectId) {
boolean isMember = projectMemberRepository
.existsByProjectIdAndUserIdAndLeftAtIsNull(projectId, userId);
Expand All @@ -143,11 +138,16 @@ private void validateMemberAccess(Long userId, Long projectId) {
}

// 게스트가 해당 영상에 접근할 자격이 있는지 검증하고, 검증된 Guest를 반환
// Guest → ShareLink → Video 체인으로 소유 여부 확인
private Guest validateGuestAccess(Long guestId, Long videoId) {
// 세션 토큰으로 본인 확인 + Guest → ShareLink → Video 소유 확인
private Guest validateGuestAccess(Long guestId, Long videoId, String guestToken) {
Guest guest = guestRepository.findById(guestId)
.orElseThrow(() -> new BaseException(CommonErrorCode.NOT_FOUND));

// 0. 세션 토큰으로 본인 확인 — 없거나 불일치면 사칭으로 간주해 차단
if (guestToken == null || !guestToken.equals(guest.getSessionToken())) {
throw new BaseException(ShareLinkErrorCode.GUEST_ACCESS_DENIED);
}

ShareLink shareLink = guest.getShareLink();

// 1. 링크가 살아있는지 (활성 + 미만료)
Expand All @@ -164,29 +164,29 @@ private Guest validateGuestAccess(Long guestId, Long videoId) {
}

@Transactional(readOnly = true)
public ReplyListResDTO getReplyList(Long feedbackId, Long userId, Long guestId, Long cursor, Integer size) {
public ReplyListResDTO getReplyList(Long feedbackId, Long userId, Long guestId, String guestToken, Long cursor, Integer size) {

// 1. 원 피드백 존재 확인
Feedback feedback = feedbackRepository.findById(feedbackId)
.filter(f -> f.getDeletedAt() == null)
.orElseThrow(() -> new BaseException(CommonErrorCode.NOT_FOUND));

// 2. 접근 검증 — 회원은 프로젝트 멤버, 게스트는 공유링크 소유
// 2. 접근 검증 — 회원은 프로젝트 멤버, 게스트는 토큰 + 공유링크 소유
// 회원도 게스트도 아니면(둘 다 null) 익명 조회 차단
if (userId != null) {
validateMemberAccess(userId, feedback.getVideo().getProject().getId());
} else {
if (guestId == null) {
throw new BaseException(ShareLinkErrorCode.GUEST_ACCESS_DENIED);
}
validateGuestAccess(guestId, feedback.getVideo().getId());
validateGuestAccess(guestId, feedback.getVideo().getId(), guestToken);
}

// 3. size 기본값 + 상한 처리
int pageSize = (size == null || size <= 0)
? DEFAULT_PAGE_SIZE
: Math.min(size, MAX_PAGE_SIZE);
Pageable pageable = PageRequest.of(0, pageSize + 1); // hasNext 판단용 +1
Pageable pageable = PageRequest.of(0, pageSize + 1);

// 4. 조회
List<FeedbackDetail> replies = (cursor == null)
Expand All @@ -208,7 +208,7 @@ public ReplyListResDTO getReplyList(Long feedbackId, Long userId, Long guestId,
}

@Transactional
public ReplyUpdateResDTO updateReply(Long replyId, Long userId, ReplyUpdateReqDTO req) {
public ReplyUpdateResDTO updateReply(Long replyId, Long userId, String guestToken, ReplyUpdateReqDTO req) {

// 1. 답글 조회 (삭제된 건 제외)
FeedbackDetail reply = feedbackDetailRepository.findById(replyId)
Expand All @@ -218,19 +218,19 @@ public ReplyUpdateResDTO updateReply(Long replyId, Long userId, ReplyUpdateReqDT
// 2. 작성자 검증
validateWriter(userId, req.guestId());

// 3. 접근 검증 — 회원은 프로젝트 멤버, 게스트는 공유링크 소유 (답글 → 피드백 → 영상)
// 3. 접근 검증 — 회원은 프로젝트 멤버, 게스트는 토큰 + 공유링크 소유 (답글 → 피드백 → 영상)
if (userId != null) {
validateMemberAccess(userId, reply.getFeedback().getVideo().getProject().getId());
} else {
validateGuestAccess(req.guestId(), reply.getFeedback().getVideo().getId());
validateGuestAccess(req.guestId(), reply.getFeedback().getVideo().getId(), guestToken);
}

// 4. 본인 확인
if (!reply.isWriter(userId, req.guestId())) {
throw new BaseException(CommonErrorCode.FORBIDDEN);
}

// 5. 수정 (더티 체킹)
// 5. 수정
reply.update(req.content());

// 6. updatedAt 갱신 반영
Expand All @@ -240,7 +240,7 @@ public ReplyUpdateResDTO updateReply(Long replyId, Long userId, ReplyUpdateReqDT
}

@Transactional
public void deleteReply(Long replyId, Long userId, Long guestId) {
public void deleteReply(Long replyId, Long userId, Long guestId, String guestToken) {

// 1. 답글 조회 (이미 삭제된 건 제외)
FeedbackDetail reply = feedbackDetailRepository.findById(replyId)
Expand All @@ -250,19 +250,19 @@ public void deleteReply(Long replyId, Long userId, Long guestId) {
// 2. 작성자 검증
validateWriter(userId, guestId);

// 3. 접근 검증 — 회원은 프로젝트 멤버, 게스트는 공유링크 소유 (답글 → 피드백 → 영상)
// 3. 접근 검증 — 회원은 프로젝트 멤버, 게스트는 토큰 + 공유링크 소유 (답글 → 피드백 → 영상)
if (userId != null) {
validateMemberAccess(userId, reply.getFeedback().getVideo().getProject().getId());
} else {
validateGuestAccess(guestId, reply.getFeedback().getVideo().getId());
validateGuestAccess(guestId, reply.getFeedback().getVideo().getId(), guestToken);
}

// 4. 본인 확인
if (!reply.isWriter(userId, guestId)) {
throw new BaseException(CommonErrorCode.FORBIDDEN);
}

// 5. soft delete (더티 체킹)
// 5. soft delete
reply.softDelete();
}

Expand All @@ -284,7 +284,7 @@ public ReplyStatusResDTO changeReplyStatus(Long replyId, Long userId, ReplyStatu
throw new BaseException(CommonErrorCode.FORBIDDEN);
}

// 3. 상태 변경 (더티 체킹)
// 3. 상태 변경
reply.changeStatus(req.status());

// 4. updatedAt 갱신 반영
Expand Down
Loading
Loading