[캡스톤] 기능 개발 - 알림 기능 - 로직1

이신행·2024년 6월 25일

capstone

목록 보기
10/15
post-thumbnail

알림 기능 구분

우선 저희 프로젝트에서는 알림의 송수진자의 역할에 따라 3가지의 알림으로 구분했습니다.

  1. 피보호자 → 보호자
    • 피보호자의 위치에 따라 자동으로 보호자에게 보내는 알림
    • 피보호자가 위험 상황에서 직접 보호자에게 보내는 알림
  2. 헬퍼 → 보호자
    • 헬퍼(주로 선생님)가 보호자에게 피보호자의 위치를 확인해주는 알림
  3. 보호자 → 주변 멤버
    • 보호자가 피보호자와 연락이 되지 않을 때, 피보호자의 주변 다른 멤버들에게 전송하는 알림

1. 피보호자 → 보호자 (Notice)

Notice

저희 프로젝트에서는 3가지로 구역을 나눴습니다.

  1. 위험구역
  2. 안전구역
  3. 중립구역 (사용자가 아무것도 설정하지 않은 경우)

피보호자가 각 구역 간의 이동이 발생할 때 마다 알림을 보내는 로직을 구현했습니다.
피보호자의 현재 위치에 따른 구역을 계산해서 직전 구역과 다른 경우에 알림을 전송합니다.

아래 코드의 로직 순서입니다.
아래의 순서를 따라 알림을 보낼 지 말지 결정합니다.

  1. updateCoordinate (위치 갱신)
  2. sendNotice (알림을 전송 할 지 결정)
    1. getCurrentStatus (현재 위치 확인)
      1. isPointInPolygon (위치로 구역 확인)
  3. sendNoticeToMember (멤버에게 전송할 메시지 생성)
  4. sendNotificationTo (멤버에게 알림 전송)
  5. SendNotificationByToken (FCM을 이용한 전송)

구현

가독성을 위해 일부 코드를 수정, 삭제했습니다. 코드 전문은 아래의 깃허브에서 확인하실 수 있습니다. (특히 optional null check)

Controller

// 위치에 따라 보내야하기 때문에 필요한 위치를 갱신하는 메서드 
@PostMapping("/update-coordinate")
public ResponseEntity<Map<String, String>> updateCoordinate(@RequestBody UpdateCoordinateDTO dto) {
    Map<String, String> result = new HashMap<>();
	// 일반 멤버의 경우
    if (dto.getType().equals("Member")) {
        if (memberService.updateMemberCoordinate(dto.getId(), dto.getLatitude(), dto.getLongitude())) {
            return addOkStatus(result);
        }
        return addErrorStatus(result);
    }
    // 피보호자의 경우 
    if (memberService.updateChildCoordinate(dto.getId(), dto.getLatitude(), dto.getLongitude())) {
        noticeController.sendNotice(dto.getId());
        return addOkStatus(result);
    }
    return addErrorStatus(result);
}

Service

// Notice를 보내는 메서드
@Transactional
public void sendNotice(String childName) {
    Child foundChild = memberService.findChildByChildName(childName);
    String currentStatus = getCurrentStatus(foundChild);
    String lastStatus = foundChild.getLastStatus();

    List<Parenting> childParentingList = foundChild.getParentingList();
    // 구역 변경 시 FCM 메시지 전송
    if (currentStatus.equals("위험구역") && (!lastStatus.equals("위험구역"))) {
        if (! sendNoticeToMember(childParentingList, foundChild.getChildName(), NoticeLevel.WARN)) {
            return;
        }
        // 마지막 상태 갱신
        foundChild.setLastStatus(currentStatus);
    } 
    else if (!lastStatus.equals(currentStatus)) {
        if (! sendNoticeToMember(childParentingList, foundChild.getChildName(), NoticeLevel.INFO)) {
            return;
        }
        // 마지막 상태 갱신
        foundChild.setLastStatus(currentStatus);
    }
}

// 구역 상태를 체크 해주는 메서드
@Transactional
public String getCurrentStatus(Child foundChild) {
    double[] childPosition = {foundChild.getLatitude(), foundChild.getLongitude()};
		// 각 구역을 사각형으로 설정하고, 각 점을 이용해 구역 내부에 있는지 확인
    ArrayList<Coordinate> coordinateArrayList = coordinateRepository.findAllByChild(foundChild);
    for (Coordinate coordinate : coordinateArrayList) {
        double[][] polygon = {
                {coordinate.getYOfNorthWest(), coordinate.getXOfNorthWest()},
                {coordinate.getYOfNorthEast(), coordinate.getXOfNorthEast()},
                {coordinate.getYOfSouthEast(), coordinate.getXOfSouthEast()},
                {coordinate.getYOfSouthWest(), coordinate.getXOfSouthWest()}
        };

        if (coordinate.isLivingArea()) {
            if (isPointInPolygon(polygon, childPosition)) {
            // 안전 구역 내부에 있는 경우
                return "안전구역";
            }
        } else {
            if (isPointInPolygon(polygon, childPosition)) {
            // 위험 구역 내부에 있는 경우
                return "위험구역";
            }
        }
    }
    return "일반구역";
}

// 구역 내부에 있는지 확인하는 메서드
public static boolean isPointInPolygon(double[][] polygon, double[] point) {
    int n = polygon.length;
    double px = point[0], py = point[1];
    boolean inside = false;

    for (int i = 0, j = n - 1; i < n; j = i++) {
        double ix = polygon[i][0], iy = polygon[i][1];
        double jx = polygon[j][0], jy = polygon[j][1];

        if ((iy > py) != (jy > py) &&
                (px < (jx - ix) * (py - iy) / (jy - iy) + ix)) {
            inside = !inside;
        }
    }
    return inside;
}

@Transactional
public boolean sendNoticeToMember(List<Parenting> parentingList, String childName, NoticeLevel noticeLevel) {
    for (Parenting parenting : parentingList) {
        Notice notice = noticeService
                .createNotice(
                        parenting.getParent().getMemberId(),
                        childName,
                        noticeLevel);
        noticeService.sendNotificationTo(notice);
    }
    return true;
}

public boolean sendNotificationTo(Notice notice){
    FCMNotificationDTO message = makeMessage(notice);
    // SendNotificationByToken는 이전 포스트에 있습니다.
    return fcmService.SendNotificationByToken(message) != null;
}

마무리

Notice 부분에서 아이의 위치를 DB에 저장하는 건 너무 많은 오버헤드를 가집니다.
실제로 앱이 상용화 되려면 수정되어야 할 부분입니다.
분량 조절 실패로 다음 포스트에서 나머지 알림 기능에 대한 로직이 이어집니다…

코드 깃허브

https://github.com/LeeShinHaeng/safeGuard

profile
언제나 Response 하는 Ability가 있는 서버를 만드는, Responsibility 있는 개발자가 되자

0개의 댓글