Skip to main content
GDPR 준수 의료 기관 찾기 지도 구축 방법
Tutorials

GDPR 준수 의료 기관 찾기 지도 구축 방법

전문과 필터, 우편번호 검색, 등시선 이동 시간 오버레이, 설계 단계부터의 환자 개인 정보 보호를 갖춘 GDPR 준수 의사 찾기 지도를 구축하세요. 완전한 JavaScript 튜토리얼.

Brent van der Heiden17 min read
#healthcare map#provider finder#doctor finder#healthcare api#gdpr healthcare#health app

의료 서비스 제공자 찾기 지도는 개발자가 구축할 수 있는 가장 중요한 지도 중 하나입니다. 이를 사용하는 환자들은 종종 불안하거나, 통증을 느끼거나, 낯선 시스템을 헤쳐나가고 있습니다. 느리게 로드되거나, 관련 없는 결과를 반환하거나, 환자의 우편번호를 미국 기반 제3자 서비스로 조용히 보내는 지도는 단순히 나쁜 사용자 경험이 아니라 GDPR 위반 가능성입니다.

이 튜토리얼은 프로덕션 준비가 된 의료 서비스 제공자 찾기 지도를 구축합니다: 전문과별 필터, 지오코딩 API를 사용한 우편번호 검색, "20분 이내 의사" 등시선 오버레이, MapAtlas Maps API를 사용한 작동 지도, 모두 아키텍처 시작부터 내장된 GDPR 준수를 포함합니다.

완전한 구현은 순수 JavaScript로, 프레임워크가 필요하지 않습니다. React, Vue, 순수 HTML, WordPress 또는 병원의 레거시 CMS를 포함한 모든 스택에서 작동합니다. 이 튜토리얼은 또한 EU 규제 상황, 영국 CQC 데이터, 독일 Kassenärztliche Vereinigung, 프랑스 CNAM을 다루므로 합법적으로 서비스 제공자 데이터를 어디서 구해야 하는지 알 수 있습니다.

의료 위치 앱이 추가 GDPR 의무를 갖는 이유

표준 앱은 개인 데이터(이름, 이메일, 사용)를 수집합니다. 의료 앱은 두 번째 위험을 가집니다: 위치 데이터에서 건강 정보 추론.

사용자가 "SE1 7PB 근처 심장 전문의"를 검색하면, 그들의 우편번호는 개인 데이터입니다. 우편번호와 의료 전문과의 결합은 GDPR 제9조(1)에 따라 건강에 관한 데이터, 즉 특별 카테고리 데이터가 될 수 있습니다. 유럽사법재판소가 민감한 정보를 간접적으로 드러내는 데이터도 제9조에 해당한다고 판단했기 때문입니다. 사용자 계정을 생성하거나 프로필을 저장한 적이 없어도 마찬가지입니다.

위험은 구체적입니다:

  • 사용자의 입력한 우편번호를 포함하는 클라이언트 측 지오코딩 API 호출이 제3자 서버로 전송됩니다. 해당 서버는 우편번호, 검색되는 전문과, IP 주소를 봅니다. DPA와 EU 데이터 거주 보장이 없으면, 이는 문제가 있는 전송입니다.
  • 브라우저 자동 완성 및 기록은 사용자의 장치에서 입력한 우편번호 및 전문의 쿼리를 유지할 수 있으며, 이는 귀하의 제어 밖이지만 개인정보 보호 정보에 기록할 가치가 있습니다.
  • 서버 측에서 API 응답을 로깅하면(디버깅에 흔함), 우편번호 + 전문과 조합을 깨닫지 못하고 로깅할 수 있습니다.

해결책은 백엔드 프록시입니다: 귀하의 서버가 검색을 받고, MapAtlas 지오코딩 API를 호출하며, 환자의 브라우저에서 타사 API로 입력된 쿼리를 보내지 않고 좌표만 반환합니다. 구현 섹션에서 자세히 설명합니다.

서비스 제공자 데이터 구조

서비스 제공자 데이터는 스크래핑된 데이터 세트가 아닌 권위 있는 공식 출처에서 와야 합니다:

각 서비스 제공자를 GeoJSON 기능으로 구성합니다:

const providers = {
  type: "FeatureCollection",
  features: [
    {
      type: "Feature",
      geometry: { type: "Point", coordinates: [4.9051, 52.3680] },
      properties: {
        id: "prov-001",
        name: "Dr. Maria van den Berg",
        specialty: "general-practitioner",
        accepting: true,
        address: "Prinsengracht 263, 1016 GV Amsterdam",
        phone: "+31 20 423 5678",
        languages: ["Dutch", "English"],
        nextAvailable: "2026-02-18"
      }
    },
    {
      type: "Feature",
      geometry: { type: "Point", coordinates: [4.8890, 52.3610] },
      properties: {
        id: "prov-002",
        name: "Dr. Jan Smits",
        specialty: "cardiologist",
        accepting: false,
        address: "Leidseplein 15, 1017 PS Amsterdam",
        phone: "+31 20 612 3456",
        languages: ["Dutch"],
        nextAvailable: null
      }
    },
    {
      type: "Feature",
      geometry: { type: "Point", coordinates: [4.9200, 52.3750] },
      properties: {
        id: "prov-003",
        name: "Anna Fischer, MD",
        specialty: "dermatologist",
        accepting: true,
        address: "Plantage Middenlaan 14, 1018 DD Amsterdam",
        phone: "+31 20 789 0123",
        languages: ["Dutch", "English", "German"],
        nextAvailable: "2026-02-20"
      }
    }
  ]
};

1단계: 전문과 필터가 있는 지도 초기화

지도와 전문과 드롭다운으로 시작합니다. 필터는 네트워크 요청 없이 클라이언트 측에서 보이는 마커를 업데이트합니다.

import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';

const map = new mapmetricsgl.Map({
  container: 'provider-map',
  style: 'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/style.json&token=YOUR_API_KEY',
  center: [4.9041, 52.3676],
  zoom: 13
});

map.on('load', () => {
  map.addSource('providers', {
    type: 'geojson',
    data: providers
  });

  // Provider markers, green = accepting, grey = full
  map.addLayer({
    id: 'provider-markers',
    type: 'circle',
    source: 'providers',
    paint: {
      'circle-radius': 10,
      'circle-color': [
        'case',
        ['==', ['get', 'accepting'], true], '#16A34A',
        '#9CA3AF'
      ],
      'circle-stroke-width': 2,
      'circle-stroke-color': '#ffffff'
    }
  });

  buildList(providers.features);
});

// Specialty filter
document.getElementById('specialty-select').addEventListener('change', (e) => {
  const specialty = e.target.value;
  if (specialty === 'all') {
    map.setFilter('provider-markers', null);
  } else {
    map.setFilter('provider-markers', ['==', ['get', 'specialty'], specialty]);
  }
});

2단계: 백엔드 프록시를 통한 우편번호 검색

브라우저에서 지오코딩 API를 호출하는 대신(환자의 우편번호를 제3자 서버에 노출함), 자신의 백엔드 엔드포인트를 통해 지오코딩 요청을 라우팅합니다. 귀하의 서버는 MapAtlas를 호출하고 좌표만 반환합니다.

프론트엔드(환자의 브라우저):

async function geocodePostcode(postcode) {
  // Call YOUR backend, not the geocoding API directly
  const res = await fetch(`/api/geocode?q=${encodeURIComponent(postcode)}`);
  const { lat, lng } = await res.json();
  return { lat, lng };
}

document.getElementById('postcode-form').addEventListener('submit', async (e) => {
  e.preventDefault();
  const postcode = document.getElementById('postcode-input').value.trim();
  const { lat, lng } = await geocodePostcode(postcode);
  map.flyTo({ center: [lng, lat], zoom: 13 });
  fetchIsochrone(lat, lng);
});

백엔드 프록시(Node.js/Express):

// /api/geocode, server-side only, postcode never leaves your infrastructure
app.get('/api/geocode', async (req, res) => {
  const url = new URL('https://gateway.mapmetrics-atlas.net/forward-geocode/');
  url.searchParams.set('text', req.query.q);
  url.searchParams.set('token', process.env.MAPATLAS_API_KEY); // server-side key
  url.searchParams.set('size', '1');

  const upstream = await fetch(url.toString());
  const data = await upstream.json();

  if (!data.features?.length) {
    return res.status(404).json({ error: 'Postcode not found' });
  }

  const [lng, lat] = data.features[0].geometry.coordinates;
  // Return ONLY coordinates, no postcode logged, no specialty in response
  res.json({ lat, lng });
});

API 키는 서버에 남아 있고, 환자의 브라우저는 지오코더에 직접 접속하지 않으며, 서버가 외부로 전달하는 것은 전문과나 환자를 식별할 수 있는 정보 없이 우편번호 자체뿐입니다.

3단계: 등시선 오버레이, "20분 이내 의사"

등시선 다각형은 주어진 운전 시간 내에 도달할 수 있는 모든 지점을 보여줍니다. 서비스 제공자 찾기에 하나를 표시하는 것은 가장 실질적인 환자 질문에 답합니다: "실제로 도달할 수 있는 의사는 몇 명인가?"

async function fetchIsochrone(lat, lng) {
  // Call through your backend proxy for the same privacy reasons
  const res = await fetch(
    `/api/isochrone?lat=${lat}&lng=${lng}&minutes=20&costing=auto`
  );
  const geojson = await res.json();

  // Remove existing isochrone layer if present
  if (map.getLayer('isochrone-fill')) map.removeLayer('isochrone-fill');
  if (map.getLayer('isochrone-border')) map.removeLayer('isochrone-border');
  if (map.getSource('isochrone')) map.removeSource('isochrone');

  map.addSource('isochrone', { type: 'geojson', data: geojson });

  map.addLayer({
    id: 'isochrone-fill',
    type: 'fill',
    source: 'isochrone',
    paint: {
      'fill-color': '#16A34A',
      'fill-opacity': 0.12
    }
  }, 'provider-markers'); // Insert below markers so pins render on top

  map.addLayer({
    id: 'isochrone-border',
    type: 'line',
    source: 'isochrone',
    paint: {
      'line-color': '#16A34A',
      'line-width': 2,
      'line-dasharray': [3, 2]
    }
  }, 'provider-markers');
}

백엔드 등시선 프록시:

app.get('/api/isochrone', async (req, res) => {
  const { lat, lng, minutes, costing } = req.query;
  const url = new URL('https://gateway.mapmetrics-atlas.net/isochrone/');
  url.searchParams.set('token', process.env.MAPATLAS_API_KEY);

  const upstream = await fetch(url.toString(), {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      locations: [{ lat: Number(lat), lon: Number(lng) }],
      costing: costing || 'auto',
      contours: [{ time: Number(minutes) }],
      polygons: true,
    }),
  });
  const data = await upstream.json();
  res.json(data); // GeoJSON polygon
});

등시선 지도 설명 글은 등시선 사용 사례에 대해 더 깊이 있게 다루며, 자동차 없는 환자에게 유용한 대중 교통 등시선과 10, 20, 30분 구역을 동시에 보여주는 다중 시간대 오버레이를 포함합니다.

4단계: 서비스 제공자 상세 팝업

환자가 의사 마커를 클릭하면, 필수 정보를 표시합니다: 이름, 전문과, 수용 상태, 언어, 다음 가능한 약속.

map.on('click', 'provider-markers', (e) => {
  const {
    name, specialty, accepting, address, phone, languages, nextAvailable
  } = e.features[0].properties;
  const coords = e.features[0].geometry.coordinates.slice();

  const statusBadge = accepting
    ? '<span style="color:#16A34A;font-weight:600">Accepting patients</span>'
    : '<span style="color:#9CA3AF">Not accepting new patients</span>';

  const apptLine = accepting && nextAvailable
    ? `<p style="margin:4px 0">Next available: <strong>${nextAvailable}</strong></p>`
    : '';

  new mapmetricsgl.Popup({ maxWidth: '280px' })
    .setLngLat(coords)
    .setHTML(`
      <strong style="font-size:15px">${name}</strong>
      <p style="margin:4px 0;text-transform:capitalize">${specialty.replace('-', ' ')}</p>
      ${statusBadge}
      ${apptLine}
      <p style="margin:6px 0;font-size:13px;color:#64748b">${address}</p>
      <p style="margin:4px 0;font-size:13px">
        Languages: ${Array.isArray(languages) ? languages.join(', ') : languages}
      </p>
      <a href="tel:${phone}" style="color:#2563EB">${phone}</a>
    `)
    .addTo(map);
});

map.on('mouseenter', 'provider-markers', () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'provider-markers', () => { map.getCanvas().style.cursor = ''; });

5단계: 지도와 동기화된 목록 패널

지도 옆의 목록 보기는 스크린 리더 사용자와 클릭하는 것보다 스크롤을 선호하는 사람들을 돕습니다.

function buildList(features) {
  const list = document.getElementById('provider-list');
  list.innerHTML = '';

  features.forEach(({ properties, geometry }) => {
    const { name, specialty, accepting, address, nextAvailable } = properties;

    const card = document.createElement('div');
    card.className = `provider-card ${accepting ? 'accepting' : 'full'}`;
    card.innerHTML = `
      <strong>${name}</strong>
      <p>${specialty.replace('-', ' ')}</p>
      <p style="font-size:12px;color:#64748b">${address}</p>
      <small>${accepting ? `Next: ${nextAvailable || 'call to confirm'}` : 'Not accepting'}</small>
    `;
    card.addEventListener('click', () => {
      map.flyTo({ center: geometry.coordinates, zoom: 15 });
    });

    list.appendChild(card);
  });
}

GDPR 구현 체크리스트

EU 의료 개발자의 경우, 이것을 최소한의 준수 체크리스트로 취급합니다:

  • 지오코딩을 위한 백엔드 프록시: 환자 우편번호는 클라이언트 측에서 제3자 API로 전송되어서는 안 됩니다.
  • 검색 용어의 세션 로깅 없음: 디버깅을 위해 요청을 로깅하면, 로그에 쓰기 전에 우편번호를 제거합니다.
  • 개인정보 보호 공지: 앱의 개인정보 보호 공지는 위치 데이터가 인근 서비스 제공자를 찾기 위해 처리된다는 것, 제6조에 따른 법적 기초, 검색 데이터가 건강 정보를 드러낼 수 있는 경우 명시적 동의와 같은 제9조(2)의 요건, 그리고 보유 기간을 공개해야 합니다.
  • 위치 접근에 대한 동의: 브라우저 Geolocation API(navigator.geolocation.getCurrentPosition)를 사용하면, 브라우저 권한 프롬프트를 트리거하기 전에 명확한 목적 설명을 보여줍니다.
  • 데이터 거주: MapAtlas는 모든 API 요청을 EU 내에서 처리합니다. Records of Processing Activities를 완료할 때 MapAtlas DPA를 참조합니다.
  • 보유: 서버 측에서 등시선 결과 또는 검색 좌표를 성능을 위해 캐시하면, 보유 기간(예: 24시간)을 정의하고 삭제를 자동화합니다.

GDPR 준수 지도 API에 대한 EU 개발자 가이드는 프로세서 계약, 데이터 거주, 동의 패턴을 완전히 다룹니다. 의료 서비스 산업 페이지는 디지털 건강 응용 프로그램과 관련된 특정 MapAtlas 기능을 나열합니다.

다음으로 구축할 것

자주 묻는 질문

환자 데이터를 저장하지 않아도 GDPR이 의사 찾기 지도에 적용되는 이유는?

의료 찾기 지도에서 환자의 검색 위치는 그들이 사는 곳을 드러내고, 추론에 의해 그들이 어떤 상태를 가질 수 있는지를 나타냅니다(예: 우편번호 근처의 종양 전문의 검색). GDPR 제9조에 따르면, 위치 데이터에서의 건강 관련 추론은 특별 카테고리 데이터가 될 수 있습니다. 프로필을 저장한 적이 없어도, 클라이언트 측에서 제3자 API로 보낸 실시간 지오코딩 요청은 법적 기초와 공개를 요하는 처리 기록을 생성합니다.

등시선이란 무엇이며 서비스 제공자 찾기에 유용한 이유는?

등시선은 시작 위치에서 주어진 이동 시간 내에 도달할 수 있는 모든 지점을 포함하는 다각형입니다. 의료 서비스 제공자 찾기에서, 등시선 오버레이는 가장 실질적인 환자 질문에 답합니다: "자동차로 20분 내에 도달할 수 있는 의사는 몇 명인가?" 이는 순수 거리보다 훨씬 더 유용한 질문입니다. 도로, 제한 속도, 교통 패턴을 고려하기 때문입니다.

NHS 또는 공개 의료 응용 프로그램을 위해 MapAtlas를 사용할 수 있습니까?

예. MapAtlas는 ISO 27001 인증 EU 데이터센터에서 호스팅되며, 제28조에 따른 GDPR 준수 데이터 처리 계약을 제공합니다. 이는 일반적인 공공 부문 조달 요구 사항을 만족합니다. 처리를 EU 안에서 유지하면 공공 부문과 의료 분야 조달 팀이 일상적으로 묻는 국제 데이터 이전 관련 질문도 해소됩니다.

자주 묻는 질문

환자 데이터를 저장하지 않아도 GDPR이 의사 찾기 지도에 적용되는 이유는 무엇인가요?

의료 기관 찾기 지도에서 환자가 검색한 위치는 그 사람이 사는 곳을 드러내고, 나아가 어떤 질환이 있을 수 있는지까지 추론하게 합니다(예: 특정 우편번호 근처의 종양 전문의 검색). GDPR 제9조에 따르면 위치 데이터에서 나온 건강 관련 추론은 특별 범주 데이터에 해당할 수 있습니다. 프로필을 저장하지 않더라도, 클라이언트 측에서 제3자 API로 보내는 실시간 지오코딩 요청은 처리 기록을 만들며, 여기에는 법적 근거와 고지가 필요합니다.

등시선이란 무엇이며 의료 기관 찾기에 유용한 이유는 무엇인가요?

등시선은 시작 위치에서 주어진 이동 시간 안에 도달할 수 있는 모든 지점을 나타내는 폴리곤입니다. 의료 기관 찾기 지도에서 등시선 오버레이는 "자동차로 20분 안에 갈 수 있는 의사는 누구인가?"라는 질문에 답합니다. 도로, 제한 속도, 교통 패턴을 반영하기 때문에 단순 거리보다 훨씬 유용한 질문입니다.

NHS 또는 공공 의료 애플리케이션에 MapAtlas를 사용할 수 있나요?

네. MapAtlas는 ISO 27001 인증 EU 데이터센터에서 호스팅되며, 제28조에 따른 GDPR 준수 데이터 처리 계약을 제공합니다. 이는 일반적인 공공 부문 조달 요건을 충족합니다. 처리를 EU 안에서 유지하면 공공 부문과 의료 분야 조달 팀이 일상적으로 묻는 국제 데이터 이전 관련 질문도 해소됩니다.

유용하셨나요? 공유해 보세요.

저자 소개

Brent van der Heiden

작성자

Brent van der Heiden

Co-Founder & CEO at MapAtlas

Brent built MapAtlas out of a conviction that developers deserve location APIs with fair pricing and genuine end-user privacy. Before that he co-founded MapMetrics, a community mapping and navigation app. He writes about geospatial infrastructure, map data, AI search visibility, and how location data powers the products people rely on every day.

모든 기사 보기 →
블로그로 돌아가기