인천공항 주차장 예측 대시보드 상세 계획

공식 주차 API를 바탕으로 짠 계획입니다. 계획을 세우면서 모델 입력으로 쓸 추가 공공 API 두 개(시간대별 예상 승객 수, 공휴일)도 확인했습니다.

0. 목표와 성공 기준

대시보드가 답할 질문: "내가 ○시에 도착하면 어느 주차장에 자리가 있을까?", 그리고 "그 예측은 실제로 얼마나 맞았나?"

초기 목표치 (4주 운영 후 실제 데이터로 다시 정합니다) → 우선은 모으는 것에 집중합니다. 아래는 임의로 지정한 지표와 목표입니다.

지표목표
1시간 뒤 예측 오차(점유율, 평균)3%p 이하
3시간 뒤 예측 오차(점유율, 평균)5%p 이하
예측 범위(하위 10%~상위 10%) 안에 실제값이 들어온 비율약 80%
"만차 위험" 경보가 실제 만차를 잡아낸 비율80% 이상
수집 성공률월 99% 이상 (빠진 5분 구간 1% 미만)

1. 데이터 소스

공공데이터포털 서비스키 하나로 아래 API를 모두 신청할 수 있습니다.

용도API호출 주기하루 호출 수
예측 대상: 구역별 주차 대수와 총 면수B551177/StatusOfParking/getTrackingParking5분288
선행 지표: 시간대별 예상 출·입국 승객 (오늘·내일)B551177/passgrAnncmt/getPassgrAnncmt1시간약 48
공휴일·연휴 달력한국천문연구원 특일 정보월 1회약 0
(선택) 날씨기상청 단기예보3시간8
  • 합계가 하루 약 350회라서 개발 계정(주차·승객예고 API는 하루 1,000회) 안에 들어옵니다. 공개할 때는 운영 계정으로 올립니다.
  • 예상 승객 수가 중요한 이유: 연휴·명절 효과를 주차 데이터만으로 배우려면 1년이 필요합니다. 공항공사가 매일 내는 오늘·내일 예상 승객 수를 넣으면 첫 연휴부터 대응할 수 있습니다.

구역 유형별로 움직이는 방식이 다릅니다.

  • 장기주차장·주차타워는 며칠에 걸쳐 차가 쌓입니다. 연휴 전에 차오르고 연휴가 끝나면 빠집니다. 그래서 "지난 3일 출국 승객 − 입국 승객" 같은 누적 값을 입력으로 씁니다.
  • 단기주차장은 몇 시간 단위로 들고 납니다. 시간대별 출국 승객 수를 입력으로 씁니다.

2. 아키텍처 (Azure, MCAP 구독)

%%{init: {'theme':'neutral'}}%% flowchart LR API["data.go.kr<br/>주차 · 승객예고 · 특일"] --> C["collect<br/>5분"] API --> X["exog<br/>1시간"] C --> RAW[("Blob raw/<br/>원본 JSON")] C --> OBS[("Blob obs/<br/>일별 JSONL")] X --> EXO[("Blob exog/")] OBS --> FC["forecast<br/>15분"] EXO --> FC FC --> FCS[("Blob forecasts/<br/>발행분 불변 저장")] FCS --> EV["evaluate + publish<br/>1시간 · 매일"] OBS --> EV EV --> PUB[("public/*.json")] C --> PUB PUB --> WEB["Static Web Apps<br/>ECharts 대시보드"] C -.-> AI["App Insights<br/>20분 무수집 알림"]


구성요소선택예상 월 비용
실행Azure Functions Flex Consumption (Python 3.12), 함수 4개무료 제공량 내외, $0~3
저장소Blob Storage (1년치가 수백 MB 수준)$0.5 미만
비밀값Key Vault (서비스키)약 $0.03
모니터링App Insights (하루 사용량 상한 설정)$0~2
웹Static Web Apps Free$0
배포GitHub + Actions(OIDC 인증) + Bicep$0

비용은 합쳐서 월 $1~5 정도로 추정합니다.

이렇게 짠 이유

  • 방문자는 미리 계산해 둔 JSON만 읽습니다. 사람이 몰려도 공공 API 호출 한도나 계산 비용에 영향이 없습니다.
  • 데이터가 하루 약 5,500행(19개 구역 × 288회)으로 작습니다. 그래서 DB 없이 일별 JSONL 파일을 DuckDB로 바로 읽는 구조로 충분합니다.


검토했지만 뺀 선택지

선택지뺀 이유
GitHub Actions 예약 실행실행이 수 분~수십 분 밀리거나 건너뛰는 일이 흔해 5분 수집에 맞지 않음
Azure SQL serverless5분마다 쓰기가 들어와 자동 일시정지가 안 돼서 비용이 커짐
ADX 무료 클러스터시계열 분석 기능은 강하지만 외부 공개가 안 됨. 개인 분석용 보조로는 쓸 수 있음
Power BI 웹에 게시사내 정책상 보통 막혀 있음

먼저 확인할 위험: MCAP 구독에 Blob 공개 접근을 막는 정책이 걸려 있을 수 있습니다. 그렇다면 Function의 HTTP 엔드포인트가 managed identity로 Blob을 읽어 JSON을 캐시와 함께 내주는 방식으로 바꿉니다. 0단계에서 확인합니다.

3. 데이터 모델

테이블주요 컬럼
zones (구역 목록)zone_id, 터미널, 유형(단기/장기/타워/예약), 원본 구역명·별칭, 사용 기간
observations (실측)5분 구간 시각(KST), zone_id, 주차 대수, 총 면수, 가용 대수, 점유율, 원본 갱신 시각(datetm), 수집 시각, 품질 표시
raw_calls (호출 기록)호출 시각, HTTP 상태, 결과 코드, 응답 시간, 원본 파일 경로
calendar (달력)날짜, 요일, 공휴일 여부, 연휴 묶음 번호, 연휴 며칠째, 연휴까지 남은 날
exog_passenger (예상 승객)날짜, 시간, 터미널, 예상 출국·입국 승객, 가져온 시각
forecasts (예측, 덮어쓰지 않음)발행 시각, 모델·버전, zone_id, 대상 시각, 몇 분 뒤 예측인지, 점유율 예측(중앙값·하위 10%·상위 10%), 가용 대수 예측, 만차 확률
accuracy (정확도 집계)날짜, 모델, 구역, 예측 거리 구간, 요일·시간, 평균 오차, 편향, 적중률, 표본 수
  • 첫 구역 목록은 19개입니다(현재 외부 사이트 기준, 1단계 첫 응답으로 확정).
    • T1 10개: 단기 지하1·2·3층·지상층, 장기 P1·P2·P3, P1·P2 주차타워, P5 예약
    • T2 9개: 단기 지하M층·지상1~4층, 장기, P1·P2 주차타워, 예약
  • 예약주차장은 일반 이용자 추천에서 따로 분리합니다.

4. 수집기 (collect, 5분마다)

  1. numOfRows=100, type=json으로 호출합니다. 타임아웃은 10초, 실패하면 2초·5초·10초 간격으로 세 번까지 다시 시도합니다.
  2. 받은 응답을 그대로 raw/parking/YYYY/MM/DD/HHmm.json에 저장합니다. 나중에 가공을 다시 하거나 점검할 때 씁니다.
  3. 정리 과정
    • 구역 이름(floor)을 구역 목록과 맞춥니다. 목록에 없는 이름이 나오면 알림을 보냅니다(신규 주차장이나 이름 변경 감지).
    • 가용 대수와 점유율을 계산합니다. 총 면수가 0이면 미운영으로 처리합니다.
  4. 품질 표시
    • STALE: 원본 갱신 시각이 직전과 같음 (원천 데이터가 멈춤)
    • OVER: 주차 대수가 총 면수보다 많음
    • CAPCHANGE: 총 면수가 바뀜 (공사 등)
    • GAP: 수집이 빠진 구간
  5. obs/YYYY/MM/DD.jsonl에 이어 붙이고, 중복은 읽을 때 걸러냅니다. public/latest.json도 갱신합니다.
  6. 20분 동안 수집 성공이 없거나 원본 갱신 시각이 30분 동안 멈추면 알림을 보냅니다.

5. 예측 모델 (데이터가 쌓이는 만큼 단계적으로)

예측 대상은 구역별 점유율입니다. 총 면수는 공사 등으로 바뀔 수 있어서, 점유율로 예측한 뒤 가용 대수로 바꿉니다. 발행은 15분마다, 15분 간격으로 24시간 뒤까지입니다.

모델방법쓸 수 있는 시점
M0지난주 같은 요일·같은 시각 값 (비교 기준선)수집 1주 뒤
M1최근 4~8주 같은 요일·같은 15분 구간의 가중 중앙값과 상·하위 10% 범위2주 뒤 (4주 뒤 안정)
M1bM1 × (오늘 예상 승객 ÷ 같은 요일 평균 예상 승객)2주 뒤, 연휴 대응
M2M1에 "지금 평소보다 얼마나 다른지"를 더하고, 먼 시간일수록 그 차이를 줄여 반영2주 뒤, 다가오는 몇 시간 예측의 핵심
M3LightGBM 분위수 회귀. 입력: 시간대, 요일, 연휴 정보, 예상 승객, 최근 값(현재·1시간 전·어제·지난주)8~12주 뒤
  • 운영 모델 선택: 모든 모델이 예측을 계속 내고, 최근 14일 오차율이 가장 낮은 모델을 대시보드에 보여줍니다. 구역 유형 × 예측 거리별로 따로 고릅니다.
  • 예측에서 뽑는 결과
    • 가용 대수 예측
    • 만차 확률: 가용 대수가 50대 또는 총 면수의 3% 중 큰 값보다 적을 확률
    • 도착 시각별 여유 구역 순위: 보수적 값(하위 10%) 기준

6. 예측과 실제 비교

  • 예측은 발행 당시 값 그대로 보관합니다. 발행 시각과 모델 버전(git 커밋)을 붙여 저장하고, 나중에 다시 계산해서 덮어쓰지 않습니다. 그래야 "그때 뭐라고 예측했나"를 정직하게 잴 수 있습니다.
  • 평가는 1시간마다 합니다. 대상 시각이 지난 예측을 가장 가까운 5분 실측값과 짝지어 비교합니다.
  • 지표
    • 평균 오차(%p와 대수 두 가지)
    • 편향: 늘 높게 또는 낮게 예측하는지
    • 오차율
    • 예측 범위 적중률
    • 만차 경보가 맞은 비율과 놓친 비율
  • 나눠 보는 기준: 모델, 구역·터미널, 예측 거리(1시간 이내 / 13시간 / 36시간 / 6~24시간), 요일 × 시간
  • 특이 상황 기록: 오차가 컸던 날에 연휴, 데이터 누락, 면수 변경을 자동으로 표시합니다. 폭설이나 공사 같은 메모는 annotations.yaml에 직접 적습니다.
  • 흐름 변화 감지: 최근 7일 편향이 ±5%p를 넘는 날이 3일 이어지면 알림을 보내고 재학습 대상으로 표시합니다.

7. 대시보드 구성

페이지보여줄 내용
앞으로 24시간 (메인)T1/T2 탭. 오늘 실측(실선), 예측 중앙값(점선), 예측 범위(띠)를 겹쳐 표시하고 현재 시각에 세로선. 만차 위험 시간대 강조. 도착 시각 선택 → 여유 예상 구역 순위와 만차 확률, 최근 정확도를 보여주는 신뢰도 표시
요일 × 시간 패턴최근 8주 기준 7×24 히트맵(점유율 / 만차 확률 / 가용 대수 전환), 요일별 하루 흐름 비교
예측 정확도어제·오늘 예측 대 실제 겹쳐 보기, 예측 거리별 오차 추이, 요일×시간 오차 히트맵, 모델 비교, 특이 상황 기록
장기 추이하루 최고 점유율 달력 히트맵, 연휴 3일 전~3일 후 겹쳐 보기, 주간 평균 추이
방법·출처출처 표시("인천국제공항공사, 공공데이터포털"), 비공식 서비스라는 안내, 데이터 수집률
  • 화면은 정적 HTML과 Apache ECharts로 만듭니다. 히트맵, 달력, 범위 띠를 그대로 지원합니다. 모바일을 먼저 생각하고, 필요하면 한/영 전환을 넣습니다.
  • 대시보드가 읽는 JSON 파일
파일갱신 주기내용
latest.json5분구역별 최신값과 오늘 5분 단위 흐름
forecast.json15분앞으로 24시간 예측
accuracy.json1시간정확도 지표
pattern.json매일요일×시간 통계
history_daily.json매일하루 단위 최고·평균 점유율

8. 저장소 구조

icn-parking-forecast/
  infra/        Bicep (main + modules)
  functions/    function_app.py, collect/, exog/, features/, models/, evaluate/, publish/
  web/          index.html, app.js, charts/, i18n/
  config/       zones.yaml, annotations.yaml
  notebooks/    데이터 탐색
  tests/
  .github/workflows/  infra.yml, functions.yml, web.yml

9. 일정

단계기간산출물완료 기준
0단계오늘~내일API 키 3개 발급, 첫 응답으로 구역 목록 확정, 리소스 그룹 결정, Blob 공개 정책 확인실제 응답을 받음
1단계 (최우선)1~3일수집기 가동, 원본 저장, 알림24시간 동안 288번 모두 수집
2단계1~2주대시보드 첫 버전: 오늘 실측 + 어제·지난주 겹쳐 보기, 수집 현황. M0 예측 발행 시작공개 URL로 접속 가능
3단계2~4주M1·M1b·M2, 정확도 페이지, 요일×시간 히트맵, 운영 계정 신청예측 거리별 정확도가 매일 갱신됨
4단계6~12주M3, 만차 확률 보정, 7일 전망운영 모델이 M0보다 20% 이상 정확
5단계3개월 이후장기 추이·연휴 비교, 전날 밤 "내일 만차 위험" 요약 알림연휴 3회 이상 평가 완료

⚠️ 1단계를 서두를 이유: 과거 데이터는 나중에 소급해서 받을 수 없습니다. 이번 주말 개천절 연휴(10/35)나 한글날 연휴(10/911) 전에 수집을 시작해야 첫 연휴 자료를 확보할 수 있습니다.

10. 위험 요소와 대응

위험대응
API 장애·지연재시도, 누락 구간 표시, 알림. 대시보드에 "데이터 지연" 표시
구역 구성 변경 (신규 주차장, 공사)구역 목록에 없는 이름이 오면 알림, 면수 변경 표시, 구역 목록 이력 관리
연휴 데이터 부족예상 승객 수로 보정(M1b), 1년 뒤 연휴 전용 모델로 전환
예측을 공식 정보로 오해"비공식 · 예측값" 안내 문구. 예상 승객 수를 화면에 보여줄 경우 API 설명에 따라 공항공사 관리부서와 안내 문구 협의 필요 (입력으로만 쓰면 협의 부담이 작음)
개발 계정 한도나 기간 만료공개 시점에 운영 계정으로 전환 (자동 승인)
비용 초과App Insights 하루 사용량 상한, 예산 알림

11. 결정할 사항 (괄호 안이 제가 권하는 기본값)

  1. 공개 범위 (외부 전체 공개)
  2. 배포할 구독 (기존 SWA와 같은 MCAP 구독)
  3. 예측 간격 (15분)
  4. 언어 (한국어 먼저, 영어는 5단계)



# 인천공항 주차 예측 대시보드 — 실행 계획

> 최종 갱신: 2026-10-01 KST · 상태: 0단계 대기 (data.go.kr 서비스키 발급 필요)

## 1. 목표

- 과거 요일×시간 패턴으로 **다가오는 시간대**의 주차 여유를 예측한다.
- 발행 시점에 고정한 예측과 실제를 계속 비교해 정확도·편향·흐름 변화를 추적한다.
- 공개 웹 대시보드로 누구나 미리 참고하고 움직일 수 있게 한다.

### 성공 기준 (초기 가설, 4주 운영 후 재설정)

| 지표 | 목표 |
|---|---|
| 1시간 앞 점유율 예측 평균 오차 | ≤ 3%p |
| 3시간 앞 점유율 예측 평균 오차 | ≤ 5%p |
| 예측 범위(P10–P90) 적중률 | ≈ 80% |
| 만차 경보 재현율 | ≥ 80% |
| 수집 가용성 | ≥ 99% / 월 |

## 2. 데이터 소스 (data.go.kr, 서비스키 1개)

| 용도 | 엔드포인트 | 주기 | 호출/일 |
|---|---|---|---|
| 목표변수: 구역별 주차대수·총면수 | `https://apis.data.go.kr/B551177/StatusOfParking/getTrackingParking` | 5분 | 288 |
| 선행지표: 시간대별 예상 출·입국 승객 (D, D+1) | `https://apis.data.go.kr/B551177/passgrAnncmt/getPassgrAnncmt` | 1시간 | ~48 |
| 공휴일·연휴 | `https://apis.data.go.kr/B090041/openapi/service/SpcdeInfoService/getRestDeInfo` | 월 1회 | ~0 |
| (선택) 날씨 | 기상청 단기예보 (data.go.kr 15084084) | 3시간 | 8 |

- 주차 API 요청: `serviceKey`, `numOfRows=100`, `pageNo=1`, `type=json`
- 주차 API 응답: `floor`(구역명), `parking`(주차대수), `parkingarea`(총면수, 0=미운영), `datetm`(`yyyyMMddHHmmss.fff`, KST)
- 조회 당일 현재값만 제공 → **과거 데이터 소급 불가, 수집을 최대한 빨리 시작**
- 개발계정(1,000회/일)으로 충분. 공개 시 운영계정(활용사례 등록, 자동승인)으로 전환
- 승객예고 데이터를 화면에 노출하면 인천공항공사 관리부서와 안내문구 협의 필요 (모델 입력으로만 쓰면 부담 적음)

### 구역 유형별 특성

- 장기주차장·주차타워: 며칠 단위로 쌓이는 **재고형** → 최근 3일 누적(출국 − 입국 예상 승객)이 핵심 입력
- 단기주차장: 시간 단위 **흐름형** → 시간대별 출국 예상 승객이 핵심 입력
- 예약주차장: 일반 이용자 추천에서 분리

## 3. 아키텍처

```mermaid
flowchart LR
  API["data.go.kr<br/>주차 · 승객예고 · 특일"] --> COL["collect<br/>5분"]
  API --> EXO["exog<br/>1시간"]
  COL --> PRIV[("Blob private<br/>raw · obs · exog<br/>forecasts · accuracy · model")]
  EXO --> PRIV
  PRIV --> FC["forecast<br/>15분"]
  FC --> PRIV
  PRIV --> EV["evaluate 1시간<br/>profile 매일"]
  EV --> PRIV
  COL --> WEB[("Blob $web<br/>대시보드 + data/*.json")]
  FC --> WEB
  EV --> WEB
  WEB --> U["브라우저"]
```

| 구성 | 선택 | 비고 |
|---|---|---|
| 실행 | Azure Functions Flex Consumption, Python 3.12, 인스턴스 2,048MB | TZ 설정 미지원 → CRON은 UTC 기준. 실행당 최소 1초 과금 |
| 저장 | Storage Account (StorageV2, Hot, LRS, Korea Central) | private 컨테이너 + `$web` 정적 웹사이트 |
| 서빙 | Storage 정적 웹사이트 (`$web`) | 대시보드와 데이터가 같은 출처 → CORS 불필요 (정적 웹사이트는 CORS 미지원). `$web`은 익명 접근 차단 설정과 무관하게 항상 공개 |
| 비밀 | Key Vault | data.go.kr 서비스키, Function managed identity로 참조 |
| 모니터링 | Azure Monitor 메트릭 알림 2개 + App Insights 최소 설정 | §10 |
| 비용 통제 | Budget 알림 $5 / $10 | |
| 배포 | GitHub + Actions(OIDC) + Bicep | 구독: MCAP, 지역: Korea Central |
| 이번 범위 제외 | Cosmos DB, Static Web Apps, CDN | §14 결정 기록 |

### 함수와 스케줄

| 함수 | CRON (UTC, 6필드) | KST | 역할 |
|---|---|---|---|
| `collect` | `0 */5 * * * *` | 5분마다 | 주차 API 수집 → raw, obs, `$web/data/latest·today` |
| `exog` | `0 5 * * * *` | 매시 05분 | 승객예고 D/D+1 수집, 매월 1일 특일 갱신 |
| `forecast` | `0 2/15 * * * *` | 02·17·32·47분 | 모델 실행 → forecasts(불변 저장), `$web/data/forecast` |
| `evaluate` | `0 10 * * * *` | 매시 10분 | 대상 시각이 지난 예측 vs 실측 → accuracy, `$web/data/accuracy` |
| `profile` | `0 30 18 * * *` | 매일 03:30 | 최근 8주 요일×슬롯 통계 → `model/profile.json`, `$web/data/pattern·history` |

## 4. 저장소 레이아웃

### private 컨테이너

```
raw/parking/YYYY/MM/DD/HHmm.json         호출별 원본 + {http_status, result_code, latency_ms, error}  (PutBlob)
raw/passenger/YYYY/MM/DD/HH.json         승객예고 원본                                             (PutBlob)
obs/YYYY/MM/DD.jsonl                     정규화 실측, 수집 성공 시에만 append                        (AppendBlock, 유일)
exog/passenger/YYYY/MM/DD.json           승객예고 정규화, 하루 파일 덮어쓰기                          (PutBlob)
exog/calendar/YYYY.json                  공휴일·연휴 블록                                          (PutBlob)
forecasts/YYYY/MM/DD/HHmm.json.gz        발행분 불변 저장, 덮어쓰기 금지                             (PutBlob)
accuracy/YYYY/MM/DD.json                 일별 정확도 집계                                          (PutBlob)
model/profile.json                       요일×15분 슬롯 통계, forecast가 읽음                        (PutBlob)
```

- `AppendBlock`은 `obs/`에서만 사용한다 → 적재 중단 알림의 기준 신호 (§10)
- 규모: 실측 5,472행/일, 예측 발행 96회/일 × 19구역 × 96시점, 1년 누적 약 3GB 이하
- 필요 시 월 1회 obs를 Parquet으로 압축 (선택)

### `$web` 컨테이너 (공개)

| 경로 | 갱신 | Cache-Control | 내용 |
|---|---|---|---|
| `index.html`, `app.js`, `style.css` | 배포 시 | `max-age=300` | 대시보드 |
| `data/latest.json` | 5분 | `public, max-age=60` | 구역별 최신값, 마지막 수집 시각, stale 여부 (~3KB) |
| `data/today.json` | 5분 | `public, max-age=60` | 오늘 5분 단위 실측 (~20KB) |
| `data/forecast.json` | 15분 | `public, max-age=300` | 앞으로 24시간 예측, P10/P50/P90, 만차 확률 (~15KB) |
| `data/accuracy.json` | 1시간 | `public, max-age=3600` | 정확도 지표 |
| `data/pattern.json` | 매일 | `public, max-age=3600` | 요일×시간 통계 |
| `data/history.json` | 매일 | `public, max-age=3600` | 일별 최고·평균 점유율 |

- 모든 데이터 JSON은 gzip으로 저장하고 블롭 속성 `Content-Encoding: gzip`, `Content-Type: application/json` 지정
- 크기는 압축 후 기준
- 브라우저는 5분 경계 + 45초에만 갱신, `pattern`·`accuracy`·`history`는 해당 탭을 열 때만 로드
- 데이터 기본 URL은 설정값 하나로 둔다 → CDN 전환 시 코드 변경 없음

## 5. 데이터 모델

| 엔터티 | 주요 필드 |
|---|---|
| `zones` (repo `config/zones.yaml`) | zone_id, terminal(T1/T2), category(단기/장기/타워/예약), raw_names(별칭), active_from, active_to |
| `observations` | slot_ts_kst(5분 내림), zone_id, parking, capacity, available, occupancy, source_ts(datetm), collected_at_utc, flags |
| `raw_calls` | collected_at_utc, http_status, result_code, latency_ms, error, blob_path |
| `calendar` | date, dow, is_holiday, holiday_name, block_id, day_in_block, days_to_block, days_from_block |
| `exog_passenger` | date, hour, terminal, dep_expected, arr_expected, fetched_at |
| `forecasts` | issued_at, model, model_version(git SHA), zone_id, target_ts, horizon_min, p10, p50, p90, available_p50, full_prob |
| `accuracy` | date, model, zone_id/terminal/category, horizon_bucket, dow, hour, mae_pp, mae_cars, bias, wape, coverage, n |

### 초기 구역 (19개, 1단계 첫 응답으로 확정)

- T1 (10): 단기 지하1층·지하2층·지하3층·지상층, 장기 P1·P2·P3, 장기 P1 주차타워·P2 주차타워, P5 예약주차장
- T2 (9): 단기 지하M층·지상1층·지상2층·지상3층·지상4층, 장기 주차장, P1 장기주차타워·P2 장기주차타워, 예약주차장

## 6. 수집기 (`collect`)

1. 주차 API 호출 (`numOfRows=100`, `type=json`), 타임아웃 10초, 재시도 3회 (2초·5초·10초)
2. 원본 + 호출 메타(상태·결과코드·지연·에러)를 `raw/parking/...` 에 PutBlob (실패해도 저장)
3. 정규화
   - `floor` → zone_id 매핑 (공백 제거, 별칭). 미등록 구역명 → 경고 로그 + `unknown_zone` flag
   - available = max(capacity − parking, 0), occupancy = parking / capacity, capacity=0 → 미운영(null)
4. 품질 플래그: `STALE`(datetm 직전과 동일), `OVER`(parking > capacity), `CAPCHANGE`(총면수 변경), `GAP`(누락 구간)
5. 성공 시에만 `obs/YYYY/MM/DD.jsonl` 에 append (중복은 읽을 때 slot+zone 기준 제거)
6. `$web/data/latest.json`, `today.json` 갱신 (datetm 30분 이상 정체 시 `stale: true`)
7. 시각: slot_ts는 KST 5분 내림, 원본 `datetm`과 UTC 수집 시각 모두 보존

## 7. 예측 모델

- 예측 대상: 구역별 점유율 → 최신 총면수로 가용 대수 환산
- 발행: 15분마다, 15분 해상도, 24시간 앞까지
- 터미널·유형 합계는 면수 가중 합산

| 모델 | 방법 | 사용 가능 시점 |
|---|---|---|
| M0 | 지난주 같은 요일·같은 시각 (벤치마크) | 1주 후 |
| M1 | 최근 4~8주 같은 요일·15분 슬롯 가중 중앙값 + 잔차 기반 P10/P90 | 2주 후 (4주 후 안정) |
| M1b | M1 × (당일 예상 승객 ÷ 같은 요일 평균 예상 승객) | 2주 후, 연휴 대응 |
| M2 | M1 + 현재 편차 Δ를 horizon에 따라 감쇠 (Δ·φ^h, φ는 구역 유형별 학습) | 2주 후, 단기 nowcast 핵심 |
| M3 | LightGBM 분위수 회귀 (슬롯, 요일, 연휴 블록, 승객예고, 래그: 현재·1시간 전·어제·지난주) | 8~12주 후 |

- 챔피언: 최근 14일 WAPE 최저 모델을 구역 유형 × horizon 버킷별로 선택, 나머지는 도전자로 계속 발행
- 파생 출력
  - 가용 대수 예측 (P50, 보수적 P10)
  - 만차 확률: P(가용 < max(50대, 총면수 3%))
  - 도착 시각별 여유 구역 순위 (P10 기준)
- 예측 작업은 `model/profile.json` + 오늘 실측 + 승객예고만 읽는다 (8주 재스캔 금지)

## 8. 정확도 추적

- 모든 예측은 발행 시각·모델 버전과 함께 불변 저장, 재계산으로 덮어쓰지 않음
- `evaluate`가 매시 대상 시각이 지난 예측을 가장 가까운 5분 실측(±5분)과 조인
- 지표: 평균 오차(%p, 대수), 편향, WAPE, P10–P90 적중률, pinball loss, 만차 경보 정밀도·재현율
- 분할: 모델 × 구역/터미널/유형 × horizon 버킷(≤1h, 1–3h, 3–6h, 6–24h) × 요일 × 시간
- 이례일 로그: 오차 상위 사례에 연휴·데이터 누락·면수 변경 자동 주석 + `config/annotations.yaml` 수동 메모
- 드리프트: 7일 편향이 ±5%p를 3일 연속 초과 → 경고 로그 + 재학습 플래그

## 9. 대시보드

| 페이지 | 내용 |
|---|---|
| 앞으로 24시간 (메인) | T1/T2 탭, 유형별 차트: 오늘 실측(실선) + 예측 P50(점선) + P10–P90 밴드 + 현재 시각선, 만차 위험 구간 강조. 도착 시각 선택 → 여유 예상 구역 순위, 만차 확률, 최근 정확도 기반 신뢰도 |
| 요일×시간 패턴 | 최근 8주 7×24 히트맵 (점유율 / 만차 확률 / 가용 대수), 요일별 하루 곡선 비교 |
| 예측 정확도 | 어제·오늘 예측 vs 실제, horizon별 오차 추이, 요일×시간 오차 히트맵, 모델 비교, 이례일 로그 |
| 장기 추이 | 일별 최고 점유율 달력 히트맵, 연휴 D-3~D+3 겹쳐 보기, 주간 평균 추이 |
| 방법·출처 | 출처 "인천국제공항공사, 공공데이터포털", 비공식 서비스·예측값 안내, 수집률, 마지막 수집 시각 |

- 기술: 정적 HTML/JS + Apache ECharts, 빌드 도구 없음, 모바일 우선
- 언어: 한국어 우선 (영어는 5단계)

## 10. 모니터링·알림

| 대상 | 방법 | 조건 | 비용 |
|---|---|---|---|
| 데이터 적재 중단 (앱 중단 + API 실패 모두) | Storage 메트릭 `Transactions`, `ApiName = AppendBlock` | 15분 합계 < 1 → 이메일 | ~$0.1/월 |
| 앱 미실행 | Function 메트릭 `OnDemandFunctionExecutionCount` | 20분 합계 < 3 → 이메일 | ~$0.1/월 |
| 원천 데이터 정지 | `latest.json`의 `stale` + 대시보드 "마지막 수집 N분 전" 배지 | datetm 30분 정체 | $0 |
| 코드 예외 | App Insights 최소 설정: 로그 Warning 이상, 샘플링, 일일 상한 50MB, 대시보드·워크북 없음 | — | $0~1/월 |
| 비용 | Budget 알림 | $5 / $10 | $0 |

## 11. 비용 추정 (Korea Central 정가, 2026-10-01 Retail Prices API)

### 단가

| 항목 | 단가 |
|---|---|
| Blob Hot LRS 저장 | $0.02 / GB·월 |
| Blob 쓰기 | $0.05 / 1만 건 |
| Blob 읽기 | $0.004 / 1만 건 |
| 인터넷 전송 | 월 100GB 무료, 이후 $0.11 / GB |
| Functions Flex 실행 | $0.000037 / GB·s (무료 제공량 있음), 실행 $0.40 / 100만 건 |
| Log Analytics 수집 | $3.11 / GB |
| (참고) Cosmos 서버리스 | $0.271 / 100만 RU, 저장 $0.25 / GB·월 |
| (참고) Front Door Standard | 기본료 $35 / 월, 요청 $1.08 / 100만 건, 전송 $0.115 / GB |

### 월 추정 (1년차)

| 항목 | 월 비용 |
|---|---|
| Functions (약 1.3만 실행, 전부 과금 시 약 7.8만 GB·s) | $0 ~ 3 |
| Blob 쓰기 (약 4만 건) | $0.2 |
| Blob 내부 읽기 | $0.06 |
| Blob 저장 (연말 약 3GB) | $0.06 |
| 방문자 서빙 (하루 1만 방문, gzip) | $0.4 |
| Key Vault, 메트릭 알림 2개 | $0.25 |
| App Insights | $0 ~ 1 |
| **합계** | **약 $1 ~ 5** |

### 방문자 수별 서빙 비용 (방문당 파일 3개, gzip 약 50KB)

| 하루 방문 | Blob 직접 | Front Door Standard |
|---|---|---|
| 1천 | $0.04 | 약 $35 |
| 1만 | $0.4 | 약 $38 |
| 10만 (한 달 지속 가정) | 약 $9 | 약 $62 |
| 10만, 압축 안 함 | 약 $75 | — |

## 12. 단계별 일정

| 단계 | 기간 | 산출물 | 완료 기준 |
|---|---|---|---|
| **0단계** | 10/1 | 서비스키 발급(주차·승객예고·특일), 첫 응답으로 구역 마스터 확정, 리소스 그룹·구독 확정 | 실제 응답 수신 |
| **1단계 (최우선)** | 10/1~10/2 | Bicep 인프라, `collect`, raw/obs 저장, `latest·today.json`, 메트릭 알림 2개, App Insights 최소, 예산 알림 | 24시간 288/288 슬롯 수집. 10/3 연휴 전 가동 |
| 2단계 | 1~2주 | 대시보드 v0 (오늘 실측 + 어제·지난주 겹쳐 보기, 수집 현황), `exog`, M0 발행 시작 | `$web` 공개 URL 접속 가능 |
| 3단계 | 2~4주 | `profile`, M1·M1b·M2, `evaluate`, 정확도 페이지, 요일×시간 히트맵, 운영계정 신청 | horizon별 정확도 매일 갱신 |
| 4단계 | 6~12주 | M3, 만차 확률 보정, 7일 전망 | 챔피언이 M0 대비 WAPE 20% 이상 개선 |
| 5단계 | 3개월~ | 장기 추이·연휴 비교, 전날 밤 "내일 만차 위험" 요약, 영어 | 연휴 3회 이상 평가 |

- 10월 연휴(10/3~5 개천절·대체공휴일, 10/9~11 한글날) 전에 1단계를 가동해야 첫 연휴 샘플을 확보한다.

## 13. 위험과 대응

| 위험 | 대응 |
|---|---|
| API 장애·지연 | 재시도, GAP 플래그, 적재 중단 알림, 대시보드 지연 배지 |
| 구역 구성 변경 (신규 주차장, 공사) | 미등록 구역명 경고, CAPCHANGE 플래그, zones 이력 관리 |
| 연휴 데이터 부족 | M1b 승객예고 스케일링, 1년 후 연휴 전용 모델 |
| 예측을 공식 정보로 오인 | "비공식·예측값" 안내, 승객예고 노출 시 공항공사 협의 |
| 개발계정 한도·만료 | 공개 시점에 운영계정 전환 |
| 트래픽 급증·악용 | gzip, Cache-Control, 예산 알림. 기준 초과 시 CDN 전환 (§14 D4) |
| Flex 제약 | TZ 미지원 → CRON UTC, 앱 초기화 30초 제한 → 무거운 import 지연 로딩 |

## 14. 결정 기록

| # | 날짜 | 결정 | 근거 |
|---|---|---|---|
| D1 | 10/1 | 저장소는 Blob 단독 (JSONL·JSON·gzip) | 시계열 append + 범위 통째 읽기 + 정적 배포 패턴. Blob 월 $0.5 미만 vs Cosmos 잘 설계 $1.5~2.5, 단순 설계 $80~250 |
| D2 | 10/1 | Cosmos 병행 보류 | 같은 데이터 이중 쓰기로 비용 절감 없이 복잡도만 증가. 방문자 API 경로는 Function 최소 1초 과금으로 Blob 대비 수십 배. 사용자 기능(알림 구독·즐겨찾기·피드백) 도입 시 그 부분만 Cosmos 서버리스. 저장소 접근은 인터페이스로 분리 |
| D3 | 10/1 | 서빙은 Storage 정적 웹사이트 `$web` (SWA 제외) | 같은 출처라 CORS 불필요, 익명 접근 차단 정책과 무관, 추가 비용 없음 |
| D4 | 10/1 | CDN 보류, CDN-ready 설계 | Front Door 기본료 $35가 전체 비용보다 큼. 전환 기준: 월 전송 100GB 초과 지속, 악용 트래픽, 커스텀 도메인+HTTPS 필요 → Front Door Standard 또는 Cloudflare 무료. Azure CDN classic은 2025-08-15부터 신규 생성 불가 |
| D5 | 10/1 | App Insights 최소 설정 | 분석·대시보드 불필요. 수집 중단 감지는 메트릭 알림으로 해결, App Insights는 코드 예외 기록용 |
| D6 | 10/1 | 예측 15분 발행, 15분 해상도, 24시간 앞 | 다가오는 시간대 중심, 저장량 부담 적음 |
| D7 | 10/1 | MCAP 구독, Korea Central | 데이터·사용자 모두 국내 |

## 15. 다음 행동

- [ ] (사용자) data.go.kr 활용신청: 인천국제공항공사_주차 정보(15095047), 승객예고-출·입국장별(15095066), 한국천문연구원_특일 정보(15012690)
- [ ] (사용자) 구독·리소스 그룹 이름 확정
- [ ] 첫 응답으로 `config/zones.yaml` 확정
- [ ] 1단계 착수: repo 생성 → Bicep → `collect` → 알림 → 배포 → 24시간 검증

## 16. 저장소 구조 (repo)

```
icn-parking-forecast/
  infra/                 main.bicep, modules/ (storage, function, keyvault, monitor, budget)
  functions/             function_app.py, collect/, exog/, forecast/, evaluate/, profile/, common/storage.py
  web/                   index.html, app.js, style.css, charts/
  config/                zones.yaml, annotations.yaml
  notebooks/             탐색 분석
  tests/
  .github/workflows/     infra.yml, functions.yml, web.yml
```


  • 레이블 없음