Spaces:
Running
Running
import streamlit as st | |
import requests | |
import xmltodict | |
import pandas as pd | |
from datetime import datetime, timedelta | |
import streamlit.components.v1 as components | |
import plotly.express as px | |
import time | |
import plotly.io as pio | |
import httpx | |
import google.generativeai as genai | |
import os | |
# API ํค ์ค์ | |
GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY') | |
genai.configure(api_key=GEMINI_API_KEY) | |
# plotly์ JSON ์ง๋ ฌํ ์์ง์ ๊ธฐ๋ณธ json์ผ๋ก ์ค์ | |
pio.json.config.default_engine = 'json' | |
# ํ์ด์ง ์ค์ | |
st.set_page_config( | |
page_title="์ฐ๋ฆฌ์ง ๋ ์จ ์ ๋ณด", | |
page_icon="๐ค๏ธ", | |
layout="wide", | |
menu_items={ | |
'Get Help': None, | |
'Report a bug': None, | |
'About': None | |
} | |
) | |
# CSS ์คํ์ผ ๊ฐ์ | |
st.markdown(""" | |
<style> | |
section[data-testid="stSidebar"] { | |
display: none; | |
} | |
#MainMenu { | |
display: none; | |
} | |
header { | |
display: none; | |
} | |
.block-container { | |
padding: 0 !important; | |
max-width: 100% !important; | |
} | |
.element-container { | |
margin: 0 !important; | |
} | |
.stApp > header { | |
display: none; | |
} | |
#other-info { | |
display: none; | |
} | |
.stPlotlyChart { | |
width: 100%; | |
margin: 0 !important; | |
padding: 0 !important; | |
} | |
[data-testid="stMetricValue"] { | |
font-size: 3rem; | |
} | |
.time-container { | |
width: 100%; | |
text-align: center; | |
margin: 0 auto; | |
padding: 15px 0; | |
} | |
.date-text { | |
font-size: 8em !important; | |
font-weight: bold !important; | |
color: rgb(0, 0, 0) !important; | |
font-family: Arial, sans-serif !important; | |
text-shadow: none !important; | |
background: transparent !important; | |
display: block !important; | |
line-height: 1.2 !important; | |
margin-bottom: 0.5px !important; | |
} | |
h1, h2, h3, h4, h5, h6, p, .stMetric > div > div { | |
color: black !important; | |
} | |
.plotly-graph-div { | |
overflow-x: scroll !important; | |
min-width: 100% !important; | |
} | |
div[data-testid="stVerticalBlock"] > div { | |
padding: 0 !important; | |
} | |
.main { | |
padding: 0 !important; | |
} | |
.stApp { | |
margin: 0 !important; | |
} | |
[data-testid="stHeader"] { | |
display: none; | |
} | |
.section-container { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100vh; | |
padding: 1rem; | |
box-sizing: border-box; | |
background-color: inherit; | |
} | |
.graph-container { | |
width: 100%; | |
height: calc(100vh - 100px); | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
} | |
iframe { | |
margin: 0 !important; | |
padding: 0 !important; | |
} | |
[data-testid="column"] { | |
padding: 0 !important; | |
} | |
[data-testid="stVerticalBlock"] { | |
padding: 0 !important; | |
gap: 0 !important; | |
} | |
.dust-status { | |
font-size: 2em; | |
font-weight: bold; | |
color: black; | |
padding: 0.3rem 1rem; | |
border-radius: 1rem; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
display: inline-block; | |
} | |
@keyframes scroll-text { | |
from { | |
transform: translateX(100%); | |
} | |
to { | |
transform: translateX(-100%); | |
} | |
} | |
.scroll-container { | |
position: fixed; | |
bottom: 20px; | |
left: 0; | |
width: 100%; | |
overflow: hidden; | |
background-color: rgba(255, 255, 255, 0.9); | |
padding: 10px 0; | |
z-index: 1000; | |
} | |
.scroll-text { | |
display: inline-block; | |
white-space: nowrap; | |
animation: scrolling 30s linear infinite; | |
font-size: 2.5em; | |
font-weight: bold; | |
color: #333; | |
position: relative; | |
left: 50%; | |
transform: translateX(-50%); | |
} | |
@keyframes scrolling { | |
0% {transform: translateX(0%); opacity: 0;} | |
10% {opacity: 1;} | |
90% {opacity: 1;} | |
100% {transform: translateX(-100%); opacity: 0;} | |
} | |
/* ๋ชจ๋ฐ์ผ ๋์์ ์ํ CSS ์ถ๊ฐ */ | |
@media (max-width: 600px) { | |
.time-container { | |
font-size: 3em; /* ์ค์ */ | |
} | |
.date-text { | |
font-size: 4em !important; /* ์ค์ */ | |
} | |
.scroll-text { | |
font-size: 1.2em; /* ํฐํธ ํฌ๊ธฐ ์ค์ */ | |
} | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
def get_korean_weekday(date): | |
weekday = date.strftime('%a') | |
weekday_dict = { | |
'Mon': '์', | |
'Tue': 'ํ', | |
'Wed': '์', | |
'Thu': '๋ชฉ', | |
'Fri': '๊ธ', | |
'Sat': 'ํ ', | |
'Sun': '์ผ' | |
} | |
return weekday_dict[weekday] | |
def check_network_status(): | |
try: | |
response = httpx.get("http://www.google.com", timeout=5) | |
return response.status_code == 200 | |
except httpx.RequestError: | |
return False | |
def check_api_status(): | |
try: | |
url = "http://openapi.seoul.go.kr:8088/77544e69764a414d363647424a655a/xml/citydata/1/5/์ ๋ฆผ์ญ" | |
response = requests.get(url, timeout=5) | |
if response.status_code == 200: | |
data = xmltodict.parse(response.text) | |
if data.get('SeoulRtd.citydata', {}).get('RESULT', {}).get('MESSAGE') == "์ ์ ์ฒ๋ฆฌ๋์์ต๋๋ค.": | |
return True | |
return False | |
except: | |
return False | |
def get_weather_data(): | |
url = "http://openapi.seoul.go.kr:8088/77544e69764a414d363647424a655a/xml/citydata/1/5/์ ๋ฆผ์ญ" | |
try: | |
response = requests.get(url, timeout=30) | |
response.raise_for_status() | |
if response.text.strip(): # ์๋ต์ด ๋น์ด์์ง ์์ ๊ฒฝ์ฐ์๋ง ํ์ฑ | |
data = xmltodict.parse(response.text) | |
result = data['SeoulRtd.citydata']['CITYDATA']['WEATHER_STTS']['WEATHER_STTS'] | |
if result: | |
return result | |
except (requests.exceptions.Timeout, | |
requests.exceptions.RequestException, | |
Exception): | |
pass | |
return None | |
def get_background_color(pm10_value): | |
try: | |
pm10 = float(pm10_value) | |
if pm10 <= 30: | |
return "#87CEEB" # ํ๋ (์ข์) | |
elif pm10 <= 80: | |
return "#90EE90" # ์ด๋ก (๋ณดํต) | |
elif pm10 <= 150: | |
return "#FFD700" # ๋ ธ๋ (๋์จ) | |
else: | |
return "#FF6B6B" # ๋นจ๊ฐ (๋งค์ฐ ๋์จ) | |
except: | |
return "#FFFFFF" # ๊ธฐ๋ณธ ํฐ์ | |
def get_current_sky_status(data): | |
current_time = datetime.utcnow() + timedelta(hours=9) | |
current_hour = current_time.hour | |
forecast_data = data['FCST24HOURS']['FCST24HOURS'] | |
if not isinstance(forecast_data, list): | |
forecast_data = [forecast_data] | |
closest_forecast = None | |
min_time_diff = float('inf') | |
for forecast in forecast_data: | |
forecast_hour = int(forecast['FCST_DT'][8:10]) | |
time_diff = abs(forecast_hour - current_hour) | |
if time_diff < min_time_diff: | |
min_time_diff = time_diff | |
closest_forecast = forecast | |
return closest_forecast['SKY_STTS'] if closest_forecast else "์ ๋ณด์์" | |
def format_news_message(news_list): | |
if not isinstance(news_list, list): | |
news_list = [news_list] | |
current_warnings = [] | |
for news in news_list: | |
if not isinstance(news, dict): | |
continue | |
warn_val = news.get('WARN_VAL', '') | |
warn_stress = news.get('WARN_STRESS', '') | |
command = news.get('COMMAND', '') | |
warn_msg = news.get('WARN_MSG', '') | |
announce_time = news.get('ANNOUNCE_TIME', '') | |
if announce_time and len(announce_time) == 12: | |
year = announce_time[0:4] | |
month = announce_time[4:6] | |
day = announce_time[6:8] | |
hour = announce_time[8:10] | |
minute = announce_time[10:12] | |
formatted_time = f"({year}๋ {month}์{day}์ผ{hour}์{minute}๋ถ)" | |
else: | |
formatted_time = "" | |
if command == 'ํด์ ': | |
warning_text = f"โ {warn_val}{warn_stress} ํด์ {formatted_time} {warn_msg}" | |
else: | |
warning_text = f"โ ๏ธ {warn_val}{warn_stress} ๋ฐ๋ น {formatted_time} {warn_msg}" | |
current_warnings.append(warning_text) | |
return ' | '.join(current_warnings) | |
def show_weather_info(data): | |
st.markdown('<div class="section-container">', unsafe_allow_html=True) | |
# Add update time display using the last API call timestamp (already in KST) | |
refresh_time = datetime.fromtimestamp(st.session_state.last_api_call) if st.session_state.last_api_call else (datetime.utcnow() + timedelta(hours=9)) | |
st.markdown(f''' | |
<div style="text-align: center; font-size: 0.8em; color: gray;"> | |
Data refreshed at: {refresh_time.strftime('%Y-%m-%d %H:%M:%S')} | |
</div> | |
''', unsafe_allow_html=True) | |
# Add this code to define formatted_date | |
current_time = datetime.utcnow() + timedelta(hours=9) | |
weekday = get_korean_weekday(current_time) | |
formatted_date = f"{current_time.strftime('%Y-%m-%d')}({weekday})" | |
pm10 = float(data['PM10']) | |
if pm10 <= 30: | |
dust_status = "์ข์" | |
dust_color = "#87CEEB" # Blue | |
elif pm10 <= 80: | |
dust_status = "๋ณดํต" | |
dust_color = "#90EE90" # Green | |
elif pm10 <= 150: | |
dust_status = "๋์จ" | |
dust_color = "#FFD700" # Yellow | |
else: | |
dust_status = "๋งค์ฐ๋์จ" | |
dust_color = "#FF6B6B" # Red | |
temp = data.get('TEMP', "์ ๋ณด์์") | |
precip_type = data.get('PRECPT_TYPE', "์ ๋ณด์์") | |
try: | |
temp = f"{float(temp):.1f}ยฐC" | |
except: | |
temp = "์ ๋ณด์์" | |
# ํ์ฌ ์๊ฐ ๊ธฐ์ค์ผ๋ก ๊ฐ์ฅ ๊ฐ๊น์ด 06์ ๋ฐ์ดํฐ ์ฐพ๊ธฐ | |
morning_six_data = None | |
current_time = datetime.utcnow() + timedelta(hours=9) # KST | |
forecast_data = data['FCST24HOURS']['FCST24HOURS'] | |
if not isinstance(forecast_data, list): | |
forecast_data = [forecast_data] | |
for fcst in forecast_data: | |
fcst_hour = int(fcst['FCST_DT'][8:10]) # HH | |
if fcst_hour == 6: | |
fcst_datetime = datetime.strptime(fcst['FCST_DT'], '%Y%m%d%H%M') | |
if fcst_datetime > current_time: | |
morning_six_data = fcst | |
break | |
# 06์ ๋ ์จ ์ ๋ณด ์ค๋น | |
tomorrow_morning_weather = "์์" | |
if morning_six_data: | |
tomorrow_temp = morning_six_data['TEMP'] | |
weather_icon = "" | |
# PRECPT_TYPE ๋จผ์ ํ์ธ | |
precip_type = morning_six_data['PRECPT_TYPE'] | |
if precip_type == "๋น" or precip_type == "๋น/๋": | |
weather_icon = "โ" | |
elif precip_type == "๋": | |
weather_icon = '<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">โ</span>' | |
# PRECPT_TYPE์ด '์์'์ด๋ฉด SKY_STTS ๊ธฐ๋ฐ์ผ๋ก ์์ด์ฝ ์ค์ | |
else: | |
if morning_six_data['SKY_STTS'] == "๋ง์": | |
weather_icon = "๐" | |
elif morning_six_data['SKY_STTS'] in ["๊ตฌ๋ฆ", "๊ตฌ๋ฆ๋ง์"]: | |
weather_icon = "โ " | |
elif morning_six_data['SKY_STTS'] == "ํ๋ฆผ": | |
weather_icon = '<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">โ</span>' | |
tomorrow_morning_weather = f"{tomorrow_temp}ยฐC {weather_icon}" | |
# ํ๋ฉด์ ํ์ | |
weather_icon = "" | |
current_time_str = current_time.strftime('%Y%m%d%H') | |
# Check current precipitation type first | |
if data['PRECPT_TYPE'] in ["๋น", "๋", "๋น/๋", "๋น๋ฐฉ์ธ"]: | |
if data['PRECPT_TYPE'] in ["๋น", "๋น๋ฐฉ์ธ"]: | |
weather_icon = "โ" | |
elif data['PRECPT_TYPE'] == "๋": | |
weather_icon = '<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">โ</span>' | |
elif data['PRECPT_TYPE'] == "๋น/๋": | |
weather_icon = 'โ<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">โ</span>' | |
else: | |
# Find nearest forecast time when no current precipitation | |
nearest_forecast = None | |
min_time_diff = float('inf') | |
for forecast in forecast_data: | |
forecast_time = datetime.strptime(forecast['FCST_DT'], '%Y%m%d%H%M') | |
time_diff = abs((forecast_time - current_time).total_seconds()) | |
if time_diff < min_time_diff: | |
min_time_diff = time_diff | |
nearest_forecast = forecast | |
if nearest_forecast: | |
if nearest_forecast['PRECPT_TYPE'] in ["๋น", "๋", "๋น/๋", "๋น๋ฐฉ์ธ"]: | |
if nearest_forecast['PRECPT_TYPE'] in ["๋น", "๋น๋ฐฉ์ธ"]: | |
weather_icon = "โ" | |
elif nearest_forecast['PRECPT_TYPE'] == "๋": | |
weather_icon = '<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">โ</span>' | |
elif nearest_forecast['PRECPT_TYPE'] == "๋น/๋": | |
weather_icon = 'โ<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">โ</span>' | |
else: | |
# Use SKY_STTS when no precipitation | |
sky_status = nearest_forecast['SKY_STTS'] | |
if sky_status == "๋ง์": | |
weather_icon = "๐" | |
elif sky_status in ["๊ตฌ๋ฆ", "๊ตฌ๋ฆ๋ง์"]: | |
weather_icon = "โ " | |
elif sky_status == "ํ๋ฆผ": | |
weather_icon = '<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">โ</span>' | |
precip_mark = weather_icon | |
st.markdown(f''' | |
<div class="time-container"> | |
<div style="text-align: center; margin-bottom: 0.5rem; font-size: 6em; font-weight: bold; color: black;"> | |
{temp}{precip_mark} {tomorrow_morning_weather} | |
</div> | |
<span class="date-text">{formatted_date}</span> | |
</div> | |
''', unsafe_allow_html=True) | |
clock_html = """ | |
<div style="width: 100%; max-width: 1200px; margin: 0 auto; padding: 0 20px;"> | |
<div style="text-align: center; height: 300px; display: flex; align-items: center; justify-content: center;"> | |
<span id="clock" style="font-size: 15em; font-weight: bold; color: black; line-height: 1.2; white-space: nowrap;"></span> | |
</div> | |
</div> | |
<script> | |
function updateClock() { | |
const now = new Date(); | |
const options = { | |
timeZone: 'Asia/Seoul', | |
hour12: true, | |
hour: 'numeric', | |
minute: '2-digit' | |
}; | |
document.getElementById('clock').textContent = now.toLocaleTimeString('ko-KR', options); | |
} | |
setInterval(updateClock, 1000); | |
updateClock(); | |
</script> | |
""" | |
components.html(clock_html, height=300) | |
# ๋ ์จ ์๋ณด ์์ฑ ๋ฐ ์คํฌ๋กค ์ปจํ ์ด๋ ํ์ | |
col1, col2, col3, col4 = st.columns([1, 1, 1, 2]) | |
with col1: | |
if st.button("๋ ์จ ์๋ณด ์คํฌ๋กค", key="toggle_scroll"): | |
st.session_state.scroll_visible = not st.session_state.scroll_visible | |
# ๋ ์จ ์๋ณด ์์ฑ | |
forecast_data = data['FCST24HOURS']['FCST24HOURS'] | |
if not isinstance(forecast_data, list): | |
forecast_data = [forecast_data] | |
forecast_data_str = "\n".join([ | |
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']}" | |
for f in forecast_data | |
]) | |
current_time = datetime.utcnow() + timedelta(hours=9) | |
current_time_str = current_time.strftime('%H์ %M๋ถ') | |
# ๋ ์จ ์๋ณด ํ ์คํธ ์์ฑ | |
st.session_state.weather_forecast = get_weather_forecast(forecast_data_str, current_time_str) | |
# ์คํฌ๋กค ์ปจํ ์ด๋ CSS | |
background_color = get_background_color(data['PM10']) | |
display_style = "block" if st.session_state.scroll_visible else "none" | |
scroll_style = f""" | |
background-color: rgba(255, 255, 255, 0.9); | |
color: #333; | |
display: {display_style}; | |
position: fixed; | |
bottom: 20px; | |
left: 0; | |
width: 100%; | |
overflow: hidden; | |
padding: 10px 0; | |
z-index: 1000; | |
""" | |
text_style = """ | |
white-space: nowrap; | |
animation: scroll-text 30s linear infinite; | |
display: inline-block; | |
font-size: 2.5em; | |
font-weight: bold; | |
""" | |
# ์คํฌ๋กค ์ปจํ ์ด๋ ํ์ | |
st.markdown(f''' | |
<div class="scroll-container" style="{scroll_style}"> | |
<div class="scroll-text" style="{text_style}">{st.session_state.weather_forecast}</div> | |
</div> | |
''', unsafe_allow_html=True) | |
with col2: | |
st.button("์๊ฐ๋๋ณ ์จ๋ ๋ณด๊ธฐ", on_click=lambda: st.session_state.update({'current_section': 'temperature'})) | |
# API ์๋ต ์ฒดํฌ ๋ฒํผ ๋ถ๋ถ ์์ | |
with col3: | |
if st.button("API ์๋ต ์ฒดํฌ"): | |
if check_api_status(): | |
st.session_state.api_failed = False | |
new_data = get_weather_data() | |
if new_data: | |
st.session_state.weather_data = new_data | |
st.session_state.last_api_call = datetime.utcnow().timestamp() | |
st.rerun() | |
# session_state์ API ์คํจ ์๊ฐ ์ ์ฅ์ ์ํ ๋ณ์ ์ถ๊ฐ | |
if 'api_failed_time' not in st.session_state: | |
st.session_state.api_failed_time = None | |
with col4: | |
network_ok = check_network_status() | |
if not network_ok: | |
status_color = "#FF0000" | |
status_text = "๋คํธ์ํฌ ์ฐ๊ฒฐ ์์" | |
else: | |
current_time = datetime.utcnow() + timedelta(hours=9) # KST | |
if not st.session_state.api_failed: | |
status_color = "#00AA00" | |
st.session_state.api_status_time = current_time | |
status_time = st.session_state.api_status_time.strftime('%Y-%m-%d %H:%M') | |
status_text = f"API ์ ์({status_time} ์ฑ๊ณต)" | |
else: | |
status_color = "#FF0000" | |
if st.session_state.api_status_time is None: | |
st.session_state.api_status_time = current_time | |
status_time = st.session_state.api_status_time.strftime('%Y-%m-%d %H:%M') | |
status_text = f"API ์๋ต ์์({status_time} ๋ฐ์)" | |
# API ์ํ ํ์๋ฅผ ์ํ ๊ณ ์ ํ ํด๋์ค๋ฅผ ์ฌ์ฉ | |
st.markdown(""" | |
<style> | |
.api-status { | |
color: %s !important; | |
font-size: 20px; | |
font-weight: bold; | |
} | |
</style> | |
<p class="api-status">%s</p> | |
""" % (status_color, status_text), unsafe_allow_html=True) | |
# forecast_data ์ฒ๋ฆฌ | |
forecast_data = data['FCST24HOURS']['FCST24HOURS'] | |
if not isinstance(forecast_data, list): | |
forecast_data = [forecast_data] | |
times = [] | |
temps = [] | |
weather_descriptions = [] | |
for forecast in forecast_data: | |
times.append(forecast['FCST_DT'][8:10] + "์") | |
temps.append(float(forecast['TEMP'])) | |
sky_status = forecast['SKY_STTS'] | |
precip_type = forecast['PRECPT_TYPE'] | |
if precip_type == "๋น": | |
description = "๋น" | |
elif precip_type == "๋": | |
description = "๋" | |
elif precip_type == "๋น/๋": | |
description = "๋น/๋" | |
elif sky_status == "๋ง์": | |
description = "๋ง์" | |
elif sky_status in ["๊ตฌ๋ฆ", "๊ตฌ๋ฆ๋ง์"]: | |
description = "๊ตฌ๋ฆ" if sky_status == "๊ตฌ๋ฆ" else "๊ตฌ๋ฆ๋ง์" | |
elif sky_status == "ํ๋ฆผ": | |
description = "ํ๋ฆผ" | |
else: | |
description = "์ ๋ณด์์" | |
weather_descriptions.append(description) | |
# ์คํฌ๋กค ์ปจํ ์ด๋ ํ์ | |
background_color = get_background_color(data['PM10']) | |
display_style = "block" if st.session_state.scroll_visible else "none" | |
scroll_style = f""" | |
background-color: rgba(255, 255, 255, 0.9); | |
color: #333; | |
display: {display_style}; | |
""" | |
# ์ ์ฅ๋ ๋ ์จ ์๋ณด ํ์ | |
st.markdown(f''' | |
<div class="scroll-container" style="{scroll_style}"> | |
<div class="scroll-text">{st.session_state.weather_forecast}</div> | |
</div> | |
''', unsafe_allow_html=True) | |
st.markdown('</div>', unsafe_allow_html=True) | |
def show_temperature_graph(data): | |
st.markdown('<div class="section-container">', unsafe_allow_html=True) | |
st.markdown('<h1 style="text-align: center; margin-bottom: 1rem;">์๊ฐ๋๋ณ ์จ๋</h1>', unsafe_allow_html=True) | |
forecast_data = data['FCST24HOURS']['FCST24HOURS'] | |
if not isinstance(forecast_data, list): | |
forecast_data = [forecast_data] | |
# Sort forecast data by FCST_DT to ensure correct time ordering | |
forecast_data = sorted(forecast_data, key=lambda x: x['FCST_DT']) | |
# ํ์ฌ ์๊ฐ ๊ธฐ์ค์ผ๋ก ์ ํจํ ์๋ณด ๋ฐ์ดํฐ๋ง ํํฐ๋ง | |
current_time = datetime.utcnow() + timedelta(hours=9) # KST | |
current_date = current_time.strftime('%Y%m%d') | |
next_date = (current_time + timedelta(days=1)).strftime('%Y%m%d') | |
# ํ์ฌ ์๊ฐ ์ดํ์ ์๋ณด ๋ฐ์ดํฐ์ ๋ค์ ๋ ์ ๋ฐ์ดํฐ ๋ชจ๋ ํฌํจ | |
valid_forecast_data = [] | |
for fcst in forecast_data: | |
fcst_date = fcst['FCST_DT'][:8] # YYYYMMDD | |
fcst_hour = int(fcst['FCST_DT'][8:10]) # HH | |
current_hour = current_time.hour | |
# ํ์ฌ ๋ ์ง์ ํ์ฌ ์๊ฐ ์ดํ ๋ฐ์ดํฐ ๋๋ ๋ค์ ๋ ์ ๋ฐ์ดํฐ | |
if (fcst_date == current_date and fcst_hour >= current_hour) or fcst_date == next_date: | |
valid_forecast_data.append(fcst) | |
# ์ ํจํ ๋ฐ์ดํฐ๊ฐ ์์ผ๋ฉด ์ ์ฒด ๋ฐ์ดํฐ ์ฌ์ฉ | |
if not valid_forecast_data: | |
valid_forecast_data = forecast_data | |
# ํ์ฌ ์๊ฐ๊ณผ ๊ฐ์ฅ ๊ฐ๊น์ด ์๋ณด ์๊ฐ ์ฐพ๊ธฐ | |
current_time = datetime.utcnow() + timedelta(hours=9) | |
# ๋ น์ ์ธ๋ก์ ์ถ๊ฐ ๋ฐ "ํ์ฌ" ํ ์คํธ ํ์ - ์ด์ ํญ์ ์ฒซ ๋ฒ์งธ ๋ฐ์ดํฐ ํฌ์ธํธ์ ํ์ | |
time_differences = [] | |
for fcst in valid_forecast_data: | |
forecast_time = datetime.strptime(fcst['FCST_DT'], '%Y%m%d%H%M') | |
time_diff = abs((forecast_time - current_time).total_seconds()) | |
time_differences.append(time_diff) | |
current_index = time_differences.index(min(time_differences)) | |
# Reorder forecast data to start from current time | |
valid_forecast_data = valid_forecast_data[current_index:] + valid_forecast_data[:current_index] | |
times = [] | |
temps = [] | |
weather_icons = [] | |
weather_descriptions = [] | |
date_changes = [] | |
for i, forecast in enumerate(valid_forecast_data): | |
time_str = forecast['FCST_DT'] | |
date = time_str[6:8] | |
hour = time_str[8:10] | |
if i > 0 and valid_forecast_data[i-1]['FCST_DT'][6:8] != date: | |
date_changes.append(i) | |
times.append(f"{hour}์") | |
temps.append(float(forecast['TEMP'])) | |
sky_status = forecast['SKY_STTS'] | |
precip_type = forecast['PRECPT_TYPE'] | |
if precip_type == "๋น": | |
icon = "โ" | |
description = "๋น" | |
elif precip_type == "๋": | |
icon = '<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">โ</span>' | |
description = "๋" | |
elif precip_type == "๋น/๋": | |
icon = 'โ<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">โ</span>' | |
description = "๋น/๋" | |
elif sky_status == "๋ง์": | |
icon = "๐" | |
description = "๋ง์" | |
elif sky_status in ["๊ตฌ๋ฆ", "๊ตฌ๋ฆ๋ง์"]: | |
icon = "โ " | |
description = "๊ตฌ๋ฆ" if sky_status == "๊ตฌ๋ฆ" else "๊ตฌ๋ฆ<br>๋ง์" | |
elif sky_status == "ํ๋ฆผ": | |
icon = '<span style="color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);">โ</span>' | |
description = "ํ๋ฆผ" | |
else: | |
icon = "โ๏ธ" | |
description = "์ ๋ณด์์" | |
weather_icons.append(icon) | |
weather_descriptions.append(description) | |
df = pd.DataFrame({ | |
'์๊ฐ': times, | |
'๊ธฐ์จ': temps, | |
'๋ ์จ': weather_icons, | |
'์ค๋ช ': weather_descriptions, | |
'FCST_DT': [f['FCST_DT'] for f in valid_forecast_data] | |
}) | |
fig = px.line(df, x='์๊ฐ', y='๊ธฐ์จ', markers=True) | |
# Add nighttime overlay (18:00-06:00) | |
for i in range(len(times)): | |
hour = int(times[i].replace('์', '')) | |
if hour >= 18 or hour < 6: | |
fig.add_vrect( | |
x0=times[i], | |
x1=times[i+1] if i < len(times)-1 else times[-1], | |
fillcolor='rgba(0, 0, 0, 0.1)', | |
layer='below', | |
line_width=0, | |
annotation_text="", | |
annotation_position="top left" | |
) | |
# ๋ น์ ์ธ๋ก์ ์ถ๊ฐ ๋ฐ "ํ์ฌ" ํ ์คํธ ํ์ | |
fig.add_vline(x=times[0], line_width=2, line_dash="dash", line_color="green") | |
fig.add_annotation( | |
x=times[0], | |
y=max(temps) + 4, | |
text="<b>ํ์ฌ</b>", | |
showarrow=True, | |
arrowhead=2, | |
) | |
bold_times = ["00์", "06์", "12์", "18์", "24์"] | |
for time in bold_times: | |
if time in times: | |
index = times.index(time) | |
fig.add_annotation( | |
x=time, | |
y=min(temps) - 3, | |
text=time, | |
showarrow=False, | |
font=dict(size=30, color="black", family="Arial") | |
) | |
fig.add_vline(x='12์', line_width=2, line_dash="dash", line_color="rgba(0,0,0,0.5)") | |
# ์ค๋๊ณผ ๋ด์ผ, ์ค์ ๊ณผ ์คํ ํ ์คํธ๋ ํด๋น ์๊ฐ๋์ ๋ฐ์ดํฐ๊ฐ ์์ ๋๋ง ํ์ | |
time_set = set(times) | |
current_date = datetime.utcnow() + timedelta(hours=9) # KST | |
current_hour = current_date.hour | |
if '11์' in time_set: | |
fig.add_annotation(x='11์', y=max(temps) + 4, text="์ค์ ", showarrow=False, font=dict(size=24)) | |
if '13์' in time_set: | |
fig.add_annotation(x='13์', y=max(temps) + 4, text="์คํ", showarrow=False, font=dict(size=24)) | |
# ์๊ฐ ์์๋๋ก ์ ๋ ฌ๋ ๋ฐ์ดํฐ๋ผ๊ณ ๊ฐ์ | |
for i, time in enumerate(times): | |
hour = int(time.replace('์', '')) | |
# ํ์ฌ ์๊ฐ์ด 23์์ด๊ณ , times[0]์ด 00์๋ผ๋ฉด ์ฒซ ๋ฒ์งธ 23์๊ฐ ์ค๋ 23์ | |
if hour == 23 and times[0] == '00์': | |
if i == 0: # ์ฒซ ๋ฒ์งธ 23์ (์ค๋ 23์) | |
fig.add_annotation(x=time, y=max(temps) + 4, text="์ค๋", showarrow=False, font=dict(size=24)) | |
# 01์๋ ๋ค์ ๋ ์ด๋ฏ๋ก "๋ด์ผ" ํ์ (00์ ๋ค์์ ์ค๋ 01์) | |
if hour == 1 and i > 0 and times[i-1] == '00์': | |
fig.add_annotation(x=time, y=max(temps) + 4, text="๋ด์ผ", showarrow=False, font=dict(size=24)) | |
fig.update_traces( | |
line_color='#FF6B6B', | |
marker=dict(size=10, color='#FF6B6B'), | |
textposition="top center", | |
mode='lines+markers+text', | |
text=[f"<b>{int(round(temp))}ยฐ</b>" for temp in df['๊ธฐ์จ']], | |
textfont=dict(size=24) | |
) | |
for i, (icon, description) in enumerate(zip(weather_icons, weather_descriptions)): | |
fig.add_annotation( | |
x=times[i], | |
y=max(temps) + 3, | |
text=f"{icon}", | |
showarrow=False, | |
font=dict(size=30) | |
) | |
fig.add_annotation( | |
x=times[i], | |
y=max(temps) + 2, | |
text=f"{description}", | |
showarrow=False, | |
font=dict(size=16), | |
textangle=0 | |
) | |
for date_change in date_changes: | |
fig.add_vline( | |
x=times[date_change], | |
line_width=2, | |
line_dash="dash", | |
line_color="rgba(255, 0, 0, 0.7)" | |
) | |
fig.update_layout( | |
title=None, | |
xaxis_title='', | |
yaxis_title=None, #'๊ธฐ์จ (ยฐC)', | |
height=600, | |
width=7200, | |
showlegend=False, | |
plot_bgcolor='rgba(255,255,255,0.9)', | |
paper_bgcolor='rgba(0,0,0,0)', | |
margin=dict(l=50, r=50, t=0, b=0), | |
xaxis=dict( | |
tickangle=0, | |
tickfont=dict(size=14), | |
gridcolor='rgba(0,0,0,0.1)', | |
dtick=1, | |
tickmode='array', | |
ticktext=[f"{i:02d}์" for i in range(24)], | |
tickvals=[f"{i:02d}์" for i in range(24)] | |
), | |
yaxis=dict( | |
tickfont=dict(size=14), | |
gridcolor='rgba(0,0,0,0.1)', | |
showticklabels=True, | |
tickformat='d', | |
ticksuffix='ยฐC', | |
automargin=True, | |
rangemode='tozero' | |
) | |
) | |
st.plotly_chart(fig, use_container_width=True) | |
# ๋ ์จ ์๋ณด ์์ฑ ๋ฐ ํ์ ๋ถ๋ถ์ ์ธ์ ์ํ๋ก ๊ด๋ฆฌ | |
if 'weather_forecast' not in st.session_state: | |
forecast_data_str = "\n".join([ | |
f"[{f['FCST_DT'][:4]}๋ {f['FCST_DT'][4:6]}์ {f['FCST_DT'][6:8]}์ผ {f['FCST_DT'][8:10]}์] {temp}๋, {description}" | |
for f, time, temp, description in zip(valid_forecast_data, times, temps, weather_descriptions) | |
]) | |
current_time_str = current_time.strftime('%H์ %M๋ถ') | |
st.session_state.weather_forecast = get_weather_forecast(forecast_data_str, current_time_str) | |
# ์ ์ฅ๋ ๋ ์จ ์๋ณด ํ์ | |
st.markdown(f''' | |
<div class="scroll-container"> | |
<div class="scroll-text">{st.session_state.weather_forecast}</div> | |
</div> | |
''', unsafe_allow_html=True) | |
# ์คํฌ๋กค ํ ์คํธ ์์ ๋ฒํผ์ด ์ค๋๋ก ๋ง์ง ์ถ๊ฐ | |
st.markdown(''' | |
<div style="margin-bottom: 10px;"> | |
''', unsafe_allow_html=True) | |
# ์ฐ๋ฆฌ์ง ๋ ์จ ์ ๋ณด๋ก ๋์๊ฐ๊ธฐ ๋ฒํผ ์ถ๊ฐ | |
st.button("์ฐ๋ฆฌ์ง ๋ ์จ ์ ๋ณด๋ก ๋์๊ฐ๊ธฐ", on_click=lambda: st.session_state.update({'current_section': 'weather'})) | |
st.markdown('</div>', unsafe_allow_html=True) | |
# 5๋ถ ์บ์ | |
def get_weather_forecast(forecast_data_str, current_time_str): | |
# Gemini ๋ชจ๋ธ ์ค์ | |
model = genai.GenerativeModel('gemini-2.0-flash-exp') | |
prompt = f"""ํ์ฌ ์๊ฐ์ {current_time_str}์ ๋๋ค. | |
๋ค์ FCST_DT์ ์๊ฐ๋๋ณ ๋ ์จ ๋ฐ์ดํฐ๋ฅผ ๋ณด๊ณ ์ค์ ๋ ์จ ์ํฉ์ ๋ง๋ ์ ํํ ๋ ์จ ์๋ณด๋ฅผ 200์์ ์์ฐ์ค๋ฌ์ด ๋ฌธ์ฅ์ผ๋ก ๋ง๋ค์ด์ฃผ์ธ์. | |
๋น๋ ๋ ์๋ณด๊ฐ ์๋ ๊ฒฝ์ฐ์๋ง ์ฐ์ฐ์ ์ค๋นํ๋๋ก ์๋ดํด์ฃผ์ธ์. | |
์ท์ฐจ๋ฆผ ๊ธฐ์ค: | |
27ยฐC์ด์: ๋ฐํํฐ, ๋ฐ๋ฐ์ง, ๋ฏผ์๋งค | |
23ยฐC~26ยฐC: ์์ ์ ์ธ , ๋ฐํํฐ, ๋ฐ๋ฐ์ง, ๋ฉด๋ฐ์ง | |
20ยฐC~22ยฐC: ์์ ๊ฐ๋๊ฑด, ๊ธดํํฐ, ๊ธด๋ฐ์ง | |
17ยฐC~19ยฐC: ์์ ๋ํธ, ๊ฐ๋๊ฑด, ๋งจํฌ๋งจ, ์์ ์์ผ, ๊ธด๋ฐ์ง | |
12ยฐC~16ยฐC: ์์ผ, ๊ฐ๋๊ฑด, ์ผ์, ๋งจํฌ๋งจ, ๋ํธ, ์คํํน, ๊ธด๋ฐ์ง | |
9ยฐC~11ยฐC: ํธ๋ ์น์ฝํธ, ์ผ์, ๊ฐ์ฃฝ ์์ผ, ์คํํน, ๊ธด๋ฐ์ง | |
5ยฐC~8ยฐC: ์ฝํธ, ํํธํ , ๋ํธ, ๊ธด๋ฐ์ง | |
4ยฐC์ดํ: ํจ๋ฉ, ๋๊บผ์ด ์ฝํธ, ๋ชฉ๋๋ฆฌ, ๊ธฐ๋ชจ์ ํ | |
์๊ฐ๋๋ณ ๋ ์จ ๋ฐ์ดํฐ: | |
{forecast_data_str} | |
""" | |
response = model.generate_content(prompt) | |
return response.text | |
def main(): | |
if 'api_status_time' not in st.session_state: | |
st.session_state.api_status_time = None | |
if 'current_section' not in st.session_state: | |
st.session_state.current_section = 'weather' | |
st.session_state.last_api_call = 0 | |
st.session_state.weather_data = None | |
st.session_state.api_failed = False | |
st.session_state.scroll_visible = False | |
st.session_state.weather_forecast = "" | |
current_time = datetime.utcnow() + timedelta(hours=9) | |
current_timestamp = current_time.timestamp() | |
if 'last_api_call' not in st.session_state: | |
st.session_state.last_api_call = 0 | |
time_since_last_call = current_timestamp - st.session_state.last_api_call | |
retry_interval = 60 if st.session_state.api_failed else 300 # API ์คํจ์ 1๋ถ, ์ ์์ 5๋ถ | |
refresh_placeholder = st.empty() | |
# ๋คํธ์ํฌ ์ํ ์ฒดํฌ ๋ฐ ๋ฐ์ดํฐ ๊ฐฑ์ | |
if not st.session_state.weather_data or time_since_last_call >= retry_interval: | |
if check_network_status(): | |
try: | |
new_data = get_weather_data() | |
if new_data: | |
st.session_state.weather_data = new_data | |
st.session_state.last_api_call = current_timestamp | |
st.session_state.api_failed = False | |
pm10_value = new_data['PM10'] | |
background_color = get_background_color(pm10_value) | |
st.markdown(f""" | |
<style> | |
.stApp {{ | |
background-color: {background_color}; | |
}} | |
</style> | |
""", unsafe_allow_html=True) | |
st.rerun() | |
else: | |
st.session_state.api_failed = True | |
st.session_state.api_status_time = current_time | |
except Exception as e: | |
st.session_state.api_failed = True | |
st.session_state.api_status_time = current_time | |
st.error(f"Failed to refresh data: {str(e)}") | |
else: | |
st.warning("ํ์ฌ ๋คํธ์ํฌ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค. ๋ฐ์ดํฐ ๊ฐฑ์ ์ด ๋ถ๊ฐ๋ฅํฉ๋๋ค.") | |
data = st.session_state.weather_data | |
if data: | |
pm10_value = data['PM10'] | |
background_color = get_background_color(pm10_value) | |
st.markdown(f""" | |
<style> | |
.stApp {{ | |
background-color: {background_color}; | |
}} | |
</style> | |
""", unsafe_allow_html=True) | |
if st.session_state.current_section == 'weather': | |
show_weather_info(data) | |
else: | |
show_temperature_graph(data) | |
# ์๋ ์๋ก๊ณ ์นจ์ ์ํ ํ์ด๋จธ | |
with refresh_placeholder: | |
if time_since_last_call >= retry_interval: | |
network_ok = check_network_status() | |
if network_ok: | |
try: | |
new_data = get_weather_data() | |
if new_data: | |
st.session_state.api_failed = False | |
st.session_state.weather_data = new_data | |
st.session_state.last_api_call = current_timestamp | |
st.rerun() | |
else: | |
st.session_state.api_failed = True | |
st.session_state.api_status_time = current_time | |
except: | |
st.session_state.api_failed = True | |
st.session_state.api_status_time = current_time | |
time.sleep(60) | |
st.rerun() | |
if __name__ == "__main__": | |
main() |