@@ -820,7 +820,142 @@ public void deleteShareLink(Long planId, User user) {
820820 planRepository .save (plan );
821821 }
822822
823- // 계획 공유 거절
824- // 계획 공유 인원 추방
825- // 계획 공유 나가기
823+ /**
824+ * 초대 거절
825+ * 공유 링크를 통해 받은 초대를 거절합니다.
826+ *
827+ * @param shareToken 공유 토큰
828+ * @param user 현재 로그인한 사용자
829+ * @throws BusinessException 공유 링크가 유효하지 않거나 참가자가 아닌 경우
830+ */
831+ @ Transactional
832+ public void declinePlanInvitation (String shareToken , User user ) {
833+ // shareToken으로 Plan 찾기
834+ Plan plan = planRepository .findByShareToken (shareToken )
835+ .orElseThrow (() -> new BusinessException (PlanErrorCode .INVALID_SHARE_TOKEN ));
836+
837+ // 만료 시간 검증
838+ if (plan .isShareTokenExpired ()) {
839+ throw new BusinessException (PlanErrorCode .SHARE_TOKEN_EXPIRED );
840+ }
841+
842+ // 자기 자신의 플랜은 거절할 수 없음
843+ if (plan .getUserId ().equals (user .getId ())) {
844+ throw new BusinessException (PlanErrorCode .USER_ALREADY_PARTICIPANT );
845+ }
846+
847+ // 참가자 조회
848+ PlanParticipant participant = planParticipantRepository
849+ .findByUser_IdAndPlan_PlanId (user .getId (), plan .getPlanId ())
850+ .orElseThrow (() -> new BusinessException (PlanErrorCode .PARTICIPANT_NOT_FOUND ));
851+
852+ // 상태를 DECLINED로 변경
853+ participant .updateInviteStatus (PlanParticipant .InviteStatus .DECLINED );
854+ planParticipantRepository .save (participant );
855+ }
856+
857+ /**
858+ * 참가자 강퇴
859+ * OWNER 또는 EDITOR 권한이 있는 사용자가 다른 참가자를 강퇴합니다.
860+ *
861+ * @param planId 계획 ID
862+ * @param participantId 참가자 ID
863+ * @param user 현재 로그인한 사용자 (권한 체크용)
864+ * @throws BusinessException 계획을 찾을 수 없거나 권한이 없거나 소유자는 강퇴할 수 없는 경우
865+ */
866+ @ Transactional
867+ public void kickParticipant (Long planId , Long participantId , User user ) {
868+ // 권한 체크 (수정 권한 확인: OWNER 또는 EDITOR)
869+ Plan plan = findPlanWithEditPermissionCheck (planId , user );
870+
871+ // 참가자 조회
872+ PlanParticipant participant = planParticipantRepository .findById (participantId )
873+ .orElseThrow (() -> new BusinessException (PlanErrorCode .PARTICIPANT_NOT_FOUND ));
874+
875+ // 참가자가 해당 Plan에 속해있는지 확인
876+ if (!participant .getPlan ().getPlanId ().equals (planId )) {
877+ throw new BusinessException (PlanErrorCode .PARTICIPANT_NOT_FOUND );
878+ }
879+
880+ // 소유자는 강퇴할 수 없음
881+ if (participant .getRole () == PlanParticipant .ParticipantRole .OWNER ||
882+ plan .getUserId ().equals (participant .getUserId ())) {
883+ throw new BusinessException (PlanErrorCode .CANNOT_REMOVE_OWNER );
884+ }
885+
886+ // 상태를 REMOVED로 변경
887+ participant .updateInviteStatus (PlanParticipant .InviteStatus .REMOVED );
888+ planParticipantRepository .save (participant );
889+ }
890+
891+ /**
892+ * 자진 나가기
893+ * 참가자가 계획에서 스스로 나갑니다.
894+ *
895+ * @param planId 계획 ID
896+ * @param user 현재 로그인한 사용자
897+ * @throws BusinessException 계획을 찾을 수 없거나 소유자는 나갈 수 없는 경우
898+ */
899+ @ Transactional
900+ public void quitPlan (Long planId , User user ) {
901+ Plan plan = planRepository .findById (planId )
902+ .orElseThrow (() -> new BusinessException (PlanErrorCode .PLAN_NOT_FOUND ));
903+
904+ // 소유자는 나갈 수 없음
905+ if (plan .getUserId ().equals (user .getId ())) {
906+ throw new BusinessException (PlanErrorCode .CANNOT_LEAVE_OWNER );
907+ }
908+
909+ // 참가자 조회
910+ PlanParticipant participant = planParticipantRepository
911+ .findByUser_IdAndPlan_PlanId (user .getId (), planId )
912+ .orElseThrow (() -> new BusinessException (PlanErrorCode .PARTICIPANT_NOT_FOUND ));
913+
914+ // 상태를 LEFT로 변경
915+ participant .updateInviteStatus (PlanParticipant .InviteStatus .LEFT );
916+ planParticipantRepository .save (participant );
917+ }
918+
919+ /**
920+ * 참가자 목록 조회
921+ * 계획의 참가자 목록을 조회합니다. 상태별 필터링이 가능합니다.
922+ *
923+ * @param planId 계획 ID
924+ * @param user 현재 로그인한 사용자 (권한 체크용)
925+ * @param inviteStatus 필터링할 초대 상태 (선택적, null이면 모든 상태)
926+ * @return 참가자 목록
927+ * @throws BusinessException 계획을 찾을 수 없거나 권한이 없는 경우
928+ */
929+ public PlanParticipantListResponse getParticipantList (Long planId , User user , PlanParticipant .InviteStatus inviteStatus ) {
930+ // 권한 체크 (참가자 여부 확인)
931+ findPlanWithParticipantCheck (planId , user );
932+
933+ // 참가자 목록 조회 (User 정보 포함)
934+ List <PlanParticipant > participants = planParticipantRepository .findByPlan_PlanId (planId );
935+
936+ // 상태별 필터링
937+ if (inviteStatus != null ) {
938+ participants = participants .stream ()
939+ .filter (p -> p .getInviteStatus () == inviteStatus )
940+ .collect (Collectors .toList ());
941+ }
942+
943+ // DTO 변환
944+ List <PlanParticipantListResponse .ParticipantDetailInfo > participantInfos = participants .stream ()
945+ .map (participant -> PlanParticipantListResponse .ParticipantDetailInfo .builder ()
946+ .participantId (participant .getParticipantId ())
947+ .userId (participant .getUserId ())
948+ .nickname (participant .getUser ().getNickname ())
949+ .email (participant .getUser ().getEmail ())
950+ .profileImage (participant .getUser ().getProfileImage ())
951+ .inviteStatus (participant .getInviteStatus ())
952+ .role (participant .getRole ())
953+ .build ())
954+ .collect (Collectors .toList ());
955+
956+ return PlanParticipantListResponse .builder ()
957+ .planId (planId )
958+ .participants (participantInfos )
959+ .build ();
960+ }
826961}
0 commit comments