SeungHyeok Jang commited on
Commit
613de59
·
1 Parent(s): a7d4b8e

modulizatioin

Browse files
Files changed (8) hide show
  1. api_utils.py +206 -0
  2. app.py +17 -1151
  3. chatbot.py +126 -0
  4. chatbot_utils.py +114 -0
  5. config.py +20 -0
  6. prediction.py +300 -0
  7. supabase_utils.py +126 -0
  8. 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 traceback
15
 
16
- # Supabase 연동 추가
17
- try:
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
- def calculate_final_tide(residual_predictions, station_id, last_time):
149
- """잔차 예측 + 조화 예측 = 최종 조위 계산"""
150
- if isinstance(last_time, pd.Timestamp):
151
- last_time = last_time.to_pydatetime()
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
- - '시작 시간'과 '종료 시간'은 'YYYY-MM-DD HH:MM:SS' 형식으로 변환해주세요.
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
- # --- 5. Gradio 인터페이스 ---
902
- with gr.Blocks(title="통합 조위 예측 시스템", theme=gr.themes.Soft()) as demo:
903
- gr.Markdown("# 🌊 통합 조위 예측 시스템 with Gemini")
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
- with gr.Tabs():
912
- # 1번 탭: 통합 조위 예측
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