@@ -224,7 +224,7 @@ public PlanResponse updatePlan(Long planId, User user, PlanUpdateRequest request
224224 */
225225 @ Transactional
226226 public PlanDeleteResponse deletePlan (Long planId , User user ) {
227- Plan plan = findPlanWithEditPermissionCheck (planId , user );
227+ Plan plan = findPlanWithOwnerCheck (planId , user );
228228 Long deletedPlanId = plan .getPlanId ();
229229
230230 // Plan 삭제 시 cascade 설정으로 인해 participants와 schedules도 함께 삭제.
@@ -460,6 +460,7 @@ public ScheduleDeleteResponse deleteSchedule(Long planId, Long scheduleId, User
460460 .build ();
461461 }
462462
463+
463464 /**
464465 * Plan을 조회하고 참가자 권한을 체크하는 메서드
465466 *
@@ -491,6 +492,28 @@ private Plan findPlanWithParticipantCheck(Long planId, User user) {
491492 return plan ;
492493 }
493494
495+ /**
496+ * Plan을 조회하고 소유자 권한을 체크
497+ * 소유자만 삭제 가능
498+ *
499+ * @param planId 계획 ID
500+ * @param user 현재 로그인한 사용자
501+ * @return Plan 엔티티
502+ * @throws BusinessException 계획을 찾을 수 없거나 소유자가 아닌 경우
503+ */
504+ private Plan findPlanWithOwnerCheck (Long planId , User user ) {
505+ Long userId = user .getId ();
506+ Plan plan = planRepository .findById (planId )
507+ .orElseThrow (() -> new BusinessException (PlanErrorCode .PLAN_NOT_FOUND ));
508+
509+ // 소유자만 삭제 가능
510+ if (!plan .getUserId ().equals (userId )) {
511+ throw new BusinessException (PlanErrorCode .PLAN_UNAUTHORIZED );
512+ }
513+
514+ return plan ;
515+ }
516+
494517 /**
495518 * Plan을 조회하고 수정/삭제 권한을 체크
496519 * OWNER 또는 EDITOR만 수정/삭제 가능
@@ -667,7 +690,95 @@ public void updateParticipantRole(Long planId, Long participantId, User user,
667690 participant .updateRole (request .getRole ());
668691 }
669692
670- // 계획 공유 초대
693+ /**
694+ * 공유 링크 생성 (UUID)
695+ *
696+ * @param planId 계획 ID
697+ * @param user 현재 로그인한 사용자 (권한 체크용)
698+ * @return 공유 링크 응답 DTO
699+ * @throws BusinessException 계획을 찾을 수 없거나 권한이 없는 경우
700+ */
701+ @ Transactional
702+ public PlanShareLinkResponse generateShareLink (Long planId , User user ) {
703+ // 권한 체크 (수정 권한 확인: OWNER 또는 EDITOR)
704+ Plan plan = findPlanWithEditPermissionCheck (planId , user );
705+
706+ // shareToken이 이미 있으면 재사용, 없으면 생성
707+ if (plan .getShareToken () == null ) {
708+ plan .generateShareToken ();
709+ planRepository .save (plan );
710+ }
711+
712+ return PlanShareLinkResponse .builder ()
713+ .planId (plan .getPlanId ())
714+ .shareToken (plan .getShareToken ())
715+ .shareLink ("/plans/share/" + plan .getShareToken ())
716+ .build ();
717+ }
718+
719+ /**
720+ * 공유 링크로 플랜 참가
721+ *
722+ * @param shareToken 공유 토큰 (UUID)
723+ * @param user 현재 로그인한 사용자
724+ * @return 플랜 상세 정보
725+ * @throws BusinessException 공유 링크가 유효하지 않은 경우, 이미 참가자인 경우
726+ */
727+ @ Transactional
728+ public PlanDetailResponse joinPlanByShareToken (String shareToken , User user ) {
729+ // shareToken으로 Plan 찾기
730+ Plan plan = planRepository .findByShareToken (shareToken )
731+ .orElseThrow (() -> new BusinessException (PlanErrorCode .INVALID_SHARE_TOKEN ));
732+
733+ // 자기 자신의 플랜은 참가할 수 없음
734+ if (plan .getUserId ().equals (user .getId ())) {
735+ throw new BusinessException (PlanErrorCode .USER_ALREADY_PARTICIPANT );
736+ }
737+
738+ // DB 레벨에서 이미 참가자인지 확인 (유니크 제약조건 검증)
739+ boolean isAlreadyParticipant = planParticipantRepository .existsByUser_IdAndPlan_PlanId (
740+ user .getId (), plan .getPlanId ());
741+
742+ if (isAlreadyParticipant ) {
743+ // 이미 참가자인 경우 상태를 ACCEPTED로 변경
744+ PlanParticipant participant = planParticipantRepository
745+ .findByUser_IdAndPlan_PlanId (user .getId (), plan .getPlanId ())
746+ .orElseThrow (() -> new BusinessException (PlanErrorCode .PLAN_NOT_FOUND ));
747+
748+ participant .updateInviteStatus (PlanParticipant .InviteStatus .ACCEPTED );
749+ planParticipantRepository .save (participant );
750+ } else {
751+ // 새로운 참가자 추가 (기본 역할은 VIEWER, 상태는 ACCEPTED)
752+ PlanParticipant participant = PlanParticipant .builder ()
753+ .user (user )
754+ .plan (plan )
755+ .inviteStatus (PlanParticipant .InviteStatus .ACCEPTED )
756+ .role (PlanParticipant .ParticipantRole .VIEWER )
757+ .build ();
758+
759+ plan .addParticipant (participant );
760+ planRepository .save (plan );
761+ }
762+
763+ return getPlanDetail (plan .getPlanId (), user );
764+ }
765+
766+ /**
767+ * 공유 링크 삭제 (shareToken 제거)
768+ *
769+ * @param planId 계획 ID
770+ * @param user 현재 로그인한 사용자 (권한 체크용)
771+ * @throws BusinessException 계획을 찾을 수 없거나 권한이 없는 경우
772+ */
773+ @ Transactional
774+ public void deleteShareLink (Long planId , User user ) {
775+ // 권한 체크 (수정 권한 확인: OWNER 또는 EDITOR)
776+ Plan plan = findPlanWithEditPermissionCheck (planId , user );
777+
778+ plan .clearShareToken ();
779+ planRepository .save (plan );
780+ }
781+
671782 // 계획 공유 수락
672783 // 계획 공유 거절
673784 // 계획 공유 인원 추방
0 commit comments