ll7098ll commited on
Commit
6d0e312
·
verified ·
1 Parent(s): efeaeeb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +504 -359
app.py CHANGED
@@ -6,12 +6,144 @@ import pandas as pd
6
  import plotly.express as px
7
  from openai import OpenAI
8
 
9
- # --- Streamlit 설정 및 CSS 스타일 (기존 코드와 동일) ---
10
-
11
- # --- API 설정 (기존 코드와 동일) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  if "OPENAI_API_KEY" not in os.environ:
13
- st.error("OPENAI_API_KEY 환경 변수...")
 
 
14
  st.stop()
 
15
  client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
16
 
17
  # --- 세션 상태 초기화 ---
@@ -20,229 +152,193 @@ if "chat_session" not in st.session_state:
20
  if "portfolio" not in st.session_state:
21
  st.session_state["portfolio"] = {"cash": 10000000, "stocks": {}}
22
  if "stocks" not in st.session_state:
23
- st.session_state["stocks"] = {} # 종목 데이터 아래에서 초기화
24
  if "news_analysis_results" not in st.session_state:
25
  st.session_state["news_analysis_results"] = {}
26
- # ... (나머지 세션 상태 초기화 - 기존 코드와 동일) ...
27
-
28
- # --- 주식 데이터 초기화 (섹터 구분 없이 종목만, 상세 설명 추가) ---
29
- if "stocks" not in st.session_state:
30
- st.session_state["stocks"] = {
31
- "삼성전자": {
32
- "current_price": random.randint(60000, 80000),
33
- "price_history": [],
34
- "description": "대한민국 대표 전자 기업. 반도체, 스마트폰, TV 등 다양한 전자 제품 생산. 글로벌 IT 시장 선도 기업.",
35
- "sector": "IT", # 섹터 정보는 description 안에 포함 (UI 표시용)
36
- },
37
- "SK하이닉스": {
38
- "current_price": random.randint(90000, 120000),
39
- "price_history": [],
40
- "description": "세계적인 메모리 반도체 기업. D램, 낸드플래시 등 고성능 메모리 반도체 생산. IT 산업 필수 기업.",
41
- "sector": "IT",
42
- },
43
- "LG전자": {
44
- "current_price": random.randint(120000, 150000),
45
- "price_history": [],
46
- "description": "생활 가전, TV, 전장 사업 등 다양한 분야 영위. 프리미엄 가전 시장 선도 기업.",
47
- "sector": "전자",
48
- },
49
- "현대자동차": {
50
- "current_price": random.randint(180000, 220000),
51
- "price_history": [],
52
- "description": "대한민국 대표 자동차 기업. 글로벌 자동차 시장 점유율 상위. 전기차, 수소차 등 미래 모빌리티 기술 개발.",
53
- "sector": "자동차",
54
- },
55
- "기아": {
56
- "current_price": random.randint(80000, 100000),
57
- "price_history": [],
58
- "description": "현대자동차 그룹의 자동차 제조 기업. 스포티지, 쏘렌토 SUV 모델 인기. 디자인 경영 강화.",
59
- "sector": "자동차",
60
- },
61
- "POSCO홀딩스": {
62
- "current_price": random.randint(350000, 450000),
63
- "price_history": [],
64
- "description": "대한민국 대표 철강 기업. 세계적인 경쟁력 갖춘 철강 제품 산. 친환경 철강 기술 개발.",
65
- "sector": "철강",
66
- },
67
- "삼성물산": {
68
- "current_price": random.randint(120000, 140000),
69
- "price_history": [],
70
- "description": "삼성그룹 모태 기업. 건설, 상사, 패션, 리조트 다양한 사업 영위. 건설 프로젝트 참여.",
71
- "sector": "종합상사",
72
- },
73
- "현대건설": {
74
- "current_price": random.randint(45000, 55000),
75
- "price_history": [],
76
- "description": "대한민국 대표 건설 기업. 국내외 건설 시장 Leading. 주택, 인프라, 플랜트 다양한 건설 사업.",
77
- "sector": "건설",
78
- },
79
- "LG화학": {
80
- "current_price": random.randint(700000, 800000),
81
- "price_history": [],
82
- "description": "대한민국 대표 화학 기업. 석유화학, 전지 소재, 첨단 소재 등 다양한 제품 생산. 친환개발.",
83
- "sector": "화학",
84
- },
85
- "SK이노베이션": {
86
- "current_price": random.randint(160000, 190000),
87
- "price_history": [],
88
- "description": "에너지, 화학, 윤활유 다양한 사업 영위. 정유 사업 Leading 기업. 친환경 에너지 사업 대.",
89
- "sector": "에너지",
90
- },
91
- "네이버": {
92
- "current_price": random.randint(250000, 300000),
93
- "price_history": [],
94
- "description": "대한민국 대표 IT 플랫폼 기업. 검색 포털 1위. 쇼핑, 웹툰, 클라우드 등 다양한 IT 서비스 제공. AI 기술 투자.",
95
- "sector": "IT 플랫폼",
96
- },
97
- "카카오": {
98
- "current_price": random.randint(50000, 60000),
99
- "price_history": [],
100
- "description": "국민 메신저 카카오톡 운영. 택시, 페이, 게임, 웹툰 다양한 플랫폼 서비스 제공. 디지털 콘텐츠 Leading 기업.",
101
- "sector": "IT 플랫폼",
102
- },
103
- "삼성바이오로직스": {
104
- "current_price": random.randint(800000, 900000),
105
- "price_history": [],
106
- "description": "바이오 의약품 생산 전문 기업. 글로벌 CMO 시장 Leading. 바이오 의약품 산업 성장 주도.",
107
- "sector": "바이오",
108
- },
109
- "셀트리온": {
110
- "current_price": random.randint(200000, 250000),
111
- "price_history": [],
112
- "description": "바이오시밀러 개발 및 생산 기업. 램시마, 트룩시마 등 바이오시밀러 제품 글로벌 시장 확대.",
113
- "sector": "바이오",
114
- },
115
- "CJ제일제당": {
116
- "current_price": random.randint(350000, 400000),
117
- "price_history": [],
118
- "description": "대한민국 대표 식품 기업. 햇반, 비비고, 고메 등 다양한 식품 브랜드 보유. 글로벌 식품 시장 진출.",
119
- "sector": "식품",
120
- },
121
- "농심": {
122
- "current_price": random.randint(350000, 400000),
123
- "price_history": [],
124
- "description": "국민 라면 '신라면' 제조 기업. 짜파게티, 새우깡 브랜드 보유. 글로벌 라면 시장 확대.",
125
- "sector": "식품",
126
- },
127
- "대한항공": {
128
- "current_price": random.randint(25000, 30000),
129
- "price_history": [],
130
- "description": "대한민국 대표 항공사. 국내선, 국제선 항공 운송 서비제공. 글로벌 항공 네트워크 구축.",
131
- "sector": "운송",
132
- },
133
- "HMM": {
134
- "current_price": random.randint(25000, 30000),
135
- "price_history": [],
136
- "description": "대한민국 대표 해운 기업. 컨테이너선, 벌크선 다양한 선박 운영. 글로벌 해상 운송 서비스 제공.",
137
- "sector": "운송",
138
- },
139
- "KT": {
140
- "current_price": random.randint(35000, 40000),
141
- "price_history": [],
142
- "description": "대한민국 대표 통신 기업. 유무선 통신 서비스 공. 5G, AI, 클라우드 등 ICT 기술 Leading.",
143
- "sector": "통신",
144
- },
145
- "SK텔레콤": {
146
- "current_price": random.randint(60000, 70000),
147
- "price_history": [],
148
- "description": "대한민국 대표 이동통신 기업. 5G 서비스 Leading. AI, 메타버스 등 미래 기술 투자.",
149
- "sector": "통신",
150
- },
151
- "KB금융": {
152
- "current_price": random.randint(55000, 65000),
153
- "price_history": [],
154
- "description": "대한민국 대표 금융 그룹. 은행, 증권, 보험, 카드 다양한 금융 서비스 제공. 디지털 금융 혁신.",
155
- "sector": "금융",
156
- },
157
- "신한지주": {
158
- "current_price": random.randint(35000, 45000),
159
- "price_history": [],
160
- "description": "대한민국 대표 금융 지주 회사. 은행, 카드, 증권, 보험 등 금융 서비스 그룹. 글로벌 금융 시장 진출.",
161
- "sector": "금융",
162
- },
163
- "하이브": {
164
- "current_price": random.randint(250000, 300000),
165
- "price_history": [],
166
- "description": "BTS, 블랙핑크 소속사. K-POP 엔터테인먼트 Leading업. 글로벌 음악 시장 영향력 확대.",
167
- "sector": "엔터테먼트",
168
- },
169
- "CJ ENM": {
170
- "current_price": random.randint(90000, 110000),
171
- "price_history": [],
172
- "description": "방송, 영화, 음악, 공연 종합 엔터테인먼트 기업. tvN, Mnet 채널 운영. K-콘텐츠 작 Leading.",
173
- "sector": "엔터테인먼트",
174
- },
175
- "오리온": {
176
- "current_price": random.randint(150000, 180000),
177
- "price_history": [],
178
- "description": "초코파이, 오!감자, 포카칩 등 인기 과자 제조 기업. 글로벌 제과 시장 확대.",
179
- "sector": "식품",
180
- },
181
- "아모레퍼시픽": {
182
- "current_price": random.randint(150000, 170000),
183
- "price_history": [],
184
- "description": "설화수, 라네즈, 이니스프리 등 화장품 브랜드 보유. K-뷰티 Leading 기업. 글로벌 화장품 시장 진출.",
185
- "sector": "화장품",
186
- },
187
- "LG생활건강": {
188
- "current_price": random.randint(700000, 800000),
189
- "price_history": [],
190
- "description": "화장품, 생활용품, 음료 등 다양한 소비재 제조 기업. '숨', '후' 등 프리미엄 화장품 브랜드.",
191
- "sector": "생활용품",
192
- },
193
- "이마트": {
194
- "current_price": random.randint(120000, 140000),
195
- "price_history": [],
196
- "description": "대한민국 대표 대형 할인 마트. 신선 식품, 가공 식품, 생활 용품 등 판매. 온라인 쇼핑몰 운영.",
197
- "sector": "유통",
198
- },
199
- "롯데쇼핑": {
200
- "current_price": random.randint(180000, 200000),
201
- "price_history": [],
202
- "description": "백화점, 마트, 아울렛, 영화관 등 운영하는 유통 기업. 롯데백화점, 롯데마트 Leading 브랜드.",
203
- "sector": "유통",
204
- },
205
- "두산에너빌리티": {
206
- "current_price": random.randint(18000, 22000),
207
- "price_history": [],
208
- "description": "발전 설비, 해수 담수화 플랜트 등 에너지 인프라 기업. 친환경 에너지 기술 개발.",
209
- "sector": "에너지",
210
- },
211
- "HD현대": {
212
- "current_price": random.randint(50000, 60000),
213
- "price_history": [],
214
- "description": "조선, 건설기계, 에너지 등 종합 중공업 기업. 글로벌 조선 시장 Leading. 친환경 선박 기술 개발.",
215
- "sector": "중공업",
216
- },
217
- "GS건설": {
218
- "current_price": random.randint(40000, 50000),
219
- "price_history": [],
220
- "description": "자이 아파트 브랜드로 유명한 건설 기업. 국내외 건설 프로젝트 수행. 플랜트, 인프라 사업.",
221
- "sector": "건설",
222
- },
223
- }
224
- for stock_name in st.session_state["stocks"]:
225
- st.session_state["stocks"][stock_name]["price_history"].append(
226
- st.session_state["stocks"][stock_name]["current_price"]
227
- )
228
 
229
- # --- generate_news 함수 (섹터 관련 내용 제거) ---
230
  def generate_news():
231
  decade_count = st.session_state["decade_count"]
232
  difficulty_level = st.session_state['difficulty_level']
233
 
234
- difficulty_prompt_map = { # 난이도 설정 (기존 코드와 동일)
235
- "초등학생": { ... },
236
- "중학생": { ... },
237
- "고등학생": { ... },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  }
 
239
  level_config = difficulty_prompt_map.get(difficulty_level, difficulty_prompt_map["중학생"])
 
240
  level_prompt = level_config["level_desc"]
241
  sentence_count = level_config["sentence_count"]
242
  vocabulary_level = level_config["vocabulary_level"]
243
  inference_level = level_config["inference_level"]
244
 
245
- decade_keyword_map = { # Decade별 키워드 (기존 코드와 동일)
246
  1950: "1950년대 한국전쟁 이후 재건, 원조 경제, 농업, 경공업",
247
  1960: "1960년대 경제 개발 5개년 계획, 수출 주도 성장, 경공업 육성",
248
  1970: "1970년대 중화학공업 육성, 오일 쇼크, 새마을 운동, 건설업",
@@ -252,15 +348,15 @@ def generate_news():
252
  2010: "2010년대 스마트폰 보급, 소셜 미디어, 핀테크, 공유 경제, 바이오시밀러",
253
  2020: "2020년대 코로나19 팬데믹, 디지털 전환, 플랫폼 경제, ESG 경영, 미래 모빌리티, 인공지능",
254
  }
255
- decade_keywords = decade_keyword_map.get(decade_count, "2020년대 미래산업, 플랫폼 경제")
256
 
257
  prompt = f"""
258
  지시:
259
  {level_prompt}에 맞춰서, **{decade_count}년대 대한민국 경제**와 관련된 뉴스 기사 5개를 생성해주세요.
260
  **뉴스 주제**는 "{decade_keywords}" 키워드를 참고하여 시대적 특징을 반영해주세요.
261
  각 기사는 {sentence_count}문장 정도로 작성하고, {vocabulary_level}를 사용하여 학생들이 이해하기 쉬워야 합니다.
262
- 학생들이 뉴스를 읽고 {inference_level} 수준에서 **개별 회사**의 유망 여부를 스스로 추론할 수 있도록 {decade_count}년대 대한민국 경제 상황이나 산업 동향에 대한 뉴스를 만들어주세요. (**섹터 언급 없이, 개별 회사 중심으로** 뉴스 생성)
263
- **특정 회사 이름이나 주식 종목을 직접적으로 언급하지 마세요.**
264
  긍정적 뉴스, 부정적 뉴스, 중립적 뉴스 다양하게 생성하세요.(긍정, 부정, 중립 이라는 말은 표시하지 마세요.)
265
  뉴스에 따라 주식이 상승하기도 하고 하락하기도 할 수 있습니다.
266
  각 뉴스 기사는 "## 뉴스 [번호]" 로 시작해주세요. (예: ## 뉴스 1, ## 뉴스 2 ...)
@@ -270,7 +366,7 @@ def generate_news():
270
  chat_session = st.session_state["chat_session"]
271
  messages = [{"role": "user", "content": prompt}]
272
 
273
- try: # OpenAI API 호출 예외 처리 (기존 코드와 동일)
274
  response = client.chat.completions.create(
275
  model="gpt-4o-mini",
276
  messages=messages,
@@ -293,17 +389,17 @@ def generate_news():
293
 
294
  return news_articles[:5]
295
 
296
- except Exception as e: # API 호출 에러 처리 (기존 코드와 동일)
297
- st.error(f"뉴스 생성 중 오류 발생했습니다: {e}")
298
- return []
299
 
300
- # explain_daily_news_meanings 함수 (섹터 관련 내용 제거)
301
  def explain_daily_news_meanings(daily_news):
302
  if daily_news is None:
303
  return {}
304
 
305
  difficulty_level = st.session_state['difficulty_level']
306
- difficulty_prompt_map = { # 난이도 설정 (기존 코드와 동일)
307
  "초등학생": "초등학생 5~6학년",
308
  "중학생": "중학생 1~3학년",
309
  "고등학생": "고등학생 1~3학년",
@@ -318,13 +414,13 @@ def explain_daily_news_meanings(daily_news):
318
 
319
  **지시:**
320
  위 신문 기사의 핵심 의미를 {level_prompt}이 이해하기 쉽게 3문장 이내로 요약해서 "해설: " 다음에 설명해주세요.
321
- **더 관련 섹터 정보는 요 없습니다.**
322
 
323
  뉴스 의미 해설:
324
  """
325
  chat_session = st.session_state["chat_session"]
326
  messages = [{"role": "user", "content": prompt}]
327
- try: # OpenAI API 호출 예외 처리 (기존 코드와 동일)
328
  response = client.chat.completions.create(
329
  model="gpt-4o-mini",
330
  messages=messages,
@@ -340,13 +436,26 @@ def explain_daily_news_meanings(daily_news):
340
  meaning_text = response.choices[0].message.content.strip()
341
 
342
  explanation = ""
 
343
 
344
  if "해설:" in meaning_text:
345
  explanation_start_index = meaning_text.find("해설:") + len("해설:")
346
- explanation = meaning_text[explanation_start_index:].strip()
347
- meanings[str(i + 1)] = {"explanation": explanation} # 관련 섹터 정보 제거
 
 
 
348
 
349
- except Exception as e: # API 에러 처리 (기존 코드와 동일)
 
 
 
 
 
 
 
 
 
350
  st.error(
351
  f"API 할당량 초과 오류가 발생했습니다. 잠시 후 다시 시도해주세요. 오류 메시지: {e}"
352
  )
@@ -354,23 +463,26 @@ def explain_daily_news_meanings(daily_news):
354
  time.sleep(1)
355
  return meanings
356
 
357
- # buy_stock, sell_stock 함수 (섹터 인자 제거)
358
- def buy_stock(stock_name, quantity): # sector 인자 제거
359
- if stock_name not in st.session_state["stocks"]: # 섹터 없이 종목 이름으로만 확인
 
 
 
360
  st.session_state["messages"].append(
361
  {"type": "error", "text": "존재하지 않는 주식 종목입니다."}
362
  )
363
  return
364
 
365
- if quantity <= 0: # 수량 체크 (기존 코드와 동일)
366
  st.session_state["messages"].append(
367
  {"type": "error", "text": "매수 수량은 1주 이상이어야 합니다."}
368
  )
369
  return
370
 
371
- stock_price = st.session_state["stocks"][stock_name]["current_price"] # 섹터 없이 종목 이름으로 가격 접근
372
  max_quantity = st.session_state["portfolio"]["cash"] // stock_price
373
- if quantity > max_quantity: # 최대 매수 가능 수량 체크 (기존 코드와 동일)
374
  st.session_state["messages"].append(
375
  {
376
  "type": "error",
@@ -385,7 +497,7 @@ def buy_stock(stock_name, quantity): # sector 인자 제거
385
 
386
  total_price = stock_price * quantity
387
 
388
- if st.session_state["portfolio"]["cash"] >= total_price: # 잔액 확인 후 매수 처리 (기존 코드와 거의 동일)
389
  st.session_state["portfolio"]["cash"] -= total_price
390
  portfolio_stocks = st.session_state["portfolio"]["stocks"]
391
  if (
@@ -413,7 +525,7 @@ def buy_stock(stock_name, quantity): # sector 인자 제거
413
  f"{stock_name} {quantity}주 매수 완료. 총 {total_price:,.0f}원 소요.", icon="✅"
414
  )
415
  st.session_state['buy_confirm'] = False
416
- else: # 잔액 부족 에러 메시지 (기존 코드와 동일)
417
  st.session_state["messages"].append(
418
  {"type": "error", "text": "잔액이 부족합니다."}
419
  )
@@ -421,8 +533,8 @@ def buy_stock(stock_name, quantity): # sector 인자 제거
421
  st.error(f"잔액이 부족합니다. (최대 {max_quantity}주까지 매수 가능)")
422
  st.session_state['buy_confirm'] = False
423
 
424
- # sell_stock 함수 (기존 코드와 동일)
425
- def sell_stock(stock_name, quantity): # sell_stock 함수는 변경 없음
426
  if stock_name not in st.session_state["portfolio"]["stocks"]:
427
  st.session_state["messages"].append(
428
  {"type": "error", "text": "보유하고 있지 않은 주식입니다."}
@@ -450,13 +562,12 @@ def sell_stock(stock_name, quantity): # sell_stock 함수는 변경 없음
450
  return
451
 
452
  stock_price = 0
453
- stock_sector = "" # stock_sector 변수 더 이상 사용 안함
454
- for sector, stocks in st.session_state["stocks"].items(): # 섹터 iteration 불필요, 수정 필요
455
  if stock_name in stocks:
456
  stock_price = stocks[stock_name]["current_price"]
457
  stock_sector = sector
458
  break
459
- stock_price = st.session_state["stocks"][stock_name]["current_price"] # 수정: 섹터 iteration 없이 바로 접근
460
 
461
  if stock_price == 0:
462
  st.session_state["messages"].append(
@@ -482,49 +593,68 @@ def sell_stock(stock_name, quantity): # sell_stock 함수는 변경 없음
482
  st.success(f"{stock_name} {quantity}주 매도 완료. 총 {sell_price:,.0f}원 획득.")
483
  st.session_state['sell_confirm'] = False
484
 
485
- # update_stock_prices 함수 (섹터 영향도 거, 종목별 개별 변동)
486
  def update_stock_prices():
487
  if not st.session_state["daily_news"]:
488
  return
489
 
 
490
  decade_count = st.session_state["decade_count"]
491
- decade_economic_conditions = { # Decade별 경제 상황 (기존 코드와 동일)
492
- 1950: {"growth_rate": 0.02, "volatility": 0.03},
493
- 1960: {"growth_rate": 0.05, "volatility": 0.02},
494
- 1970: {"growth_rate": 0.08, "volatility": 0.05},
495
- 1980: {"growth_rate": 0.06, "volatility": 0.04},
496
- 1990: {"growth_rate": 0.03, "volatility": 0.1},
497
- 2000: {"growth_rate": 0.04, "volatility": 0.07},
498
- 2010: {"growth_rate": 0.03, "volatility": 0.05},
499
- 2020: {"growth_rate": 0.02, "volatility": 0.08},
500
  }
501
- economic_condition = decade_economic_conditions.get(decade_count, {"growth_rate": 0.03, "volatility": 0.06})
502
-
503
- # 섹터 영향도 계산 로직 제거
504
-
505
- for stock_name in st.session_state["stocks"]: # 모든 종목에 대해 주가 변동 적용
506
- # 경제 상황 반영 (기존 코드와 동일)
507
- base_change_rate = economic_condition["growth_rate"]
508
- volatility = economic_condition["volatility"]
509
- change_rate = random.uniform(-volatility, volatility) + base_change_rate # 섹터 영향도 제거
510
-
511
- change_rate = max(-0.2, min(0.2, change_rate)) # 변동폭 제한 (기존 코드와 동일)
512
- st.session_state["stocks"][stock_name]["current_price"] *= (
513
- 1 + change_rate
514
- )
515
- st.session_state["stocks"][stock_name]["current_price"] = max(
516
- 1, int(st.session_state["stocks"][stock_name]["current_price"])
517
- )
518
- st.session_state["stocks"][stock_name]["price_history"].append(
519
- st.session_state["stocks"][stock_name]["current_price"]
520
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  st.session_state["messages"].append({"type": "info", "text": "주가가 변동되었습니다."})
522
  st.toast("주가가 변동되었습니다.", icon="📈")
523
  st.info("주가가 변동되었습니다.")
524
- st.session_state["sector_news_impact"] = {} # 섹터 영향도 정보 초기화 (더 이상 사용 안함)
525
 
526
- # display_portfolio, display_stock_prices, display_portfolio_table 함수 (섹터 정보 표시 조정)
527
- def display_portfolio(): # display_portfolio 함수는 변경 없음
528
  portfolio = st.session_state["portfolio"]
529
  cash = portfolio["cash"]
530
  total_value = cash
@@ -533,15 +663,12 @@ def display_portfolio(): # display_portfolio 함수는 변경 없음
533
  quantity = stock_info["quantity"]
534
  purchase_price = stock_info["purchase_price"]
535
  current_price = 0
536
- stock_sector = "" # stock_sector 변수 더 이상 사용 안함
537
- for sector, stocks in st.session_state["stocks"].items(): # 섹터 iteration 불필요, 수정 필요
538
  if stock_name in stocks:
539
  current_price = stocks[stock_name]["current_price"]
540
- stock_sector = stocks[stock_name]["sector"] # description에서 sector 정보 가져옴
541
  break
542
- current_price = st.session_state["stocks"][stock_name]["current_price"] # 수정: 섹터 iteration 없이 바로 접근
543
- stock_sector = st.session_state["stocks"][stock_name]["sector"] # 수정: 섹터 iteration 없이 바로 접근
544
-
545
  if current_price != 0:
546
  stock_value = current_price * quantity
547
  total_value += stock_value
@@ -558,33 +685,34 @@ def display_portfolio(): # display_portfolio 함수는 변경 없음
558
 
559
  def display_stock_prices():
560
  stocks_data = []
561
- for stock_name, stock_info in st.session_state["stocks"].items(): # 섹터 iteration 없이 종목 iteration
562
- price_history = stock_info["price_history"]
563
- daily_change_rate_str = " - " # 기본값
564
- if len(price_history) >= 2:
565
- previous_day_price = price_history[-2]
566
- current_price = price_history[-1]
567
- daily_change_rate = (current_price - previous_day_price) / previous_day_price * 100
568
- daily_change_rate_str = f"{daily_change_rate:.2f}%"
569
-
570
- stocks_data.append(
571
- {
572
- "종목": stock_name,
573
- "섹터": stock_info["sector"], # description에서 sector 정보 가져옴
574
- "현재 주가": f"{stock_info['current_price']:,} 원",
575
- "전일 대비": daily_change_rate_str,
576
- "price_history": stock_info["price_history"],
577
- "description": stock_info["description"],
578
- }
579
- )
 
580
  stocks_df = pd.DataFrame(stocks_data)
581
- st.dataframe(stocks_df[["섹터", "종목", "현재 주가", "전일 대비"]], hide_index=True) # "섹터" 컬럼 추가
582
 
583
  selected_stock_all_info = st.selectbox(
584
  "종목 선택 (기업 정보 및 주가 그래프)", stocks_df["종목"].tolist()
585
  )
586
  if selected_stock_all_info:
587
- selected_stock_sector = stocks_df[ # selected_stock_sector 변수 더 이상 사용 안함
588
  stocks_df["종목"] == selected_stock_all_info
589
  ]["섹터"].iloc[0]
590
  col1_info, col2_graph = st.columns([1, 2])
@@ -592,34 +720,38 @@ def display_stock_prices():
592
  with col1_info:
593
  st.subheader("기업 정보")
594
  st.info(
595
- f"**{selected_stock_all_info} ({selected_stock_sector})**\n\n{st.session_state['stocks'][selected_stock_all_info]['description']}" # 섹터 정보 description에서 가져옴
596
  )
597
 
598
- with col2_graph: # 주가 그래프 (기존 코드와 동일)
599
  st.subheader("주가 그래프")
600
  price_history_df = pd.DataFrame(
601
  {
602
  "날짜": range(
603
  1,
604
  len(
605
- st.session_state["stocks"][selected_stock_all_info]["price_history"] # 섹터 없이 종목 이름으로 접근
 
 
606
  )
607
  + 1,
608
  ),
609
- "주가": st.session_state["stocks"][selected_stock_all_info]["price_history"], # 섹터 없이 종목 이름으로 접근
 
 
610
  }
611
  )
612
  fig = px.line(
613
  price_history_df,
614
  x="날짜",
615
  y="주가",
616
- title=f"{selected_stock_all_info} ({selected_stock_sector}) 주가 변동", # 섹터 정보 description에서 가져옴
617
  )
618
  st.plotly_chart(fig)
619
  else:
620
  st.info("종목을 선택하여 기업 정보와 주가 그래프를 확인하세요.")
621
 
622
- def display_portfolio_table(): # display_portfolio_table 함수는 섹터 정보 표시만 조정
623
  portfolio = st.session_state["portfolio"]
624
  if portfolio["stocks"]:
625
  portfolio_data = []
@@ -631,14 +763,12 @@ def display_portfolio_table(): # display_portfolio_table 함수는 섹터 정보
631
  quantity = stock_info["quantity"]
632
  purchase_price = stock_info["purchase_price"]
633
  current_price = 0
634
- stock_sector = "" # stock_sector 변수 더 이상 사용 안함
635
- for sector, stocks in st.session_state["stocks"].items(): # 섹터 iteration 불필요, 수정 필요
636
  if stock_name in stocks:
637
  current_price = stocks[stock_name]["current_price"]
638
- stock_sector = stocks[stock_name]["sector"] # description에서 sector 정보 가져옴
639
  break
640
- current_price = st.session_state["stocks"][stock_name]["current_price"] # 수정: 섹터 iteration 없이 바로 접근
641
- stock_sector = st.session_state["stocks"][stock_name]["sector"] # 수정: 섹터 iteration 없이 바로 접근
642
 
643
  if current_price == 0:
644
  continue
@@ -656,7 +786,7 @@ def display_portfolio_table(): # display_portfolio_table 함수는 섹터 정보
656
  portfolio_data.append(
657
  {
658
  "종목": stock_name,
659
- "섹터": stock_sector, # description에서 sector 정보 가져옴
660
  "보유 수량": quantity,
661
  "매수 단가": f"{purchase_price:,.0f} 원",
662
  "현재가": f"{current_price:,.0f} 원",
@@ -665,7 +795,7 @@ def display_portfolio_table(): # display_portfolio_table 함수는 섹터 정보
665
  "수익률": f"{profit_rate:.2f}%",
666
  }
667
  )
668
- portfolio_data.append( # 현금 row (기존 코드와 동일)
669
  {
670
  "종목": "현금",
671
  "섹터": "-",
@@ -679,7 +809,7 @@ def display_portfolio_table(): # display_portfolio_table 함수는 섹터 정보
679
  )
680
  portfolio_df = pd.DataFrame(portfolio_data)
681
  st.dataframe(portfolio_df, hide_index=True, height=350)
682
- st.markdown( # 총 자산 정보 (기존 코드와 동일)
683
  f"""**현금 잔고:** {portfolio['cash']:,} 원
684
  **📊 총 평가액:** {total_value:,.0f} 원
685
  **🛒 총 매수 금액:** {total_purchase_value:,.0f} 원
@@ -690,7 +820,7 @@ def display_portfolio_table(): # display_portfolio_table 함수는 섹터 정보
690
  st.info("보유 주식이 없습니다.")
691
 
692
  # display_stock_glossary 함수 (기존 코드와 동일)
693
- def display_stock_glossary(): # display_stock_glossary 함수는 변경 없음
694
  glossary = {
695
  "주식": "회사의 일부분을 나타내는 증서. 주식을 사면 회사의 주인이 되는 거예요.",
696
  "주가": "주식 1개당 가격. 사람들이 주식을 사고팔 때 가격이 변해요.",
@@ -714,15 +844,15 @@ def display_stock_glossary(): # display_stock_glossary 함수는 변경 없음
714
  def main():
715
  col_news, col_main_ui = st.columns([1, 2])
716
 
717
- with col_news: # 뉴스 영역 (기존 코드와 거의 동일)
718
- st.header(f"📰 {st.session_state['decade_count']}년대 뉴스")
719
  if st.button("뉴스 생성", use_container_width=True, key="news_gen_button"):
720
- with st.spinner(f"{st.session_state['decade_count']}년대 뉴스 생성 중..."):
721
  current_daily_news = generate_news()
722
  st.session_state["daily_news"] = current_daily_news
723
 
724
  if st.session_state["daily_news"]:
725
- st.subheader(f"{st.session_state['decade_count']}년대 뉴스")
726
  for i, news in enumerate(st.session_state["daily_news"]):
727
  with st.expander(f"뉴스 {i+1}", expanded=False):
728
  st.write(news)
@@ -730,14 +860,18 @@ def main():
730
  if st.session_state["previous_daily_news"] and st.session_state[
731
  "news_meanings"
732
  ]:
733
- previous_decade = st.session_state['decade_count'] - 10
734
- st.subheader(f"{previous_decade}년대 뉴스 해설")
735
  st.info("AI가 분석한 지난 뉴스 해설입니다.")
736
- with st.expander(f"{previous_decade}년대 뉴스 해설 보기", expanded=False):
737
  if st.session_state["news_meanings"]:
738
  for i, meaning_data in st.session_state["news_meanings"].items():
739
  st.markdown(f"**뉴스 {i}**:")
740
- st.markdown(f"**AI 해설:** {meaning_data['explanation']}") # 섹터 정보 제거
 
 
 
 
741
  else:
742
  st.info("지난 뉴스에 대한 해설이 없습니다.")
743
 
@@ -745,33 +879,39 @@ def main():
745
  st.info("뉴스 생성 버튼을 눌러 오늘의 뉴스를 받아보세요.")
746
 
747
  with col_main_ui:
748
- menu = st.tabs( # 탭 메뉴 (기존 코드와 동일)
749
  ['현재 주가', '내 포트폴리오', '주식 매수', '주식 매도', '지난 뉴스 해설']
750
  )
751
 
752
- with menu[0]: # 현재 주가 탭 (기존 코드와 동일)
753
  st.subheader("📈 현재 주가 및 기업 정보")
754
  st.markdown("주식 시장의 현재 가격과 기업 정보를 확인하세요.")
755
  display_stock_prices()
756
 
757
- with menu[1]: # 내 포트폴리오 탭 (기존 코드와 동일)
758
  st.subheader("📊 내 포트폴리오")
759
  st.markdown("현재 보유 중인 주식과 자산을 확인하세요.")
760
  display_portfolio_table()
761
 
762
- with menu[2]: # 주식 매수 탭 (섹터 선택 제거, 종목 선택 방식 변경)
763
  st.subheader("💰 주식 매수")
764
  st.markdown("AI 예측과 뉴스 분석을 바탕으로 주식을 매수해보세요.")
765
- stock_names_buy = list(st.session_state["stocks"].keys()) # 섹터 선택 없이 전체 종목 리스트 사용
766
- selected_stock_buy = st.selectbox("매수 종목 선택:", stock_names_buy) # 종목 선택 UI
 
 
 
 
767
 
768
- stock_price_buy = st.session_state["stocks"][selected_stock_buy]["current_price"] # 섹터 없이 종목 이름으로 가격 접근
 
 
769
  st.info(f"**{selected_stock_buy}** 현재 주가: {stock_price_buy:,.0f}원")
770
  quantity_buy = st.number_input(
771
  "매수 수량 (주):", min_value=1, value=1, step=1
772
  )
773
 
774
- if not st.session_state['buy_confirm']: # 매수 확인/취소 로직 (기존 코드와 동일)
775
  if st.button("주식 매수", use_container_width=True, key='buy_button_confirm'):
776
  st.session_state['buy_confirm'] = True
777
  else:
@@ -779,25 +919,24 @@ def main():
779
  col_confirm, col_cancel = st.columns([1, 1])
780
  with col_confirm:
781
  if st.button("✅ 매수 확인", use_container_width=True, key='buy_confirm_button'):
782
- buy_stock(selected_stock_buy, quantity_buy) # buy_stock 함수 호출 (sector 인자 제거)
783
 
784
  with col_cancel:
785
  if st.button("❌ 매수 취소", use_container_width=True, key='buy_cancel_button', type='secondary'):
786
  st.session_state['buy_confirm'] = False
787
  st.info("매수를 취소했습니다.")
788
 
789
- with menu[3]: # 주식 매도 탭 (기존 코드와 동일)
790
  st.subheader("📉 주식 매도")
791
  st.markdown("보유 중인 주식을 판매하고 수익을 실현해보세요.")
792
  if st.session_state["portfolio"]["stocks"]:
793
  stock_names_sell = list(st.session_state["portfolio"]["stocks"].keys())
794
  selected_stock_sell = st.selectbox("매도 종목 선택:", stock_names_sell)
795
  stock_price_sell = 0
796
- for sector, stocks in st.session_state["stocks"].items(): # 섹터 iteration 불필요, 수정 필요
797
  if selected_stock_sell in stocks:
798
  stock_price_sell = stocks[selected_stock_sell]["current_price"]
799
  break
800
- stock_price_sell = st.session_state["stocks"][selected_stock_sell]["current_price"] # 수정: 섹터 iteration 없이 바로 접근
801
 
802
  st.info(f"**{selected_stock_sell}** 현재 주가: {stock_price_sell:,.0f}원")
803
  max_sell_quantity = st.session_state["portfolio"]["stocks"][
@@ -810,7 +949,7 @@ def main():
810
  value=1,
811
  step=1,
812
  )
813
- if not st.session_state['sell_confirm']: # 매도 확인/취소 로직 (기존 코드와 동일)
814
  if st.button("주식 매도", use_container_width=True, key='sell_button_confirm'):
815
  st.session_state['sell_confirm'] = True
816
  else:
@@ -818,7 +957,7 @@ def main():
818
  col_confirm, col_cancel = st.columns([1, 1])
819
  with col_confirm:
820
  if st.button("✅ 매도 확인", use_container_width=True, key='sell_confirm_button'):
821
- sell_stock(selected_stock_sell, quantity_sell) # sell_stock 함수 호출 (기존 코드와 동일)
822
  with col_cancel:
823
  if st.button("❌ 매도 취소", use_container_width=True, key='sell_cancel_button', type='secondary'):
824
  st.session_state['sell_confirm'] = False
@@ -826,12 +965,12 @@ def main():
826
  else:
827
  st.info("보유 주식이 없습니다. 포트폴리오 탭에서 확인하세요.")
828
 
829
- with menu[4]: # 지난 뉴스 해설 탭 (기존 코드와 동일)
830
  if st.session_state["previous_daily_news"] and st.session_state[
831
  "news_meanings"
832
  ]:
833
- previous_decade = st.session_state['decade_count'] - 10
834
- st.subheader(f"{previous_decade}년대 뉴스 해설")
835
  st.info("AI가 분석한 지난 뉴스 해설입니다.")
836
  for i in range(len(st.session_state["previous_daily_news"])):
837
  with st.expander(f"뉴스 {i+1}", expanded=False):
@@ -841,7 +980,12 @@ def main():
841
  meaning_data = st.session_state["news_meanings"].get(str(i+1))
842
  if meaning_data:
843
  st.markdown("**AI 해설:**")
844
- st.info(meaning_data['explanation']) # 섹터 정보 제거
 
 
 
 
 
845
  else:
846
  st.warning("뉴스 해설을 생성하지 못했습니다.")
847
  else:
@@ -849,12 +993,13 @@ def main():
849
  "이전 뉴스 해설이 없습니다. 10년 후 버튼을 눌러 뉴스 해설을 받아보세요."
850
  )
851
 
852
- with st.sidebar: # 사이드바 (기존 코드와 거의 동일)
853
- st.markdown(f"# ⏳ 대한민국 경제 발전사 게임")
854
- st.markdown(f"### {st.session_state['decade_count']}년대")
855
  st.markdown("---")
856
 
857
- difficulty_level = st.selectbox( # 난이도 선택 (기존 코드와 동일)
 
858
  "📈 난이도 선택",
859
  ["초등학생", "중학생", "고등학생"],
860
  index=["초등학생", "중학생", "고등학생"].index(st.session_state['difficulty_level'])
@@ -862,7 +1007,7 @@ def main():
862
  st.session_state['difficulty_level'] = difficulty_level
863
  st.markdown("---")
864
 
865
- decade_description_map = { # Decade별 게임 설명 (기존 코드와 동일)
866
  1950: "🇰🇷 **1950년대: 재건의 시대**\n\n 전쟁의 상처를 딛고 일어서는 시기입니다. 원조 경제와 농업을 기반으로 재건에 힘쓰세요.",
867
  1960: "🇰🇷 **1960년대: 경제 개발의 닻을 올리다**\n\n 경제 개발 5개년 계획이 시작됩니다. 수출과 경공업을 통해 경제 성장의 기반을 마련하세요.",
868
  1970: "🇰🇷 **1970년대: 중화학공업, 성장의 엔진**\n\n 중화학공업 육성 정책이 본격화됩니다. 건설 붐과 함께 중동 시장을 개척해보세요.",
@@ -872,19 +1017,19 @@ def main():
872
  2010: "🇰🇷 **2010년대: 스마트 혁명, 플랫폼 경제**\n\n 스마트폰이 세상을 바꾸고, 플랫폼 기업이 성장합니다. 새로운 경제 질서에 올라타세요.",
873
  2020: "🇰🇷 **2020년대: 미래를 향한 도전**\n\n 팬데믹을 극복하고 미래 산업을 육성합니다. 친환경 에너지, 바이오, 플랫폼 기업의 미래를 예측해보세요.",
874
  }
875
- decade_description = decade_description_map.get(st.session_state['decade_count'], "🇰🇷 **미래 시대:**\n\n 미래 시대에 대비하여 새로운 산업과 기술에 투자해보세요.")
876
 
877
- st.markdown(decade_description)
878
 
879
- cash, total_value, profit_rate = display_portfolio() # 포트폴리오 정보 (기존 코드와 동일)
880
  st.metric(label="💰 현금 잔고", value=f"{cash:,.0f} 원")
881
  st.metric(label="📊 총 평가 금액", value=f"{total_value:,.0f} 원")
882
  st.metric(label="🚀 총 수익률", value=f"{profit_rate:.2f}%")
883
  st.markdown("---")
884
 
885
- if st.button("10년 후", use_container_width=True, key="decade_pass_button"): # 10년 후 버튼 (기존 코드와 동일)
886
  if st.session_state["daily_news"]:
887
- with st.spinner(f"{st.session_state['decade_count']}년대 주가 변동 및 지난 뉴스 분석..."):
888
  st.session_state["previous_daily_news"] = st.session_state["daily_news"]
889
  meanings = explain_daily_news_meanings(
890
  st.session_state["previous_daily_news"]
@@ -893,18 +1038,18 @@ def main():
893
  st.session_state["news_meanings"] = meanings
894
  update_stock_prices()
895
  st.session_state["daily_news"] = generate_news()
896
- st.session_state["decade_count"] += 10
897
- # st.session_state["stocks"] = initialize_stocks_for_decade(st.session_state["decade_count"]) # 이상 Decade stocks 초기화 안함
898
  st.rerun()
899
- previous_decade = st.session_state['decade_count'] - 10
900
- st.info(f"지난 {previous_decade}년대 뉴스 해설 탭에서 AI 분석을 확인해보세요.")
901
  else:
902
  st.warning("오늘의 뉴스를 먼저 생성해주세요.")
903
  st.markdown("***")
904
 
905
- display_stock_glossary() # 주식 용어 사전 (기존 코드와 동일)
906
 
907
- with st.expander("🚀 앱 사용 가이드", expanded=False): # 앱 사용 가이드 (기존 코드와 동일)
908
  st.markdown(
909
  """
910
  **대한민국 경제 발전사 주식 투자 게임**에 오신 것을 환영합니다! ⏳
@@ -948,8 +1093,8 @@ def main():
948
 
949
  **대한민국 경제 발전사를 따라가는 흥미진진한 주식 투자 게임! 지금 시작하세요!** 🚀
950
  """.format(decade=st.session_state['decade_count'], next_decade=st.session_state['decade_count'] + 10)
951
-
952
- # Supabase 관련 함수 및 로그인 사이드바 제거
953
 
954
  if __name__ == "__main__":
 
955
  main()
 
6
  import plotly.express as px
7
  from openai import OpenAI
8
 
9
+ # --- Streamlit 설정 ---
10
+ st.set_page_config(
11
+ page_title="⏳ 대한민국 경제 발전사 주식 투자 게임",
12
+ page_icon="📈",
13
+ layout="wide",
14
+ initial_sidebar_state="expanded",
15
+ )
16
+
17
+ # --- Custom CSS (스타일링) ---
18
+ st.markdown(
19
+ """
20
+ <style>
21
+ /* 전체 폰트 (Nanum Gothic) */
22
+ @import url('https://fonts.googleapis.com/css2?family=Nanum+Gothic:wght@400;700&display=swap');
23
+ body {
24
+ font-family: 'Nanum Gothic', sans-serif !important;
25
+ }
26
+ /* 탭 메뉴 스타일 */
27
+ .stTabs [data-baseweb="tab-list"] button[aria-selected="true"] {
28
+ background-color: #007bff !important;
29
+ color: white !important;
30
+ font-weight: bold;
31
+ }
32
+ .stTabs [data-baseweb="tab-list"] button {
33
+ background-color: #f0f2f6;
34
+ color: #333;
35
+ border-radius: 8px 8px 0 0;
36
+ padding: 0.75em 1em;
37
+ margin-bottom: -1px; /* border overlap */
38
+ }
39
+ /* 사이드바 스타일 */
40
+ [data-testid="stSidebar"] {
41
+ width: 350px !important;
42
+ background-color: #f8f9fa; /* Light gray sidebar background */
43
+ padding: 20px;
44
+ }
45
+ [data-testid="stSidebar"] h1, [data-testid="stSidebar"] h3 {
46
+ color: #212529; /* Dark gray sidebar headings */
47
+ }
48
+ [data-testid="stSidebar"] hr {
49
+ border-top: 1px solid #e0e0e0; /* Lighter sidebar hr */
50
+ }
51
+ /* Metric 스타일 */
52
+ .streamlit-metric-label {
53
+ font-size: 16px;
54
+ color: #4a4a4a;
55
+ }
56
+ .streamlit-metric-value {
57
+ font-size: 28px;
58
+ font-weight: bold;
59
+ }
60
+ /* 버튼 스타일 */
61
+ div.stButton > button {
62
+ background-color: #007bff;
63
+ color: white;
64
+ padding: 12px 24px;
65
+ font-size: 16px;
66
+ border-radius: 8px;
67
+ border: none;
68
+ box-shadow: 2px 2px 5px rgba(0,0,0,0.1); /* Soft shadow */
69
+ transition: background-color 0.3s ease;
70
+ }
71
+ div.stButton > button:hover {
72
+ background-color: #0056b3;
73
+ box-shadow: 2px 2px 7px rgba(0,0,0,0.15); /* Slightly stronger shadow on hover */
74
+ }
75
+ /* 보조 버튼 스타일 */
76
+ div.stButton > button.secondary-button {
77
+ background-color: #6c757d;
78
+ color: white;
79
+ padding: 10px 20px;
80
+ font-size: 14px;
81
+ border-radius: 6px;
82
+ border: none;
83
+ transition: background-color 0.3s ease;
84
+ }
85
+ div.stButton > button.secondary-button:hover {
86
+ background-color: #5a6268;
87
+ }
88
+ /* Expander 스타일 */
89
+ .streamlit-expanderHeader {
90
+ font-weight: bold;
91
+ color: #212529;
92
+ border-bottom: 1px solid #e0e0e0;
93
+ padding-bottom: 8px;
94
+ margin-bottom: 15px;
95
+ }
96
+ /* Dataframe 스타일 */
97
+ .dataframe {
98
+ border: 1px solid #e0e0e0;
99
+ border-radius: 8px;
100
+ padding: 12px;
101
+ box-shadow: 2px 2px 5px rgba(0,0,0,0.05); /* Very subtle shadow */
102
+ }
103
+ /* Info, Success, Error, Warning Box 스타일 */
104
+ div.stInfo, div.stSuccess, div.stError, div.stWarning {
105
+ border-radius: 8px;
106
+ padding: 15px;
107
+ margin-bottom: 15px;
108
+ box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
109
+ }
110
+ div.stInfo {
111
+ background-color: #e7f3ff;
112
+ border-left: 5px solid #007bff;
113
+ }
114
+ div.stSuccess {
115
+ background-color: #e6f7ec;
116
+ border-left: 5px solid #28a745;
117
+ }
118
+ div.stError {
119
+ background-color: #fdeded;
120
+ border-left: 5px solid #dc3545;
121
+ }
122
+ div.stWarning {
123
+ background-color: #fffbe6;
124
+ border-left: 5px solid #ffc107;
125
+ }
126
+ /* Toast message 스타일 */
127
+ div.streamlit-toast-container {
128
+ z-index: 10000; /* Toast를 항상 맨 위에 표시 */
129
+ }
130
+ div[data-testid="stToast"] {
131
+ border-radius: 8px;
132
+ padding: 15px;
133
+ box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
134
+ }
135
+ </style>
136
+ """,
137
+ unsafe_allow_html=True,
138
+ )
139
+
140
+ # --- API 키 설정 ---
141
  if "OPENAI_API_KEY" not in os.environ:
142
+ st.error(
143
+ "OPENAI_API_KEY 환경 변수가 설정되지 않았습니다. Hugging Face Secrets 또는 환경 변수에 API 키를 설정해주세요."
144
+ )
145
  st.stop()
146
+
147
  client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
148
 
149
  # --- 세션 상태 초기화 ---
 
152
  if "portfolio" not in st.session_state:
153
  st.session_state["portfolio"] = {"cash": 10000000, "stocks": {}}
154
  if "stocks" not in st.session_state:
155
+ st.session_state["stocks"] = {} # Decade별로 stocks 데이터 초기화하므로, 여기서는 빈 딕셔너리로 시작
156
  if "news_analysis_results" not in st.session_state:
157
  st.session_state["news_analysis_results"] = {}
158
+ if "messages" not in st.session_state:
159
+ st.session_state["messages"] = []
160
+ if "daily_news" not in st.session_state:
161
+ st.session_state["daily_news"] = None
162
+ if "previous_daily_news" not in st.session_state:
163
+ st.session_state["previous_daily_news"] = None
164
+ if "news_date" not in st.session_state:
165
+ st.session_state["news_date"] = None
166
+ if "news_meanings" not in st.session_state:
167
+ st.session_state["news_meanings"] = {}
168
+ if (
169
+ "ai_news_analysis_output" not in st.session_state
170
+ ):
171
+ st.session_state["ai_news_analysis_output"] = {}
172
+ if "day_count" not in st.session_state:
173
+ st.session_state["day_count"] = 1
174
+ if "decade_count" not in st.session_state: # Decade 카운트 추가
175
+ st.session_state["decade_count"] = 1950 # 시작 년도 설정
176
+ if "sector_news_impact" not in st.session_state:
177
+ st.session_state["sector_news_impact"] = {}
178
+ if 'buy_confirm' not in st.session_state:
179
+ st.session_state['buy_confirm'] = False
180
+ if 'sell_confirm' not in st.session_state:
181
+ st.session_state['sell_confirm'] = False
182
+ if 'difficulty_level' not in st.session_state:
183
+ st.session_state['difficulty_level'] = "중학생" # 기본 난이도 중학생으로 변경
184
+
185
+ # --- 주식 데이터 초기화 함수 (Decade별) ---
186
+ def initialize_stocks_for_decade(decade):
187
+ if decade == 1950:
188
+ return {
189
+ "재건": {
190
+ "대한방직": {"current_price": random.randint(100, 200), "price_history": [], "description": "6.25 전쟁 이후 재건 시대, 섬유 산업의 부흥을 이끈 대표 기업."},
191
+ "조선맥주": {"current_price": random.randint(50, 100), "price_history": [], "description": "국민들의 갈증을 해소하며 성장한 맥주 회사. 현재는 하이트진로."},
192
+ "락희화학": {"current_price": random.randint(80, 150), "price_history": [], "description": "플라스틱 산업의 선구자, 럭키화학! 현재 LG화학의 모태."},
193
+ },
194
+ "농업": {
195
+ "경성고무": {"current_price": random.randint(30, 60), "price_history": [], "description": "농업용 고무 제품 생산. 생활 필수품 수요 증가로 성장."},
196
+ "고려제당": {"current_price": random.randint(40, 80), "price_history": [], "description": "설탕, 밀가루 기초 식량 . CJ제일제당의 전신."},
197
+ }
198
+ }
199
+ elif decade == 1960:
200
+ return {
201
+ "경공업": {
202
+ "삼성물산": {"current_price": random.randint(300, 600), "price_history": [], "description": "수출 주도 경제 성장 견인. 종합상사해외 시장 개척."},
203
+ "선경직물": {"current_price": random.randint(250, 500), "price_history": [], "description": "Made in Korea 신화의 주역. 섬유 수출의 선두 주자, 현재 SK네트웍스."},
204
+ "태평양화학": {"current_price": random.randint(200, 400), "price_history": [], "description": "화장품 산업의 태동. ABC 크림으로 화장품 대중화, 아모레퍼시픽."},
205
+ },
206
+ "식품": {
207
+ "신동방": {"current_price": random.randint(150, 300), "price_history": [], "description": "라면, 스낵 등 인스턴트 식품 인기. 농심의 옛 이름."},
208
+ "미원": {"current_price": random.randint(120, 250), "price_history": [], "description": "MSG 조미료 '미원'으로 식탁 변화. 대상그룹의 모태."},
209
+ }
210
+ }
211
+ elif decade == 1970:
212
+ return {
213
+ "중화학공업": {
214
+ "포항제철": {"current_price": random.randint(800, 1500), "price_history": [], "description": "산업 쌀, 철강 생산. 국가 발전의 핵심 동력, 현POSCO홀딩스."},
215
+ "현대조선": {"current_price": random.randint(700, 1300), "price_history": [], "description": "수출 효자 산업, 조선업. 세계적인 조선 강국으로 발돋움."},
216
+ "기아산업": {"current_price": random.randint(600, 1200), "price_history": [], "description": "자동차 산업 육성 정책 수혜. 브리사, 승용차 생산 시작, 현재 기아."},
217
+ "금성사": {"current_price": random.randint(500, 1000), "price_history": [], "description": "가전 제품 국산화. 흑백 TV, 냉장고 생산, 현재 LG전자."},
218
+ },
219
+ "건설": {
220
+ "현대건설": {"current_price": random.randint(400, 800), "price_history": [], "description": "중동 건설 주도. 해외 건설 시장 진출, 현재 건설."},
221
+ "대림산업": {"current_price": random.randint(350, 700), "price_history": [], "description": "국내 건설 시장 성장. 아파트 건설 붐, 현재 DL이앤씨."},
222
+ }
223
+ }
224
+ elif decade == 1980:
225
+ return {
226
+ "전자": {
227
+ "삼성전자": {"current_price": random.randint(1500, 3000), "price_history": [], "description": "반도체 산업 투자 확대. D램 개발, 세계적인 IT 기업으로 성장."},
228
+ "금성사": {"current_price": random.randint(1200, 2500), "price_history": [], "description": "컬러 TV, VCR 생산. 가전 제품 수출 증가, 현재 LG전자."},
229
+ "대우전자": {"current_price": random.randint(1000, 2000), "price_history": [], "description": "전자 산업 경쟁 심화. '탱크주의'로 유명, 현재 위니아."},
230
+ },
231
+ "자동차": {
232
+ "현대자동차": {"current_price": random.randint(1300, 2700), "price_history": [], "description": "포니, 엑셀 수출. 자동차 대중화 시대 개막, 현재 현대자동차."},
233
+ "기아자동차": {"current_price": random.randint(1100, 2400), "price_history": [], "description": "프라이드 출시. 소형차 시장 확대, 현재 기아."},
234
+ "대우자동차": {"current_price": random.randint(900, 1800), "price_history": [], "description": "르망 출시. 다양한 차종 개발 경쟁, 현재 한국GM."},
235
+ }
236
+ }
237
+ elif decade == 1990:
238
+ return {
239
+ "정보통신": {
240
+ "한국통신": {"current_price": random.randint(2500, 5000), "price_history": [], "description": "통신 시장 개방 및 민영화. 초고속 인터넷 서비스 시작, 현재 KT."},
241
+ "SK텔레콤": {"current_price": random.randint(2300, 4500), "price_history": [], "description": "이동통신 서비스 확산. 디지털 이동통신 'CDMA' 상용화, 현재 SK텔레콤."},
242
+ "LG텔레콤": {"current_price": random.randint(2000, 4000), "price_history": [], "description": "PCS 서비스 경쟁. 이동통신 시장 경쟁 심화, 현재 LG유플러스."},
243
+ "데이콤": {"current_price": random.randint(1800, 3500), "price_history": [], "description": "국제전화, 시외전화 서비스. 통신 시장 다변화, 현재 LG유플러스."},
244
+ },
245
+ "금융": {
246
+ "국민은행": {"current_price": random.randint(1600, 3200), "price_history": [], "description": "금융 시장 자유화. 은행 경쟁 심화, 현재 KB국민은행."},
247
+ "신한은행": {"current_price": random.randint(1400, 2800), "price_history": [], "description": "IMF 외환 위기 속에서 성장. M&A 통해 몸집 불린 은행, 현재 신한은행."},
248
+ "하나은행": {"current_price": random.randint(1300, 2600), "price_history": [], "description": "외환 은행 강자. 국제 금융 업무 특화, 현재 하나은행."},
249
+ }
250
+ }
251
+ elif decade == 2000:
252
+ return {
253
+ "IT": {
254
+ "네이버": {"current_price": random.randint(4000, 8000), "price_history": [], "description": "인터넷 시대 개막. 검색 포털 1위, IT 강국 코리아."},
255
+ "다음": {"current_price": random.randint(3500, 7000), "price_history": [], "description": "Daum 카페, 이메일 서비스 인기. 네이버와 경쟁."},
256
+ "NHN": {"current_price": random.randint(3000, 6000), "price_history": [], "description": "온라인 게임 시성장. '한게임' 포털 운영, 현재 NHN."},
257
+ "싸이월드": {"current_price": random.randint(2500, 5000), "price_history": [], "description": "소셜 미디어 열풍. 미니홈피, 아바타 서비스 인기."},
258
+ },
259
+ "엔터테인먼트": {
260
+ "SM엔터테인먼트": {"current_price": random.randint(2000, 4000), "price_history": [], "description": "한류 열풍 시작. BoA, 동방신기 등 아이돌 그룹 인기."},
261
+ "YG엔터테인먼트": {"current_price": random.randint(1800, 3500), "price_history": [], "description": "힙합 음악 대중화. 빅뱅, 2NE1 등 개성 강한 그룹."},
262
+ "JYP엔터테인먼트": {"current_price": random.randint(1600, 3200), "price_history": [], "description": "댄음악 강세. god, 원더걸스 국민 그룹 배출."},
263
+ }
264
+ }
265
+ elif decade == 2010:
266
+ return {
267
+ "플랫폼": {
268
+ "카카오": {"current_price": random.randint(6000, 12000), "price_history": [], "description": "모바일 메신저 시대. 카카오톡, 국민 앱으로 성장."},
269
+ "네이버": {"current_price": random.randint(5500, 11000), "price_history": [], "description": "모바일 플랫폼 강화. 라인 메신저 해외 진출."},
270
+ "쿠팡": {"current_price": random.randint(5000, 10000), "price_history": [], "description": "이커머스 시장 급성장. 로켓 배송으로 온라인 쇼핑 혁신."},
271
+ "배달의민족": {"current_price": random.randint(4500, 9000), "price_history": [], "description": "O2O 서비스 확대. 배달 앱 시장 선점, 현재 우아한형제들."},
272
+ },
273
+ "바이오": {
274
+ "셀트리온": {"current_price": random.randint(4000, 8000), "price_history": [], "description": "바이오시밀러 개발 성공. 바이오 의약품 수출, K-바이오."},
275
+ "삼성바이오로직스": {"current_price": random.randint(3500, 7000), "price_history": [], "description": "바이오 의약품 생산 대행. CMO 사업 성장, 바이오 산업 육성."},
276
+ "메디톡스": {"current_price": random.randint(3000, 6000), "price_history": [], "description": "보톡스 국산화 성공. 미용 의료 시장 확대."},
277
+ }
278
+ }
279
+ elif decade == 2020:
280
+ return {
281
+ "미래산업": {
282
+ "LG에너지솔루션": {"current_price": random.randint(8000, 16000), "price_history": [], "description": "전기차 배터리 시장 선도. 친환경 에너지 시대 개막."},
283
+ "SK하이닉스": {"current_price": random.randint(7000, 14000), "price_history": [], "description": "반도체 슈퍼 호황. 메모리 반도체 수요 폭증, IT 강국 위상."},
284
+ "현대자동차": {"current_price": random.randint(6500, 13000), "price_history": [], "description": "전기차, 수소차 개발 박차. 미래 모빌리티 전환."},
285
+ "카카오뱅크": {"current_price": random.randint(6000, 12000), "price_history": [], "description": "디지털 금융 혁신. 인터넷 전문 은행 시대 개막."},
286
+ "하이브": {"current_price": random.randint(5500, 11000), "price_history": [], "description": "K-팝 세계화. BTS, 블랙핑크 글로벌 팬덤 확보."},
287
+ },
288
+ "플랫폼": { # 2020년대 플랫폼 기업들은 미래산업 섹터에도 포함될 수 있지만, 분리하여 표현
289
+ "네이버": {"current_price": random.randint(5000, 10000), "price_history": [], "description": "AI, 클라우드 등 신기술 투자 확대. 플랫폼 경쟁 심화."},
290
+ "카카오": {"current_price": random.randint(4500, 9000), "price_history": [], "description": "ESG 경영 강화. 사회적 책임 강조, 플랫폼 규제."},
291
+ }
292
+ }
293
+ else: # 2030년 이후를 위한 기본 데이터 (현재 2020년대 데이터와 유사하게 설정)
294
+ return {
295
+ "미래산업": {
296
+ "LG에너지솔루션": {"current_price": random.randint(8000, 16000), "price_history": [], "description": "전기차 배터리 시장 선도. 친환경 에너지 시대 개막."},
297
+ "SK하이닉스": {"current_price": random.randint(7000, 14000), "price_history": [], "description": "반도체 슈퍼 호황. 메모리 반도체 수요 폭증, IT 강국 위상."},
298
+ "현대자동차": {"current_price": random.randint(6500, 13000), "price_history": [], "description": "전차, 수소차 개발 박차. 미래 모빌리티 전환."},
299
+ "카카오뱅크": {"current_price": random.randint(6000, 12000), "price_history": [], "description": "디지털 금융 혁신. 터넷 전문 은행 시대 개막."},
300
+ "하이브": {"current_price": random.randint(5500, 11000), "price_history": [], "description": "K-팝 세계화. BTS, 블랙핑크 글로벌 팬덤 확보."},
301
+ },
302
+ "플랫폼": { # 2020년대 플랫폼 기업들은 미래산업 섹터에도 포함될 수 있지만, 분리하여 표현
303
+ "네이버": {"current_price": random.randint(5000, 10000), "price_history": [], "description": "AI, 클라우드 등 신기술 투자 확대. 플랫폼 경쟁 심화."},
304
+ "카카오": {"current_price": random.randint(4500, 9000), "price_history": [], "description": "ESG 경영 강화. 사회적 책임 강조, 플랫폼 제."},
305
+ }
306
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
 
308
+ # --- 뉴스 생성 함수 ---
309
  def generate_news():
310
  decade_count = st.session_state["decade_count"]
311
  difficulty_level = st.session_state['difficulty_level']
312
 
313
+ difficulty_prompt_map = {
314
+ "초등학생": {
315
+ "level_desc": "초등학생 5~6학년 수준",
316
+ "sentence_count": "8~10",
317
+ "vocabulary_level": "쉬운 어휘와 짧은 문장",
318
+ "inference_level": "단순하고 명확한 정보",
319
+ },
320
+ "중학생": {
321
+ "level_desc": "중학생 1~3학년 수준",
322
+ "sentence_count": "10~12",
323
+ "vocabulary_level": "일상적인 어휘와 약간의 전문 용어",
324
+ "inference_level": "일반적인 경제 흐름과 관련된 추론",
325
+ },
326
+ "고등학생": {
327
+ "level_desc": "고등학생 1~3학년 수준",
328
+ "sentence_count": "12~15",
329
+ "vocabulary_level": "다양한 어휘와 경제 전문 용어 포함",
330
+ "inference_level": "심층적인 경제 분석 및 다각적인 추론",
331
+ },
332
  }
333
+
334
  level_config = difficulty_prompt_map.get(difficulty_level, difficulty_prompt_map["중학생"])
335
+
336
  level_prompt = level_config["level_desc"]
337
  sentence_count = level_config["sentence_count"]
338
  vocabulary_level = level_config["vocabulary_level"]
339
  inference_level = level_config["inference_level"]
340
 
341
+ decade_keyword_map = { # Decade별 경제 뉴스 키워드 설정
342
  1950: "1950년대 한국전쟁 이후 재건, 원조 경제, 농업, 경공업",
343
  1960: "1960년대 경제 개발 5개년 계획, 수출 주도 성장, 경공업 육성",
344
  1970: "1970년대 중화학공업 육성, 오일 쇼크, 새마을 운동, 건설업",
 
348
  2010: "2010년대 스마트폰 보급, 소셜 미디어, 핀테크, 공유 경제, 바이오시밀러",
349
  2020: "2020년대 코로나19 팬데믹, 디지털 전환, 플랫폼 경제, ESG 경영, 미래 모빌리티, 인공지능",
350
  }
351
+ decade_keywords = decade_keyword_map.get(decade_count, "2020년대 미래산업, 플랫폼 경제") # 기본 키워드 설정
352
 
353
  prompt = f"""
354
  지시:
355
  {level_prompt}에 맞춰서, **{decade_count}년대 대한민국 경제**와 관련된 뉴스 기사 5개를 생성해주세요.
356
  **뉴스 주제**는 "{decade_keywords}" 키워드를 참고하여 시대적 특징을 반영해주세요.
357
  각 기사는 {sentence_count}문장 정도로 작성하고, {vocabulary_level}를 사용하여 학생들이 이해하기 쉬워야 합니다.
358
+ 학생들이 뉴스를 읽고 {inference_level} 수준에서 어떤 회사 유망할지 또는 쇠락할지 스스로 추론할 수 있도록 {decade_count}년대 대한민국 경제 상황이나 산업 동향에 대한 뉴스를 만들어주세요.
359
+ 특정 회사 이름이나 주식 종목을 직접적으로 언급하지 마세요.
360
  긍정적 뉴스, 부정적 뉴스, 중립적 뉴스 다양하게 생성하세요.(긍정, 부정, 중립 이라는 말은 표시하지 마세요.)
361
  뉴스에 따라 주식이 상승하기도 하고 하락하기도 할 수 있습니다.
362
  각 뉴스 기사는 "## 뉴스 [번호]" 로 시작해주세요. (예: ## 뉴스 1, ## 뉴스 2 ...)
 
366
  chat_session = st.session_state["chat_session"]
367
  messages = [{"role": "user", "content": prompt}]
368
 
369
+ try: # OpenAI API 호출 예외 처리
370
  response = client.chat.completions.create(
371
  model="gpt-4o-mini",
372
  messages=messages,
 
389
 
390
  return news_articles[:5]
391
 
392
+ except Exception as e: # API 호출 에러 발생 사용자에게 알림
393
+ st.error(f"뉴스 생성 중 오류 발생했습니다: {e}")
394
+ return [] # 오류 발생 시 빈 리스트 반환
395
 
396
+ # explain_daily_news_meanings 함수 (기존 코드와 동일)
397
  def explain_daily_news_meanings(daily_news):
398
  if daily_news is None:
399
  return {}
400
 
401
  difficulty_level = st.session_state['difficulty_level']
402
+ difficulty_prompt_map = {
403
  "초등학생": "초등학생 5~6학년",
404
  "중학생": "중학생 1~3학년",
405
  "고등학생": "고등학생 1~3학년",
 
414
 
415
  **지시:**
416
  위 신문 기사의 핵심 의미를 {level_prompt}이 이해하기 쉽게 3문장 이내로 요약해서 "해설: " 다음에 설명해주세요.
417
+ 그리고뉴스와 관련 주식 섹터 1~2개를 쉼표로 구분해서 "관련 섹터: " 다음에 알려주세. 관련 섹터가 없다면 "관련 섹터: 없음" 이라고 해주세요.
418
 
419
  뉴스 의미 해설:
420
  """
421
  chat_session = st.session_state["chat_session"]
422
  messages = [{"role": "user", "content": prompt}]
423
+ try:
424
  response = client.chat.completions.create(
425
  model="gpt-4o-mini",
426
  messages=messages,
 
436
  meaning_text = response.choices[0].message.content.strip()
437
 
438
  explanation = ""
439
+ related_sectors = []
440
 
441
  if "해설:" in meaning_text:
442
  explanation_start_index = meaning_text.find("해설:") + len("해설:")
443
+ explanation_end_index = meaning_text.find("관련 섹터:")
444
+ if explanation_end_index != -1:
445
+ explanation = meaning_text[explanation_start_index:explanation_end_index].strip()
446
+ else:
447
+ explanation = meaning_text[explanation_start_index:].strip()
448
 
449
+ if "관련 섹터:" in meaning_text:
450
+ related_sectors_str = meaning_text.split("관련 섹터:")[1].strip()
451
+ if related_sectors_str.lower() != "없음":
452
+ related_sectors = [sector.strip() for sector in related_sectors_str.split(',')]
453
+ else:
454
+ related_sectors = [] # "없음" explicitly means empty list
455
+
456
+ meanings[str(i + 1)] = {"explanation": explanation, "sectors": related_sectors}
457
+
458
+ except Exception as e: # google.api_core.exceptions.ResourceExhausted -> Exception으로 변경
459
  st.error(
460
  f"API 할당량 초과 오류가 발생했습니다. 잠시 후 다시 시도해주세요. 오류 메시지: {e}"
461
  )
 
463
  time.sleep(1)
464
  return meanings
465
 
466
+ # buy_stock, sell_stock 함수 (기존 코드와 동일)
467
+ def buy_stock(stock_name, quantity, sector):
468
+ if (
469
+ sector not in st.session_state["stocks"]
470
+ or stock_name not in st.session_state["stocks"][sector]
471
+ ):
472
  st.session_state["messages"].append(
473
  {"type": "error", "text": "존재하지 않는 주식 종목입니다."}
474
  )
475
  return
476
 
477
+ if quantity <= 0:
478
  st.session_state["messages"].append(
479
  {"type": "error", "text": "매수 수량은 1주 이상이어야 합니다."}
480
  )
481
  return
482
 
483
+ stock_price = st.session_state["stocks"][sector][stock_name]["current_price"]
484
  max_quantity = st.session_state["portfolio"]["cash"] // stock_price
485
+ if quantity > max_quantity:
486
  st.session_state["messages"].append(
487
  {
488
  "type": "error",
 
497
 
498
  total_price = stock_price * quantity
499
 
500
+ if st.session_state["portfolio"]["cash"] >= total_price:
501
  st.session_state["portfolio"]["cash"] -= total_price
502
  portfolio_stocks = st.session_state["portfolio"]["stocks"]
503
  if (
 
525
  f"{stock_name} {quantity}주 매수 완료. 총 {total_price:,.0f}원 소요.", icon="✅"
526
  )
527
  st.session_state['buy_confirm'] = False
528
+ else:
529
  st.session_state["messages"].append(
530
  {"type": "error", "text": "잔액이 부족합니다."}
531
  )
 
533
  st.error(f"잔액이 부족합니다. (최대 {max_quantity}주까지 매수 가능)")
534
  st.session_state['buy_confirm'] = False
535
 
536
+
537
+ def sell_stock(stock_name, quantity):
538
  if stock_name not in st.session_state["portfolio"]["stocks"]:
539
  st.session_state["messages"].append(
540
  {"type": "error", "text": "보유하고 있지 않은 주식입니다."}
 
562
  return
563
 
564
  stock_price = 0
565
+ stock_sector = ""
566
+ for sector, stocks in st.session_state["stocks"].items():
567
  if stock_name in stocks:
568
  stock_price = stocks[stock_name]["current_price"]
569
  stock_sector = sector
570
  break
 
571
 
572
  if stock_price == 0:
573
  st.session_state["messages"].append(
 
593
  st.success(f"{stock_name} {quantity}주 매도 완료. 총 {sell_price:,.0f}원 획득.")
594
  st.session_state['sell_confirm'] = False
595
 
596
+ # update_stock_prices 함수 (Decade 상황 반영 추가)
597
  def update_stock_prices():
598
  if not st.session_state["daily_news"]:
599
  return
600
 
601
+ sector_impacts = {sector: 0 for sector in st.session_state["stocks"]}
602
  decade_count = st.session_state["decade_count"]
603
+ decade_economic_conditions = { # Decade별 경제 상황 반영 (수치 예시는 임의)
604
+ 1950: {"growth_rate": 0.02, "volatility": 0.03}, # 재건 시대, 낮은 성장률, 변동성 중간
605
+ 1960: {"growth_rate": 0.05, "volatility": 0.02}, # 경제 개발, 높은 성장률, 변동성 낮음
606
+ 1970: {"growth_rate": 0.08, "volatility": 0.05}, # 중화학 공업, 고성장, 오일쇼크 변동성 증가
607
+ 1980: {"growth_rate": 0.06, "volatility": 0.04}, # 안정 성장, 변동성 중간
608
+ 1990: {"growth_rate": 0.03, "volatility": 0.1}, # IMF 외환위기, 낮은 성장률, 높은 변동성
609
+ 2000: {"growth_rate": 0.04, "volatility": 0.07}, # IT 버블, 성장률 회복, 변동성 증가
610
+ 2010: {"growth_rate": 0.03, "volatility": 0.05}, # 저성장 시대, 변동성 중간
611
+ 2020: {"growth_rate": 0.02, "volatility": 0.08}, # 코로나19, 저성장, 높은 변동성
612
  }
613
+ economic_condition = decade_economic_conditions.get(decade_count, {"growth_rate": 0.03, "volatility": 0.06}) # 기본값 설정
614
+
615
+ for i, news_article in enumerate(st.session_state["daily_news"]):
616
+ news_meaning = st.session_state["news_meanings"].get(str(i + 1))
617
+ if news_meaning:
618
+ related_sectors = news_meaning.get("sectors", [])
619
+ news_explanation = news_meaning.get("explanation", "")
620
+
621
+ news_sentiment = 0
622
+ if "상승" in news_article or "성장" in news_article or "긍정적" in news_article or "유망" in news_article or "호황" in news_article:
623
+ news_sentiment = 1
624
+ elif "하락" in news_article or "감소" in news_article or "부정적" in news_article or "어려움" in news_article or "침체" in news_article or "위기" in news_article:
625
+ news_sentiment = -1
626
+ else:
627
+ news_sentiment = 0
628
+
629
+ for sector in related_sectors:
630
+ if sector in sector_impacts:
631
+ sector_impacts[sector] += news_sentiment * 0.03 # 뉴스 sentiment 영향도 감소
632
+
633
+ for sector in st.session_state["stocks"]:
634
+ sector_impact = sector_impacts[sector]
635
+ # 경제 상황 반영: 성장률, 변동성
636
+ base_change_rate = economic_condition["growth_rate"] # 기본 성장률 적용
637
+ volatility = economic_condition["volatility"] # 변동성 적용
638
+ change_rate = random.uniform(-volatility, volatility) + base_change_rate + sector_impact # 변동폭 증가, sector_impact 감소
639
+
640
+ for stock_name in st.session_state["stocks"][sector]:
641
+ change_rate = max(-0.2, min(0.2, change_rate)) # 변동폭 제한 (±20% 이내)
642
+ st.session_state["stocks"][sector][stock_name]["current_price"] *= (
643
+ 1 + change_rate
644
+ )
645
+ st.session_state["stocks"][sector][stock_name]["current_price"] = max(
646
+ 1, int(st.session_state["stocks"][sector][stock_name]["current_price"])
647
+ )
648
+ st.session_state["stocks"][sector][stock_name]["price_history"].append(
649
+ st.session_state["stocks"][sector][stock_name]["current_price"]
650
+ )
651
  st.session_state["messages"].append({"type": "info", "text": "주가가 변동되었습니다."})
652
  st.toast("주가가 변동되었습니다.", icon="📈")
653
  st.info("주가가 변동되었습니다.")
654
+ st.session_state["sector_news_impact"] = sector_impacts
655
 
656
+ # display_portfolio, display_stock_prices, display_portfolio_table 함수 (기존 코드와 동일)
657
+ def display_portfolio():
658
  portfolio = st.session_state["portfolio"]
659
  cash = portfolio["cash"]
660
  total_value = cash
 
663
  quantity = stock_info["quantity"]
664
  purchase_price = stock_info["purchase_price"]
665
  current_price = 0
666
+ stock_sector = ""
667
+ for sector, stocks in st.session_state["stocks"].items():
668
  if stock_name in stocks:
669
  current_price = stocks[stock_name]["current_price"]
670
+ stock_sector = sector
671
  break
 
 
 
672
  if current_price != 0:
673
  stock_value = current_price * quantity
674
  total_value += stock_value
 
685
 
686
  def display_stock_prices():
687
  stocks_data = []
688
+ for sector, sector_stocks in st.session_state["stocks"].items():
689
+ for stock_name, stock_info in sector_stocks.items():
690
+ price_history = stock_info["price_history"]
691
+ daily_change_rate_str = " - " # 기본값
692
+ if len(price_history) >= 2:
693
+ previous_day_price = price_history[-2]
694
+ current_price = price_history[-1]
695
+ daily_change_rate = (current_price - previous_day_price) / previous_day_price * 100
696
+ daily_change_rate_str = f"{daily_change_rate:.2f}%"
697
+
698
+ stocks_data.append(
699
+ {
700
+ "종목": stock_name,
701
+ "섹터": sector,
702
+ "현재 주가": f"{stock_info['current_price']:,} 원",
703
+ "전일 대비": daily_change_rate_str, # 전일 대비 등락률 추가
704
+ "price_history": stock_info["price_history"],
705
+ "description": stock_info["description"],
706
+ }
707
+ )
708
  stocks_df = pd.DataFrame(stocks_data)
709
+ st.dataframe(stocks_df[["섹터", "종목", "현재 주가", "전일 대비"]], hide_index=True) # "전일 대비" 컬럼 추가
710
 
711
  selected_stock_all_info = st.selectbox(
712
  "종목 선택 (기업 정보 및 주가 그래프)", stocks_df["종목"].tolist()
713
  )
714
  if selected_stock_all_info:
715
+ selected_stock_sector = stocks_df[
716
  stocks_df["종목"] == selected_stock_all_info
717
  ]["섹터"].iloc[0]
718
  col1_info, col2_graph = st.columns([1, 2])
 
720
  with col1_info:
721
  st.subheader("기업 정보")
722
  st.info(
723
+ f"**{selected_stock_all_info} ({selected_stock_sector})**\n\n{st.session_state['stocks'][selected_stock_sector][selected_stock_all_info]['description']}"
724
  )
725
 
726
+ with col2_graph:
727
  st.subheader("주가 그래프")
728
  price_history_df = pd.DataFrame(
729
  {
730
  "날짜": range(
731
  1,
732
  len(
733
+ st.session_state["stocks"][selected_stock_sector][
734
+ selected_stock_all_info
735
+ ]["price_history"]
736
  )
737
  + 1,
738
  ),
739
+ "주가": st.session_state["stocks"][selected_stock_sector][
740
+ selected_stock_all_info
741
+ ]["price_history"],
742
  }
743
  )
744
  fig = px.line(
745
  price_history_df,
746
  x="날짜",
747
  y="주가",
748
+ title=f"{selected_stock_all_info} ({selected_stock_sector}) 주가 변동",
749
  )
750
  st.plotly_chart(fig)
751
  else:
752
  st.info("종목을 선택하여 기업 정보와 주가 그래프를 확인하세요.")
753
 
754
+ def display_portfolio_table():
755
  portfolio = st.session_state["portfolio"]
756
  if portfolio["stocks"]:
757
  portfolio_data = []
 
763
  quantity = stock_info["quantity"]
764
  purchase_price = stock_info["purchase_price"]
765
  current_price = 0
766
+ stock_sector = ""
767
+ for sector, stocks in st.session_state["stocks"].items():
768
  if stock_name in stocks:
769
  current_price = stocks[stock_name]["current_price"]
770
+ stock_sector = sector
771
  break
 
 
772
 
773
  if current_price == 0:
774
  continue
 
786
  portfolio_data.append(
787
  {
788
  "종목": stock_name,
789
+ "섹터": stock_sector,
790
  "보유 수량": quantity,
791
  "매수 단가": f"{purchase_price:,.0f} 원",
792
  "현재가": f"{current_price:,.0f} 원",
 
795
  "수익률": f"{profit_rate:.2f}%",
796
  }
797
  )
798
+ portfolio_data.append(
799
  {
800
  "종목": "현금",
801
  "섹터": "-",
 
809
  )
810
  portfolio_df = pd.DataFrame(portfolio_data)
811
  st.dataframe(portfolio_df, hide_index=True, height=350)
812
+ st.markdown(
813
  f"""**현금 잔고:** {portfolio['cash']:,} 원
814
  **📊 총 평가액:** {total_value:,.0f} 원
815
  **🛒 총 매수 금액:** {total_purchase_value:,.0f} 원
 
820
  st.info("보유 주식이 없습니다.")
821
 
822
  # display_stock_glossary 함수 (기존 코드와 동일)
823
+ def display_stock_glossary():
824
  glossary = {
825
  "주식": "회사의 일부분을 나타내는 증서. 주식을 사면 회사의 주인이 되는 거예요.",
826
  "주가": "주식 1개당 가격. 사람들이 주식을 사고팔 때 가격이 변해요.",
 
844
  def main():
845
  col_news, col_main_ui = st.columns([1, 2])
846
 
847
+ with col_news:
848
+ st.header(f"📰 {st.session_state['decade_count']}년대 뉴스") # Decade 표시
849
  if st.button("뉴스 생성", use_container_width=True, key="news_gen_button"):
850
+ with st.spinner(f"{st.session_state['decade_count']}년대 뉴스 생성 중..."): # Decade 표시
851
  current_daily_news = generate_news()
852
  st.session_state["daily_news"] = current_daily_news
853
 
854
  if st.session_state["daily_news"]:
855
+ st.subheader(f"{st.session_state['decade_count']}년대 뉴스") # Decade 표시
856
  for i, news in enumerate(st.session_state["daily_news"]):
857
  with st.expander(f"뉴스 {i+1}", expanded=False):
858
  st.write(news)
 
860
  if st.session_state["previous_daily_news"] and st.session_state[
861
  "news_meanings"
862
  ]:
863
+ previous_decade = st.session_state['decade_count'] - 10 # 이전 Decade 계산
864
+ st.subheader(f"{previous_decade}년대 뉴스 해설") # 이전 Decade 표시
865
  st.info("AI가 분석한 지난 뉴스 해설입니다.")
866
+ with st.expander(f"{previous_decade}년대 뉴스 해설 보기", expanded=False): # 이전 Decade 표시
867
  if st.session_state["news_meanings"]:
868
  for i, meaning_data in st.session_state["news_meanings"].items():
869
  st.markdown(f"**뉴스 {i}**:")
870
+ st.markdown(f"**AI 해설:** {meaning_data['explanation']}") # Markdown 으로 변경
871
+ if meaning_data['sectors']:
872
+ st.markdown(f"**관련 섹터:** {', '.join(meaning_data['sectors'])}") # Markdown 으로 변경
873
+ else:
874
+ st.markdown("**관련 섹터:** 없음") # Markdown 으로 변경
875
  else:
876
  st.info("지난 뉴스에 대한 해설이 없습니다.")
877
 
 
879
  st.info("뉴스 생성 버튼을 눌러 오늘의 뉴스를 받아보세요.")
880
 
881
  with col_main_ui:
882
+ menu = st.tabs(
883
  ['현재 주가', '내 포트폴리오', '주식 매수', '주식 매도', '지난 뉴스 해설']
884
  )
885
 
886
+ with menu[0]:
887
  st.subheader("📈 현재 주가 및 기업 정보")
888
  st.markdown("주식 시장의 현재 가격과 기업 정보를 확인하세요.")
889
  display_stock_prices()
890
 
891
+ with menu[1]:
892
  st.subheader("📊 내 포트폴리오")
893
  st.markdown("현재 보유 중인 주식과 자산을 확인하세요.")
894
  display_portfolio_table()
895
 
896
+ with menu[2]:
897
  st.subheader("💰 주식 매수")
898
  st.markdown("AI 예측과 뉴스 분석을 바탕으로 주식을 매수해보세요.")
899
+ sector_names = list(st.session_state["stocks"].keys())
900
+ selected_sector_buy = st.selectbox("매수 섹터 선택:", sector_names)
901
+ stock_names_in_sector = list(
902
+ st.session_state["stocks"][selected_sector_buy].keys()
903
+ )
904
+ selected_stock_buy = st.selectbox("매수 종목 선택:", stock_names_in_sector)
905
 
906
+ stock_price_buy = st.session_state["stocks"][selected_sector_buy][
907
+ selected_stock_buy
908
+ ]["current_price"]
909
  st.info(f"**{selected_stock_buy}** 현재 주가: {stock_price_buy:,.0f}원")
910
  quantity_buy = st.number_input(
911
  "매수 수량 (주):", min_value=1, value=1, step=1
912
  )
913
 
914
+ if not st.session_state['buy_confirm']:
915
  if st.button("주식 매수", use_container_width=True, key='buy_button_confirm'):
916
  st.session_state['buy_confirm'] = True
917
  else:
 
919
  col_confirm, col_cancel = st.columns([1, 1])
920
  with col_confirm:
921
  if st.button("✅ 매수 확인", use_container_width=True, key='buy_confirm_button'):
922
+ buy_stock(selected_stock_buy, quantity_buy, selected_sector_buy)
923
 
924
  with col_cancel:
925
  if st.button("❌ 매수 취소", use_container_width=True, key='buy_cancel_button', type='secondary'):
926
  st.session_state['buy_confirm'] = False
927
  st.info("매수를 취소했습니다.")
928
 
929
+ with menu[3]:
930
  st.subheader("📉 주식 매도")
931
  st.markdown("보유 중인 주식을 판매하고 수익을 실현해보세요.")
932
  if st.session_state["portfolio"]["stocks"]:
933
  stock_names_sell = list(st.session_state["portfolio"]["stocks"].keys())
934
  selected_stock_sell = st.selectbox("매도 종목 선택:", stock_names_sell)
935
  stock_price_sell = 0
936
+ for sector, stocks in st.session_state["stocks"].items():
937
  if selected_stock_sell in stocks:
938
  stock_price_sell = stocks[selected_stock_sell]["current_price"]
939
  break
 
940
 
941
  st.info(f"**{selected_stock_sell}** 현재 주가: {stock_price_sell:,.0f}원")
942
  max_sell_quantity = st.session_state["portfolio"]["stocks"][
 
949
  value=1,
950
  step=1,
951
  )
952
+ if not st.session_state['sell_confirm']:
953
  if st.button("주식 매도", use_container_width=True, key='sell_button_confirm'):
954
  st.session_state['sell_confirm'] = True
955
  else:
 
957
  col_confirm, col_cancel = st.columns([1, 1])
958
  with col_confirm:
959
  if st.button("✅ 매도 확인", use_container_width=True, key='sell_confirm_button'):
960
+ sell_stock(selected_stock_sell, quantity_sell)
961
  with col_cancel:
962
  if st.button("❌ 매도 취소", use_container_width=True, key='sell_cancel_button', type='secondary'):
963
  st.session_state['sell_confirm'] = False
 
965
  else:
966
  st.info("보유 주식이 없습니다. 포트폴리오 탭에서 확인하세요.")
967
 
968
+ with menu[4]:
969
  if st.session_state["previous_daily_news"] and st.session_state[
970
  "news_meanings"
971
  ]:
972
+ previous_decade = st.session_state['decade_count'] - 10 # 이전 Decade 계산
973
+ st.subheader(f"{previous_decade}년대 뉴스 해설") # 이전 Decade 표시
974
  st.info("AI가 분석한 지난 뉴스 해설입니다.")
975
  for i in range(len(st.session_state["previous_daily_news"])):
976
  with st.expander(f"뉴스 {i+1}", expanded=False):
 
980
  meaning_data = st.session_state["news_meanings"].get(str(i+1))
981
  if meaning_data:
982
  st.markdown("**AI 해설:**")
983
+ st.info(meaning_data['explanation'])
984
+ if meaning_data['sectors']:
985
+ st.markdown("**관련 섹터:**")
986
+ st.info(', '.join(meaning_data['sectors']))
987
+ else:
988
+ st.info("**관련 섹터:** 없음")
989
  else:
990
  st.warning("뉴스 해설을 생성하지 못했습니다.")
991
  else:
 
993
  "이전 뉴스 해설이 없습니다. 10년 후 버튼을 눌러 뉴스 해설을 받아보세요."
994
  )
995
 
996
+ with st.sidebar:
997
+ st.markdown(f"# ⏳ 대한민국 경제 발전사 게임") # 앱 제목 변경
998
+ st.markdown(f"### {st.session_state['decade_count']}년대") # Decade 표시
999
  st.markdown("---")
1000
 
1001
+ # 난이도 선택 Selectbox (기존 코드와 동일)
1002
+ difficulty_level = st.selectbox(
1003
  "📈 난이도 선택",
1004
  ["초등학생", "중학생", "고등학생"],
1005
  index=["초등학생", "중학생", "고등학생"].index(st.session_state['difficulty_level'])
 
1007
  st.session_state['difficulty_level'] = difficulty_level
1008
  st.markdown("---")
1009
 
1010
+ decade_description_map = { # Decade별 게임 설명
1011
  1950: "🇰🇷 **1950년대: 재건의 시대**\n\n 전쟁의 상처를 딛고 일어서는 시기입니다. 원조 경제와 농업을 기반으로 재건에 힘쓰세요.",
1012
  1960: "🇰🇷 **1960년대: 경제 개발의 닻을 올리다**\n\n 경제 개발 5개년 계획이 시작됩니다. 수출과 경공업을 통해 경제 성장의 기반을 마련하세요.",
1013
  1970: "🇰🇷 **1970년대: 중화학공업, 성장의 엔진**\n\n 중화학공업 육성 정책이 본격화됩니다. 건설 붐과 함께 중동 시장을 개척해보세요.",
 
1017
  2010: "🇰🇷 **2010년대: 스마트 혁명, 플랫폼 경제**\n\n 스마트폰이 세상을 바꾸고, 플랫폼 기업이 성장합니다. 새로운 경제 질서에 올라타세요.",
1018
  2020: "🇰🇷 **2020년대: 미래를 향한 도전**\n\n 팬데믹을 극복하고 미래 산업을 육성합니다. 친환경 에너지, 바이오, 플랫폼 기업의 미래를 예측해보세요.",
1019
  }
1020
+ decade_description = decade_description_map.get(st.session_state['decade_count'], "🇰🇷 **미래 시대:**\n\n 미래 시대에 대비하여 새로운 산업과 기술에 투자해보세요.") # 기본 설명
1021
 
1022
+ st.markdown(decade_description) # Decade별 설명
1023
 
1024
+ cash, total_value, profit_rate = display_portfolio()
1025
  st.metric(label="💰 현금 잔고", value=f"{cash:,.0f} 원")
1026
  st.metric(label="📊 총 평가 금액", value=f"{total_value:,.0f} 원")
1027
  st.metric(label="🚀 총 수익률", value=f"{profit_rate:.2f}%")
1028
  st.markdown("---")
1029
 
1030
+ if st.button("10년 후", use_container_width=True, key="decade_pass_button"): # 버튼 텍스트 변경
1031
  if st.session_state["daily_news"]:
1032
+ with st.spinner(f"{st.session_state['decade_count']}년대 주가 변동 및 지난 뉴스 분석..."): # Decade 표시
1033
  st.session_state["previous_daily_news"] = st.session_state["daily_news"]
1034
  meanings = explain_daily_news_meanings(
1035
  st.session_state["previous_daily_news"]
 
1038
  st.session_state["news_meanings"] = meanings
1039
  update_stock_prices()
1040
  st.session_state["daily_news"] = generate_news()
1041
+ st.session_state["decade_count"] += 10 # Decade + 10
1042
+ st.session_state["stocks"] = initialize_stocks_for_decade(st.session_state["decade_count"]) # 다음 Decade 주식 데이터 초기화
1043
  st.rerun()
1044
+ previous_decade = st.session_state['decade_count'] - 10 # 이전 Decade 계산
1045
+ st.info(f"지난 {previous_decade}년대 뉴스 해설 탭에서 AI 분석을 확인해보세요.") # 이전 Decade 표시
1046
  else:
1047
  st.warning("오늘의 뉴스를 먼저 생성해주세요.")
1048
  st.markdown("***")
1049
 
1050
+ display_stock_glossary()
1051
 
1052
+ with st.expander("🚀 앱 사용 가이드", expanded=False): # 앱 가이드 수정
1053
  st.markdown(
1054
  """
1055
  **대한민국 경제 발전사 주식 투자 게임**에 오신 것을 환영합니다! ⏳
 
1093
 
1094
  **대한민국 경제 발전사를 따라가는 흥미진진한 주식 투자 게임! 지금 시작하세요!** 🚀
1095
  """.format(decade=st.session_state['decade_count'], next_decade=st.session_state['decade_count'] + 10)
1096
+ )
 
1097
 
1098
  if __name__ == "__main__":
1099
+ st.session_state["stocks"] = initialize_stocks_for_decade(st.session_state["decade_count"]) # stocks 초기화 (최초 실행 시 1950년대 데이터)
1100
  main()