Spaces:
Sleeping
Sleeping
SeungHyeok Jang
commited on
Commit
·
613de59
1
Parent(s):
a7d4b8e
modulizatioin
Browse files- api_utils.py +206 -0
- app.py +17 -1151
- chatbot.py +126 -0
- chatbot_utils.py +114 -0
- config.py +20 -0
- prediction.py +300 -0
- supabase_utils.py +126 -0
- ui.py +42 -0
api_utils.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime, timedelta
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import pytz
|
| 4 |
+
import plotly.graph_objects as go
|
| 5 |
+
from plotly.subplots import make_subplots
|
| 6 |
+
|
| 7 |
+
from supabase_utils import get_supabase_client
|
| 8 |
+
from config import STATION_NAMES
|
| 9 |
+
|
| 10 |
+
def api_get_current_tide(station_id):
|
| 11 |
+
"""현재 조위 조회"""
|
| 12 |
+
supabase = get_supabase_client()
|
| 13 |
+
if not supabase:
|
| 14 |
+
return {"error": "Supabase 클라이언트를 생성할 수 없습니다."}
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
result = supabase.table('tide_predictions') \
|
| 18 |
+
.select('predicted_at, final_tide_level') \
|
| 19 |
+
.eq('station_id', station_id) \
|
| 20 |
+
.order('predicted_at', desc=True) \
|
| 21 |
+
.limit(1) \
|
| 22 |
+
.execute()
|
| 23 |
+
|
| 24 |
+
if result.data:
|
| 25 |
+
return result.data[0]
|
| 26 |
+
else:
|
| 27 |
+
return {"error": "데이터가 없습니다."}
|
| 28 |
+
except Exception as e:
|
| 29 |
+
return {"error": f"데이터 조회 오류: {e}"}
|
| 30 |
+
|
| 31 |
+
def api_get_historical_tide(station_id, date_str, hours=24):
|
| 32 |
+
"""과거 특정 날짜의 조위 데이터 조회"""
|
| 33 |
+
supabase = get_supabase_client()
|
| 34 |
+
if not supabase:
|
| 35 |
+
return {"error": "Supabase 클라이언트를 생성할 수 없습니다."}
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
start_time = datetime.strptime(date_str, '%Y-%m-%d')
|
| 39 |
+
start_time_kst = pytz.timezone('Asia/Seoul').localize(start_time)
|
| 40 |
+
end_time_kst = start_time_kst + timedelta(hours=hours)
|
| 41 |
+
|
| 42 |
+
start_utc = start_time_kst.astimezone(pytz.UTC).isoformat()
|
| 43 |
+
end_utc = end_time_kst.astimezone(pytz.UTC).isoformat()
|
| 44 |
+
|
| 45 |
+
result = supabase.table('historical_tide') \
|
| 46 |
+
.select('observed_at, tide_level') \
|
| 47 |
+
.eq('station_id', station_id) \
|
| 48 |
+
.gte('observed_at', start_utc) \
|
| 49 |
+
.lte('observed_at', end_utc) \
|
| 50 |
+
.order('observed_at') \
|
| 51 |
+
.execute()
|
| 52 |
+
|
| 53 |
+
if not result.data:
|
| 54 |
+
return {"error": f"{date_str}에 대한 과거 데이터가 없습니다."}
|
| 55 |
+
|
| 56 |
+
df = pd.DataFrame(result.data)
|
| 57 |
+
df['observed_at'] = pd.to_datetime(df['observed_at']).dt.tz_convert('Asia/Seoul')
|
| 58 |
+
df['tide_level'] = pd.to_numeric(df['tide_level'])
|
| 59 |
+
|
| 60 |
+
fig = go.Figure()
|
| 61 |
+
fig.add_trace(go.Scatter(x=df['observed_at'], y=df['tide_level'], mode='lines',
|
| 62 |
+
name=f'{STATION_NAMES.get(station_id, station_id)} 조위'))
|
| 63 |
+
fig.update_layout(
|
| 64 |
+
title=f'{STATION_NAMES.get(station_id, station_id)} - {date_str} 조위',
|
| 65 |
+
xaxis_title='시간',
|
| 66 |
+
yaxis_title='조위 (cm)',
|
| 67 |
+
height=400
|
| 68 |
+
)
|
| 69 |
+
return fig
|
| 70 |
+
|
| 71 |
+
except Exception as e:
|
| 72 |
+
return {"error": f"데이터 조회 중 오류 발생: {e}"}
|
| 73 |
+
|
| 74 |
+
def api_get_historical_extremes(station_id, date_str):
|
| 75 |
+
"""과거 특정 날짜의 만조/간조 정보"""
|
| 76 |
+
supabase = get_supabase_client()
|
| 77 |
+
if not supabase:
|
| 78 |
+
return {"error": "Supabase 클라이언트를 생성할 수 없습니다."}
|
| 79 |
+
|
| 80 |
+
try:
|
| 81 |
+
start_time = datetime.strptime(date_str, '%Y-%m-%d')
|
| 82 |
+
start_time_kst = pytz.timezone('Asia/Seoul').localize(start_time)
|
| 83 |
+
end_time_kst = start_time_kst + timedelta(days=1)
|
| 84 |
+
|
| 85 |
+
start_utc = start_time_kst.astimezone(pytz.UTC).isoformat()
|
| 86 |
+
end_utc = end_time_kst.astimezone(pytz.UTC).isoformat()
|
| 87 |
+
|
| 88 |
+
result = supabase.table('historical_tide') \
|
| 89 |
+
.select('observed_at, tide_level') \
|
| 90 |
+
.eq('station_id', station_id) \
|
| 91 |
+
.gte('observed_at', start_utc) \
|
| 92 |
+
.lte('observed_at', end_utc) \
|
| 93 |
+
.order('observed_at') \
|
| 94 |
+
.execute()
|
| 95 |
+
|
| 96 |
+
if not result.data or len(result.data) < 3:
|
| 97 |
+
return {"error": f"{date_str}의 만조/간조를 계산할 데이터가 부족합니다."}
|
| 98 |
+
|
| 99 |
+
df = pd.DataFrame(result.data)
|
| 100 |
+
df['observed_at'] = pd.to_datetime(df['observed_at']).dt.tz_convert('Asia/Seoul')
|
| 101 |
+
df['tide_level'] = pd.to_numeric(df['tide_level'])
|
| 102 |
+
|
| 103 |
+
df['min'] = df.tide_level[(df.tide_level.shift(1) > df.tide_level) & (df.tide_level.shift(-1) > df.tide_level)]
|
| 104 |
+
df['max'] = df.tide_level[(df.tide_level.shift(1) < df.tide_level) & (df.tide_level.shift(-1) < df.tide_level)]
|
| 105 |
+
|
| 106 |
+
extremes_df = df.dropna(subset=['min', 'max'], how='all').copy()
|
| 107 |
+
extremes_df['type'] = extremes_df.apply(lambda row: '만조' if pd.notna(row['max']) else '간조', axis=1)
|
| 108 |
+
extremes_df['value'] = extremes_df.apply(lambda row: row['max'] if pd.notna(row['max']) else row['min'], axis=1)
|
| 109 |
+
extremes_df['time'] = extremes_df['observed_at'].dt.strftime('%H:%M')
|
| 110 |
+
|
| 111 |
+
return extremes_df[['time', 'type', 'value']]
|
| 112 |
+
|
| 113 |
+
except Exception as e:
|
| 114 |
+
return {"error": f"데이터 처리 중 오류 발생: {e}"}
|
| 115 |
+
|
| 116 |
+
def api_compare_dates(station_id, date1, date2):
|
| 117 |
+
"""두 날짜의 조위 패턴 비교"""
|
| 118 |
+
supabase = get_supabase_client()
|
| 119 |
+
if not supabase:
|
| 120 |
+
return {"error": "Supabase 클라이언트를 생성할 수 없습니다."}
|
| 121 |
+
|
| 122 |
+
def get_data_for_date(target_date):
|
| 123 |
+
start = pytz.timezone('Asia/Seoul').localize(datetime.strptime(target_date, '%Y-%m-%d'))
|
| 124 |
+
end = start + timedelta(days=1)
|
| 125 |
+
res = supabase.table('historical_tide') \
|
| 126 |
+
.select('observed_at, tide_level') \
|
| 127 |
+
.eq('station_id', station_id) \
|
| 128 |
+
.gte('observed_at', start.astimezone(pytz.UTC).isoformat()) \
|
| 129 |
+
.lte('observed_at', end.astimezone(pytz.UTC).isoformat()) \
|
| 130 |
+
.order('observed_at') \
|
| 131 |
+
.execute()
|
| 132 |
+
return res.data
|
| 133 |
+
|
| 134 |
+
data1 = get_data_for_date(date1)
|
| 135 |
+
data2 = get_data_for_date(date2)
|
| 136 |
+
|
| 137 |
+
if not data1 or not data2:
|
| 138 |
+
return {"error": "두 날짜 중 하나의 데이터가 없습니다."}
|
| 139 |
+
|
| 140 |
+
df1 = pd.DataFrame(data1)
|
| 141 |
+
df1['tide_level'] = pd.to_numeric(df1['tide_level'])
|
| 142 |
+
df1['minutes_from_start'] = (pd.to_datetime(df1['observed_at']) - pd.to_datetime(df1['observed_at']).iloc[0]).dt.total_seconds() / 60
|
| 143 |
+
|
| 144 |
+
df2 = pd.DataFrame(data2)
|
| 145 |
+
df2['tide_level'] = pd.to_numeric(df2['tide_level'])
|
| 146 |
+
df2['minutes_from_start'] = (pd.to_datetime(df2['observed_at']) - pd.to_datetime(df2['observed_at']).iloc[0]).dt.total_seconds() / 60
|
| 147 |
+
|
| 148 |
+
fig = go.Figure()
|
| 149 |
+
fig.add_trace(go.Scatter(x=df1['minutes_from_start'], y=df1['tide_level'], mode='lines', name=date1))
|
| 150 |
+
fig.add_trace(go.Scatter(x=df2['minutes_from_start'], y=df2['tide_level'], mode='lines', name=date2))
|
| 151 |
+
fig.update_layout(title=f'{STATION_NAMES.get(station_id, station_id)} 조위 비교: {date1} vs {date2}',
|
| 152 |
+
xaxis_title='자정부터 경과 시간(분)', yaxis_title='조위 (cm)')
|
| 153 |
+
return fig
|
| 154 |
+
|
| 155 |
+
def api_get_monthly_summary(station_id, year, month):
|
| 156 |
+
"""월간 조위 요약 통계"""
|
| 157 |
+
supabase = get_supabase_client()
|
| 158 |
+
if not supabase:
|
| 159 |
+
return {"error": "Supabase 클라이언트를 생성할 수 없습니다."}
|
| 160 |
+
|
| 161 |
+
try:
|
| 162 |
+
start_date = f"{year}-{int(month):02d}-01"
|
| 163 |
+
end_date = (datetime.strptime(start_date, '%Y-%m-%d') + pd.offsets.MonthEnd(1)).strftime('%Y-%m-%d')
|
| 164 |
+
|
| 165 |
+
start_utc = pytz.timezone('Asia/Seoul').localize(datetime.strptime(start_date, '%Y-%m-%d')).astimezone(pytz.UTC).isoformat()
|
| 166 |
+
end_utc = (pytz.timezone('Asia/Seoul').localize(datetime.strptime(end_date, '%Y-%m-%d')) + timedelta(days=1)).astimezone(pytz.UTC).isoformat()
|
| 167 |
+
|
| 168 |
+
result = supabase.table('historical_tide') \
|
| 169 |
+
.select('observed_at, tide_level') \
|
| 170 |
+
.eq('station_id', station_id) \
|
| 171 |
+
.gte('observed_at', start_utc) \
|
| 172 |
+
.lte('observed_at', end_utc) \
|
| 173 |
+
.order('observed_at') \
|
| 174 |
+
.execute()
|
| 175 |
+
|
| 176 |
+
if not result.data:
|
| 177 |
+
return {"error": f"{year}년 {month}월 데이터가 없습니다."}
|
| 178 |
+
|
| 179 |
+
df = pd.DataFrame(result.data)
|
| 180 |
+
df['observed_at'] = pd.to_datetime(df['observed_at']).dt.tz_convert('Asia/Seoul')
|
| 181 |
+
df['tide_level'] = pd.to_numeric(df['tide_level'])
|
| 182 |
+
|
| 183 |
+
highest = df.loc[df['tide_level'].idxmax()]
|
| 184 |
+
lowest = df.loc[df['tide_level'].idxmin()]
|
| 185 |
+
avg_tide = df['tide_level'].mean()
|
| 186 |
+
|
| 187 |
+
df['date'] = df['observed_at'].dt.date
|
| 188 |
+
daily_range = df.groupby('date')['tide_level'].apply(lambda x: x.max() - x.min())
|
| 189 |
+
avg_range = daily_range.mean()
|
| 190 |
+
|
| 191 |
+
summary = {
|
| 192 |
+
"최고 조위": f"{highest['tide_level']:.1f}cm ({highest['observed_at'].strftime('%Y-%m-%d %H:%M')})",
|
| 193 |
+
"최저 조위": f"{lowest['tide_level']:.1f}cm ({lowest['observed_at'].strftime('%Y-%m-%d %H:%M')})",
|
| 194 |
+
"평균 조위": f"{avg_tide:.1f}cm",
|
| 195 |
+
"평균 조차": f"{avg_range:.1f}cm"
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
fig = make_subplots(rows=2, cols=1, subplot_titles=("일별 조위 변화", "일별 조차"))
|
| 199 |
+
fig.add_trace(go.Box(x=df['observed_at'].dt.strftime('%Y-%m-%d'), y=df['tide_level'], name='조위'), row=1, col=1)
|
| 200 |
+
fig.add_trace(go.Bar(x=daily_range.index.strftime('%Y-%m-%d'), y=daily_range.values, name='조차'), row=2, col=1)
|
| 201 |
+
fig.update_layout(height=700, title_text=f"{STATION_NAMES.get(station_id, station_id)} - {year}년 {month}월 요약")
|
| 202 |
+
|
| 203 |
+
return summary, fig
|
| 204 |
+
|
| 205 |
+
except Exception as e:
|
| 206 |
+
return {"error": f"월간 요약 생성 중 오류 발생: {e}"}, None
|
app.py
CHANGED
|
@@ -1,1157 +1,23 @@
|
|
| 1 |
-
import gradio as gr
|
| 2 |
-
import subprocess
|
| 3 |
-
import json
|
| 4 |
-
import os
|
| 5 |
-
import numpy as np
|
| 6 |
-
import pandas as pd
|
| 7 |
-
import plotly.graph_objects as go
|
| 8 |
-
from plotly.subplots import make_subplots
|
| 9 |
-
import plotly.express as px
|
| 10 |
-
from datetime import datetime, timedelta
|
| 11 |
-
import pytz
|
| 12 |
-
from dateutil import parser as date_parser
|
| 13 |
import warnings
|
| 14 |
-
import
|
| 15 |
|
| 16 |
-
#
|
| 17 |
-
|
| 18 |
-
from supabase import create_client, Client
|
| 19 |
-
SUPABASE_AVAILABLE = True
|
| 20 |
-
except ImportError:
|
| 21 |
-
SUPABASE_AVAILABLE = False
|
| 22 |
-
print("Supabase 패키지가 설치되지 않았습니다.")
|
| 23 |
-
|
| 24 |
-
# Gemini 연동 확인
|
| 25 |
-
try:
|
| 26 |
-
import google.generativeai as genai
|
| 27 |
-
GEMINI_AVAILABLE = True
|
| 28 |
-
except ImportError:
|
| 29 |
-
GEMINI_AVAILABLE = False
|
| 30 |
-
print("Gemini (google-generativeai) 패키지가 설치되지 않았습니다.")
|
| 31 |
-
|
| 32 |
-
warnings.filterwarnings('ignore')
|
| 33 |
-
|
| 34 |
-
# --- 0. 설정 ---
|
| 35 |
-
SUPABASE_URL = os.environ.get("SUPABASE_URL")
|
| 36 |
-
SUPABASE_KEY = os.environ.get("SUPABASE_KEY") # SUPABASE_ANON_KEY를 SUPABASE_KEY로 통일
|
| 37 |
-
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
|
| 38 |
-
|
| 39 |
-
STATIONS = [
|
| 40 |
-
"DT_0001", "DT_0065", "DT_0008", "DT_0067", "DT_0043", "DT_0002",
|
| 41 |
-
"DT_0050", "DT_0017", "DT_0052", "DT_0025", "DT_0051", "DT_0037",
|
| 42 |
-
"DT_0024", "DT_0018", "DT_0068", "DT_0003", "DT_0066"
|
| 43 |
-
]
|
| 44 |
-
|
| 45 |
-
STATION_NAMES = {
|
| 46 |
-
"DT_0001": "인천", "DT_0002": "평택", "DT_0003": "영광", "DT_0008": "안산",
|
| 47 |
-
"DT_0017": "대산", "DT_0018": "군산", "DT_0024": "장항", "DT_0025": "보령",
|
| 48 |
-
"DT_0037": "어청도", "DT_0043": "영흥도", "DT_0050": "태안", "DT_0051": "서천마량",
|
| 49 |
-
"DT_0052": "인천송도", "DT_0065": "덕적도", "DT_0066": "향화도", "DT_0067": "안흥",
|
| 50 |
-
"DT_0068": "위도"
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
# --- 1. 유틸리티 함수들 ---
|
| 54 |
-
def clean_string(s):
|
| 55 |
-
"""문자열에서 특수 유니코드 문자 제거"""
|
| 56 |
-
if s is None:
|
| 57 |
-
return None
|
| 58 |
-
cleaned = s.replace('\u2028', '').replace('\u2029', '')
|
| 59 |
-
import re
|
| 60 |
-
cleaned = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', cleaned)
|
| 61 |
-
return cleaned.strip()
|
| 62 |
-
|
| 63 |
-
def get_supabase_client():
|
| 64 |
-
"""Supabase 클라이언트 생성"""
|
| 65 |
-
if not SUPABASE_AVAILABLE:
|
| 66 |
-
return None
|
| 67 |
-
|
| 68 |
-
try:
|
| 69 |
-
url = os.getenv("SUPABASE_URL")
|
| 70 |
-
key = os.getenv("SUPABASE_KEY")
|
| 71 |
-
|
| 72 |
-
if not url or not key:
|
| 73 |
-
print("Supabase 환경변수가 설정되지 않았습니다.")
|
| 74 |
-
return None
|
| 75 |
-
|
| 76 |
-
url = clean_string(url)
|
| 77 |
-
key = clean_string(key)
|
| 78 |
-
|
| 79 |
-
if not url.startswith('http'):
|
| 80 |
-
print(f"잘못된 SUPABASE_URL 형식: {url}")
|
| 81 |
-
return None
|
| 82 |
-
|
| 83 |
-
return create_client(url, key)
|
| 84 |
-
except Exception as e:
|
| 85 |
-
print(f"Supabase 연결 오류: {e}")
|
| 86 |
-
import traceback
|
| 87 |
-
traceback.print_exc()
|
| 88 |
-
return None
|
| 89 |
-
|
| 90 |
-
# --- 2. 예측 관련 함수들 ---
|
| 91 |
-
def get_harmonic_predictions(station_id, start_time, end_time):
|
| 92 |
-
"""해당 시간 범위의 조화 예측값 조회"""
|
| 93 |
-
supabase = get_supabase_client()
|
| 94 |
-
if not supabase:
|
| 95 |
-
print("Supabase 클라이언트를 생성할 수 없습니다.")
|
| 96 |
-
return []
|
| 97 |
-
|
| 98 |
-
try:
|
| 99 |
-
import pytz
|
| 100 |
-
kst = pytz.timezone('Asia/Seoul')
|
| 101 |
-
|
| 102 |
-
if start_time.tzinfo is None:
|
| 103 |
-
start_time = kst.localize(start_time)
|
| 104 |
-
if end_time.tzinfo is None:
|
| 105 |
-
end_time = kst.localize(end_time)
|
| 106 |
-
|
| 107 |
-
start_utc = start_time.astimezone(pytz.UTC)
|
| 108 |
-
end_utc = end_time.astimezone(pytz.UTC)
|
| 109 |
-
|
| 110 |
-
start_str = start_utc.strftime('%Y-%m-%dT%H:%M:%S+00:00')
|
| 111 |
-
end_str = end_utc.strftime('%Y-%m-%dT%H:%M:%S+00:00')
|
| 112 |
-
|
| 113 |
-
print(f"조화 예측 조회: {station_id}")
|
| 114 |
-
print(f" KST 시간: {start_time.strftime('%Y-%m-%d %H:%M')} ~ {end_time.strftime('%Y-%m-%d %H:%M')}")
|
| 115 |
-
print(f" UTC 시간: {start_str} ~ {end_str}")
|
| 116 |
-
|
| 117 |
-
result = supabase.table('harmonic_predictions')\
|
| 118 |
-
.select('predicted_at, harmonic_level')\
|
| 119 |
-
.eq('station_id', station_id)\
|
| 120 |
-
.gte('predicted_at', start_str)\
|
| 121 |
-
.lte('predicted_at', end_str)\
|
| 122 |
-
.order('predicted_at')\
|
| 123 |
-
.limit(1000)\
|
| 124 |
-
.execute()
|
| 125 |
-
|
| 126 |
-
if result.data:
|
| 127 |
-
print(f"조화 예측 데이터 {len(result.data)}개 조회 성공")
|
| 128 |
-
for i, item in enumerate(result.data[:3]):
|
| 129 |
-
print(f" 샘플 {i+1}: {item['predicted_at']}, {item['harmonic_level']:.2f}cm")
|
| 130 |
-
else:
|
| 131 |
-
print("조화 예측 데이터가 없습니다.")
|
| 132 |
-
check_result = supabase.table('harmonic_predictions')\
|
| 133 |
-
.select('predicted_at')\
|
| 134 |
-
.eq('station_id', station_id)\
|
| 135 |
-
.order('predicted_at')\
|
| 136 |
-
.limit(1)\
|
| 137 |
-
.execute()
|
| 138 |
-
|
| 139 |
-
if check_result.data:
|
| 140 |
-
print(f" 해당 관측소의 가장 빠른 예측 시간: {check_result.data[0]['predicted_at']}")
|
| 141 |
-
|
| 142 |
-
return result.data if result.data else []
|
| 143 |
-
except Exception as e:
|
| 144 |
-
print(f"조화 예측값 조회 오류: {e}")
|
| 145 |
-
traceback.print_exc()
|
| 146 |
-
return []
|
| 147 |
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
import pytz
|
| 154 |
-
kst = pytz.timezone('Asia/Seoul')
|
| 155 |
-
if last_time.tzinfo is None:
|
| 156 |
-
last_time = kst.localize(last_time)
|
| 157 |
-
|
| 158 |
-
start_time = last_time + timedelta(minutes=5)
|
| 159 |
-
end_time = last_time + timedelta(minutes=72*5)
|
| 160 |
-
|
| 161 |
-
harmonic_data = get_harmonic_predictions(station_id, start_time, end_time)
|
| 162 |
-
|
| 163 |
-
residual_flat = residual_predictions.flatten()
|
| 164 |
-
num_points = len(residual_flat)
|
| 165 |
-
|
| 166 |
-
if not harmonic_data:
|
| 167 |
-
print("조화 예측 데이터를 찾을 수 없습니다. 잔차 예측만 반환합니다.")
|
| 168 |
-
return {
|
| 169 |
-
'times': [last_time + timedelta(minutes=(i+1)*5) for i in range(num_points)],
|
| 170 |
-
'residual': residual_flat.tolist(),
|
| 171 |
-
'harmonic': [0.0] * num_points,
|
| 172 |
-
'final_tide': residual_flat.tolist()
|
| 173 |
-
}
|
| 174 |
-
|
| 175 |
-
final_results = {
|
| 176 |
-
'times': [],
|
| 177 |
-
'residual': [],
|
| 178 |
-
'harmonic': [],
|
| 179 |
-
'final_tide': []
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
harmonic_dict = {}
|
| 183 |
-
for h_data in harmonic_data:
|
| 184 |
-
h_time_str = h_data['predicted_at']
|
| 185 |
-
|
| 186 |
-
try:
|
| 187 |
-
if 'T' in h_time_str:
|
| 188 |
-
if h_time_str.endswith('Z'):
|
| 189 |
-
h_time = datetime.fromisoformat(h_time_str[:-1] + '+00:00')
|
| 190 |
-
elif '+' in h_time_str or '-' in h_time_str[-6:]:
|
| 191 |
-
h_time = datetime.fromisoformat(h_time_str)
|
| 192 |
-
else:
|
| 193 |
-
h_time = datetime.fromisoformat(h_time_str + '+00:00')
|
| 194 |
-
else:
|
| 195 |
-
from dateutil import parser
|
| 196 |
-
h_time = parser.parse(h_time_str)
|
| 197 |
-
|
| 198 |
-
if h_time.tzinfo is None:
|
| 199 |
-
h_time = pytz.UTC.localize(h_time)
|
| 200 |
-
h_time = h_time.astimezone(kst)
|
| 201 |
-
|
| 202 |
-
except Exception as e:
|
| 203 |
-
print(f"시간 파싱 오류: {h_time_str}, {e}")
|
| 204 |
-
continue
|
| 205 |
-
|
| 206 |
-
minutes = (h_time.minute // 5) * 5
|
| 207 |
-
h_time = h_time.replace(minute=minutes, second=0, microsecond=0)
|
| 208 |
-
harmonic_value = float(h_data['harmonic_level'])
|
| 209 |
-
harmonic_dict[h_time] = harmonic_value
|
| 210 |
-
|
| 211 |
-
for i, residual in enumerate(residual_flat):
|
| 212 |
-
pred_time = last_time + timedelta(minutes=(i+1)*5)
|
| 213 |
-
pred_time = pred_time.replace(second=0, microsecond=0)
|
| 214 |
-
|
| 215 |
-
harmonic_value = harmonic_dict.get(pred_time, 0.0)
|
| 216 |
-
|
| 217 |
-
if harmonic_value == 0.0 and harmonic_dict:
|
| 218 |
-
min_diff = float('inf')
|
| 219 |
-
for h_time, h_val in harmonic_dict.items():
|
| 220 |
-
diff = abs((h_time - pred_time).total_seconds())
|
| 221 |
-
if diff < min_diff and diff < 300:
|
| 222 |
-
min_diff = diff
|
| 223 |
-
harmonic_value = h_val
|
| 224 |
-
|
| 225 |
-
final_tide = float(residual) + harmonic_value
|
| 226 |
-
|
| 227 |
-
final_results['times'].append(pred_time)
|
| 228 |
-
final_results['residual'].append(float(residual))
|
| 229 |
-
final_results['harmonic'].append(harmonic_value)
|
| 230 |
-
final_results['final_tide'].append(final_tide)
|
| 231 |
-
|
| 232 |
-
print(f"최종 조위 계산 완료: {len(final_results['times'])}개 포인트")
|
| 233 |
-
return final_results
|
| 234 |
-
|
| 235 |
-
def save_predictions_to_supabase(station_id, prediction_results):
|
| 236 |
-
"""예측 결과를 Supabase에 저장"""
|
| 237 |
-
supabase = get_supabase_client()
|
| 238 |
-
if not supabase:
|
| 239 |
-
print("Supabase 클라이언트를 생성할 수 없습니다.")
|
| 240 |
-
return 0
|
| 241 |
-
|
| 242 |
-
try:
|
| 243 |
-
if prediction_results['times']:
|
| 244 |
-
start_time = prediction_results['times'][0].strftime('%Y-%m-%dT%H:%M:%S')
|
| 245 |
-
end_time = prediction_results['times'][-1].strftime('%Y-%m-%dT%H:%M:%S')
|
| 246 |
-
|
| 247 |
-
supabase.table('tide_predictions')\
|
| 248 |
-
.delete()\
|
| 249 |
-
.eq('station_id', station_id)\
|
| 250 |
-
.gte('predicted_at', start_time)\
|
| 251 |
-
.lte('predicted_at', end_time)\
|
| 252 |
-
.execute()
|
| 253 |
-
|
| 254 |
-
insert_data = []
|
| 255 |
-
for i in range(len(prediction_results['times'])):
|
| 256 |
-
time_str = prediction_results['times'][i].strftime('%Y-%m-%dT%H:%M:%S')
|
| 257 |
-
|
| 258 |
-
insert_data.append({
|
| 259 |
-
'station_id': station_id,
|
| 260 |
-
'predicted_at': time_str,
|
| 261 |
-
'predicted_residual': float(prediction_results['residual'][i]),
|
| 262 |
-
'harmonic_level': float(prediction_results['harmonic'][i]),
|
| 263 |
-
'final_tide_level': float(prediction_results['final_tide'][i])
|
| 264 |
-
})
|
| 265 |
-
|
| 266 |
-
result = supabase.table('tide_predictions')\
|
| 267 |
-
.insert(insert_data)\
|
| 268 |
-
.execute()
|
| 269 |
-
|
| 270 |
-
return len(insert_data)
|
| 271 |
-
except Exception as e:
|
| 272 |
-
print(f"예측 결과 저장 오류: {e}")
|
| 273 |
-
traceback.print_exc()
|
| 274 |
-
return 0
|
| 275 |
-
|
| 276 |
-
def get_common_args(station_id):
|
| 277 |
-
return [
|
| 278 |
-
"--model", "TimeXer", "--features", "MS", "--seq_len", "144", "--pred_len", "72",
|
| 279 |
-
"--label_len", "96", "--enc_in", "5", "--dec_in", "5", "--c_out", "1",
|
| 280 |
-
"--d_model", "256", "--d_ff", "512", "--n_heads", "8", "--e_layers", "1",
|
| 281 |
-
"--d_layers", "1", "--factor", "3", "--patch_len", "16", "--expand", "2", "--d_conv", "4"
|
| 282 |
-
]
|
| 283 |
-
|
| 284 |
-
def validate_csv_file(file_path, required_rows=144):
|
| 285 |
-
"""CSV 파일 유효성 검사"""
|
| 286 |
-
try:
|
| 287 |
-
df = pd.read_csv(file_path)
|
| 288 |
-
required_columns = ['date', 'air_pres', 'wind_dir', 'wind_speed', 'air_temp', 'residual']
|
| 289 |
-
missing_columns = [col for col in required_columns if col not in df.columns]
|
| 290 |
-
|
| 291 |
-
if missing_columns:
|
| 292 |
-
return False, f"필수 컬럼이 누락되었습니다: {missing_columns}"
|
| 293 |
-
|
| 294 |
-
if len(df) < required_rows:
|
| 295 |
-
return False, f"데이터가 부족합니다. 최소 {required_rows}행 필요, 현재 {len(df)}행"
|
| 296 |
-
|
| 297 |
-
return True, "파일이 유효합니다."
|
| 298 |
-
except Exception as e:
|
| 299 |
-
return False, f"파일 읽기 오류: {str(e)}"
|
| 300 |
-
|
| 301 |
-
def execute_inference_and_get_results(command):
|
| 302 |
-
"""inference 실행하고 결과 파일을 읽어서 반환"""
|
| 303 |
-
try:
|
| 304 |
-
print(f"실행 명령어: {' '.join(command)}")
|
| 305 |
-
result = subprocess.run(command, capture_output=True, text=True, timeout=300)
|
| 306 |
-
|
| 307 |
-
if result.returncode != 0:
|
| 308 |
-
error_message = (
|
| 309 |
-
f"실행 실패 (Exit Code: {result.returncode}):\n\n"
|
| 310 |
-
f"--- 에러 로그 ---\n{result.stderr}\n\n"
|
| 311 |
-
f"--- 일반 출력 ---\n{result.stdout}"
|
| 312 |
-
)
|
| 313 |
-
raise gr.Error(error_message)
|
| 314 |
-
|
| 315 |
-
return True, result.stdout
|
| 316 |
-
except subprocess.TimeoutExpired:
|
| 317 |
-
raise gr.Error("실행 시간이 초과되었습니다. (5분 제한)")
|
| 318 |
-
except Exception as e:
|
| 319 |
-
raise gr.Error(f"내부 오류: {str(e)}")
|
| 320 |
-
|
| 321 |
-
def create_enhanced_prediction_plot(prediction_results, input_data, station_name):
|
| 322 |
-
"""잔차 + 조화 + 최종 조위를 모두 표시하는 향상된 플롯"""
|
| 323 |
-
try:
|
| 324 |
-
input_df = pd.read_csv(input_data.name)
|
| 325 |
-
input_df['date'] = pd.to_datetime(input_df['date'])
|
| 326 |
-
|
| 327 |
-
recent_data = input_df.tail(24)
|
| 328 |
-
future_times = pd.to_datetime(prediction_results['times'])
|
| 329 |
-
|
| 330 |
-
fig = go.Figure()
|
| 331 |
-
|
| 332 |
-
fig.add_trace(go.Scatter(
|
| 333 |
-
x=recent_data['date'],
|
| 334 |
-
y=recent_data['residual'],
|
| 335 |
-
mode='lines+markers',
|
| 336 |
-
name='실제 잔차조위',
|
| 337 |
-
line=dict(color='blue', width=2),
|
| 338 |
-
marker=dict(size=4)
|
| 339 |
-
))
|
| 340 |
-
|
| 341 |
-
fig.add_trace(go.Scatter(
|
| 342 |
-
x=future_times,
|
| 343 |
-
y=prediction_results['residual'],
|
| 344 |
-
mode='lines+markers',
|
| 345 |
-
name='잔차 예측',
|
| 346 |
-
line=dict(color='red', width=2, dash='dash'),
|
| 347 |
-
marker=dict(size=3)
|
| 348 |
-
))
|
| 349 |
-
|
| 350 |
-
if any(h != 0 for h in prediction_results['harmonic']):
|
| 351 |
-
fig.add_trace(go.Scatter(
|
| 352 |
-
x=future_times,
|
| 353 |
-
y=prediction_results['harmonic'],
|
| 354 |
-
mode='lines',
|
| 355 |
-
name='조화 예측',
|
| 356 |
-
line=dict(color='orange', width=2)
|
| 357 |
-
))
|
| 358 |
-
|
| 359 |
-
fig.add_trace(go.Scatter(
|
| 360 |
-
x=future_times,
|
| 361 |
-
y=prediction_results['final_tide'],
|
| 362 |
-
mode='lines+markers',
|
| 363 |
-
name='최종 조위',
|
| 364 |
-
line=dict(color='green', width=3),
|
| 365 |
-
marker=dict(size=4)
|
| 366 |
-
))
|
| 367 |
-
|
| 368 |
-
last_time = recent_data['date'].iloc[-1]
|
| 369 |
-
|
| 370 |
-
fig.add_annotation(
|
| 371 |
-
x=last_time,
|
| 372 |
-
y=0,
|
| 373 |
-
text="← 과거 | 미래 →",
|
| 374 |
-
showarrow=False,
|
| 375 |
-
yref="paper",
|
| 376 |
-
yshift=10,
|
| 377 |
-
font=dict(size=12, color="gray")
|
| 378 |
-
)
|
| 379 |
-
|
| 380 |
-
fig.update_layout(
|
| 381 |
-
title=f'{station_name} 통합 조위 예측 결과',
|
| 382 |
-
xaxis_title='시간',
|
| 383 |
-
yaxis_title='수위 (cm)',
|
| 384 |
-
hovermode='x unified',
|
| 385 |
-
height=600,
|
| 386 |
-
showlegend=True,
|
| 387 |
-
xaxis=dict(tickformat='%H:%M<br>%m/%d', gridcolor='lightgray', showgrid=True),
|
| 388 |
-
yaxis=dict(gridcolor='lightgray', showgrid=True),
|
| 389 |
-
plot_bgcolor='white'
|
| 390 |
-
)
|
| 391 |
-
|
| 392 |
-
return fig
|
| 393 |
-
except Exception as e:
|
| 394 |
-
print(f"Enhanced plot creation error: {e}")
|
| 395 |
-
traceback.print_exc()
|
| 396 |
-
fig = go.Figure()
|
| 397 |
-
fig.add_annotation(
|
| 398 |
-
text=f"시각화 생성 중 오류: {str(e)}",
|
| 399 |
-
xref="paper", yref="paper",
|
| 400 |
-
x=0.5, y=0.5, showarrow=False
|
| 401 |
-
)
|
| 402 |
-
return fig
|
| 403 |
-
|
| 404 |
-
def single_prediction(station_id, input_csv_file):
|
| 405 |
-
if input_csv_file is None:
|
| 406 |
-
raise gr.Error("예측을 위한 입력 파일을 업로드해��세요.")
|
| 407 |
-
|
| 408 |
-
is_valid, message = validate_csv_file(input_csv_file.name)
|
| 409 |
-
if not is_valid:
|
| 410 |
-
raise gr.Error(f"파일 오류: {message}")
|
| 411 |
-
|
| 412 |
-
station_name = STATION_NAMES.get(station_id, station_id)
|
| 413 |
-
|
| 414 |
-
common_args = get_common_args(station_id)
|
| 415 |
-
setting_name = f"long_term_forecast_{station_id}_144_72_TimeXer_TIDE_ftMS_sl144_ll96_pl72_dm256_nh8_el1_dl1_df512_expand2_dc4_fc3_ebtimeF_dtTrue_Exp_0"
|
| 416 |
-
checkpoint_path = f"./checkpoints/{setting_name}/checkpoint.pth"
|
| 417 |
-
scaler_path = f"./checkpoints/{setting_name}/scaler.gz"
|
| 418 |
-
|
| 419 |
-
if not os.path.exists(checkpoint_path):
|
| 420 |
-
raise gr.Error(f"모델 파일을 찾을 수 없습니다: {checkpoint_path}")
|
| 421 |
-
if not os.path.exists(scaler_path):
|
| 422 |
-
raise gr.Error(f"스케일러 파일을 찾을 수 없습니다: {scaler_path}")
|
| 423 |
-
|
| 424 |
-
command = ["python", "inference.py",
|
| 425 |
-
"--checkpoint_path", checkpoint_path,
|
| 426 |
-
"--scaler_path", scaler_path,
|
| 427 |
-
"--predict_input_file", input_csv_file.name] + common_args
|
| 428 |
-
|
| 429 |
-
gr.Info(f"{station_name}({station_id}) 통합 조위 예측을 실행중입니다...")
|
| 430 |
-
|
| 431 |
-
success, output = execute_inference_and_get_results(command)
|
| 432 |
-
|
| 433 |
-
try:
|
| 434 |
-
prediction_file = "pred_results/prediction_future.npy"
|
| 435 |
-
if os.path.exists(prediction_file):
|
| 436 |
-
residual_predictions = np.load(prediction_file)
|
| 437 |
-
|
| 438 |
-
input_df = pd.read_csv(input_csv_file.name)
|
| 439 |
-
input_df['date'] = pd.to_datetime(input_df['date'])
|
| 440 |
-
last_time = input_df['date'].iloc[-1]
|
| 441 |
-
|
| 442 |
-
prediction_results = calculate_final_tide(residual_predictions, station_id, last_time)
|
| 443 |
-
plot = create_enhanced_prediction_plot(prediction_results, input_csv_file, station_name)
|
| 444 |
-
|
| 445 |
-
has_harmonic = any(h != 0 for h in prediction_results['harmonic'])
|
| 446 |
-
|
| 447 |
-
if has_harmonic:
|
| 448 |
-
result_df = pd.DataFrame({
|
| 449 |
-
'예측 시간': [t.strftime('%Y-%m-%d %H:%M') for t in prediction_results['times']],
|
| 450 |
-
'잔차 예측 (cm)': [f"{val:.2f}" for val in prediction_results['residual']],
|
| 451 |
-
'조화 예측 (cm)': [f"{val:.2f}" for val in prediction_results['harmonic']],
|
| 452 |
-
'최종 조위 (cm)': [f"{val:.2f}" for val in prediction_results['final_tide']]
|
| 453 |
-
})
|
| 454 |
-
else:
|
| 455 |
-
result_df = pd.DataFrame({
|
| 456 |
-
'예측 시간': [t.strftime('%Y-%m-%d %H:%M') for t in prediction_results['times']],
|
| 457 |
-
'잔차 예측 (cm)': [f"{val:.2f}" for val in prediction_results['residual']]
|
| 458 |
-
})
|
| 459 |
-
|
| 460 |
-
saved_count = save_predictions_to_supabase(station_id, prediction_results)
|
| 461 |
-
if saved_count > 0:
|
| 462 |
-
save_message = f"\n💾 Supabase에 {saved_count}개 예측 결과 저장 완료!"
|
| 463 |
-
elif get_supabase_client() is None:
|
| 464 |
-
save_message = "\n⚠️ Supabase 연결 실패 (환경변수 확인 필요)"
|
| 465 |
-
else:
|
| 466 |
-
save_message = "\n⚠️ Supabase 저장 실패"
|
| 467 |
-
|
| 468 |
-
return plot, result_df, f"✅ 예측 완료!{save_message}\n\n{output}"
|
| 469 |
-
else:
|
| 470 |
-
return None, None, f"❌ 결과 파일을 찾을 수 없습니다.\n\n{output}"
|
| 471 |
-
except Exception as e:
|
| 472 |
-
print(f"Result processing error: {e}")
|
| 473 |
-
traceback.print_exc()
|
| 474 |
-
return None, None, f"❌ 결과 처리 중 오류: {str(e)}\n\n{output}"
|
| 475 |
-
|
| 476 |
-
# --- 3. Gemini 챗봇 관련 함수들 ---
|
| 477 |
-
def parse_intent_with_llm(message: str) -> dict:
|
| 478 |
-
"""LLM을 사용해 사용자 질문에서 의도를 분석하고 JSON으로 반환"""
|
| 479 |
-
if not GEMINI_API_KEY:
|
| 480 |
-
return {"error": "Gemini API 키가 설정되지 않았습니다."}
|
| 481 |
-
|
| 482 |
-
prompt = f"""
|
| 483 |
-
당신은 사용자의 자연어 질문을 분석하여 JSON 객체로 변환하는 전문가입니다.
|
| 484 |
-
질문에서 '관측소 이름', '원하는 정보', '시작 시간', '종료 시간'을 추출해주세요.
|
| 485 |
-
현재 시간은 {datetime.now(pytz.timezone('Asia/Seoul')).strftime('%Y-%m-%d %H:%M:%S')} KST 입니다.
|
| 486 |
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
- 관측소 이름이 없으면 '인천'을 기본값으로 사용하세요.
|
| 491 |
-
|
| 492 |
-
[사용자 질문]: {message}
|
| 493 |
-
[JSON 출력]:
|
| 494 |
-
"""
|
| 495 |
-
try:
|
| 496 |
-
genai.configure(api_key=GEMINI_API_KEY)
|
| 497 |
-
model = genai.GenerativeModel('gemini-1.5-flash', generation_config={"response_mime_type": "application/json"})
|
| 498 |
-
response = model.generate_content(prompt)
|
| 499 |
-
return json.loads(response.text)
|
| 500 |
-
except Exception as e:
|
| 501 |
-
return {"error": f"LLM 의도 분석 중 오류 발생: {e}"}
|
| 502 |
-
|
| 503 |
-
def retrieve_context_from_db(intent: dict) -> str:
|
| 504 |
-
"""분석된 의도를 바탕으로 데이터베이스에서 정보 검색"""
|
| 505 |
-
supabase = get_supabase_client()
|
| 506 |
-
if not supabase:
|
| 507 |
-
return "데이터베이스에 연결할 수 없습니다."
|
| 508 |
-
|
| 509 |
-
if "error" in intent:
|
| 510 |
-
return f"의도 분석에 실패했습니다: {intent['error']}"
|
| 511 |
-
|
| 512 |
-
station_name = intent.get("관측소 이름", "인천")
|
| 513 |
-
start_time_str = intent.get("시작 시간")
|
| 514 |
-
end_time_str = intent.get("종료 시간")
|
| 515 |
-
|
| 516 |
-
station_id = next((sid for sid, name in STATION_NAMES.items() if name == station_name), "DT_0001")
|
| 517 |
-
|
| 518 |
-
if not start_time_str or not end_time_str:
|
| 519 |
-
return "질문에서 시간 정보를 찾을 수 없습니다."
|
| 520 |
-
|
| 521 |
-
try:
|
| 522 |
-
start_time = date_parser.parse(start_time_str)
|
| 523 |
-
end_time = date_parser.parse(end_time_str)
|
| 524 |
-
|
| 525 |
-
start_query_str = start_time.strftime('%Y-%m-%d %H:%M:%S')
|
| 526 |
-
end_query_str = end_time.strftime('%Y-%m-%d %H:%M:%S')
|
| 527 |
-
|
| 528 |
-
print(f"'{station_id}'의 최종 예측 테이블 조회 중...")
|
| 529 |
-
print(f"조회 시간 범위 (KST): {start_query_str} ~ {end_query_str}")
|
| 530 |
-
|
| 531 |
-
result = supabase.table('tide_predictions')\
|
| 532 |
-
.select('*')\
|
| 533 |
-
.eq('station_id', station_id)\
|
| 534 |
-
.gte('predicted_at', start_query_str)\
|
| 535 |
-
.lte('predicted_at', end_query_str)\
|
| 536 |
-
.order('predicted_at')\
|
| 537 |
-
.execute()
|
| 538 |
-
|
| 539 |
-
if result.data:
|
| 540 |
-
kst = pytz.timezone('Asia/Seoul')
|
| 541 |
-
info_text = f"'{station_name}'의 '{start_time_str}'부터 '{end_time_str}'까지 조위 정보입니다.\n\n"
|
| 542 |
-
|
| 543 |
-
if len(result.data) > 10:
|
| 544 |
-
levels = [d['final_tide_level'] for d in result.data]
|
| 545 |
-
max_level = max(levels)
|
| 546 |
-
min_level = min(levels)
|
| 547 |
-
info_text += f"- 최고 조위: {max_level:.1f}cm\n- 최저 조위: {min_level:.1f}cm"
|
| 548 |
-
else:
|
| 549 |
-
for d in result.data:
|
| 550 |
-
time_kst = date_parser.parse(d['predicted_at']).strftime('%H:%M')
|
| 551 |
-
info_text += f"- {time_kst}: 최종 조위 {d['final_tide_level']:.1f}cm (잔차 {d['predicted_residual']:.1f}cm)\n"
|
| 552 |
-
return info_text
|
| 553 |
-
else:
|
| 554 |
-
return "해당 기간의 예측 데이터를 찾을 수 없습니다. '통합 조위 예측' 탭에서 먼저 예측을 실행해주세요."
|
| 555 |
-
|
| 556 |
-
except Exception as e:
|
| 557 |
-
return f"데이터 검색 중 오류 발생: {traceback.format_exc()}"
|
| 558 |
-
|
| 559 |
-
def process_chatbot_query_with_llm(message: str, history: list) -> str:
|
| 560 |
-
"""최종 RAG 파이프라인"""
|
| 561 |
-
intent = parse_intent_with_llm(message)
|
| 562 |
-
retrieved_data = retrieve_context_from_db(intent)
|
| 563 |
-
|
| 564 |
-
prompt = f"""당신은 친절한 해양 조위 정보 전문가입니다. 주어진 [검색된 데이터]를 바탕으로 사용자의 [질문]에 대해 자연스러운 문장으로 답변해주세요.
|
| 565 |
-
[검색된 데이터]: {retrieved_data}
|
| 566 |
-
[사용자 질문]: {message}
|
| 567 |
-
[답변]:"""
|
| 568 |
-
|
| 569 |
-
try:
|
| 570 |
-
genai.configure(api_key=GEMINI_API_KEY)
|
| 571 |
-
model = genai.GenerativeModel('gemini-1.5-flash')
|
| 572 |
-
response = model.generate_content(prompt)
|
| 573 |
-
return response.text
|
| 574 |
-
except Exception as e:
|
| 575 |
-
return f"Gemini 답변 생성 중 오류가 발생했습니다: {e}"
|
| 576 |
-
|
| 577 |
-
# --- 4. API 함수들 ---
|
| 578 |
-
def api_get_current_tide(station_id):
|
| 579 |
-
"""현재 조위 조회"""
|
| 580 |
-
supabase = get_supabase_client()
|
| 581 |
-
if not supabase:
|
| 582 |
-
return {"error": "Database connection failed"}
|
| 583 |
-
|
| 584 |
-
result = supabase.table('tide_predictions')\
|
| 585 |
-
.select('*')\
|
| 586 |
-
.eq('station_id', station_id)\
|
| 587 |
-
.gte('predicted_at', datetime.now().isoformat())\
|
| 588 |
-
.order('predicted_at')\
|
| 589 |
-
.limit(1)\
|
| 590 |
-
.execute()
|
| 591 |
-
|
| 592 |
-
if result.data:
|
| 593 |
-
data = result.data[0]
|
| 594 |
-
return {
|
| 595 |
-
"station_id": station_id,
|
| 596 |
-
"time": data['predicted_at'],
|
| 597 |
-
"tide_level": data.get('final_tide_level', 0),
|
| 598 |
-
"residual": data.get('predicted_residual', 0),
|
| 599 |
-
"harmonic": data.get('harmonic_level', 0)
|
| 600 |
-
}
|
| 601 |
-
return {"error": "No data found"}
|
| 602 |
-
|
| 603 |
-
def api_get_extremes(station_id, hours=24):
|
| 604 |
-
"""만조/간조 시간 조회"""
|
| 605 |
-
supabase = get_supabase_client()
|
| 606 |
-
if not supabase:
|
| 607 |
-
return {"error": "Database connection failed"}
|
| 608 |
-
|
| 609 |
-
end_time = datetime.now() + timedelta(hours=hours)
|
| 610 |
-
|
| 611 |
-
result = supabase.table('tide_predictions')\
|
| 612 |
-
.select('predicted_at, final_tide_level')\
|
| 613 |
-
.eq('station_id', station_id)\
|
| 614 |
-
.gte('predicted_at', datetime.now().isoformat())\
|
| 615 |
-
.lte('predicted_at', end_time.isoformat())\
|
| 616 |
-
.order('predicted_at')\
|
| 617 |
-
.execute()
|
| 618 |
-
|
| 619 |
-
if not result.data:
|
| 620 |
-
return {"error": "No data found"}
|
| 621 |
-
|
| 622 |
-
extremes = []
|
| 623 |
-
data = result.data
|
| 624 |
-
|
| 625 |
-
for i in range(1, len(data) - 1):
|
| 626 |
-
prev_level = data[i-1]['final_tide_level']
|
| 627 |
-
curr_level = data[i]['final_tide_level']
|
| 628 |
-
next_level = data[i+1]['final_tide_level']
|
| 629 |
-
|
| 630 |
-
if curr_level > prev_level and curr_level > next_level:
|
| 631 |
-
extremes.append({
|
| 632 |
-
'type': 'high',
|
| 633 |
-
'time': data[i]['predicted_at'],
|
| 634 |
-
'level': curr_level
|
| 635 |
-
})
|
| 636 |
-
elif curr_level < prev_level and curr_level < next_level:
|
| 637 |
-
extremes.append({
|
| 638 |
-
'type': 'low',
|
| 639 |
-
'time': data[i]['predicted_at'],
|
| 640 |
-
'level': curr_level
|
| 641 |
-
})
|
| 642 |
-
|
| 643 |
-
return {
|
| 644 |
-
"station_id": station_id,
|
| 645 |
-
"hours": hours,
|
| 646 |
-
"count": len(extremes),
|
| 647 |
-
"extremes": extremes
|
| 648 |
-
}
|
| 649 |
-
def api_get_historical_tide(station_id, date_str, hours=24):
|
| 650 |
-
"""과거 특정 날짜의 조위 데이터 조회"""
|
| 651 |
-
supabase = get_supabase_client()
|
| 652 |
-
if not supabase:
|
| 653 |
-
return {"error": "Database connection failed"}
|
| 654 |
-
|
| 655 |
-
try:
|
| 656 |
-
# 날짜 파싱 (YYYY-MM-DD 형식)
|
| 657 |
-
start_date = datetime.strptime(date_str, "%Y-%m-%d")
|
| 658 |
-
end_date = start_date + timedelta(hours=hours)
|
| 659 |
-
|
| 660 |
-
# 과거 예측 데이터 조회
|
| 661 |
-
result = supabase.table('tide_predictions')\
|
| 662 |
-
.select('predicted_at, final_tide_level, harmonic_level, predicted_residual')\
|
| 663 |
-
.eq('station_id', station_id)\
|
| 664 |
-
.gte('predicted_at', start_date.isoformat())\
|
| 665 |
-
.lte('predicted_at', end_date.isoformat())\
|
| 666 |
-
.order('predicted_at')\
|
| 667 |
-
.execute()
|
| 668 |
-
|
| 669 |
-
if result.data:
|
| 670 |
-
# 통계 계산
|
| 671 |
-
levels = [d['final_tide_level'] for d in result.data]
|
| 672 |
-
|
| 673 |
-
return {
|
| 674 |
-
"station_id": station_id,
|
| 675 |
-
"date": date_str,
|
| 676 |
-
"hours": hours,
|
| 677 |
-
"count": len(result.data),
|
| 678 |
-
"statistics": {
|
| 679 |
-
"max": max(levels),
|
| 680 |
-
"min": min(levels),
|
| 681 |
-
"avg": sum(levels) / len(levels)
|
| 682 |
-
},
|
| 683 |
-
"data": result.data[:100] # 최대 100개만 반환
|
| 684 |
-
}
|
| 685 |
-
|
| 686 |
-
# 예측 데이터가 없으면 관측 데이터 확인
|
| 687 |
-
result = supabase.table('tide_observations')\
|
| 688 |
-
.select('observed_at, residual, air_pres, wind_speed, air_temp')\
|
| 689 |
-
.eq('station_id', station_id)\
|
| 690 |
-
.gte('observed_at', start_date.isoformat())\
|
| 691 |
-
.lte('observed_at', end_date.isoformat())\
|
| 692 |
-
.order('observed_at')\
|
| 693 |
-
.execute()
|
| 694 |
-
|
| 695 |
-
if result.data:
|
| 696 |
-
return {
|
| 697 |
-
"station_id": station_id,
|
| 698 |
-
"date": date_str,
|
| 699 |
-
"type": "observation", # 관측 데이터임을 표시
|
| 700 |
-
"count": len(result.data),
|
| 701 |
-
"data": result.data[:100]
|
| 702 |
-
}
|
| 703 |
-
|
| 704 |
-
return {"error": "No historical data found for this date"}
|
| 705 |
-
|
| 706 |
-
except Exception as e:
|
| 707 |
-
return {"error": f"Date parsing error: {str(e)}"}
|
| 708 |
-
|
| 709 |
-
def api_get_historical_extremes(station_id, date_str):
|
| 710 |
-
"""과거 특정 날짜의 만조/간조 정보"""
|
| 711 |
-
supabase = get_supabase_client()
|
| 712 |
-
if not supabase:
|
| 713 |
-
return {"error": "Database connection failed"}
|
| 714 |
-
|
| 715 |
-
try:
|
| 716 |
-
# 하루 전체 데이터
|
| 717 |
-
start_date = datetime.strptime(date_str, "%Y-%m-%d")
|
| 718 |
-
end_date = start_date + timedelta(days=1)
|
| 719 |
-
|
| 720 |
-
result = supabase.table('tide_predictions')\
|
| 721 |
-
.select('predicted_at, final_tide_level')\
|
| 722 |
-
.eq('station_id', station_id)\
|
| 723 |
-
.gte('predicted_at', start_date.isoformat())\
|
| 724 |
-
.lt('predicted_at', end_date.isoformat())\
|
| 725 |
-
.order('predicted_at')\
|
| 726 |
-
.execute()
|
| 727 |
-
|
| 728 |
-
if not result.data or len(result.data) < 3:
|
| 729 |
-
return {"error": "Insufficient data for this date"}
|
| 730 |
-
|
| 731 |
-
# 극값 찾기
|
| 732 |
-
extremes = []
|
| 733 |
-
data = result.data
|
| 734 |
-
|
| 735 |
-
for i in range(1, len(data) - 1):
|
| 736 |
-
prev_level = data[i-1]['final_tide_level']
|
| 737 |
-
curr_level = data[i]['final_tide_level']
|
| 738 |
-
next_level = data[i+1]['final_tide_level']
|
| 739 |
-
|
| 740 |
-
if curr_level > prev_level and curr_level > next_level:
|
| 741 |
-
extremes.append({
|
| 742 |
-
'type': 'high',
|
| 743 |
-
'time': data[i]['predicted_at'],
|
| 744 |
-
'level': curr_level
|
| 745 |
-
})
|
| 746 |
-
elif curr_level < prev_level and curr_level < next_level:
|
| 747 |
-
extremes.append({
|
| 748 |
-
'type': 'low',
|
| 749 |
-
'time': data[i]['predicted_at'],
|
| 750 |
-
'level': curr_level
|
| 751 |
-
})
|
| 752 |
-
|
| 753 |
-
# 최고/최저 찾기
|
| 754 |
-
all_levels = [d['final_tide_level'] for d in data]
|
| 755 |
-
daily_max = max(all_levels)
|
| 756 |
-
daily_min = min(all_levels)
|
| 757 |
-
|
| 758 |
-
return {
|
| 759 |
-
"station_id": station_id,
|
| 760 |
-
"date": date_str,
|
| 761 |
-
"daily_max": daily_max,
|
| 762 |
-
"daily_min": daily_min,
|
| 763 |
-
"daily_range": daily_max - daily_min,
|
| 764 |
-
"extremes": extremes,
|
| 765 |
-
"high_tide_count": len([e for e in extremes if e['type'] == 'high']),
|
| 766 |
-
"low_tide_count": len([e for e in extremes if e['type'] == 'low'])
|
| 767 |
-
}
|
| 768 |
-
|
| 769 |
-
except Exception as e:
|
| 770 |
-
return {"error": f"Error: {str(e)}"}
|
| 771 |
-
|
| 772 |
-
def api_compare_dates(station_id, date1, date2):
|
| 773 |
-
"""두 날짜의 조위 패턴 비교"""
|
| 774 |
-
supabase = get_supabase_client()
|
| 775 |
-
if not supabase:
|
| 776 |
-
return {"error": "Database connection failed"}
|
| 777 |
-
|
| 778 |
-
try:
|
| 779 |
-
results = {}
|
| 780 |
-
|
| 781 |
-
for date_str in [date1, date2]:
|
| 782 |
-
start_date = datetime.strptime(date_str, "%Y-%m-%d")
|
| 783 |
-
end_date = start_date + timedelta(days=1)
|
| 784 |
-
|
| 785 |
-
result = supabase.table('tide_predictions')\
|
| 786 |
-
.select('predicted_at, final_tide_level')\
|
| 787 |
-
.eq('station_id', station_id)\
|
| 788 |
-
.gte('predicted_at', start_date.isoformat())\
|
| 789 |
-
.lt('predicted_at', end_date.isoformat())\
|
| 790 |
-
.order('predicted_at')\
|
| 791 |
-
.execute()
|
| 792 |
-
|
| 793 |
-
if result.data:
|
| 794 |
-
levels = [d['final_tide_level'] for d in result.data]
|
| 795 |
-
results[date_str] = {
|
| 796 |
-
"max": max(levels),
|
| 797 |
-
"min": min(levels),
|
| 798 |
-
"avg": sum(levels) / len(levels),
|
| 799 |
-
"range": max(levels) - min(levels)
|
| 800 |
-
}
|
| 801 |
-
|
| 802 |
-
if len(results) == 2:
|
| 803 |
-
# 차이 계산
|
| 804 |
-
diff = {
|
| 805 |
-
"max_diff": results[date1]["max"] - results[date2]["max"],
|
| 806 |
-
"min_diff": results[date1]["min"] - results[date2]["min"],
|
| 807 |
-
"avg_diff": results[date1]["avg"] - results[date2]["avg"],
|
| 808 |
-
"range_diff": results[date1]["range"] - results[date2]["range"]
|
| 809 |
-
}
|
| 810 |
-
|
| 811 |
-
return {
|
| 812 |
-
"station_id": station_id,
|
| 813 |
-
"date1": {**{"date": date1}, **results[date1]},
|
| 814 |
-
"date2": {**{"date": date2}, **results[date2]},
|
| 815 |
-
"difference": diff
|
| 816 |
-
}
|
| 817 |
-
|
| 818 |
-
return {"error": "Data not available for both dates"}
|
| 819 |
-
|
| 820 |
-
except Exception as e:
|
| 821 |
-
return {"error": f"Error: {str(e)}"}
|
| 822 |
-
|
| 823 |
-
def api_get_monthly_summary(station_id, year, month):
|
| 824 |
-
"""월간 조위 요약 통계"""
|
| 825 |
-
supabase = get_supabase_client()
|
| 826 |
-
if not supabase:
|
| 827 |
-
return {"error": "Database connection failed"}
|
| 828 |
-
|
| 829 |
-
try:
|
| 830 |
-
# 월 시작/종료 날짜
|
| 831 |
-
start_date = datetime(year, month, 1)
|
| 832 |
-
if month == 12:
|
| 833 |
-
end_date = datetime(year + 1, 1, 1)
|
| 834 |
-
else:
|
| 835 |
-
end_date = datetime(year, month + 1, 1)
|
| 836 |
-
|
| 837 |
-
result = supabase.table('tide_predictions')\
|
| 838 |
-
.select('predicted_at, final_tide_level')\
|
| 839 |
-
.eq('station_id', station_id)\
|
| 840 |
-
.gte('predicted_at', start_date.isoformat())\
|
| 841 |
-
.lt('predicted_at', end_date.isoformat())\
|
| 842 |
-
.execute()
|
| 843 |
-
|
| 844 |
-
if not result.data:
|
| 845 |
-
return {"error": "No data for this month"}
|
| 846 |
-
|
| 847 |
-
# 일별 통계 계산
|
| 848 |
-
daily_stats = {}
|
| 849 |
-
for item in result.data:
|
| 850 |
-
date = item['predicted_at'][:10] # YYYY-MM-DD
|
| 851 |
-
if date not in daily_stats:
|
| 852 |
-
daily_stats[date] = []
|
| 853 |
-
daily_stats[date].append(item['final_tide_level'])
|
| 854 |
-
|
| 855 |
-
# 월간 통계
|
| 856 |
-
all_levels = [d['final_tide_level'] for d in result.data]
|
| 857 |
-
monthly_max = max(all_levels)
|
| 858 |
-
monthly_min = min(all_levels)
|
| 859 |
-
|
| 860 |
-
# 가장 높았던 날과 낮았던 날 찾기
|
| 861 |
-
highest_day = None
|
| 862 |
-
lowest_day = None
|
| 863 |
-
highest_value = 0
|
| 864 |
-
lowest_value = 9999
|
| 865 |
-
|
| 866 |
-
for date, levels in daily_stats.items():
|
| 867 |
-
day_max = max(levels)
|
| 868 |
-
day_min = min(levels)
|
| 869 |
-
|
| 870 |
-
if day_max > highest_value:
|
| 871 |
-
highest_value = day_max
|
| 872 |
-
highest_day = date
|
| 873 |
-
|
| 874 |
-
if day_min < lowest_value:
|
| 875 |
-
lowest_value = day_min
|
| 876 |
-
lowest_day = date
|
| 877 |
-
|
| 878 |
-
return {
|
| 879 |
-
"station_id": station_id,
|
| 880 |
-
"year": year,
|
| 881 |
-
"month": month,
|
| 882 |
-
"statistics": {
|
| 883 |
-
"monthly_max": monthly_max,
|
| 884 |
-
"monthly_min": monthly_min,
|
| 885 |
-
"monthly_avg": sum(all_levels) / len(all_levels),
|
| 886 |
-
"monthly_range": monthly_max - monthly_min,
|
| 887 |
-
"total_observations": len(result.data),
|
| 888 |
-
"days_with_data": len(daily_stats)
|
| 889 |
-
},
|
| 890 |
-
"extreme_days": {
|
| 891 |
-
"highest_tide_day": highest_day,
|
| 892 |
-
"highest_tide_value": highest_value,
|
| 893 |
-
"lowest_tide_day": lowest_day,
|
| 894 |
-
"lowest_tide_value": lowest_value
|
| 895 |
-
}
|
| 896 |
-
}
|
| 897 |
-
|
| 898 |
-
except Exception as e:
|
| 899 |
-
return {"error": f"Error: {str(e)}"}
|
| 900 |
|
| 901 |
-
#
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
client = get_supabase_client()
|
| 907 |
-
supabase_status = "🟢 연결됨" if client else "🔴 연결 안됨 (환경변수 확인 필요)"
|
| 908 |
-
gemini_status = "🟢 연결됨" if GEMINI_AVAILABLE and GEMINI_API_KEY else "🔴 연결 안됨 (환경변수 확인 필요)"
|
| 909 |
-
gr.Markdown(f"**Supabase 상태**: {supabase_status} | **Gemini 상태**: {gemini_status}")
|
| 910 |
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
with gr.TabItem("🔮 통합 조위 예측"):
|
| 914 |
-
gr.Markdown("""
|
| 915 |
-
### 🌟 새로운 기능
|
| 916 |
-
- **잔차 예측**: TimeXer 모델로 기상 영향 예측
|
| 917 |
-
- **조화 예측**: MATLAB 조화분석으로 천체 영향 예측 (Supabase 연결 시)
|
| 918 |
-
- **최종 조위**: 잔차 + 조화 = 완전한 조위 예측
|
| 919 |
-
- **자동 저장**: 예측 결과를 데이터베이스에 자동 저장
|
| 920 |
-
""")
|
| 921 |
-
|
| 922 |
-
with gr.Row():
|
| 923 |
-
with gr.Column(scale=1):
|
| 924 |
-
station_dropdown1 = gr.Dropdown(
|
| 925 |
-
choices=[(f"{STATION_NAMES[s]} ({s})", s) for s in STATIONS],
|
| 926 |
-
label="관측소 선택",
|
| 927 |
-
value=STATIONS[0]
|
| 928 |
-
)
|
| 929 |
-
with gr.Column(scale=2):
|
| 930 |
-
file_input1 = gr.File(
|
| 931 |
-
label="입력 데이터 (.csv 파일)",
|
| 932 |
-
file_types=[".csv"],
|
| 933 |
-
file_count="single"
|
| 934 |
-
)
|
| 935 |
-
|
| 936 |
-
submit_btn1 = gr.Button("🚀 예측 실행", variant="primary", size="lg")
|
| 937 |
-
|
| 938 |
-
with gr.Row():
|
| 939 |
-
with gr.Column(scale=2):
|
| 940 |
-
plot_output1 = gr.Plot(label="예측 결과 시각화")
|
| 941 |
-
with gr.Column(scale=1):
|
| 942 |
-
table_output1 = gr.Dataframe(
|
| 943 |
-
label="예측 결과 테이블"
|
| 944 |
-
)
|
| 945 |
-
|
| 946 |
-
text_output1 = gr.Textbox(
|
| 947 |
-
label="실행 로그",
|
| 948 |
-
lines=5,
|
| 949 |
-
show_copy_button=True
|
| 950 |
-
)
|
| 951 |
-
|
| 952 |
-
submit_btn1.click(
|
| 953 |
-
fn=single_prediction,
|
| 954 |
-
inputs=[station_dropdown1, file_input1],
|
| 955 |
-
outputs=[plot_output1, table_output1, text_output1]
|
| 956 |
-
)
|
| 957 |
-
|
| 958 |
-
# 2번 탭: AI 조위 챗봇
|
| 959 |
-
with gr.TabItem("💬 AI 조위 챗봇"):
|
| 960 |
-
gr.Markdown("""
|
| 961 |
-
### 💡 AI 조위 정보 도우미 (Gemini 기반)
|
| 962 |
-
데이터베이스에 저장된 예측 정보를 바탕으로 AI가 원하는 정보를 찾아옵니다.
|
| 963 |
-
|
| 964 |
-
**질문 예시:**
|
| 965 |
-
- "현재 인천 조위 알려줘"
|
| 966 |
-
- "내일 오후 3시 평택 조위는?"
|
| 967 |
-
- "오늘 만조 시간 알려줘"
|
| 968 |
-
""")
|
| 969 |
-
|
| 970 |
-
gr.ChatInterface(
|
| 971 |
-
fn=process_chatbot_query_with_llm,
|
| 972 |
-
title="",
|
| 973 |
-
examples=[
|
| 974 |
-
"현재 인천 조위 알려줘",
|
| 975 |
-
"내일 오후 3시 평택 조위는?",
|
| 976 |
-
"오늘 만조 시간 알려줘"
|
| 977 |
-
]
|
| 978 |
-
)
|
| 979 |
-
|
| 980 |
-
# 3번 탭: API
|
| 981 |
-
with gr.TabItem("🔌 API"):
|
| 982 |
-
gr.Markdown("""
|
| 983 |
-
## API 엔드포인트
|
| 984 |
-
|
| 985 |
-
이 앱은 자동으로 API를 제공합니다:
|
| 986 |
-
|
| 987 |
-
### 사용 가능한 엔드포인트:
|
| 988 |
-
- `/api/current_tide` - 현재 조위
|
| 989 |
-
- `/api/extremes` - 만조/간조 시간
|
| 990 |
-
|
| 991 |
-
### 사용 예시:
|
| 992 |
-
```python
|
| 993 |
-
from gradio_client import Client
|
| 994 |
-
|
| 995 |
-
client = Client("https://your-space.hf.space/")
|
| 996 |
-
result = client.predict(
|
| 997 |
-
"DT_0001",
|
| 998 |
-
api_name="/current_tide"
|
| 999 |
-
)
|
| 1000 |
-
print(result)
|
| 1001 |
-
```
|
| 1002 |
-
""")
|
| 1003 |
-
|
| 1004 |
-
# API 테스트 인터페이스
|
| 1005 |
-
with gr.Row():
|
| 1006 |
-
with gr.Column():
|
| 1007 |
-
gr.Markdown("### 현재 조위 API 테스트")
|
| 1008 |
-
api_station_input = gr.Textbox(
|
| 1009 |
-
label="관측소 ID",
|
| 1010 |
-
value="DT_0001"
|
| 1011 |
-
)
|
| 1012 |
-
api_current_btn = gr.Button("조회")
|
| 1013 |
-
api_current_output = gr.JSON(label="결과")
|
| 1014 |
-
|
| 1015 |
-
api_current_btn.click(
|
| 1016 |
-
fn=api_get_current_tide,
|
| 1017 |
-
inputs=api_station_input,
|
| 1018 |
-
outputs=api_current_output,
|
| 1019 |
-
api_name="current_tide"
|
| 1020 |
-
)
|
| 1021 |
-
|
| 1022 |
-
with gr.Column():
|
| 1023 |
-
gr.Markdown("### 만조/간조 API 테스트")
|
| 1024 |
-
api_extreme_station = gr.Textbox(
|
| 1025 |
-
label="관측소 ID",
|
| 1026 |
-
value="DT_0001"
|
| 1027 |
-
)
|
| 1028 |
-
api_extreme_hours = gr.Number(
|
| 1029 |
-
label="시간",
|
| 1030 |
-
value=24
|
| 1031 |
-
)
|
| 1032 |
-
api_extreme_btn = gr.Button("조회")
|
| 1033 |
-
api_extreme_output = gr.JSON(label="결과")
|
| 1034 |
-
|
| 1035 |
-
api_extreme_btn.click(
|
| 1036 |
-
fn=api_get_extremes,
|
| 1037 |
-
inputs=[api_extreme_station, api_extreme_hours],
|
| 1038 |
-
outputs=api_extreme_output,
|
| 1039 |
-
api_name="extremes"
|
| 1040 |
-
)
|
| 1041 |
-
with gr.TabItem("📜 과거 데이터"):
|
| 1042 |
-
gr.Markdown("""
|
| 1043 |
-
### 과거 조위 데이터 조회
|
| 1044 |
-
특정 날짜의 조위 정보를 확인할 수 있습니다.
|
| 1045 |
-
""")
|
| 1046 |
-
|
| 1047 |
-
with gr.Row():
|
| 1048 |
-
with gr.Column():
|
| 1049 |
-
hist_station = gr.Dropdown(
|
| 1050 |
-
choices=[(f"{STATION_NAMES[s]} ({s})", s) for s in STATIONS],
|
| 1051 |
-
label="관측소 선택",
|
| 1052 |
-
value=STATIONS[0]
|
| 1053 |
-
)
|
| 1054 |
-
hist_date = gr.Textbox(
|
| 1055 |
-
label="날짜 (YYYY-MM-DD)",
|
| 1056 |
-
value=datetime.now().strftime("%Y-%m-%d")
|
| 1057 |
-
)
|
| 1058 |
-
hist_hours = gr.Number(
|
| 1059 |
-
label="조회 시간 (시간)",
|
| 1060 |
-
value=24,
|
| 1061 |
-
minimum=1,
|
| 1062 |
-
maximum=168
|
| 1063 |
-
)
|
| 1064 |
-
|
| 1065 |
-
hist_btn = gr.Button("조회", variant="primary")
|
| 1066 |
-
|
| 1067 |
-
with gr.Column():
|
| 1068 |
-
hist_output = gr.JSON(label="조회 결과")
|
| 1069 |
-
|
| 1070 |
-
hist_btn.click(
|
| 1071 |
-
fn=api_get_historical_tide,
|
| 1072 |
-
inputs=[hist_station, hist_date, hist_hours],
|
| 1073 |
-
outputs=hist_output,
|
| 1074 |
-
api_name="historical_tide"
|
| 1075 |
-
)
|
| 1076 |
-
|
| 1077 |
-
# 과거 만조/간조
|
| 1078 |
-
with gr.Row():
|
| 1079 |
-
with gr.Column():
|
| 1080 |
-
gr.Markdown("### 과거 만조/간조 정보")
|
| 1081 |
-
ext_station = gr.Dropdown(
|
| 1082 |
-
choices=[(f"{STATION_NAMES[s]} ({s})", s) for s in STATIONS],
|
| 1083 |
-
label="관측소",
|
| 1084 |
-
value=STATIONS[0]
|
| 1085 |
-
)
|
| 1086 |
-
ext_date = gr.Textbox(
|
| 1087 |
-
label="날짜 (YYYY-MM-DD)",
|
| 1088 |
-
value=(datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
|
| 1089 |
-
)
|
| 1090 |
-
ext_btn = gr.Button("만조/간조 조회")
|
| 1091 |
-
ext_output = gr.JSON(label="만조/간조 정보")
|
| 1092 |
-
|
| 1093 |
-
with gr.Column():
|
| 1094 |
-
gr.Markdown("### 날짜 비교")
|
| 1095 |
-
comp_station = gr.Dropdown(
|
| 1096 |
-
choices=[(f"{STATION_NAMES[s]} ({s})", s) for s in STATIONS],
|
| 1097 |
-
label="관측소",
|
| 1098 |
-
value=STATIONS[0]
|
| 1099 |
-
)
|
| 1100 |
-
comp_date1 = gr.Textbox(
|
| 1101 |
-
label="날짜 1",
|
| 1102 |
-
value=datetime.now().strftime("%Y-%m-%d")
|
| 1103 |
-
)
|
| 1104 |
-
comp_date2 = gr.Textbox(
|
| 1105 |
-
label="날짜 2",
|
| 1106 |
-
value=(datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
|
| 1107 |
-
)
|
| 1108 |
-
comp_btn = gr.Button("비교")
|
| 1109 |
-
comp_output = gr.JSON(label="비교 결과")
|
| 1110 |
-
|
| 1111 |
-
ext_btn.click(
|
| 1112 |
-
fn=api_get_historical_extremes,
|
| 1113 |
-
inputs=[ext_station, ext_date],
|
| 1114 |
-
outputs=ext_output,
|
| 1115 |
-
api_name="historical_extremes"
|
| 1116 |
-
)
|
| 1117 |
-
|
| 1118 |
-
comp_btn.click(
|
| 1119 |
-
fn=api_compare_dates,
|
| 1120 |
-
inputs=[comp_station, comp_date1, comp_date2],
|
| 1121 |
-
outputs=comp_output,
|
| 1122 |
-
api_name="compare_dates"
|
| 1123 |
-
)
|
| 1124 |
-
|
| 1125 |
-
# 월간 요약
|
| 1126 |
-
gr.Markdown("### 월간 요약 통계")
|
| 1127 |
-
with gr.Row():
|
| 1128 |
-
month_station = gr.Dropdown(
|
| 1129 |
-
choices=[(f"{STATION_NAMES[s]} ({s})", s) for s in STATIONS],
|
| 1130 |
-
label="관측소",
|
| 1131 |
-
value=STATIONS[0]
|
| 1132 |
-
)
|
| 1133 |
-
month_year = gr.Number(
|
| 1134 |
-
label="년도",
|
| 1135 |
-
value=datetime.now().year,
|
| 1136 |
-
precision=0
|
| 1137 |
-
)
|
| 1138 |
-
month_month = gr.Number(
|
| 1139 |
-
label="월",
|
| 1140 |
-
value=datetime.now().month,
|
| 1141 |
-
minimum=1,
|
| 1142 |
-
maximum=12,
|
| 1143 |
-
precision=0
|
| 1144 |
-
)
|
| 1145 |
-
month_btn = gr.Button("월간 통계 조회")
|
| 1146 |
-
|
| 1147 |
-
month_output = gr.JSON(label="월간 통계")
|
| 1148 |
-
|
| 1149 |
-
month_btn.click(
|
| 1150 |
-
fn=api_get_monthly_summary,
|
| 1151 |
-
inputs=[month_station, month_year, month_month],
|
| 1152 |
-
outputs=month_output,
|
| 1153 |
-
api_name="monthly_summary"
|
| 1154 |
-
)
|
| 1155 |
-
|
| 1156 |
-
if __name__ == "__main__":
|
| 1157 |
-
demo.launch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import warnings
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
|
| 4 |
+
# Load environment variables from .env file
|
| 5 |
+
load_dotenv()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
+
# Import handlers and UI creator from modules
|
| 8 |
+
from prediction import single_prediction
|
| 9 |
+
from chatbot import process_chatbot_query_with_llm
|
| 10 |
+
from ui import create_ui
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
+
if __name__ == "__main__":
|
| 13 |
+
# Suppress warnings for a cleaner output
|
| 14 |
+
warnings.filterwarnings('ignore')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
+
# Create the Gradio UI by passing the handlers to the UI generator
|
| 17 |
+
demo = create_ui(
|
| 18 |
+
prediction_handler=single_prediction,
|
| 19 |
+
chatbot_handler=process_chatbot_query_with_llm
|
| 20 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
# Launch the application
|
| 23 |
+
demo.launch(share=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
chatbot.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
import pytz
|
| 5 |
+
import traceback
|
| 6 |
+
from dateutil import parser as date_parser
|
| 7 |
+
|
| 8 |
+
# Local imports
|
| 9 |
+
from supabase_utils import get_supabase_client
|
| 10 |
+
|
| 11 |
+
# Attempt to import Gemini
|
| 12 |
+
try:
|
| 13 |
+
import google.generativeai as genai
|
| 14 |
+
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 15 |
+
if GEMINI_API_KEY:
|
| 16 |
+
genai.configure(api_key=GEMINI_API_KEY)
|
| 17 |
+
GEMINI_AVAILABLE = True
|
| 18 |
+
else:
|
| 19 |
+
GEMINI_AVAILABLE = False
|
| 20 |
+
except ImportError:
|
| 21 |
+
GEMINI_AVAILABLE = False
|
| 22 |
+
|
| 23 |
+
# Station names (dependency for retrieve_context_from_db)
|
| 24 |
+
STATION_NAMES = {
|
| 25 |
+
"DT_0001": "인천", "DT_0065": "평택", "DT_0008": "안산", "DT_0067": "대산",
|
| 26 |
+
"DT_0043": "보령", "DT_0002": "군산", "DT_0050": "목포", "DT_0017": "제주",
|
| 27 |
+
"DT_0052": "여수", "DT_0025": "마산", "DT_0051": "부산", "DT_0037": "포항",
|
| 28 |
+
"DT_0068": "위도"
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
def parse_intent_with_llm(message: str) -> dict:
|
| 32 |
+
"""LLM을 사용해 사용자 질문에서 의도를 분석하고 JSON으로 반환"""
|
| 33 |
+
if not GEMINI_AVAILABLE:
|
| 34 |
+
return {"error": "Gemini API를 사용할 수 없습니다. API 키를 확인하세요."}
|
| 35 |
+
|
| 36 |
+
prompt = f"""
|
| 37 |
+
당신은 사용자의 자연어 질문을 분석하여 JSON 객체로 변환하는 전문가입니다.
|
| 38 |
+
질문에서 '관측소 이름', '원하는 정보', '시작 시간', '종료 시간'을 추출해주세요.
|
| 39 |
+
현재 시간은 {datetime.now(pytz.timezone('Asia/Seoul')).strftime('%Y-%m-%d %H:%M:%S')} KST 입니다.
|
| 40 |
+
|
| 41 |
+
- '원하는 정보'는 '특정 시간 조위' 또는 '구간 조위' 중 하나여야 합니다.
|
| 42 |
+
- '시작 시간'과 '종료 시간'은 'YYYY-MM-DD HH:MM:SS' 형식으로 변환해주세요.
|
| 43 |
+
- 단일 시간이면 시작과 종료 시간을 동일하게 설정하고, 구간이면 그에 맞게 설정하세요.
|
| 44 |
+
- 관측소 이름이 없으면 '인천'을 기본값으로 사용하세요.
|
| 45 |
+
|
| 46 |
+
[사용자 질문]: {message}
|
| 47 |
+
[JSON 출력]:
|
| 48 |
+
"""
|
| 49 |
+
try:
|
| 50 |
+
model = genai.GenerativeModel('gemini-1.5-flash', generation_config={"response_mime_type": "application/json"})
|
| 51 |
+
response = model.generate_content(prompt)
|
| 52 |
+
return json.loads(response.text)
|
| 53 |
+
except Exception as e:
|
| 54 |
+
return {"error": f"LLM 의도 분석 중 오류 발생: {e}"}
|
| 55 |
+
|
| 56 |
+
def retrieve_context_from_db(intent: dict) -> str:
|
| 57 |
+
"""분석된 의도를 바탕으로 데이터베이스에서 정보 검색"""
|
| 58 |
+
supabase = get_supabase_client()
|
| 59 |
+
if not supabase:
|
| 60 |
+
return "데이터베이스에 연결할 수 없습니다."
|
| 61 |
+
|
| 62 |
+
if "error" in intent:
|
| 63 |
+
return f"의도 분석에 실패했습니다: {intent['error']}"
|
| 64 |
+
|
| 65 |
+
station_name = intent.get("관측소 이름", "인천")
|
| 66 |
+
start_time_str = intent.get("시작 시간")
|
| 67 |
+
end_time_str = intent.get("종료 시간")
|
| 68 |
+
|
| 69 |
+
station_id = next((sid for sid, name in STATION_NAMES.items() if name == station_name), "DT_0001")
|
| 70 |
+
|
| 71 |
+
if not start_time_str or not end_time_str:
|
| 72 |
+
return "질문에서 시간 정보를 찾을 수 없습니다."
|
| 73 |
+
|
| 74 |
+
try:
|
| 75 |
+
start_time = date_parser.parse(start_time_str)
|
| 76 |
+
end_time = date_parser.parse(end_time_str)
|
| 77 |
+
|
| 78 |
+
start_query_str = start_time.strftime('%Y-%m-%d %H:%M:%S')
|
| 79 |
+
end_query_str = end_time.strftime('%Y-%m-%d %H:%M:%S')
|
| 80 |
+
|
| 81 |
+
result = supabase.table('tide_predictions')\
|
| 82 |
+
.select('*')\
|
| 83 |
+
.eq('station_id', station_id)\
|
| 84 |
+
.gte('predicted_at', start_query_str)\
|
| 85 |
+
.lte('predicted_at', end_query_str)\
|
| 86 |
+
.order('predicted_at')\
|
| 87 |
+
.execute()
|
| 88 |
+
|
| 89 |
+
if result.data:
|
| 90 |
+
info_text = f"'{station_name}'의 '{start_time_str}'부터 '{end_time_str}'까지 조위 정보입니다.\n\n"
|
| 91 |
+
|
| 92 |
+
if len(result.data) > 10:
|
| 93 |
+
levels = [d['final_tide_level'] for d in result.data]
|
| 94 |
+
max_level = max(levels)
|
| 95 |
+
min_level = min(levels)
|
| 96 |
+
info_text += f"- 최고 조위: {max_level:.1f}cm\n- 최저 조위: {min_level:.1f}cm"
|
| 97 |
+
else:
|
| 98 |
+
for d in result.data:
|
| 99 |
+
time_kst = date_parser.parse(d['predicted_at']).strftime('%H:%M')
|
| 100 |
+
info_text += f"- {time_kst}: 최종 조위 {d['final_tide_level']:.1f}cm (잔차 {d['predicted_residual']:.1f}cm)\n"
|
| 101 |
+
return info_text
|
| 102 |
+
else:
|
| 103 |
+
return "해당 기간의 예측 데이터를 찾을 수 없습니다. '통합 조위 예측' 탭에서 먼저 예측을 실행해주세요."
|
| 104 |
+
|
| 105 |
+
except Exception as e:
|
| 106 |
+
return f"데이터 검색 중 오류 발생: {traceback.format_exc()}"
|
| 107 |
+
|
| 108 |
+
def process_chatbot_query_with_llm(message: str, history: list) -> str:
|
| 109 |
+
"""최종 RAG 파이프라인"""
|
| 110 |
+
if not GEMINI_AVAILABLE:
|
| 111 |
+
return "Gemini API를 사용할 수 없습니다. API 키를 확인하세요."
|
| 112 |
+
|
| 113 |
+
intent = parse_intent_with_llm(message)
|
| 114 |
+
retrieved_data = retrieve__context_from_db(intent)
|
| 115 |
+
|
| 116 |
+
prompt = f"""당신은 친절한 해양 ��위 정보 전문가입니다. 주어진 [검색된 데이터]를 바탕으로 사용자의 [질문]에 대해 자연스러운 문장으로 답변해주세요.
|
| 117 |
+
[검색된 데이터]: {retrieved_data}
|
| 118 |
+
[사용자 질문]: {message}
|
| 119 |
+
[답변]:"""
|
| 120 |
+
|
| 121 |
+
try:
|
| 122 |
+
model = genai.GenerativeModel('gemini-1.5-flash')
|
| 123 |
+
response = model.generate_content(prompt)
|
| 124 |
+
return response.text
|
| 125 |
+
except Exception as e:
|
| 126 |
+
return f"Gemini 답변 생성 중 오류가 발생했습니다: {e}"
|
chatbot_utils.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import traceback
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
import pytz
|
| 6 |
+
from dateutil import parser as date_parser
|
| 7 |
+
|
| 8 |
+
from api_utils import api_get_extremes, api_get_current_tide
|
| 9 |
+
from config import GEMINI_API_KEY, STATION_NAMES
|
| 10 |
+
from supabase_utils import get_supabase_client
|
| 11 |
+
|
| 12 |
+
# Gemini 연동 확인
|
| 13 |
+
try:
|
| 14 |
+
import google.generativeai as genai
|
| 15 |
+
GEMINI_AVAILABLE = True
|
| 16 |
+
except ImportError:
|
| 17 |
+
GEMINI_AVAILABLE = False
|
| 18 |
+
print("Gemini (google-generativeai) 패키지가 설치되지 않았습니다.")
|
| 19 |
+
|
| 20 |
+
def parse_intent_with_llm(message: str) -> dict:
|
| 21 |
+
"""LLM을 사용해 사용자 질문에서 의도를 분석하고 JSON으로 반환"""
|
| 22 |
+
if not GEMINI_API_KEY:
|
| 23 |
+
return {"error": "Gemini API 키가 설정되지 않았습니다."}
|
| 24 |
+
|
| 25 |
+
prompt = f"""
|
| 26 |
+
당신은 사용자의 자연어 질문을 분석하여 JSON 객체로 변환하는 전문가입니다.
|
| 27 |
+
질문에서 '관측소 이름', '원하는 정보', '시작 시간', '종료 시간'을 추출해주세요.
|
| 28 |
+
현재 시간은 {datetime.now(pytz.timezone('Asia/Seoul')).strftime('%Y-%m-%d %H:%M:%S')} KST 입니다.
|
| 29 |
+
|
| 30 |
+
- '원하는 정보'는 '특정 시간 조위' 또는 '구간 조위' 중 하나여야 합니다.
|
| 31 |
+
- '시작 시간'과 '종료 시간'은 'YYYY-MM-DD HH:MM:SS' 형식으로 변환해주세요.
|
| 32 |
+
- 단일 시간이면 시작과 종료 시간을 동일하게 설정하고, 구간이면 그에 맞게 설정하세요.
|
| 33 |
+
- 관측소 이름이 없으면 '인천'을 기본값으로 사용하세요.
|
| 34 |
+
|
| 35 |
+
[사용자 질문]: {message}
|
| 36 |
+
[JSON 출력]:
|
| 37 |
+
"""
|
| 38 |
+
try:
|
| 39 |
+
genai.configure(api_key=GEMINI_API_KEY)
|
| 40 |
+
model = genai.GenerativeModel('gemini-1.5-flash', generation_config={"response_mime_type": "application/json"})
|
| 41 |
+
response = model.generate_content(prompt)
|
| 42 |
+
return json.loads(response.text)
|
| 43 |
+
except Exception as e:
|
| 44 |
+
return {"error": f"LLM 의도 분석 중 오류 발생: {e}"}
|
| 45 |
+
|
| 46 |
+
def retrieve_context_from_db(intent: dict) -> str:
|
| 47 |
+
"""분석된 의도를 바탕으로 데이터베이스에서 정보 검색"""
|
| 48 |
+
supabase = get_supabase_client()
|
| 49 |
+
if not supabase:
|
| 50 |
+
return "데이터베이스에 연결할 수 없습니다."
|
| 51 |
+
|
| 52 |
+
if "error" in intent:
|
| 53 |
+
return f"의도 분석에 실패했습니다: {intent['error']}"
|
| 54 |
+
|
| 55 |
+
station_name = intent.get("관측소 이름", "인천")
|
| 56 |
+
start_time_str = intent.get("시작 시간")
|
| 57 |
+
end_time_str = intent.get("종료 시간")
|
| 58 |
+
|
| 59 |
+
station_id = next((sid for sid, name in STATION_NAMES.items() if name == station_name), "DT_0001")
|
| 60 |
+
|
| 61 |
+
if not start_time_str or not end_time_str:
|
| 62 |
+
return "질문에서 시간 정보를 찾을 수 없습니다."
|
| 63 |
+
|
| 64 |
+
try:
|
| 65 |
+
start_time = date_parser.parse(start_time_str)
|
| 66 |
+
end_time = date_parser.parse(end_time_str)
|
| 67 |
+
|
| 68 |
+
start_query_str = start_time.strftime('%Y-%m-%d %H:%M:%S')
|
| 69 |
+
end_query_str = end_time.strftime('%Y-%m-%d %H:%M:%S')
|
| 70 |
+
|
| 71 |
+
result = supabase.table('tide_predictions')\
|
| 72 |
+
.select('*')\
|
| 73 |
+
.eq('station_id', station_id)\
|
| 74 |
+
.gte('predicted_at', start_query_str)\
|
| 75 |
+
.lte('predicted_at', end_query_str)\
|
| 76 |
+
.order('predicted_at')\
|
| 77 |
+
.execute()
|
| 78 |
+
|
| 79 |
+
if result.data:
|
| 80 |
+
info_text = f"'{station_name}'의 '{start_time_str}'부터 '{end_time_str}'까지 조위 정보입니다.\n\n"
|
| 81 |
+
|
| 82 |
+
if len(result.data) > 10:
|
| 83 |
+
levels = [d['final_tide_level'] for d in result.data]
|
| 84 |
+
max_level = max(levels)
|
| 85 |
+
min_level = min(levels)
|
| 86 |
+
info_text += f"- 최고 조위: {max_level:.1f}cm\n- 최저 조위: {min_level:.1f}cm"
|
| 87 |
+
else:
|
| 88 |
+
for d in result.data:
|
| 89 |
+
time_kst = date_parser.parse(d['predicted_at']).strftime('%H:%M')
|
| 90 |
+
info_text += f"- {time_kst}: 최종 조위 {d['final_tide_level']:.1f}cm (잔차 {d['predicted_residual']:.1f}cm)\n"
|
| 91 |
+
return info_text
|
| 92 |
+
else:
|
| 93 |
+
return "해당 기간의 예측 데이터를 찾을 수 없습니다. '통합 조위 예측' 탭에서 먼저 예측을 실행해주세요."
|
| 94 |
+
|
| 95 |
+
except Exception as e:
|
| 96 |
+
return f"데이터 검색 중 오류 발생: {traceback.format_exc()}"
|
| 97 |
+
|
| 98 |
+
def process_chatbot_query_with_llm(message: str, history: list) -> str:
|
| 99 |
+
"""최종 RAG 파이프라인"""
|
| 100 |
+
intent = parse_intent_with_llm(message)
|
| 101 |
+
retrieved_data = retrieve_context_from_db(intent)
|
| 102 |
+
|
| 103 |
+
prompt = f"""당신은 친절한 해양 조위 정보 전문가입니다. 주어진 [검색된 데이터]를 바탕으로 사용자의 [질문]에 대해 자연스러운 문장으로 답변해주세요.
|
| 104 |
+
[검색된 데이터]: {retrieved_data}
|
| 105 |
+
[사용자 질문]: {message}
|
| 106 |
+
[답변]:"""
|
| 107 |
+
|
| 108 |
+
try:
|
| 109 |
+
genai.configure(api_key=GEMINI_API_KEY)
|
| 110 |
+
model = genai.GenerativeModel('gemini-1.5-flash')
|
| 111 |
+
response = model.generate_content(prompt)
|
| 112 |
+
return response.text
|
| 113 |
+
except Exception as e:
|
| 114 |
+
return f"Gemini 답변 생성 중 오류가 발생했습니다: {e}"
|
config.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
# --- 0. 설정 ---
|
| 4 |
+
SUPABASE_URL = os.environ.get("SUPABASE_URL")
|
| 5 |
+
SUPABASE_KEY = os.environ.get("SUPABASE_KEY")
|
| 6 |
+
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
|
| 7 |
+
|
| 8 |
+
STATIONS = [
|
| 9 |
+
"DT_0001", "DT_0065", "DT_0008", "DT_0067", "DT_0043", "DT_0002",
|
| 10 |
+
"DT_0050", "DT_0017", "DT_0052", "DT_0025", "DT_0051", "DT_0037",
|
| 11 |
+
"DT_0024", "DT_0018", "DT_0068", "DT_0003", "DT_0066"
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
STATION_NAMES = {
|
| 15 |
+
"DT_0001": "인천", "DT_0002": "평택", "DT_0003": "영광", "DT_0008": "안산",
|
| 16 |
+
"DT_0017": "대산", "DT_0018": "군산", "DT_0024": "장항", "DT_0025": "보령",
|
| 17 |
+
"DT_0037": "어청도", "DT_0043": "영흥도", "DT_0050": "태안", "DT_0051": "서천마량",
|
| 18 |
+
"DT_0052": "인천송도", "DT_0065": "덕적도", "DT_0066": "향화도", "DT_0067": "안흥",
|
| 19 |
+
"DT_0068": "위도"
|
| 20 |
+
}
|
prediction.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import subprocess
|
| 3 |
+
import traceback
|
| 4 |
+
from datetime import datetime, timedelta
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import plotly.graph_objects as go
|
| 10 |
+
import pytz
|
| 11 |
+
|
| 12 |
+
from config import STATION_NAMES
|
| 13 |
+
from supabase_utils import (
|
| 14 |
+
get_harmonic_predictions, save_predictions_to_supabase, get_supabase_client
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
def get_common_args(station_id):
|
| 18 |
+
return [
|
| 19 |
+
"--model", "TimeXer", "--features", "MS", "--seq_len", "144", "--pred_len", "72",
|
| 20 |
+
"--label_len", "96", "--enc_in", "5", "--dec_in", "5", "--c_out", "1",
|
| 21 |
+
"--d_model", "256", "--d_ff", "512", "--n_heads", "8", "--e_layers", "1",
|
| 22 |
+
"--d_layers", "1", "--factor", "3", "--patch_len", "16", "--expand", "2", "--d_conv", "4"
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
def validate_csv_file(file_path, required_rows=144):
|
| 26 |
+
"""CSV 파일 유효성 검사"""
|
| 27 |
+
try:
|
| 28 |
+
df = pd.read_csv(file_path)
|
| 29 |
+
required_columns = ['date', 'air_pres', 'wind_dir', 'wind_speed', 'air_temp', 'residual']
|
| 30 |
+
missing_columns = [col for col in required_columns if col not in df.columns]
|
| 31 |
+
|
| 32 |
+
if missing_columns:
|
| 33 |
+
return False, f"필수 컬럼이 누락되었습니다: {missing_columns}"
|
| 34 |
+
|
| 35 |
+
if len(df) < required_rows:
|
| 36 |
+
return False, f"데이터가 부족합니다. 최소 {required_rows}행 필요, 현재 {len(df)}행"
|
| 37 |
+
|
| 38 |
+
return True, "파일이 유효합니다."
|
| 39 |
+
except Exception as e:
|
| 40 |
+
return False, f"파일 읽기 오류: {str(e)}"
|
| 41 |
+
|
| 42 |
+
def execute_inference_and_get_results(command):
|
| 43 |
+
"""inference 실행하고 결과 파일을 읽어서 반환"""
|
| 44 |
+
try:
|
| 45 |
+
print(f"실행 명령어: {' '.join(command)}")
|
| 46 |
+
result = subprocess.run(command, capture_output=True, text=True, timeout=300)
|
| 47 |
+
|
| 48 |
+
if result.returncode != 0:
|
| 49 |
+
error_message = (
|
| 50 |
+
f"실행 실패 (Exit Code: {result.returncode}):\n\n"
|
| 51 |
+
f"--- 에러 로그 ---\n{result.stderr}\n\n"
|
| 52 |
+
f"--- 일반 출력 ---\n{result.stdout}"
|
| 53 |
+
)
|
| 54 |
+
raise gr.Error(error_message)
|
| 55 |
+
|
| 56 |
+
return True, result.stdout
|
| 57 |
+
except subprocess.TimeoutExpired:
|
| 58 |
+
raise gr.Error("실행 시간이 초과되었습니다. (5분 제한)")
|
| 59 |
+
except Exception as e:
|
| 60 |
+
raise gr.Error(f"내부 오류: {str(e)}")
|
| 61 |
+
|
| 62 |
+
def calculate_final_tide(residual_predictions, station_id, last_time):
|
| 63 |
+
"""잔차 예측 + 조화 예측 = 최종 조위 계산"""
|
| 64 |
+
if isinstance(last_time, pd.Timestamp):
|
| 65 |
+
last_time = last_time.to_pydatetime()
|
| 66 |
+
|
| 67 |
+
kst = pytz.timezone('Asia/Seoul')
|
| 68 |
+
if last_time.tzinfo is None:
|
| 69 |
+
last_time = kst.localize(last_time)
|
| 70 |
+
|
| 71 |
+
start_time = last_time + timedelta(minutes=5)
|
| 72 |
+
end_time = last_time + timedelta(minutes=72*5)
|
| 73 |
+
|
| 74 |
+
harmonic_data = get_harmonic_predictions(station_id, start_time, end_time)
|
| 75 |
+
|
| 76 |
+
residual_flat = residual_predictions.flatten()
|
| 77 |
+
num_points = len(residual_flat)
|
| 78 |
+
|
| 79 |
+
if not harmonic_data:
|
| 80 |
+
print("조화 예측 데이터를 찾을 수 없습니다. 잔차 예측만 반환합니다.")
|
| 81 |
+
return {
|
| 82 |
+
'times': [last_time + timedelta(minutes=(i+1)*5) for i in range(num_points)],
|
| 83 |
+
'residual': residual_flat.tolist(),
|
| 84 |
+
'harmonic': [0.0] * num_points,
|
| 85 |
+
'final_tide': residual_flat.tolist()
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
final_results = {
|
| 89 |
+
'times': [],
|
| 90 |
+
'residual': [],
|
| 91 |
+
'harmonic': [],
|
| 92 |
+
'final_tide': []
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
harmonic_dict = {}
|
| 96 |
+
for h_data in harmonic_data:
|
| 97 |
+
h_time_str = h_data['predicted_at']
|
| 98 |
+
|
| 99 |
+
try:
|
| 100 |
+
if 'T' in h_time_str:
|
| 101 |
+
if h_time_str.endswith('Z'):
|
| 102 |
+
h_time = datetime.fromisoformat(h_time_str[:-1] + '+00:00')
|
| 103 |
+
elif '+' in h_time_str or '-' in h_time_str[-6:]:
|
| 104 |
+
h_time = datetime.fromisoformat(h_time_str)
|
| 105 |
+
else:
|
| 106 |
+
h_time = datetime.fromisoformat(h_time_str + '+00:00')
|
| 107 |
+
else:
|
| 108 |
+
from dateutil import parser
|
| 109 |
+
h_time = parser.parse(h_time_str)
|
| 110 |
+
|
| 111 |
+
if h_time.tzinfo is None:
|
| 112 |
+
h_time = pytz.UTC.localize(h_time)
|
| 113 |
+
h_time = h_time.astimezone(kst)
|
| 114 |
+
|
| 115 |
+
except Exception as e:
|
| 116 |
+
print(f"시간 파싱 오류: {h_time_str}, {e}")
|
| 117 |
+
continue
|
| 118 |
+
|
| 119 |
+
minutes = (h_time.minute // 5) * 5
|
| 120 |
+
h_time = h_time.replace(minute=minutes, second=0, microsecond=0)
|
| 121 |
+
harmonic_value = float(h_data['harmonic_level'])
|
| 122 |
+
harmonic_dict[h_time] = harmonic_value
|
| 123 |
+
|
| 124 |
+
for i, residual in enumerate(residual_flat):
|
| 125 |
+
pred_time = last_time + timedelta(minutes=(i+1)*5)
|
| 126 |
+
pred_time = pred_time.replace(second=0, microsecond=0)
|
| 127 |
+
|
| 128 |
+
harmonic_value = harmonic_dict.get(pred_time, 0.0)
|
| 129 |
+
|
| 130 |
+
if harmonic_value == 0.0 and harmonic_dict:
|
| 131 |
+
min_diff = float('inf')
|
| 132 |
+
for h_time, h_val in harmonic_dict.items():
|
| 133 |
+
diff = abs((h_time - pred_time).total_seconds())
|
| 134 |
+
if diff < min_diff and diff < 300:
|
| 135 |
+
min_diff = diff
|
| 136 |
+
harmonic_value = h_val
|
| 137 |
+
|
| 138 |
+
final_tide = float(residual) + harmonic_value
|
| 139 |
+
|
| 140 |
+
final_results['times'].append(pred_time)
|
| 141 |
+
final_results['residual'].append(float(residual))
|
| 142 |
+
final_results['harmonic'].append(harmonic_value)
|
| 143 |
+
final_results['final_tide'].append(final_tide)
|
| 144 |
+
|
| 145 |
+
return final_results
|
| 146 |
+
|
| 147 |
+
def create_enhanced_prediction_plot(prediction_results, input_data, station_name):
|
| 148 |
+
"""잔차 + 조화 + 최종 조위를 모두 표시하는 향상된 플롯"""
|
| 149 |
+
try:
|
| 150 |
+
input_df = pd.read_csv(input_data.name)
|
| 151 |
+
input_df['date'] = pd.to_datetime(input_df['date'])
|
| 152 |
+
|
| 153 |
+
recent_data = input_df.tail(24)
|
| 154 |
+
future_times = pd.to_datetime(prediction_results['times'])
|
| 155 |
+
|
| 156 |
+
fig = go.Figure()
|
| 157 |
+
|
| 158 |
+
fig.add_trace(go.Scatter(
|
| 159 |
+
x=recent_data['date'],
|
| 160 |
+
y=recent_data['residual'],
|
| 161 |
+
mode='lines+markers',
|
| 162 |
+
name='실제 잔차조위',
|
| 163 |
+
line=dict(color='blue', width=2),
|
| 164 |
+
marker=dict(size=4)
|
| 165 |
+
))
|
| 166 |
+
|
| 167 |
+
fig.add_trace(go.Scatter(
|
| 168 |
+
x=future_times,
|
| 169 |
+
y=prediction_results['residual'],
|
| 170 |
+
mode='lines+markers',
|
| 171 |
+
name='잔차 예측',
|
| 172 |
+
line=dict(color='red', width=2, dash='dash'),
|
| 173 |
+
marker=dict(size=3)
|
| 174 |
+
))
|
| 175 |
+
|
| 176 |
+
if any(h != 0 for h in prediction_results['harmonic']):
|
| 177 |
+
fig.add_trace(go.Scatter(
|
| 178 |
+
x=future_times,
|
| 179 |
+
y=prediction_results['harmonic'],
|
| 180 |
+
mode='lines',
|
| 181 |
+
name='조화 예측',
|
| 182 |
+
line=dict(color='orange', width=2)
|
| 183 |
+
))
|
| 184 |
+
|
| 185 |
+
fig.add_trace(go.Scatter(
|
| 186 |
+
x=future_times,
|
| 187 |
+
y=prediction_results['final_tide'],
|
| 188 |
+
mode='lines+markers',
|
| 189 |
+
name='최종 조위',
|
| 190 |
+
line=dict(color='green', width=3),
|
| 191 |
+
marker=dict(size=4)
|
| 192 |
+
))
|
| 193 |
+
|
| 194 |
+
last_time = recent_data['date'].iloc[-1]
|
| 195 |
+
|
| 196 |
+
fig.add_annotation(
|
| 197 |
+
x=last_time,
|
| 198 |
+
y=0,
|
| 199 |
+
text="← 과거 | 미래 →",
|
| 200 |
+
showarrow=False,
|
| 201 |
+
yref="paper",
|
| 202 |
+
yshift=10,
|
| 203 |
+
font=dict(size=12, color="gray")
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
fig.update_layout(
|
| 207 |
+
title=f'{station_name} 통합 조위 예측 결과',
|
| 208 |
+
xaxis_title='시간',
|
| 209 |
+
yaxis_title='수위 (cm)',
|
| 210 |
+
hovermode='x unified',
|
| 211 |
+
height=600,
|
| 212 |
+
showlegend=True,
|
| 213 |
+
xaxis=dict(tickformat='%H:%M<br>%m/%d', gridcolor='lightgray', showgrid=True),
|
| 214 |
+
yaxis=dict(gridcolor='lightgray', showgrid=True),
|
| 215 |
+
plot_bgcolor='white'
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
return fig
|
| 219 |
+
except Exception as e:
|
| 220 |
+
print(f"Enhanced plot creation error: {e}")
|
| 221 |
+
traceback.print_exc()
|
| 222 |
+
fig = go.Figure()
|
| 223 |
+
fig.add_annotation(
|
| 224 |
+
text=f"시각화 생성 중 오류: {str(e)}",
|
| 225 |
+
xref="paper", yref="paper",
|
| 226 |
+
x=0.5, y=0.5, showarrow=False
|
| 227 |
+
)
|
| 228 |
+
return fig
|
| 229 |
+
|
| 230 |
+
def single_prediction(station_id, input_csv_file):
|
| 231 |
+
if input_csv_file is None:
|
| 232 |
+
raise gr.Error("예측을 위한 입력 파일을 업로드해주세요.")
|
| 233 |
+
|
| 234 |
+
is_valid, message = validate_csv_file(input_csv_file.name)
|
| 235 |
+
if not is_valid:
|
| 236 |
+
raise gr.Error(f"파일 오류: {message}")
|
| 237 |
+
|
| 238 |
+
station_name = STATION_NAMES.get(station_id, station_id)
|
| 239 |
+
|
| 240 |
+
common_args = get_common_args(station_id)
|
| 241 |
+
setting_name = f"long_term_forecast_{station_id}_144_72_TimeXer_TIDE_ftMS_sl144_ll96_pl72_dm256_nh8_el1_dl1_df512_expand2_dc4_fc3_ebtimeF_dtTrue_Exp_0"
|
| 242 |
+
checkpoint_path = f"./checkpoints/{setting_name}/checkpoint.pth"
|
| 243 |
+
scaler_path = f"./checkpoints/{setting_name}/scaler.gz"
|
| 244 |
+
|
| 245 |
+
if not os.path.exists(checkpoint_path):
|
| 246 |
+
raise gr.Error(f"모델 파일을 찾을 수 없습니다: {checkpoint_path}")
|
| 247 |
+
if not os.path.exists(scaler_path):
|
| 248 |
+
raise gr.Error(f"스케일러 파일을 찾을 수 없습니다: {scaler_path}")
|
| 249 |
+
|
| 250 |
+
command = ["python", "inference.py",
|
| 251 |
+
"--checkpoint_path", checkpoint_path,
|
| 252 |
+
"--scaler_path", scaler_path,
|
| 253 |
+
"--predict_input_file", input_csv_file.name] + common_args
|
| 254 |
+
|
| 255 |
+
gr.Info(f"{station_name}({station_id}) 통합 조위 예측을 실행중입니다...")
|
| 256 |
+
|
| 257 |
+
success, output = execute_inference_and_get_results(command)
|
| 258 |
+
|
| 259 |
+
try:
|
| 260 |
+
prediction_file = "pred_results/prediction_future.npy"
|
| 261 |
+
if os.path.exists(prediction_file):
|
| 262 |
+
residual_predictions = np.load(prediction_file)
|
| 263 |
+
|
| 264 |
+
input_df = pd.read_csv(input_csv_file.name)
|
| 265 |
+
input_df['date'] = pd.to_datetime(input_df['date'])
|
| 266 |
+
last_time = input_df['date'].iloc[-1]
|
| 267 |
+
|
| 268 |
+
prediction_results = calculate_final_tide(residual_predictions, station_id, last_time)
|
| 269 |
+
plot = create_enhanced_prediction_plot(prediction_results, input_csv_file, station_name)
|
| 270 |
+
|
| 271 |
+
has_harmonic = any(h != 0 for h in prediction_results['harmonic'])
|
| 272 |
+
|
| 273 |
+
if has_harmonic:
|
| 274 |
+
result_df = pd.DataFrame({
|
| 275 |
+
'예측 시간': [t.strftime('%Y-%m-%d %H:%M') for t in prediction_results['times']],
|
| 276 |
+
'잔차 예측 (cm)': [f"{val:.2f}" for val in prediction_results['residual']],
|
| 277 |
+
'조화 예측 (cm)': [f"{val:.2f}" for val in prediction_results['harmonic']],
|
| 278 |
+
'최종 조위 (cm)': [f"{val:.2f}" for val in prediction_results['final_tide']]
|
| 279 |
+
})
|
| 280 |
+
else:
|
| 281 |
+
result_df = pd.DataFrame({
|
| 282 |
+
'예측 시간': [t.strftime('%Y-%m-%d %H:%M') for t in prediction_results['times']],
|
| 283 |
+
'잔차 예측 (cm)': [f"{val:.2f}" for val in prediction_results['residual']]
|
| 284 |
+
})
|
| 285 |
+
|
| 286 |
+
saved_count = save_predictions_to_supabase(station_id, prediction_results)
|
| 287 |
+
if saved_count > 0:
|
| 288 |
+
save_message = f"\n💾 Supabase에 {saved_count}개 예측 결과 저장 완료!"
|
| 289 |
+
elif get_supabase_client() is None:
|
| 290 |
+
save_message = "\n⚠️ Supabase 연결 실패 (환경변수 확인 필요)"
|
| 291 |
+
else:
|
| 292 |
+
save_message = "\n⚠️ Supabase 저장 실패"
|
| 293 |
+
|
| 294 |
+
return plot, result_df, f"✅ 예측 완료!{save_message}\n\n{output}"
|
| 295 |
+
else:
|
| 296 |
+
return None, None, f"❌ 결과 파일을 찾을 수 없습니다.\n\n{output}"
|
| 297 |
+
except Exception as e:
|
| 298 |
+
print(f"Result processing error: {e}")
|
| 299 |
+
traceback.print_exc()
|
| 300 |
+
return None, None, f"❌ 결과 처리 중 오류: {str(e)}\n\n{output}"
|
supabase_utils.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import traceback
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
|
| 6 |
+
import pandas as pd
|
| 7 |
+
import pytz
|
| 8 |
+
from dateutil import parser as date_parser
|
| 9 |
+
|
| 10 |
+
from config import SUPABASE_URL, SUPABASE_KEY
|
| 11 |
+
|
| 12 |
+
# Supabase 연동 추가
|
| 13 |
+
try:
|
| 14 |
+
from supabase import create_client, Client
|
| 15 |
+
SUPABASE_AVAILABLE = True
|
| 16 |
+
except ImportError:
|
| 17 |
+
SUPABASE_AVAILABLE = False
|
| 18 |
+
print("Supabase 패키지가 설치되지 않았습니다.")
|
| 19 |
+
|
| 20 |
+
def clean_string(s):
|
| 21 |
+
"""문자열에서 특수 유니코드 문자 제거"""
|
| 22 |
+
if s is None:
|
| 23 |
+
return None
|
| 24 |
+
cleaned = s.replace('\u2028', '').replace('\u2029', '')
|
| 25 |
+
cleaned = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', cleaned)
|
| 26 |
+
return cleaned.strip()
|
| 27 |
+
|
| 28 |
+
def get_supabase_client():
|
| 29 |
+
"""Supabase 클라이언트 생성"""
|
| 30 |
+
if not SUPABASE_AVAILABLE:
|
| 31 |
+
return None
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
if not SUPABASE_URL or not SUPABASE_KEY:
|
| 35 |
+
print("Supabase 환경변수가 설정되지 않았습니다.")
|
| 36 |
+
return None
|
| 37 |
+
|
| 38 |
+
url = clean_string(SUPABASE_URL)
|
| 39 |
+
key = clean_string(SUPABASE_KEY)
|
| 40 |
+
|
| 41 |
+
if not url.startswith('http'):
|
| 42 |
+
print(f"잘못된 SUPABASE_URL 형식: {url}")
|
| 43 |
+
return None
|
| 44 |
+
|
| 45 |
+
return create_client(url, key)
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f"Supabase 연결 오류: {e}")
|
| 48 |
+
traceback.print_exc()
|
| 49 |
+
return None
|
| 50 |
+
|
| 51 |
+
def get_harmonic_predictions(station_id, start_time, end_time):
|
| 52 |
+
"""해당 시간 범위의 조화 예측값 조회"""
|
| 53 |
+
supabase = get_supabase_client()
|
| 54 |
+
if not supabase:
|
| 55 |
+
print("Supabase 클라이언트를 생성할 수 없습니다.")
|
| 56 |
+
return []
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
kst = pytz.timezone('Asia/Seoul')
|
| 60 |
+
|
| 61 |
+
if start_time.tzinfo is None:
|
| 62 |
+
start_time = kst.localize(start_time)
|
| 63 |
+
if end_time.tzinfo is None:
|
| 64 |
+
end_time = kst.localize(end_time)
|
| 65 |
+
|
| 66 |
+
start_utc = start_time.astimezone(pytz.UTC)
|
| 67 |
+
end_utc = end_time.astimezone(pytz.UTC)
|
| 68 |
+
|
| 69 |
+
start_str = start_utc.strftime('%Y-%m-%dT%H:%M:%S+00:00')
|
| 70 |
+
end_str = end_utc.strftime('%Y-%m-%dT%H:%M:%S+00:00')
|
| 71 |
+
|
| 72 |
+
result = supabase.table('harmonic_predictions')\
|
| 73 |
+
.select('predicted_at, harmonic_level')\
|
| 74 |
+
.eq('station_id', station_id)\
|
| 75 |
+
.gte('predicted_at', start_str)\
|
| 76 |
+
.lte('predicted_at', end_str)\
|
| 77 |
+
.order('predicted_at')\
|
| 78 |
+
.limit(1000)\
|
| 79 |
+
.execute()
|
| 80 |
+
|
| 81 |
+
return result.data if result.data else []
|
| 82 |
+
except Exception as e:
|
| 83 |
+
print(f"조화 예측값 조회 오류: {e}")
|
| 84 |
+
traceback.print_exc()
|
| 85 |
+
return []
|
| 86 |
+
|
| 87 |
+
def save_predictions_to_supabase(station_id, prediction_results):
|
| 88 |
+
"""예측 결과를 Supabase에 저장"""
|
| 89 |
+
supabase = get_supabase_client()
|
| 90 |
+
if not supabase:
|
| 91 |
+
print("Supabase 클라이언트를 생성할 수 없습니다.")
|
| 92 |
+
return 0
|
| 93 |
+
|
| 94 |
+
try:
|
| 95 |
+
if prediction_results['times']:
|
| 96 |
+
start_time = prediction_results['times'][0].strftime('%Y-%m-%dT%H:%M:%S')
|
| 97 |
+
end_time = prediction_results['times'][-1].strftime('%Y-%m-%dT%H:%M:%S')
|
| 98 |
+
|
| 99 |
+
supabase.table('tide_predictions')\
|
| 100 |
+
.delete()\
|
| 101 |
+
.eq('station_id', station_id)\
|
| 102 |
+
.gte('predicted_at', start_time)\
|
| 103 |
+
.lte('predicted_at', end_time)\
|
| 104 |
+
.execute()
|
| 105 |
+
|
| 106 |
+
insert_data = []
|
| 107 |
+
for i in range(len(prediction_results['times'])):
|
| 108 |
+
time_str = prediction_results['times'][i].strftime('%Y-%m-%dT%H:%M:%S')
|
| 109 |
+
|
| 110 |
+
insert_data.append({
|
| 111 |
+
'station_id': station_id,
|
| 112 |
+
'predicted_at': time_str,
|
| 113 |
+
'predicted_residual': float(prediction_results['residual'][i]),
|
| 114 |
+
'harmonic_level': float(prediction_results['harmonic'][i]),
|
| 115 |
+
'final_tide_level': float(prediction_results['final_tide'][i])
|
| 116 |
+
})
|
| 117 |
+
|
| 118 |
+
result = supabase.table('tide_predictions')\
|
| 119 |
+
.insert(insert_data)\
|
| 120 |
+
.execute()
|
| 121 |
+
|
| 122 |
+
return len(insert_data)
|
| 123 |
+
except Exception as e:
|
| 124 |
+
print(f"예측 결과 저장 오류: {e}")
|
| 125 |
+
traceback.print_exc()
|
| 126 |
+
return 0
|
ui.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import os
|
| 3 |
+
from config import STATIONS
|
| 4 |
+
from supabase_utils import get_supabase_client
|
| 5 |
+
|
| 6 |
+
def create_ui(prediction_handler, chatbot_handler):
|
| 7 |
+
"""Gradio UI를 생성하고 반환합니다."""
|
| 8 |
+
with gr.Blocks(title="통합 조위 예측 시스템", theme=gr.themes.Soft()) as demo:
|
| 9 |
+
gr.Markdown("# 🌊 통합 조위 예측 시스템 with Gemini")
|
| 10 |
+
|
| 11 |
+
# 연결 상태 표시
|
| 12 |
+
client = get_supabase_client()
|
| 13 |
+
supabase_status = "🟢 연결됨" if client else "🔴 연결 안됨 (환경변수 확인 필요)"
|
| 14 |
+
gemini_status = "🟢 연결됨" if os.getenv("GEMINI_API_KEY") else "🔴 연결 안됨 (환경변수 확인 필요)"
|
| 15 |
+
gr.Markdown(f"**Supabase 상태**: {supabase_status} | **Gemini 상태**: {gemini_status}")
|
| 16 |
+
|
| 17 |
+
with gr.Tabs():
|
| 18 |
+
with gr.TabItem("통합 조위 예측"):
|
| 19 |
+
with gr.Row():
|
| 20 |
+
with gr.Column(scale=1):
|
| 21 |
+
station_id_input = gr.Dropdown(STATIONS, label="관측소 선택", value="DT_0001")
|
| 22 |
+
input_csv = gr.File(label="과거 데이터 업로드 (.csv)")
|
| 23 |
+
predict_btn = gr.Button("예측 실행", variant="primary")
|
| 24 |
+
with gr.Column(scale=3):
|
| 25 |
+
output_plot = gr.Plot(label="예측 결과 시각화")
|
| 26 |
+
output_df = gr.DataFrame(label="예측 결과 데이터")
|
| 27 |
+
output_log = gr.Textbox(label="실행 로그", lines=5, interactive=False)
|
| 28 |
+
|
| 29 |
+
with gr.TabItem("AI 조위 챗봇"):
|
| 30 |
+
gr.ChatInterface(
|
| 31 |
+
fn=chatbot_handler,
|
| 32 |
+
title="AI 조위 챗봇",
|
| 33 |
+
description="조위에 대해 궁금한 점을 물어보세요. (예: '인천 오늘 현재 조위 알려줘')",
|
| 34 |
+
#examples=[]
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
predict_btn.click(
|
| 38 |
+
fn=prediction_handler,
|
| 39 |
+
inputs=[station_id_input, input_csv],
|
| 40 |
+
outputs=[output_plot, output_df, output_log]
|
| 41 |
+
)
|
| 42 |
+
return demo
|