diff --git a/src/main/java/com/teamEWSN/gitdeun/common/exception/ErrorCode.java b/src/main/java/com/teamEWSN/gitdeun/common/exception/ErrorCode.java index 62cee37..7a7feff 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/exception/ErrorCode.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/exception/ErrorCode.java @@ -43,7 +43,15 @@ public enum ErrorCode { // 멤버 관련 MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER-001", "해당 멤버를 찾을 수 없습니다."), + MEMBER_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER-002", "이미 마인드맵에 등록된 멤버입니다."), + // 초대 관련 + INVITATION_NOT_FOUND(HttpStatus.NOT_FOUND, "INVITE-001", "초대 정보를 찾을 수 없습니다."), + INVITATION_EXPIRED(HttpStatus.BAD_REQUEST, "INVITE-002", "만료된 초대입니다."), + INVITATION_ALREADY_EXISTS(HttpStatus.CONFLICT, "INVITE-003", "이미 초대 대기 중인 사용자입니다."), + CANNOT_INVITE_SELF(HttpStatus.BAD_REQUEST, "INVITE-004", "자기 자신을 초대할 수 없습니다."), + INVITATION_REJECTED_USER(HttpStatus.FORBIDDEN, "INVITE-005", "초대를 거절한 사용자이므로 초대할 수 없습니다."), + // 방문기록 관련 HISTORY_NOT_FOUND(HttpStatus.NOT_FOUND, "VISITHISTORY-001", "방문 기록을 찾을 수 없습니다."), diff --git a/src/main/java/com/teamEWSN/gitdeun/invitation/controller/InvitationController.java b/src/main/java/com/teamEWSN/gitdeun/invitation/controller/InvitationController.java index 74f7854..7142cfd 100644 --- a/src/main/java/com/teamEWSN/gitdeun/invitation/controller/InvitationController.java +++ b/src/main/java/com/teamEWSN/gitdeun/invitation/controller/InvitationController.java @@ -1,5 +1,102 @@ package com.teamEWSN.gitdeun.invitation.controller; +import com.teamEWSN.gitdeun.common.jwt.CustomUserDetails; +import com.teamEWSN.gitdeun.invitation.dto.InvitationActionResponseDto; +import com.teamEWSN.gitdeun.invitation.dto.InvitationResponseDto; +import com.teamEWSN.gitdeun.invitation.dto.InviteRequestDto; +import com.teamEWSN.gitdeun.invitation.dto.LinkResponseDto; +import com.teamEWSN.gitdeun.invitation.service.InvitationService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/invitations") public class InvitationController { - + private final InvitationService invitationService; + + // 이메일로 멤버 초대 + @PostMapping("/mindmaps/{mapId}") + public ResponseEntity inviteByEmail( + @PathVariable Long mapId, + @Valid @RequestBody InviteRequestDto requestDto, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + invitationService.inviteUserByEmail(mapId, requestDto, userDetails.getId()); + return ResponseEntity.noContent().build(); + } + + // 특정 마인드맵의 전체 초대 목록 조회 (페이지네이션 적용) + @GetMapping("/mindmaps/{mapId}") + public ResponseEntity> getInvitations( + @PathVariable Long mapId, + @AuthenticationPrincipal CustomUserDetails userDetails, + @PageableDefault(size = 10, sort = "createdAt,desc") Pageable pageable + ) { + return ResponseEntity.ok(invitationService.getInvitationsByMindmap(mapId, userDetails.getId(), pageable)); + } + + // 초대 수락 + @PostMapping("/{invitationId}/accept") + public ResponseEntity acceptInvitation( + @PathVariable Long invitationId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + invitationService.acceptInvitation(invitationId, userDetails.getId()); + return ResponseEntity.noContent().build(); + } + + // 초대 거절 + @PostMapping("/{invitationId}/reject") + public ResponseEntity rejectInvitation( + @PathVariable Long invitationId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + invitationService.rejectInvitation(invitationId, userDetails.getId()); + return ResponseEntity.noContent().build(); + } + + + // 초대 링크 생성 + @PostMapping("/mindmaps/{mapId}/link") + public ResponseEntity createInvitationLink( + @PathVariable Long mapId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + return ResponseEntity.ok(invitationService.createInvitationLink(mapId, userDetails.getId())); + } + + // 초대 링크를 통해 수락 (로그인한 사용자가 링크 클릭 시) + @PostMapping("/link/{token}/accept") + public ResponseEntity acceptInvitationByLink( + @PathVariable String token, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + invitationService.acceptInvitationByLink(token, userDetails.getId()); + return ResponseEntity.noContent().build(); + } + + // Owner가 링크 초대 승인 + @PostMapping("/{invitationId}/approve") + public ResponseEntity approveLinkInvitation( + @PathVariable Long invitationId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + return ResponseEntity.ok(invitationService.approveLinkInvitation(invitationId, userDetails.getId())); + } + // Owner가 링크 초대 거부 + @PostMapping("/{invitationId}/reject-link") + public ResponseEntity rejectLinkApproval( + @PathVariable Long invitationId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + return ResponseEntity.ok(invitationService.rejectLinkApproval(invitationId, userDetails.getId())); + } } \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/invitation/dto/InvitationActionResponseDto.java b/src/main/java/com/teamEWSN/gitdeun/invitation/dto/InvitationActionResponseDto.java new file mode 100644 index 0000000..4cf77c3 --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/invitation/dto/InvitationActionResponseDto.java @@ -0,0 +1,10 @@ +package com.teamEWSN.gitdeun.invitation.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class InvitationActionResponseDto { + private String message; +} \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/invitation/dto/InvitationDto.java b/src/main/java/com/teamEWSN/gitdeun/invitation/dto/InvitationDto.java deleted file mode 100644 index c3c3a3f..0000000 --- a/src/main/java/com/teamEWSN/gitdeun/invitation/dto/InvitationDto.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.teamEWSN.gitdeun.invitation.dto; - -public class InvitationDto { - -} \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/invitation/dto/InvitationResponseDto.java b/src/main/java/com/teamEWSN/gitdeun/invitation/dto/InvitationResponseDto.java new file mode 100644 index 0000000..1568d5c --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/invitation/dto/InvitationResponseDto.java @@ -0,0 +1,21 @@ +package com.teamEWSN.gitdeun.invitation.dto; + +import com.teamEWSN.gitdeun.invitation.entity.InvitationStatus; +import com.teamEWSN.gitdeun.mindmapmember.entity.MindmapRole; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +public class InvitationResponseDto { + + private Long invitationId; + private String mindmapName; + private String inviteeName; + private String inviteeEmail; + private MindmapRole role; + private InvitationStatus status; + private LocalDateTime createdAt; +} \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/invitation/dto/InviteRequestDto.java b/src/main/java/com/teamEWSN/gitdeun/invitation/dto/InviteRequestDto.java new file mode 100644 index 0000000..1f5c190 --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/invitation/dto/InviteRequestDto.java @@ -0,0 +1,20 @@ +package com.teamEWSN.gitdeun.invitation.dto; + +import com.teamEWSN.gitdeun.mindmapmember.entity.MindmapRole; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class InviteRequestDto { + @Email(message = "올바른 이메일 형식이 아닙니다.") + @NotNull(message = "이메일을 입력해주세요.") + private String email; + + @NotNull(message = "권한을 선택해주세요.") + private MindmapRole role; +} \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/invitation/dto/LinkResponseDto.java b/src/main/java/com/teamEWSN/gitdeun/invitation/dto/LinkResponseDto.java new file mode 100644 index 0000000..2b6adee --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/invitation/dto/LinkResponseDto.java @@ -0,0 +1,10 @@ +package com.teamEWSN.gitdeun.invitation.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class LinkResponseDto { + private String invitationLink; +} diff --git a/src/main/java/com/teamEWSN/gitdeun/invitation/entity/Invitation.java b/src/main/java/com/teamEWSN/gitdeun/invitation/entity/Invitation.java index 40cceac..788c5b1 100644 --- a/src/main/java/com/teamEWSN/gitdeun/invitation/entity/Invitation.java +++ b/src/main/java/com/teamEWSN/gitdeun/invitation/entity/Invitation.java @@ -1,47 +1,61 @@ package com.teamEWSN.gitdeun.invitation.entity; +import com.teamEWSN.gitdeun.common.util.CreatedEntity; import com.teamEWSN.gitdeun.mindmap.entity.Mindmap; +import com.teamEWSN.gitdeun.mindmapmember.entity.MindmapRole; import com.teamEWSN.gitdeun.user.entity.User; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.hibernate.annotations.ColumnDefault; -import org.hibernate.annotations.CreationTimestamp; +import lombok.*; import java.time.LocalDateTime; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) // 빌더만 사용하도록 강제 +@Builder(toBuilder = true) @Table(name = "invitation") -public class Invitation { +public class Invitation extends CreatedEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "mindmap_id", nullable = false) private Mindmap mindmap; + // 초대한 사람 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "inviter_id", nullable = false) + private User inviter; + + // 초대받은 사람 (이메일 초대의 경우) + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "invitee_id") + private User invitee; + @Column(length = 36, nullable = false, unique = true) private String token; @Enumerated(EnumType.STRING) @Column(nullable = false) - @ColumnDefault("'READ_ONLY'") + private MindmapRole role; // 초대 시 부여할 권한 + + @Enumerated(EnumType.STRING) + @Column(nullable = false) private InvitationStatus status; - @CreationTimestamp - @Column(name = "invited_at", updatable = false) - private LocalDateTime invitedAt; + @Column(name = "expires_at") + private LocalDateTime expiresAt; // 초대 만료 시간 + + + public Invitation accept() { + this.status = InvitationStatus.ACCEPTED; + return this; + } - @Column(name = "is_accept", nullable = false) - @ColumnDefault("false") - private boolean isAccept; + public void reject() { + this.status = InvitationStatus.REJECTED; + } } \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/invitation/entity/InvitationStatus.java b/src/main/java/com/teamEWSN/gitdeun/invitation/entity/InvitationStatus.java index 09101f3..baf90c5 100644 --- a/src/main/java/com/teamEWSN/gitdeun/invitation/entity/InvitationStatus.java +++ b/src/main/java/com/teamEWSN/gitdeun/invitation/entity/InvitationStatus.java @@ -1,6 +1,7 @@ package com.teamEWSN.gitdeun.invitation.entity; public enum InvitationStatus { - READ_ONLY, - EDIT_ALLOWED + PENDING, // 초대 승인 대기중 + ACCEPTED, // 수락 + REJECTED // 거절 } diff --git a/src/main/java/com/teamEWSN/gitdeun/invitation/mapper/InvitationMapper.java b/src/main/java/com/teamEWSN/gitdeun/invitation/mapper/InvitationMapper.java new file mode 100644 index 0000000..405974e --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/invitation/mapper/InvitationMapper.java @@ -0,0 +1,17 @@ +package com.teamEWSN.gitdeun.invitation.mapper; + +import com.teamEWSN.gitdeun.invitation.dto.InvitationResponseDto; +import com.teamEWSN.gitdeun.invitation.entity.Invitation; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; + +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface InvitationMapper { + + @Mapping(source = "id", target = "invitationId") + @Mapping(source = "mindmap.field", target = "mindmapName") + @Mapping(source = "invitee.name", target = "inviteeName") + @Mapping(source = "invitee.email", target = "inviteeEmail", defaultExpression = "java(\"링크 초대\")") + InvitationResponseDto toResponseDto(Invitation invitation); +} diff --git a/src/main/java/com/teamEWSN/gitdeun/invitation/repository/InvitationRepository.java b/src/main/java/com/teamEWSN/gitdeun/invitation/repository/InvitationRepository.java index 7837948..fd05ee8 100644 --- a/src/main/java/com/teamEWSN/gitdeun/invitation/repository/InvitationRepository.java +++ b/src/main/java/com/teamEWSN/gitdeun/invitation/repository/InvitationRepository.java @@ -1,5 +1,31 @@ package com.teamEWSN.gitdeun.invitation.repository; -public class InvitationRepository { - +import com.teamEWSN.gitdeun.invitation.entity.Invitation; +import com.teamEWSN.gitdeun.invitation.entity.InvitationStatus; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +public interface InvitationRepository extends JpaRepository { + + // 특정 마인드맵의 모든 초대 목록을 페이징하여 조회 + Page findByMindmapId(Long mindmapId, Pageable pageable); + + // 특정 마인드맵에 특정 유저가 이미 초대 대기중인지 확인 + boolean existsByMindmapIdAndInviteeIdAndStatusAndExpiresAtAfter(Long mindmapId, Long inviteeId, InvitationStatus status, LocalDateTime now); + + boolean existsByMindmapIdAndInviteeIdAndStatus(Long mindmapId, Long inviteeId, InvitationStatus status); + + // 사용자가 받은 모든 초대 목록 조회 + List findByInviteeIdAndStatus(Long inviteeId, InvitationStatus status); + + // 고유 토큰으로 초대 정보 조회 + Optional findByToken(String token); + } \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/invitation/service/InvitationService.java b/src/main/java/com/teamEWSN/gitdeun/invitation/service/InvitationService.java index 379089e..9c231e4 100644 --- a/src/main/java/com/teamEWSN/gitdeun/invitation/service/InvitationService.java +++ b/src/main/java/com/teamEWSN/gitdeun/invitation/service/InvitationService.java @@ -1,5 +1,256 @@ package com.teamEWSN.gitdeun.invitation.service; +import com.teamEWSN.gitdeun.common.exception.ErrorCode; +import com.teamEWSN.gitdeun.common.exception.GlobalException; +import com.teamEWSN.gitdeun.invitation.dto.InvitationActionResponseDto; +import com.teamEWSN.gitdeun.invitation.dto.InvitationResponseDto; +import com.teamEWSN.gitdeun.invitation.dto.InviteRequestDto; +import com.teamEWSN.gitdeun.invitation.dto.LinkResponseDto; +import com.teamEWSN.gitdeun.invitation.entity.Invitation; +import com.teamEWSN.gitdeun.invitation.entity.InvitationStatus; +import com.teamEWSN.gitdeun.invitation.mapper.InvitationMapper; +import com.teamEWSN.gitdeun.invitation.repository.InvitationRepository; +import com.teamEWSN.gitdeun.mindmap.entity.Mindmap; +import com.teamEWSN.gitdeun.mindmap.repository.MindmapRepository; +import com.teamEWSN.gitdeun.mindmapmember.entity.MindmapMember; +import com.teamEWSN.gitdeun.mindmapmember.entity.MindmapRole; +import com.teamEWSN.gitdeun.mindmapmember.repository.MindmapMemberRepository; +import com.teamEWSN.gitdeun.mindmapmember.service.MindmapAuthService; +import com.teamEWSN.gitdeun.user.entity.User; +import com.teamEWSN.gitdeun.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Service +@RequiredArgsConstructor public class InvitationService { - + + private final InvitationRepository invitationRepository; + private final UserRepository userRepository; + private final MindmapRepository mindmapRepository; + private final MindmapMemberRepository mindmapMemberRepository; + private final MindmapAuthService mindmapAuthService; + // private final NotificationService notificationService; + private final InvitationMapper invitationMapper; + + private static final String INVITATION_BASE_URL = "http://localhost:8080/invitations/"; + // private static final String INVITATION_BASE_URL = "https://gitdeun.site/invitations/"; + + // 초대 전송(이메일 + 알림) + @Transactional + public void inviteUserByEmail(Long mapId, InviteRequestDto requestDto, Long inviterId) { + // 마인드맵 소유자만 초대 가능 + if (!mindmapAuthService.isOwner(mapId, inviterId)) { + throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS); + } + + User invitee = userRepository.findByEmailAndDeletedAtIsNull(requestDto.getEmail()) + .orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND_BY_EMAIL)); + + // 본인 초대 불가 + if (inviterId.equals(invitee.getId())) { + throw new GlobalException(ErrorCode.CANNOT_INVITE_SELF); + } + + // 기존 멤버 여부 확인 + if (mindmapMemberRepository.existsByMindmapIdAndUserId(mapId, invitee.getId())) { + throw new GlobalException(ErrorCode.MEMBER_ALREADY_EXISTS); + } + + // 이미 초대 거절한 사용자 확인 + if (invitationRepository.existsByMindmapIdAndInviteeIdAndStatus(mapId, invitee.getId(), InvitationStatus.REJECTED)) { + throw new GlobalException(ErrorCode.INVITATION_REJECTED_USER); + } + + // 이미 초대했는지 확인 (만료 체크) + if (invitationRepository.existsByMindmapIdAndInviteeIdAndStatusAndExpiresAtAfter(mapId, invitee.getId(), InvitationStatus.PENDING, LocalDateTime.now())) { + throw new GlobalException(ErrorCode.INVITATION_ALREADY_EXISTS); + } + + Mindmap mindmap = mindmapRepository.findById(mapId) + .orElseThrow(() -> new GlobalException(ErrorCode.MINDMAP_NOT_FOUND)); + User inviter = userRepository.findById(inviterId) + .orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND_BY_ID)); + + Invitation invitation = Invitation.builder() + .mindmap(mindmap) + .inviter(inviter) + .invitee(invitee) + .token(UUID.randomUUID().toString()) + .role(requestDto.getRole()) + .status(InvitationStatus.PENDING) + .expiresAt(LocalDateTime.now().plusDays(1)) + .build(); + invitationRepository.save(invitation); + + // 알림 전송 + 이메일 전송 + // notificationService.sendInvitation(invitation); + } + + // 초대한 목록 조회(member) + @Transactional(readOnly = true) + public Page getInvitationsByMindmap(Long mapId, Long userId, Pageable pageable) { + if (!mindmapAuthService.hasView(mapId, userId)) { + throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS); + } + + Page invitations = invitationRepository.findByMindmapId(mapId, pageable); + + return invitations.map(invitationMapper::toResponseDto); + } + + // 초대 수락 + @Transactional + public void acceptInvitation(Long invitationId, Long userId) { + Invitation invitation = invitationRepository.findById(invitationId) + .orElseThrow(() -> new GlobalException(ErrorCode.INVITATION_NOT_FOUND)); + + // 초대 중복 여부 + if (!invitation.getInvitee().getId().equals(userId)) { + throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS); + } + + // 초대시간 만료 + if (invitation.getExpiresAt().isBefore(LocalDateTime.now())) { + throw new GlobalException(ErrorCode.INVITATION_EXPIRED); + } + + Invitation newInvitation = invitation.accept(); + MindmapMember newMember = MindmapMember.of(newInvitation.getMindmap(), newInvitation.getInvitee(), newInvitation.getRole()); + mindmapMemberRepository.save(newMember); + + // notificationService.sendAcceptance(invitation); + } + + // 초대 거절 + @Transactional + public void rejectInvitation(Long invitationId, Long userId) { + Invitation invitation = invitationRepository.findById(invitationId) + .orElseThrow(() -> new GlobalException(ErrorCode.INVITATION_NOT_FOUND)); + + if (!invitation.getInvitee().getId().equals(userId)) { + throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS); + } + + // 초대 시간 만료 + if (invitation.getExpiresAt().isBefore(LocalDateTime.now())) { + throw new GlobalException(ErrorCode.INVITATION_EXPIRED); + } + + invitation.reject(); + // notificationService.sendRejection(invitation); + } + + // 초대 링크 생성(owner) + @Transactional + public LinkResponseDto createInvitationLink(Long mapId, Long inviterId) { + if (!mindmapAuthService.isOwner(mapId, inviterId)) { + throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS); + } + + Mindmap mindmap = mindmapRepository.findById(mapId) + .orElseThrow(() -> new GlobalException(ErrorCode.MINDMAP_NOT_FOUND)); + User inviter = userRepository.findById(inviterId) + .orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND_BY_ID)); + + Invitation invitation = Invitation.builder() + .mindmap(mindmap) + .inviter(inviter) + .invitee(null) // 링크 초대는 처음엔 초대받는 사람이 없음 + .token(UUID.randomUUID().toString()) + .role(MindmapRole.VIEWER) // 링크 초대는 기본적으로 가장 낮은 권한 부여 + .status(InvitationStatus.PENDING) // 링크 자체는 PENDING 상태 + .expiresAt(LocalDateTime.now().plusDays(1)) + .build(); + invitationRepository.save(invitation); + + return new LinkResponseDto(INVITATION_BASE_URL + invitation.getToken()); + } + + // 초대 링크 접근 + @Transactional + public void acceptInvitationByLink(String token, Long userId) { + Invitation invitation = invitationRepository.findByToken(token) + .orElseThrow(() -> new GlobalException(ErrorCode.INVITATION_NOT_FOUND)); + + if (invitation.getExpiresAt().isBefore(LocalDateTime.now())) { + throw new GlobalException(ErrorCode.INVITATION_EXPIRED); + } + + // 이미 초대 거절한 사용자 확인 + if (invitationRepository.existsByMindmapIdAndInviteeIdAndStatus(invitation.getMindmap().getId(), userId, InvitationStatus.REJECTED)) { + throw new GlobalException(ErrorCode.INVITATION_REJECTED_USER); + } + + User user = userRepository.findById(userId) + .orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND_BY_ID)); + + Invitation updatedInvitation = invitation.toBuilder() + .invitee(user) + .status(InvitationStatus.PENDING) + .build(); + invitationRepository.save(updatedInvitation); + + // notificationService.sendLinkApprovalRequest(invitation); + } + + // 초대 링크 수락(owner) + @Transactional + public InvitationActionResponseDto approveLinkInvitation(Long invitationId, Long ownerId) { + Invitation invitation = invitationRepository.findById(invitationId) + .orElseThrow(() -> new GlobalException(ErrorCode.INVITATION_NOT_FOUND)); + + // owner 확인 + if (!invitation.getMindmap().getUser().getId().equals(ownerId)) { + throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS); + } + + // 만료 시간 확인 + if (invitation.getExpiresAt().isBefore(LocalDateTime.now())) { + throw new GlobalException(ErrorCode.INVITATION_EXPIRED); + } + + // 입장 사용자 확인 + if (invitation.getInvitee() == null) { + throw new GlobalException(ErrorCode.INVITATION_NOT_FOUND); + } + + Invitation newInvitation = invitation.accept(); + MindmapMember newMember = MindmapMember.of(newInvitation.getMindmap(), newInvitation.getInvitee(), newInvitation.getRole()); + mindmapMemberRepository.save(newMember); + + return new InvitationActionResponseDto("초대 요청이 승인되었습니다."); + } + + // Owner의 링크 초대 요청 거절 메서드 + @Transactional + public InvitationActionResponseDto rejectLinkApproval(Long invitationId, Long ownerId) { + Invitation invitation = invitationRepository.findById(invitationId) + .orElseThrow(() -> new GlobalException(ErrorCode.INVITATION_NOT_FOUND)); + + // owner 확인 + if (!invitation.getMindmap().getUser().getId().equals(ownerId)) { + throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS); + } + + // 만료 시간 확인 + if (invitation.getExpiresAt().isBefore(LocalDateTime.now())) { + throw new GlobalException(ErrorCode.INVITATION_EXPIRED); + } + + // 입장 사용자 확인 + if (invitation.getInvitee() == null) { + throw new GlobalException(ErrorCode.INVITATION_NOT_FOUND); + } + + invitation.reject(); + + return new InvitationActionResponseDto("초대 요청이 거부되었습니다."); + } } \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/notification/entity/Notification.java b/src/main/java/com/teamEWSN/gitdeun/notification/entity/Notification.java new file mode 100644 index 0000000..1a1edcd --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/notification/entity/Notification.java @@ -0,0 +1,4 @@ +package com.teamEWSN.gitdeun.notification.entity; + +public class Notification { +}