의료 서비스 제공자 찾기 지도는 개발자가 구축할 수 있는 가장 중요한 지도 중 하나입니다. 이를 사용하는 환자들은 종종 불안하거나, 통증을 느끼거나, 낯선 시스템을 헤쳐나가고 있습니다. 느리게 로드되거나, 관련 없는 결과를 반환하거나, 환자의 우편번호를 미국 기반 제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)에 따라 건강 상태를 드러내는 데이터인 특별 카테고리 데이터가 될 수 있습니다. 사용자 계정을 생성하거나 프로필을 저장한 적이 없어도 마찬가지입니다.
위험은 구체적입니다:
- 사용자의 입력한 우편번호를 포함하는 클라이언트 측 지오코딩 API 호출이 제3자 서버로 전송됩니다. 해당 서버는 우편번호, 검색되는 전문과, IP 주소를 봅니다. DPA와 EU 데이터 거주 보장이 없으면, 이는 문제가 있는 전송입니다.
- 브라우저 자동 완성 및 기록은 사용자의 장치에서 입력한 우편번호 및 전문의 쿼리를 유지할 수 있으며, 이는 귀하의 제어 밖이지만 개인정보 보호 정보에 기록할 가치가 있습니다.
- 서버 측에서 API 응답을 로깅하면(디버깅에 흔함), 우편번호 + 전문과 조합을 깨닫지 못하고 로깅할 수 있습니다.
해결책은 백엔드 프록시입니다: 귀하의 서버가 검색을 받고, MapAtlas 지오코딩 API를 호출하며, 환자의 브라우저에서 타사 API로 입력된 쿼리를 보내지 않고 좌표만 반환합니다. 구현 섹션에서 자세히 설명합니다.
서비스 제공자 데이터 구조
서비스 제공자 데이터는 스크래핑된 데이터 세트가 아닌 권위 있는 공식 출처에서 와야 합니다:
- 영국: NHS Digital의 NHS Choices API 및 CQC 등록 제공자 목록
- 독일: Kassenärztliche Bundesvereinigung (KBV) 공개 데이터 및 Arztsuche API
- 프랑스: CNAM의 Ameli API 및 répertoire RPPS
각 서비스 제공자를 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://tiles.mapatlas.eu/styles/basic/style.json?key=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://api.mapatlas.eu/geocoding/v1/search');
url.searchParams.set('text', req.query.q);
url.searchParams.set('key', 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자에게 보내지지 않습니다.
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&profile=driving`
);
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, profile } = req.query;
const url = new URL('https://api.mapatlas.eu/v1/isochrone');
url.searchParams.set('lat', lat);
url.searchParams.set('lng', lng);
url.searchParams.set('time', minutes * 60); // seconds
url.searchParams.set('profile', profile || 'driving');
url.searchParams.set('key', process.env.MAPATLAS_API_KEY);
const upstream = await fetch(url.toString());
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로 전송되어서는 안 됩니다.
- 검색 용어의 세션 로깅 없음: 디버깅을 위해 요청을 로깅하면, 로그에 쓰기 전에 우편번호를 제거합니다.
- 개인정보 보호 공지: 앱의 개인정보 보호 공지는 위치 데이터가 인근 서비스 제공자를 찾기 위해 처리된다는 것, 법적 기초(정당한 이익 또는 명시적 동의) 및 보유 기간을 공개해야 합니다.
- 위치 접근에 대한 동의: 브라우저 Geolocation API(
navigator.geolocation.getCurrentPosition)를 사용하면, 브라우저 권한 프롬프트를 트리거하기 전에 명확한 목적 설명을 보여줍니다. - 데이터 거주: MapAtlas는 모든 API 요청을 EU 내에서 처리합니다. Records of Processing Activities를 완료할 때 MapAtlas DPA를 참조합니다.
- 보유: 서버 측에서 등시선 결과 또는 검색 좌표를 성능을 위해 캐시하면, 보유 기간(예: 24시간)을 정의하고 삭제를 자동화합니다.
GDPR 준수 지도 API에 대한 EU 개발자 가이드는 프로세서 계약, 데이터 거주, 동의 패턴을 완전히 다룹니다. 의료 서비스 산업 페이지는 디지털 건강 응용 프로그램과 관련된 특정 MapAtlas 기능을 나열합니다.
다음으로 구축할 것
- 무료 MapAtlas API 키 가입, 지도, 지오코딩, 라우팅, 등시선을 위한 하나의 키
- 더 많은 위치가 있는 다중 클리닉 네트워크를 위해 Store Locator 튜토리얼 패턴을 추가합니다
- 우편번호 조회, 역 지오코딩, 배치 서비스 제공자 주소 검증을 위해 지오코딩 API 기능 페이지를 탐색합니다
자주 묻는 질문
환자 데이터를 저장하지 않아도 GDPR이 의사 찾기 지도에 적용되는 이유는?
의료 찾기 지도에서 환자의 검색 위치는 그들이 사는 곳을 드러내고, 추론에 의해 그들이 어떤 상태를 가질 수 있는지를 나타냅니다(예: 우편번호 근처의 종양 전문의 검색). GDPR 제9조에 따르면, 위치 데이터에서의 건강 관련 추론은 특별 카테고리 데이터가 될 수 있습니다. 프로필을 저장한 적이 없어도, 클라이언트 측에서 제3자 API로 보낸 실시간 지오코딩 요청은 법적 기초와 공개를 요하는 처리 기록을 생성합니다.
등시선이란 무엇이며 서비스 제공자 찾기에 유용한 이유는?
등시선은 시작 위치에서 주어진 이동 시간 내에 도달할 수 있는 모든 지점을 포함하는 다각형입니다. 의료 서비스 제공자 찾기에서, 등시선 오버레이는 가장 실질적인 환자 질문에 답합니다: "자동차로 20분 내에 도달할 수 있는 의사는 몇 명인가?" 이는 순수 거리보다 훨씬 더 유용한 질문입니다. 도로, 제한 속도, 교통 패턴을 고려하기 때문입니다.
NHS 또는 공개 의료 응용 프로그램을 위해 MapAtlas를 사용할 수 있습니까?
예. MapAtlas는 EU 호스팅, ISO 27001 인증, 제28조에 따른 GDPR 준수 데이터 처리 계약을 제공합니다. 이는 일반적인 공공 부문 조달 요구 사항을 만족합니다. 영국 NHS Digital 및 독일(DSGVO) 및 프랑스(CNIL)의 동등한 기관은 MapAtlas가 이행하는 EU/UK 내의 데이터 거주를 요구합니다.

