본문 바로가기

카테고리 없음

Django GIS 지오펜싱

1. 지오펜싱(GeoFencing)이란?

지오펜싱(Geofencing)은 특정 지리적 영역(예: 건물, 공원, 상점 등)에 가상 경계를 설정하고, 사용자의 위치가 이 경계를 출입할 때 이벤트를 발생시키는 기술입니다.

지오펜싱을 통해 다음과 같은 기능을 구현할 수 있습니다.

사용자가 특정 지역에 도착하거나 떠날 때 알림 전송

매장 근처를 지나갈 때 할인 쿠폰 제공

출퇴근 시간에 따라 자동 출석 체크

 

Django에서는 Django GIS(Geographic Information System)를 사용하여 PostGIS 기반의 공간 데이터를 처리하고 지오펜싱을 구현할 수 있습니다.

 

Django에서 지오펜싱을 구현하는 이유

자녀의 실시간 위치를 알려주는 앱 개발 중 자녀가 특정 위치를 출입할 때 부모에게 알림을 보내야 했습니다. 부모가 특정 위치를 즐겨찾는 위치로 저장할 수 있었고 자녀가 특정 위치 반경 200M안에 들어왔는지 여부를 체크했어야 했습니다. 이와 같은 문제를 Django GIS, PostGIS를 활용하여 해결할 수 있었습니다.

 

 

2. Django에서 지오펜싱 구현을 위한 사전 준비

 

📌 2.1 Django 프로젝트 설정

 

지오펜싱을 구현하려면 Django에서 공간 데이터를 다룰 수 있도록 Django GIS를 활성화해야 합니다.

# base.py
INSTALLED_APPS = [
    'django.contrib.gis',  # GIS 기능 추가
    'rest_framework',
    'app.location',
    'app.geo_fence',
]

 

📍 데이터베이스(PostGIS) 설정

Django에서는 PostgreSQL의 PostGIS 확장 기능을 사용하여 공간 데이터를 저장합니다.

settings.py에서 PostGIS를 사용할 수 있도록 설정합니다.

DATABASES = {
    'default': {
        'ENGINE': 'django.contrib.gis.db.backends.postgis',
        'NAME': 'geofencing_db',
        'USER': 'postgres',
        'PASSWORD': 'yourpassword',
        'HOST': 'localhost',
        'PORT': '5432',
    }
}

 

📌 2.2 필요한 패키지 설치

 

다음 패키지를 설치해야 합니다.

pip install django django-rest-framework psycopg2 django-cors-headers

 

PostGIS를 사용하기 위해 PostgreSQL과 PostGIS를 먼저 설치해야 합니다.

# Ubuntu (Linux) 기준
sudo apt update
sudo apt install postgis

 

 

3. 지오펜싱을 위한 모델 설계

 

📌 3.1 Location 모델 (사용자의 현재 위치 저장)

from django.contrib.gis.db import models
from app.common.models import BaseModel

class Location(BaseModel):
    user = models.OneToOneField("user.User", on_delete=models.CASCADE)
    name = models.CharField(verbose_name="위치 이름", max_length=255, null=True, blank=True)
    address = models.CharField(verbose_name="위치 주소", max_length=128, null=True, blank=True)
    latitude = models.FloatField(verbose_name="위도")
    longitude = models.FloatField(verbose_name="경도")
    is_geofence_inside = models.BooleanField(verbose_name="지오펜싱 안에 있는지 여부", default=False)

    class Meta:
        db_table = "location"
        verbose_name = "위치"
        verbose_name_plural = verbose_name

 

저의 경우에는 Location 모델을 생성하여 주기적으로 위도와 경도를 업데이트하는 방식으로 개발하였습니다.

 

📌 3.2 GeoFence 모델 (지오펜싱 영역 설정)

from django.contrib.gis.db import models
from django.utils import timezone

class GeoFence(models.Model):
    user = models.ForeignKey("user.User", verbose_name="유저", on_delete=models.CASCADE)
    schedule = models.OneToOneField("schedule.Schedule", verbose_name="일정", on_delete=models.CASCADE)
    name = models.CharField(verbose_name="지역 이름", max_length=255)
    location = models.PointField(verbose_name="좌표")  # 중심 좌표
    boundary = models.PolygonField(verbose_name="경계")  # 지오펜스 영역
    is_active = models.BooleanField(verbose_name="활성화 여부", default=False)

    class Meta:
        db_table = "geo_fence"

DjangoGIS에서 지원하는 PolygonField와 PointField를 사용하였습니다.

참고 : https://docs.djangoproject.com/en/5.1/ref/contrib/gis/geos/

 

GEOS API | Django documentation

The web framework for perfectionists with deadlines.

docs.djangoproject.com

 

4. 지오펜싱 로직 구현 (위치 업데이트 시 검증)

 

사용자가 위치를 업데이트하면, 해당 위치가 지오펜싱 경계 안에 있는지 확인합니다.

# serializer.py

from datetime import timedelta

from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.contrib.gis.geos import Point
from django.db import transaction
from django.utils import timezone
from rest_framework import serializers

from api.v1.location.utils import send_geofence_notification
from app.geo_fence.models import GeoFence
from app.location.models import Location
from app.schedule.models import Schedule


class LocationSerializer(serializers.ModelSerializer):
    user = serializers.CharField(source="user.username", read_only=True)

    class Meta:
        model = Location
        fields = ["id", "user", "latitude", "longitude", "name", "address", "is_geofence_inside"]
        read_only_fields = ["id", "user", "is_geofence_inside"]

    def validate(self, attrs):
        attrs = super().validate(attrs)
        return attrs

    @transaction.atomic
    def create(self, validated_data):
        instance = Location.objects.create(**validated_data, user_id=self.context["view"].kwargs["child_id"])
        return instance

    @transaction.atomic
    def update(self, instance, validated_data):
        point = Point(validated_data["longitude"], validated_data["latitude"])
        today = timezone.now()
        activation_time = today + timedelta(minutes=30)
        # 오늘 날짜의 스케쥴에 관련된 지오 펜싱 핉터
        current_schedule = Schedule.objects.filter(
            start_time__lte=activation_time, end_time__gte=today, user_id=self.context["view"].kwargs["child_id"]
        ).values_list("id", flat=True)
        # 현재 유저가 지오 펜싱 반경에 위치해 있다면 푸시 알림 발송
        geo_fences = GeoFence.objects.filter(user=instance.user, schedule_id__in=current_schedule)
        for geo_fence in geo_fences:
            is_inside_geofence = geo_fence.boundary.contains(point)
            if is_inside_geofence:
                if not instance.is_geofence_inside and not geo_fence.is_active:
                    instance.is_geofence_inside = True
                    geo_fence.is_active = True
                    geo_fence.save()
                    send_geofence_notification(geo_fence, "도착")
                    break
            elif not is_inside_geofence:
                if instance.is_geofence_inside and geo_fence.is_active:
                    instance.is_geofence_inside = False
                    geo_fence.is_active = False
                    geo_fence.save()
                    send_geofence_notification(geo_fence, "떠났")
                    break
        if not geo_fences.exists() and instance.is_geofence_inside:
            instance.is_geofence_inside = False

        instance = super().update(instance, validated_data)
        channel_layer = get_channel_layer()
        async_to_sync(channel_layer.group_send)(
            f"location_group_{instance.id}",
            {
                "type": "location_message",
                "data": "location",
                "latitude": instance.latitude,
                "longitude": instance.longitude,
            },
        )

        return instance

Django Channels를 활용하여 사용자의 위치가 변경될 때 WebSocket을 통해 실시간 알림을 보내도록 구현했습니다.

 

5. 지오펜싱 생성

해당 위치의 위도 경도를 기준으로 가상의 지오펜싱을 만들어서 DB에 저장합니다.

def create_geo_fence(longitude, latitude, location_name, user_id, schedules):
    center = Point(longitude, latitude)
    distance = Distance(km=0.2)
    buffer_degrees = (distance.m / 6371000) / (math.pi / 180)
    circle = center.buffer(buffer_degrees)
    boundary = GEOSGeometry(circle).convex_hull
    if isinstance(schedules, list):
        geo_fence_list = [
            GeoFence(schedule_id=schedule.id, user_id=user_id, location=center, boundary=boundary, name=location_name)
            for schedule in schedules
        ]
        GeoFence.objects.bulk_create(geo_fence_list)
    else:
        GeoFence.objects.create(
            schedule_id=schedules.id, user_id=user_id, location=center, boundary=boundary, name=location_name
        )

 

6. 결론 및 배운 점

Django GIS와 PostGIS를 활용한 공간 데이터 관리 방법을 배울 수 있었습니다.

지오펜싱을 활용한 이벤트 트리거를 구현할 수 있었습니다.

Django Channels를 활용한 실시간 알림 시스템을 구축할 수 있었습니다.