프로젝트/공유 캘린더

공유 캘린더 만들기 #4 (Service 및 스케줄러 디테일 구성)

핑구우 2023. 7. 17. 16:19

Service 부분은 대부분 CRUD 구성으로 비슷한 로직으로 구성이 되어있어 차별을 둔 점이나 핵심적인 부분 위주로 작성하였습니다.

 

캘린더에 일정 안에서 사용자가 사진도 넣고, 상세한 내용도 작성 할 수 있도록 구현을 하였고, 또한 공유된 일정이 있을때 공유된 사용자들끼리 그 일정에 댓글을 달 수 있도록 구현하였습니다.

 

메시지 기능을 넣어 개인일정을 공유할 수 있도록 하였고, 그룹 참여에 대해서는 카카오톡 링크 API를 이용해 공유코드를 입력하는 방식으로 구현하였습니다.

 

가장 고민이 있었던 부분은 일정이 다가왔을때나 설정한 시간에 사용자에게 효율적으로 알림을 주고싶은데 사용자가 웹에 계속해서 접속해있지 않은 이상 알림기능을 주기 힘들었다.

 

그렇게 생각해 낸 방법이

 

1. 카카오톡 메시지 api를 이용해서 구현을 한다 -> 카카오톡 로그인을 서버에서 구현해야한다 -> 기존 회원 로직을 재구성 해야한다 또한 메시지 기능이 개인 사용자가 이용하기엔 인원수가 정해져 있고, 직접 사용자를 등록해야지만 메시지가 전송이 되는 번거로움이 있다

 

2.카카오톡 플러스 친구를 이용하자 -> 개인 사용자가 플러스 친구를 통해 알림을 주는 것은 불가능하였고, 사업자 정보와 같은 것을 제출하여 비즈니스 채널을 등록해야지만 플러스 친구 알림 기능을 이용 할 수 있었다.

3. 문자 전송 플랫폼 이용(CoolSMS) -> 사용자가 회원가입 할 때 작성하는 휴대폰 번호로 알림 기능을 전송한다 -> 유료이지만 정해진 횟수 안에서는 무료 포인트를 제공해준다.

 

 

2번을 해보고싶었는데 개인이 할 수 없어서 아쉬웠다..

결론은 1번과 3번을 고민하다가 비교적 3번이 나은 거 같아서 3번을 택했다. 더 좋은 방법이 있다면 시도해보고싶다.

 

ScheduleService.java

- 스케줄을 생성하는 방법은 공유된 일정으로 생성할지 개인 스케줄로만 생성할지 구분지어 코드를 구성했다.

- 나의 일정을 불러올 때 이미지를 인코딩해서 전달하였다(이 방식은 용량을 많이 차지하는 문제가 있어 추후에 AWS S3에 url로 저장하는 방식으로 수정 할 계획이다.)

@Transactional
    public ScheduleDto createSchedule(ScheduleDto scheduleDto) {
        MemberResponseDto myInfoBySecurity = memberService.getMyInfoBySecurity();
        scheduleDto.setMemberId(myInfoBySecurity.getId());

        if (scheduleDto.isAlarm() && scheduleDto.getAlarmDateTime() == null) {
            throw new IllegalArgumentException("알람이 True인데 시간 설정이 안되었습니다.");
        }
        if (!scheduleDto.isAlarm() && scheduleDto.getAlarmDateTime()!=null) {
            throw new IllegalArgumentException("알람이 False인데 시간 설정이 되어있습니다.");
        }

        Schedule schedule = modelMapper.map(scheduleDto, Schedule.class);
        Member member = memberRepository.findById(scheduleDto.getMemberId()).orElse(null);
        schedule.setMember(member);

        Schedule savedSchedule = scheduleRepository.save(schedule);
        return modelMapper.map(savedSchedule, ScheduleDto.class);
    }

    @Transactional
    public ScheduleDto createSharedSchedule(ScheduleDto scheduleDto, List<String> sharedWithIds) {
        MemberResponseDto myInfoBySecurity = memberService.getMyInfoBySecurity();
        scheduleDto.setMemberId(myInfoBySecurity.getId());

        if (scheduleDto.isAlarm() && scheduleDto.getAlarmDateTime() == null) {
            throw new IllegalArgumentException("알람이 True인데 시간 설정이 안되었습니다.");
        }
        if (!scheduleDto.isAlarm() && scheduleDto.getAlarmDateTime()!=null) {
            throw new IllegalArgumentException("알람이 False인데 시간 설정이 되어있습니다.");
        }

        Schedule schedule = modelMapper.map(scheduleDto, Schedule.class);
        Member member = memberRepository.findById(scheduleDto.getMemberId()).orElse(null);
        schedule.setMember(member);

        Schedule savedSchedule = scheduleRepository.save(schedule);

        // 공유 대상 멤버들에 대한 공유 스케줄 정보 저장
        List<Member> sharedWithMembers = memberRepository.findAllByEmailIn(sharedWithIds);
        List<SharedSchedule> sharedSchedules = sharedWithMembers.stream().map(sharedWith -> {
            SharedSchedule sharedSchedule = new SharedSchedule();
            sharedSchedule.setSchedule(savedSchedule);
            sharedSchedule.setMember(sharedWith);

            sharedScheduleRepository.save(sharedSchedule); // 공유 스케줄 저장 후 아이디값이 생성됨

            // 공유받는 멤버에게 메시지 전송
            String messageTitle = "새로운 공유 스케줄이 도착했습니다.";
            String messageContent = schedule.getTitle()+"";
            MessageDto messageDto = new MessageDto();
            messageDto.setSenderName(myInfoBySecurity.getNickname());
            messageDto.setReceiverName(sharedWith.getNickname());
            messageDto.setTitle(messageTitle);
            messageDto.setContent(messageContent);
            messageDto.setSharedScheduleId(sharedSchedule.getId()); // 공유 스케줄 아이디값 설정
            messageServices.write(messageDto); // 메시지 전송

            return sharedSchedule;
        }).collect(Collectors.toList());

        return modelMapper.map(savedSchedule, ScheduleDto.class);
    }

    public List<ScheduleDto> getMySchedules(){
        MemberResponseDto myInfoBySecurity = memberService.getMyInfoBySecurity();
        Member member = memberRepository.findById(myInfoBySecurity.getId()).orElseThrow(() -> new EntityNotFoundException("Member not found"));
        List<Schedule> schedules = member.getSchedules();
        if (!sharedScheduleRepository.findByMemberId(member.getId()).isEmpty()) {
            List<SharedSchedule> sharedSchedules = sharedScheduleRepository.findByMemberId(member.getId());
            for (SharedSchedule sharedSchedule : sharedSchedules) {
                if (sharedSchedule.isApproved()) {
                    schedules.add(sharedSchedule.getSchedule());
                }
            }
        }
        List<ScheduleDto> scheduleDtos = new ArrayList<>();
        for (Schedule schedule : schedules) {
            ScheduleDto scheduleDto = modelMapper.map(schedule, ScheduleDto.class);

            // 이미지 처리
            List<Image> images = schedule.getImages();
            if (images != null && !images.isEmpty()) {
                List<ImageDto> imageDtos = new ArrayList<>();
                for (Image image : images) {
                    ImageDto imageDto = new ImageDto();
                    byte[] imageBytes = image.getImageData();
                    String base64Image = Base64.getEncoder().encodeToString(imageBytes);
                    imageDto.setImageData(base64Image);
                    imageDtos.add(imageDto);
                }
                scheduleDto.setImages(imageDtos);
            }

            scheduleDtos.add(scheduleDto);
        }
        return scheduleDtos;
    }

 

스케줄링을 이용하여 현재 시간과 지속적으로 비교하여 알림 기능을 구현하였다.

- 그룹 스케줄도 이와 비슷하게 owner에게 알림이 가도록 하였다(메신저 전송 포인트가 제한적이라 인원을 최소화 하였다..)

@Scheduled(cron = "0 * * * * *")// 1분마다 실행
    @Transactional
    public void SendOne() {
        LocalDateTime currentDateTime = LocalDateTime.now();
        LocalDateTime modifiedDateTime = currentDateTime.plusHours(9);
        log.info("Current DateTime: {}", currentDateTime);
        // alarmDateTime과 현재 시간 비교
        List<Schedule> schedules = scheduleRepository.findByAlarmDateTimeBefore(modifiedDateTime);

        for (Schedule schedule : schedules) {
            if (schedule.isAlarm()) {
                net.nurigo.sdk.message.model.Message message = new Message();
                // 발신번호 및 수신번호는 반드시 01012345678 형태로 입력되어야 합니다.
                Member member = schedule.getMember();
                message.setFrom("01039028407");
                message.setTo(member.getPhoneNumber());
                message.setText("금일은 " + schedule.getTitle() + " 일정이 있는 날입니다.");
                this.messageService.sendOne(new SingleMessageSendingRequest(message));
                schedule.setAlarm(false);
                scheduleRepository.save(schedule);
            }
        }
    }

 

Group.java

- 그룹을 생성할때 sharedCode를 RandomStirngUtils안에있는 randomAlphanumeric함수를 이용해서 랜덤 코드를 저장했다.

- 사용자가 공유코드를 입력하면 그룹의 owner가 승인하여 member를 추가 할 수 있도록 하였다.

- 공유코드는 Front 단에서 카카오 링크 API를 구현하여 전송하였다. (카카오 로그인 정보가 필요없이 카카오 링크로 연결만 해주는 API라서 프론트단에서 구현해도 무방하다고 생각된다.)

@Transactional
    public Long createGroup(GroupDto groupDto) {
        MyGroup myGroup = MyGroup.builder()
                .name(groupDto.getName())
                .build();
        String sharedCode = RandomStringUtils.randomAlphanumeric(8);
        Long ownerId = SecurityUtil.getCurrentMemberId();
        Member member = memberRepository.findById(ownerId).orElse(null);

        if (member != null) {
            MemberGroup memberGroup = new MemberGroup();
            memberGroup.setMember(member);
            memberGroup.setGroup(myGroup);
            myGroup.getMemberGroups().add(memberGroup);

            myGroup.setOwner(member);
            myGroup.setSharedCode(sharedCode);
            memberGroupRepository.save(memberGroup);
            return groupRepository.save(myGroup).getId();
        }

        return null;
    }

@Transactional
    public GroupDto addMemberToGroup(String sharedCode, String email) {
        MemberResponseDto myInfoBySecurity = memberService.getMyInfoBySecurity();
        Member owner = memberRepository.findById(myInfoBySecurity.getId())
                .orElseThrow(() -> new EntityNotFoundException("Member not found"));

        MyGroup myGroup = groupRepository.findBySharedCode(sharedCode);

        // 승인하는 사람이 그룹의 오너인지 확인
        if (!myGroup.getOwner().equals(owner)) {
            throw new IllegalStateException("Only the owner can approve group requests.");
        }

        Member member = memberRepository.findByEmail(email).orElse(null);
        if (member != null) {
            // 중복 체크: 이미 해당 그룹에 속한 멤버인지 확인
            boolean isMemberInGroup = memberGroupRepository.existsByGroupAndMember(myGroup, member);
            if (isMemberInGroup) {
                throw new IllegalStateException("The member is already in the group.");
            }

            MemberGroup memberGroup = new MemberGroup();
            memberGroup.setMember(member);
            memberGroup.setGroup(myGroup);
            myGroup.getMemberGroups().add(memberGroup);
            memberGroupRepository.save(memberGroup);

            MyGroup savedGroup = groupRepository.save(myGroup);
            return modelMapper.map(savedGroup, GroupDto.class);
        } else {
            throw new EntityNotFoundException("Member not found");
        }
    }



    @Transactional
    public void removeMemberFromGroup(Long groupId, Long memberId) {
        MyGroup myGroup = groupRepository.findById(groupId)
                .orElseThrow(() -> new IllegalArgumentException("Invalid group ID: " + groupId));
        Member member = memberRepository.findById(memberId)
                .orElseThrow(() -> new IllegalArgumentException("Invalid member ID: " + memberId));

        List<MemberGroup> memberGroups = myGroup.getMemberGroups();
        MemberGroup memberGroupToRemove = null;

        // 해당 멤버를 가진 MemberGroup 엔티티를 찾아 제거
        for (MemberGroup memberGroup : memberGroups) {
            if (memberGroup.getMember().equals(member)) {
                memberGroupToRemove = memberGroup;
                break;
            }
        }

        if (memberGroupToRemove != null) {
            memberGroups.remove(memberGroupToRemove);
            groupRepository.save(myGroup);
        }
    }

 

CommentService.java

@Service
public class CommentService {
    private final CommentRepository commentRepository;
    private final MemberRepository memberRepository;
    private final ScheduleRepository scheduleRepository;
    private final GroupScheduleRepository groupScheduleRepository;

    private final ModelMapper modelMapper;

    @Autowired
    public CommentService(CommentRepository commentRepository, MemberRepository memberRepository, ScheduleRepository scheduleRepository,
                          ModelMapper modelMapper,GroupScheduleRepository groupScheduleRepository) {
        this.commentRepository = commentRepository;
        this.memberRepository = memberRepository;
        this.scheduleRepository = scheduleRepository;
        this.modelMapper = modelMapper;
        this.groupScheduleRepository = groupScheduleRepository;
    }

    public CommentDTO createComment(CommentDTO commentDTO) {
        Comment comment = new Comment();
        comment.setText(commentDTO.getText());

        if(commentDTO.getScheduleId()!=null) {
                Schedule schedule = scheduleRepository.findById(commentDTO.getScheduleId())
                    .orElseThrow(() -> new IllegalArgumentException("Invalid Schedule ID"));

            comment.setSchedule(schedule);
        }
        if(commentDTO.getGroupScheduleId()!=null) {
            GroupSchedule groupSchedule = groupScheduleRepository.findById(commentDTO.getGroupScheduleId())
                    .orElseThrow(() -> new IllegalArgumentException("Invalid Schedule ID"));

            comment.setGroupSchedule(groupSchedule);
        }

        Member member = memberRepository.findById(SecurityUtil.getCurrentMemberId()).orElse(null);

        comment.setMember(member);

        Comment savedComment = commentRepository.save(comment);

        ModelMapper modelMapper = new ModelMapper();

        CommentDTO savedCommentDTO = modelMapper.map(savedComment, CommentDTO.class);

        return savedCommentDTO;
    }
    public CommentDTO getComment(Long commentId) {
        Comment comment = commentRepository.findById(commentId)
                .orElseThrow(() -> new IllegalArgumentException("Invalid Comment ID"));

        return modelMapper.map(comment, CommentDTO.class);
    }

    public List<CommentDTO> getCommentsBySchedule(Long scheduleId) {
        Schedule schedule = scheduleRepository.findById(scheduleId)
                .orElseThrow(() -> new IllegalArgumentException("Invalid Schedule ID"));

        List<Comment> comments = commentRepository.findBySchedule(schedule);

        return comments.stream()
                .map(comment -> {
                    CommentDTO commentDTO = modelMapper.map(comment, CommentDTO.class);
                    commentDTO.setMemberId(comment.getMember().getId()); // member의 memberId를 commentDTO에 설정
                    commentDTO.setMemberNickname(comment.getMember().getNickname());
                    return commentDTO;
                })
                .collect(Collectors.toList());
    }
    public List<CommentDTO> getCommentsByGroupSchedule(Long groupScheduleId) {
        GroupSchedule groupSchedule = groupScheduleRepository.findById(groupScheduleId)
                .orElseThrow(() -> new IllegalArgumentException("Invalid Schedule ID"));

        List<Comment> comments = commentRepository.findByGroupSchedule(groupSchedule);

        return comments.stream()
                .map(comment -> {
                    CommentDTO commentDTO = modelMapper.map(comment, CommentDTO.class);
                    commentDTO.setMemberId(comment.getMember().getId()); // member의 memberId를 commentDTO에 설정
                    commentDTO.setMemberNickname(comment.getMember().getNickname());
                    return commentDTO;
                })
                .collect(Collectors.toList());
    }

    public void deleteComment(Long commentId) {
        Comment comment = commentRepository.findById(commentId)
                .orElseThrow(() -> new IllegalArgumentException("Invalid Comment ID"));

        // 현재 로그인된 사용자의 ID를 가져옵니다.
        Long currentMemberId = SecurityUtil.getCurrentMemberId();

        // 현재 로그인된 사용자의 ID와 댓글 작성자의 ID를 비교하여 일치할 경우에만 삭제합니다.
        if (comment.getMember().getId().equals(currentMemberId)) {
            commentRepository.delete(comment);
        } else {
            throw new IllegalArgumentException("You don't have permission to delete this comment.");
        }
    }

    public CommentDTO updateComment(Long commentId, CommentDTO commentDTO) {
        Comment comment = commentRepository.findById(commentId)
                .orElseThrow(() -> new IllegalArgumentException("Invalid Comment ID"));

        // 현재 로그인된 사용자의 ID를 가져옵니다.
        Long currentMemberId = SecurityUtil.getCurrentMemberId();

        // 현재 로그인된 사용자의 ID와 댓글 작성자의 ID를 비교하여 일치할 경우에만 수정합니다.
        if (comment.getMember().getId().equals(currentMemberId)) {
            comment.setText(commentDTO.getText());

            Comment updatedComment = commentRepository.save(comment);

            return modelMapper.map(updatedComment, CommentDTO.class);
        } else {
            throw new IllegalArgumentException("You don't have permission to update this comment.");
        }
    }
}

 

 

추가로  간단한 To Do List 기능도 함께 구현해보았다.

 

Todo.java

@Entity
@Getter
@Setter
@Table(name = "todos")
public class Todo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private boolean completed;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    private LocalDate dueDate;

}

TodoService.java

@Service
public class TodoService {
    private TodoRepository todoRepository;
    private MemberRepository memberRepository;

    public TodoService(TodoRepository todoRepository, MemberRepository memberRepository) {
        this.todoRepository = todoRepository;
        this.memberRepository = memberRepository;
    }

    public List<TodoDTO> getAllTodos() {
        Member member = memberRepository.findById(SecurityUtil.getCurrentMemberId()).orElse(null);
        List<Todo> todos = todoRepository.findByMember(member);
        return todos.stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
    }

    public TodoDTO createTodo(TodoDTO todoDTO) {
        Todo todo = convertToEntity(todoDTO);
        Member member = memberRepository.findById(SecurityUtil.getCurrentMemberId()).orElse(null);
        todo.setMember(member);
        Todo savedTodo = todoRepository.save(todo);
        return convertToDTO(savedTodo);
    }

    public TodoDTO updateTodo(Long id, TodoDTO todoDTO) {
        Todo todo = todoRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Invalid todo id: " + id));
        if(todoDTO.getTitle()!=null&&todoDTO.getTitle().isEmpty()) {
            todo.setTitle(todoDTO.getTitle());
        }

        if (todoDTO.isCompleted() != todo.isCompleted()) {
            todo.setCompleted(todoDTO.isCompleted());
        }
        Todo updatedTodo = todoRepository.save(todo);
        return convertToDTO(updatedTodo);
    }

    public void deleteTodo(Long id) {
        todoRepository.deleteById(id);
    }

    private TodoDTO convertToDTO(Todo todo) {
        TodoDTO todoDTO = new TodoDTO();
        todoDTO.setId(todo.getId());
        todoDTO.setTitle(todo.getTitle());
        todoDTO.setCompleted(todo.isCompleted());
        todoDTO.setDueDate(todo.getDueDate());
        todoDTO.setMemberId(todo.getId());
        todoDTO.setMemberNickname(todo.getMember().getNickname());
        return todoDTO;
    }

    private Todo convertToEntity(TodoDTO todoDTO) {
        Todo todo = new Todo();
        todo.setTitle(todoDTO.getTitle());
        todo.setCompleted(todoDTO.isCompleted());
        todo.setDueDate(todoDTO.getDueDate());
        return todo;
    }
}