제3장

제3장: 예측 요청·결과와 경보 메시지 포맷

제2장이 센서와 운영자가 관측한 것을 다루었다면, 이 장은 예측 시스템이 미래에 대해 말하는 것과 경보 권한기관이 주민에게 전하는 것을 다룬다. 대상은 FloodPredictionRequest와 FloodPredictionResult, 5단계 위험 단계, FloodAlert 경보 메시지와 그 공통 경보 프로토콜(CAP) 1.2 매핑, 그리고 대피 지시를 실제로 실행할 수 있게 해 주는 대피소·대피 경로 객체이다.

이 객체들은 WIA-ENE-033의 PHASE-1 §5.1–5.2와 PHASE-3 §7.2에 정의되어 있고, 참조 SDK(api/typescript/src/types.ts)에 TypeScript 타입으로 다시 정의되어 있다. 두 원천은 대부분 일치하지만 전부 일치하지는 않으며, 시스템 경계에서 메시지를 검증하는 사람에게는 그 차이가 중요하다. 이 장은 차이가 있는 곳마다 이를 밝히고 해석 방안을 제시한다. 이 장의 모든 JSON 문서는 예시이다. 지역명, 좌표, 인구, 확률은 서로 모순되지 않도록 지어낸 값이며 실제 사건에서 가져온 것이 아니다.

3.1 예측 요청: FloodPredictionRequest

예측 요청은 세 가지 물음에 답한다. 어디를, 얼마나 앞까지, 어떤 근거로 예측할 것인가. 요청 객체는 의도적으로 작게 설계되었다. 호출자는 관측값을 보내지 않는다. 예측 노드는 제2장에서 다룬 강우·하천·댐·배수 데이터를 이미 보유하고 있거나 스스로 가져오는 것으로 전제되며, 요청은 그 가운데 어떤 원천을 고려할지만 알려 준다.

3.1.1 필드

필드타입명세 (PHASE-1 §5.1)SDK (types.ts)의미
locationLocation필수필수대상 지역. regionId, name, coordinates, 선택적 boundary(GeoJSON). SDK 타입은 선택적 address도 허용한다.
timeHorizon'1h' | '6h' | '24h' | '72h'필수필수예측 시간 범위.
resolution'coarse' | 'medium' | 'fine'필수선택결과, 특히 침수 범위의 공간 해상도.
includePrecipitationboolean필수선택강우 관측·예보를 사용한다.
includeRiverLevelsboolean필수선택수위·유량 관측을 사용한다.
includeDamStatusboolean필수선택저수위, 방류량, 방류 계획을 사용한다.
includeDrainageboolean필수선택배수 펌프장·하수관로·저류지 상태를 사용한다.
includeTopographyboolean필수선택지형 자료를 사용한다. 침수 지도를 만들려면 전제 조건이다.
runSimulationboolean필수선택2차원 침수 모의와 시나리오 분석을 수행한다.
simulationScenariosstring[]선택선택평가할 시나리오 이름.

3.1.2 예측 시간 범위와 공간 해상도

네 가지 시간 범위 값은 임의로 정해진 것이 아니다. README는 각 범위를 서로 다른 근거와 연결한다. 1~6시간 앞은 레이더 기반 초단기 강우 예측과 이미 하도에 들어온 물이 지배하고, 6~24시간은 수치 기상 모델과 예측 모델의 결합이 필요하며, 24~72시간은 앙상블 기상 예보에 의존한다. 따라서 같은 유역에 대해 72h를 요청한 쪽은 6h를 요청한 쪽보다 낮은 confidence와 더 넓은 시나리오 간 편차를 예상해야 한다. 표준은 "사용자 지정" 범위를 정의하지 않는다. 12시간이 필요한 구현은 24h를 선택하고 결과의 시간별 값을 읽거나, 상호운용 프로필에서 열거값을 확장해야 한다(제7장).

resolution이 정성적인 값인 것도 의도된 것이다. 달성 가능한 해상도는 노드가 가진 지형 모델에 달려 있으므로 표준은 'fine'을 특정 격자 크기에 묶지 않는다. 대신 각 배포는 상호운용 프로필에 이 값들이 현장에서 무엇을 뜻하는지 밝혀야 한다. 예를 들면 "coarse = 유역 단위 요약, medium = 동네 단위 다각형, fine = 가구(街區) 단위 다각형"과 같은 식이다. 이런 선언이 없으면 적합한 두 노드가 같은 요청에 전혀 다른 지도를 돌려줄 수 있다.

3.1.3 데이터 원천 플래그는 필수인가 선택인가

PHASE-1은 여섯 개의 include* 플래그와 runSimulation에 물음표를 붙이지 않았으므로 명세상 필수이다. 반면 SDK는 이들을 선택으로 표시한다. 명세대로 만든 서버는 플래그를 생략한 SDK 요청을 거부하고, SDK대로 만든 서버는 명세가 무효로 보는 요청을 받아들인다. 두 정의가 조정되기 전까지는 다음 방식이 안전하다.

다음 요청은 모든 데이터 원천과 세 가지 표준 시나리오를 사용하는 24시간 예측을 요구한다.

{
  "location": {
    "regionId": "REG-SEOUL-001",
    "name": "서울특별시 강남구 (예시 / example)",
    "coordinates": { "latitude": 37.5172, "longitude": 127.0473 }
  },
  "timeHorizon": "24h",
  "resolution": "medium",
  "includePrecipitation": true,
  "includeRiverLevels": true,
  "includeDamStatus": true,
  "includeDrainage": true,
  "includeTopography": true,
  "runSimulation": true,
  "simulationScenarios": ["bestCase", "mostLikely", "worstCase"]
}

3.2 예측 결과: FloodPredictionResult

예측 결과는 한 지역, 한 시간 범위에 대한 예측 노드의 진술이다. 첫 블록은 예측을 식별하고, 둘째 블록은 위험을 평가하며, 셋째 블록은 침수 범위를 추정하고, 나머지 블록은 그 이유와 대응 방안을 설명한다.

3.2.1 식별자와 시각

predictionId는 GET /api/v1/flood/prediction/{id}가 사용하는 고정 식별자이다. v1.0의 FloodAlert에는 이 식별자를 가리키는 필드가 없으므로, 예측에서 경보를 도출한 발령 기관은 그 연결을 감사 로그에 직접 기록하고, CAP로 보낼 때는 이름 있는 parameter로 실어야 한다. timestamp는 예측을 발행한 시각이며 관측 시각이 아니다. 관측 시각은 contributingFactors 안에 따로 있다. peakTime은 해당 지역에서 상황이 가장 나빠질 것으로 예상되는 시각이다. 세 시각 모두 제2장의 ISO 8601 UTC 규칙을 따른다.

3.2.2 probability와 confidence의 차이

두 필드는 모두 0~100 % 범위여서 혼동하기 쉽지만 서로 다른 물음에 답한다.

확률은 높은데 신뢰도가 낮을 수 있고("돌아간 구성원은 모두 홍수를 예측했지만 수위 관측소의 절반이 꺼져 있었다"), 확률은 낮은데 신뢰도가 높을 수도 있다("모든 구성원이 수위가 경계 기준 아래에 머문다는 데 일치한다"). 명세는 두 값을 더 자세히 정의하지 않으므로 배포마다 계산 방식을 문서로 밝혀야 한다. 합리적인 선택은 임계값을 넘는 앙상블 구성원의 비율로 probability를 구하고, 제5장에서 다루는 앙상블 불확실성과 데이터 완전성으로 confidence를 구하는 것이다. 수신 측은 두 값을 곱해 하나의 숫자로 만들어서는 안 된다. probability는 경보 단계를, confidence는 발령 전에 사람의 검토가 필요한지를 결정하는 데 각각 쓰인다.

3.2.3 inundationArea

필드단위의미와 주의점
estimatedAreakm²첨두 시점에 물에 잠길 것으로 예상되는 평면 면적.
maxDepthm범위 안의 최대 침수 심도. 인명 안전 메시지를 좌우한다. 면적은 좁아도 깊은 곳이 넓고 얕은 곳보다 위험할 수 있다.
averageDepthmestimatedArea에 대한 평균 심도. README 예시에는 빠져 있지만 명세와 SDK 모두 필수로 정의한다.
affectedPopulation명침수 범위 안의 추정 거주 인구. 보통 인구 격자와 중첩해 산출한다.
affectedBuildings동침수 범위와 겹치는 건물 수.
geoJsonGeoJSON침수 범위 도형. 보통 [경도, 위도] 좌표의 Polygon 또는 MultiPolygon이다. README 예시는 여기에 문자열 "..."을 넣었는데, 이는 유효한 도형이 아니다.

runSimulation이 false였다면 노드에는 침수 범위를 산정할 수리학적 근거가 없다. 스키마가 요구하므로 객체 자체는 반환하되, 빈 도형 컬렉션과 0인 수치를 담고 recommendations에 모의를 수행하지 않았다고 밝혀야 한다. 모의 없이 짐작한 다각형을 돌려주는 것은 아무것도 돌려주지 않는 것보다 나쁘다.

3.2.4 전체 예시

다음 결과는 앞의 요청과 일관되게 구성한 예시이다. 수위는 4.2 m로 경계 기준 4.5 m, 위험 기준 5.0 m보다 낮다. 시간당 0.15 m씩 오르고 있지만 상승이 둔화하고 있으며, 예측 첨두 수위 4.8 m는 위험 기준에 이르지 않는다. 모델은 이 지역을 확률 65.5 %의 LEVEL-2로 평가한다. 상류 저수지에는 예정된 방류가 없으므로 damStatus는 빈 배열이다. 키 이름이 contributingFactors라는 점에 유의해야 한다. README 예시는 공백을 넣어 "contributing factors"로 적었는데, 스키마 기반 파서는 이를 받아들이지 않는다.

{
  "predictionId": "PRED-2026-000123",
  "timestamp": "2026-07-15T01:30:00Z",
  "location": {
    "regionId": "REG-SEOUL-001",
    "name": "서울특별시 강남구 (예시 / example)",
    "coordinates": { "latitude": 37.5172, "longitude": 127.0473 }
  },
  "riskLevel": "LEVEL-2",
  "probability": 65.5,
  "confidence": 72.0,
  "peakTime": "2026-07-15T09:00:00Z",
  "inundationArea": {
    "estimatedArea": 2.5,
    "maxDepth": 0.8,
    "averageDepth": 0.3,
    "affectedPopulation": 15000,
    "affectedBuildings": 3200,
    "geoJson": {
      "type": "Polygon",
      "coordinates": [[
        [127.040, 37.500], [127.060, 37.500], [127.060, 37.515],
        [127.040, 37.515], [127.040, 37.500]
      ]]
    }
  },
  "contributingFactors": {
    "precipitation": {
      "observed": { "current": 25.5, "last1h": 24.0, "last3h": 58.0,
                    "last6h": 83.0, "last24h": 131.0 },
      "forecast": { "next1h": 28.0, "next3h": 70.0, "next6h": 110.0,
                    "next12h": 150.0, "next24h": 175.0,
                    "hourly": [28.0, 22.0, 20.0, 16.0, 13.0, 11.0] },
      "statistics": { "returnPeriod": 20, "percentile": 95.0, "isExtreme": false }
    },
    "riverLevels": [{
      "riverId": "HAN-001",
      "name": "Example tributary gauge",
      "stationId": "HAN-001-ST05",
      "location": { "latitude": 37.5010, "longitude": 127.0580 },
      "current": { "waterLevel": 4.2, "flowRate": 850.0, "velocity": 2.1,
                   "timestamp": "2026-07-15T01:20:00Z" },
      "levels": { "normal": 2.0, "attention": 3.5, "warning": 4.5,
                  "danger": 5.0, "bankHeight": 6.0 },
      "forecast": { "peak": 4.8, "peakTime": "2026-07-15T08:00:00Z",
                    "hourly": [4.35, 4.48, 4.59, 4.68, 4.75, 4.79] },
      "trend": { "rateOfRise": 0.15, "direction": "rising", "acceleration": -0.01 },
      "status": { "condition": "attention", "overflowRisk": 20.0 }
    }],
    "damStatus": [],
    "drainage": {
      "facilityId": "DRN-GN-007",
      "name": "Example pump station",
      "type": "pump-station",
      "location": { "latitude": 37.5065, "longitude": 127.0512 },
      "capacity": { "design": 60.0, "current": 45.0, "utilization": 75.0 },
      "pumps": { "total": 8, "active": 6, "standby": 1, "failed": 1, "efficiency": 88.0 },
      "operational": { "status": "active",
                       "lastMaintenance": "2026-05-20T00:00:00Z",
                       "nextMaintenance": "2026-11-20T00:00:00Z",
                       "alerts": ["pump-5 failure"] },
      "performance": { "flowRate": 45.0, "energyUsage": 1850.0, "reliability": 87.5 }
    },
    "soilSaturation": 82.0
  },
  "recommendations": [
    "저지대 주민 대피 준비 / Prepare low-lying residents for evacuation",
    "지하 주차장 차량 이동 / Move vehicles out of underground car parks",
    "배수 시설 가동 점검 / Check drainage facility operation"
  ],
  "evacuationRequired": false,
  "affectedAreas": ["REG-SEOUL-001"]
}

이 예시에서 눈여겨볼 점이 두 가지 있다. 첫째, 선택 필드인 status.estimatedTimeToOverflow를 생략했다. 직선 외삽(남은 0.8 m를 시간당 0.15 m로 나누면 약 5.3시간)은 첨두 수위가 위험 기준에 이르지 않는다는 예측과 모순된다. 두 숫자를 함께 내보내면 주민은 서로 다른 두 이야기를 듣게 된다. 둘째, evacuationRequired가 false이다. LEVEL-2에서 권고되는 조치는 이동이 아니라 준비이다. 참조 SDK의 isEvacuationRequired 도우미는 LEVEL-3 이상이면서 확률 60 % 이상일 때만 true를 돌려주는데, 이 기준이 검증된 규칙이 아니라 기본값인 이유는 제5장에서 다룬다.

3.3 예측의 근거: 기여 요인, 시나리오, 권고 사항

3.3.1 contributingFactors

이 블록은 제2장의 관측 객체를 예측에 쓰인 그대로 포함한다. PrecipitationData 하나, RiverLevelData 배열, DamStatusData 배열, DrainageData 하나, 그리고 선택적인 soilSaturation(%)이다. 참조가 아니라 포함하는 방식을 택한 데에는 결과가 따른다. 장점은 각 결과가 그 자체로 완결되고 감사 가능하다는 것이다. 검토자는 실시간 데이터가 나중에 보정되더라도 발행 시점에 모델이 본 수위와 강우를 정확히 확인할 수 있다. 대가는 크기이다. 수위 관측소 20곳과 저수지 5곳이 있는 유역이라면 문서가 커지고, 저장되는 모든 예측이 입력의 사본을 함께 지닌다.

구조적 한계도 두 가지 알아 둘 필요가 있다. drainage는 배열이 아니라 단일 객체이므로, 펌프장이 여러 곳인 도시는 가장 관련 깊은 한 곳을 고르거나 여러 곳을 집계해야 한다. 이 책은 가장 제약이 큰 시설을 보고하고 집계 사실을 recommendations에 적기를 권하며, 이 문제를 v1.1의 개정 후보로 제기할 것을 권한다. 또 빈 배열은 의미를 가진다. "damStatus": []는 "이 지역에 영향을 주는 댐이 없다"는 뜻일 수도, "includeDamStatus가 false여서 댐 데이터를 제외했다"는 뜻일 수도 있으므로, 노드의 로그에서 두 경우를 구별할 수 있어야 한다.

3.3.2 scenarios

모의를 수행하면 결과에 bestCase, mostLikely, worstCase 세 개의 ScenarioResult를 실을 수 있다. 각 시나리오는 이름, 확률, 첨두 시각, 자체 inundationArea를 가진다.

"scenarios": {
  "bestCase": {
    "scenarioName": "bestCase",
    "probability": 20.0,
    "peakTime": "2026-07-15T07:00:00Z",
    "inundationArea": { "estimatedArea": 0.4, "maxDepth": 0.3, "averageDepth": 0.1,
                        "affectedPopulation": 1800, "affectedBuildings": 350,
                        "geoJson": { "type": "Polygon",
                          "coordinates": [[[127.048,37.505],[127.052,37.505],[127.052,37.508],[127.048,37.508],[127.048,37.505]]] } }
  },
  "mostLikely": {
    "scenarioName": "mostLikely",
    "probability": 60.0,
    "peakTime": "2026-07-15T09:00:00Z",
    "inundationArea": { "estimatedArea": 2.5, "maxDepth": 0.8, "averageDepth": 0.3,
                        "affectedPopulation": 15000, "affectedBuildings": 3200,
                        "geoJson": { "type": "Polygon",
                          "coordinates": [[[127.040,37.500],[127.060,37.500],[127.060,37.515],[127.040,37.515],[127.040,37.500]]] } }
  },
  "worstCase": {
    "scenarioName": "worstCase",
    "probability": 20.0,
    "peakTime": "2026-07-15T10:00:00Z",
    "inundationArea": { "estimatedArea": 6.1, "maxDepth": 1.6, "averageDepth": 0.6,
                        "affectedPopulation": 41000, "affectedBuildings": 8900,
                        "geoJson": { "type": "Polygon",
                          "coordinates": [[[127.030,37.495],[127.065,37.495],[127.065,37.520],[127.030,37.520],[127.030,37.495]]] } }
  }
}

여기서 시나리오 확률은 세 시나리오의 가중치(20 %, 60 %, 20 %)이며 합이 100 %가 된다. 최상위 probability(65.5 %)는 다른 양이다. 제시된 위험 단계의 사건이 일어날 가능성이다. 표준은 시나리오 확률의 합이 100 %여야 하는지, 최상위 값과 어떤 관계인지 정하지 않았으므로, 생산자는 자신의 규칙을 밝히고 수신자는 어떤 규칙도 가정하지 말아야 한다. 다만 mostLikely가 최상위 inundationArea와 같은 범위를 기술해야 한다는 점은 분명하다. 둘이 다르면 수신자는 지도에 어느 것을 그려야 할지 알 수 없다.

최악 시나리오는 확률이 낮더라도 대피소 수용력 산정과 대응 인력 사전 배치에 쓰인다. 최악의 경우 영향 인구가 41,000명인데 대피소 수용력은 그 일부에 불과하다는 사실을 본 경보 담당자는, 가장 가능성 높은 시나리오만으로는 알 수 없는 것을 알게 된다.

3.3.3 recommendations, evacuationRequired, affectedAreas

recommendations는 자유 텍스트 배열이다. 운영자에게는 유용하지만 기계가 처리할 수 있는 형태가 아니며, 표준은 언어도 정하지 않는다. 이중 언어 배포라면 예시처럼 각 문자열에 두 언어를 함께 적거나, 권고 사항은 운영자의 언어로만 적고 주민용 문구는 FloodAlert.instructions에 맡기는 것이 가장 단순하다. evacuationRequired는 예측 노드의 조언일 뿐 명령이 아니다. 사람을 이동시키는 것은 FloodAlert를 통한 경보 권한기관뿐이다. affectedAreas는 location.regionId와 같은 형식의 지역 식별자 목록이며, 하천 구간 하나에 대한 결과를 여러 자치단체 지역으로 나누어 전달할 때 쓰인다.

3.4 5단계 위험 단계: LEVEL-0~LEVEL-4

RiskLevel은 LEVEL-0부터 LEVEL-4까지의 문자열 열거형이다. SDK의 formatRiskLevel 도우미는 각 값에 한국어 표기, 영어 표기, 표시 색상을 붙인다. 아래 표의 "대표 조치" 열은 README의 표를 따랐다.

값한국어 표기영어 표기색상대표 조치 (README)
LEVEL-0정상Normalgreen일상 모니터링
LEVEL-1관심Attentionblue상황 주시
LEVEL-2주의Cautionyellow주민 안내
LEVEL-3경계Warningorange대피 준비
LEVEL-4심각Severered긴급 대피

한국어 표기는 국가 위기경보의 4단계 어휘(관심/주의/경계/심각)를 의도적으로 따르고, 평상 상태를 뜻하는 정상을 더했다. 이것은 홍수통제소가 발표하는 공식 홍수특보 용어(홍수주의보, 홍수경보)가 아니며, 그렇게 제시해서도 안 된다. 대응 관계는 제1장에서 설명했다. 메시지를 설계할 때의 규칙은 간단하다. ENE-033 표기는 단계 코드와 함께 표시하고, 공식 홍수특보가 함께 발효 중이라면 그것은 발표 기관을 밝힌 별도의 진술로 인용한다.

색상만으로 의미를 전달해서는 안 된다. 색각 이상 사용자를 위해서도, 같은 색이 다른 경보 체계에서 다른 뜻을 갖기 때문에도, 단계를 표시할 때는 언제나 표기와 단계 번호를 함께 보여야 한다.

함정: 같은 낱말, 한 단계 차이

README의 단계 표는 LEVEL-1을 "주의 수위 도달", LEVEL-2를 "경계 수위 도달", LEVEL-3을 "위험 수위 도달"과 연결한다. 그런데 LEVEL-2의 표기는 "주의", LEVEL-3의 표기는 "경계"이다. 즉 "주의"라는 표기는 LEVEL-2인데 "주의 수위"는 LEVEL-1을 촉발하고, "경계"라는 표기는 LEVEL-3인데 "경계 수위"는 LEVEL-2를 촉발한다. 낱말끼리 맞추어 읽으면 한 단계씩 어긋난다. 여기에 RiverLevelData.levels의 필드 이름(attention, warning, danger, bankHeight)이 세 번째 어휘로 더해지고, 영어 "Warning"은 LEVEL-3의 표기인데 levels.warning은 LEVEL-2를 촉발한다.

그래서 이 책은 낱말이 아니라 필드 이름을 기준으로 한 명시적 규칙을 제시한다. README의 순서를 따르지만 v1.0의 규범 문구가 아니라 권고안이다.

이 규칙은 관측 수위와 예측 첨두 수위 모두에 적용하며, 그 결과는 하한이다. 모델이 산출한 단계는 이 하한을 올릴 수는 있어도 관측소가 이미 보여 주는 수준 아래로 내려서는 안 된다.

이 규칙과 3.6절에서 쓰는 CAP 변환 도우미를 간결하게 구현하면 다음과 같다. 단계 비교는 숫자 색인으로 한다. 문자열을 직접 비교해도('LEVEL-3' >= 'LEVEL-2') 사전순 정렬 덕분에 우연히 맞지만, 프로필이 LEVEL-10 같은 값이나 접미사를 도입하면 소리 없이 깨진다.

import { RiskLevel, ReferenceLevels } from './types';

// Book-recommended rule (not normative in v1.0):
// the observed or forecast stage sets a floor for the risk level,
// using the field names of RiverLevelData.levels, never the Korean labels.
const LEVEL_INDEX: Record<RiskLevel, number> = {
  [RiskLevel.LEVEL_0]: 0, [RiskLevel.LEVEL_1]: 1, [RiskLevel.LEVEL_2]: 2,
  [RiskLevel.LEVEL_3]: 3, [RiskLevel.LEVEL_4]: 4,
};
const BY_INDEX = [RiskLevel.LEVEL_0, RiskLevel.LEVEL_1, RiskLevel.LEVEL_2,
                  RiskLevel.LEVEL_3, RiskLevel.LEVEL_4];

export function stageFloor(stage: number, lv: ReferenceLevels): RiskLevel {
  if (stage >= lv.bankHeight) return RiskLevel.LEVEL_4;
  if (stage >= lv.danger)     return RiskLevel.LEVEL_3;
  if (stage >= lv.warning)    return RiskLevel.LEVEL_2;
  if (stage >= lv.attention)  return RiskLevel.LEVEL_1;
  return RiskLevel.LEVEL_0;
}

// Final level = the higher of the model's level and the stage floor.
export function combine(modelLevel: RiskLevel, floor: RiskLevel): RiskLevel {
  return BY_INDEX[Math.max(LEVEL_INDEX[modelLevel], LEVEL_INDEX[floor])];
}

// ENE-033 lower-case enums -> CAP 1.2 capitalised enumerations
const cap = (v: string) => v.charAt(0).toUpperCase() + v.slice(1);
export const toCapSeverity  = cap;   // 'severe'   -> 'Severe'
export const toCapUrgency   = cap;   // 'expected' -> 'Expected'
export const toCapCertainty = cap;   // 'likely'   -> 'Likely'

// CAP forbids the "Z" designator; UTC is written as "-00:00".
export function toCapTime(iso: string): string {
  return new Date(iso).toISOString().replace(/\.\d{3}Z$/, '-00:00');
}

// GeoJSON rings are [lon, lat]; CAP polygons are "lat,lon" pairs.
export function toCapPolygon(ring: [number, number][]): string {
  return ring.map(([lon, lat]) => `${lat},${lon}`).join(' ');
}

앞의 예시에 적용하면, 관측 수위 4.2 m는 attention(3.5 m)과 warning(4.5 m) 사이이므로 관측 하한은 LEVEL-1이고, 예측 첨두 4.8 m는 warning을 넘으므로 예측 하한은 LEVEL-2이다. 모델의 LEVEL-2는 두 하한과 모두 일치한다. 모델이 LEVEL-1을 내놓았더라도 결합 단계는 LEVEL-2가 된다.

3.5 경보 메시지: FloodAlert

예측은 내부의 기술 산출물이고, 경보는 권한기관이 행하는 공적 행위이다. 표준이 둘을 별도 객체로 둔 이유가 여기에 있다. FloodAlert에는 신원이 밝혀진 발령 주체, 명시적인 유효 기간, 시스템이 아니라 사람을 위해 쓴 행동 요령이 담긴다. 참조 API에서 경보는 POST /api/v1/alert/create로 생성하며, SDK의 createAlert는 서버가 부여하는 alertId와 timestamp를 제외한 모든 필드를 받는다. API의 세부는 제4장에서 다루고, 여기서는 내용에 집중한다.

3.5.1 구조

블록필드목적
식별alertId, timestamp고유 식별자와 발령 시각. 둘 다 서버가 부여한다.
issuerorganizationId, name, authorityLevel법적으로 누가 말하는가. v1.0에서 authorityLevel은 자유 텍스트(예: national, basin, municipal)이므로 배포마다 허용값을 정해야 한다.
alertlevel, severity, urgency, certaintyENE-033 단계와 CAP 방식의 세 가지 한정어.
affectedAreasregionIds, geoJson, population, buildings경보가 적용되는 곳과 그 안의 인구·건물 수.
eventtype, onset, expiry, peakTime홍수의 유형과 시작·첨두·종료 예상 시각.
instructionsko, en, actions, evacuationRequired, evacuationZones이중 언어 행동 요령, 기계 판독용 조치 코드, 대피 구역.
shelters배열 (선택)어디로 가야 하는가.
contactemergency, information, website어디에 연락하고 어디서 더 볼 수 있는가.

3.5.2 한정어: 단계, 심각도, 긴급도, 확실도

단계는 ENE-033 척도에서 홍수 상황이 얼마나 심각한지를 말한다. 세 한정어는 같은 내용을 전 세계 경보 시스템이 공유하는 어휘로 말한다. severity(minor, moderate, severe, extreme)는 예상 피해의 크기, urgency(immediate, expected, future)는 얼마나 빨리 행동해야 하는지, certainty(observed, likely, possible)는 발령 기관이 얼마나 확신하는지이다. 표준은 단계와 심각도 사이의 대응을 정의하지 않는다. 아래 표는 배포가 채택해 프로필에 공개할 수 있는 예시이며 규범이 아니다.

단계심각도 (예시)긴급도 도출확실도 도출
LEVEL-0(경보 없음, 발효 중인 경보는 해제)——
LEVEL-1minorevent.onset 자체가 아니라 보호 조치를 시작해야 하는 시점 기준: 지금 → immediate, 대략 한 시간 안 → expected, 그 이후 → future수위가 이미 기준을 넘음 → observed, probability 약 50 % 초과 → likely, 그 밖 → possible
LEVEL-2moderate
LEVEL-3severe
LEVEL-4extreme

위의 긴급도·확실도 규칙은 CAP 1.2의 정의를 따른 것이다. CAP에서 "Expected"는 곧(다음 한 시간 안에) 대응 조치를 취해야 함을, "Likely"는 대략 50 %를 넘는 확률을, "Possible"은 가능하지만 가능성이 높지는 않음을 뜻한다. CAP 정의를 그대로 쓰면 ENE-033 경보가 수신 시스템이 다루는 다른 모든 경보와 일관성을 유지한다.

3.5.3 사건 유형

명세는 river-flood, urban-flood, coastal-flood, flash-flood 네 가지 사건 유형을 허용하며, 이는 제1장의 네 가지 홍수 유형과 일치한다. SDK의 FloodEventType 열거형은 다섯 번째 값 dam-discharge를 추가한다. 명세를 엄격히 따르는 검증기는 이 값을 거부한다. 저수지의 계획 방류는 하류 주민에게 경보를 보내는 흔한 이유이므로 이 값은 쓸모가 있다. 명세에 추가되기 전까지 이식성 있는 선택은 river-flood를 보내고 행동 요령 본문에 방류 사실을 적는 것이며, 양측이 지원하는 경우에 한해 dam-discharge를 프로필 확장으로 선언하는 것이다.

3.5.4 행동 요령과 유효 기간

instructions.ko와 instructions.en은 사람들이 실제로 읽는 문장이므로 짧고 구체적이어야 하며, 직역이 아니라 각 언어로 자연스럽게 써야 한다. 무엇을, 어디서, 언제까지 해야 하는지가 모두 들어가야 한다. actions는 예컨대 모바일 앱이 해당 버튼을 강조하는 데 쓸 수 있는 기계 판독용 코드이다. 표준은 코드 목록을 정의하지 않으므로 배포가 자체 목록을 공개해야 한다. 여기의 evacuationRequired는 예측 결과의 같은 이름 필드와 달리 지시이며, evacuationZones는 그 지시가 정확히 누구에게 해당하는지 알려 준다.

onset과 expiry는 메시지의 유효 기간을 정한다. 현실적인 만료 시각이 없는 경보는 물이 빠진 뒤에도 오래도록 상황판에 남고, 너무 일찍 만료되는 경보는 첨두 시점에 주민을 안내 없이 남겨 둔다. 발령 기관은 경보가 만료되도록 두었다가 새로 발령하기보다 갱신 오퍼레이션으로 만료 시각을 조정해야 한다. 그래야 수신자가 하나의 메시지 사슬을 따라갈 수 있다.

3.5.5 예시

3.2절의 예측으로부터 두 시간 반 뒤, 수위가 계속 올라 새 예측이 이 지역을 LEVEL-3으로 평가한다. 구청 재난안전 부서는 다음 경보를 발령한다. 시작까지는 세 시간이 남았지만 하천변 구역의 대피는 한 시간 안에 시작해야 하므로 긴급도는 expected이다. 확률은 50 %를 넘지만 수위가 아직 danger를 넘지 않았으므로 확실도는 likely이다.

{
  "alertId": "ALERT-2026-000045",
  "timestamp": "2026-07-15T04:00:00Z",
  "issuer": {
    "organizationId": "ORG-EXAMPLE-GN",
    "name": "Example District Disaster Safety Office",
    "authorityLevel": "municipal"
  },
  "alert": {
    "level": "LEVEL-3",
    "severity": "severe",
    "urgency": "expected",
    "certainty": "likely"
  },
  "affectedAreas": {
    "regionIds": ["REG-SEOUL-001"],
    "geoJson": {
      "type": "Polygon",
      "coordinates": [[
        [127.040, 37.500], [127.060, 37.500], [127.060, 37.515],
        [127.040, 37.515], [127.040, 37.500]
      ]]
    },
    "population": 15000,
    "buildings": 3200
  },
  "event": {
    "type": "river-flood",
    "onset": "2026-07-15T07:00:00Z",
    "expiry": "2026-07-15T18:00:00Z",
    "peakTime": "2026-07-15T09:00:00Z"
  },
  "instructions": {
    "ko": "하천변 저지대 주민은 지정 대피소로 이동하십시오. 지하 공간에 머무르지 마십시오.",
    "en": "Residents of low-lying riverside areas should move to a designated shelter. Do not stay in underground spaces.",
    "actions": ["evacuate-low-lying", "avoid-underground", "avoid-riverside-roads"],
    "evacuationRequired": true,
    "evacuationZones": ["ZONE-GN-A", "ZONE-GN-B"]
  },
  "shelters": [{
    "shelterId": "SHL-GN-012",
    "name": "Example Elementary School Gym",
    "type": "school",
    "location": { "latitude": 37.5190, "longitude": 127.0450 },
    "address": "(example address)",
    "capacity": { "total": 600, "current": 140, "available": 460 },
    "facilities": { "restrooms": true, "kitchen": false, "medicalRoom": true,
                    "powerSupply": true, "water": true, "heating": false,
                    "cooling": true, "wifi": true },
    "accessibility": { "wheelchairAccessible": true, "elevatorAvailable": false,
                       "parkingAvailable": true, "publicTransport": ["bus"] },
    "supplies": { "food": 1200, "water": 1800, "blankets": 500,
                  "firstAidKits": 20, "emergencyLights": 40 },
    "status": { "operational": true, "lastInspection": "2026-06-30T00:00:00Z",
                "contact": "(example)", "openingHours": "24h during alert" }
  }],
  "contact": {
    "emergency": "119",
    "information": "(example information line)",
    "website": "https://alerts.example.org/ALERT-2026-000045"
  }
}

3.5.6 shelters의 두 가지 형태

PHASE-3 §7.2는 shelters의 원소를 평탄한 객체로 정의한다. id, name, location, 숫자 하나인 capacity와 available, 문자열 배열인 facilities이다. 반면 SDK는 3.7절에서 설명하는 완전한 EvacuationShelter 객체를 쓰며, 여기에는 shelterId, 중첩된 수용 인원, 불리언 시설 플래그가 들어 있다. 위 예시는 SDK 형태를 따랐다. 두 형태를 모두 처리해야 하는 수신자는 어느 쪽이든 받아서 정규화해야 한다. id ↔ shelterId, capacity ↔ capacity.total, available ↔ capacity.available, 시설 문자열 목록 ↔ 값이 true인 플래그들로 대응시키면 된다.

3.6 CAP 1.2 매핑

공통 경보 프로토콜(CAP) 1.2는 권한기관, 방송사, 이동통신망, 애플리케이션 사이에서 공공 경보를 교환하기 위한 OASIS 표준이다. ENE-033은 CAP를 대체하지 않는다. CAP 메시지를 만들어 내는 더 풍부한 도메인 객체를 정의할 뿐이다. ENE-033 시스템을 떠나 국가 또는 국제 경보 집약 시스템으로 가는 공공 경보는 CAP로 보내야 한다.

3.6.1 필드 대응

FloodAlertCAP 1.2비고
alertIdalert/identifier발신자 안에서 유일해야 한다.
issuer.organizationIdalert/senderCAP는 전역적으로 유일한 발신자 식별자를 요구한다. 도메인 기반 형식이 일반적이다.
timestampalert/sentCAP 날짜·시각 형식으로 변환한다(3.6.3 참조).
(없음)alert/status, msgType, scope게시자가 채운다. 보통 Actual, Alert(또는 Update/Cancel), Public이다. 훈련은 반드시 Exercise를 쓰고 Actual을 쓰지 않는다.
issuer.nameinfo/senderName사람이 읽는 발령 기관명.
event.typeinfo/event, info/eventCode언어별로 읽기 쉬운 사건명, ENE-033 값은 사건 코드로.
(없음)info/categoryCAP 필수 요소이나 ENE-033에는 없다. 게시자가 고정값(예: Met)을 정한다.
alert.severity/urgency/certaintyinfo/severity/urgency/certainty같은 낱말을 첫 글자만 대문자로.
alert.levelinfo/parameterENE-033 수신자가 단계를 잃지 않도록 이름 있는 매개변수로 싣는다.
event.onset / event.expiryinfo/onset / info/expires직접 대응.
event.peakTimeinfo/parameterCAP에는 첨두 시각 요소가 없다.
instructions.ko / .en두 info 블록의 info/instruction언어마다 info 하나, 각자 language 코드를 가진다.
instructions.evacuationRequiredinfo/responseTypetrue → Evacuate, 그 밖에는 상황에 따라 Prepare 또는 Monitor.
contact.website / contact.emergencyinfo/web / info/contact직접 대응.
affectedAreas.geoJsonarea/polygon좌표 순서를 바꿔야 한다(3.6.3 참조).
affectedAreas.regionIdsarea/geocode이름 있는 지오코드. 국가 행정구역 코드를 나란히 추가할 수 있다.
affectedAreas.population, buildings, shelters(없음)CAP 핵심 요소에는 담기지 않는다. web이나 parameter로 연결한다.

3.6.2 ENE-033이 표현하지 못하는 CAP 값

CAP의 열거값은 ENE-033보다 넓다. 심각도와 확실도에는 Unknown이 있고, 긴급도에는 Past와 Unknown이 있으며, 확실도에는 Unlikely도 있다. ENE-033에서 CAP로 가는 변환은 손실이 없다. ENE-033의 모든 값에 대응하는 CAP 값이 있기 때문이다. 반대 방향에서는 CAP 메시지를 받는 ENE-033 수신자가 이 추가 값을 어떻게 처리할지 정해야 한다. 이 책의 권고는 다음과 같다.

메시지 유형에도 같은 논리가 적용된다. CAP의 Update와 Cancel은 references로 이전 메시지를 가리키며, ENE-033 API의 갱신·해제 오퍼레이션에 대응한다. 갱신 시에는 ENE-033 alertId를 그대로 쓰고, 이전 메시지를 참조하는 새 CAP 식별자를 만들어야 한다.

3.6.3 변환의 두 가지 함정

좌표 순서. GeoJSON은 좌표를 [경도, 위도] 순으로 쓴다. CAP 다각형은 WGS 84 기준 "위도,경도" 쌍을 공백으로 구분해 나열하며, 첫 쌍과 마지막 쌍이 같아야 한다. 숫자를 순서대로 복사하는 변환기는 서울의 다각형을 남극 어딘가에 찍는다. 앞의 toCapPolygon 도우미는 순서를 명시적으로 바꾼다. 또 CAP 다각형은 구멍이 없는 단일 고리이므로, GeoJSON MultiPolygon은 같은 area 안의 여러 polygon 요소가 되고 구멍은 버리거나 근사해야 한다.

시간대 표기. ENE-033 시각은 Z로 끝난다. CAP 1.2는 알파벳 시간대 표기를 허용하지 않고 UTC를 -00:00으로 쓴다. 엄격한 CAP 검증기는 2026-07-15T04:00:00Z를 거부한다. toCapTime 도우미가 이 변환을 맡는다.

3.6.4 CAP로 표현한 예시

<?xml version="1.0" encoding="UTF-8"?>
<alert xmlns="urn:oasis:names:tc:emergency:cap:1.2">
  <identifier>ALERT-2026-000045</identifier>
  <sender>alerts@example.org</sender>
  <sent>2026-07-15T04:00:00-00:00</sent>
  <status>Actual</status>
  <msgType>Alert</msgType>
  <scope>Public</scope>
  <info>
    <language>ko-KR</language>
    <category>Met</category>
    <event>하천 홍수</event>
    <responseType>Evacuate</responseType>
    <urgency>Expected</urgency>
    <severity>Severe</severity>
    <certainty>Likely</certainty>
    <onset>2026-07-15T07:00:00-00:00</onset>
    <expires>2026-07-15T18:00:00-00:00</expires>
    <senderName>Example District Disaster Safety Office</senderName>
    <headline>[경계] 하천 홍수 위험 — 저지대 대피</headline>
    <instruction>하천변 저지대 주민은 지정 대피소로 이동하십시오. 지하 공간에 머무르지 마십시오.</instruction>
    <web>https://alerts.example.org/ALERT-2026-000045</web>
    <eventCode><valueName>WIA-ENE-033:eventType</valueName><value>river-flood</value></eventCode>
    <parameter><valueName>WIA-ENE-033:riskLevel</valueName><value>LEVEL-3</value></parameter>
    <parameter><valueName>WIA-ENE-033:predictionId</valueName><value>PRED-2026-000124</value></parameter>
    <parameter><valueName>WIA-ENE-033:peakTime</valueName><value>2026-07-15T09:00:00-00:00</value></parameter>
    <area>
      <areaDesc>REG-SEOUL-001 riverside low-lying zones A, B</areaDesc>
      <polygon>37.500,127.040 37.500,127.060 37.515,127.060 37.515,127.040 37.500,127.040</polygon>
      <geocode><valueName>WIA:regionId</valueName><value>REG-SEOUL-001</value></geocode>
    </area>
  </info>
  <info>
    <language>en-US</language>
    <!-- category, urgency, severity, certainty, onset, expires and area
         must be repeated here exactly as in the ko-KR block; omitted for brevity -->
    <event>River flood</event>
    <headline>[Warning] River flood risk: evacuate low-lying areas</headline>
    <instruction>Residents of low-lying riverside areas should move to a designated shelter. Do not stay in underground spaces.</instruction>
  </info>
</alert>

CAP 메시지는 게시하기 전에 게시 파이프라인 안에서 CAP 1.2 XML 스키마로 검증해야 한다. 제7장의 자체 점검 시나리오에 이 검증 단계가 들어 있다.

3.7 대피 데이터: 대피소와 대피 경로

목적지와 안전한 이동 경로가 없는 대피 지시는 완결되지 않는다. SDK는 이를 위해 EvacuationShelter와 EvacuationRoute 두 객체를 정의한다. 이들은 PHASE-1 데이터 모델 본문에는 없지만 대피 엔드포인트가 제공하고 경보에 포함되므로, 실제로는 표준의 메시지 포맷에 속한다.

3.7.1 EvacuationShelter

블록필드비고
식별shelterId, name, type, location, addresstype: school, community-center, sports-facility, temporary.
capacitytotal, current, available단위는 명. available은 total − current와 같아야 하며, 수신자는 이를 확인하고 어긋나면 작은 값을 택한다.
facilities불리언 8개restrooms, kitchen, medicalRoom, powerSupply, water, heating, cooling, wifi.
accessibilitywheelchairAccessible, elevatorAvailable, parkingAvailable, publicTransport[]이동 약자의 경로 안내에 필수적이다.
suppliesfood(인분), water(L), blankets, firstAidKits, emergencyLights수량. 단위는 SDK 주석을 따른다.
statusoperational, lastInspection, contact, openingHoursoperational: false인 대피소는 발효 중인 경보에 올리면 안 된다.

사건이 진행되는 동안 대피소 수용 현황은 빠르게 바뀌지만, 경보는 발령 시점에 고정된 메시지이다. 따라서 경보에 포함된 값은 "발령 시점 기준"으로 읽어야 하며, 실시간 여유 인원을 보여 주는 애플리케이션은 경보 안의 사본에 기대지 말고 대피 엔드포인트에서 대피소 정보를 가져와야 한다. 또한 대피소 자체가 예측 침수 범위 밖에, 가능하면 최악 시나리오의 범위 밖에도 있어야 한다. 경보에 올린 각 대피소의 위치를 inundationArea.geoJson과 교차 검사하는 일은 비용이 적고, 곤란한 실수를 막아 준다.

3.7.2 EvacuationRoute

경로는 origin(좌표)과 destination(완전한 EvacuationShelter)을 잇는다. path에는 좌표 목록, 미터 단위 distance, 분 단위 estimatedTime, 0~100의 safetyScore가 들어 있다. hazards는 경로를 위험하게 만드는 요소를 나열한다. 시작점·끝점·수심(depth, m)을 가진 floodedSegments, blockedRoads, highRiskAreas이다. alternatives는 거리·시간·안전 점수를 각각 가진 다른 경로를 제시하고, updates는 상태 변화의 시각별 기록이며 각 항목은 clear, caution, blocked 중 하나의 RouteStatus를 가진다.

표준은 safetyScore의 계산 방법을 정하지 않으며, 어느 수심부터 구간을 통행 불가로 볼지도 정하지 않는다. 이는 지역의 공학적·정책적 결정이므로 배포가 문서로 밝혀야 한다. 피해야 할 것은 침수 구간을 나열하면서도 상태가 clear인 경로이다. 상태와 위험 목록은 같은 데이터에서 도출되어야 한다. 운영자가 통행 불가로 보는 구간을 하나라도 지나는 경로는 blocked여야 하고, 첫 번째 대체 경로를 올려서 제시해야 한다.

3.8 요약

제4장은 이 객체들이 오가는 경로, 즉 요청을 받고 결과를 돌려주며 경보를 생성·갱신·해제하는 엔드포인트를 다룬다.