Delete app.py
Browse files
app.py
DELETED
|
@@ -1,1238 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import streamlit as st
|
| 3 |
-
import random
|
| 4 |
-
import time
|
| 5 |
-
import pandas as pd
|
| 6 |
-
from datetime import date
|
| 7 |
-
import plotly.express as px # 그래프 라이브러리 추가
|
| 8 |
-
from openai import OpenAI
|
| 9 |
-
import json
|
| 10 |
-
from supabase import create_client, Client
|
| 11 |
-
|
| 12 |
-
# --- Streamlit 설정 ---
|
| 13 |
-
st.set_page_config(
|
| 14 |
-
page_title="뉴스 보고, 주식 잡고! 모의 투자 게임", # 최종 앱 타이틀 적용
|
| 15 |
-
page_icon="💰",
|
| 16 |
-
layout="wide",
|
| 17 |
-
initial_sidebar_state="expanded",
|
| 18 |
-
)
|
| 19 |
-
|
| 20 |
-
# --- Custom CSS (스타일링) ---
|
| 21 |
-
st.markdown(
|
| 22 |
-
"""
|
| 23 |
-
<style>
|
| 24 |
-
/* 전체 폰트 변경 (Nanum Gothic, Google Fonts CDN 사용) */
|
| 25 |
-
@import url('https://fonts.googleapis.com/css2?family=Nanum+Gothic:wght@400;700&display=swap');
|
| 26 |
-
body {
|
| 27 |
-
font-family: 'Nanum Gothic', sans-serif !important;
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
/* 탭 메뉴 스타일 */
|
| 31 |
-
.stTabs [data-baseweb="tab-list"] button[aria-selected="true"] {
|
| 32 |
-
background-color: #007bff !important;
|
| 33 |
-
color: white !important;
|
| 34 |
-
font-weight: bold;
|
| 35 |
-
}
|
| 36 |
-
.stTabs [data-baseweb="tab-list"] button {
|
| 37 |
-
background-color: #f0f2f6;
|
| 38 |
-
color: #333;
|
| 39 |
-
border-radius: 8px 8px 0 0;
|
| 40 |
-
padding: 0.75em 1em;
|
| 41 |
-
margin-bottom: -1px; /* border overlap */
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
/* 사이드바 스타일 */
|
| 45 |
-
[data-testid="stSidebar"] {
|
| 46 |
-
width: 350px !important;
|
| 47 |
-
background-color: #f8f9fa; /* Light gray sidebar background */
|
| 48 |
-
padding: 20px;
|
| 49 |
-
}
|
| 50 |
-
[data-testid="stSidebar"] h1, [data-testid="stSidebar"] h3 {
|
| 51 |
-
color: #212529; /* Dark gray sidebar headings */
|
| 52 |
-
}
|
| 53 |
-
[data-testid="stSidebar"] hr {
|
| 54 |
-
border-top: 1px solid #e0e0e0; /* Lighter sidebar hr */
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
/* Metric 스타일 */
|
| 58 |
-
.streamlit-metric-label {
|
| 59 |
-
font-size: 16px;
|
| 60 |
-
color: #4a4a4a;
|
| 61 |
-
}
|
| 62 |
-
.streamlit-metric-value {
|
| 63 |
-
font-size: 28px;
|
| 64 |
-
font-weight: bold;
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
/* 버튼 스타일 */
|
| 68 |
-
div.stButton > button {
|
| 69 |
-
background-color: #007bff;
|
| 70 |
-
color: white;
|
| 71 |
-
padding: 12px 24px;
|
| 72 |
-
font-size: 16px;
|
| 73 |
-
border-radius: 8px;
|
| 74 |
-
border: none;
|
| 75 |
-
box-shadow: 2px 2px 5px rgba(0,0,0,0.1); /* Soft shadow */
|
| 76 |
-
transition: background-color 0.3s ease;
|
| 77 |
-
}
|
| 78 |
-
div.stButton > button:hover {
|
| 79 |
-
background-color: #0056b3;
|
| 80 |
-
box-shadow: 2px 2px 7px rgba(0,0,0,0.15); /* Slightly stronger shadow on hover */
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
/* 보조 버튼 스타일 */
|
| 84 |
-
div.stButton > button.secondary-button {
|
| 85 |
-
background-color: #6c757d;
|
| 86 |
-
color: white;
|
| 87 |
-
padding: 10px 20px;
|
| 88 |
-
font-size: 14px;
|
| 89 |
-
border-radius: 6px;
|
| 90 |
-
border: none;
|
| 91 |
-
transition: background-color 0.3s ease;
|
| 92 |
-
}
|
| 93 |
-
div.stButton > button.secondary-button:hover {
|
| 94 |
-
background-color: #5a6268;
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
/* Expander 스타일 */
|
| 98 |
-
.streamlit-expanderHeader {
|
| 99 |
-
font-weight: bold;
|
| 100 |
-
color: #212529;
|
| 101 |
-
border-bottom: 1px solid #e0e0e0;
|
| 102 |
-
padding-bottom: 8px;
|
| 103 |
-
margin-bottom: 15px;
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
/* Dataframe 스타일 */
|
| 107 |
-
.dataframe {
|
| 108 |
-
border: 1px solid #e0e0e0;
|
| 109 |
-
border-radius: 8px;
|
| 110 |
-
padding: 12px;
|
| 111 |
-
box-shadow: 2px 2px 5px rgba(0,0,0,0.05); /* Very subtle shadow */
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
/* Info, Success, Error, Warning Box 스타일 (더 부드러운 스타일) */
|
| 115 |
-
div.stInfo, div.stSuccess, div.stError, div.stWarning {
|
| 116 |
-
border-radius: 8px;
|
| 117 |
-
padding: 15px;
|
| 118 |
-
margin-bottom: 15px;
|
| 119 |
-
box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
|
| 120 |
-
}
|
| 121 |
-
div.stInfo {
|
| 122 |
-
background-color: #e7f3ff;
|
| 123 |
-
border-left: 5px solid #007bff;
|
| 124 |
-
}
|
| 125 |
-
div.stSuccess {
|
| 126 |
-
background-color: #e6f7ec;
|
| 127 |
-
border-left: 5px solid #28a745;
|
| 128 |
-
}
|
| 129 |
-
div.stError {
|
| 130 |
-
background-color: #fdeded;
|
| 131 |
-
border-left: 5px solid #dc3545;
|
| 132 |
-
}
|
| 133 |
-
div.stWarning {
|
| 134 |
-
background-color: #fffbe6;
|
| 135 |
-
border-left: 5px solid #ffc107;
|
| 136 |
-
}
|
| 137 |
-
|
| 138 |
-
/* Toast message 스타일 */
|
| 139 |
-
div.streamlit-toast-container {
|
| 140 |
-
z-index: 10000; /* Toast를 항상 맨 위에 표시 */
|
| 141 |
-
}
|
| 142 |
-
div[data-testid="stToast"] {
|
| 143 |
-
border-radius: 8px;
|
| 144 |
-
padding: 15px;
|
| 145 |
-
box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
</style>
|
| 149 |
-
""",
|
| 150 |
-
unsafe_allow_html=True,
|
| 151 |
-
)
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
# --- API 키 설정 (Hugging Face Secrets에서 관리 권장) ---
|
| 155 |
-
if "OPENAI_API_KEY" not in os.environ: # 환경 변수 이름 변경
|
| 156 |
-
st.error(
|
| 157 |
-
"OPENAI_API_KEY 환경 변수가 설정되지 않았습니다. Hugging Face Secrets 또는 환경 변수에 API 키를 설정해주세요." # 에러 메시지 변경
|
| 158 |
-
)
|
| 159 |
-
st.stop()
|
| 160 |
-
|
| 161 |
-
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) # OpenAI 클라이언트 초기화
|
| 162 |
-
|
| 163 |
-
# --- 세션 상태 초기화 (Streamlit 앱 상태 관리) ---
|
| 164 |
-
if "chat_session" not in st.session_state:
|
| 165 |
-
st.session_state["chat_session"] = [] # OpenAI는 채팅 세션 관리가 다름. 메시지 리스트로 관리
|
| 166 |
-
if "portfolio" not in st.session_state:
|
| 167 |
-
st.session_state["portfolio"] = {"cash": 10000000, "stocks": {}}
|
| 168 |
-
if "stocks" not in st.session_state:
|
| 169 |
-
st.session_state["stocks"] = { # 섹터별 종목 재구성 및 설명 확장
|
| 170 |
-
"기술(Tech)": {
|
| 171 |
-
"삼성전자": {
|
| 172 |
-
"current_price": random.randint(50000, 80000),
|
| 173 |
-
"price_history": [],
|
| 174 |
-
"description": "대한민국을 대표하는 전자 제품 회사, 삼성전자! 텔레비전, 스마트폰, 냉장고, 세탁기, 컴퓨터 칩 등 우리 생활에 필요한 다양한 제품들을 만들고 있어요. 특히 갤럭시 스마트폰은 전 세계에서 아주 인기가 많고, 텔레비전은 최고 화질로 유명해요. 반도체 기술도 세계 최고 수준이라서, 컴퓨터나 스마트폰의 두뇌 역할을 하는 칩을 만들어 다른 회사들에게도 팔고 있답니다. 우리나라 경제 발전에 아주 큰 역할을 하는 회사예요.",
|
| 175 |
-
},
|
| 176 |
-
"SK하이닉스": {
|
| 177 |
-
"current_price": random.randint(80000, 120000),
|
| 178 |
-
"price_history": [],
|
| 179 |
-
"description": "컴퓨터와 스마트폰의 기억력을 책임지는 SK하이닉스! 우리가 사용하는 컴퓨터나 스마트폰이 사진, 영상, 게임 같은 정보를 저장하고 빠르게 불러올 수 있는 건 SK하이닉스 덕분이에요. 이 회사는 ‘DRAM’과 ‘NAND 플래시’라는 아주 중요한 반도체를 만드는데, 이 반도체들은 컴퓨터, 스마트폰뿐만 아니라 인공지능, 빅데이터, 자율주행차 같은 미래 기술에도 꼭 필요하답니다. 세계적으로 손꼽히는 반도체 기술력을 가진 회사예요.",
|
| 180 |
-
},
|
| 181 |
-
"LG디스플레이": {
|
| 182 |
-
"current_price": random.randint(20000, 40000),
|
| 183 |
-
"price_history": [],
|
| 184 |
-
"description": "화면을 더욱 선명하게, LG디스플레이! 우리가 매일 보는 텔레비전, 스마트폰, 노트북 화면을 만드는 회사예요. LG디스플레이는 특히 ‘OLED’라는 특별한 기술로 화면을 만드는데, OLED는 색깔이 진짜처럼 선명하고, 얇고 가벼워서 미래 디스플레이 기술로 주목받고 있어요. 영화관처럼 생생한 화질의 텔레비전, 얇고 예쁜 스마트폰 화면, 자동차 계기판과 투명 디스플레이까지, LG디스플레이 기술은 우리 생활 곳곳에 사용되고 있답니다.",
|
| 185 |
-
},
|
| 186 |
-
},
|
| 187 |
-
"자동차(Auto)": {
|
| 188 |
-
"현대자동차": {
|
| 189 |
-
"current_price": random.randint(150000, 250000),
|
| 190 |
-
"price_history": [],
|
| 191 |
-
"description": "대한민국 대표 자동차 회사, 현대자동차! 우리가 타고 다니는 자동차를 만드는 회사 중 가장 유명해요. 쏘나타, 아반떼, 팰리세이드, 아이오닉 등 멋진 이름의 자동차들을 디자인하고 만들어서 우리나라뿐 아니라 전 세계에 팔고 있어요. 최근에는 전기자동차와 수소자동차 같은 친환경 자동차를 개발해서 미래 자동차 시장을 이끌고 있답니다. 자동차를 좋아하는 친구라면 누구나 한 번쯤 들어봤을 이름일 거예요.",
|
| 192 |
-
},
|
| 193 |
-
"기아": {
|
| 194 |
-
"current_price": random.randint(70000, 100000),
|
| 195 |
-
"price_history": [],
|
| 196 |
-
"description": "개성 넘치는 디자인, 기아자동차! 현대자동차와 함께 우리나라 자동차 산업을 이끌고 있어요. K3, K5, 쏘렌토, 스포티지, EV6, EV9 등 이름만 들어도 멋진 자동차들을 만들고 있어요. 기아자동차는 특히 디자인이 예쁘기로 유명하고, 젊은 친구들에게 인기가 많아요. 최근에는 전기차 EV6와 EV9이 세계적으로 디자인 상을 많이 받아서 더욱 유명해졌답니다. 나만의 개성을 표현하고 싶은 친구들에게 딱 맞는 자동차 회사예요.",
|
| 197 |
-
},
|
| 198 |
-
"현대모비스": {
|
| 199 |
-
"current_price": random.randint(200000, 250000),
|
| 200 |
-
"price_history": [],
|
| 201 |
-
"description": "자동차를 튼튼하게, 안전하게, 현대모비스! 자동차 회사는 아니지만, 자동차를 만드는 데 꼭 필요한 부품들을 전문적으로 만드는 회사예요. 자동차의 심장인 엔진 부품부터, 안전을 지켜주는 브레이크, 에어백, 운전을 편리하게 해주는 첨단 장치까지, 자동차 30000여 개 부품을 만들어요. 현대자동차, 기아뿐 아니라 전 세계 자동차 회사에 부품을 공급하는 아주 중요한 회사랍니다. 겉으로 잘 보이지 않지만, 자동차의 안전과 성능을 책임지는 숨은 영웅 같은 회사예요.",
|
| 202 |
-
},
|
| 203 |
-
},
|
| 204 |
-
"에너지(Energy)": {
|
| 205 |
-
"LG에너지솔루션": {
|
| 206 |
-
"current_price": random.randint(300000, 500000),
|
| 207 |
-
"price_history": [],
|
| 208 |
-
"description": "미래 에너지를 만드는 LG에너지솔루션! 우리가 타고 다니는 전기자동차에 꼭 필요한 배터리를 만드는 회사 중 세계 1등이에요. 전기차 배터리뿐 아니라, 스마트폰, 노트북, 에너지 저장 장치(ESS) 등 다양한 곳에 사용되는 배터리를 만들어요. 태양광, 풍력 같은 친환경 에너지를 더욱 효율적으로 사용할 수 있도록 돕는 기술을 개발하고 있답니다. 지구를 깨끗하게 만드는 데 아주 중요한 역할을 하는 회사예요.",
|
| 209 |
-
},
|
| 210 |
-
"SK이노베이션": {
|
| 211 |
-
"current_price": random.randint(100000, 150000),
|
| 212 |
-
"price_history": [],
|
| 213 |
-
"description": "에너지와 화학의 힘, SK이노베이션! 우리가 사용하는 휘발유, 경유 같은 기름을 만들고, 플라스틱, 옷, 타이어 같은 다양한 제품의 원료가 되는 화학 제품도 만들어요. 최근에는 전기차 배터리 사업을 키워서 미래 에너지 시대를 준비하고 있답니다. 오래전부터 우리나라 에너지 산업을 이끌어온 회사이고, 지금은 친환경 에너지 회사로 변신하고 있어요.",
|
| 214 |
-
},
|
| 215 |
-
"두산에너빌리티": {
|
| 216 |
-
"current_price": random.randint(15000, 25000),
|
| 217 |
-
"price_history": [],
|
| 218 |
-
"description": "힘찬 에너지를 만드는 두산에너빌리티! 우리가 사용하는 전기를 만드는 발전소를 짓고, 발전소에 필요한 기계를 만드는 회사예요. 화력 발전소, 원자력 발전소, 수력 발전소, 풍력 발전소 등 다양한 발전소를 건설하고, 바닷물을 깨끗한 물로 바꾸는 해수담수화 설비도 만들어요. 최근에는 친환경 에너지 기술을 개발해서 지구를 위한 깨끗한 에너지를 만드는 데 힘쓰고 있답니다. 우리나라 전력 공급에 아주 중요한 역할을 하는 회사예요.",
|
| 219 |
-
},
|
| 220 |
-
},
|
| 221 |
-
"인터넷(Internet)": {
|
| 222 |
-
"네이버": {
|
| 223 |
-
"current_price": random.randint(200000, 300000),
|
| 224 |
-
"price_history": [],
|
| 225 |
-
"description": "궁금한 건 뭐든지 물어봐, 네이버! 우리나라에서 가장 유명한 인터넷 검색 엔진 '네이버'를 만드는 회사예요. 검색뿐 아니라 뉴스, 쇼핑, 블로그, 카페, 웹툰, 지도, 번역 등 다양한 인터넷 서비스를 제공하고 있어요. 우리가 매일 사용하는 카카오톡처럼, 라인(LINE)이라는 메신저 앱을 만들어서 해외에서도 인기가 많답니다. 우리나라 인터넷 세상을 만들어가는 대표적인 회사예요.",
|
| 226 |
-
},
|
| 227 |
-
"카카오": {
|
| 228 |
-
"current_price": random.randint(40000, 60000),
|
| 229 |
-
"price_history": [],
|
| 230 |
-
"description": "세상을 연결하는 즐거움, 카카오! 국민 메신저 '카카오톡'을 만든 회사예요. 카카오톡뿐 아니라 카카오택시, 카카오페이, 카카오게임, 카카오웹툰, 카카오뱅크, 카카오맵 등 우리 생활을 편리하고 즐겁게 만들어주는 다양한 서비스를 만들고 있어요. 귀여운 카카오프렌즈 캐릭터도 아주 인기가 많죠? 우리나라 사람들의 하루를 카카오 서비스로 시작해서 카카오 서비스로 끝난다고 할 정도로, 우리 생활에 아주 깊숙이 들어와 있는 회사예요.",
|
| 231 |
-
},
|
| 232 |
-
"카카오뱅크": {
|
| 233 |
-
"current_price": random.randint(20000, 30000),
|
| 234 |
-
"price_history": [],
|
| 235 |
-
"description": "내 손안의 은행, 카카오뱅크! 카카오톡을 만든 카카오에서 만든 특별한 은행이에요. 은행에 직접 가지 않아도 스마트폰 앱으로 계좌를 만들고, 돈을 보내고, 대출도 받을 수 있어요. 복잡한 서류 없이 간편하게 이용할 수 있고, 24시간 언제든지 은행 업무를 볼 수 있다는 장점이 있어요. 은행을 딱딱하고 어렵게 생각하지 않고, 쉽고 재미있게 이용할 수 있도록 도와주는 은행이에요.",
|
| 236 |
-
},
|
| 237 |
-
},
|
| 238 |
-
"소비재(Consumer Goods)": {
|
| 239 |
-
"CJ제일제당": {
|
| 240 |
-
"current_price": random.randint(300000, 400000),
|
| 241 |
-
"price_history": [],
|
| 242 |
-
"description": "맛있는 식탁을 책임지는 CJ제일제당! 우리가 먹는 맛있는 음식들을 만드는 회사예요. 햇반, 비비고, 고메, 백설, 다시다 등 유명한 식품 브랜드를 많이 가지고 있어요. 김치, 만두, 햇반 같은 간편 식품부터, 밀가루, 설탕, 식용유 같은 요리 재료까지, 우리의 식탁을 풍요롭게 만들어주는 다양한 식품들을 만들어요. 영화관에서 먹는 팝콘, 뚜레쥬르 빵, 투썸플레이스 케이크도 CJ제일제당에서 만들어요.",
|
| 243 |
-
},
|
| 244 |
-
"아모레퍼시픽": {
|
| 245 |
-
"current_price": random.randint(130000, 170000),
|
| 246 |
-
"price_history": [],
|
| 247 |
-
"description": "예뻐지는 마법, 아모레퍼시픽! 우리나라 대표 화장품 회사예요. 설화수, 라네즈, 마몽드, 이니스프리, 에뛰드하우스 등 다양한 화장품 브랜드를 만들어서, 아름다움을 꿈꾸는 사람들을 도와주고 있어요. 화장품뿐 아니라 샴푸, 치약, 바디워시 같은 생활용품도 만들고, 녹차, 건강기능식품 사업도 하고 있답니다. 우리나라 여성들의 아름다움을 책임지는 회사라고 할 수 있어요.",
|
| 248 |
-
},
|
| 249 |
-
"LG생활건강": {
|
| 250 |
-
"current_price": random.randint(600000, 800000),
|
| 251 |
-
"price_history": [],
|
| 252 |
-
"description": "깨끗하고 아름다운 생활, LG생활건강! 우리 생활에 필요한 다양한 제품들을 만드는 회사예요. 샴푸, 린스, 비누, 치약, 세제 같은 생활용품부터, 오휘, 숨37°, 빌리프, 더페이스샵 같은 화장품 브랜드까지, 우리 생활을 더욱 깨끗하고 아름답게 만들어주는 제품들을 만들어요. 코카콜라, 스프라이트, 환타 같은 음료수도 LG생활건강에서 판매하고 있답니다. 우리 생활 곳곳에서 만날 수 있는 친근한 회사예요.",
|
| 253 |
-
},
|
| 254 |
-
},
|
| 255 |
-
"금융(Finance)": {
|
| 256 |
-
"KB금융": {
|
| 257 |
-
"current_price": random.randint(50000, 60000),
|
| 258 |
-
"price_history": [],
|
| 259 |
-
"description": "든든한 금융 파트너, KB금융! 우리나라 대표 금융 회사 중 하나예요. KB국민은행, KB증권, KB손해보험, KB국민카드 등 다양한 금융 회사를 가지고 있어서, 은행, 증권, 보험, 카드 등 다양한 금융 서비스를 제공하고 있어요. 우리나라 사람들이 가장 많이 이용하는 은행 중 하나인 KB국민은행을 운영하고 있고, 집을 살 때 돈을 빌려주는 주택담보대출도 많이 해주는 회사예요. 우리나라 경제를 튼튼하게 만드는 데 중요한 역할을 하고 있어요.",
|
| 260 |
-
},
|
| 261 |
-
"신한지주": {
|
| 262 |
-
"current_price": random.randint(30000, 40000),
|
| 263 |
-
"price_history": [],
|
| 264 |
-
"description": "금융을 새롭게, 신한지주! KB금융과 함께 우리나라 대표 금융 회사로 손꼽혀요. 신한은행, 신한카드, 신한금융투자, 신한생명 등 다양한 금융 회사를 가지고 있어서, 은행, 카드, 증권, 보험 등 모든 금융 서비스를 제공하고 있어요. 특히 젊은 고객들을 위한 다양한 금융 상품과 서비스를 개발하고 있고, 해외 시장에도 적극적으로 진출하고 있답니다. 빠르게 변화하는 금융 시장을 이끌어가는 회사예요.",
|
| 265 |
-
},
|
| 266 |
-
"하나금융지주": {
|
| 267 |
-
"current_price": random.randint(40000, 50000),
|
| 268 |
-
"price_history": [],
|
| 269 |
-
"description": "금융으로 더 나은 미래, 하나금융지주! 우리나라 대표 금융 회사 중 하나예요. 하나은행, 하나증권, 하나카드, 하나생명 등 금융 회사를 가지고 있어서, 은행, 증권, 카드, 보험 등 금융 서비스를 제공하고 있어요. 외국 돈을 사고파는 외환 거래를 오랫동안 해왔고, 해외 투자와 관련된 금융 서비스도 잘 제공하는 회사예요. 글로벌 금융 시장에서 활약하는 회사라고 할 수 있어요.",
|
| 270 |
-
},
|
| 271 |
-
},
|
| 272 |
-
"건설(Construction)": {
|
| 273 |
-
"삼성물산": {
|
| 274 |
-
"current_price": random.randint(100000, 150000),
|
| 275 |
-
"price_history": [],
|
| 276 |
-
"description": "세계를 건설하는 힘, 삼성물산! 삼성 그룹의 뿌리이자, 건설, 상사, 패션, 리조트 등 다양한 사업을 하는 회사예요. 우리나라 랜드마크 건물인 부르즈 할리파, 페트로나스 트윈 타워 건설에 참여했고, 인천국제공항, 싱가포르 지하철 같은 큰 프로젝트들을 많이 했어요. 건설뿐 아니라 옷을 만들고 팔기도 하고 (빈폴, 갤럭시), 에버랜드, 호텔신라 같은 리조트도 운영하는 다재다능한 회사예요.",
|
| 277 |
-
},
|
| 278 |
-
"HD현대": {
|
| 279 |
-
"current_price": random.randint(40000, 60000),
|
| 280 |
-
"price_history": [],
|
| 281 |
-
"description": "바다를 개척하는 HD현대! 배를 만들고, 건설 기계를 만드는 회사예요. 울산에 있는 큰 조선소에서 아주 큰 배들을 만들고, 굴착기, 지게차 같은 건설 현장에서 볼 수 있는 노란색 기계들도 만들어요. 최근에는 로봇, 인공지능 기술을 개발해서 건설 현장을 더욱 스마트하게 만드는 기술을 개발하고 있답니다. 우리나라 조선 산업과 건설 기계 산업을 이끌어가는 회사예요.",
|
| 282 |
-
},
|
| 283 |
-
"GS건설": {
|
| 284 |
-
"current_price": random.randint(30000, 50000),
|
| 285 |
-
"price_history": [],
|
| 286 |
-
"description": "행복을 짓는 GS건설! 우리가 사는 아파트 '자이'를 만드는 회사예요. 자이 아파트는 살기 좋은 아파트로 유명하고, 우리나라 아파트 브랜드 중에서 인기가 많아요. 아파트뿐 아니라 다리, 도로, 터널 같은 사회 기반 시설도 건설하고, 해외에서도 다양한 건설 프로젝트를 하고 있답니다. 우리나라 주거 문화를 만들어가는 대표적인 건설 회사예요.",
|
| 287 |
-
},
|
| 288 |
-
},
|
| 289 |
-
"유통(Retail)": {
|
| 290 |
-
"롯데쇼핑": {
|
| 291 |
-
"current_price": random.randint(150000, 250000),
|
| 292 |
-
"price_history": [],
|
| 293 |
-
"description": "쇼핑의 즐거움, 롯데쇼핑! 우리나라 대표 유통 회사예요. 롯데백화점, 롯데마트, 롯데슈퍼, 롯데아울렛, 롯데ON 등 다양한 쇼핑 공간을 운영하고 있어요. 옷, 화장품, 식품, 가전제품 등 없는 게 없는 백화점부터, 저렴하고 신선한 식재료를 살 수 있는 마트까지, 우리의 쇼핑 생활을 책임지고 있어요. 영화관 롯데시네마, 테마파크 롯데월드도 롯데쇼핑에서 운영해요.",
|
| 294 |
-
},
|
| 295 |
-
"이마트": {
|
| 296 |
-
"current_price": random.randint(100000, 150000),
|
| 297 |
-
"price_history": [],
|
| 298 |
-
"description": "생활 필수품은 모두 다, 이마트! 우리나라 대표 대형 할인 마트예요. 집에서 사용하는 거의 모든 물건을 살 수 있다고 생각하면 돼요. 신선한 채소, 과일, 고기 같은 식품부터, 세제, 샴푸, 휴지 같은 생활용품, 옷, 장난감, 가전제품까지 정말 다양한 상품을 팔고 있어요. 이마트 자체 브랜드인 '노브랜드', '피코크' 제품들도 인기가 많고, 온라인 쇼핑몰 'SSG닷컴'도 운영하고 있답니다. 우리나라 사람들의 장보기 문화를 대표하는 곳이에요.",
|
| 299 |
-
},
|
| 300 |
-
},
|
| 301 |
-
"통신(Telecom)": {
|
| 302 |
-
"KT": {
|
| 303 |
-
"current_price": random.randint(30000, 40000),
|
| 304 |
-
"price_history": [],
|
| 305 |
-
"description": "빠르고 편리한 통신, KT! 우리나라 대표 통신 회사예요. 집에서 사용하는 인터넷, 스마트폰으로 사용하는 이동통신, 텔레비전 방송(IPTV), 기업들이 사용하는 IT 솔루션 등 다양한 통신 서비스를 제공하고 있어요. 오래전부터 우리나라 통신 산업을 이끌어왔고, 지금도 5G, 인공지능 같은 새로운 기술을 개발해서 더욱 편리한 통신 세상을 만들고 있답니다. 우리나라 정보 통신 발전에 큰 역할을 하는 회사예요.",
|
| 306 |
-
},
|
| 307 |
-
"SK텔레콤": {
|
| 308 |
-
"current_price": random.randint(50000, 70000),
|
| 309 |
-
"price_history": [],
|
| 310 |
-
"description": "무선 통신의 강자, SK텔레콤! 우리나라 대표 통신 회사이고, 특히 이동통신 서비스에서 1등이에요. 스마트폰으로 데이터를 빠르게 사용할 수 있도록 5G, LTE 같은 무선 통신 기술을 개발하고, 인공지능, 메타버스 같은 미래 기술에도 투자하고 있어요. 우리가 스마트폰으로 영상 통화를 하고, 게임을 하고, 유튜브를 볼 수 있는 건 SK텔레콤 덕분이라고 할 수 있어요. 우리나라 무선 통신 기술을 이끌어가는 회사예요.",
|
| 311 |
-
},
|
| 312 |
-
},
|
| 313 |
-
"제약/바이오(Pharma/Bio)": {
|
| 314 |
-
"삼성바이오로직스": {
|
| 315 |
-
"current_price": random.randint(700000, 900000),
|
| 316 |
-
"price_history": [],
|
| 317 |
-
"description": "생명을 소중하게, 삼성바이오로직스! 약은 약인데, 그냥 약이 아니라 아주 특별한 ‘바이오 의약품’을 만드는 회사예요. 우리 몸속 세포를 이용해서 만드는 바이오 의약품은 병을 치료하는 힘이 아주 세다고 해요. 삼성바이오로직스는 다른 제약 회사들을 위해 바이오 의약품을 대신 만들어주는 일을 전문으로 하고 있어요. 공장을 아주 크게 지어서, 최첨단 설비로 최고 품질의 바이오 의약품을 만들고 있답니다. 아픈 사람들을 위한 희망을 만드는 회사라고 할 수 있어요.",
|
| 318 |
-
},
|
| 319 |
-
"셀트리온": {
|
| 320 |
-
"current_price": random.randint(180000, 250000),
|
| 321 |
-
"price_history": [],
|
| 322 |
-
"description": "바이오 의약품으로 질병과 싸우는 셀트리온! 삼성바이오로직스처럼 바이오 의약품을 만드는 회사인데, 셀트리온은 직접 새로운 바이오 의약품을 개발하고, 만들어서 전 세계에 팔고 있어요. 관절염, 암, 자가면역질환 같은 무서운 병들을 치료하는 바이오 의약품을 만들고 있고, 저렴한 가격으로 바이오 의약품을 만들어서 더 많은 사람들이 치료받을 수 있도록 노력하고 있답니다. 바이오 의약품 분야에서 우리나라를 대표하는 회사예요.",
|
| 323 |
-
},
|
| 324 |
-
},
|
| 325 |
-
"화학(Chemical)": {
|
| 326 |
-
"LG화학": {
|
| 327 |
-
"current_price": random.randint(600000, 800000),
|
| 328 |
-
"price_history": [],
|
| 329 |
-
"description": "생활 속 화학, LG화학! 우리가 매일 사용하는 플라스틱, 옷, 신발, 건전지, 자동차 배터리, 화장품 원료까지 정말 다양한 화학 제품을 만드는 회사예요. 눈에 보이지 않지만 우리 생활 곳곳에 LG화학 제품들이 사용되고 있답니다. 최근에는 친환경 플라스틱, 전기차 배터리 소재 같은 미래 기술 개발에도 힘쓰고 있어요. 우리나라 화학 산업을 이끌어가는 대표적인 회사예요.",
|
| 330 |
-
},
|
| 331 |
-
"금호석유화학": {
|
| 332 |
-
"current_price": random.randint(120000, 180000),
|
| 333 |
-
"price_history": [],
|
| 334 |
-
"description": "산업의 기초 소재, 금호석유화학! 자동차 타이어, 건축 자재, 포장재, 장갑, 운동화 밑창 등 다양한 제품의 원료가 되는 합성고무를 만드는 회사예요. 합성고무는 천연고무보다 더 튼튼하고, 다양한 기능을 가질 수 있어서 산업 현장에서 아주 많이 사용된답니다. 우리나라 합성고무 산업을 처음 시작했고, 지금도 세계적인 기술력을 가지고 있어요. 산업 발전에 꼭 필요한 숨은 영웅 같은 회사예요.",
|
| 335 |
-
},
|
| 336 |
-
},
|
| 337 |
-
"철강(Steel)": {
|
| 338 |
-
"POSCO홀딩스": {
|
| 339 |
-
"current_price": random.randint(300000, 400000),
|
| 340 |
-
"price_history": [],
|
| 341 |
-
"description": "철강으로 나라를 튼튼하게, POSCO홀딩스! 우리나라 대표 철강 회사이고, 세계적으로도 아주 큰 철강 회사예요. 자동차, 배, 건물, 다리, 기차, 가전제품 등 우리 생활 곳곳에 사용되는 철강 제품을 만들어요. 철강은 튼튼하고 튼튼해서 오랫동안 사용할 수 있고, 재활용도 잘 돼서 친환경적인 소재이기도 해요. 우리나라 산업 발전에 없어서는 안 될 중요한 회사예요.",
|
| 342 |
-
},
|
| 343 |
-
"현대제철": {
|
| 344 |
-
"current_price": random.randint(50000, 70000),
|
| 345 |
-
"price_history": [],
|
| 346 |
-
"description": "자동차와 건설의 뼈대, 현대제철! 현대자동차 그룹의 철강 회사이고, 자동차와 건설에 사용되는 철강 제품을 전문적으로 만들어요. 자동차 차체를 튼튼하게 만드는 철판, 건물을 짓는 뼈대 역할을 하는 철근, 배를 만드는 데 사용하는 후판 등 다양한 철강 제품을 만들어요. 최근에는 친환경 철강 제조 기술을 개발해서 더욱 깨끗한 환경을 만드는 데 노력하고 있답니다. 현대자동차 그룹의 성장에 큰 힘이 되는 회사예요.",
|
| 347 |
-
},
|
| 348 |
-
},
|
| 349 |
-
"운송(Transportation)": {
|
| 350 |
-
"대한항공": {
|
| 351 |
-
"current_price": random.randint(20000, 30000),
|
| 352 |
-
"price_history": [],
|
| 353 |
-
"description": "하늘을 나는 꿈, 대한항공! 우리나라 대표 항공사이고, 가장 많은 비행기를 가지고 있어요. 우리나라에서 다른 나라로 여행을 가거나, 다른 나라에서 우리나라로 여행을 올 때 대한항공 비행기를 많이 이용해요. 사람뿐 아니라 소중한 물건들을 안전하고 빠르게 전 세계로 운송하는 일도 하고 있답니다. 비행기 조종사, 승무원을 꿈꾸는 친구들이라면 누구나 가고 싶어 하는 회사일 거예요.",
|
| 354 |
-
},
|
| 355 |
-
"HMM": {
|
| 356 |
-
"current_price": random.randint(20000, 30000),
|
| 357 |
-
"price_history": [],
|
| 358 |
-
"description": "바다를 누비는 HMM! 우리나라 대표 해운 회사이고, 아주 큰 배들을 많이 가지고 있어요. 우리가 사용하는 물건들은 대부분 배를 통해서 다른 나라에서 우리나라로, 우리나라에서 다른 나라로 이동한답니다. HMM은 컨테이너선이라는 큰 배로 물건들을 실어 나르는 일을 전문으로 하고 있어요. 우리나라와 전 세계를 연결하는 중요한 역할을 하는 회사예요.",
|
| 359 |
-
},
|
| 360 |
-
},
|
| 361 |
-
"엔터테인먼트(Entertainment)": {
|
| 362 |
-
"CJ ENM": {
|
| 363 |
-
"current_price": random.randint(80000, 120000),
|
| 364 |
-
"price_history": [],
|
| 365 |
-
"description": "즐거움을 디자인하는 CJ ENM! 텔레비전 방송, 영화, 음악, 공연 등 다양한 엔터테인먼트 사업을 하는 회사예요. tvN, Mnet, OCN 같은 유명한 텔레비전 채널을 운영하고 있고, '기생충', '부산행', '겨울왕국 2' 같은 유명한 영화들을 만들거나 투자했어요. 마마, KCON 같은 큰 음악 행사도 만들고, 뮤지컬, 연극 공연도 제작하는 등 우리 생활에 즐거움을 주는 다양한 문화 콘텐츠를 만들고 있어요.",
|
| 366 |
-
},
|
| 367 |
-
"하이브": {
|
| 368 |
-
"current_price": random.randint(200000, 300000),
|
| 369 |
-
"price_history": [],
|
| 370 |
-
"description": "음악으로 세상을 감동시키는 하이브! 전 세계적으로 엄청난 인기를 누리고 있는 방탄소년단(BTS)을 키운 회사예요. BTS뿐 아니라 투모로우바이투게더(TXT), 세븐틴, 르세라핌, 뉴진스 등 인기 아이돌 그룹들이 많이 소속되어 있어요. 음반 제작, 매니지먼트, 공연뿐 아니라 게임, 웹툰, 교육 사업까지 확장해서 다양한 분야에서 즐거움을 주고 있답니다. 우리나라 대중문화를 세계에 알리는 데 큰 역할을 하는 회사예요.",
|
| 371 |
-
},
|
| 372 |
-
},
|
| 373 |
-
"식품(Food)": {
|
| 374 |
-
"오리온": {
|
| 375 |
-
"current_price": random.randint(120000, 180000),
|
| 376 |
-
"price_history": [],
|
| 377 |
-
"description": "맛있는 과자, 오리온! 우리나라 대표 과자 회사이고, 초코파이, 오!감자, 포카칩, 꼬북칩, 고래밥 등 맛있고 재미있는 과자들을 많이 만들어요. 어린이부터 어른까지 누구나 좋아하는 과자들을 만들어서, 우리나라뿐 아니라 중국, 러시아, 베트남 등 해외에서도 인기가 많답니다. 과자를 좋아하는 친구라면 오리온 과자를 한 번쯤 먹어봤을 거예요.",
|
| 378 |
-
},
|
| 379 |
-
"농심": {
|
| 380 |
-
"current_price": random.randint(300000, 400000),
|
| 381 |
-
"price_history": [],
|
| 382 |
-
"description": "국민 라면, 농심! 우리나라 대표 라면 회사이고, 신라면, 안성탕면, 짜파게티, 너구리, 새우깡 등 오랜 시간 동안 사랑받는 라면과 스낵들을 많이 만들어요. 매콤한 신라면, 구수한 안성탕면, 달콤 짭짤한 짜파게티, 얼큰한 너구리, 고소한 새우깡 등 다양한 맛과 종류의 라면과 스낵을 만들어서, 우리나라 사람들의 입맛을 즐겁게 해주고 있어요. 라면을 좋아하는 친구라면 농심 라면을 꼭 먹어봤을 거예요.",
|
| 383 |
-
},
|
| 384 |
-
},
|
| 385 |
-
}
|
| 386 |
-
for sector in st.session_state["stocks"]: # 초기 price_history 채우기 (섹터별로 순회)
|
| 387 |
-
for stock_name in st.session_state["stocks"][sector]:
|
| 388 |
-
st.session_state["stocks"][sector][stock_name]["price_history"].append(
|
| 389 |
-
st.session_state["stocks"][sector][stock_name]["current_price"]
|
| 390 |
-
)
|
| 391 |
-
|
| 392 |
-
if "news_analysis_results" not in st.session_state:
|
| 393 |
-
st.session_state["news_analysis_results"] = {}
|
| 394 |
-
if "messages" not in st.session_state:
|
| 395 |
-
st.session_state["messages"] = []
|
| 396 |
-
if "daily_news" not in st.session_state:
|
| 397 |
-
st.session_state["daily_news"] = None
|
| 398 |
-
if "previous_daily_news" not in st.session_state:
|
| 399 |
-
st.session_state["previous_daily_news"] = None
|
| 400 |
-
if "news_date" not in st.session_state:
|
| 401 |
-
st.session_state["news_date"] = None
|
| 402 |
-
if "news_meanings" not in st.session_state:
|
| 403 |
-
st.session_state["news_meanings"] = {}
|
| 404 |
-
if (
|
| 405 |
-
"ai_news_analysis_output" not in st.session_state
|
| 406 |
-
):
|
| 407 |
-
st.session_state["ai_news_analysis_output"] = {}
|
| 408 |
-
if "day_count" not in st.session_state:
|
| 409 |
-
st.session_state["day_count"] = 1
|
| 410 |
-
if "sector_news_impact" not in st.session_state:
|
| 411 |
-
st.session_state["sector_news_impact"] = {}
|
| 412 |
-
if 'buy_confirm' not in st.session_state:
|
| 413 |
-
st.session_state['buy_confirm'] = False
|
| 414 |
-
if 'sell_confirm' not in st.session_state:
|
| 415 |
-
st.session_state['sell_confirm'] = False
|
| 416 |
-
if 'difficulty_level' not in st.session_state:
|
| 417 |
-
st.session_state['difficulty_level'] = "초등학생" # 기본 난이도 설정
|
| 418 |
-
|
| 419 |
-
# --- 뉴스 난이도별 설정 ---
|
| 420 |
-
difficulty_prompt_map = {
|
| 421 |
-
"초등학생": {
|
| 422 |
-
"level_desc": "초등학생 5~6학년 수준",
|
| 423 |
-
"sentence_count": "8~10", # 더 짧은 문장
|
| 424 |
-
"vocabulary_level": "아주 쉬운 어휘와 짧고 명확한 문장 사용", # 더 쉬운 어휘 강조
|
| 425 |
-
"inference_level": "매우 단순하고 직관적인 정보", # 더 단순한 추론
|
| 426 |
-
"news_prompt_instructions": "단순한 경제 상황, 쉬운 단어, 짧은 문장, 명확한 정보 위주로 작성. 복잡한 경제 용어, 전문 용어, 추상적인 개념 사용 금지.", # 초등학생용 뉴스 생성 지침 추가
|
| 427 |
-
"explanation_prompt_instructions": "핵심 용어, 쉬운 비유 사용, 짧고 명확한 설명, 3문장 이내 요약", # 초등학생용 해설 지침 추가
|
| 428 |
-
},
|
| 429 |
-
"중학생": {
|
| 430 |
-
"level_desc": "중학생 1~3학년 수준",
|
| 431 |
-
"sentence_count": "12~15",
|
| 432 |
-
"vocabulary_level": "일상적인 어휘와 기본적인 경제 용어 포함", # 중학생 수준 어휘
|
| 433 |
-
"inference_level": "일반적인 경제 흐름과 관련된 추론",
|
| 434 |
-
"news_prompt_instructions": "일상적인 경제 뉴스, 쉬운 경제 용어, 적절한 문장 길이, 일반적인 경제 흐름과 관련, 약간의 추론 필요", # 중학생용 뉴스 생성 지침
|
| 435 |
-
"explanation_prompt_instructions": "기본 경제 용어 설명, 경제 개념 쉬운 풀이, 3~4문장 요약", # 중학생용 해설 지침
|
| 436 |
-
},
|
| 437 |
-
"고등학생": {
|
| 438 |
-
"level_desc": "고등학생 1~3학년 수준",
|
| 439 |
-
"sentence_count": "15~20", # 고등학생 수준 문장 수 증가
|
| 440 |
-
"vocabulary_level": "다양한 어휘와 경제 전문 용어 적극 사용", # 고등학생 수준 어휘
|
| 441 |
-
"inference_level": "심층적인 경제 분석 및 다각적인 추론 요구", # 고등학생 수준 심층 추론
|
| 442 |
-
"news_prompt_instructions": "심층 경제 뉴스, 경제 전문 용어 적극 사용, 긴 문장, 복잡한 구문, 산업 동향, 경제 정책, 국제 경제, 다각적인 분석과 심층 추론 필요", # 고등학생용 뉴스 생성 지침
|
| 443 |
-
"explanation_prompt_instructions": "전문 경제 용어 해설 포함, 심층적인 경제 분석, 5문장 내외 요약, 다양한 관점 제시", # 고등학생용 해설 지침
|
| 444 |
-
},
|
| 445 |
-
}
|
| 446 |
-
|
| 447 |
-
# --- 뉴스 생성 함수 ---
|
| 448 |
-
def generate_news():
|
| 449 |
-
day_count = st.session_state["day_count"]
|
| 450 |
-
difficulty_level = st.session_state['difficulty_level']
|
| 451 |
-
|
| 452 |
-
level_config = difficulty_prompt_map.get(difficulty_level, difficulty_prompt_map["초등학생"]) # 기본값 초등학생
|
| 453 |
-
|
| 454 |
-
sentence_count = level_config["sentence_count"]
|
| 455 |
-
vocabulary_level = level_config["vocabulary_level"]
|
| 456 |
-
inference_level = level_config["inference_level"]
|
| 457 |
-
news_prompt_instructions = level_config["news_prompt_instructions"] # 난이도별 뉴스 생성 지침 추가
|
| 458 |
-
|
| 459 |
-
prompt = f"""
|
| 460 |
-
지시:
|
| 461 |
-
{level_config["level_desc"]}에 맞춰서, 주식 시장과 경제에 관련된 뉴스 기사 5개를 생성해주세요.
|
| 462 |
-
각 기사는 {sentence_count}문장 정도로 작성하고, {vocabulary_level}를 사용하여 학생들이 이해하기 쉬워야 합니다.
|
| 463 |
-
학생들이 뉴스를 읽고 {inference_level} 수준에서 어떤 회사가 유망할지 또는 쇠락할지 스스로 추론할 수 있도록 {news_prompt_instructions}
|
| 464 |
-
특정 회사 이름이나 주식 종목을 직접적으로 언급하지 마세요.
|
| 465 |
-
긍정적 뉴스, 부정적 뉴스, 중립적 뉴스 다양하게 생성하세요.(긍정, 부정, 중립 이라는 말은 표시하지 마세요.)
|
| 466 |
-
뉴스에 따라 주식이 상승하기도 하고 하락하기도 할 수 있습니다.
|
| 467 |
-
각 뉴스 기사는 "## 뉴스 [번호]" 로 시작해주세요. (예: ## 뉴스 1, ## 뉴스 2 ...)
|
| 468 |
-
|
| 469 |
-
**생성된 뉴스 기사:**
|
| 470 |
-
"""
|
| 471 |
-
chat_session = st.session_state["chat_session"]
|
| 472 |
-
messages = [{"role": "user", "content": prompt}] # messages 리스트 생성
|
| 473 |
-
|
| 474 |
-
response = client.chat.completions.create( # OpenAI ChatCompletions API 호출
|
| 475 |
-
model="gpt-4o-mini",
|
| 476 |
-
messages=messages, # messages 리스트 전달
|
| 477 |
-
response_format={
|
| 478 |
-
"type": "text"
|
| 479 |
-
},
|
| 480 |
-
temperature=0.7,
|
| 481 |
-
max_completion_tokens=10000,
|
| 482 |
-
top_p=0.95,
|
| 483 |
-
frequency_penalty=0,
|
| 484 |
-
presence_penalty=0
|
| 485 |
-
)
|
| 486 |
-
news_text = response.choices[0].message.content.strip() # 응답 텍스트 추출
|
| 487 |
-
|
| 488 |
-
news_articles = []
|
| 489 |
-
if news_text:
|
| 490 |
-
news_articles = [
|
| 491 |
-
article.strip() for article in news_text.split("## 뉴스 ") if article.strip()
|
| 492 |
-
]
|
| 493 |
-
|
| 494 |
-
return news_articles[:5]
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
def explain_daily_news_meanings(daily_news):
|
| 498 |
-
if daily_news is None:
|
| 499 |
-
return {}
|
| 500 |
-
|
| 501 |
-
difficulty_level = st.session_state['difficulty_level']
|
| 502 |
-
level_config = difficulty_prompt_map.get(difficulty_level, difficulty_prompt_map["초등학생"]) # 기본값 초등학생
|
| 503 |
-
explanation_prompt_instructions = level_config["explanation_prompt_instructions"] # 난이도별 해설 지침 추가
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
meanings = {}
|
| 507 |
-
for i, news_article in enumerate(daily_news):
|
| 508 |
-
prompt = f"""
|
| 509 |
-
**신문 기사:**
|
| 510 |
-
{news_article}
|
| 511 |
-
|
| 512 |
-
**지시:**
|
| 513 |
-
위 신문 기사의 핵심 의미를 {level_config["level_desc"]}이 이해하기 쉽게 {explanation_prompt_instructions}해서 "해설: " 다음에 설명해주세요.
|
| 514 |
-
그리고 이 뉴스와 관련된 주식 섹터 1~2개를 쉼표로 구분해서 "관련 섹터: " 다음에 알려주세요. 관련 섹터가 없다면 "관련 섹터: 없음" 이라고 해주세요.
|
| 515 |
-
|
| 516 |
-
뉴스 의미 해설:
|
| 517 |
-
"""
|
| 518 |
-
chat_session = st.session_state["chat_session"]
|
| 519 |
-
messages = [{"role": "user", "content": prompt}] # messages 리스트 생성
|
| 520 |
-
try:
|
| 521 |
-
response = client.chat.completions.create( # OpenAI ChatCompletions API 호출
|
| 522 |
-
model="gpt-4o-mini",
|
| 523 |
-
messages=messages, # messages 리스트 전달
|
| 524 |
-
response_format={
|
| 525 |
-
"type": "text"
|
| 526 |
-
},
|
| 527 |
-
temperature=0.7,
|
| 528 |
-
max_completion_tokens=10000,
|
| 529 |
-
top_p=0.95,
|
| 530 |
-
frequency_penalty=0,
|
| 531 |
-
presence_penalty=0
|
| 532 |
-
)
|
| 533 |
-
meaning_text = response.choices[0].message.content.strip() # 응답 텍스트 추출
|
| 534 |
-
|
| 535 |
-
explanation = ""
|
| 536 |
-
related_sectors = []
|
| 537 |
-
|
| 538 |
-
if "해설:" in meaning_text:
|
| 539 |
-
explanation_start_index = meaning_text.find("해설:") + len("해설:")
|
| 540 |
-
explanation_end_index = meaning_text.find("관련 섹터:")
|
| 541 |
-
if explanation_end_index != -1:
|
| 542 |
-
explanation = meaning_text[explanation_start_index:explanation_end_index].strip()
|
| 543 |
-
else:
|
| 544 |
-
explanation = meaning_text[explanation_start_index:].strip()
|
| 545 |
-
|
| 546 |
-
if "관련 섹터:" in meaning_text:
|
| 547 |
-
related_sectors_str = meaning_text.split("관련 섹터:")[1].strip()
|
| 548 |
-
if related_sectors_str.lower() != "없음":
|
| 549 |
-
related_sectors = [sector.strip() for sector in related_sectors_str.split(',')]
|
| 550 |
-
else:
|
| 551 |
-
related_sectors = [] # "없음" explicitly means empty list
|
| 552 |
-
|
| 553 |
-
meanings[str(i + 1)] = {"explanation": explanation, "sectors": related_sectors}
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
except Exception as e: # google.api_core.exceptions.ResourceExhausted -> Exception으로 변경
|
| 557 |
-
st.error(
|
| 558 |
-
f"API 할당량 초과 오류가 발생했습니다. 잠시 후 다시 시도해주세요. 오류 메시지: {e}"
|
| 559 |
-
)
|
| 560 |
-
return None
|
| 561 |
-
time.sleep(1)
|
| 562 |
-
return meanings
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
def buy_stock(stock_name, quantity, sector):
|
| 566 |
-
if (
|
| 567 |
-
sector not in st.session_state["stocks"]
|
| 568 |
-
or stock_name not in st.session_state["stocks"][sector]
|
| 569 |
-
):
|
| 570 |
-
st.session_state["messages"].append(
|
| 571 |
-
{"type": "error", "text": "존재하지 않는 주식 종목입니다."}
|
| 572 |
-
)
|
| 573 |
-
return
|
| 574 |
-
|
| 575 |
-
if quantity <= 0:
|
| 576 |
-
st.session_state["messages"].append(
|
| 577 |
-
{"type": "error", "text": "매수 수량은 1주 이상이어야 합니다."}
|
| 578 |
-
)
|
| 579 |
-
return
|
| 580 |
-
|
| 581 |
-
stock_price = st.session_state["stocks"][sector][stock_name]["current_price"]
|
| 582 |
-
max_quantity = st.session_state["portfolio"]["cash"] // stock_price
|
| 583 |
-
if quantity > max_quantity:
|
| 584 |
-
st.session_state["messages"].append(
|
| 585 |
-
{
|
| 586 |
-
"type": "error",
|
| 587 |
-
"text": f"매수 가능 수량을 초과했습니다. (최대 {max_quantity}주까지 매수 가능)",
|
| 588 |
-
}
|
| 589 |
-
)
|
| 590 |
-
st.toast(
|
| 591 |
-
f"매수 가능 수량을 초과했습니다. (최대 {max_quantity}주까지 매수 가능)", icon="❌"
|
| 592 |
-
)
|
| 593 |
-
st.error(f"잔액이 부족합니다. (최대 {max_quantity}주까지 매수 가능)")
|
| 594 |
-
return
|
| 595 |
-
|
| 596 |
-
total_price = stock_price * quantity
|
| 597 |
-
|
| 598 |
-
if st.session_state["portfolio"]["cash"] >= total_price:
|
| 599 |
-
st.session_state["portfolio"]["cash"] -= total_price
|
| 600 |
-
portfolio_stocks = st.session_state["portfolio"]["stocks"]
|
| 601 |
-
if (
|
| 602 |
-
stock_name in portfolio_stocks
|
| 603 |
-
):
|
| 604 |
-
portfolio_stocks[stock_name]["quantity"] += quantity
|
| 605 |
-
portfolio_stocks[stock_name]["purchase_price"] = (
|
| 606 |
-
portfolio_stocks[stock_name]["purchase_price"]
|
| 607 |
-
* (portfolio_stocks[stock_name]["quantity"] - quantity)
|
| 608 |
-
+ total_price
|
| 609 |
-
) / portfolio_stocks[stock_name]["quantity"]
|
| 610 |
-
else:
|
| 611 |
-
portfolio_stocks[stock_name] = {
|
| 612 |
-
"quantity": quantity,
|
| 613 |
-
"purchase_price": total_price / quantity,
|
| 614 |
-
}
|
| 615 |
-
st.session_state["messages"].append(
|
| 616 |
-
{
|
| 617 |
-
"type": "success",
|
| 618 |
-
"text": f"{stock_name} {quantity}주 매수 완료. 총 {total_price:,.0f}원 소요.",
|
| 619 |
-
}
|
| 620 |
-
)
|
| 621 |
-
st.success(f"{stock_name} {quantity}주 매수 완료. 총 {total_price:,.0f}원 소요.")
|
| 622 |
-
st.toast(
|
| 623 |
-
f"{stock_name} {quantity}주 매수 완료. 총 {total_price:,.0f}원 소요.", icon="✅"
|
| 624 |
-
)
|
| 625 |
-
st.session_state['buy_confirm'] = False
|
| 626 |
-
else:
|
| 627 |
-
st.session_state["messages"].append(
|
| 628 |
-
{"type": "error", "text": "잔액이 부족합니다."}
|
| 629 |
-
)
|
| 630 |
-
st.toast("잔액이 부족합니다.", icon="❌")
|
| 631 |
-
st.error(f"잔액이 부족합니다. (최대 {max_quantity}주까지 매수 가능)")
|
| 632 |
-
st.session_state['buy_confirm'] = False
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
def sell_stock(stock_name, quantity):
|
| 636 |
-
if stock_name not in st.session_state["portfolio"]["stocks"]:
|
| 637 |
-
st.session_state["messages"].append(
|
| 638 |
-
{"type": "error", "text": "보유하고 있지 않은 주식입니다."}
|
| 639 |
-
)
|
| 640 |
-
st.error("보유하고 있지 않은 주식입니다.")
|
| 641 |
-
st.toast("보유하고 있지 않은 주식입니다.", icon="❌")
|
| 642 |
-
return
|
| 643 |
-
|
| 644 |
-
owned_quantity = st.session_state["portfolio"]["stocks"][stock_name]["quantity"]
|
| 645 |
-
|
| 646 |
-
if owned_quantity < quantity:
|
| 647 |
-
st.session_state["messages"].append(
|
| 648 |
-
{"type": "error", "text": "매도 수량이 보유 주식 수를 초과했습니다."}
|
| 649 |
-
)
|
| 650 |
-
st.error("매도 수량이 보유 주식 수를 초과했습니다.")
|
| 651 |
-
st.toast("매도 수량이 보유 주식 수를 초과했습니다.", icon="❌")
|
| 652 |
-
return
|
| 653 |
-
|
| 654 |
-
if quantity <= 0:
|
| 655 |
-
st.session_state["messages"].append(
|
| 656 |
-
{"type": "error", "text": "잘못된 매도 수량입니다."}
|
| 657 |
-
)
|
| 658 |
-
st.error("잘못된 매도 수량입니다.")
|
| 659 |
-
st.toast("잘못된 매도 수량입니다.", icon="❌")
|
| 660 |
-
return
|
| 661 |
-
|
| 662 |
-
stock_price = 0
|
| 663 |
-
stock_sector = ""
|
| 664 |
-
for sector, stocks in st.session_state["stocks"].items():
|
| 665 |
-
if stock_name in stocks:
|
| 666 |
-
stock_price = stocks[stock_name]["current_price"]
|
| 667 |
-
stock_sector = sector
|
| 668 |
-
break
|
| 669 |
-
|
| 670 |
-
if stock_price == 0:
|
| 671 |
-
st.session_state["messages"].append(
|
| 672 |
-
{"type": "error", "text": "주식 정보를 찾을 수 없습니다."}
|
| 673 |
-
)
|
| 674 |
-
return
|
| 675 |
-
|
| 676 |
-
sell_price = stock_price * quantity
|
| 677 |
-
st.session_state["portfolio"]["cash"] += sell_price
|
| 678 |
-
st.session_state["portfolio"]["stocks"][stock_name]["quantity"] -= quantity
|
| 679 |
-
if st.session_state["portfolio"]["stocks"][stock_name]["quantity"] == 0:
|
| 680 |
-
del st.session_state["portfolio"]["stocks"][stock_name]
|
| 681 |
-
|
| 682 |
-
st.session_state["messages"].append(
|
| 683 |
-
{
|
| 684 |
-
"type": "success",
|
| 685 |
-
"text": f"{stock_name} {quantity}주 매도 완료. 총 {sell_price:,.0f}원 획득.",
|
| 686 |
-
}
|
| 687 |
-
)
|
| 688 |
-
st.toast(
|
| 689 |
-
f"{stock_name} {quantity}주 매도 완료. 총 {sell_price:,.0f}원 획득.", icon="✅"
|
| 690 |
-
)
|
| 691 |
-
st.success(f"{stock_name} {quantity}주 매도 완료. 총 {sell_price:,.0f}원 획득.")
|
| 692 |
-
st.session_state['sell_confirm'] = False
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
def update_stock_prices():
|
| 696 |
-
if not st.session_state["daily_news"]:
|
| 697 |
-
return
|
| 698 |
-
|
| 699 |
-
sector_impacts = {sector: 0 for sector in st.session_state["stocks"]}
|
| 700 |
-
|
| 701 |
-
for i, news_article in enumerate(st.session_state["daily_news"]):
|
| 702 |
-
news_meaning = st.session_state["news_meanings"].get(str(i + 1))
|
| 703 |
-
if news_meaning:
|
| 704 |
-
related_sectors = news_meaning.get("sectors", [])
|
| 705 |
-
news_explanation = news_meaning.get("explanation", "")
|
| 706 |
-
|
| 707 |
-
news_sentiment = 0
|
| 708 |
-
if "상승" in news_article or "성장" in news_article or "긍정적" in news_article or "유망" in news_article or "호황" in news_article:
|
| 709 |
-
news_sentiment = 1
|
| 710 |
-
elif "하락" in news_article or "감소" in news_article or "부정적" in news_article or "어려움" in news_article or "침체" in news_article or "위기" in news_article:
|
| 711 |
-
news_sentiment = -1
|
| 712 |
-
else:
|
| 713 |
-
news_sentiment = 0
|
| 714 |
-
|
| 715 |
-
for sector in related_sectors:
|
| 716 |
-
if sector in sector_impacts:
|
| 717 |
-
sector_impacts[sector] += news_sentiment * 0.05
|
| 718 |
-
|
| 719 |
-
for sector in st.session_state["stocks"]:
|
| 720 |
-
sector_impact = sector_impacts[sector]
|
| 721 |
-
for stock_name in st.session_state["stocks"][sector]:
|
| 722 |
-
change_rate = random.uniform(-0.02, 0.02) + sector_impact
|
| 723 |
-
change_rate = max(-0.3, min(0.3, change_rate))
|
| 724 |
-
st.session_state["stocks"][sector][stock_name]["current_price"] *= (
|
| 725 |
-
1 + change_rate
|
| 726 |
-
)
|
| 727 |
-
st.session_state["stocks"][sector][stock_name]["current_price"] = max(
|
| 728 |
-
1, int(st.session_state["stocks"][sector][stock_name]["current_price"])
|
| 729 |
-
)
|
| 730 |
-
st.session_state["stocks"][sector][stock_name]["price_history"].append(
|
| 731 |
-
st.session_state["stocks"][sector][stock_name]["current_price"]
|
| 732 |
-
)
|
| 733 |
-
st.session_state["messages"].append({"type": "info", "text": "주가가 변동되었습니다."})
|
| 734 |
-
st.toast("주가가 변동되었습니다.", icon="📈")
|
| 735 |
-
st.info("주가가 변동되었습니다.")
|
| 736 |
-
st.session_state["sector_news_impact"] = sector_impacts
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
def display_portfolio():
|
| 740 |
-
portfolio = st.session_state["portfolio"]
|
| 741 |
-
cash = portfolio["cash"]
|
| 742 |
-
total_value = cash
|
| 743 |
-
total_purchase_value = 0
|
| 744 |
-
for stock_name, stock_info in portfolio["stocks"].items():
|
| 745 |
-
quantity = stock_info["quantity"]
|
| 746 |
-
purchase_price = stock_info["purchase_price"]
|
| 747 |
-
current_price = 0
|
| 748 |
-
stock_sector = ""
|
| 749 |
-
for sector, stocks in st.session_state["stocks"].items():
|
| 750 |
-
if stock_name in stocks:
|
| 751 |
-
current_price = stocks[stock_name]["current_price"]
|
| 752 |
-
stock_sector = sector
|
| 753 |
-
break
|
| 754 |
-
if current_price != 0:
|
| 755 |
-
stock_value = current_price * quantity
|
| 756 |
-
total_value += stock_value
|
| 757 |
-
purchase_value = purchase_price * quantity
|
| 758 |
-
total_purchase_value += purchase_value
|
| 759 |
-
|
| 760 |
-
initial_cash = 10000000
|
| 761 |
-
total_profit_loss = total_value - initial_cash
|
| 762 |
-
total_profit_rate = (
|
| 763 |
-
(total_profit_loss / initial_cash) * 100 if initial_cash != 0 else 0
|
| 764 |
-
)
|
| 765 |
-
|
| 766 |
-
return cash, total_value, total_profit_rate
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
def display_stock_prices():
|
| 770 |
-
stocks_data = []
|
| 771 |
-
for sector, sector_stocks in st.session_state["stocks"].items():
|
| 772 |
-
for stock_name, stock_info in sector_stocks.items():
|
| 773 |
-
price_history = stock_info["price_history"]
|
| 774 |
-
daily_change_rate_str = " - " # 기본값
|
| 775 |
-
if len(price_history) >= 2:
|
| 776 |
-
previous_day_price = price_history[-2]
|
| 777 |
-
current_price = price_history[-1]
|
| 778 |
-
daily_change_rate = (current_price - previous_day_price) / previous_day_price * 100
|
| 779 |
-
daily_change_rate_str = f"{daily_change_rate:.2f}%"
|
| 780 |
-
|
| 781 |
-
stocks_data.append(
|
| 782 |
-
{
|
| 783 |
-
"종목": stock_name,
|
| 784 |
-
"섹터": sector,
|
| 785 |
-
"현재 주가": f"{stock_info['current_price']:,} 원",
|
| 786 |
-
"전일 대비": daily_change_rate_str, # 전일 대비 등락률 추가
|
| 787 |
-
"price_history": stock_info["price_history"],
|
| 788 |
-
"description": stock_info["description"],
|
| 789 |
-
}
|
| 790 |
-
)
|
| 791 |
-
stocks_df = pd.DataFrame(stocks_data)
|
| 792 |
-
st.dataframe(stocks_df[["섹터", "종목", "현재 주가", "전일 대비"]], hide_index=True) # "전일 대비" 컬럼 추가
|
| 793 |
-
|
| 794 |
-
selected_stock_all_info = st.selectbox(
|
| 795 |
-
"종목 선택 (기업 정보 및 주가 그래프)", stocks_df["종목"].tolist()
|
| 796 |
-
)
|
| 797 |
-
if selected_stock_all_info:
|
| 798 |
-
selected_stock_sector = stocks_df[
|
| 799 |
-
stocks_df["종목"] == selected_stock_all_info
|
| 800 |
-
]["섹터"].iloc[0]
|
| 801 |
-
col1_info, col2_graph = st.columns([1, 2])
|
| 802 |
-
|
| 803 |
-
with col1_info:
|
| 804 |
-
st.subheader("기업 정보")
|
| 805 |
-
st.info(
|
| 806 |
-
f"**{selected_stock_all_info} ({selected_stock_sector})**\n\n{st.session_state['stocks'][selected_stock_sector][selected_stock_all_info]['description']}"
|
| 807 |
-
)
|
| 808 |
-
|
| 809 |
-
with col2_graph:
|
| 810 |
-
st.subheader("주가 그래프")
|
| 811 |
-
price_history_df = pd.DataFrame(
|
| 812 |
-
{
|
| 813 |
-
"날짜": range(
|
| 814 |
-
1,
|
| 815 |
-
len(
|
| 816 |
-
st.session_state["stocks"][selected_stock_sector][
|
| 817 |
-
selected_stock_all_info
|
| 818 |
-
]["price_history"]
|
| 819 |
-
)
|
| 820 |
-
+ 1,
|
| 821 |
-
),
|
| 822 |
-
"주가": st.session_state["stocks"][selected_stock_sector][
|
| 823 |
-
selected_stock_all_info
|
| 824 |
-
]["price_history"],
|
| 825 |
-
}
|
| 826 |
-
)
|
| 827 |
-
fig = px.line(
|
| 828 |
-
price_history_df,
|
| 829 |
-
x="날짜",
|
| 830 |
-
y="주가",
|
| 831 |
-
title=f"{selected_stock_all_info} ({selected_stock_sector}) 주가 변동",
|
| 832 |
-
)
|
| 833 |
-
st.plotly_chart(fig)
|
| 834 |
-
else:
|
| 835 |
-
st.info("종목을 선택하여 기업 정보와 주가 그래프를 확인하세요.")
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
def display_portfolio_table():
|
| 839 |
-
portfolio = st.session_state["portfolio"]
|
| 840 |
-
if portfolio["stocks"]:
|
| 841 |
-
portfolio_data = []
|
| 842 |
-
total_value = portfolio["cash"]
|
| 843 |
-
total_purchase_value = 0
|
| 844 |
-
total_profit_loss = 0
|
| 845 |
-
total_profit_rate = 0.0
|
| 846 |
-
for stock_name, stock_info in portfolio["stocks"].items():
|
| 847 |
-
quantity = stock_info["quantity"]
|
| 848 |
-
purchase_price = stock_info["purchase_price"]
|
| 849 |
-
current_price = 0
|
| 850 |
-
stock_sector = ""
|
| 851 |
-
for sector, stocks in st.session_state["stocks"].items():
|
| 852 |
-
if stock_name in stocks:
|
| 853 |
-
current_price = stocks[stock_name]["current_price"]
|
| 854 |
-
stock_sector = sector
|
| 855 |
-
break
|
| 856 |
-
|
| 857 |
-
if current_price == 0:
|
| 858 |
-
continue
|
| 859 |
-
|
| 860 |
-
stock_value = current_price * quantity
|
| 861 |
-
purchase_value = purchase_price * quantity
|
| 862 |
-
profit_loss = stock_value - purchase_value
|
| 863 |
-
profit_rate = (
|
| 864 |
-
(profit_loss / purchase_value) * 100 if purchase_value != 0 else 0
|
| 865 |
-
)
|
| 866 |
-
total_value += stock_value
|
| 867 |
-
total_purchase_value += purchase_value
|
| 868 |
-
total_profit_loss += profit_loss
|
| 869 |
-
|
| 870 |
-
portfolio_data.append(
|
| 871 |
-
{
|
| 872 |
-
"종목": stock_name,
|
| 873 |
-
"섹터": stock_sector,
|
| 874 |
-
"보유 수량": quantity,
|
| 875 |
-
"매수 단가": f"{purchase_price:,.0f} 원",
|
| 876 |
-
"현재가": f"{current_price:,.0f} 원",
|
| 877 |
-
"평가액": f"{stock_value:,.0f} 원",
|
| 878 |
-
"손익": f"{profit_loss:,.0f} 원",
|
| 879 |
-
"수익률": f"{profit_rate:.2f}%",
|
| 880 |
-
}
|
| 881 |
-
)
|
| 882 |
-
portfolio_data.append(
|
| 883 |
-
{
|
| 884 |
-
"종목": "현금",
|
| 885 |
-
"섹터": "-",
|
| 886 |
-
"보유 수량": "-",
|
| 887 |
-
"매수 단가": "-",
|
| 888 |
-
"현재가": "-",
|
| 889 |
-
"평가액": f"{portfolio['cash']:,} 원",
|
| 890 |
-
"손익": "-",
|
| 891 |
-
"수익률": "-",
|
| 892 |
-
}
|
| 893 |
-
)
|
| 894 |
-
portfolio_df = pd.DataFrame(portfolio_data)
|
| 895 |
-
st.dataframe(portfolio_df, hide_index=True, height=350)
|
| 896 |
-
st.markdown(
|
| 897 |
-
f"""**현금 잔고:** {portfolio['cash']:,} 원
|
| 898 |
-
**📊 총 평가액:** {total_value:,.0f} 원
|
| 899 |
-
**🛒 총 매수 금액:** {total_purchase_value:,.0f} 원
|
| 900 |
-
**📈📉 총 손익:** {total_profit_loss:,.0f} 원 (🚀 수익률: {total_profit_rate:.2f}%)
|
| 901 |
-
"""
|
| 902 |
-
)
|
| 903 |
-
else:
|
| 904 |
-
st.info("보유 주식이 없습니다.")
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
# --- 주식 용어 사전 ---
|
| 908 |
-
def display_stock_glossary():
|
| 909 |
-
glossary = {
|
| 910 |
-
"주식": "회사의 일부분을 나타내는 증서. 주식을 사면 회사의 주인이 되는 거예요.",
|
| 911 |
-
"주가": "주식 1개당 가격. 사람들이 주식을 사고팔 때 가격이 변해요.",
|
| 912 |
-
"매수": "주식을 사는 것.",
|
| 913 |
-
"매도": "주식을 파는 것.",
|
| 914 |
-
"포트폴리오": "내가 가진 주식과 현금 목록.",
|
| 915 |
-
"수익률": "원래 돈에서 얼마나 이익을 보았는지 비율로 나타낸 것. (이익 / 원래 돈) X 100",
|
| 916 |
-
"상승": "주가가 오르는 것.",
|
| 917 |
-
"하락": "주가가 내리는 것.",
|
| 918 |
-
"변동": "주가가 오르락내리락 움직이는 것.",
|
| 919 |
-
"투자": "돈을 넣어 이익을 얻으려고 하는 모든 활동.",
|
| 920 |
-
"섹터": "비슷한 사업을 하는 회사들을 묶어 놓은 그룹 (예: 기술 섹터, 자동차 섹터)",
|
| 921 |
-
"전일 대비 등락률": "오늘 주가가 어제보다 얼마나 변했는지 퍼센트로 나타낸 것. +는 상승, -는 하락.", # 용어 추가
|
| 922 |
-
}
|
| 923 |
-
with st.sidebar.expander("📚 주식 용어 사전", expanded=False):
|
| 924 |
-
for term, definition in glossary.items():
|
| 925 |
-
st.markdown(f"**{term}:** {definition}")
|
| 926 |
-
st.markdown("---")
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
# --- 메인 화면 ---
|
| 930 |
-
def main():
|
| 931 |
-
col_news, col_main_ui = st.columns([1, 2])
|
| 932 |
-
|
| 933 |
-
with col_news:
|
| 934 |
-
st.header(f"📰 Day {st.session_state['day_count']} 뉴스")
|
| 935 |
-
if st.button("뉴스 생성", use_container_width=True, key="news_gen_button"):
|
| 936 |
-
with st.spinner(f"Day {st.session_state['day_count']} 뉴스 생성 중..."):
|
| 937 |
-
current_daily_news = generate_news()
|
| 938 |
-
st.session_state["daily_news"] = current_daily_news
|
| 939 |
-
|
| 940 |
-
if st.session_state["daily_news"]:
|
| 941 |
-
st.subheader(f"Day {st.session_state['day_count']} 뉴스")
|
| 942 |
-
for i, news in enumerate(st.session_state["daily_news"]):
|
| 943 |
-
with st.expander(f"뉴스 {i+1}", expanded=False):
|
| 944 |
-
st.write(news)
|
| 945 |
-
|
| 946 |
-
if st.session_state["previous_daily_news"] and st.session_state[
|
| 947 |
-
"news_meanings"
|
| 948 |
-
]:
|
| 949 |
-
st.subheader(f"Day {st.session_state['day_count'] - 1} 뉴스 해설")
|
| 950 |
-
st.info("AI가 분석한 어제 뉴스 해설입니다.")
|
| 951 |
-
with st.expander(f"Day {st.session_state['day_count'] - 1} 뉴스 해설 보기", expanded=False):
|
| 952 |
-
if st.session_state["news_meanings"]:
|
| 953 |
-
for i, meaning_data in st.session_state["news_meanings"].items():
|
| 954 |
-
st.markdown(f"**뉴스 {i}**:")
|
| 955 |
-
st.markdown(f"**AI 해설:** {meaning_data['explanation']}") # Markdown 으로 변경
|
| 956 |
-
if meaning_data['sectors']:
|
| 957 |
-
st.markdown(f"**관련 섹터:** {', '.join(meaning_data['sectors'])}") # Markdown 으로 변경
|
| 958 |
-
else:
|
| 959 |
-
st.markdown("**관련 섹터:** 없음") # Markdown 으로 변경
|
| 960 |
-
else:
|
| 961 |
-
st.info("어제 뉴스에 대한 해설이 없습니다.")
|
| 962 |
-
|
| 963 |
-
elif not st.session_state["daily_news"]:
|
| 964 |
-
st.info("뉴스 생성 버튼을 눌러 오늘의 뉴스를 받아보세요.")
|
| 965 |
-
|
| 966 |
-
with col_main_ui:
|
| 967 |
-
menu = st.tabs(
|
| 968 |
-
['현재 주가', '내 포트폴리오', '주식 매수', '주식 매도', '어제 뉴스 해설']
|
| 969 |
-
)
|
| 970 |
-
|
| 971 |
-
with menu[0]:
|
| 972 |
-
st.subheader("📈 현재 주가 및 기업 정보")
|
| 973 |
-
st.markdown("주식 시장의 현재 가격과 기업 정보를 확인하세요.")
|
| 974 |
-
display_stock_prices()
|
| 975 |
-
|
| 976 |
-
with menu[1]:
|
| 977 |
-
st.subheader("📊 내 포트폴리오")
|
| 978 |
-
st.markdown("현재 보유 중인 주식과 자산을 확인하세요.")
|
| 979 |
-
display_portfolio_table()
|
| 980 |
-
|
| 981 |
-
with menu[2]:
|
| 982 |
-
st.subheader("💰 주식 매수")
|
| 983 |
-
st.markdown("AI 예측과 뉴스 분석을 바탕으로 주식을 매수해보세요.")
|
| 984 |
-
sector_names = list(st.session_state["stocks"].keys())
|
| 985 |
-
selected_sector_buy = st.selectbox("매수 섹터 선택:", sector_names)
|
| 986 |
-
stock_names_in_sector = list(
|
| 987 |
-
st.session_state["stocks"][selected_sector_buy].keys()
|
| 988 |
-
)
|
| 989 |
-
selected_stock_buy = st.selectbox("매수 종목 선택:", stock_names_in_sector)
|
| 990 |
-
|
| 991 |
-
stock_price_buy = st.session_state["stocks"][selected_sector_buy][
|
| 992 |
-
selected_stock_buy
|
| 993 |
-
]["current_price"]
|
| 994 |
-
st.info(f"**{selected_stock_buy}** 현재 주가: {stock_price_buy:,.0f}원")
|
| 995 |
-
quantity_buy = st.number_input(
|
| 996 |
-
"매수 수량 (주):", min_value=1, value=1, step=1
|
| 997 |
-
)
|
| 998 |
-
|
| 999 |
-
if not st.session_state['buy_confirm']:
|
| 1000 |
-
if st.button("주식 매수", use_container_width=True, key='buy_button_confirm'):
|
| 1001 |
-
st.session_state['buy_confirm'] = True
|
| 1002 |
-
else:
|
| 1003 |
-
st.warning("정말 매수하시겠습니까?")
|
| 1004 |
-
col_confirm, col_cancel = st.columns([1, 1])
|
| 1005 |
-
with col_confirm:
|
| 1006 |
-
if st.button("✅ 매수 확인", use_container_width=True, key='buy_confirm_button'):
|
| 1007 |
-
buy_stock(selected_stock_buy, quantity_buy, selected_sector_buy)
|
| 1008 |
-
|
| 1009 |
-
with col_cancel:
|
| 1010 |
-
if st.button("❌ 매수 취소", use_container_width=True, key='buy_cancel_button', type='secondary'):
|
| 1011 |
-
st.session_state['buy_confirm'] = False
|
| 1012 |
-
st.info("매수를 취소했습니다.")
|
| 1013 |
-
|
| 1014 |
-
with menu[3]:
|
| 1015 |
-
st.subheader("📉 주식 매도")
|
| 1016 |
-
st.markdown("보유 중인 주식을 판매하고 수익을 실현해보세요.")
|
| 1017 |
-
if st.session_state["portfolio"]["stocks"]:
|
| 1018 |
-
stock_names_sell = list(st.session_state["portfolio"]["stocks"].keys())
|
| 1019 |
-
selected_stock_sell = st.selectbox("매도 종목 선택:", stock_names_sell)
|
| 1020 |
-
stock_price_sell = 0
|
| 1021 |
-
for sector, stocks in st.session_state["stocks"].items():
|
| 1022 |
-
if selected_stock_sell in stocks:
|
| 1023 |
-
stock_price_sell = stocks[selected_stock_sell]["current_price"]
|
| 1024 |
-
break
|
| 1025 |
-
|
| 1026 |
-
st.info(f"**{selected_stock_sell}** 현재 주가: {stock_price_sell:,.0f}원")
|
| 1027 |
-
max_sell_quantity = st.session_state["portfolio"]["stocks"][
|
| 1028 |
-
selected_stock_sell
|
| 1029 |
-
]["quantity"]
|
| 1030 |
-
quantity_sell = st.number_input(
|
| 1031 |
-
"매도 수량 (주):",
|
| 1032 |
-
min_value=1,
|
| 1033 |
-
max_value=max_sell_quantity,
|
| 1034 |
-
value=1,
|
| 1035 |
-
step=1,
|
| 1036 |
-
)
|
| 1037 |
-
if not st.session_state['sell_confirm']:
|
| 1038 |
-
if st.button("주식 매도", use_container_width=True, key='sell_button_confirm'):
|
| 1039 |
-
st.session_state['sell_confirm'] = True
|
| 1040 |
-
else:
|
| 1041 |
-
st.warning("정말 매도하시겠습니까?")
|
| 1042 |
-
col_confirm, col_cancel = st.columns([1, 1])
|
| 1043 |
-
with col_confirm:
|
| 1044 |
-
if st.button("✅ 매도 확인", use_container_width=True, key='sell_confirm_button'):
|
| 1045 |
-
sell_stock(selected_stock_sell, quantity_sell)
|
| 1046 |
-
with col_cancel:
|
| 1047 |
-
if st.button("❌ 매도 취소", use_container_width=True, key='sell_cancel_button', type='secondary'):
|
| 1048 |
-
st.session_state['sell_confirm'] = False
|
| 1049 |
-
st.info("매도를 취소했습니다.")
|
| 1050 |
-
else:
|
| 1051 |
-
st.info("보유 주식이 없습니다. 포트폴리오 탭에서 확인하세요.")
|
| 1052 |
-
|
| 1053 |
-
with menu[4]:
|
| 1054 |
-
if st.session_state["previous_daily_news"] and st.session_state[
|
| 1055 |
-
"news_meanings"
|
| 1056 |
-
]:
|
| 1057 |
-
st.subheader(f"Day {st.session_state['day_count'] - 1} 뉴스 해설")
|
| 1058 |
-
st.info("AI가 분석한 어제 뉴스 해설입니다.")
|
| 1059 |
-
for i in range(len(st.session_state["previous_daily_news"])):
|
| 1060 |
-
with st.expander(f"뉴스 {i+1}", expanded=False):
|
| 1061 |
-
news_content = st.session_state["previous_daily_news"][i]
|
| 1062 |
-
st.markdown("**뉴스 원문:**")
|
| 1063 |
-
st.write(news_content)
|
| 1064 |
-
meaning_data = st.session_state["news_meanings"].get(str(i+1))
|
| 1065 |
-
if meaning_data:
|
| 1066 |
-
st.markdown("**AI 해설:**")
|
| 1067 |
-
st.info(meaning_data['explanation'])
|
| 1068 |
-
if meaning_data['sectors']:
|
| 1069 |
-
st.markdown("**관련 섹터:**")
|
| 1070 |
-
st.info(', '.join(meaning_data['sectors']))
|
| 1071 |
-
else:
|
| 1072 |
-
st.info("**관련 섹터:** 없음")
|
| 1073 |
-
else:
|
| 1074 |
-
st.warning("뉴스 해설을 생성하지 못했습니다.")
|
| 1075 |
-
else:
|
| 1076 |
-
st.info(
|
| 1077 |
-
"이전 뉴스 해설이 없습니다. 하루 지나가기 버튼을 눌러 뉴스 해설을 받아보세요."
|
| 1078 |
-
)
|
| 1079 |
-
|
| 1080 |
-
with st.sidebar:
|
| 1081 |
-
st.markdown("# 💰 뉴스 보고, 주식 잡고! 모의 투자 게임") # 최종 앱 타이틀 적용
|
| 1082 |
-
st.markdown(f"### Day {st.session_state['day_count']}")
|
| 1083 |
-
st.markdown("---")
|
| 1084 |
-
|
| 1085 |
-
# 난이도 선택 Selectbox 추가
|
| 1086 |
-
difficulty_level = st.selectbox(
|
| 1087 |
-
"📈 난이도 선택",
|
| 1088 |
-
["초등학생", "중학생", "고등학생"],
|
| 1089 |
-
index=["초등학생", "중학생", "고등학생"].index(st.session_state['difficulty_level']) # 현재 난이도 기준으로 초기값 설정
|
| 1090 |
-
)
|
| 1091 |
-
st.session_state['difficulty_level'] = difficulty_level # 선택된 난이도 세션 상태에 저장
|
| 1092 |
-
st.markdown("---")
|
| 1093 |
-
|
| 1094 |
-
st.markdown("쉽고 재미있는 주식 투자 📈")
|
| 1095 |
-
st.markdown("📰 신문 기사를 읽고 미래를 예측해보세요!")
|
| 1096 |
-
cash, total_value, profit_rate = display_portfolio()
|
| 1097 |
-
st.metric(label="💰 현금 잔고", value=f"{cash:,.0f} 원")
|
| 1098 |
-
st.metric(label="📊 총 평가 금액", value=f"{total_value:,.0f} 원")
|
| 1099 |
-
st.metric(label="🚀 총 수익률", value=f"{profit_rate:.2f}%")
|
| 1100 |
-
st.markdown("---")
|
| 1101 |
-
|
| 1102 |
-
if st.button("하루 지나기", use_container_width=True, key="day_pass_button"):
|
| 1103 |
-
if st.session_state["daily_news"]:
|
| 1104 |
-
with st.spinner(f"Day {st.session_state['day_count']} 주가 변동 및 이전 뉴스 분석..."):
|
| 1105 |
-
st.session_state["previous_daily_news"] = st.session_state["daily_news"]
|
| 1106 |
-
meanings = explain_daily_news_meanings(
|
| 1107 |
-
st.session_state["previous_daily_news"]
|
| 1108 |
-
)
|
| 1109 |
-
if meanings:
|
| 1110 |
-
st.session_state["news_meanings"] = meanings
|
| 1111 |
-
update_stock_prices()
|
| 1112 |
-
st.session_state["daily_news"] = generate_news()
|
| 1113 |
-
st.session_state["day_count"] += 1
|
| 1114 |
-
st.rerun()
|
| 1115 |
-
st.info("어제 뉴스 해설 탭에서 AI가 분석한 뉴스 해설을 확인해보세요.")
|
| 1116 |
-
else:
|
| 1117 |
-
st.warning("오늘의 뉴스를 먼저 생성해주세요.")
|
| 1118 |
-
st.markdown("***")
|
| 1119 |
-
|
| 1120 |
-
display_stock_glossary()
|
| 1121 |
-
|
| 1122 |
-
with st.expander("🚀 앱 사용 가이드", expanded=False):
|
| 1123 |
-
st.markdown(
|
| 1124 |
-
"""
|
| 1125 |
-
**1단계: 뉴스 생성하기**
|
| 1126 |
-
- 왼쪽 '오늘의 뉴스' 영역에서 '뉴스 생성' 버튼을 클릭하세요.
|
| 1127 |
-
- AI가 주식 시장 뉴스를 5개 만들어줍니다.
|
| 1128 |
-
|
| 1129 |
-
**2단계: 뉴스 읽고 예측하기**
|
| 1130 |
-
- '오늘의 뉴스' 아래 뉴스들을 꼼꼼히 읽어보세요.
|
| 1131 |
-
- 각 뉴스를 읽고 '🤔 어떤 회사가 이 뉴스 때문에 돈을 더 많이 벌 수 있을까?' 또는 '😥 손해를 볼 회사는 어디일까?' 생각해 보세요.
|
| 1132 |
-
- 초중고학생 눈높이에서 쉽고 재미있게, 미래를 예측하는 연습을 할 수 있습니다.
|
| 1133 |
-
|
| 1134 |
-
**3단계: 주가 및 기업 정보 확인하기**
|
| 1135 |
-
- 메뉴에서 '📈 현재 주가' 탭을 선택하세요.
|
| 1136 |
-
- 현재 여러분이 가진 주식과 현금 잔고, 총 평가액, 수익률 등을 한눈에 볼 수 있습니다.
|
| 1137 |
-
- 투자 결과가 어떤지, 얼마나 이익/손해를 봤는지 확인해보세요.
|
| 1138 |
-
|
| 1139 |
-
**4단계: 주식 매수하기**
|
| 1140 |
-
- 메뉴에서 '💰 주식 매수' 탭을 선택하세요.
|
| 1141 |
-
- '매수 종목 선택' 메뉴에서 원하는 회사를 고르세요.
|
| 1142 |
-
- '매수 수량'을 입력하고 '주식 매수' 버튼을 클릭하면, 주식을 살 수 있습니다.
|
| 1143 |
-
- **팁:** 여러분의 뉴스 예측을 바탕으로 주식을 골라보세요!
|
| 1144 |
-
|
| 1145 |
-
**5단계: 포트폴리오 확인하기**
|
| 1146 |
-
- 메뉴에서 '📊 내 포트폴리오' 탭을 선택하세요.
|
| 1147 |
-
- 현재 여러분이 가진 주식과 현금 잔고, 총 평가액, 수익률 등을 한눈에 볼 수 있습니다.
|
| 1148 |
-
- 투자 결과가 어떤지, 얼마나 이익/손해를 봤는지 확인해보세요.
|
| 1149 |
-
|
| 1150 |
-
**6단계: 주식 매도하기**
|
| 1151 |
-
- 메뉴에서 '📉 주식 매도' 탭을 선택하세요.
|
| 1152 |
-
- '매도 종목 선택' 메뉴에서 팔고 싶은 주식을 고르세요.
|
| 1153 |
-
- '매도 수량'을 입력하고 '주식 매도' 버튼을 클릭하면, 주식을 팔고 현금을 얻을 수 있습니다.
|
| 1154 |
-
- **팁:** 주가가 올랐을 때 팔아서 이익을 남겨보세요!
|
| 1155 |
-
|
| 1156 |
-
**7단계: 하루 지나기 & 이전 뉴스 의미 해설 보기**
|
| 1157 |
-
- 사이드바 메뉴의 '하루 지나기' 버튼을 클릭하세요.
|
| 1158 |
-
- 주가가 변동되고, 새로운 하루가 시작됩니다.
|
| 1159 |
-
- '어제 뉴스 해설' 탭에서 각 뉴스의 '뉴스 의미' 해설이 추가된 것을 확인해보세요.
|
| 1160 |
-
- AI가 이전 뉴스 (어제 뉴스)의 핵심 의미를 초중고고학생 눈높이에 맞춰 쉽게 설명해줍니다.
|
| 1161 |
-
|
| 1162 |
-
**계속해서 배우고 성장하기! 🌱**
|
| 1163 |
-
- 모의 주식 거래 앱을 꾸준히 사용하면서, 뉴스-주가 관계를 배우고 투자 감각을 키워보세요.
|
| 1164 |
-
- 처음에는 어렵더라도, 계속 연습하면 주식 투자가 더 재미있고 쉬워질 거예요! 😉
|
| 1165 |
-
"""
|
| 1166 |
-
)
|
| 1167 |
-
|
| 1168 |
-
# supabase 클라이언트를 초기화합니다.
|
| 1169 |
-
SUPABASE_URL = os.environ.get("SUPABASE_URL") # .env 파일에서 Supabase URL을 불러옵니다.
|
| 1170 |
-
SUPABASE_KEY = os.environ.get("SUPABASE_KEY") # .env 파일에서 Supabase API KEY를 불러옵니다.
|
| 1171 |
-
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 1172 |
-
|
| 1173 |
-
def login_sidebar():
|
| 1174 |
-
# 이미 로그인 되어 있다면, 로그인 버튼을 비활성화합니다.
|
| 1175 |
-
if 'user_settings' in st.session_state:
|
| 1176 |
-
st.sidebar.success("이미 로그인 되어 있습니다.")
|
| 1177 |
-
st.sidebar.button("로그인", disabled=True)
|
| 1178 |
-
return
|
| 1179 |
-
# 사이드바에 로그인 폼을 생성하는 함수입니다.
|
| 1180 |
-
st.sidebar.header("로그인")
|
| 1181 |
-
account = st.sidebar.text_input("계정", value="") # 사용자에게 계정을 입력받습니다.
|
| 1182 |
-
pw = st.sidebar.text_input("비밀번호", type="password") # 사용자에게 비밀번호��� 입력받습니다.
|
| 1183 |
-
|
| 1184 |
-
# 로그인 버튼 클릭 시
|
| 1185 |
-
if st.sidebar.button("로그인"):
|
| 1186 |
-
# supabase의 users 테이블에서 account와 pw를 기준으로 사용자 조회
|
| 1187 |
-
response = supabase.table("users").select("*").eq("account", account).eq("pw", pw).execute()
|
| 1188 |
-
if response.data and len(response.data) > 0:
|
| 1189 |
-
user_data = response.data[0]
|
| 1190 |
-
# 조회된 user_data에 JSON 형식의 data 컬럼이 있는지 확인합니다.
|
| 1191 |
-
if "data" in user_data and user_data["data"]:
|
| 1192 |
-
try:
|
| 1193 |
-
user_settings = json.loads(user_data["data"])
|
| 1194 |
-
# 저장된 데이터를 session_state에 복원
|
| 1195 |
-
for key in ["stocks", "previous_daily_news", "news_meanings", "day_count", "portfolio", "daily_news", 'difficulty_level']: # difficulty_level 추가
|
| 1196 |
-
if key in user_settings:
|
| 1197 |
-
st.session_state[key] = user_settings[key]
|
| 1198 |
-
except Exception as e:
|
| 1199 |
-
st.sidebar.error("데이터 JSON 파싱 중 오류 발생, 기본 설정을 사용합니다.")
|
| 1200 |
-
user_settings = {"default_setting": True} # 기본 설정 예시
|
| 1201 |
-
else:
|
| 1202 |
-
# 데이터가 없으면 기본 설정을 사용합니다.
|
| 1203 |
-
user_settings = {"default_setting": True}
|
| 1204 |
-
st.sidebar.success("로그인 성공!")
|
| 1205 |
-
st.session_state["user_settings"] = user_settings
|
| 1206 |
-
# 사용자 id를 세션에 저장합니다. 'id' 또는 'user_id' 키 대신 'account' 필드를 사용합니다.
|
| 1207 |
-
st.session_state["user_id"] = account
|
| 1208 |
-
else:
|
| 1209 |
-
st.sidebar.error("아이디 또는 비밀번호가 일치하지 않습니다.")
|
| 1210 |
-
|
| 1211 |
-
# 새로 추가할 함수: session_state의 데이터를 JSON형식으로 저장하는 함수입니다.
|
| 1212 |
-
def save_session_data():
|
| 1213 |
-
# 사용자 id가 존재할 때에만 데이터 저장을 시도합니다.
|
| 1214 |
-
if "user_id" not in st.session_state:
|
| 1215 |
-
return
|
| 1216 |
-
# 저장할 session key 목록
|
| 1217 |
-
keys = ["stocks", "previous_daily_news", "news_meanings", "day_count", "portfolio", "daily_news", 'difficulty_level'] # difficulty_level 추가
|
| 1218 |
-
data_to_save = { key: st.session_state.get(key) for key in keys }
|
| 1219 |
-
try:
|
| 1220 |
-
json_data = json.dumps(data_to_save, ensure_ascii=False) # 기존 코드는 json.dumps(data_to_save) 였습니다.
|
| 1221 |
-
except Exception as e:
|
| 1222 |
-
st.error("세션 데이터를 JSON으로 변환 실패했습니다: " + str(e))
|
| 1223 |
-
return
|
| 1224 |
-
try:
|
| 1225 |
-
response = supabase.table("users").update({"data": json_data}).eq("account", st.session_state["user_id"]).execute()
|
| 1226 |
-
if not response.data:
|
| 1227 |
-
st.error("세션 데이터 업데이트 실패했습니다.")
|
| 1228 |
-
return
|
| 1229 |
-
st.info("세션 데이터를 데이터베이스에 저장했습니다.")
|
| 1230 |
-
except Exception as e:
|
| 1231 |
-
st.error(f"세션 데이터 업데이트 중 오류가 발생했습니다: {str(e)}")
|
| 1232 |
-
return
|
| 1233 |
-
|
| 1234 |
-
# main 함수 전에 sidebar 로그인을 호출합니다.
|
| 1235 |
-
login_sidebar()
|
| 1236 |
-
|
| 1237 |
-
if __name__ == "__main__":
|
| 1238 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|