Skip to content

Commit

Permalink
feat: SSE 기능 추가
Browse files Browse the repository at this point in the history
사용자 가입 요청 시 알람, 사용자 가입 승인 시 알람, 사용자 가입 거절 시
알람
  • Loading branch information
EOTAEGYU committed Oct 24, 2024
1 parent 01a7e3d commit c293c87
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 142 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/github-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
run: |
docker push eotaegyu/keynote.app:latest
# GCP 인스턴스에서 Docker 이미지를 풀링하고 업데이트하는 단계.
# GCP 인스턴스에서 Docker 이미지를 풀링하고 업데이트하는 단계..
deploy:
runs-on: ubuntu-latest
needs: build
Expand All @@ -64,3 +64,4 @@ jobs:
sudo docker pull eotaegyu/keynote.app:latest
sudo docker-compose down
sudo docker-compose up -d
debug: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.ucd.keynote.domain.notification;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.concurrent.CopyOnWriteArrayList;

@RestController
public class NotificationController {
// SSE 연결된 사용자들을 관리하기 위한 리스트
private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>();

// 클라이언트가 이 API를 호출하면 SSE 연결이 수립됨
@GetMapping(value = "/api/notifications/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribe() {
SseEmitter emitter = new SseEmitter(60 * 60 * 1000L); // 1시간 동안 연결 유지
emitters.add(emitter);

emitter.onCompletion(() -> emitters.remove(emitter)); // 연결이 완료되면 삭제
emitter.onTimeout(() -> emitters.remove(emitter)); // 타임아웃 시 삭제
emitter.onError(e -> emitters.remove(emitter)); // 에러 발생 시 삭제

return emitter;
}

// 알림을 보낼 메소드
public void sendNotification(String message) {
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event().name("notification").data(message));
} catch (IOException e) {
emitters.remove(emitter); // 실패한 emitter는 제거
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ public interface UserOrganizationRepository extends JpaRepository<UserOrganizati

// 사용자가 특정 조직에 속해 있는지 여부 확인
boolean existsByOrganization_OrganizationIdAndUser_UserId(Long organizationId, Long userId);

List<UserOrganization> findByOrganization_OrganizationIdAndRole(Long organizationId, String role);
}
Original file line number Diff line number Diff line change
@@ -1,155 +1,170 @@
package com.ucd.keynote.domain.organization.service;

import com.ucd.keynote.common.service.AuthService;
import com.ucd.keynote.domain.organization.dto.join.JoinRequestDTO;
import com.ucd.keynote.domain.organization.dto.join.JoinRequestResponseDTO;
import com.ucd.keynote.domain.organization.dto.join.JoinResponseDTO;
import com.ucd.keynote.domain.organization.entity.Organization;
import com.ucd.keynote.domain.organization.entity.UserOrganization;
import com.ucd.keynote.domain.organization.entity.UserOrganizationId;
import com.ucd.keynote.domain.organization.entity.join.OrganizationJoinRequest;
import com.ucd.keynote.domain.organization.repository.OrganizationRepository;
import com.ucd.keynote.domain.organization.repository.UserOrganizationRepository;
import com.ucd.keynote.domain.organization.repository.join.OrganizationJoinRepository;
import com.ucd.keynote.domain.user.entity.UserEntity;
import lombok.AllArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

@Service
@AllArgsConstructor
public class OrganizationJoinService {
private final OrganizationJoinRepository organizationJoinRepository;
private final OrganizationRepository organizationRepository;
private final AuthService authService;
private final UserOrganizationRepository userOrganizationRepository;

// 가입 신청 처리
public JoinResponseDTO requestJoinOrganization(Long organizationId, JoinRequestDTO reqeust){
Organization organization = organizationRepository.findById(organizationId)
.orElseThrow(() -> new IllegalArgumentException("조직이 존재하지 않습니다."));
UserEntity user = authService.getAuthenticatedUser();

// 사용자가 이미 조직에 속해 있는지 확인
boolean alreadyInOrganization = userOrganizationRepository.existsByOrganization_OrganizationIdAndUser_UserId(organizationId, user.getUserId());
if (alreadyInOrganization) {
throw new IllegalStateException("사용자가 이미 조직에 속해 있습니다.");
package com.ucd.keynote.domain.organization.service;

import com.ucd.keynote.common.service.AuthService;
import com.ucd.keynote.domain.notification.NotificationController;
import com.ucd.keynote.domain.organization.dto.join.JoinRequestDTO;
import com.ucd.keynote.domain.organization.dto.join.JoinRequestResponseDTO;
import com.ucd.keynote.domain.organization.dto.join.JoinResponseDTO;
import com.ucd.keynote.domain.organization.entity.Organization;
import com.ucd.keynote.domain.organization.entity.UserOrganization;
import com.ucd.keynote.domain.organization.entity.UserOrganizationId;
import com.ucd.keynote.domain.organization.entity.join.OrganizationJoinRequest;
import com.ucd.keynote.domain.organization.repository.OrganizationRepository;
import com.ucd.keynote.domain.organization.repository.UserOrganizationRepository;
import com.ucd.keynote.domain.organization.repository.join.OrganizationJoinRepository;
import com.ucd.keynote.domain.user.entity.UserEntity;
import lombok.AllArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

@Service
@AllArgsConstructor
public class OrganizationJoinService {
private final OrganizationJoinRepository organizationJoinRepository;
private final OrganizationRepository organizationRepository;
private final AuthService authService;
private final UserOrganizationRepository userOrganizationRepository;
private final NotificationController notificationController;

// 가입 신청 처리
public JoinResponseDTO requestJoinOrganization(Long organizationId, JoinRequestDTO reqeust){
Organization organization = organizationRepository.findById(organizationId)
.orElseThrow(() -> new IllegalArgumentException("조직이 존재하지 않습니다."));
UserEntity user = authService.getAuthenticatedUser();

// 사용자가 이미 조직에 속해 있는지 확인
boolean alreadyInOrganization = userOrganizationRepository.existsByOrganization_OrganizationIdAndUser_UserId(organizationId, user.getUserId());
if (alreadyInOrganization) {
throw new IllegalStateException("사용자가 이미 조직에 속해 있습니다.");
}

// 이미 가입 신청이 존재하는지 확인(PENDING 상태)
boolean alreadyRequested = organizationJoinRepository.existsByOrganization_OrganizationIdAndUser_UserIdAndStatus
(organizationId, user.getUserId(), "PENDING");
if (alreadyRequested){
throw new IllegalArgumentException("이미 조직에 가입 신청을 보냈습니다.");
}

// 가입 신청 생성
OrganizationJoinRequest joinRequest = new OrganizationJoinRequest();
joinRequest.setOrganization(organization);
joinRequest.setUser(user);
joinRequest.setStatus("PENDING");
joinRequest.setRequestMessage(reqeust.getMessage());
joinRequest.setRequestedAt(LocalDateTime.now());

organizationJoinRepository.save(joinRequest);

// 조직 내 관리자에게 알림 전송
List<UserOrganization> admins = userOrganizationRepository.findByOrganization_OrganizationIdAndRole(organizationId, "admin");
String notificationMessage = "새로운 가입 요청이 있습니다. 사용자: " + user.getUsername();
admins.forEach(admin -> notificationController.sendNotification(notificationMessage));


// 웅답 DTO 생성
return JoinResponseDTO.builder()
.requestId(joinRequest.getId())
.organizationId(organization.getOrganizationId())
.status(joinRequest.getStatus())
.createdAt(joinRequest.getRequestedAt())
.build();
}

// 이미 가입 신청이 존재하는지 확인(PENDING 상태)
boolean alreadyRequested = organizationJoinRepository.existsByOrganization_OrganizationIdAndUser_UserIdAndStatus
(organizationId, user.getUserId(), "PENDING");
if (alreadyRequested){
throw new IllegalArgumentException("이미 조직에 가입 신청을 보냈습니다.");
// 가입 신청 목록 조회
public List<JoinRequestResponseDTO> getJoinRequests(Long organizationId){
List<OrganizationJoinRequest> joinRequests = organizationJoinRepository.findByOrganization_OrganizationIdAndStatus
(organizationId, "PENDING");
return joinRequests.stream().map(request -> JoinRequestResponseDTO.builder()
.requestId(request.getId())
.userId(request.getUser().getUserId())
.userName(request.getUser().getUsername())
.message(request.getRequestMessage())
.status(request.getStatus())
.requestedAt(request.getRequestedAt())
.build()).collect(Collectors.toList());
}

// 가입 신청 생성
OrganizationJoinRequest joinRequest = new OrganizationJoinRequest();
joinRequest.setOrganization(organization);
joinRequest.setUser(user);
joinRequest.setStatus("PENDING");
joinRequest.setRequestMessage(reqeust.getMessage());
joinRequest.setRequestedAt(LocalDateTime.now());

organizationJoinRepository.save(joinRequest);

// 웅답 DTO 생성
return JoinResponseDTO.builder()
.requestId(joinRequest.getId())
.organizationId(organization.getOrganizationId())
.status(joinRequest.getStatus())
.createdAt(joinRequest.getRequestedAt())
.build();
}

// 가입 신청 목록 조회
public List<JoinRequestResponseDTO> getJoinRequests(Long organizationId){
List<OrganizationJoinRequest> joinRequests = organizationJoinRepository.findByOrganization_OrganizationIdAndStatus
(organizationId, "PENDING");
return joinRequests.stream().map(request -> JoinRequestResponseDTO.builder()
.requestId(request.getId())
.userId(request.getUser().getUserId())
.userName(request.getUser().getUsername())
.message(request.getRequestMessage())
.status(request.getStatus())
.requestedAt(request.getRequestedAt())
.build()).collect(Collectors.toList());
}

// 가입 요청 승인
@Transactional
public void approveJoniRequest(Long organizationId, Long requestId){
// 사용자 권한 확인
UserEntity userEntity = authService.getAuthenticatedUser();
// 해당 조직에 admin 권한이 있는지 확인
UserOrganization Organization = userOrganizationRepository.findByOrganization_OrganizationIdAndUser_UserId(organizationId, userEntity.getUserId())
.orElseThrow(() -> new AccessDeniedException("이 조직에서 권한이 없습니다."));
//admin 권한 체크
if (!"admin".equals(Organization.getRole())) {
throw new AccessDeniedException("admin 권한이 있어야 요청을 승인할 수 있습니다.");
}

// 가입 요청 가져오기
OrganizationJoinRequest joinRequest = organizationJoinRepository.findById(requestId)
.orElseThrow(() -> new IllegalArgumentException("Join request not found"));

// 이미 처리된 요청인지 확인
if (!"PENDING".equals(joinRequest.getStatus())){
throw new IllegalStateException("Join request has already been processed");
// 가입 요청 승인
@Transactional
public void approveJoniRequest(Long organizationId, Long requestId){
// 사용자 권한 확인
UserEntity userEntity = authService.getAuthenticatedUser();
// 해당 조직에 admin 권한이 있는지 확인
UserOrganization Organization = userOrganizationRepository.findByOrganization_OrganizationIdAndUser_UserId(organizationId, userEntity.getUserId())
.orElseThrow(() -> new AccessDeniedException("이 조직에서 권한이 없습니다."));
//admin 권한 체크
if (!"admin".equals(Organization.getRole())) {
throw new AccessDeniedException("admin 권한이 있어야 요청을 승인할 수 있습니다.");
}

// 가입 요청 가져오기
OrganizationJoinRequest joinRequest = organizationJoinRepository.findById(requestId)
.orElseThrow(() -> new IllegalArgumentException("Join request not found"));

// 이미 처리된 요청인지 확인
if (!"PENDING".equals(joinRequest.getStatus())){
throw new IllegalStateException("Join request has already been processed");
}

// 조직 가져오기
Organization organization = organizationRepository.findById(organizationId)
.orElseThrow(() -> new IllegalArgumentException("Organization not found"));

System.out.println("get userId: " + joinRequest.getUser().getUserId());
System.out.println("get organization: " + organization.getOrganizationId());

// UserOrganizationId 설정
UserOrganizationId userOrganizationId = new UserOrganizationId(joinRequest.getUser().getUserId(), organization.getOrganizationId());

// 사용자를 조직에 추가
UserOrganization userOrganization = new UserOrganization();
userOrganization.setId(userOrganizationId);
userOrganization.setUser(joinRequest.getUser());
userOrganization.setOrganization(organization);
userOrganization.setRole("user");
userOrganization.setJoinedAt(LocalDateTime.now());
userOrganizationRepository.save(userOrganization);

// 가입 요청 상태 변경
joinRequest.setStatus("APPROVED");
organizationJoinRepository.save(joinRequest);

// 가입 요청 사용자에게 승인 알림 전송
String notificationMessage = "가입 요청이 승인되었습니다. 조직: " + organization.getOrganizationName();
notificationController.sendNotification(notificationMessage);
}

// 조직 가져오기
Organization organization = organizationRepository.findById(organizationId)
.orElseThrow(() -> new IllegalArgumentException("Organization not found"));

System.out.println("get userId: " + joinRequest.getUser().getUserId());
System.out.println("get organization: " + organization.getOrganizationId());
// 가입 요청 거절
public void rejectJoinRequest(Long organizationId, Long requestId){
// 현재 로그인 한 사용자 정보 받아오기
UserEntity userEntity = authService.getAuthenticatedUser();

// UserOrganizationId 설정
UserOrganizationId userOrganizationId = new UserOrganizationId(joinRequest.getUser().getUserId(), organization.getOrganizationId());
// 해당 조직에 admin 권한이 있는지 확인
UserOrganization userOrganization = userOrganizationRepository.findByOrganization_OrganizationIdAndUser_UserId(organizationId, userEntity.getUserId())
.orElseThrow(() -> new AccessDeniedException("이 조직에서 권한이 없습니다."));
//admin 권한 체크
if (!"admin".equals(userOrganization.getRole())) {
throw new AccessDeniedException("admin 권한이 있어야 요청을 승인할 수 있습니다.");
}

// 사용자를 조직에 추가
UserOrganization userOrganization = new UserOrganization();
userOrganization.setId(userOrganizationId);
userOrganization.setUser(joinRequest.getUser());
userOrganization.setOrganization(organization);
userOrganization.setRole("user");
userOrganization.setJoinedAt(LocalDateTime.now());
userOrganizationRepository.save(userOrganization);
// 가입 요청 조회
OrganizationJoinRequest joinRequest = organizationJoinRepository.findById(requestId)
.orElseThrow(() -> new IllegalArgumentException("가입 요청을 찾을 수 없습니다."));

// 가입 요청 상태 변경
joinRequest.setStatus("APPROVED");
organizationJoinRepository.save(joinRequest);
// 가입 요청 상태를 REJECTED로 변경
joinRequest.setStatus("REJECTED");
organizationJoinRepository.save(joinRequest);

}


// 가입 요청 거절
public void rejectJoinRequest(Long organizationId, Long requestId){
// 현재 로그인 한 사용자 정보 받아오기
UserEntity userEntity = authService.getAuthenticatedUser();

// 해당 조직에 admin 권한이 있는지 확인
UserOrganization userOrganization = userOrganizationRepository.findByOrganization_OrganizationIdAndUser_UserId(organizationId, userEntity.getUserId())
.orElseThrow(() -> new AccessDeniedException("이 조직에서 권한이 없습니다."));
//admin 권한 체크
if (!"admin".equals(userOrganization.getRole())) {
throw new AccessDeniedException("admin 권한이 있어야 요청을 승인할 수 있습니다.");
// 가입 요청 사용자에게 거절 알림 전송
String notificationMessage = "가입 요청이 거절되었습니다. 조직: " + joinRequest.getOrganization().getOrganizationName();
notificationController.sendNotification(notificationMessage);
}

// 가입 요청 조회
OrganizationJoinRequest joinRequest = organizationJoinRepository.findById(requestId)
.orElseThrow(() -> new IllegalArgumentException("가입 요청을 찾을 수 없습니다."));

// 가입 요청 상태를 REJECTED로 변경
joinRequest.setStatus("REJECTED");
organizationJoinRepository.save(joinRequest);
}

}

0 comments on commit c293c87

Please sign in to comment.