story · how it was built
설명
무엇을 (문제·대상)
2026년 8월 15~17일 광복절 연휴에 거제에 관측 이래 최악의 비가 왔습니다. 일강수 577.4mm, 1시간 최대 124.5mm, 3일 누적 956.1mm. 새벽 4시 42분 옥포동 산사태로 아파트가 덮치면서 1명이 숨지고 2명이 다쳤습니다. 정전 1,500세대, 도로 20여 곳 통제, 이재민 691명.
거제안길은 그 사흘을 실측 데이터로 그대로 재현하고, 같은 비가 다시 오면 어디가 먼저 잠기고 누구를 먼저 옮겨야 하는지 미리 계산하는 시스템입니다. 대피는 고속도로가 아니라 집 앞 골목에서 시작한다는 것이 이름의 뜻입니다.
주요 기능
- 최소위험경로 — 통행시간 × (1 + λ × 침수위험) + 통제 페널티. 가장 짧은 길이 아니라 가장 덜 위험한 길을 냅니다.
- 대상별 4종 — 차량 · 성인도보 · 고령자 · 휠체어. λ와 이동상한·경사상한을 각각 다르게 둡니다.
- 대피소 정원 압력 — 인근 인구를 최근접 배정해 기대 수요를 내고, 거리뿐 아니라 「가 봤자 자리가 있는지」까지 함께 답합니다.
- 사건 재현 상황판 — 관심·주의·경계·첨두 4단계 프리셋으로 8월 17일 새벽을 시간대별로 되돌려 봅니다. 수위 첨두는 6개 하천 관측소가 02~06시에 동시에 왔습니다.
- 3D 트윈 — 고현만·옥포만 전역(격자 667×595 = 16.9×18.3km, 도로 7,730개). 수면은 유역별로 그리고, 관측이 없는 유역은 채우지 않고 「자료 없음」으로 비웁니다.
- 자체검증 공개 — /verify 에서 불변식·역검증·성능·신선도를 전수 공개합니다(통과 29 · 실패 0).
어떻게 만들었는가 (과정·도구 활용)
충북안길(청주·증평) → 사천안길 → 거제안길 순으로 지역을 넓혀 온 세 번째 이식입니다. 수집·정제는 Python 파이프라인(WAMIS 수위 · OSM 도로망 · 행안부 대피소 · DEM · SGIS 격자인구), 화면은 Next.js 16입니다. Claude Code로 파이프라인과 앱을 함께 몰고, 고칠 때마다 검증 스크립트를 먼저 늘리는 방식으로 갔습니다.
이식에서 가장 많이 깨진 것은 코드가 아니라 상수였습니다. 전 페이지에 「사천」 문자열이 60여 곳 남아 있었고, /verify 는 사천 실행분을 게시하고 있었고, 지도 첫 중심 좌표는 청주였습니다. 가정 시나리오의 수계 묶음은 통째로 사천(곤양천·가화천)이라 어떤 프리셋을 눌러도 단계가 만들어지지 않았습니다. 지역 이식은 문자열 치환이 아니라 상수·산출물·검증 스크립트를 함께 옮기는 일이었습니다.
구조·실행 (짧게)
data-pipeline/ 에서 수집·정제 → app/data/runtime/ 에 산출물(graph.bin · shelters.json · shelter_demand.json) → app/ 의 Next.js 앱이 그것을 읽습니다. Vercel 은 app/ 만 올리므로 앱이 읽는 파일은 반드시 app/ 아래에 둬야 합니다. 저장소 루트를 참조하면 로컬에서만 되고 배포에서 500이 납니다.
한계·다음
- 시제품입니다. 지자체가 운영하는 공식 대피 안내가 아닙니다.
- 화면의 모든 숫자에 실측·추정·시뮬레이션 배지를 붙였습니다. 「추정」 배지는 근거가 관측이 아니라 이력 백분위라는 뜻입니다.
- 다음은 예방 쪽입니다. 사건 재현에서 나온 위험 구간을 평상시 정비 우선순위로 바꾸는 작업이 남아 있습니다.
journey · lessons
과정 · 배운 점
본문 = 무엇·기능·사용법. 여기 = 시간순 여정 + 다음에 남긴 교훈.
자료 해상도 밖의 값으로 통행을 하드 차단하면 안 됩니다. 90m DEM으로 51m짜리 골목의 경사를 판정해 끊었더니 노드의 17.1%가 「나갈 길 없음」이 됐습니다. 해상 가능한 링크(90m 이상)만 차단하고, 짧은 급구간은 통행시간 가중치로 넘기고 화면에 표시하는 쪽으로 바꿨습니다.
면적 비율을 사람 비율처럼 쓰면 안 됩니다. 같은 상황에서 면적 기준 82.2% 대 인원 기준 44.5%로 두 배 가까이 갈렸습니다. SGIS 격자가 「라라」 블록만 실려 도심이 통째로 빠져 있었고(시 인구의 8%만 잡혔습니다), 「마라」 블록까지 넣어 868셀·233,120명을 확보한 뒤에야 인원 기준 숫자가 나왔습니다.
결손을 좋은 값으로 메우면 안 됩니다. 보고서 읍면동 표 18행이 전부 빈 칸이었는데, 원인 중 하나가 예방 격자 파일을 catch(() => null) 로 삼킨 것이었습니다. 파일명이 달랐을 뿐인데 화면에는 「자료 없음」이 아니라 그냥 빈 칸이 나왔습니다. 자료가 없는 자리는 없다고 인쇄돼야 합니다.
수집 범위와 관할은 다릅니다. 지하차도 통제 대상 3건이 전부 부산 강서구 것이었고, 정작 국토부에 등록된 거제 2건은 OSM 태그가 없어 통째로 빠져 있었습니다. 등록대장 매칭으로 교체했습니다.
피드백 원함 · 개선 제안 · 질문 · 스택·도구 조언