본문 바로가기

위치 데이터의 새로운 얼굴 — 직접 만지는 나만의 인터랙티브 지도 플랫폼

728x90

  • “데이터가 길이 되는 순간 — 여행·일상 경로의 웹 시각화 UX 가이드”

웹 UI 형태로 제공되는 위치 시각화 서비스의 설계·디자인 가이드입니다. 목표는 사용자가 직접 UI를 조정하여 “원하는 스토리로 지도를 구성”할 수 있도록 하는 것—즉, 가시성(visibility)·조작성(control)·프라이버시(safety) 를 모두 만족시키는 제품화 가능한 UX 청사진입니다.

제품 목표 & 핵심 원칙

  1. 즉시 이해: 첫 5초 안에 “어디를, 언제, 어떻게 움직였는지” 감 잡기
  2. 직접 조정: 레이어·시간·클러스터링·프라이버시 모호화 강도를 사용자 손으로 튜닝
  3. 스토리텔링: 경로 재생·하이라이트·노트·사진을 엮어 여행기/보고서 생성
  4. 안전 기본값: 민감 위치 비노출, 데이터 범위 제한, 공유 전 확인 디폴트 ON
  5. 반응형 & 접근성: 모바일·데스크톱 모두 최적, 키보드/스크린리더 지원

페르소나 & 대표 여정(User Journey)

  1. 개인 기록가: 사진·경로를 섞어 “한 주/여행” 스토리맵 완성 → 링크 공유
  2. 운영 관리자: 특정 기기/기간 필터링 → 머무름 패턴 확인 → 보고용 이미지/CSV export
  3. 팀 리더: 팀별 경로 비교, 프라이버시 레벨 한 번에 강제 → 외부 공유용 뷰 생성

정보구조(IA) & 주요 화면

  1. 대시보드
    • 상단: 날짜 범위·기기 선택·프라이버시 레벨(보호 강도)
    • 중앙: 지도(메인 캔버스)
    • 우측 패널: 레이어/스타일/분석/공유
    • 하단: 타임바(재생/속도/구간 선택)
  2. 스토리 모드
    • 단계별 씬(Scene) 카드(예: “공항 → 호텔 → 명소 A”)
    • 씬별 애니메이션·자막·사진/노트 배치, 공유 프리셋
  3. 관리자 설정
    • 역할/권한, 프라이버시 정책(모호화 반경·보존기간), 브랜드(로고/테마), 감사 로그

지도 캔버스 — 인터랙션 패턴(코드 無)

  1. 기본 도구
    • 드래그/줌, 나침반/스케일바, 현재 위치 버튼
    • 툴팁(hover): 시점별 속도·정확도, 클릭 시 상세(사진·노트·주소)
    • 선택(브러싱): 지도에서 드래그로 구간 선택 → 하단 타임바에도 범위 동기화
  2. 타임 컨트롤
    • 재생/일시정지, 속도(0.5×~8×), 구간 루프, 스냅(일/시간/15분 단위)
    • 이벤트 스냅: 사진 촬영 시각·머무름 시작/종료·경로 분기점에 자동 정렬
  3. 레이어 토글
    • 경로(Line)·포인트(Point)·머무름(Polygon or Marker)·클러스터·히트맵
    • 정확도 원(circle): GPS 오차 시각화 ON/OFF
    • 지도 스타일: 기본/다크/야간 드라이브/고대비(접근성)
  4. 컨텍스트 퀵액션
    • “여기서 씬 만들기”, “이 구간 공유하기”, “노트 추가”, “민감 장소로 지정(자동 모호화)”

사용자 조정(컨트롤 패널) — 라벨·범위·설명 복기

탭 1. 레이어

  • 경로 두께: 1–6
  • 경로 투명도: 0–100%
  • 포인트 표시: ○ 표시/작게/보통/크게
  • 클러스터링: Off / 자동 / 수동(반경 슬라이더)
  • 히트맵 강도: 0–100
  • 정확도 원 표시: On/Off (범례에 “±m” 표시)

탭 2. 시간

  • 날짜 범위: 시작–종료(빠른 선택: 오늘/어제/지난 7일/여행 기간)
  • 시간 필터: 주간/야간/출퇴근
  • 재생 옵션: 속도, 이벤트 스냅, 경로 끊김 보정(스무싱) 강도

탭 3. 스타일/테마

  • 테마: 라이트/다크/고대비/브랜드
  • 포인트 색상 매핑: 시간대/기기/속도/정확도 중 선택
  • 경로 색상 매핑: 속도 그라데이션 / 고도 그라데이션
  • 지도 스타일: 기본/심플/위성(있다면)/모노톤
  • 라벨 모드: 레이블 최소 확대레벨, 중복 겹침 방지

탭 4. 분석

  • 머무름 탐지: 반경(m) 50–200 / 최소 체류 5–30분
  • 주요 장소(POI) 군집: 클러스터 민감도(낮음–높음)
  • 프리셋: 도심/교외/등산 모드(자동 파라미터)

탭 5. 프라이버시

  • 민감 위치(집/직장/자주 방문) 자동 모호화: Off/약/중/강(반경 안내)
  • 좌표 정밀도: 정수 ~ 소수점 5자리(도메인별 권장값 안내)
  • 시간 버킷: 5/15/30/60분
  • 공유 전 검토: 민감 영역 자동 마스킹 미리보기

탭 6. 공유/내보내기

  • 공유 범위: 현재 뷰/선택 구간/특정 씬
  • 만료: 1일/1주/1개월/커스텀
  • 워터마크·저작권 라벨
  • Export: 이미지(PNG), 벡터(SVG), 데이터(GeoJSON/CSV), 애니메이션(MP4/GIF)

구성 요소(컴포넌트) 체크리스트

  1. 헤더 바: 로고/프로젝트 선택, 빠른 검색(장소·주소·사진 파일명), 사용자 메뉴
  2. 필터 바: 기간, 기기(멀티 선택), 활동 유형(걷기/차량), 프라이버시 레벨
  3. 지도 카드: 전경(맵), 배지(총 거리·시간·머무름 수), 상태(로드 중/오류/빈 데이터)
  4. 우측 패널: 탭+아코디언 패턴, 도움말(“i” 아이콘)
  5. 타임바: 줌 가능한 미니맵 + 분포 히스토그램(데이터 밀도, 사진 이벤트 표시)
  6. 씬 카드: 제목, 서브타이틀(장소·시간), 썸네일(지도 캡처/사진 1장), 재생 버튼
  7. 프라이버시 경고 바: 공유 직전, 민감 위치 포함 여부 안내 + 자동 수정 버튼

디자인 시스템(토큰·스타일 가이드)

  1. 컬러 토큰
    • Primary / Accent / Success / Warning / Danger / Info
    • 지도 대비 고려: 경로(고채도), 머무름(반투명 배경), 포인트(화이트 스트로크)
    • 색맹 친화 팔레트: 빨강–초록 대비 회피, 청색·주황 조합 선호
  2. 타이포그래피
    • 제목: 18–22px, 본문: 14–16px, 캡션: 12–13px
    • 숫자 탭/배지: 모노스페이스(거리·시간 정렬 정교함)
  3. 아이코노그래피
    • 경로(↝), 머무름(⦿), 재생(▶), 스냅(🧲), 프라이버시(🛡️)
    • 상태: 로딩(스켈레톤), 빈 상태(친절한 안내), 오류(해결 가이드 링크)
  4. 모션
    • 경로 재생: ease-in-out(400–800ms), 포인트 등장 페이드(150ms)
    • 과도한 애니메이션 금지(현기증·가시성 저하 예방)
  5. 레이아웃
    • 12 컬럼 그리드, 최소 여백 16px(모바일 12px), 카드 라운드 10–16px
    • 패널 폭 조절 가능(맵 우선/패널 우선 토글)

접근성 & 국제화

  1. 키보드 네비게이션: 탭 순서, 포커스 링 명확, 단축키(재생 P, 정지 S, 줌 +/-)
  2. 명도 대비: 텍스트 4.5:1 이상, 지도 위 라벨은 외곽선/그림자 적용
  3. 대체 텍스트: 사진·아이콘 설명, 스크린리더용 힌트(예: “머무름 12분”)
  4. 언어·단위: 한국어/영어, 거리(km/mi), 시간 표기(24h/AM-PM), 날짜 지역화

프라이버시·보안 — UI 측면 특별 설계

  1. 보호 레벨 프리셋: Off/약/중/강(모호화 반경·시간 버킷·정밀도 세트)
  2. 민감 장소 자동 탐지: “집/직장/자주” 배지 표시 → 공유 시 자동 흐림/마스킹
  3. 공유 미리보기: 외부에 보이는 최종 뷰를 그대로 프리뷰(내부와 불일치 방지)
  4. 보안 배너: 외부 공유 링크에 만료·조회수 제한·비밀번호 옵션
  5. 감사 로그: “누가/언제/무엇을” 열람/내보내기 했는지 관리자 화면에서 조회
  6. 역할 기반 권한: 개인용, 팀 열람용, 관리자(정책 강제/내보내기 제한)

측정 지표 & 실험(A/B) 아이디어

  1. 핵심 지표:
    • 첫 유의미 뷰까지 시간(TTFV), 장면(씬) 생성율, 공유율, 재방문율
    • 프라이버시 변경 후 공유율(안전 기본값의 UX 저항 최소화 지표)
  2. A/B 아이디어:
    • 타임바 유형(히스토그램 vs 축만)
    • 프라이버시 프리셋 노출 위치(헤더 vs 패널 상단)
    • 경로 색상 매핑(속도 vs 시간대) 선호

콘텐츠 & 마이크로카피(예시 문구)

  • 빈 상태(처음 진입)
    “기간을 선택하거나 데이터를 불러오면 이동 경로가 여기에 나타납니다.”
    버튼: “기간 선택”, “샘플 데이터 보기”
  • 공유 전 경고
    “민감 위치 2곳이 포함되어 있습니다. 자동 모호화를 적용할까요?” [적용] [그대로 공유]
  • 툴팁(포인트)
    “오전 9:12 · ±12m · 4.2km/h · 사진 1장 연결됨”
  • 오류(데이터 없음)
    “선택한 기간에 데이터가 없습니다. 기간을 늘리거나 장치를 바꿔 보세요.”

예시 워크플로(코드 無)

  1. 여행 일지 만들어 공유
    • 날짜 범위 = 여행 기간 → 경로 색상=시간대 → 머무름=중간 강도 →
    • 씬 5개(이동/숙소/명소) → 각 씬에 사진 1~3장, 설명 1줄 →
    • 공유: 만료 7일, 워터마크 ON
  2. 한 달 이동 패턴 리포트
    • 범위=지난 30일, 클러스터 자동 → 머무름 15분 이상만 →
    • 프라이버시=중 (집/직장 모호화 300m) →
    • Export: PNG(대시보드 표지), CSV(머무름 목록)
  3. 팀 비교(운영 관제)
    • 기기=팀 A/B 비교, 시간=업무시간만 →
    • 히트맵 강도=중, 정확도 원 표시=On →
    • 관리자 화면에서 내보내기 제한 정책 적용

운영 가드레일(프런트 기준)

  • 대용량 처리 시 단계적 렌더링(스켈레톤 → 단순화 → 정밀)
  • 모바일에서 패널 오버레이 모드(풀스크린 패널 스와이프)
  • 지도 상 과밀도 경고(히트맵 권장)
  • UI 저장: 사용자별 프리셋(마지막 레이어/스타일 기억)
  • 다크 모드 자동 전환(시스템 따라가기)

내 이동 경로·주요 장소·여행지를 “자동으로” 지도에 그리는 설계서

스마트폰/사진/앱 로그 등 다양한 위치 데이터를 자동 수집 → 정규화 → 분석(머무름/핵심 장소·경로 보정) → 시각화 → 배포까지 이어지는 엔드투엔드(End-to-End) 파이프라인보안·프라이버시 가이드와 함께 “바로 실행 가능한 예시”로 정리한 안입니다.

목표와 전체 아키텍처

  1. 데이터 소스: 스마트폰 백그라운드 위치, 사진(EXIF), 러닝/등산 앱 GPX, Google 타임라인 백업 JSON, 차량·워치·IoT(MQTT) 등
  2. 수집(ingest): 모바일 앱 → MQTT/HTTPS 업로드, 사진 폴더 동기화, Takeout/백업 파일 업로드
  3. 저장/정규화: 공통 스키마(GeoJSON/CSV/GPX) → DB(PostgreSQL + PostGIS, 선택: TimescaleDB)
  4. 분석
    • 머무름(Staypoint)핵심 장소(POI) 군집화(DBSCAN/HDBSCAN)
    • 맵 매칭(Map-Matching) 으로 GPS 흔들림 보정(OSRM/Valhalla)
    • 역지오코딩(주소/행정구/카테고리 라벨링)
  5. 시각화/배포
    • 빠른 분석: Kepler.gl 타임플레이백
    • 웹앱: MapLibre GL JS + (선택) 자체 벡터 타일 서버(tippecanoe + tileserver-gl)
  6. 자동화/운영: 배치(Cron/GitHub Actions), 알림, 백업/보안/프라이버시

텍스트 다이어그램(개념)

[Phone/Watch/Car/Photos/GPX/JSON]
         │
     (MQTT/HTTPS/SFTP/Sync)
         │
   [Ingest Service]──→ [Raw Store]
         │                 │
   Normalize/Validate      │
         │                 ▼
      [PostGIS/TimescaleDB(옵션)]
         │
  Staypoints/POI/Match/Reverse Geocode
         │
   [GeoJSON/MBTiles/Cache]
         │
 [Kepler.gl]   [tileserver-gl → MapLibre 웹앱]

위치 데이터 소스(현실적인 선택지)

  1. 스마트폰 실시간 스트림(개인 프라이버시 중심)
    • 예: OwnTracks(안드로이드/iOS) → MQTT 브로커(Mosquitto) 에 주기적으로 위치 Publish
    • 서버에서 Recorder 또는 자체 Ingest API가 받아 DB에 적재
  2. 저전력 로그 앱
    • 예: GPSLogger(오픈소스) → GPX/KML/CSV/NMEA 생성 → SFTP/웹훅 자동 업로드
  3. Google 타임라인(기기 내 저장/백업)
    • 백업/내보내기 JSON → 변환 스크립트로 표준 스키마에 적재
  4. 사진(EXIF) 기반
    • 사진의 GPS/시간 메타데이터로 머무름/경로 재구성(특히 여행에 최적)
  5. 운동/등산 앱 GPX
    • GPX 트랙을 정기 업로드 → 경로 레이어로 즉시 시각화

저장소와 공통 스키마

스키마(권장 컬럼)

  • points: id, device_id, ts, lat, lon, alt, speed, accuracy, source, geom(POINT)
  • stays: id, device_id, start_ts, end_ts, duration_sec, centroid(GEOMETRY), poi_label
  • trips: id, device_id, start_ts, end_ts, distance_m, path(LINESTRINGZ/M)
  • pois: id, label, category, centroid, first_seen, last_seen
  • photos: id, file, taken_at, lat, lon, geom
300x250

PostGIS/TimescaleDB 생성 예시

CREATE EXTENSION IF NOT EXISTS postgis;
-- (옵션) TimescaleDB: CREATE EXTENSION IF NOT EXISTS timescaledb;

CREATE TABLE points (
  id BIGSERIAL PRIMARY KEY,
  device_id TEXT NOT NULL,
  ts TIMESTAMPTZ NOT NULL,
  lat DOUBLE PRECISION NOT NULL,
  lon DOUBLE PRECISION NOT NULL,
  alt DOUBLE PRECISION,
  speed DOUBLE PRECISION,
  accuracy DOUBLE PRECISION,
  source TEXT,
  geom GEOMETRY(POINT, 4326) GENERATED ALWAYS AS (ST_SetSRID(ST_MakePoint(lon,lat),4326)) STORED
);
CREATE INDEX idx_points_ts ON points(ts);
CREATE INDEX idx_points_geom ON points USING GIST(geom);

-- (옵션) 시계열 최적화
-- SELECT create_hypertable('points','ts', if_not_exists => TRUE);

CREATE TABLE trips (
  id BIGSERIAL PRIMARY KEY,
  device_id TEXT NOT NULL,
  start_ts TIMESTAMPTZ NOT NULL,
  end_ts TIMESTAMPTZ NOT NULL,
  distance_m DOUBLE PRECISION,
  path GEOMETRY(LINESTRING, 4326)
);

CREATE TABLE stays (
  id BIGSERIAL PRIMARY KEY,
  device_id TEXT NOT NULL,
  start_ts TIMESTAMPTZ NOT NULL,
  end_ts TIMESTAMPTZ NOT NULL,
  duration_sec INTEGER NOT NULL,
  centroid GEOMETRY(POINT, 4326),
  poi_label TEXT
);

수집(ingest)과 정규화

Docker Compose(핵심 구성 예시)

version: "3.9"
services:
  mqtt:
    image: eclipse-mosquitto:2
    ports: ["1883:1883"]
    volumes:
      - ./mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
      - ./mosquitto_pwd:/mosquitto/config/password_file:ro
  db:
    image: postgis/postgis:16-3.4
    environment:
      POSTGRES_PASSWORD: strong_pass
      POSTGRES_DB: geo
    ports: ["5432:5432"]
    volumes: [ "pgdata:/var/lib/postgresql/data" ]
  ingest:
    image: python:3.12
    command: uvicorn app:app --host 0.0.0.0 --port 8080
    working_dir: /app
    volumes: [ "./ingest:/app" ]
    environment:
      DATABASE_URL: postgresql://postgres:strong_pass@db:5432/geo
    ports: ["8080:8080"]
volumes: { pgdata: {} }

Ingest API(FastAPI 예시)

# ./ingest/app.py
from fastapi import FastAPI
from pydantic import BaseModel
import asyncpg, os

app = FastAPI()
DB = os.getenv("DATABASE_URL")

class Point(BaseModel):
    device_id: str
    ts: str       # ISO8601
    lat: float
    lon: float
    alt: float | None = None
    speed: float | None = None
    accuracy: float | None = None
    source: str | None = "mobile"

@app.on_event("startup")
async def startup():
    app.state.pool = await asyncpg.create_pool(dsn=DB)

@app.post("/ingest")
async def ingest(p: Point):
    q = """INSERT INTO points(device_id, ts, lat, lon, alt, speed, accuracy, source)
           VALUES ($1,$2,$3,$4,$5,$6,$7,$8)"""
    async with app.state.pool.acquire() as conn:
        await conn.execute(q, p.device_id, p.ts, p.lat, p.lon, p.alt, p.speed, p.accuracy, p.source)
    return {"ok": True}

사진(EXIF) → CSV/GeoJSON

# EXIF → CSV (파일/촬영시간/좌표 추출)
exiftool -api GeoMaxInt=6 -n -csv -r \
  -Filename -DateTimeOriginal -GPSLatitude -GPSLongitude -GPSAltitude \
  /photos > photos.csv

분석 로직(머무름/POI/맵 매칭/역지오코딩)

머무름(Staypoint) & POI 군집(DBSCAN)

  • 규칙: 반경 R(예: 100m) 안에서 T분(예: 10분) 이상 머무르면 머무름으로 간주
  • 여러 머무름을 DBSCAN으로 묶어 핵심 장소(POI) 도출
import pandas as pd, numpy as np
from sklearn.cluster import DBSCAN

R = 6371000.0
def haversine(a,b):
    lat1,lon1,lat2,lon2 = np.radians([a[0],a[1],b[0],b[1]])
    dlat, dlon = lat2-lat1, lon2-lon1
    h = np.sin(dlat/2)**2 + np.cos(lat1)*np.cos(lat2)*np.sin(dlon/2)**2
    return 2*R*np.arcsin(np.sqrt(h))

df = pd.read_csv("points.csv")   # columns: lat, lon, ts, device_id
coords = df[['lat','lon']].to_numpy()
db = DBSCAN(eps=100, min_samples=4, metric=lambda x,y: haversine(x,y)).fit(coords)
df['cluster'] = db.labels_
stays = (df[df.cluster!=-1]
         .groupby('cluster')
         .agg(lat=('lat','mean'), lon=('lon','mean'),
              start=('ts','min'), end=('ts','max'), n=('ts','count')))

경로 보정(Map-Matching)

  • OSRM/Valhalla 등의 엔진을 로컬 컨테이너로 띄워, 원시 GPS 포인트를 도로망에 “스냅”
  • 컨테이너(OSRM) 쉬운 실행 예시
    # 1) 최신 PBF 지도 다운로드(관심 지역)
    # 2) 추출/파티션/커스터마이즈
    docker run -t -v $PWD:/data osrm/osrm-backend osrm-extract -p /opt/car.lua /data/region.osm.pbf
    docker run -t -v $PWD:/data osrm/osrm-backend osrm-partition /data/region.osrm
    docker run -t -v $PWD:/data osrm/osrm-backend osrm-customize /data/region.osrm
    # 3) 라우트 서버 오픈
    docker run -it -p 5000:5000 -v $PWD:/data osrm/osrm-backend osrm-routed --algorithm mld /data/region.osrm
  • 매칭 API 호출 예
    curl "http://localhost:5000/match/v1/driving/127.0,37.5;127.001,37.501?geometries=geojson&overview=full"

역지오코딩(주소·행정동·카테고리 라벨)

  • Nominatim 등 오픈소스 엔진을 자가 호스팅하고 비동기 큐로 일괄 처리(레이트 제한 준수)
  • 결과는 stays.poi_label, pois.category 등에 저장

시각화(Kepler.gl / MapLibre / 벡터 타일)

Kepler.gl(드래그&드롭 분석, 타임플레이백)

  • Trip Layer: [lon, lat, alt, epochSec] 형식으로 준 LineString GeoJSON을 넣으면 시간대별 이동 애니메이션 가능
# points.csv: id(트립 id), ts, lon, lat
import pandas as pd, json
df = pd.read_csv("points.csv")
df['t'] = pd.to_datetime(df.ts).astype('int64') // 10**9
features=[]
for tid,g in df.sort_values('t').groupby('id'):
    coords=[[r.lon, r.lat, 0, int(r.t)] for r in g.itertuples()]
    features.append({"type":"Feature","properties":{"id":tid},
                     "geometry":{"type":"LineString","coordinates":coords}})
open("trips.geojson","w").write(json.dumps({"type":"FeatureCollection","features":features}))
  • 바로 보기: 브라우저에서 kepler.gl 열고 trips.geojson 드래그

 

MapLibre GL JS(가벼운 자체 웹앱)

<!doctype html><html><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css" rel="stylesheet">
<style>html,body,#map{height:100%;margin:0}</style></head><body>
<div id="map"></div>
<script src="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js"></script>
<script>
const map = new maplibregl.Map({
  container:'map',
  style:{version:8, sources:{osm:{type:'raster',
    tiles:['https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'], tileSize:256}},
    layers:[{id:'osm',type:'raster',source:'osm'}]
  },
  center:[127.0,37.5], zoom:11
});
map.on('load', ()=>{
  map.addSource('route',{type:'geojson',data:'trips.geojson'});
  map.addLayer({id:'route-line',type:'line',source:'route',
    paint:{'line-width':3,'line-opacity':0.85}});
});
</script></body></html>

대용량을 위한 “자체 벡터 타일” 파이프라인

# GeoJSON → MBTiles(Vector Tiles)
tippecanoe -o trips.mbtiles -zg --drop-densest-as-needed trips.geojson
# 타일 서버 실행
docker run -it -v $PWD:/data -p 8080:8080 maptiler/tileserver-gl /data/trips.mbtiles
  • MapLibre에서 style.json에 위 타일 소스를 추가하면 대용량 렌더링/스타일 변경이 유연해집니다.

자동화(배치/스케줄/CI)

하루 배치(개념 스텝)

  1. 00:10 수집 집계: 전일 points 파티션 완료
  2. 00:20 맵 매칭: 원시 포인트 → 매칭 경로 생성(trips.path)
  3. 00:40 머무름/POI: staypoints 계산 → pois 갱신
  4. 01:00 라벨링: 역지오코딩(큐/캐시)
  5. 01:30 산출물: GeoJSON/MBTiles 재생성 → tileserver-gl 롤링
  6. 01:40 알림: 처리 통계(레코드 수/실패/지연)를 Slack/메일로 통보

GitHub Actions 예시(YAML)

name: nightly-geo-pipeline
on:
  schedule: [{ cron: "10 0 * * *" }]
jobs:
  run:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }
      - run: pip install -r requirements.txt
      - run: python jobs/01_map_match.py
      - run: python jobs/02_stays_poi.py
      - run: python jobs/03_reverse_geocode.py
      - run: bash jobs/04_build_tiles.sh
      - run: python jobs/05_notify.py

간단 배시 파이프라인

#!/usr/bin/env bash
set -euo pipefail
python jobs/01_map_match.py --date $(date -d "yesterday" +%F)
python jobs/02_stays_poi.py --date $(date -d "yesterday" +%F)
python jobs/03_reverse_geocode.py --batch yesterday
bash   jobs/04_build_tiles.sh
python jobs/05_notify.py --channel geo-pipeline

예시 시나리오

사진만으로 여행 지도 만들기(가장 간단)

  1. 여행 사진 폴더 동기화 → exiftoolphotos.csv 생성
  2. 파이썬으로 GeoJSON/머무름 클러스터 생성 → Kepler.gl에 드래그
  3. 타임플레이백/히트맵/포인트 & 라인 레이어로 “여정 요약 맵” 완성

실시간 추적 + 일일 자동 요약(프라이버시 퍼스트)

  1. 휴대폰에 OwnTracks, 서버에 Mosquitto/Recorder/DB
  2. 매일 새벽 배치: 맵 매칭 → 머무름/POI → 역지오코딩 → 타일 재빌드
  3. 내부 포털(MapLibre)에서 최근 7일 경로/주요 장소 자동 표출

Google 타임라인 백업 재활용

  1. 기기 백업/내보내기 JSON → 변환 스크립트로 points.geojson 생성
  2. Trip Layer 형식의 trips.geojson 생성 → Kepler.gl 애니메이션 생성

실전 스니펫 모음

원시 포인트 → 트립 라인(PostGIS)

-- 디바이스·날짜 기준으로 시간순 정렬 후 LineString 생성
INSERT INTO trips(device_id, start_ts, end_ts, distance_m, path)
SELECT device_id,
       MIN(ts) AS start_ts,
       MAX(ts) AS end_ts,
       SUM( ST_DistanceSphere(geom, LAG(geom) OVER w) ) AS distance_m,
       ST_MakeLine(geom ORDER BY ts) AS path
FROM points
WINDOW w AS (PARTITION BY device_id ORDER BY ts)
WHERE ts::date = CURRENT_DATE - 1
GROUP BY device_id;

타일 빌드 스크립트

#!/usr/bin/env bash
set -euo pipefail
psql "$DATABASE_URL" -c "\COPY (
  SELECT jsonb_build_object(
    'type','FeatureCollection',
    'features', jsonb_agg(
      jsonb_build_object(
        'type','Feature',
        'properties', jsonb_build_object('device_id', device_id, 'start', start_ts, 'end', end_ts, 'dist', distance_m),
        'geometry', ST_AsGeoJSON(path)::jsonb
      )
    )
  ) FROM trips WHERE start_ts::date >= CURRENT_DATE - 7
) TO 'trips.geojson' WITH (FORMAT CSV, HEADER false, QUOTE E'\b');"

tippecanoe -o trips.mbtiles -zg --drop-densest-as-needed trips.geojson
docker kill tiles || true
docker run -d --rm --name tiles -p 8080:8080 -v $PWD:/data maptiler/tileserver-gl /data/trips.mbtiles

Google 백업 JSON → GeoJSON 변환(간단)

import json, time
from datetime import datetime, timezone
feat=[]
data=json.load(open("LocationHistory.json"))
for loc in data.get("locations", []):
    lat=loc["latitudeE7"]/1e7; lon=loc["longitudeE7"]/1e7; ts=int(loc["timestampMs"])/1000
    feat.append({"type":"Feature","properties":{"ts":datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()},
                 "geometry":{"type":"Point","coordinates":[lon,lat]}})
json.dump({"type":"FeatureCollection","features":feat}, open("points.geojson","w"))

선택 가이드

  1. 가볍게 시작: 사진(EXIF) → Kepler.gl로 여행 타임라인 지도
  2. 실시간·프라이버시: OwnTracks + MQTT + Recorder + PostGIS + OSRM + tileserver-gl + MapLibre
  3. 기존 기록 재활용: Google 타임라인 JSON → 변환 → Trip Layer 애니메이션
  4. 운영 필수: TLS/암호화, RBAC, 레이트 제한, 모호화, 보존·파기 자동화, 모니터링
728x90
그리드형(광고전용)

댓글