본문 바로가기
게임서버-스파르타코딩NodeJs_7기/CH6 최종 프로젝트

위치 기반 북마크 도착 이벤트 기능 구현 계획

by GREEN나무 2025. 2. 26.
728x90

1. 개요

  • 목적: 사용자가 사전에 저장한 북마크 위치 반경 10m 내로 진입하면 도착 이벤트를 발생시키는 시스템 구현
  • 기술 스택:
    • 서버: NestJS, WebSocket
    • 클라이언트: Chrome, Kakao Maps API
    • 데이터베이스: Valkyrie (발키)

2. 주요 기능 및 흐름

1) 북마크 저장

  • 사용자가 특정 위치를 북마크로 저장하면 해당 좌표 (위도, 경도)를 Valkyrie DB에 저장

2) 사용자 위치 추적

  • 클라이언트에서 정기적으로 (setInterval) 위치 정보를 가져옴
  • Kakao Maps API 또는 Geolocation API를 활용하여 사용자의 현재 위치 확인

3) 서버와 위치 데이터 통신

  • 클라이언트 → 서버: WebSocket을 통해 현재 위치를 주기적으로 전송
  • 서버 → 클라이언트: 사용자의 위치가 북마크 반경 10m 내에 들어오면 도착 이벤트를 전송

4) 도착 여부 판별 로직

  • 서버에서 사용자 위치와 북마크 위치 간 거리 계산
  • 거리 계산 공식을 사용하여 반경 10m 이내 여부 확인
  • 도착 시 WebSocket을 통해 클라이언트에 이벤트 전송

3. 기술 상세 및 코드 예시

1) 거리 계산 (Haversine 공식 사용)

function getDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
  const R = 6371e3; // 지구 반지름 (미터)
  const φ1 = (lat1 * Math.PI) / 180;
  const φ2 = (lat2 * Math.PI) / 180;
  const Δφ = ((lat2 - lat1) * Math.PI) / 180;
  const Δλ = ((lon2 - lon1) * Math.PI) / 180;

  const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
            Math.cos(φ1) * Math.cos(φ2) *
            Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

  return R * c; // 결과값: 미터(m)
}

2) WebSocket을 이용한 위치 업데이트 (클라이언트)

const socket = new WebSocket("ws://your-server.com");

navigator.geolocation.watchPosition(
  (position) => {
    const { latitude, longitude } = position.coords;
    socket.send(JSON.stringify({ latitude, longitude }));
  },
  (error) => console.error(error),
  { enableHighAccuracy: true }
);

socket.onmessage = (event) => {
  const data = JSON.parse(event.data);
  if (data.status === "ARRIVED") {
    alert("북마크 위치에 도착했습니다!");
  }
};

3) NestJS WebSocket 서버

import { WebSocketGateway, SubscribeMessage, MessageBody, WebSocketServer } from '@nestjs/websockets';
import { Server } from 'socket.io';

@WebSocketGateway()
export class LocationGateway {
  @WebSocketServer()
  server: Server;

  private bookmarks = [
    { id: 1, lat: 37.5665, lon: 126.978, radius: 10 } // 예제 데이터 (서울)
  ];

  @SubscribeMessage('locationUpdate')
  handleLocationUpdate(@MessageBody() data: { latitude: number; longitude: number }) {
    const { latitude, longitude } = data;

    for (const bookmark of this.bookmarks) {
      const distance = getDistance(latitude, longitude, bookmark.lat, bookmark.lon);
      if (distance <= bookmark.radius) {
        this.server.emit('locationEvent', { status: 'ARRIVED', location: bookmark });
        break;
      }
    }
  }
}

4. 예상되는 오류 및 해결 방법

예상 오류 원인 해결 방법
GPS 오차로 인해 10m 이내인데 이벤트가 발생하지 않음 GPS는 5~20m 오차 발생 가능 반경을 15m로 설정하거나 GPS 정확도 향상 옵션 (enableHighAccuracy: true) 사용
클라이언트 위치 업데이트가 너무 잦아 서버 부하 발생 watchPosition이 과도하게 실행됨 위치 변화 감지 간격 조정 (setInterval로 최소 간격 설정)
WebSocket 연결이 끊어짐 네트워크 불안정 또는 서버 다운 재연결 로직 추가 (socket.onclose에서 자동 재연결)
여러 개의 북마크를 관리할 때 충돌 발생 여러 북마크와의 거리 비교 미흡 북마크별로 개별 거리 계산 후 가장 가까운 북마크 기준으로 이벤트 처리

5. 결론

  • NestJS와 WebSocket을 활용하여 실시간 위치 이벤트 감지를 구현
  • GPS 오차를 고려하여 반경을 조정하거나 정확도를 향상시키는 방식 적용
  • WebSocket 연결 관리 및 최적화된 거리 계산을 통해 성능 이슈 해결
  • 이 시스템을 기반으로 향후 퀘스트 자동 완료, 보상 지급 기능 등을 추가 가능

카카오맵 API 으로 지오펜싱(Geofencing) 기능 구현하기

일반적으로 지오펜싱은 특정 위치 반경 내에 사용자가 진입하거나 이탈했을 때 이벤트를 트리거하는 기능입니다.

1. 특정 지점과 현재 위치 간 거리 계산

카카오맵 API에서 제공하는 Coord 객체와 Coord.distance 메서드를 이용하면, 특정 좌표와 사용자의 위치 간 거리를 계산할 수 있습니다.

📌 예제 코드 (반경 100m 이내 감지)

// 사용자의 현재 위치 (예제 값)
let userLat = 37.5665;
let userLng = 126.9780;

// 지정된 지오펜싱 중심 좌표
let targetLat = 37.5670;
let targetLng = 126.9785;

// 좌표 객체 생성
let userPos = new kakao.maps.LatLng(userLat, userLng);
let targetPos = new kakao.maps.LatLng(targetLat, targetLng);

// 두 지점 사이 거리 계산 (미터 단위)
let distance = kakao.maps.geometry.computeDistance(userPos, targetPos);

console.log("현재 위치와 목표 지점 간 거리:", distance, "m");

// 반경 100m 이내인지 확인
if (distance <= 100) {
    alert("목표 지점 반경 100m 이내에 도착했습니다!");
}

computeDistance() 함수를 사용하여 거리 계산을 수행하고, 일정 반경 내에 들어왔을 때 이벤트를 트리거할 수 있습니다.


2. 실시간 위치 추적하여 지오펜싱 감지

사용자가 이동할 때마다 지오펜싱을 감지하려면, HTML5의 navigator.geolocation.watchPosition()을 활용하면 됩니다.

📌 예제 코드 (실시간 위치 추적)

navigator.geolocation.watchPosition(
    function (position) {
        let userLat = position.coords.latitude;
        let userLng = position.coords.longitude;

        let userPos = new kakao.maps.LatLng(userLat, userLng);
        let targetPos = new kakao.maps.LatLng(37.5670, 126.9785);

        let distance = kakao.maps.geometry.computeDistance(userPos, targetPos);

        console.log("현재 거리:", distance, "m");

        if (distance <= 100) {
            console.log("목표 지점 반경 100m 이내 진입!");
        }
    },
    function (error) {
        console.error("위치 정보를 가져오는 데 실패했습니다.", error);
    },
    {
        enableHighAccuracy: true,
        maximumAge: 10000,
        timeout: 5000
    }
);

✅ 사용자의 위치가 갱신될 때마다 지오펜싱 여부를 체크합니다.


3. 다각형 지오펜싱 (다중 좌표)

원형 지오펜싱(반경) 대신, 다각형 지오펜싱을 설정하고 특정 지역 안에 사용자가 있는지 확인할 수도 있습니다.

📌 예제 코드 (다각형 내부 확인)

// 지오펜싱 영역 설정 (다각형 좌표 리스트)
let geofenceCoords = [
    new kakao.maps.LatLng(37.5671, 126.9775),
    new kakao.maps.LatLng(37.5675, 126.9780),
    new kakao.maps.LatLng(37.5670, 126.9790),
    new kakao.maps.LatLng(37.5665, 126.9785)
];

// 다각형 생성
let polygon = new kakao.maps.Polygon({
    path: geofenceCoords
});

// 사용자의 현재 위치 (예제 값)
let userPos = new kakao.maps.LatLng(37.5672, 126.9782);

// 사용자가 다각형 내부에 있는지 확인
if (kakao.maps.Polygon.contains(polygon, userPos)) {
    console.log("사용자가 지오펜싱 내에 있습니다!");
} else {
    console.log("사용자가 지오펜싱 바깥에 있습니다.");
}

✅ kakao.maps.Polygon.contains() 함수를 사용하면 다각형 내부에 사용자가 있는지 확인할 수 있습니다.


📌 정리

기능 구현방법
반경 기반 지오펜싱 computeDistance()로 거리 측정
실시간 위치 추적 navigator.geolocation.watchPosition() 사용
다각형(폴리곤) 기반 지오펜싱 kakao.maps.Polygon.contains() 활용

 



10초마다 Redis(벨키)에 업데이트되는 사용자 위치 정보를 활용하여 지오펜싱을 구현하는 방법. (백엔드 : nestJs)


📌 0. 시스템 개요

  • 프론트엔드 (카카오맵 API)
    • navigator.geolocation.watchPosition()을 이용해 10초마다 사용자의 위치를 갱신
    • 위치 데이터를 NestJS API에 전송
  • 백엔드 (NestJS + Redis)
    • 사용자 위치를 Redis에 저장 (SET user:{id}:location lat,lng)
    • 주기적으로 Redis에 저장된 위치를 불러와 특정 지점과 거리 비교
    • 지오펜싱 영역 내 진입 시 이벤트 발생 (DB 저장, 알림 전송 등)

📌 1. 주기적으로 사용자 위치 확인 (지오펜싱 적용)

10초마다 Redis에 저장된 유저 위치를 확인하고, 특정 지점 반경 내에 들어오면 이벤트를 발생시키는 작업을 한다.

📌 특정 좌표와 거리 비교 (location.service.ts 수정)

import * as haversine from 'haversine-distance';

const TARGET_LOCATION = { lat: 37.5670, lng: 126.9785 }; // 지오펜싱 타겟 위치
const RADIUS = 100; // 반경 100m

async checkGeofencing(userId: string) {
  const userLocation = await this.getUserLocation(userId);
  if (!userLocation) return false;

  const distance = haversine(TARGET_LOCATION, userLocation);
  console.log(`사용자 ${userId} 거리: ${distance}m`);

  if (distance <= RADIUS) {
    console.log(`✅ 사용자 ${userId}가 지오펜싱 영역 내에 있습니다!`);
    return true;
  }
  return false;
}

✅ haversine-distance 패키지를 이용해 거리 계산 (npm install haversine-distance)


📌 2. 크론 작업으로 자동 검사 (geofencing.task.ts)

NestJS에서 @nestjs/schedule을 사용하면 일정 주기로 Redis의 사용자 위치를 확인할 수 있다.

📌 @nestjs/schedule 설치

npm install @nestjs/schedule

📌 주기적 작업 추가 (geofencing.task.ts)

import { Injectable } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { LocationService } from './location.service';

@Injectable()
export class GeofencingTask {
  constructor(private readonly locationService: LocationService) {}

  @Cron('*/10 * * * * *') // 10초마다 실행
  async checkUserLocations() {
    const userIds = ['123', '456']; // Redis에서 전체 유저 리스트를 가져오는 방식도 가능
    for (const userId of userIds) {
      const isInFence = await this.locationService.checkGeofencing(userId);
      if (isInFence) {
        console.log(`🎯 ${userId}이(가) 지오펜싱 내부에 있습니다!`);
        // 여기에 알림 전송 로직 추가 가능 (Firebase, WebSocket 등)
      }
    }
  }
}

✅ 10초마다 Redis에 저장된 사용자 위치를 검사하여 지오펜싱 범위 내에 있는지 확인


📌 3. 정리

단계 구현 방법
1. 프론트엔드 10초마다 위치 전송 (navigator.geolocation.getCurrentPosition)
2. 백엔드 저장 NestJS + Redis로 사용자 위치 저장
3. 지오펜싱 검사 Haversine 거리 계산 후 반경 내 여부 확인
4. 자동 실행 @nestjs/schedule로 10초마다 검사
5. 알림 WebSocket / Firebase로 알림 전송 가능

➡️ NestJS + Redis를 활용하면 실시간 지오펜싱을 효율적으로 구현할 수 있음 🚀

 

 


 

 

 


참고하면 좋을 링크

 

재능- 실시간 위치 기반 서비스: 지오펜싱과 맵 API 활용: https://www.jaenung.net/tree/7647

우아한 기술 블로그 - Hello, Geo-fence! : https://techblog.woowahan.com/2567/