Gemini_Weather / app.py
JAMESPARK3's picture
Update app.py
b128ab7 verified
raw
history blame
36.1 kB
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
@st.cache_data(ttl=300)
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} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {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)
@st.cache_data(ttl=300) # 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()