배경 및 전제
현황
- 경기도 특별교통수단: 31개 시군 운영, 4가지 호출 유형(실시간·서울권·관내·광역 예약), 미합의 11건
- 기존 레포: shucle-apigw · shucle-routing · shucle-billing · shucle-operation-tool · shucle-proto 확인 완료
- 매칭 엔진: MatchingService gRPC 인터페이스(proto)는 있으나 구현체(서버) 코드 없음 → 신규 작성 필요
- TaxiServiceType: 현재 BASIC(1)·GYUNGGI_VOUCHER(2)·R1(3)·YEONGAM_PUBLIC(4)·YEONGDEOK_TOUR(5), 특교용 신규 값 필요
전체 시스템 구조
아키텍처 개요
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 경기도 특교 — 전체 흐름 │
└─────────────────────────────────────────────────────────────────────────────────┘
[라이더 앱 / 전화 호출]
│ gRPC
▼
┌─────────────────────┐ ┌──────────────────────┐
│ shucle-apigw │────▶│ shucle-callagent │ AI 음성 예약 (inbound)
│ (확장) │ │ (기존 유지) │
│ │ └──────────────────────┘
│ 특교 전용 검증: │
│ ① 출발지 시군 체크 │
│ ② 일일 쿼터 (유형별) │
│ ③ 패널티 정지 체크 │
│ ④ 사전예약 시간 체크 │
│ ⑤ IPCC 발신번호 조회│
└──────────┬──────────┘
│ MatchRequest (gRPC)
▼
┌─────────────────────┐ ┌──────────────────────┐
│ shucle-matching │────▶│ shucle-routing │ 도로망 경로 계산
│ ★ 신규 레포 (핵심) │ │ (기존 유지, C++) │
│ │ └──────────────────────┘
│ DRT 매칭 엔진: │
│ ① 구역 필터 (관내/광역)│
│ ② 차량 필터 (와상/휠체어)│
│ ③ 휴식 필터 (기사) │
│ ④ CAP 필터 (광역 한도)│
│ ⑤ 스코어 (ETA·효율·균형)│
│ ⑥ CDO 재배차 (매 1분) │
│ ⑦ 강제배차 + 영향 계산│
└──────────┬──────────┘
│
┌──────┴───────┐
▼ ▼
┌──────────┐ ┌──────────────────────┐
│ shucle- │ │ shucle-operation- │
│ billing │ │ tool (확장) │
│ (확장) │ │ │
│ │ │ ① CAP 실시간 대시보드│
│ 특교 플랜│ │ ② 강제배차 UI │
│ 패널티 │ │ ③ 배차표 이력 조회 │
│ 카운터 │ │ ④ 기사 휴식 현황 │
└──────────┘ │ ⑤ IPCC 이용자 팝업 │
└──────────────────────┘
레포별 변경 범위
레포 변경 요약
shucle-matching
Go · gRPC · Redis
신규 생성
- MatchingService gRPC 인터페이스 구현체 (현재 없음)
- DRT 매칭 엔진: 필터 → 스코어 → 경로 삽입
- CDO 재배차 루프 (매 1분 전체 fleet 재평가)
- 강제배차 + 하류 영향 계산
- 시군별 CAP 실시간 카운터 (Redis)
- 시군 정책 로더 (DB 캐시 5분)
shucle-proto
Protobuf
확장 (최소)
- taxi.proto: GYUNGGI_SPECIAL = 6 추가
- item.proto: STRETCHER = 6 추가 (와상장애인)
- event.proto: Demand에 SpecialTransportAttr 추가
Supply에 BreakState 추가 - service.proto: MatchRequest에 SpecialTransportPolicy 추가
shucle-apigw
Go · gRPC
확장
- demand_service.go: 특교 수요 검증 함수 추가
(구역·쿼터·패널티·예약 시간) - 왕복예약 API (CreateRoundTripDemand)
- IPCC 발신번호 → 이용자 조회 엔드포인트
- 특교 전용 에러 코드 추가
shucle-billing
Go · Kafka
확장
- GYUNGGI_SPECIAL 플랜 타입 지원
- 패널티 카운터 모델 (cancel/noshow)
- 30일 리셋 크론 추가
- 특교 요금 정책 (무료 or 정액)
shucle-operation-tool
TypeScript · React
확장
- /special-transport/cap — CAP 실시간 슬라이더
- /special-transport/dispatch — 강제배차 UI
- /special-transport/history — 배차표 이력 (읽기 전용)
- /special-transport/drivers — 기사 휴식 현황
- /special-transport/ipcc — 발신번호 → 이용자 팝업
shucle-routing
Go · C++ (RoutingKit)
변경 없음
- shucle-matching이 기존 그대로 호출
- 도로망 경로 계산은 기존 엔진 재사용
- 특교용 별도 지도 설정 필요 시 configs/conv 추가만
Proto 변경 상세
shucle-proto 변경
① msg/common/v1/taxi.proto — GYUNGGI_SPECIAL 추가
enum TaxiServiceType {
TAXI_SERVICE_TYPE_UNSPECIFIED = 0;
BASIC = 1; // 코나투스 외부택시
GYUNGGI_VOUCHER = 2; // 경기 바우처 택시
R1 = 3;
YEONGAM_PUBLIC = 4;
YEONGDEOK_TOUR = 5;
GYUNGGI_SPECIAL = 6; // 경기도 특별교통수단 ← NEW
}
② msg/common/v1/item.proto — STRETCHER 추가 (와상장애인 #58)
enum Item {
ITEM_UNSPECIFIED = 0;
ADULT = 1;
CHILD_WITH_SEAT = 2;
YOUTH = 3;
CHILD = 4;
WHEELCHAIR = 5; // 기존
STRETCHER = 6; // 와상장애인 전용 공간 ← NEW
}
③ msg/matching/v1/event.proto — Demand · Supply 확장
message Demand {
// ... 기존 필드 유지 ...
// 특교 전용 (field 28~29 사용)
string pair_demand_id = 28; // 왕복 쌍 demand ID (#14)
SpecialTransportAttr special_transport = 29;
message SpecialTransportAttr {
string municipality_id = 1; // 등록 시군 (31개 중 1개)
bool is_bedridden = 2; // 와상장애인 여부 (#58)
CallType call_type = 3;
enum CallType {
REALTIME = 0; // 실시간 호출 (일 2회)
SEOUL_RESERVED = 1; // 서울권 예약 (일 1회)
INTERNAL_RESERVED = 2; // 관내 예약 (일 2회, 7개 시군)
REGIONAL_RESERVED = 3; // 광역 예약 (일 2회, 26개 시군)
}
}
}
message Supply {
// ... 기존 필드 유지 ...
BreakState break_state = 28; // 기사 휴식 상태 (#26)
message BreakState {
bool is_resting = 1;
google.protobuf.Timestamp rest_until = 2;
float continuous_duty_hours = 3; // 연속 운행 누적 시간 (hours)
}
}
④ api/matching/v1/service.proto — MatchRequest 확장
message MatchRequest {
matchingmsgv1.Demand demand = 1;
repeated matchingmsgv1.Supply supplies = 2;
repeated matchingmsgv1.OtherSupply other_supplies = 5;
bool check_only = 4;
SpecialTransportPolicy special_transport_policy = 6; // ← NEW (#60)
message SpecialTransportPolicy {
string municipality_id = 1;
float regional_cap_ratio = 2; // 목표 광역 비율 (예: 0.2)
int32 regional_used_count = 3; // 당일 누적 광역 배차 수
int32 regional_max_count = 4; // 당일 광역 배차 최대값
float driver_rest_threshold_hours = 5;
}
}
신규 레포 상세 설계
shucle-matching — 매칭 엔진
핵심 전제
기존 MatchingService gRPC 인터페이스(proto에 정의됨)를 구현하는 서버. apigw는 이미 MatchingService.Match()를 호출하도록 구현되어 있음. 새 레포는 이 인터페이스를 구현하되 특교 제약 로직을 추가.
디렉토리 구조
shucle-matching/
├─ cmd/server/main.go # gRPC 서버 진입점 + CDO 루프 시작
├─ configs/
│ └─ prod.yml # 라우팅 서비스 주소, DB DSN, Redis 주소
└─ pkg/
├─ service/
│ └─ matching.go # MatchingService 인터페이스 구현 (핵심)
├─ engine/
│ ├─ engine.go # 공통 유틸 (ETA 추정, 재배차 기준)
│ ├─ redispatch.go # CDO 재배차 루프 (#59)
│ └─ forced.go # 강제배차 + 하류 영향 계산 (#44)
├─ filter/
│ ├─ filter.go # FilterFn 인터페이스 + All()
│ ├─ zone.go # 구역 검증 (DRTAttribute 기반)
│ ├─ vehicle.go # 차량 속성 매칭 (STRETCHER/WHEELCHAIR)
│ ├─ break.go # 기사 휴식 시간 검증 (#26)
│ └─ cap.go # 광역 CAP 초과 시 관내 차량만 허용 (#60)
├─ scorer/
│ ├─ scorer.go # ScorerFn 인터페이스 + Score()
│ ├─ eta.go # ETA 기반 비용
│ ├─ efficiency.go # 기존 동승자 우회 비용
│ └─ cap_balance.go # CAP 균형 유도 페널티
├─ cap/
│ └─ tracker.go # 시군별 관내/광역 실시간 카운터 (Redis)
├─ policy/
│ ├─ model.go # MunicipalityPolicy 구조체
│ └─ loader.go # DB에서 정책 로딩 (5분 캐시)
└─ client/
└─ routing.go # shucle-routing gRPC 클라이언트
매칭 처리 흐름
Match(MatchRequest) 호출 시:
1. 시군 정책 로딩
└─ policy.Load(municipality_id) → MunicipalityPolicy (DB 캐시)
2. 공급 필터링 (4단계, AND 조건)
├─ filter.ByZone(demand) 출발지·목적지 구역 검증
├─ filter.ByVehicle(demand) 와상 → STRETCHER 공간 필요
│ 휠체어 → WHEELCHAIR 공간 필요
├─ filter.ByBreak(threshold) 연속 운행 초과 / 휴식 중 기사 제외
└─ filter.ByCAP(policy, state) 광역 CAP 소진 시 관내 차량만
3. 스코어링 (합산 비용, 낮을수록 우선)
├─ scorer.ETA() 픽업 예상 대기 시간
├─ scorer.RouteEfficiency() 기존 동승자 우회 증가량
└─ scorer.CAPBalance() CAP 목표 초과 시 광역 차량 페널티
4. 최적 공급 선택
└─ 1위 공급으로 shucle-routing.ComputeSharedRoute() 호출
5. CAP 카운터 증가
└─ cap.Tracker.Increment(municipality_id, isRegional)
6. MatchResponse 반환
└─ supply_id + route + changed_demands
CDO 재배차 흐름 (#59)
RunRedispatchLoop() — 매 60초:
모든 활성 demand × 모든 활성 supply 교차 평가
┌─ 현재 배차 ETA vs 대안 공급 ETA
│
├─ 개선량 ≥ 3분 → 재배차 실행
│ ├─ ComputeSharedRoute(새 공급, demand)
│ ├─ dispatch_history 기록 (REASSIGNED)
│ └─ Kafka 이벤트 발행 → apigw → 라이더/기사 알림
│
└─ 개선량 < 3분 → 현 배차 유지
강제배차 흐름 (#44)
ForceAssign(demand_id, supply_id, operator_id, reason):
1. 지정된 공급으로 경로 재산출
2. 다른 수요에 미치는 ETA 영향 계산 (DownstreamImpacts)
└─ 운영툴에서 "A 기사 선택 시 기존 B·C 이용자 +4분" 시각화
3. dispatch_history 기록 (FORCED, operator_id, reason)
4. DownstreamImpacts 반환 → 운영툴 표시 후 확정
DB 스키마
신규/변경 테이블
① municipality_policy — 31개 시군별 정책
CREATE TABLE municipality_policy (
municipality_id VARCHAR PRIMARY KEY, -- 시군 코드 (예: gyeonggi-suwon)
-- 일일 호출 쿼터
max_realtime_calls INT DEFAULT 2,
max_seoul_reserved INT DEFAULT 1,
max_internal_reserved INT DEFAULT 2,
max_regional_reserved INT DEFAULT 2,
-- CAP (#60)
regional_cap_ratio FLOAT DEFAULT 0.2, -- 광역 배차 최대 비율
-- 서비스 시간
service_hours_start TIME,
service_hours_end TIME,
-- 기사 휴식 (#26)
driver_duty_hours FLOAT DEFAULT 2.0, -- 연속 운행 한도 (시간)
driver_rest_minutes INT DEFAULT 15, -- 의무 휴식 (분)
-- 패널티 (#27)
penalty_cancel_threshold INT DEFAULT 6,
penalty_noshow_threshold INT DEFAULT 6,
penalty_reset_days INT DEFAULT 30,
-- 허용 목적지 구역 (호출 유형별 JSON)
eligible_dest_zones JSONB
-- 예: {"SEOUL_RESERVED": ["seoul-zone-1", ...], "REGIONAL_RESERVED": [...]}
);
② user_daily_quota — 일일 호출 쿼터 추적
CREATE TABLE user_daily_quota (
user_id VARCHAR,
date DATE,
call_type VARCHAR, -- REALTIME / SEOUL_RESERVED / INTERNAL_RESERVED / REGIONAL_RESERVED
count INT DEFAULT 0,
PRIMARY KEY (user_id, date, call_type)
);
-- apigw가 수요 생성 전 SELECT FOR UPDATE로 체크, 생성 후 +1
③ user_penalty — 패널티 카운터 (#27)
CREATE TABLE user_penalty (
user_id VARCHAR PRIMARY KEY,
cancel_count INT DEFAULT 0, -- 배차 후 4분 초과 취소 카운트
noshow_count INT DEFAULT 0, -- 기사 도착 후 10분 내 미탑승 카운트
penalty_until TIMESTAMP, -- 이용 정지 종료 시각 (NULL이면 정상)
last_reset_at TIMESTAMP -- 마지막 카운터 리셋 일시
);
-- billing 크론: 30일마다 cancel_count, noshow_count 리셋
④ municipality_cap_state — CAP 실시간 상태 (#60)
CREATE TABLE municipality_cap_state (
municipality_id VARCHAR,
date DATE,
hour INT, -- 0~23 (시간대별 집계)
internal_count INT DEFAULT 0,
regional_count INT DEFAULT 0,
cap_override FLOAT, -- 운영툴 수동 조정값 (NULL이면 정책 기본값)
PRIMARY KEY (municipality_id, date, hour)
);
-- shucle-matching 배차 완료 시 INSERT ON CONFLICT DO UPDATE
⑤ dispatch_history — 배차 이력 감사 로그 (#41)
CREATE TABLE dispatch_history (
id BIGSERIAL PRIMARY KEY,
demand_id VARCHAR NOT NULL,
supply_id VARCHAR,
action_type VARCHAR NOT NULL, -- ASSIGNED / REASSIGNED / FORCED / CANCELLED
operator_id VARCHAR, -- 강제배차 시 운영자 ID
reason TEXT, -- 강제배차 사유
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 당일 배차 수정 불가: application 레벨에서 date(created_at) = today 체크 후 거부
-- 이력 보관: 정책 미확정, 최소 1년 권장
⑥ vehicles 테이블 컬럼 추가 (와상장애인 #58)
ALTER TABLE vehicles
ADD COLUMN has_stretcher BOOLEAN DEFAULT FALSE, -- 와상장애인 전용 공간
ADD COLUMN has_wheelchair_ramp BOOLEAN DEFAULT FALSE,
ADD COLUMN max_wheelchair_seats INT DEFAULT 0;
-- 운영툴에서 차량 등록 시 기사가 선택
-- shucle-matching이 Supply.Spaces를 통해 STRETCHER 허용 여부 판단
미합의 항목 → 구현 위치 매핑
11건 미합의 항목 개발 배치
| # | 항목 | 구현 레포 | 구현 위치 | 비고 |
|---|---|---|---|---|
| #14 | 왕복 일괄 예약 | shucle-apigw | demand_service.go — CreateRoundTripDemand() | 편도 취소 시 왕복 자동 취소 없음 (CapMetro 정책 참조) |
| #26 | 기사 휴게시간 | shucle-matching shucle-operation-tool |
filter/break.go — ByBreak() /special-transport/drivers |
Supply.BreakState 필드로 상태 전달. 시군별 threshold는 MunicipalityPolicy에서 로딩 |
| #27 | 목적지 취소·변경 | shucle-apigw shucle-billing |
demand_service.go — 취소 유형 분기 penalty 테이블 카운터 업데이트 |
3가지 취소 유형(노쇼/도어/늦은취소) 별도 카운트. 6건 초과 시 6일 정지 |
| #30 | 앱미터기 | — | 미구현 (중장기) | 국토부 계량법 형식승인 필요. 정책서 v1.6에서 중장기 과제로 분류됨 |
| #32 | IPCC 연계 | shucle-apigw shucle-operation-tool |
GET /v1/special-transport/rider-by-phone?phone= /special-transport/ipcc |
발신번호 → user 조회 → 최근 예약 이력 반환. 경기교통공사 IPCC가 이 API 호출 |
| #41 | 배차표 이력관리 | shucle-matching shucle-operation-tool |
engine/forced.go — dispatch_history 기록 /special-transport/history (읽기 전용) |
당일 배차 수정 불가는 application 레벨 체크. 감사 로그는 dispatch_history 테이블 |
| #44 | 강제지정배차 | shucle-matching shucle-operation-tool |
engine/forced.go — ForceAssign() /special-transport/dispatch |
추천 목록 제공 + 직접 ID 입력 모두 지원. 영향도(DownstreamImpacts) 미리 표시 후 확정 |
| #53 | 공공클라우드 이관 | 인프라 과제 | 별도 진행 | CSAP 인증 18개월+ 소요. 아키텍처와 독립적으로 진행 |
| #54 | 홈페이지 구축 | 별도 웹 | 별도 진행 | 운영 주체·도메인·콘텐츠 관리자 합의 후 진행 |
| #58 | 와상장애인 관리 | shucle-matching shucle-proto |
filter/vehicle.go — ByVehicle() item.proto STRETCHER 추가 |
Demand.is_bedridden → STRETCHER 공간 있는 차량만 매칭. 차량 속성은 운영툴에서 등록 |
| #59 | 동적 재배차 | shucle-matching | engine/redispatch.go — RunRedispatchLoop() | 매 60초 CDO 실행. 3분 이상 개선 시 재배차. 이용자/기사 알림은 Kafka → apigw |
| #60 | 광역 CAP 관리 | shucle-matching shucle-operation-tool |
filter/cap.go + cap/tracker.go /special-transport/cap |
Redis 기반 실시간 카운터. 운영툴 슬라이더로 즉시 조정 가능 |
개발 우선순위
개발 단계 및 순서
전제
미합의 항목(#14 #26 #27 #44 #59 #60)은 정책 확정 전 기본 구조만 잡고 파라미터화해 DB 정책으로 제어. 합의 즉시 설정값만 변경하면 동작하도록 설계.
Phase 1
Proto + 인프라 기반 — 1~2주
- shucle-proto: taxi.proto GYUNGGI_SPECIAL, item.proto STRETCHER, Demand/Supply 필드 추가
- DB 마이그레이션: municipality_policy, user_daily_quota, user_penalty, municipality_cap_state, dispatch_history 테이블 생성
- 31개 시군 정책 데이터 입력: municipality_policy 초기 데이터 (운영툴 없이 SQL로 입력)
- shucle-matching 레포 생성: gRPC 서버 껍데기 + 기본 매칭 흐름 (필터 없이 첫 번째 공급 반환)
Phase 2
매칭 엔진 핵심 — 2~3주
- filter/zone.go: DRTAttribute 구역 필터 (GYUNGGI_VOUCHER 코드 참고)
- filter/vehicle.go: WHEELCHAIR / STRETCHER 공간 매칭
- filter/break.go: BreakState 기반 연속 운행 검사
- scorer/eta.go: ETA 기반 스코어 (routing 서비스 실 호출)
- policy/loader.go: DB에서 시군 정책 로딩 + 캐시
- apigw 확장: 특교 수요 검증 (구역·쿼터·패널티)
Phase 3
CAP + 강제배차 + 이력 — 2주
- filter/cap.go + cap/tracker.go: 광역 CAP 실시간 관리 (#60)
- engine/forced.go: 강제배차 + dispatch_history 기록 (#44 · #41)
- apigw: IPCC 발신번호 조회 API (#32)
- apigw: 왕복예약 API (#14)
- billing: 패널티 카운터 + 30일 리셋 크론 (#27)
Phase 4
운영툴 + CDO — 2~3주
- engine/redispatch.go: CDO 재배차 루프 (#59)
- operation-tool: CAP 실시간 슬라이더 대시보드 (#60)
- operation-tool: 강제배차 UI (추천 + 직접 입력 + 영향도 표시) (#44)
- operation-tool: 배차표 이력 조회 페이지 (#41)
- operation-tool: 기사 휴식 상태 현황 (#26)
- operation-tool: IPCC 발신번호 팝업 컴포넌트 (#32)
Phase 5
통합 테스트 + 스테이징 검증
- 31개 시군 정책별 매칭 시나리오 E2E 테스트 (shucle-integration)
- 와상장애인 → STRETCHER 차량 자동 매칭 검증
- CAP 소진 시 관내 차량만 배차되는지 검증
- CDO 재배차 발동 조건 검증 (3분 임계값)
- 강제배차 이력 dispatch_history 기록 검증
- 패널티 카운터 누적 + 6일 정지 + 30일 리셋 검증
열린 질문 및 미결 사항
정책 미확정으로 개발 시작 전 결정 필요
- #26 연속 운행 기준: 2시간? 4시간? 시군마다 다른가 → municipality_policy.driver_duty_hours로 파라미터화 예정
- #44 강제배차 방식: "추천 후 선택" vs "직접 지정" → 운영툴에서 양쪽 모두 지원하되 기본값 결정 필요
- #59 재배차 임계값: 3분? 5분? → ReassignThresholdSec 상수, 정책 확정 후 조정
- #60 CAP 조정 권한: 도 단위 관제센터만? 시군 위임 가능? → operation-tool 권한 레벨 설계에 영향
- #41 이력 보관 기간: 1년? 3년? → dispatch_history 파티셔닝 설계에 영향
- IPCC API 스펙: 경기교통공사 IPCC 시스템이 REST? SOAP? 실시간 push? → #32 구현 방식 결정
파라미터화로 정책 미확정 리스크 대응
미확정 값은 모두 municipality_policy 테이블로 분리. 코드 재배포 없이 DB 값 변경만으로 정책 적용 가능. 운영툴에서 시군별 정책 편집 UI 제공 예정.