Spaces:
Sleeping
Sleeping
| 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 | |
| from openai import OpenAI | |
| # 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): | |
| client = OpenAI( | |
| api_key="glhf_9ea0e0babe1e45353dd03b44cb979e22", | |
| base_url="https://glhf.chat/api/openai/v1", | |
| http_client=httpx.Client( | |
| follow_redirects=True, | |
| timeout=30.0 | |
| ) | |
| ) | |
| response = client.chat.completions.create( | |
| model="hf:Nexusflow/Athene-V2-Chat", | |
| messages=[ | |
| {"role": "system", "content": "๋น์ ์ ๋ ์จ ์๋ณด๊ด์ ๋๋ค. ์ฃผ์ด์ง ์๊ฐ๋๋ณ ๋ ์จ ๋ฐ์ดํฐ๋ฅผ ๋ฐํ์ผ๋ก ์ ํํ ๋ ์จ ์๋ณด๋ฅผ ์์ฑํด์ฃผ์ธ์."}, | |
| {"role": "user", "content": 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}"""} | |
| ] | |
| ) | |
| return response.choices[0].message.content | |
| 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() |