Gyeonggi Special Transportation — System Architecture

경기도 특교
시스템 아키텍처 및 개발 설계

shucle 플랫폼 기반 경기도 특별교통수단 구현을 위한 레포별 확장 계획, 신규 매칭 엔진 설계, DB 스키마, 개발 우선순위 정리
📅 2026-04-09 🎯 31개 시군 · 미합의 11건 포함 🔧 신규 레포 1개 · 기존 레포 4개 확장
신규: shucle-matching 확장: apigw · billing · operation-tool Proto 변경: taxi · item · event DB 신규 테이블 5개

현황

  • 경기도 특별교통수단: 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 추가만

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 반환 → 운영툴 표시 후 확정

신규/변경 테이블

① 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 제공 예정.