| 파일 | 역할 | 핵심 기술 |
|---|---|---|
| 원주_의료택시_관제시스템.html | 상담원·관리자 웹앱(예약·추천·배차·통계) | SPA·localStorage·SVG지도 |
| 원주_의료택시_기사앱.html | 기사 모바일앱(배차수신·운행·귀가) | 상태기계·BroadcastChannel |
| ai_dispatch/ (백엔드) | 실서비스용 서버(진짜 OR-Tools·DB·ML) | FastAPI·PostgreSQL·OR-Tools·sklearn |
| 원주_통합_대시보드.html | 공공데이터 지도 분석 | GeoJSON·choropleth |
| 원주_의료기관_카카오지도.html | 509곳 주소→좌표 변환 | 카카오 services |
무엇: 원주 행정경계와 병원·차량을 화면에 그린다. 왜: 외부 지도 API(구글·카카오)는 키·과금·도메인 제한이 있어서, 키 없이 되도록 직접 그린다.
지구는 둥근데 화면은 평평하다. 그래서 등거리 투영으로 좌표를 픽셀로 바꾼다. 위도에 따라 경도 1도의 실제 폭이 달라지므로 cos(위도)로 보정한다.
// 위경도 → 화면 좌표 (PX 함수) function PX(lon, lat){ const kx = Math.cos(중앙위도 * π/180); // 경도 폭 보정 const s = min(가로/경도범위, 세로/위도범위); // 화면에 맞춘 배율 return [ 왼쪽여백 + (lon-최소경도)*kx*s, 아래여백 - (lat-최소위도)*s ]; // y는 위아래 뒤집기 }
cos로 눌러준다.행정경계는 GeoJSON(각 읍면동의 꼭짓점 좌표 목록)을 받아 PX로 변환해 다각형(<path>)으로 그린다.
무엇: 진료과·거주지만 넣으면 갈 병원을 추천. 어떻게: 후보 병원마다 4가지를 점수화해 합산, 높은 순 Top5.
// 추천점수 = 거리 45% + 소요 20% + 혼잡도 20% + 예약가능 15% score = 45*(1 - 거리/최대거리) // 가까울수록 ↑ + 20*(1 - 예상소요/60) + 20*(1 - 병원혼잡도/100) // 덜 붐빌수록 ↑ + 15*(예약가능 ? 1 : 0.2);
병원이 실시간 대기수를 공개하지 않으므로 가진 데이터로 근사한다. 그리고 혼잡도는 두 종류다:
| 종류 | 뜻 | 계산 |
|---|---|---|
| 병원 혼잡도 | 그 병원이 붐빔(예약 몰림) | 우리 콜센터가 그 병원에 보낸 예약 수 + 건보 통계 밀도 |
| 도로 교통혼잡도 | 길 막힘(정체) | 카카오모빌리티 실시간 교통(백엔드) |
무엇: 하루 예약 전체를 택시들에 나눠, 빈차 주행(공차)을 최소로. 왜 어렵나: 손님 15명만 돼도 배차 조합이 1조 가지가 넘는다(사람이 못 푼다).
# 백엔드 optimizer.py — 진짜 OR-Tools routing.SetArcCostEvaluatorOfAllVehicles(거리콜백) # 비용=거리 routing.AddDimension(시간콜백, ...) # 시간창 제약 routing.AddPickupAndDelivery(픽업, 하차) # 집→병원 짝 routing.AddDimensionWithVehicleCapacity(..., 1) # 정원 1 params.local_search = GUIDED_LOCAL_SEARCH # 최적 탐색
총거리 최소 = 공차 최소인 이유: 총거리 = 실차 + 공차. 실차(손님 태운 거리)는 무조건 필요해 고정 → OR-Tools가 줄일 수 있는 건 공차뿐.
A를 병원에 내려준 택시가 빈 차로 돌아가지 않고, 그 병원에서 진료 끝난 B를 태워 귀가시킨다. "누군가의 등원 종료 = 다른 이의 귀가 시작"으로 이어 붙여 빈 주행 2번을 0으로.
무엇: 쓸수록 똑똑해진다. 어떻게: 운영 데이터를 localStorage(브라우저) 또는 DB(서버)에 실제로 저장하고, 새 데이터가 올 때마다 평균을 조금씩 갱신(온라인 학습)한다.
// 실제 학습 코드 (관제앱) function learnDemand(지역, 시각){ // 지역×시간대 수요 카운트 LEARN.demand[지역+'@'+시(hour)] += 1; } function learnDuration(진료과, 분){ // 진료과별 소요 평균 m.n++; m.mean += (분 - m.mean)/m.n; // 온라인 평균 갱신 LEARN.dur[진료과] = m; } localStorage.setItem('medtaxi_learn', JSON.stringify(LEARN)); // 저장
평균 += (새값-평균)/개수로 조금씩 이동. 데이터가 쌓일수록 실제값에 수렴 = "학습". 실서비스에선 이걸 scikit-learn(GradientBoosting)으로 요일·날씨·연령까지 넣어 정교화.| 수요예측 | 소요시간 | |
|---|---|---|
| 입력 | 요일·시간·계절·날씨·지역 | 진료과·시간대 |
| 정답 | 실제 예약 건수 | 실제 진료 소요(분) |
| 용도 | 택시 사전배치 | 회송 택시 미리 준비 |
⚠️ 귀가시각은 정확 예측 안 한다: 진료시간은 검사·대기로 변수가 커서 평균 경향만 배운다. 실제 귀가 배차는 "진료완료" 버튼(호출형, 카카오택시처럼) → 가장 가까운 차량. 예측이 틀려도 안전.
무엇: 상담원이 배차하면 기사앱에 즉시 알림. 어떻게: BroadcastChannel — 같은 주소(도메인)의 여러 탭이 서로 메시지를 주고받는 브라우저 기능. 서버 없이 실시간.
// 관제앱: 배차 확정 시 방송 BC.postMessage({t:'dispatch', 승객, 픽업, 병원, 기사}); // 기사앱: 수신 → 배차 알림 팝업(30초 카운트다운) BC.onmessage = e => { if(e.data.t==='dispatch') 배차알림표시(e.data); }; // 기사 수락 → 관제로 되돌려 상태 갱신 BC.postMessage({t:'accepted', id});
예약·배차·완료·학습을 매번 localStorage에 저장 → 새로고침해도 유지·누적. 대시보드 "누적 학습 데이터 N건"이 그 증거.