Files
Aislo/B08_Quantity/B08_Quantity_Hydro_Site.py
T
eomsangdonandClaude Opus 5.5 426e574cd2 feat(B08): 수리집수면적표 우리 양식 · 공사지 지역 · 재현기간 드롭다운 · 처음 값 빈칸 · [저장](PLAN 69-4~69-6)
- 엑셀 모양(흰 종이 · 노랑 칸 · 쪽 표시) 대신 공용 카드 · 입력 · 드롭다운 · 테마 토큰 색
- 테두리는 입력 · 선택 칸만 · 굵게 + 붉은 계열은 Qd · 유출량 · 판정 · 유출율만 · 관 단면 그림 테마 색
- 지역 = 전처리 행정구역(노선 대표점) > 시군구 하나 > 프로젝트 등록 지역 · R24 = 전처리 강우량표의 재현기간별 24시간 값(없으면 입력 칸)
- 계산 때 R24 는 서버가 공사지 표로 정함 · 빈칸에 기대는 계산 칸은 「—」
- 처음 값 = 표준 값만(할증 2.0 · 관 종류 · 단면 가정 · 유출계수 0.8 · 재현기간 100년) · 측점 고유 값 빈칸
- [저장] = PUT …/quantity/hydro → B08_Quantity/quantities/hydro_inputs.json 만 · 성공 뒤 캐시 비움 · 캐시 판 올림(v2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
2026-10-06 23:09:21 +09:00

122 lines
5.0 KiB
Python

"""B08 4탭 홍수량 계산서 — 공사지 지역 · 확률강우량 · 저장 파일 (PLAN 69-5 · 69-6).
읽기만: 전처리 강우량표(`B04_PreProcess/drainage/rainfall_table.json` · 관측소 확률강우량)
· 행정구역(`B04_PreProcess/processed/행정구역_*_bounds.geojson` · WGS84) · 프로젝트 등록 지역(DB).
배수유역 계산 코드는 부르지 않음 — 이미 저장된 파일만 읽음.
쓰기: B08 자기 파일 `B08_Quantity/quantities/hydro_inputs.json` 하나([저장] 때만).
"""
from __future__ import annotations
import json
import os
from pathlib import Path
from typing import Any
from shapely.geometry import Point, shape
RAINFALL_FILE = Path("B04_PreProcess") / "drainage" / "rainfall_table.json"
PROCESSED_DIR = Path("B04_PreProcess") / "processed"
EMD_FILE = "행정구역_읍면동_bounds.geojson"
SGG_FILE = "행정구역_시군구_bounds.geojson"
SAVED_FILE = Path("B08_Quantity") / "quantities" / "hydro_inputs.json"
#: 강우량표가 없을 때 재현기간 드롭다운 — 확률강우량 자료 표의 재현기간
FALLBACK_PERIODS = (2, 3, 5, 10, 20, 30, 50, 70, 80, 100, 200, 300, 500)
DAY_MINUTES = 1440
def _read_json(path: Path) -> dict[str, Any] | None:
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
return data if isinstance(data, dict) else None
def _features(path: Path) -> list[dict[str, Any]]:
data = _read_json(path) or {}
return [f for f in data.get("features") or [] if isinstance(f, dict)]
def _feature_at(path: Path, lon: float, lat: float) -> dict[str, Any] | None:
"""점이 드는 행정구역 속성 — 경계 파일은 WGS84."""
point = Point(lon, lat)
for feature in _features(path):
try:
if shape(feature["geometry"]).covers(point):
return feature.get("properties") or {}
except (KeyError, TypeError, ValueError):
continue
return None
def _region(root: Path, rain: dict[str, Any] | None, db_region: str | None) -> dict[str, str]:
"""공사지 지역 — 전처리 행정구역(노선 대표점이 드는 곳) > 시군구 하나뿐 > 프로젝트 등록 지역."""
lat, lon = (rain or {}).get("lat"), (rain or {}).get("lon")
if isinstance(lat, (int, float)) and isinstance(lon, (int, float)):
emd = _feature_at(root / PROCESSED_DIR / EMD_FILE, lon, lat)
sgg = _feature_at(root / PROCESSED_DIR / SGG_FILE, lon, lat)
name = (sgg or {}).get("sig_kor_nm") or ""
full = (emd or {}).get("full_nm") or (sgg or {}).get("full_nm") or ""
if name or full:
return {"name": name or full.split()[-1], "full": full, "source": "전처리 행정구역"}
sggs = _features(root / PROCESSED_DIR / SGG_FILE)
if len(sggs) == 1:
props = sggs[0].get("properties") or {}
return {
"name": props.get("sig_kor_nm") or "",
"full": props.get("full_nm") or "",
"source": "전처리 행정구역",
}
if db_region:
return {"name": db_region, "full": db_region, "source": "프로젝트 등록"}
return {"name": "", "full": "", "source": ""}
def _r24_table(rain: dict[str, Any] | None) -> dict[int, float]:
"""재현기간(년) → 24시간 확률강우량(mm)."""
table: dict[int, float] = {}
for row in (rain or {}).get("values") or []:
try:
if int(row["duration_min"]) == DAY_MINUTES:
table[int(row["return_period_yr"])] = float(row["depth_mm"])
except (KeyError, TypeError, ValueError):
continue
return table
def site_info(root: Path, db_region: str | None) -> dict[str, Any]:
"""화면 머리 — 지역 · 관측소 · 재현기간 목록 · 재현기간별 R24(없으면 빈 표)."""
rain = _read_json(root / RAINFALL_FILE)
r24 = _r24_table(rain)
station = (rain or {}).get("station") or {}
return {
"region": _region(root, rain, db_region),
"station": station.get("name") or "",
"periods": sorted(r24) or list(FALLBACK_PERIODS),
"r24": {str(k): v for k, v in sorted(r24.items())},
}
def site_r24(root: Path, return_period: float) -> float | None:
"""계산 때 서버가 정본으로 쓰는 R24 — 자료가 있으면 그 값 · 없으면 None(화면 입력)."""
return _r24_table(_read_json(root / RAINFALL_FILE)).get(int(return_period))
def read_saved(root: Path) -> dict[str, dict[str, Any]]:
data = _read_json(root / SAVED_FILE) or {}
stations = data.get("stations")
return stations if isinstance(stations, dict) else {}
def write_saved(root: Path, stations: dict[str, dict[str, Any]]) -> None:
"""원자적 쓰기 — 임시 파일에 쓴 뒤 바꿔 끼움."""
path = root / SAVED_FILE
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(".part")
payload = {"version": 1, "stations": stations}
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=1), encoding="utf-8")
os.replace(tmp, path)