wing-ops/prediction/opendrift/latestForecastDate.py
jeonghyo.k 88eb6b121a feat(prediction): OpenDrift 유류 확산 시뮬레이션 통합 + CCTV/관리자 고도화
[예측]
- OpenDrift Python API 서버 및 스크립트 추가 (prediction/opendrift/)
- 시뮬레이션 상태 폴링 훅(useSimulationStatus), 로딩 오버레이 추가
- HydrParticleOverlay: deck.gl 기반 입자 궤적 시각화 레이어
- OilSpillView/LeftPanel/RightPanel: 시뮬레이션 실행·결과 표시 UI 개편
- predictionService/predictionRouter: 시뮬레이션 CRUD 및 상태 관리 API
- simulation.ts: OpenDrift 연동 엔드포인트 확장
- docs/PREDICTION-GUIDE.md: 예측 기능 개발 가이드 추가

[CCTV/항공방제]
- CCTV 오일 감지 GPU 추론 연동 (OilDetectionOverlay, useOilDetection)
- CCTV 안전관리 감지 기능 추가 (선박 출입, 침입 감지)
- oil_inference_server.py: Python GPU 추론 서버

[관리자]
- 관리자 화면 고도화 (사용자/권한/게시판/선박신호 패널)
- AdminSidebar, BoardMgmtPanel, VesselSignalPanel 신규 컴포넌트

[기타]
- DB: 시뮬레이션 결과, 선박보험 시드(1391건), 역할 정리 마이그레이션
- 팀 워크플로우 v1.6.1 동기화

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 14:55:46 +09:00

204 lines
6.9 KiB
Python

import os
import xarray as xr
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from typing import Optional, List
from config import STORAGE
from logger import get_logger
logger = get_logger("latestForecastDate")
def check_file_size(file_path: str, min_size_bytes: int = 1024) -> bool:
"""파일 크기 확인"""
try:
if not os.path.exists(file_path):
return False
file_size = os.path.getsize(file_path)
if file_size < min_size_bytes:
logger.debug(f"File size insufficient: {os.path.basename(file_path)} ({file_size} bytes)")
return False
return True
except Exception as e:
logger.debug(f"File check error: {os.path.basename(file_path)} - {e}")
return False
def check_folder_completeness(folder_path: str,
required_patterns: Optional[List[str]] = None,
min_file_size: int = 1024) -> bool:
"""폴더의 다운로드 완전성 확인"""
try:
all_files = os.listdir(folder_path)
if required_patterns:
for pattern in required_patterns:
matching_files = [f for f in all_files if f.startswith(pattern)]
if not matching_files:
return False
for matched_file in matching_files:
file_path = os.path.join(folder_path, matched_file)
if not check_file_size(file_path, min_file_size):
return False
return True
nc_files = [f for f in os.listdir(folder_path) if f.endswith('.nc')]
if not nc_files:
logger.debug("No NC files found")
return False
invalid_count = 0
for nc_file in nc_files:
file_path = os.path.join(folder_path, nc_file)
if not check_file_size(file_path, min_file_size):
invalid_count += 1
if invalid_count > 0:
logger.debug(f"{invalid_count} files with insufficient size")
return False
return True
except Exception as e:
logger.debug(f"Validation error: {e}")
return False
def get_latest_forecast_date(base_path: str,
max_folders_to_check: int = 7,
required_patterns: Optional[List[str]] = None,
min_file_size: int = 1024) -> Optional[str]:
"""
YYYYMMDD 폴더 중 내림차순 상위 N개에서 완전한 최신 폴더의 생성 시간 반환
Args:
base_path: 예보 파일 저장 경로
max_folders_to_check: 확인할 최신 폴더 개수
required_patterns: 필수 파일 목록
min_file_size: 파일 최소 크기 (bytes)
Returns:
YYYYMMDDHHmm 형식 또는 None
"""
if not os.path.exists(base_path):
logger.warning(f"Path not found: {base_path}")
return None
try:
subdirs = [d for d in os.listdir(base_path)
if os.path.isdir(os.path.join(base_path, d))]
except PermissionError:
logger.warning(f"Permission denied: {base_path}")
return None
valid_folders = []
for subdir in subdirs:
if len(subdir) == 8 and subdir.isdigit():
try:
datetime.strptime(subdir, '%Y%m%d')
folder_path = os.path.join(base_path, subdir)
creation_time = os.path.getctime(folder_path)
valid_folders.append((subdir, creation_time, folder_path))
except (ValueError, OSError):
continue
if not valid_folders:
logger.warning(f"No valid date folders: {base_path}")
return None
valid_folders.sort(key=lambda x: x[0], reverse=True)
folders_to_check = valid_folders[:max_folders_to_check]
folders_to_check.sort(key=lambda x: x[1], reverse=True)
for folder_name, creation_timestamp, folder_path in folders_to_check:
is_complete = check_folder_completeness(
folder_path,
required_patterns=required_patterns,
min_file_size=min_file_size
)
if is_complete:
return folder_name
logger.warning(f"No complete folder: {base_path}")
return None
def get_earliest_latest_forecast_date(wind_path: str = None,
hydr_path: str = None,
max_folders_to_check: int = 7,
required_patterns: Optional[List[str]] = None,
min_file_size: int = 1024) -> Optional[dict]:
"""
바람과 해수 예보의 완전한 최신 폴더 생성 시간 중 더 과거 시간 반환
Args:
wind_path: 바람 예보 경로
hydr_path: 해수 예보 경로
max_folders_to_check: 확인할 최신 폴더 개수
required_patterns: 필수 파일 목록
min_file_size: 파일 최소 크기 (bytes)
Returns:
예보 정보 딕셔너리 또는 None
"""
if wind_path is None:
wind_path = str(STORAGE.POS_WIND)
if hydr_path is None:
hydr_path = str(STORAGE.POS_HYDR)
if required_patterns is None:
required_patterns = ["EA012", "KO108"]
wind_latest = get_latest_forecast_date(
wind_path,
max_folders_to_check=max_folders_to_check,
required_patterns=required_patterns,
min_file_size=min_file_size
)
hydr_latest = get_latest_forecast_date(
hydr_path,
max_folders_to_check=max_folders_to_check,
required_patterns=required_patterns,
min_file_size=min_file_size
)
if wind_latest is None or hydr_latest is None:
logger.warning(f"Warning: No forecast received in the last {max_folders_to_check} days. Contact administrator.")
return None
latest_folder_name = min(wind_latest, hydr_latest)
latest_folder_name_formatted = datetime.strptime(latest_folder_name, "%Y%m%d").strftime("%Y-%m-%d")
latest_receive_date = (datetime.strptime(latest_folder_name, "%Y%m%d") + timedelta(days=1)).strftime("%Y-%m-%d")
hydr_file_path = os.path.join(hydr_path, hydr_latest, f"KO108_MOHID_HYDR_SURF_{hydr_latest}00.nc")
with xr.open_dataset(hydr_file_path) as ds:
start_date = ds['time'].values[0]
end_date = ds['time'].values[-1]
diff = end_date - start_date
hour_diff = diff / np.timedelta64(1, 'h')
hour_diff_string = f"{int(hour_diff)}h"
return_json = {
"date": latest_folder_name_formatted,
"receivedDate": latest_receive_date + " 12:00",
"startDate": pd.Timestamp(start_date).strftime("%Y-%m-%d %H:%M:%S"),
"endDate": pd.Timestamp(end_date).strftime("%Y-%m-%d %H:%M:%S"),
"diff": hour_diff_string
}
return return_json
if __name__ == "__main__":
result = get_earliest_latest_forecast_date()
if result:
logger.info(f"Result: {result}")
else:
logger.info("No complete forecast folder found")