JAMESPARK3 commited on
Commit
ab9bf23
·
verified ·
1 Parent(s): 7e03ec0

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1000 -0
app.py ADDED
@@ -0,0 +1,1000 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import requests
3
+ import xmltodict
4
+ import pandas as pd
5
+ from datetime import datetime, timedelta
6
+ import streamlit.components.v1 as components
7
+ import plotly.express as px
8
+ import time
9
+ import plotly.io as pio
10
+ import httpx
11
+ from openai import OpenAI
12
+ import google.generativeai as genai
13
+ genai.configure(api_key=GEMINI_API_KEY)
14
+
15
+
16
+ # plotly의 JSON 직렬화 엔진을 기본 json으로 설정
17
+ pio.json.config.default_engine = 'json'
18
+
19
+ # 페이지 설정
20
+ st.set_page_config(
21
+ page_title="우리집 날씨 정보",
22
+ page_icon="🌤️",
23
+ layout="wide",
24
+ menu_items={
25
+ 'Get Help': None,
26
+ 'Report a bug': None,
27
+ 'About': None
28
+ }
29
+ )
30
+
31
+ # CSS 스타일 개선
32
+ st.markdown("""
33
+ <style>
34
+ section[data-testid="stSidebar"] {
35
+ display: none;
36
+ }
37
+ #MainMenu {
38
+ display: none;
39
+ }
40
+ header {
41
+ display: none;
42
+ }
43
+ .block-container {
44
+ padding: 0 !important;
45
+ max-width: 100% !important;
46
+ }
47
+ .element-container {
48
+ margin: 0 !important;
49
+ }
50
+ .stApp > header {
51
+ display: none;
52
+ }
53
+ #other-info {
54
+ display: none;
55
+ }
56
+ .stPlotlyChart {
57
+ width: 100%;
58
+ margin: 0 !important;
59
+ padding: 0 !important;
60
+ }
61
+ [data-testid="stMetricValue"] {
62
+ font-size: 3rem;
63
+ }
64
+ .time-container {
65
+ width: 100%;
66
+ text-align: center;
67
+ margin: 0 auto;
68
+ padding: 15px 0;
69
+ }
70
+ .date-text {
71
+ font-size: 8em !important;
72
+ font-weight: bold !important;
73
+ color: rgb(0, 0, 0) !important;
74
+ font-family: Arial, sans-serif !important;
75
+ text-shadow: none !important;
76
+ background: transparent !important;
77
+ display: block !important;
78
+ line-height: 1.2 !important;
79
+ margin-bottom: 0.5px !important;
80
+ }
81
+ h1, h2, h3, h4, h5, h6, p, .stMetric > div > div {
82
+ color: black !important;
83
+ }
84
+ .plotly-graph-div {
85
+ overflow-x: scroll !important;
86
+ min-width: 100% !important;
87
+ }
88
+ div[data-testid="stVerticalBlock"] > div {
89
+ padding: 0 !important;
90
+ }
91
+ .main {
92
+ padding: 0 !important;
93
+ }
94
+ .stApp {
95
+ margin: 0 !important;
96
+ }
97
+ [data-testid="stHeader"] {
98
+ display: none;
99
+ }
100
+ .section-container {
101
+ position: absolute;
102
+ top: 0;
103
+ left: 0;
104
+ width: 100%;
105
+ height: 100vh;
106
+ padding: 1rem;
107
+ box-sizing: border-box;
108
+ background-color: inherit;
109
+ }
110
+ .graph-container {
111
+ width: 100%;
112
+ height: calc(100vh - 100px);
113
+ display: flex;
114
+ flex-direction: column;
115
+ align-items: center;
116
+ }
117
+ iframe {
118
+ margin: 0 !important;
119
+ padding: 0 !important;
120
+ }
121
+ [data-testid="column"] {
122
+ padding: 0 !important;
123
+ }
124
+ [data-testid="stVerticalBlock"] {
125
+ padding: 0 !important;
126
+ gap: 0 !important;
127
+ }
128
+ .dust-status {
129
+ font-size: 2em;
130
+ font-weight: bold;
131
+ color: black;
132
+ padding: 0.3rem 1rem;
133
+ border-radius: 1rem;
134
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
135
+ display: inline-block;
136
+ }
137
+ @keyframes scroll-text {
138
+ from {
139
+ transform: translateX(100%);
140
+ }
141
+ to {
142
+ transform: translateX(-100%);
143
+ }
144
+ }
145
+ .scroll-container {
146
+ position: fixed;
147
+ bottom: 20px;
148
+ left: 0;
149
+ width: 100%;
150
+ overflow: hidden;
151
+ background-color: rgba(255, 255, 255, 0.9);
152
+ padding: 10px 0;
153
+ z-index: 1000;
154
+ }
155
+ .scroll-text {
156
+ display: inline-block;
157
+ white-space: nowrap;
158
+ animation: scrolling 30s linear infinite;
159
+ font-size: 2.5em;
160
+ font-weight: bold;
161
+ color: #333;
162
+ position: relative;
163
+ left: 50%;
164
+ transform: translateX(-50%);
165
+ }
166
+ @keyframes scrolling {
167
+ 0% {transform: translateX(0%); opacity: 0;}
168
+ 10% {opacity: 1;}
169
+ 90% {opacity: 1;}
170
+ 100% {transform: translateX(-100%); opacity: 0;}
171
+ }
172
+
173
+
174
+ /* 모바일 대응을 위한 CSS 추가 */
175
+ @media (max-width: 600px) {
176
+ .time-container {
177
+ font-size: 3em; /* 줄임 */
178
+ }
179
+ .date-text {
180
+ font-size: 4em !important; /* 줄임 */
181
+ }
182
+
183
+ .scroll-text {
184
+ font-size: 1.2em; /* 폰트 크기 줄임 */
185
+ }
186
+ }
187
+ </style>
188
+ """, unsafe_allow_html=True)
189
+
190
+
191
+ def get_korean_weekday(date):
192
+ weekday = date.strftime('%a')
193
+ weekday_dict = {
194
+ 'Mon': '월',
195
+ 'Tue': '화',
196
+ 'Wed': '수',
197
+ 'Thu': '목',
198
+ 'Fri': '금',
199
+ 'Sat': '토',
200
+ 'Sun': '일'
201
+ }
202
+ return weekday_dict[weekday]
203
+
204
+ def check_network_status():
205
+ try:
206
+ response = httpx.get("http://www.google.com", timeout=5)
207
+ return response.status_code == 200
208
+ except httpx.RequestError:
209
+ return False
210
+
211
+ def check_api_status():
212
+ try:
213
+ url = "http://openapi.seoul.go.kr:8088/77544e69764a414d363647424a655a/xml/citydata/1/5/신림역"
214
+ response = requests.get(url, timeout=5)
215
+ if response.status_code == 200:
216
+ data = xmltodict.parse(response.text)
217
+ if data.get('SeoulRtd.citydata', {}).get('RESULT', {}).get('MESSAGE') == "정상 처리되었습니다.":
218
+ return True
219
+ return False
220
+ except:
221
+ return False
222
+
223
+ @st.cache_data(ttl=300)
224
+ def get_weather_data():
225
+ url = "http://openapi.seoul.go.kr:8088/77544e69764a414d363647424a655a/xml/citydata/1/5/신림역"
226
+ try:
227
+ response = requests.get(url, timeout=30)
228
+ response.raise_for_status()
229
+
230
+ if response.text.strip(): # 응답이 비어있지 않은 경우에만 파싱
231
+ data = xmltodict.parse(response.text)
232
+ result = data['SeoulRtd.citydata']['CITYDATA']['WEATHER_STTS']['WEATHER_STTS']
233
+ if result:
234
+ return result
235
+
236
+ except (requests.exceptions.Timeout,
237
+ requests.exceptions.RequestException,
238
+ Exception):
239
+ pass
240
+
241
+ return None
242
+
243
+
244
+ def get_background_color(pm10_value):
245
+ try:
246
+ pm10 = float(pm10_value)
247
+ if pm10 <= 30:
248
+ return "#87CEEB" # 파랑 (좋음)
249
+ elif pm10 <= 80:
250
+ return "#90EE90" # 초록 (보통)
251
+ elif pm10 <= 150:
252
+ return "#FFD700" # 노랑 (나쁨)
253
+ else:
254
+ return "#FF6B6B" # 빨강 (매우 나쁨)
255
+ except:
256
+ return "#FFFFFF" # 기본 흰색
257
+
258
+
259
+ def get_current_sky_status(data):
260
+ current_time = datetime.utcnow() + timedelta(hours=9)
261
+ current_hour = current_time.hour
262
+
263
+ forecast_data = data['FCST24HOURS']['FCST24HOURS']
264
+ if not isinstance(forecast_data, list):
265
+ forecast_data = [forecast_data]
266
+
267
+ closest_forecast = None
268
+ min_time_diff = float('inf')
269
+
270
+ for forecast in forecast_data:
271
+ forecast_hour = int(forecast['FCST_DT'][8:10])
272
+ time_diff = abs(forecast_hour - current_hour)
273
+ if time_diff < min_time_diff:
274
+ min_time_diff = time_diff
275
+ closest_forecast = forecast
276
+
277
+ return closest_forecast['SKY_STTS'] if closest_forecast else "정보없음"
278
+
279
+ def format_news_message(news_list):
280
+ if not isinstance(news_list, list):
281
+ news_list = [news_list]
282
+
283
+ current_warnings = []
284
+ for news in news_list:
285
+ if not isinstance(news, dict):
286
+ continue
287
+
288
+ warn_val = news.get('WARN_VAL', '')
289
+ warn_stress = news.get('WARN_STRESS', '')
290
+ command = news.get('COMMAND', '')
291
+ warn_msg = news.get('WARN_MSG', '')
292
+ announce_time = news.get('ANNOUNCE_TIME', '')
293
+
294
+ if announce_time and len(announce_time) == 12:
295
+ year = announce_time[0:4]
296
+ month = announce_time[4:6]
297
+ day = announce_time[6:8]
298
+ hour = announce_time[8:10]
299
+ minute = announce_time[10:12]
300
+ formatted_time = f"({year}년{month}월{day}일{hour}시{minute}분)"
301
+ else:
302
+ formatted_time = ""
303
+
304
+ if command == '해제':
305
+ warning_text = f"✅ {warn_val}{warn_stress} 해제 {formatted_time} {warn_msg}"
306
+ else:
307
+ warning_text = f"⚠️ {warn_val}{warn_stress} 발령 {formatted_time} {warn_msg}"
308
+ current_warnings.append(warning_text)
309
+
310
+ return ' | '.join(current_warnings)
311
+
312
+ def show_weather_info(data):
313
+ st.markdown('<div class="section-container">', unsafe_allow_html=True)
314
+
315
+ # Add update time display using the last API call timestamp (already in KST)
316
+ refresh_time = datetime.fromtimestamp(st.session_state.last_api_call) if st.session_state.last_api_call else (datetime.utcnow() + timedelta(hours=9))
317
+ st.markdown(f'''
318
+ <div style="text-align: center; font-size: 0.8em; color: gray;">
319
+ Data refreshed at: {refresh_time.strftime('%Y-%m-%d %H:%M:%S')}
320
+ </div>
321
+ ''', unsafe_allow_html=True)
322
+
323
+ # Add this code to define formatted_date
324
+ current_time = datetime.utcnow() + timedelta(hours=9)
325
+ weekday = get_korean_weekday(current_time)
326
+ formatted_date = f"{current_time.strftime('%Y-%m-%d')}({weekday})"
327
+
328
+ pm10 = float(data['PM10'])
329
+ if pm10 <= 30:
330
+ dust_status = "좋음"
331
+ dust_color = "#87CEEB" # Blue
332
+ elif pm10 <= 80:
333
+ dust_status = "보통"
334
+ dust_color = "#90EE90" # Green
335
+ elif pm10 <= 150:
336
+ dust_status = "나쁨"
337
+ dust_color = "#FFD700" # Yellow
338
+ else:
339
+ dust_status = "매우나쁨"
340
+ dust_color = "#FF6B6B" # Red
341
+
342
+ temp = data.get('TEMP', "정보없음")
343
+ precip_type = data.get('PRECPT_TYPE', "정보없음")
344
+
345
+ try:
346
+ temp = f"{float(temp):.1f}°C"
347
+ except:
348
+ temp = "정보없음"
349
+
350
+ # 현재 시간 기준으로 가장 가까운 06시 데이터 찾기
351
+ morning_six_data = None
352
+ current_time = datetime.utcnow() + timedelta(hours=9) # KST
353
+
354
+ forecast_data = data['FCST24HOURS']['FCST24HOURS']
355
+ if not isinstance(forecast_data, list):
356
+ forecast_data = [forecast_data]
357
+
358
+ for fcst in forecast_data:
359
+ fcst_hour = int(fcst['FCST_DT'][8:10]) # HH
360
+ if fcst_hour == 6:
361
+ fcst_datetime = datetime.strptime(fcst['FCST_DT'], '%Y%m%d%H%M')
362
+ if fcst_datetime > current_time:
363
+ morning_six_data = fcst
364
+ break
365
+
366
+ # 06시 날씨 정보 준비
367
+ tomorrow_morning_weather = "없음"
368
+ if morning_six_data:
369
+ tomorrow_temp = morning_six_data['TEMP']
370
+ weather_icon = ""
371
+
372
+ # PRECPT_TYPE 먼저 확인
373
+ precip_type = morning_six_data['PRECPT_TYPE']
374
+ if precip_type == "비" or precip_type == "비/눈":
375
+ weather_icon = "☔"
376
+ elif precip_type == "눈":
377
+ weather_icon = '<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">❄</span>'
378
+ # PRECPT_TYPE이 '없음'이면 SKY_STTS 기반으로 아이콘 설정
379
+ else:
380
+ if morning_six_data['SKY_STTS'] == "맑음":
381
+ weather_icon = "🌞"
382
+ elif morning_six_data['SKY_STTS'] in ["구름", "구름많음"]:
383
+ weather_icon = "⛅"
384
+ elif morning_six_data['SKY_STTS'] == "흐림":
385
+ weather_icon = '<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">☁</span>'
386
+
387
+ tomorrow_morning_weather = f"{tomorrow_temp}°C {weather_icon}"
388
+
389
+ # 화면에 표시
390
+ weather_icon = ""
391
+ current_time_str = current_time.strftime('%Y%m%d%H')
392
+
393
+ # Check current precipitation type first
394
+ if data['PRECPT_TYPE'] in ["비", "눈", "비/눈", "빗방울"]:
395
+ if data['PRECPT_TYPE'] in ["비", "빗방울"]:
396
+ weather_icon = "☔"
397
+ elif data['PRECPT_TYPE'] == "눈":
398
+ weather_icon = '<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">❄</span>'
399
+ elif data['PRECPT_TYPE'] == "비/눈":
400
+ weather_icon = '☔<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">❄</span>'
401
+
402
+ else:
403
+ # Find nearest forecast time when no current precipitation
404
+ nearest_forecast = None
405
+ min_time_diff = float('inf')
406
+
407
+ for forecast in forecast_data:
408
+ forecast_time = datetime.strptime(forecast['FCST_DT'], '%Y%m%d%H%M')
409
+ time_diff = abs((forecast_time - current_time).total_seconds())
410
+ if time_diff < min_time_diff:
411
+ min_time_diff = time_diff
412
+ nearest_forecast = forecast
413
+
414
+ if nearest_forecast:
415
+ if nearest_forecast['PRECPT_TYPE'] in ["비", "눈", "비/눈", "빗방울"]:
416
+ if nearest_forecast['PRECPT_TYPE'] in ["비", "빗방울"]:
417
+ weather_icon = "☔"
418
+ elif nearest_forecast['PRECPT_TYPE'] == "눈":
419
+ weather_icon = '<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">❄</span>'
420
+ elif nearest_forecast['PRECPT_TYPE'] == "비/눈":
421
+ weather_icon = '☔<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">❄</span>'
422
+
423
+ else:
424
+ # Use SKY_STTS when no precipitation
425
+ sky_status = nearest_forecast['SKY_STTS']
426
+ if sky_status == "맑음":
427
+ weather_icon = "🌞"
428
+ elif sky_status in ["구름", "구름많음"]:
429
+ weather_icon = "⛅"
430
+ elif sky_status == "흐림":
431
+ weather_icon = '<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">☁</span>'
432
+
433
+ precip_mark = weather_icon
434
+ st.markdown(f'''
435
+ <div class="time-container">
436
+ <div style="text-align: center; margin-bottom: 0.5rem; font-size: 6em; font-weight: bold; color: black;">
437
+ {temp}{precip_mark} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {tomorrow_morning_weather}
438
+ </div>
439
+ <span class="date-text">{formatted_date}</span>
440
+ </div>
441
+ ''', unsafe_allow_html=True)
442
+
443
+
444
+ clock_html = """
445
+ <div style="width: 100%; max-width: 1200px; margin: 0 auto; padding: 0 20px;">
446
+ <div style="text-align: center; height: 300px; display: flex; align-items: center; justify-content: center;">
447
+ <span id="clock" style="font-size: 15em; font-weight: bold; color: black; line-height: 1.2; white-space: nowrap;"></span>
448
+ </div>
449
+ </div>
450
+ <script>
451
+ function updateClock() {
452
+ const now = new Date();
453
+ const options = {
454
+ timeZone: 'Asia/Seoul',
455
+ hour12: true,
456
+ hour: 'numeric',
457
+ minute: '2-digit'
458
+ };
459
+ document.getElementById('clock').textContent = now.toLocaleTimeString('ko-KR', options);
460
+ }
461
+ setInterval(updateClock, 1000);
462
+ updateClock();
463
+ </script>
464
+ """
465
+ components.html(clock_html, height=300)
466
+
467
+ # 날씨 예보 생성 및 스크롤 컨테이너 표시
468
+ col1, col2, col3, col4 = st.columns([1, 1, 1, 2])
469
+ with col1:
470
+ if st.button("날씨 예보 스크롤", key="toggle_scroll"):
471
+ st.session_state.scroll_visible = not st.session_state.scroll_visible
472
+
473
+ # 날씨 예보 생성
474
+ forecast_data = data['FCST24HOURS']['FCST24HOURS']
475
+ if not isinstance(forecast_data, list):
476
+ forecast_data = [forecast_data]
477
+
478
+ forecast_data_str = "\n".join([
479
+ f"[{f['FCST_DT'][:4]}년 {f['FCST_DT'][4:6]}월 {f['FCST_DT'][6:8]}일 {f['FCST_DT'][8:10]}시] {f['TEMP']}도, {f['SKY_STTS']}"
480
+ for f in forecast_data
481
+ ])
482
+
483
+ current_time = datetime.utcnow() + timedelta(hours=9)
484
+ current_time_str = current_time.strftime('%H시 %M분')
485
+
486
+ # 날씨 예보 텍스트 생성
487
+ st.session_state.weather_forecast = get_weather_forecast(forecast_data_str, current_time_str)
488
+
489
+ # 스크롤 컨테이너 CSS
490
+ background_color = get_background_color(data['PM10'])
491
+ display_style = "block" if st.session_state.scroll_visible else "none"
492
+ scroll_style = f"""
493
+ background-color: rgba(255, 255, 255, 0.9);
494
+ color: #333;
495
+ display: {display_style};
496
+ position: fixed;
497
+ bottom: 20px;
498
+ left: 0;
499
+ width: 100%;
500
+ overflow: hidden;
501
+ padding: 10px 0;
502
+ z-index: 1000;
503
+ """
504
+
505
+ text_style = """
506
+ white-space: nowrap;
507
+ animation: scroll-text 30s linear infinite;
508
+ display: inline-block;
509
+ font-size: 2.5em;
510
+ font-weight: bold;
511
+ """
512
+
513
+ # 스크롤 컨테이너 표시
514
+ st.markdown(f'''
515
+ <div class="scroll-container" style="{scroll_style}">
516
+ <div class="scroll-text" style="{text_style}">{st.session_state.weather_forecast}</div>
517
+ </div>
518
+ ''', unsafe_allow_html=True)
519
+
520
+ with col2:
521
+ st.button("시간대별 온도 보기", on_click=lambda: st.session_state.update({'current_section': 'temperature'}))
522
+ # API 응답 체크 버튼 부분 수정
523
+ with col3:
524
+ if st.button("API 응답 체크"):
525
+ if check_api_status():
526
+ st.session_state.api_failed = False
527
+ new_data = get_weather_data()
528
+ if new_data:
529
+ st.session_state.weather_data = new_data
530
+ st.session_state.last_api_call = datetime.utcnow().timestamp()
531
+ st.rerun()
532
+
533
+ # session_state에 API 실패 시간 저장을 위한 변수 추가
534
+ if 'api_failed_time' not in st.session_state:
535
+ st.session_state.api_failed_time = None
536
+
537
+ with col4:
538
+ network_ok = check_network_status()
539
+ if not network_ok:
540
+ status_color = "#FF0000"
541
+ status_text = "네트워크 연결 없음"
542
+ else:
543
+ current_time = datetime.utcnow() + timedelta(hours=9) # KST
544
+ if not st.session_state.api_failed:
545
+ status_color = "#00AA00"
546
+ st.session_state.api_status_time = current_time
547
+ status_time = st.session_state.api_status_time.strftime('%Y-%m-%d %H:%M')
548
+ status_text = f"API 정상({status_time} 성공)"
549
+ else:
550
+ status_color = "#FF0000"
551
+ if st.session_state.api_status_time is None:
552
+ st.session_state.api_status_time = current_time
553
+ status_time = st.session_state.api_status_time.strftime('%Y-%m-%d %H:%M')
554
+ status_text = f"API 응답 없음({status_time} 발생)"
555
+
556
+ # API 상태 표시를 위한 고유한 클래스를 사용
557
+ st.markdown("""
558
+ <style>
559
+ .api-status {
560
+ color: %s !important;
561
+ font-size: 20px;
562
+ font-weight: bold;
563
+ }
564
+ </style>
565
+ <p class="api-status">%s</p>
566
+ """ % (status_color, status_text), unsafe_allow_html=True)
567
+
568
+ # forecast_data 처리
569
+ forecast_data = data['FCST24HOURS']['FCST24HOURS']
570
+ if not isinstance(forecast_data, list):
571
+ forecast_data = [forecast_data]
572
+
573
+ times = []
574
+ temps = []
575
+ weather_descriptions = []
576
+
577
+ for forecast in forecast_data:
578
+ times.append(forecast['FCST_DT'][8:10] + "시")
579
+ temps.append(float(forecast['TEMP']))
580
+
581
+ sky_status = forecast['SKY_STTS']
582
+ precip_type = forecast['PRECPT_TYPE']
583
+
584
+ if precip_type == "비":
585
+ description = "비"
586
+ elif precip_type == "눈":
587
+ description = "눈"
588
+ elif precip_type == "비/눈":
589
+ description = "비/눈"
590
+ elif sky_status == "맑음":
591
+ description = "맑음"
592
+ elif sky_status in ["구름", "구름많음"]:
593
+ description = "구름" if sky_status == "구름" else "구름많음"
594
+ elif sky_status == "흐림":
595
+ description = "흐림"
596
+ else:
597
+ description = "정보없음"
598
+
599
+ weather_descriptions.append(description)
600
+
601
+ # 스크롤 컨테이너 표시
602
+ background_color = get_background_color(data['PM10'])
603
+ display_style = "block" if st.session_state.scroll_visible else "none"
604
+ scroll_style = f"""
605
+ background-color: rgba(255, 255, 255, 0.9);
606
+ color: #333;
607
+ display: {display_style};
608
+ """
609
+
610
+ # 저장된 날씨 예보 표시
611
+ st.markdown(f'''
612
+ <div class="scroll-container" style="{scroll_style}">
613
+ <div class="scroll-text">{st.session_state.weather_forecast}</div>
614
+ </div>
615
+ ''', unsafe_allow_html=True)
616
+
617
+
618
+ st.markdown('</div>', unsafe_allow_html=True)
619
+
620
+
621
+ def show_temperature_graph(data):
622
+ st.markdown('<div class="section-container">', unsafe_allow_html=True)
623
+ st.markdown('<h1 style="text-align: center; margin-bottom: 1rem;">시간대별 온도</h1>', unsafe_allow_html=True)
624
+
625
+ forecast_data = data['FCST24HOURS']['FCST24HOURS']
626
+ if not isinstance(forecast_data, list):
627
+ forecast_data = [forecast_data]
628
+
629
+ # Sort forecast data by FCST_DT to ensure correct time ordering
630
+ forecast_data = sorted(forecast_data, key=lambda x: x['FCST_DT'])
631
+
632
+ # 현재 시간 기준으로 유효한 예보 데이터만 필터링
633
+ current_time = datetime.utcnow() + timedelta(hours=9) # KST
634
+ current_date = current_time.strftime('%Y%m%d')
635
+ next_date = (current_time + timedelta(days=1)).strftime('%Y%m%d')
636
+
637
+ # 현재 시간 이후의 예보 데이터와 다음 날의 데이터 모두 포함
638
+ valid_forecast_data = []
639
+ for fcst in forecast_data:
640
+ fcst_date = fcst['FCST_DT'][:8] # YYYYMMDD
641
+ fcst_hour = int(fcst['FCST_DT'][8:10]) # HH
642
+ current_hour = current_time.hour
643
+
644
+ # 현재 날짜의 현재 시간 이후 데이터 또는 다음 날의 데이터
645
+ if (fcst_date == current_date and fcst_hour >= current_hour) or fcst_date == next_date:
646
+ valid_forecast_data.append(fcst)
647
+
648
+ # 유효한 데이터가 없으면 전체 데이터 사용
649
+ if not valid_forecast_data:
650
+ valid_forecast_data = forecast_data
651
+
652
+ # 현재 시각과 가장 가까운 예보 시간 찾기
653
+ current_time = datetime.utcnow() + timedelta(hours=9)
654
+
655
+ # 녹색 세로선 추가 및 "현재" 텍스트 표시 - 이제 항상 첫 번째 데이터 포인트에 표시
656
+ time_differences = []
657
+ for fcst in valid_forecast_data:
658
+ forecast_time = datetime.strptime(fcst['FCST_DT'], '%Y%m%d%H%M')
659
+ time_diff = abs((forecast_time - current_time).total_seconds())
660
+ time_differences.append(time_diff)
661
+
662
+ current_index = time_differences.index(min(time_differences))
663
+
664
+ # Reorder forecast data to start from current time
665
+ valid_forecast_data = valid_forecast_data[current_index:] + valid_forecast_data[:current_index]
666
+
667
+ times = []
668
+ temps = []
669
+ weather_icons = []
670
+ weather_descriptions = []
671
+ date_changes = []
672
+
673
+ for i, forecast in enumerate(valid_forecast_data):
674
+ time_str = forecast['FCST_DT']
675
+ date = time_str[6:8]
676
+ hour = time_str[8:10]
677
+
678
+ if i > 0 and valid_forecast_data[i-1]['FCST_DT'][6:8] != date:
679
+ date_changes.append(i)
680
+ times.append(f"{hour}시")
681
+ temps.append(float(forecast['TEMP']))
682
+
683
+ sky_status = forecast['SKY_STTS']
684
+ precip_type = forecast['PRECPT_TYPE']
685
+
686
+ if precip_type == "비":
687
+ icon = "☔"
688
+ description = "비"
689
+ elif precip_type == "눈":
690
+ icon = '<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">❄</span>'
691
+ description = "눈"
692
+ elif precip_type == "비/눈":
693
+ icon = '☔<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">❄</span>'
694
+ description = "비/눈"
695
+ elif sky_status == "맑음":
696
+ icon = "🌞"
697
+ description = "맑음"
698
+ elif sky_status in ["구름", "구름많음"]:
699
+ icon = "⛅"
700
+ description = "구름" if sky_status == "구름" else "구름<br>많음"
701
+ elif sky_status == "흐림":
702
+ icon = '<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">☁</span>'
703
+ description = "흐림"
704
+ else:
705
+ icon = "☀️"
706
+ description = "정보없음"
707
+
708
+ weather_icons.append(icon)
709
+ weather_descriptions.append(description)
710
+
711
+ df = pd.DataFrame({
712
+ '시간': times,
713
+ '기온': temps,
714
+ '날씨': weather_icons,
715
+ '설명': weather_descriptions,
716
+ 'FCST_DT': [f['FCST_DT'] for f in valid_forecast_data]
717
+ })
718
+
719
+ fig = px.line(df, x='시간', y='기온', markers=True)
720
+
721
+ # Add nighttime overlay (18:00-06:00)
722
+ for i in range(len(times)):
723
+ hour = int(times[i].replace('시', ''))
724
+ if hour >= 18 or hour < 6:
725
+ fig.add_vrect(
726
+ x0=times[i],
727
+ x1=times[i+1] if i < len(times)-1 else times[-1],
728
+ fillcolor='rgba(0, 0, 0, 0.1)',
729
+ layer='below',
730
+ line_width=0,
731
+ annotation_text="",
732
+ annotation_position="top left"
733
+ )
734
+
735
+ # 녹색 세로선 추가 및 "현재" 텍스트 표시
736
+ fig.add_vline(x=times[0], line_width=2, line_dash="dash", line_color="green")
737
+ fig.add_annotation(
738
+ x=times[0],
739
+ y=max(temps) + 4,
740
+ text="<b>현재</b>",
741
+ showarrow=True,
742
+ arrowhead=2,
743
+ )
744
+
745
+ bold_times = ["00시", "06시", "12시", "18시", "24시"]
746
+ for time in bold_times:
747
+ if time in times:
748
+ index = times.index(time)
749
+ fig.add_annotation(
750
+ x=time,
751
+ y=min(temps) - 3,
752
+ text=time,
753
+ showarrow=False,
754
+ font=dict(size=30, color="black", family="Arial")
755
+ )
756
+
757
+ fig.add_vline(x='12시', line_width=2, line_dash="dash", line_color="rgba(0,0,0,0.5)")
758
+
759
+ # 오늘과 내일, 오전과 오후 텍스트는 해당 시간대의 데이터가 있을 때만 표시
760
+ time_set = set(times)
761
+ current_date = datetime.utcnow() + timedelta(hours=9) # KST
762
+ current_hour = current_date.hour
763
+
764
+ if '11시' in time_set:
765
+ fig.add_annotation(x='11시', y=max(temps) + 4, text="오전", showarrow=False, font=dict(size=24))
766
+
767
+ if '13시' in time_set:
768
+ fig.add_annotation(x='13시', y=max(temps) + 4, text="오후", showarrow=False, font=dict(size=24))
769
+
770
+ # 시간 순서대로 정렬된 데이터라고 가정
771
+ for i, time in enumerate(times):
772
+ hour = int(time.replace('시', ''))
773
+
774
+ # 현재 시각이 23시이고, times[0]이 00시라면 첫 번째 23시가 오늘 23시
775
+ if hour == 23 and times[0] == '00시':
776
+ if i == 0: # 첫 번째 23시 (오늘 23시)
777
+ fig.add_annotation(x=time, y=max(temps) + 4, text="오늘", showarrow=False, font=dict(size=24))
778
+
779
+ # 01시는 다음 날이므로 "내일" 표시 (00시 다음에 오는 01시)
780
+ if hour == 1 and i > 0 and times[i-1] == '00시':
781
+ fig.add_annotation(x=time, y=max(temps) + 4, text="내일", showarrow=False, font=dict(size=24))
782
+
783
+ fig.update_traces(
784
+ line_color='#FF6B6B',
785
+ marker=dict(size=10, color='#FF6B6B'),
786
+ textposition="top center",
787
+ mode='lines+markers+text',
788
+ text=[f"<b>{int(round(temp))}°</b>" for temp in df['기온']],
789
+ textfont=dict(size=24)
790
+ )
791
+
792
+ for i, (icon, description) in enumerate(zip(weather_icons, weather_descriptions)):
793
+ fig.add_annotation(
794
+ x=times[i],
795
+ y=max(temps) + 3,
796
+ text=f"{icon}",
797
+ showarrow=False,
798
+ font=dict(size=30)
799
+ )
800
+ fig.add_annotation(
801
+ x=times[i],
802
+ y=max(temps) + 2,
803
+ text=f"{description}",
804
+ showarrow=False,
805
+ font=dict(size=16),
806
+ textangle=0
807
+ )
808
+
809
+ for date_change in date_changes:
810
+ fig.add_vline(
811
+ x=times[date_change],
812
+ line_width=2,
813
+ line_dash="dash",
814
+ line_color="rgba(255, 0, 0, 0.7)"
815
+ )
816
+
817
+ fig.update_layout(
818
+ title=None,
819
+ xaxis_title='',
820
+ yaxis_title=None, #'기온 (°C)',
821
+ height=600,
822
+ width=7200,
823
+ showlegend=False,
824
+ plot_bgcolor='rgba(255,255,255,0.9)',
825
+ paper_bgcolor='rgba(0,0,0,0)',
826
+ margin=dict(l=50, r=50, t=0, b=0),
827
+ xaxis=dict(
828
+ tickangle=0,
829
+ tickfont=dict(size=14),
830
+ gridcolor='rgba(0,0,0,0.1)',
831
+ dtick=1,
832
+ tickmode='array',
833
+ ticktext=[f"{i:02d}시" for i in range(24)],
834
+ tickvals=[f"{i:02d}시" for i in range(24)]
835
+ ),
836
+ yaxis=dict(
837
+ tickfont=dict(size=14),
838
+ gridcolor='rgba(0,0,0,0.1)',
839
+ showticklabels=True,
840
+ tickformat='d',
841
+ ticksuffix='°C',
842
+ automargin=True,
843
+ rangemode='tozero'
844
+ )
845
+ )
846
+
847
+ st.plotly_chart(fig, use_container_width=True)
848
+
849
+ # 날�� 예보 생성 및 표시 부분을 세션 상태로 관리
850
+ if 'weather_forecast' not in st.session_state:
851
+ forecast_data_str = "\n".join([
852
+ f"[{f['FCST_DT'][:4]}년 {f['FCST_DT'][4:6]}월 {f['FCST_DT'][6:8]}일 {f['FCST_DT'][8:10]}시] {temp}도, {description}"
853
+ for f, time, temp, description in zip(valid_forecast_data, times, temps, weather_descriptions)
854
+ ])
855
+
856
+ current_time_str = current_time.strftime('%H시 %M분')
857
+ st.session_state.weather_forecast = get_weather_forecast(forecast_data_str, current_time_str)
858
+
859
+ # 저장된 날씨 예보 표시
860
+ st.markdown(f'''
861
+ <div class="scroll-container">
862
+ <div class="scroll-text">{st.session_state.weather_forecast}</div>
863
+ </div>
864
+ ''', unsafe_allow_html=True)
865
+
866
+ # 스크롤 텍스트 위에 버튼이 오도록 마진 추가
867
+ st.markdown('''
868
+ <div style="margin-bottom: 10px;">
869
+ ''', unsafe_allow_html=True)
870
+
871
+ # 우리집 날씨 정보로 돌아가기 버튼 추가
872
+ st.button("우리집 날씨 정보로 돌아가기", on_click=lambda: st.session_state.update({'current_section': 'weather'}))
873
+
874
+ st.markdown('</div>', unsafe_allow_html=True)
875
+
876
+ @st.cache_data(ttl=300) # 5분 캐시
877
+ def get_weather_forecast(forecast_data_str, current_time_str):
878
+ # Gemini 모델 설정
879
+ model = genai.GenerativeModel('gemini-2.0-flash-exp')
880
+
881
+ prompt = f"""현재 시각은 {current_time_str}입니다.
882
+
883
+ 다음 FCST_DT의 시간대별 날씨 데이터를 보고 실제 날씨 상황에 맞는 정확한 날씨 예보를 200자의 자연스러운 문장으로 만들어주세요.
884
+ 비나 눈 예보가 있는 경우에만 우산을 준비하도록 안내해주세요.
885
+
886
+ 옷차림 기준:
887
+ 27°C이상: 반팔티, 반바지, 민소매
888
+ 23°C~26°C: 얇은 셔츠, 반팔티, 반바지, 면바지
889
+ 20°C~22°C: 얇은 가디건, 긴팔티, 긴바지
890
+ 17°C~19°C: 얇은 니트, 가디건, 맨투맨, 얇은 자켓, 긴바지
891
+ 12°C~16°C: 자켓, 가디건, 야상, 맨투맨, 니트, 스타킹, 긴바지
892
+ 9°C~11°C: 트렌치코트, 야상, 가죽 자켓, 스타킹, 긴바지
893
+ 5°C~8°C: 코트, 히트텍, 니트, 긴바지
894
+ 4°C이하: 패딩, 두꺼운 코트, 목도리, 기모제품
895
+
896
+ 시간대별 날씨 데이터:
897
+ {forecast_data_str}
898
+ """
899
+
900
+ response = model.generate_content(prompt)
901
+ return response.text
902
+
903
+
904
+ def main():
905
+ if 'api_status_time' not in st.session_state:
906
+ st.session_state.api_status_time = None
907
+
908
+ if 'current_section' not in st.session_state:
909
+ st.session_state.current_section = 'weather'
910
+ st.session_state.last_api_call = 0
911
+ st.session_state.weather_data = None
912
+ st.session_state.api_failed = False
913
+ st.session_state.scroll_visible = False
914
+ st.session_state.weather_forecast = ""
915
+
916
+ current_time = datetime.utcnow() + timedelta(hours=9)
917
+ current_timestamp = current_time.timestamp()
918
+
919
+ if 'last_api_call' not in st.session_state:
920
+ st.session_state.last_api_call = 0
921
+
922
+ time_since_last_call = current_timestamp - st.session_state.last_api_call
923
+ retry_interval = 60 if st.session_state.api_failed else 300 # API 실패시 1분, 정상시 5분
924
+
925
+ refresh_placeholder = st.empty()
926
+
927
+ # 네트워크 상태 체크 및 데이터 갱신
928
+ if not st.session_state.weather_data or time_since_last_call >= retry_interval:
929
+ if check_network_status():
930
+ try:
931
+ new_data = get_weather_data()
932
+ if new_data:
933
+ st.session_state.weather_data = new_data
934
+ st.session_state.last_api_call = current_timestamp
935
+ st.session_state.api_failed = False
936
+
937
+ pm10_value = new_data['PM10']
938
+ background_color = get_background_color(pm10_value)
939
+ st.markdown(f"""
940
+ <style>
941
+ .stApp {{
942
+ background-color: {background_color};
943
+ }}
944
+ </style>
945
+ """, unsafe_allow_html=True)
946
+
947
+ st.rerun()
948
+ else:
949
+ st.session_state.api_failed = True
950
+ st.session_state.api_status_time = current_time
951
+
952
+ except Exception as e:
953
+ st.session_state.api_failed = True
954
+ st.session_state.api_status_time = current_time
955
+ st.error(f"Failed to refresh data: {str(e)}")
956
+ else:
957
+ st.warning("현재 네트워크에 문제가 발생했습니다. 데이터 갱신이 불가능합니다.")
958
+
959
+ data = st.session_state.weather_data
960
+ if data:
961
+ pm10_value = data['PM10']
962
+ background_color = get_background_color(pm10_value)
963
+
964
+ st.markdown(f"""
965
+ <style>
966
+ .stApp {{
967
+ background-color: {background_color};
968
+ }}
969
+ </style>
970
+ """, unsafe_allow_html=True)
971
+
972
+ if st.session_state.current_section == 'weather':
973
+ show_weather_info(data)
974
+ else:
975
+ show_temperature_graph(data)
976
+
977
+ # 자동 새로고침을 위한 타이머
978
+ with refresh_placeholder:
979
+ if time_since_last_call >= retry_interval:
980
+ network_ok = check_network_status()
981
+ if network_ok:
982
+ try:
983
+ new_data = get_weather_data()
984
+ if new_data:
985
+ st.session_state.api_failed = False
986
+ st.session_state.weather_data = new_data
987
+ st.session_state.last_api_call = current_timestamp
988
+ st.rerun()
989
+ else:
990
+ st.session_state.api_failed = True
991
+ st.session_state.api_status_time = current_time
992
+ except:
993
+ st.session_state.api_failed = True
994
+ st.session_state.api_status_time = current_time
995
+
996
+ time.sleep(60)
997
+ st.rerun()
998
+
999
+ if __name__ == "__main__":
1000
+ main()