ll7098ll commited on
Commit
14115e9
·
verified ·
1 Parent(s): 61b34c0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +311 -153
app.py CHANGED
@@ -3,12 +3,22 @@ import streamlit as st
3
  import random
4
  import time
5
  import math
 
 
 
6
 
7
  # --- 페이지 설정 (스크립트 최상단) ---
8
  st.set_page_config(layout="wide", page_title="진실을 찾아서: 현대사 취재기록")
9
 
 
 
 
 
 
 
 
10
  # --- 게임 설정 ---
11
- ACTIONS_PER_TURN_LIMIT = 2 # 턴 당 취재 활동 횟수 제한
12
 
13
  # --- 시나리오 설정 ---
14
  SCENARIOS = {
@@ -17,37 +27,34 @@ SCENARIOS = {
17
  "start_year": 1960,
18
  "player_role": "신입 기자 (자유일보)",
19
  "initial_press_freedom": 40,
20
- "initial_scoop_points": 0,
21
  "initial_reporter_safety": 70,
22
  "initial_public_trust": 50,
23
  "vocab_level": "보통",
24
- "max_turns": 8,
25
  },
26
  "5.18_gwangju": {
27
  "display_name": "5.18 광주 현장 취재 (1980)",
28
  "start_year": 1980,
29
  "player_role": "지방 주재 기자 (민주신문)",
30
  "initial_press_freedom": 20,
31
- "initial_scoop_points": 0,
32
  "initial_reporter_safety": 50,
33
  "initial_public_trust": 40,
34
  "vocab_level": "보통",
35
- "max_turns": 10,
36
  },
37
  "june_struggle": {
38
  "display_name": "6월 항쟁 동행 취재 (1987)",
39
  "start_year": 1987,
40
  "player_role": "사회부 기자 (시민일보)",
41
  "initial_press_freedom": 30,
42
- "initial_scoop_points": 0,
43
  "initial_reporter_safety": 60,
44
  "initial_public_trust": 45,
45
  "vocab_level": "보통",
46
- "max_turns": 7,
47
  }
48
  }
49
 
50
- # --- 텍스트 저장소 (기자 컨셉에 맞게 대폭 수정) ---
51
  ALL_TEXTS = {
52
  # --- 공통 UI ---
53
  "game_title": {"보통": "🎙️ 진실을 찾아서: 현대사 취재기록"},
@@ -56,7 +63,7 @@ ALL_TEXTS = {
56
  "dashboard_title": {"보통": "📊 기자 상황판"},
57
  "dashboard_term": {"보통": "{year}년 {turn}번째 취재일"},
58
  "term_press_freedom": {"보통": "취재 자유도"},
59
- "term_scoop_points": {"보통": "특종 점수"},
60
  "term_reporter_safety": {"보통": "기자 안전도"},
61
  "term_public_trust": {"보통": "대중 신뢰도"},
62
  "current_assignment_title": {"보통": "📋 오늘의 취재 지시"},
@@ -72,84 +79,142 @@ ALL_TEXTS = {
72
  "historical_source_title": {"보통": "참고 자료"},
73
  "reporter_notebook_title": {"보통": "📝 나의 취재 노트"},
74
  "article_writing_title": {"보통": "🖋️ 기사 작성"},
75
- "desk_feedback_title": {"보통": "📢 데스크 피드백"},
76
  "status_loading_assignment": {"보통": "{year}년 {turn}번째 취재일 준비 중..."},
77
  "status_actions_taken": {"보통": "오늘의 주요 취재 활동을 마쳤습니다. 기사를 정리하거나 다음 날로 진행하세요."},
78
  "sidebar_title": {"보통": "메뉴"},
79
  "sidebar_glossary_title": {"보통": "📰 관련 용어/인물"},
80
  "sidebar_current_source_title": {"보통": "📎 현재 참고 자료"},
81
  "sidebar_no_source": {"보통": "현재 참고할 만한 특별 자료가 없습니다."},
82
- "input_placeholder_keyword": {"보통": "핵심 키워드 입력..."},
83
- "input_placeholder_sentence": {"보통": "간단한 문장으로 요약..."},
84
- "button_add_to_notebook": {"보통": "노트에 추가"},
85
- "button_submit_article": {"보통": "기사 송고"},
86
  "article_headline_label": {"보통": "기사 제목:"},
87
- "article_body_label": {"보통": "기사 본문 핵심:"},
88
  "article_tone_label": {"보통": "기사 논조 선택:"},
89
  "warning_empty_article": {"보통": "기사 제목과 핵심 내용을 모두 작성해주세요."},
90
  "info_no_special_info": {"보통": "특별한 정보는 얻지 못했습니다."},
91
  "log_freedom_loss": {"보통": " - 취재 중 제약 발생, 취재 자유도 {loss} 감소."},
92
  "log_safety_loss": {"보통": " - 취재 중 신변 위협 감지, 기자 안전도 {loss} 감소."},
93
- "log_scoop_gain": {"보통": " - 결정적 단서 포착! 특종 점수 {gain} 획득."},
94
  "log_info_acquired": {"보통": " - 정보 획득: {info}"},
95
  "log_no_info_acquired": {"보통": " - 특별한 정보는 얻지 못함."},
96
- "log_article_submitted": {"보통": "기사 송고: '{headline}' (논조: {tone})"},
97
- "log_desk_feedback": {"보통": " - 데스크 피드백: {feedback}"},
98
  "log_trust_change": {"보통": " - 대중 신뢰도 {change:+} 변동."},
99
  "log_freedom_change_article": {"보통": " - 취재 자유도 {change:+} 변동."},
100
  "log_safety_change_article": {"보통": " - 기자 안전도 {change:+} 변동."},
101
  "log_assignment_over": {"보통": "--- {scenario_name} 취재 기간 종료 ---"},
102
  "log_next_day_start": {"보통": "--- {year}년 {turn}번째 취재일 시작 ---"},
103
  "button_go_to_article_writing": {"보통": "기사 작성하기"},
 
104
 
105
  # --- 4.19 혁명 취재 시나리오 ---
106
  "scenario_419_revolution_name": {"보통": "4.19 혁명 취재 (1960)"},
107
- "event_419_t1_assignment": {"보통": "3.15 통령 선거일. 전국 투표소 상황 및 개표 과정에서 부정행위 정황을 포착하고 관련자 증언을 확보하라."},
108
- "event_419_t1_source": {"보통": "[배경] 자유당권의 장기집권을 조직적인 선 개입예상됨. 야당 시민사회는 감시 활동을 벌이고 있음."},
109
- "action_419_t1_opt1_text": {"보통": "주요 투표소 잠입 취재"},
110
- "action_419_t1_opt2_text": {"보통": "야당 선거 감 동행 취재"},
111
- "action_419_t1_opt3_text": {"보통": "경찰 관위 관계자 익명 인터뷰 시도"},
112
- "info_419_t1_opt1_got": {"보통": "정보: 투표함 바꿔치기 현장 목격. (키워드: 투표함, 바꿔치기, 자유당)"},
113
- "info_419_t1_opt2_got": {"보통": "정보: 감시단, '3인조 공개투표' 부정 사례 보. (키워드: 공개투표, 협박, 감시단)"},
114
- "info_419_t1_opt3_got": {"보통": "정보: 익명의 경찰, '윗선시로 어쩔 없었다' 로. (키워드: 경찰, 윗선지시, 양심선언)"},
115
- "desk_feedback_419_t1_fact_based": {"보통": "데스크: 훌륭해! 정도면 특종감이야. 사실 확인 철저히 해서 ."},
116
- "desk_feedback_419_t1_critical": {"보통": "데스크: (한숨) 이런 민감한 시기에는... 알지? 너무 정권에 불리한 기 우리도 곤란해. 적당히 수위 조절하게."},
117
- "desk_feedback_419_t1_cautious": {"보통": "데스크: 아직 물증부족하군. 파고들어야겠어. 괜히 긁어 부스럼 만들지 신중하게 접근."},
118
- "desk_feedback_419_t1_sympathetic": {"보통": "데스크: 국민들의 분노가 느껴지는군. 하지 감정보 팩트에 집중해야 때일세."},
119
-
120
- "event_419_t2_assignment": {"보통": "마산에서 김주열 학생신이 발견되었다. 사건의 진과 시민들의 반응, 경찰의 대처를 심층 취재하라."},
121
- "event_419_t2_source": {"보통": "[배경] 3.15 부정선거 항의 실종된 김주열 군 시신이 참혹한 상태로 발견되어 전국적 충격을 주고 있음."},
122
- "action_419_t2_opt1_text": {"보통": "김주열시신 발견 현장병원 취재"},
123
- "action_419_t2_opt2_text": {"보통": "마산 시민 학생 인터뷰 (격앙된 )"},
124
- "action_419_t2_opt3_text": {"보통": "경찰의 사건폐/축소 정황 포착"},
125
- "info_419_t2_opt1_got": {"보통": "정보: 김주열 군 눈에 최루탄이 직접 박혀 있었음. (키워드: 김주열, 최루탄, 사인)"},
126
- "info_419_t2_opt2_got": {"보통": "정보: 마산민들, '살인 물러나라!' 분노 발. (키워드: 마산시위, 분노, 정권타도)"},
127
- "info_419_t2_opt3_got": {"보통": "정보: 경찰, 단순 사로 발표하려다 기자에게각. (키워드: 경찰발표, 은폐시, 언론통)"},
128
- "desk_feedback_419_t2_critical": {"보통": "데스크: 경찰의 만행이 도를 넘었군! 사실을 반드시 알려야 한다. 다만, 자네 안전도 생각하게."},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
  # --- 5.18 광주 취재 시나리오 ---
131
  "scenario_518_gwangju_name": {"보통": "5.18 광주 현장 취재 (1980)"},
132
- "event_518_t1_assignment": {"보통": "5월 17일, 비상계엄 전국 확대. 광주 지역 분위기 및 주요 인사 동향을 파악하고, 계엄군의 움직임을 주시하라."},
133
- "event_518_t1_source": {"보통": "[배경] 신군부의 권력 장악 가속화. 김대중 등 정치인 연행. 광주 지역 중심 불온 움직임 감지."},
134
- "action_518_t1_opt1_text": {"보통": "전대 등 주요 대학가 탐문 취재"},
135
- "action_518_t1_opt2_text": {"보통": "광주 시내 주요 (도청, 경찰서) 주변 상황 취재"},
136
- "action_518_t1_opt3_text": {"보통": "지역 유지재야인사 접촉 시도 (보안 유의)"},
137
- "info_518_t1_opt1_got": {"보통": "정보: 전남대 학생들, '휴 반대, 계엄 철폐' 등 구호 준. (키워드: 전남대, 학생위, 계엄반대)"},
138
- "info_518_t1_opt2_got": {"보통": "정보: 도청변에 병력 배치 증가, 분위기. (키워드: 계엄, 병력증강, 긴장고조)"},
139
- "info_518_t1_opt3_got": {"보통": "정보: 재야인사, '신군부의 폭압이 시작 것' 우려 표명. (키워드: 신군부, 민주인사, 탄압예상)"},
140
- "desk_feedback_518_t1_cautious": {"보통": "데스크: 광주 상황이 심상치 않네. 변이 최우선이니취재는 . 확인사실만 보고하게."},
 
 
 
 
 
 
 
 
 
 
141
 
142
  # --- 6월 항쟁 취재 시나리오 ---
143
  "scenario_june_struggle_name": {"보통": "6월 항쟁 동행 취재 (1987)"},
144
- "event_june_t1_assignment": {"보통": "박종철 고문치사 사건 발생. 경찰 발표의 허점을 찾아내고, 사건의 진실을 규명할 단서를 확보하라."},
145
- "event_june_t1_source": {"보통": "[배] 경찰, '단순 쇼크사' 발표. 그러나 고문 의혹 확산. 천주교정의구현사 진실 규명 움직임."},
146
- "action_june_t1_opt1_text": {"보통": "사 담당 경찰 주변 탐문 접촉"},
147
- "action_june_t1_opt2_text": {"보통": "박종철 유족학교 친구 인터뷰"},
148
- "action_june_t1_opt3_text": {"보통": "의료계 전문가 익명 자문 (사인 관련)"},
149
- "info_june_t1_opt1_got": {"보통": "정보: 경찰 내에서도 '고문이 있었다'파다. (키워드: 경찰내부, 고문소문, 은폐의혹)"},
150
- "info_june_t1_opt2_got": {"보통": "정보: 유족, '아들은강했다. 경찰 발표 믿을 다' 오열. (키워드: 박종철유족, 경찰불신, 진상규명요구)"},
151
- "info_june_t1_opt3_got": {"보통": "정보: 익명의 의사, '경찰 발표 사인은 의학적으로해하기 어렵다. 외부 충격 가능성 배제 못해.' (키워드: 의료자문, 타살의혹, 고문흔적)"},
152
- "desk_feedback_june_t1_critical": {"보통": "데스크: 이건백한 살인 은폐 시도야! 모든 수단 동원해서 진실밝혀야 다. 우리 신문이 앞장서자!"},
 
153
 
154
  "article_tone_fact_based": {"보통": "객관적 사실 전달 위주"},
155
  "article_tone_critical": {"보통": "정부/기관 비판적 논조"},
@@ -159,8 +224,23 @@ ALL_TEXTS = {
159
  "glossary_419_revolution_free_party": {"보통": "**자유당 (自由黨):** 1951년 이승만 대통령을 중심으로 창당된 정당. 1960년 4.19 혁명으로 이승만 대통령이 하야하면서 사실상 해체되었다."},
160
  "glossary_419_revolution_315_election": {"보통": "**3.15 부정선거:** 1960년 3월 15일 실시된 제4대 대통령 및 제5대 부통령 선거에서 자유당 정권이 이승만-이기붕 후보를 당선시키기 위해 자행한 대규모 부정선거. 4.19 혁명의 직접적인 도화선이 되었다."},
161
  "glossary_419_revolution_kimjuyeol": {"보통": "**김주열 (金朱烈):** 1943~1960. 마산상업고등학교 학생. 3.15 마산 시위 중 실종되었다가 4월 11일 눈에 최루탄이 박힌 시신으로 발견되어 4.19 혁명을 격화시키는 계기가 되었다."},
 
 
 
162
  "glossary_518_gwangju_new_military": {"보통": "**신군부 (新軍部):** 1979년 10.26 사건 이후 12.12 군사반란을 통해 권력을 장악한 전두환, 노태우 등 하나회 중심의 군부 세력."},
 
 
 
 
 
 
163
  "glossary_june_struggle_parkjongchul": {"보통": "**박종철 (朴鍾哲):** 1965~1987. 서울대학교 언어학과 학생. 1987년 1월 경찰의 고문으로 사망하여 6월 민주 항쟁의 도화선이 되었다."},
 
 
 
 
 
 
164
  }
165
 
166
  # --- 어휘 조정 함수 ---
@@ -187,7 +267,8 @@ def initialize_reporter_scenario_state(scenario_key):
187
  'game_year': scenario_settings['start_year'],
188
  'status': {
189
  'press_freedom': scenario_settings['initial_press_freedom'],
190
- 'scoop_points': scenario_settings['initial_scoop_points'],
 
191
  'reporter_safety': scenario_settings['initial_reporter_safety'],
192
  'public_trust': scenario_settings['initial_public_trust'],
193
  },
@@ -201,39 +282,90 @@ HISTORICAL_ASSIGNMENTS = {
201
  "4.19_revolution": [
202
  {"turn": 1, "assignment_key": "event_419_t1_assignment", "source_key": "event_419_t1_source",
203
  "options": [
204
- {"action_key": "action_419_t1_opt1_text", "cost_freedom_risk": 10, "safety_risk": 20, "scoop_potential": 15, "info_key": "info_419_t1_opt1_got"},
205
- {"action_key": "action_419_t1_opt2_text", "cost_freedom_risk": 5, "safety_risk": 5, "scoop_potential": 10, "info_key": "info_419_t1_opt2_got"},
206
- {"action_key": "action_419_t1_opt3_text", "cost_freedom_risk": 5, "safety_risk": 10, "scoop_potential": 8, "info_key": "info_419_t1_opt3_got"},
207
- ], "article_writing_phase": True,
208
- "desk_feedback_keys": {"fact_based": "desk_feedback_419_t1_fact_based", "critical": "desk_feedback_419_t1_critical", "cautious": "desk_feedback_419_t1_cautious", "sympathetic": "desk_feedback_419_t1_sympathetic"}},
209
  {"turn": 2, "assignment_key": "event_419_t2_assignment", "source_key": "event_419_t2_source",
210
  "options": [
211
- {"action_key": "action_419_t2_opt1_text", "cost_freedom_risk": 15, "safety_risk": 25, "scoop_potential": 20, "info_key": "info_419_t2_opt1_got"},
212
- {"action_key": "action_419_t2_opt2_text", "cost_freedom_risk": 10, "safety_risk": 15, "scoop_potential": 12, "info_key": "info_419_t2_opt2_got"},
213
- {"action_key": "action_419_t2_opt3_text", "cost_freedom_risk": 20, "safety_risk": 10, "scoop_potential": 18, "info_key": "info_419_t2_opt3_got"},
214
- ], "article_writing_phase": True,
215
- "desk_feedback_keys": {"critical": "desk_feedback_419_t2_critical"}},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  ],
217
  "5.18_gwangju": [
218
  {"turn": 1, "assignment_key": "event_518_t1_assignment", "source_key": "event_518_t1_source",
219
  "options": [
220
- {"action_key": "action_518_t1_opt1_text", "cost_freedom_risk": 15, "safety_risk": 25, "scoop_potential": 10, "info_key": "info_518_t1_opt1_got"},
221
- {"action_key": "action_518_t1_opt2_text", "cost_freedom_risk": 10, "safety_risk": 20, "scoop_potential": 8, "info_key": "info_518_t1_opt2_got"},
222
- {"action_key": "action_518_t1_opt3_text", "cost_freedom_risk": 20, "safety_risk": 30, "scoop_potential": 12, "info_key": "info_518_t1_opt3_got"},
223
- ], "article_writing_phase": False,
224
- "desk_feedback_keys": {"cautious": "desk_feedback_518_t1_cautious"}},
 
 
 
 
 
 
 
 
 
 
 
 
225
  ],
226
  "june_struggle": [
227
  {"turn": 1, "assignment_key": "event_june_t1_assignment", "source_key": "event_june_t1_source",
228
  "options": [
229
- {"action_key": "action_june_t1_opt1_text", "cost_freedom_risk": 10, "safety_risk": 15, "scoop_potential": 10, "info_key": "info_june_t1_opt1_got"},
230
- {"action_key": "action_june_t1_opt2_text", "cost_freedom_risk": 5, "safety_risk": 10, "scoop_potential": 15, "info_key": "info_june_t1_opt2_got"},
231
- {"action_key": "action_june_t1_opt3_text", "cost_freedom_risk": 5, "safety_risk": 5, "scoop_potential": 12, "info_key": "info_june_t1_opt3_got"},
232
- ], "article_writing_phase": True,
233
- "desk_feedback_keys": {"critical": "desk_feedback_june_t1_critical"}},
 
 
 
 
 
 
234
  ]
235
  }
236
-
237
  # --- 다음 취재 지시 가져오기 ---
238
  def get_next_assignment(scenario_key, current_turn, game_state):
239
  scenario_assignments = HISTORICAL_ASSIGNMENTS.get(scenario_key, [])
@@ -247,14 +379,13 @@ def get_next_assignment(scenario_key, current_turn, game_state):
247
  "source_text": get_text_for_assignment(assignment_data.get("source_key", "")),
248
  "options": [],
249
  "article_writing_phase": assignment_data.get("article_writing_phase", False),
250
- "desk_feedback_keys": assignment_data.get("desk_feedback_keys", {})
251
  }
252
  for opt_data in assignment_data.get("options", []):
253
  final_assignment["options"].append({
254
  "action_text": get_text_for_assignment(opt_data["action_key"]),
255
  "cost_freedom_risk": opt_data.get("cost_freedom_risk", 0),
256
  "safety_risk": opt_data.get("safety_risk", 0),
257
- "scoop_potential": opt_data.get("scoop_potential", 0),
258
  "info_key": opt_data.get("info_key", "")
259
  })
260
 
@@ -280,12 +411,6 @@ def process_reporter_action(selected_option, game_state):
280
  status['reporter_safety'] -= safety_loss
281
  game_state['event_log'].append(get_text_for_processing("log_safety_loss").format(loss=safety_loss))
282
 
283
- scoop_gain_potential = selected_option.get("scoop_potential", 0)
284
- if scoop_gain_potential > 0 and random.random() < 0.7:
285
- actual_scoop = random.randint(scoop_gain_potential // 2, scoop_gain_potential)
286
- status['scoop_points'] += actual_scoop
287
- game_state['event_log'].append(get_text_for_processing("log_scoop_gain").format(gain=actual_scoop))
288
-
289
  info_text = ""
290
  if selected_option.get("info_key"):
291
  info_text = get_text_for_processing(selected_option["info_key"])
@@ -312,12 +437,12 @@ def generate_article_interface(game_state, vocab_level):
312
  for idx, note in enumerate(game_state['reporter_notebook']):
313
  st.markdown(f"- {note}")
314
  else:
315
- st.caption("아직 취재한 내용이 없습니다.")
316
  st.markdown("---")
317
 
318
  with st.form(key="article_form"):
319
- article_headline = st.text_input(get_text_func("article_headline_label"), placeholder=get_text("article_template_headline_placeholder", vocab_level))
320
- article_body_summary = st.text_area(get_text_func("article_body_label"), height=150, placeholder=get_text("article_template_body_placeholder", vocab_level))
321
 
322
  article_tones = {
323
  "fact_based": get_text_func("article_tone_fact_based"),
@@ -338,6 +463,10 @@ def generate_article_interface(game_state, vocab_level):
338
  st.warning(get_text_func("warning_empty_article"))
339
  return None
340
 
 
 
 
 
341
  return {
342
  "turn": game_state['current_turn'],
343
  "headline": article_headline,
@@ -347,68 +476,93 @@ def generate_article_interface(game_state, vocab_level):
347
  }
348
  return None
349
 
350
- # --- 기사 평가 및 데스크 피드백 ---
351
- def evaluate_article_and_get_feedback(article, game_state, assignment_data):
352
  status = game_state['status']
353
  vocab_level = SCENARIOS[game_state['scenario_key']]["vocab_level"]
354
  get_text_func = lambda k: get_text(k, vocab_level)
355
 
356
- feedback_text = "데스크: 기사 잘 봤네. "
357
- trust_change = 0
358
- freedom_change = 0
359
- safety_change = 0
360
-
361
- desk_feedback_keys = assignment_data.get("desk_feedback_keys", {})
362
- specific_feedback_key = desk_feedback_keys.get(article['tone'])
363
-
364
- default_tone_feedback_key = f"desk_feedback_{game_state['scenario_key']}_t{game_state['current_turn']}_{article['tone']}"
365
- if specific_feedback_key and get_text_func(specific_feedback_key) != specific_feedback_key :
366
- feedback_text += get_text_func(specific_feedback_key).replace("데스크: ", "")
367
- elif get_text_func(default_tone_feedback_key) != default_tone_feedback_key:
368
- feedback_text += get_text_func(default_tone_feedback_key).replace("데스크: ", "")
369
- else:
370
- if article['tone'] == "critical": feedback_text += "날카로운 지적이군."
371
- elif article['tone'] == "fact_based": feedback_text += "객관성이 돋보이는군."
372
- elif article['tone'] == "sympathetic": feedback_text += "독자들의 공감을 얻겠어."
373
- elif article['tone'] == "cautious": feedback_text += "신중한 접근이군."
374
-
375
- if article['tone'] == "critical":
376
- trust_change += random.randint(5, 10)
377
- freedom_change -= random.randint(5, 15)
378
- safety_change -= random.randint(3, 10)
379
- feedback_text += " 파장이 같으니 신변 조심."
380
- elif article['tone'] == "fact_based":
381
- trust_change += random.randint(2, 7)
382
- elif article['tone'] == "sympathetic":
383
- trust_change += random.randint(3, 8)
384
- safety_change -= random.randint(1, 5)
385
- elif article['tone'] == "cautious":
386
- trust_change -= random.randint(1, 5)
387
- freedom_change += random.randint(1, 3)
388
-
389
- if status['scoop_points'] > (game_state['current_turn'] * 3):
390
- scoop_bonus = status['scoop_points'] // 3
391
- trust_change += scoop_bonus
392
- feedback_text += f" 이번 특종(점수: {status['scoop_points']}) 덕분에 우리 신문 위상이 한층 올라가겠어!"
393
- status['scoop_points'] = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
 
 
395
  status['public_trust'] = max(0, min(100, status['public_trust'] + trust_change))
396
- status['press_freedom'] = max(0, min(100, status['press_freedom'] + freedom_change))
397
- status['reporter_safety'] = max(0, min(100, status['reporter_safety'] + safety_change))
398
 
399
- tone_text_key = f"article_tone_{article['tone']}"
400
- tone_display_text = get_text_func(tone_text_key)
401
- game_state['event_log'].append(get_text_func("log_article_submitted").format(headline=article['headline'], tone=tone_display_text))
 
 
 
 
 
 
 
402
 
403
- game_state['event_log'].append(get_text_func("log_desk_feedback").format(feedback=feedback_text))
 
 
404
  if trust_change != 0: game_state['event_log'].append(get_text_func("log_trust_change").format(change=trust_change))
405
- if freedom_change != 0: game_state['event_log'].append(get_text_func("log_freedom_change_article").format(change=freedom_change))
406
- if safety_change != 0: game_state['event_log'].append(get_text_func("log_safety_change_article").format(change=safety_change))
407
 
408
  game_state['submitted_articles'].append(article)
409
  game_state['reporter_notebook'] = []
410
 
411
- return feedback_text
412
 
413
  # --- UI 표시 함수들 ---
414
  def display_reporter_dashboard(game_state):
@@ -417,11 +571,14 @@ def display_reporter_dashboard(game_state):
417
  get_text_for_ui = lambda k: get_text(k, vocab_level)
418
  status = game_state['status']
419
 
 
 
420
  col1, col2, col3, col4 = st.columns(4)
421
  with col1: st.metric(get_text_for_ui("term_public_trust"), f"{status['public_trust']}%")
422
  with col2: st.metric(get_text_for_ui("term_press_freedom"), f"{status['press_freedom']}%")
423
  with col3: st.metric(get_text_for_ui("term_reporter_safety"), f"{status['reporter_safety']}%")
424
- with col4: st.metric(get_text_for_ui("term_scoop_points"), f"{status['scoop_points']}점")
 
425
 
426
  def display_reporter_notebook(game_state, vocab_level):
427
  get_text_func = lambda k: get_text(k, vocab_level)
@@ -505,9 +662,9 @@ def reporter_simulation_main():
505
  st.subheader("송고한 주요 기사 목록")
506
  if game_state['submitted_articles']:
507
  for article in game_state['submitted_articles']:
508
- tone_text_key = f"article_tone_{article['tone']}" # Corrected line
509
- tone_display_text = get_text_main(tone_text_key) # Corrected line
510
- st.markdown(f"- **{article['headline']}** (논조: {tone_display_text}) - {article['turn']}일차 송고")
511
  else:
512
  st.caption("이번 취재 기간 동안 송고한 기사가 없습니다.")
513
 
@@ -556,7 +713,7 @@ def reporter_simulation_main():
556
  st.session_state.desk_feedback_message = None
557
  st.session_state.game_mode = 'reporter_action'
558
  st.rerun()
559
- else:
560
  st.session_state.game_mode = 'assignment_over'
561
  if game_state.get('event_log') is not None:
562
  game_state['event_log'].append(get_text_main("log_assignment_over").format(scenario_name=SCENARIOS[scenario_key]['display_name']))
@@ -594,25 +751,25 @@ def reporter_simulation_main():
594
  assignment_data = st.session_state.current_assignment_data
595
  submitted_article = generate_article_interface(game_state, vocab_level)
596
  if submitted_article:
597
- feedback = evaluate_article_and_get_feedback(submitted_article, game_state, assignment_data)
598
  st.session_state.desk_feedback_message = feedback
599
- st.session_state.game_mode = 'reporter_action'
600
  st.rerun()
601
 
602
  if st.session_state.desk_feedback_message and st.session_state.game_mode == 'reporter_action':
603
  st.divider()
604
  st.subheader(get_text_main("desk_feedback_title"))
605
- st.warning(st.session_state.desk_feedback_message)
606
 
607
- with col_actions_notebook:
608
  st.header("활동 기록 및 다음 단계")
609
  can_proceed_to_next_day = False
610
  if st.session_state.game_mode == 'reporter_action' and st.session_state.current_assignment_data:
611
  assignment_data = st.session_state.current_assignment_data
612
  if st.session_state.actions_taken_this_turn >= ACTIONS_PER_TURN_LIMIT:
613
- if not assignment_data.get("article_writing_phase"):
614
  can_proceed_to_next_day = True
615
- elif st.session_state.desk_feedback_message:
616
  can_proceed_to_next_day = True
617
 
618
  if can_proceed_to_next_day:
@@ -620,7 +777,7 @@ def reporter_simulation_main():
620
  current_turn = game_state['current_turn']
621
  max_turns_for_scenario = SCENARIOS[scenario_key]['max_turns']
622
 
623
- if current_turn >= max_turns_for_scenario:
624
  st.session_state.game_mode = 'assignment_over'
625
  if game_state.get('event_log') is not None:
626
  game_state['event_log'].append(get_text_main("log_assignment_over").format(scenario_name=SCENARIOS[scenario_key]['display_name']))
@@ -632,6 +789,7 @@ def reporter_simulation_main():
632
 
633
  st.session_state.current_assignment_data = None
634
  st.session_state.actions_taken_this_turn = 0
 
635
  st.rerun()
636
 
637
  st.divider()
@@ -663,7 +821,7 @@ def reporter_simulation_main():
663
  keys_to_clear = ['current_scenario_key', 'game_state', 'current_assignment_data', 'actions_taken_this_turn', 'desk_feedback_message']
664
  for k_to_clear in keys_to_clear:
665
  if k_to_clear in st.session_state: del st.session_state[k_to_clear]
666
- time.sleep(1)
667
  st.rerun()
668
 
669
  if __name__ == "__main__":
 
3
  import random
4
  import time
5
  import math
6
+ import os
7
+ from openai import OpenAI
8
+ import json
9
 
10
  # --- 페이지 설정 (스크립트 최상단) ---
11
  st.set_page_config(layout="wide", page_title="진실을 찾아서: 현대사 취재기록")
12
 
13
+ # --- OpenAI API 키 설정 ---
14
+ if "OPENAI_API_KEY" not in os.environ:
15
+ st.error("OPENAI_API_KEY 환경 변수가 설정되지 않았습니다. API 키를 설정해주세요.")
16
+ st.stop()
17
+
18
+ client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
19
+
20
  # --- 게임 설정 ---
21
+ ACTIONS_PER_TURN_LIMIT = 2
22
 
23
  # --- 시나리오 설정 ---
24
  SCENARIOS = {
 
27
  "start_year": 1960,
28
  "player_role": "신입 기자 (자유일보)",
29
  "initial_press_freedom": 40,
 
30
  "initial_reporter_safety": 70,
31
  "initial_public_trust": 50,
32
  "vocab_level": "보통",
33
+ "max_turns": 8,
34
  },
35
  "5.18_gwangju": {
36
  "display_name": "5.18 광주 현장 취재 (1980)",
37
  "start_year": 1980,
38
  "player_role": "지방 주재 기자 (민주신문)",
39
  "initial_press_freedom": 20,
 
40
  "initial_reporter_safety": 50,
41
  "initial_public_trust": 40,
42
  "vocab_level": "보통",
43
+ "max_turns": 8,
44
  },
45
  "june_struggle": {
46
  "display_name": "6월 항쟁 동행 취재 (1987)",
47
  "start_year": 1987,
48
  "player_role": "사회부 기자 (시민일보)",
49
  "initial_press_freedom": 30,
 
50
  "initial_reporter_safety": 60,
51
  "initial_public_trust": 45,
52
  "vocab_level": "보통",
53
+ "max_turns": 7,
54
  }
55
  }
56
 
57
+ # --- 텍스트 저장소 ---
58
  ALL_TEXTS = {
59
  # --- 공통 UI ---
60
  "game_title": {"보통": "🎙️ 진실을 찾아서: 현대사 취재기록"},
 
63
  "dashboard_title": {"보통": "📊 기자 상황판"},
64
  "dashboard_term": {"보통": "{year}년 {turn}번째 취재일"},
65
  "term_press_freedom": {"보통": "취재 자유도"},
66
+ "term_article_score_avg": {"보통": "평균 기사 점수"},
67
  "term_reporter_safety": {"보통": "기자 안전도"},
68
  "term_public_trust": {"보통": "대중 신뢰도"},
69
  "current_assignment_title": {"보통": "📋 오늘의 취재 지시"},
 
79
  "historical_source_title": {"보통": "참고 자료"},
80
  "reporter_notebook_title": {"보통": "📝 나의 취재 노트"},
81
  "article_writing_title": {"보통": "🖋️ 기사 작성"},
82
+ "desk_feedback_title": {"보통": "📢 AI 편집장 평가"},
83
  "status_loading_assignment": {"보통": "{year}년 {turn}번째 취재일 준비 중..."},
84
  "status_actions_taken": {"보통": "오늘의 주요 취재 활동을 마쳤습니다. 기사를 정리하거나 다음 날로 진행하세요."},
85
  "sidebar_title": {"보통": "메뉴"},
86
  "sidebar_glossary_title": {"보통": "📰 관련 용어/인물"},
87
  "sidebar_current_source_title": {"보통": "📎 현재 참고 자료"},
88
  "sidebar_no_source": {"보통": "현재 참고할 만한 특별 자료가 없습니다."},
89
+ "button_submit_article": {"보통": "기사 송고 (AI 평가)"},
 
 
 
90
  "article_headline_label": {"보통": "기사 제목:"},
91
+ "article_body_label": {"보통": "기사 본문 핵심 (3-5문장 요약):"},
92
  "article_tone_label": {"보통": "기사 논조 선택:"},
93
  "warning_empty_article": {"보통": "기사 제목과 핵심 내용을 모두 작성해주세요."},
94
  "info_no_special_info": {"보통": "특별한 정보는 얻지 못했습니다."},
95
  "log_freedom_loss": {"보통": " - 취재 중 제약 발생, 취재 자유도 {loss} 감소."},
96
  "log_safety_loss": {"보통": " - 취재 중 신변 위협 감지, 기자 안전도 {loss} 감소."},
 
97
  "log_info_acquired": {"보통": " - 정보 획득: {info}"},
98
  "log_no_info_acquired": {"보통": " - 특별한 정보는 얻지 못함."},
99
+ "log_article_submitted": {"보통": "기사 송고: '{headline}' (논조: {tone}) - AI 평가 점수: {score}점"},
100
+ "log_desk_feedback": {"보통": " - AI 편집장 코멘트: {feedback}"},
101
  "log_trust_change": {"보통": " - 대중 신뢰도 {change:+} 변동."},
102
  "log_freedom_change_article": {"보통": " - 취재 자유도 {change:+} 변동."},
103
  "log_safety_change_article": {"보통": " - 기자 안전도 {change:+} 변동."},
104
  "log_assignment_over": {"보통": "--- {scenario_name} 취재 기간 종료 ---"},
105
  "log_next_day_start": {"보통": "--- {year}년 {turn}번째 취재일 시작 ---"},
106
  "button_go_to_article_writing": {"보통": "기사 작성하기"},
107
+ "error_openai_api": {"보통": "OpenAI API 호출 중 오류가 발생했습니다: {error}"},
108
 
109
  # --- 4.19 혁명 취재 시나리오 ---
110
  "scenario_419_revolution_name": {"보통": "4.19 혁명 취재 (1960)"},
111
+ # Turn 1 (3.15 부선거)
112
+ "event_419_t1_assignment": {"보통": "3.15 정부통령 선거일입니다. 전국 투표소 상황 및 개표 과에서 예상되는 부정행정황을 포착하고, 관련자(참관, 유권자, 관위 관계자 등)의 증언을 확보하는 것오늘의 주요 임무입니다. 특히 야당 참관인들의 활동과 여당 측의 조직적인 움직임면밀히 관찰하십시오."},
113
+ "event_419_t1_source": {"보통": "[배경 정보] 현재 자유당 정권의 장기집권에 대한 국민적 불만이 높은 상황입니다. 이번 선거에서 이승만 대통령의 4선과 이기붕 부통령의 당선을 위해 정부와 여당이 조직적으로 선거에 개입할 것이라는 예측이 지배적입니다. 사전투표함 바꿔치기, 유령 유권자 동원, 3인조 및 5인조 공개투표 강요, 야당 참관인 축출 등의 문이 이미 파다하게 퍼져 있습니다."},
114
+ "action_419_t1_opt1_text": {"보통": "서울 ���요 투표소 잠입 및 비밀 촬영 시도"},
115
+ "action_419_t1_opt2_text": {"보통": "야당 감시단과 동행하며 부정 사례 공동 취재"},
116
+ "action_419_t1_opt3_text": {"보통": "선관위 내부 고발자로 추정되는 인물과 비밀 접선 시도"},
117
+ "info_419_t1_opt1_got": {"보통": "정보: 종로구 A투표소에서 자유완장을 청년들이 투표함에량의 투표용지를 추가로 넣는 장면을 멀리서 목격함. 접근하려 하자 위협적인 눈초리를 . (키워드: 투표함 추가 투입, 자유당 완장, 위협)"},
118
+ "info_419_t1_opt2_got": {"보통": "정보: 민주당 참관인 박씨, '성북구 B투표소에서 경찰이 특정 유권자들에게 기표소까동행하며 기표 내용을 확인하는 것을 목격했. 항의했으나 묵살당했다' 격앙된 목소리 증언. 관련 사진 일부 확보. (키워드: 경찰 동행 기표, 참관인 항의 묵살, 민주당)"},
119
+ "info_419_t1_opt3_got": {"보통": "정보: 익명을 요구한 선관위 하급 직원, '오늘 새벽, 일부 지역 투표소향하는 전투표함 중 몇 개가 정체불명의 차량으로 옮겨지는 것을 보았다. 매우 불안하다'고 떨는 목소리로 제보. (키워드: 사전투표함 교체 의혹, 선관위 내부 제보, 불안감)"},
120
+ # Turn 2 (김주열 발견)
121
+ "event_419_t2_assignment": {"보통": "마산에서 3.15 부정선거에 항의하던 시위 중 종된 김주열 학생의 시신참혹한 모습으로 발견되었습니다. 사건의 정확한 사망 원인, 경찰의 초기 대응 과정에서의 문제점, 그리마산 시민과 학생들의 격앙된 반응을 심층 취재십시오."},
122
+ "event_419_t2_source": {"보통": "[배경 정보] 4월 11일, 마산 중앙부두 앞바다에서 오른쪽 눈에 최루탄이 박힌 채 떠오른 김주열 군의 시신은 전국민에게 엄청난 충격을 주었습니다. 경찰은 초기 사인을 단순 익사로 발표려 했으나, 시신 상태가 공개되면서 은폐 의혹이 커 있습니. 마산 지역은 이미 3.15 시위 이후 긴장 상태가 지속되고 있었습니다."},
123
+ "action_419_t2_opt1_text": {"보통": "김주열 군 시신 안치 병원을 찾아 부검의 또는 의료진 접촉 시도"},
124
+ "action_419_t2_opt2_text": {"보통": "마산 시내 학생 시 현장(마산, 북마산파출소 등) 직접 취재"},
125
+ "action_419_t2_opt3_text": {"보통": "최초 발견 어민 김주열 군 가족터뷰"},
126
+ "info_419_t2_opt1_got": {"보통": "정보: 병원 관계자(익명), '부검 결과 김 군 사인은 최루탄 파편에 의한 두개골 골절 뇌손상으로 잠정 결론났다. 경찰이 부검 결과 발표를 미루고 있다'고 전함. (키워드: 최루탄 사인, 두개골 골절, 경찰 발표 지연)"},
127
+ "info_419_t2_opt2_got": {"보통": "정보: 마산상고 학생들이 '학우를 살려내라!', '살인경찰 처단하라!' 등의 구호를 외치며 경찰과 격렬하게 대치 중. 돌과 화염병이 등장했으며, 경찰은 최루탄을 난사하며 강경 진압. 부상 학생 속출. (키워드: 마산 학생시, 경찰과격진압, 부상자)"},
128
+ "info_419_t2_opt3_got": {"보통": "정보: 김주열 군 어머니, '내 아들은 시 나간다고 말하고 나갔다. 경찰이 죽인 것이 틀림없다. 이 원통함을 어떻게 풀어야 할지 모르겠다'며 오열. (키워드: 김주열 가족, 경찰 책임 주장, 원통함)"},
129
+ # Turn 3 (고려대생 피습)
130
+ "event_419_t3_assignment": {"보통": "4월 18일, 부선거 규탄위를 마치고 귀교하던 고려대학교 학생들이 치깡패들에게 피습당하는 사건이생했습니다. 사건의 전모, 경찰의 대응, 학생 및 시민 사회의 반응을 취재하십시오."},
131
+ "event_419_t3_source": {"보통": "[배경 정보] 김주열 열사건 이후 학생 시위가 전국적으확산되는 가운데, 조직폭력배(정치깡패)가 동원되어 시위 학생 폭행하는 사건이 생하여 국민적 분노가 더욱 커지고 있습니다. 경찰 묵인 또는 방조 의혹도 제기되고 있습니다."},
132
+ "action_419_t3_opt1_text": {"보통": "피습 현장(종로4가) 피해 학생 입원 병원 방문 취재"},
133
+ "action_419_t3_opt2_text": {"보통": "경찰서 방문, 사건 수사 진행 상황 및 정치깡패 배후설 관련 질의"},
134
+ "action_419_t3_opt3_text": {"보통": "고려대학교 총학생회 및 다른 대학 학생 대표 인터뷰"},
135
+ "info_419_t3_opt1_got": {"보통": "정보: 피해 학생 증언, '시위 후 평화롭게 돌아가는데 갑자기 쇠파이프와 각목을 든 수십 명의 괴한이 덮쳤다. 살려달라고 외쳤지만 무자비하게 폭행했다.' 병원 복도에는 부상당한 학생들로 가득. (키워드: 고려대생 피습, 정치깡패, 쇠파이프, 집단폭행)"},
136
+ "info_419_t3_opt2_got": {"보통": "정보: 경찰 관계자, '우발적인 학생 간 충돌로 파악하고 있다. 정치깡패 배후설은 낭설이다'라며 수사 확대에 소극적인 태도. (키워드: 경찰 축소수사, 우발적 충돌 주장, 배후부인)"},
137
+ "info_419_t3_opt3_got": {"보통": "정보: 서울대 총학생회장, '이는 명백한 야만적 테러 행위이며, 정권의 비호 없이는 불가능한 일이다. 모든 학생이 연대하여 투쟁할 것'이라고 강력 규탄. (키워드: 학생사회 규탄, 정권 배후 의혹, 연대투쟁)"},
138
+ # Turn 4 (4.19 당일, 경무대 앞 발포)
139
+ "event_419_t4_assignment": {"보통": "4월 19일, 서울 시내에서 대규모 시위가 발생하여 경무대로 향하고 있습니다. 시위대의 규모와 요구사항, 경찰의 대응(발포 여부 포함), 사상자 발생 현황을 긴급 타전해야 합니다. 극도로 위험한 상황이니 신변 안전에 각별히 유의하십시오."},
140
+ "event_419_t4_source": {"보통": "[배경 정보] 고려대생 피습 사건에 분노한 서울 시내 대학생과 중고생, 시민들이 거리로 쏟아져 나와 '이승만 하야', '부정선거 다시 하라' 등을 외치며 경무대로 향하고 있습니다. 경찰은 최루탄과 소방차를 동원해 저지하고 있으나 역부족인 상황입니다."},
141
+ "action_419_t4_opt1_text": {"보통": "경무대 인근 시위대 선두 합류, 현장 상황 실시간 취재 (매우 위험)"},
142
+ "action_419_t4_opt2_text": {"보통": "안전한 건물 옥상 등에서 시위 전체 규모 및 경찰 대응 관찰"},
143
+ "action_419_t4_opt3_text": {"보통": "병원 응급실 방문, 부상자 및 사망자 현황 파악"},
144
+ "info_419_t4_opt1_got": {"보통": "정보: 시위대, 경찰 저지선 뚫고 경무대 진입 시도. 경찰, 시위대를 향해 무차별 발포 시작. 눈앞에서 학생들이 피를 흘리며 쓰러지는 아비규환의 현장. (키워드: 경무대 발포, 무차별 사격, 학생 희생, 아비규환)"},
145
+ "info_419_t4_opt2_got": {"보통": "정보: 수만 명의 시위대가 태평로와 광화문 일대를 가득 메움. 경찰, 최루탄과 함께 실탄 발포 확인. 시가지는 전쟁터 방불. (키워드: 대규모 시위, 경찰 실탄 발포, 시가전 양상)"},
146
+ "info_419_t4_opt3_got": {"보통": "정보: 세브란스 병원 응급실, 총상 환자로 가득. 사망자 다수 발생. 의료진은 쉴 새 없이 움직이지만 역부족. '학생들이 죽어가고 있다'는 절규. (키워드: 총상환자, 다수 사망, 병원 참상, 의료진 부족)"},
147
+ # Turn 5 (계엄령 선포)
148
+ "event_419_t5_assignment": {"보통": "정부가 서울 등 주요 도시에 비상계엄을 선포했습니다. 계엄군의 배치 상황, 시민들의 반응, 그리고 계엄 하에서의 언론 통제 실태를 취재하십시오."},
149
+ "event_419_t5_source": {"보통": "[배경 정보] 4.19 시위가 격화되자 정부는 4월 19일 오후 5시를 기해 서울 지역에 비상계엄을 선포하고 군 병력을 투입했습니다. 이후 부산, 대구, 광주 등 주요 도시로 계엄이 확대되었습니다. 계엄사령부는 집회 및 시위 금지, 언론 검열 등을 포고했습니다."},
150
+ "action_419_t5_opt1_text": {"보통": "시내 주요 지점 계엄군 배치 및 검문검색 현황 취재"},
151
+ "action_419_t5_opt2_text": {"보통": "계엄 하 시민 인터뷰 (두려움 속에서도 저항 의지 확인)"},
152
+ "action_419_t5_opt3_text": {"보통": "계엄사령부 방문, 언론 검열 지침 및 협조 요청 내용 파악"},
153
+ "info_419_t5_opt1_got": {"보통": "정보: 탱크와 장갑차를 앞세운 계엄군이 시내 주요 도로와 관공서를 장악. 시민들의 통행을 엄격히 통제하며, 일부 지역에서는 학생들을 강제 연행하는 모습 목격. (키워드: 계엄군 시내 장악, 탱크 장갑차, 통행금지, 강제연행)"},
154
+ "info_419_t5_opt2_got": {"보통": "정보: 시민 A씨, '군인들이 총칼을 들고 거리를 활보하니 무섭지만, 이대로 독재를 용납할 수는 없다. 기회가 되면 다시 거리로 나갈 것'이라고 다짐. (키워드: 계엄하 시민반응, 공포와 저항, 독재타도 의지)"},
155
+ "info_419_t5_opt3_got": {"보통": "정보: 계엄사 공보실, '국가 안정을 위해 보도 내용을 사전 검열하며, 유언비어 유포 시 엄단할 것'이라는 내용의 보도지침 전달. 사실상 정부 발표 외 보도 금지. (키워드: 언론검열, 보도지침, 계엄사령부, 정보통제)"},
156
+ # Turn 6 (대학교수단 시위)
157
+ "event_419_t6_assignment": {"보통": "4월 25일, 전국 대학교수단이 시국선언문을 발표하고 거리 시위에 나섰습니다. 교수단의 요구사항, 시위 규모 및 양상, 이에 대한 정부와 시민들의 반응을 취재하십시오."},
158
+ "event_419_t6_source": {"보통": "[배경 정보] 학생들의 피에 지식인 사회도 분노했습니다. 서울대 문리대 교수들을 시작으로 전국 27개 대학 258명의 교수들이 '학생들의 피에 보답하라'며 이승만 대통령의 하야와 정부 총사퇴를 요구하는 시국선언을 하고 거리로 나섰습니다. 이는 정권에 큰 도덕적 타격을 주었습니다."},
159
+ "action_419_t6_opt1_text": {"보통": "교수단 시위 현장(국회의사당 앞 등) 동행 취재"},
160
+ "action_419_t6_opt2_text": {"보통": "시국선언 주도 교수 인터뷰 (요구사항 및 결의)"},
161
+ "action_419_t6_opt3_text": {"보통": "시민들의 교수단 시위 지지 및 환호 반응 취재"},
162
+ "info_419_t6_opt1_got": {"보통": "정보: 수백 명의 교수들이 '이승만은 하야하라', '민주주의 수호하자' 등의 플래카드를 들고 질서정연하게 행진. 시민들은 박수와 환호로 지지. 경찰도 이전보다 소극적 대응. (키워드: 교수단 시위, 이승만 하야 요구, 시민 지지, 평화 행진)"},
163
+ "info_419_t6_opt2_got": {"보통": "정보: 서울대 K교수, '더 이상 지식인으로서 불의를 외면할 수 없었다. 이 대통령은 즉각 물러나고, 민주 질서를 회복해야 한다'고 단호히 발언. (키워드: 교수 시국선언, 불의 항거, 민주질서 회복)"},
164
+ "info_419_t6_opt3_got": {"보통": "정보: 시위 지켜보던 시민 L씨, '교수님들까지 나서주시니 이제 정말 이 정권 끝났다. 우리 학생들, 시민들 힘내자!'며 눈시울을 붉힘. (키워드: 시민 감격, 교수 지지, 정권 종말 예감)"},
165
+ # Turn 7 (미국의 압력과 이승만 하야 결심)
166
+ "event_419_t7_assignment": {"보통": "미국 정부가 현 사태에 대한 우려와 함께 이승만 대통령의 결단을 촉구하는 메시지를 전달한 것으로 알려졌습니다. 이승만 대통령의 심경 변화와 하야 가능성, 그리고 자유당 내부의 움직임을 긴급 취재하십시오."},
167
+ "event_419_t7_source": {"보통": "[배경 정보] 4.19 혁명이 격화되자 미국 정부는 월터 매카나기 주한미국대사를 통해 이승만 대통령에게 사실상의 하야를 권고했습니다. 국제적인 고립과 내부의 거센 저항에 직면한 이 대통령은 중대한 결심을 앞두고 있습니다."},
168
+ "action_419_t7_opt1_text": {"보통": "경무대 주변 취재, 이 대통령 측근 동향 파악"},
169
+ "action_419_t7_opt2_text": {"보통": "자유당 고위 관계자 접촉, 당내 분위기 및 수습책 논의 내용 취재"},
170
+ "action_419_t7_opt3_text": {"보통": "주한미국대사관 주변 취재, 미국의 입장 변화 확인"},
171
+ "info_419_t7_opt1_got": {"보통": "정보: 경무대 관계자(익명), '대통령께서 며칠 밤잠을 못 이루시고 깊은 고뇌에 빠져 계신다. 곧 중대 발표가 있을 것이라는 소문이 파다하다.' (키워드: 이승만 고뇌, 중대발표 임박, 경무대 분위기)"},
172
+ "info_419_t7_opt2_got": {"보통": "정보: 자유당 중진 의원 M씨, '이미 대세는 기울었다. 대통령께서 명예롭게 퇴진하시는 것만이 유일한 수습책이다. 당내에서도 하야를 건의해야 한다는 목소리가 커지고 있다.' (키워드: 자유당 하야 건의, 명예퇴진, 당내 분열)"},
173
+ "info_419_t7_opt3_got": {"보통": "정보: 미 대사관 관계자(비공식), '미국 정부는 한국의 민주주의 회복과 안정적인 정권 이양을 강력히 희망한다. 현 상황이 지속되는 것은 바람직하지 않다.' (키워드: 미국 정부 입장, 민주주의 회복 촉구, 정권이양 희망)"},
174
+ # Turn 8 (이승만 대통령 하야 발표)
175
+ "event_419_t8_assignment": {"보통": "4월 26일, 이승만 대통령이 하야를 발표했습니다. 하야 성명 내용, 시민들의 반응, 그리고 향후 정국 전망을 종합적으로 취재하여 특별 기사를 작성해야 합니다. (이번 턴은 취재 활동 없이 바로 기사 작성으로 이어집니다.)"},
176
+ "event_419_t8_source": {"보통": "[속보] 이승만 대통령, 국민의 요구에 따라 대통령직에서 물러나겠다고 발표. 라디오를 통해 하야 성명 발표. 시민들 거리로 쏟아져 나와 환호. 12년간의 자유당 정권 종식."},
177
+ "action_419_t8_opt1_text": {"보통": "하야 성명 전문 및 주요 내용 분석"}, # 정보 획득용 선택지 (실제로는 자동 획득)
178
+ "action_419_t8_opt2_text": {"보통": "시민 환호 현장 및 인터뷰"},
179
+ "action_419_t8_opt3_text": {"보통": "정치권 및 학계 향후 정국 전망 분석"},
180
+ "info_419_t8_opt1_got": {"보통": "정보: 이승만 대통령 하야 성명 전문 입수. '국민이 원한다면 대통령직을 사임하겠다', '3.15 선거는 부정이 있었으니 다시 하도록 지시', '내각책임제 개헌도 할 것' 등 내용 포함. (키워드: 이승만 하야 성명, 국민의 뜻, 부정선거 인정, 내각제 개헌)"},
181
+ "info_419_t8_opt2_got": {"보통": "정보: 서울 시내, 시민들 거리로 뛰쳐나와 '독재 타도, 민주 승리' 외치며 서로 부둥켜안고 눈물. 자동차 경적 울리며 축제 분위기. (키워드: 시민 환호, 민주승리, 축제분위기, 독재종식)"},
182
+ "info_419_t8_opt3_got": {"보통": "정보: 정치학자 P교수, '허정 과도정부 수립 후 조속한 시일 내에 총선거 실시 예상. 민주당 중심의 내각책임제 정부 출범 가능성 높다. 다만, 정치적 혼란과 사회적 요구 분출 우려.' (키워드: 허정과도정부, 총선거, 내각책임제, 정치혼란 우려)"},
183
 
184
  # --- 5.18 광주 취재 시나리오 ---
185
  "scenario_518_gwangju_name": {"보통": "5.18 광주 현장 취재 (1980)"},
186
+ # Turn 1 (5.17 비상계엄 전국 확대)
187
+ "event_518_t1_assignment": {"보통": "5월 17일 24시를 기해 비상계엄이 전국으로 확대되었습니다. 광주 지역 분위기, 주요 인사(생운동 지도부, 재야인사)들의 동향, 그리고 계엄군의 움직임 파악하여 보고하십시오. 언론 통제가 심하니 보안에 각별히 유의해야 합니다."},
188
+ "event_518_t1_source": {"보통": "[배경 정보] 10.26 사건 이후 '서울의 봄'이 찾아왔으나, 신군부가 12.12 군사반란으로 실권을 장악하면서 정국은 다시 경색되고 있습니다. 신군부는 사회 혼란을 빌미로 비상계엄을 국으로 확하고, 김대중, 김영삼 등 주요 정치인들을 체포/연금했습니다. 광주 지역 대학가에서는 학생들의 시위 움직임이 감지되고 있습니다."},
189
+ "action_518_t1_opt1_text": {"보통": "전남대학교 주요 대학가 학생회관 주변 탐문"},
190
+ "action_518_t1_opt2_text": {"보통": "광주 시내 주요 도로 공공기관 계엄군 배치 상황 확인"},
191
+ "action_518_t1_opt3_text": {"보통": "지역 재야인사 또는 지도자 인터뷰"},
192
+ "info_518_t1_opt1_got": {"보통": "정보: 전남대 학생회 간부들, '계엄 확대는 민 열망을 짓밟는 폭거'라며 내일(18일) 오전 교내 시위 계획 중. '휴교령 반대, 철폐' 구호 준비. (키워드: 전남대 학생시위 예고, 계엄반대, 휴교령 철회 요구)"},
193
+ "info_518_t1_opt2_got": {"보통": "정보: 광주역, 버스터미널, 도청 등 주요 지점에 중무장한 공수부대원들배치되기 시작. 시민들은 불안감과 공포감을 느끼고 있음. (키워드: 공수대 배치, 삼엄한 경계, 시민 불안)"},
194
+ "info_518_t1_opt3_got": {"보통": "정보: 익명의 종교 지도, '군부가 정권 장악을 위해 자비탄압을 자행할 능성이 크다. 광주가 희생양이 될까 우려다'며 깊은 한숨. (키워드: 신군부 탄압 우려, 광주 희생양, 종교계 시국관)"},
195
+ # Turn 2 (5.18 첫날, 전남대 앞 충돌)
196
+ "event_518_t2_assignment": {"보통": "5월 18일 오전, 전남대학교 앞에서 학생들과 계엄군 간의 첫 충돌이 발생했습니다. 충돌의 원인과 과정, 학생 및 계엄군의 피해 상황, 그리고 시민들의 반응을 긴급히 취재하십시오. 현장 접근 시 신변 안전에 극히 유의해야 합니다."},
197
+ "event_518_t2_source": {"보통": "[배경 정보] 어제(17일) 비상계엄 전국 확대 조치에 따라 대학 휴교령이 내려졌으나, 전남대 학생들은 이에 항의하며 교문 앞에서 시위를 벌였습니다. 계엄군(공수부대)은 이를 강경 진압하기 시작했습니다."},
198
+ "action_518_t2_opt1_text": {"보통": "전남대학교 정문 앞 시위 현장 직접 취재 (매우 위험)"},
199
+ "action_518_t2_opt2_text": {"보통": "부상 학생 후송된 인근 병원 방문, 피해 상황 ���악"},
200
+ "action_518_t2_opt3_text": {"보통": "현장 주변 상인 및 주민 목격담 청취"},
201
+ "info_518_t2_opt1_got": {"보통": "정보: 공수부대원들이 교문으로 진입하려는 학생들을 향해 무차별적으로 곤봉을 휘두르고 군홧발로 폭행. 여학생, 남학생 가리지 않고 구타. 학생들이 피를 흘리며 쓰러지고 끌려가는 모습 목격. (키워드: 공수부대 과잉진압, 무차별 폭행, 학생 부상, 전남대 정문)"},
202
+ "info_518_t2_opt2_got": {"보통": "정보: 병원 응급실, 두부 외상 및 골절상 입은 학생들로 가득. 한 학생은 '단지 학교에 들어가려 했을 뿐인데, 군인들이 갑자기 몽둥이로 때렸다'며 울분 토로. (키워드: 학생 중상, 병원 응급실, 군인 폭력 증언)"},
203
+ "info_518_t2_opt3_got": {"보통": "정보: 인근 상인, '학생들이 무슨 큰 죄를 지었다고 저렇게까지 때리나. 이건 너무 심하다. 군인들이 아니라 깡패 같다'며 분개. (키워드: 시민 분노, 계엄군 만행 비판, 과잉진압 목격)"},
204
+ # (이하 5.18 턴들 생략)
205
 
206
  # --- 6월 항쟁 취재 시나리오 ---
207
  "scenario_june_struggle_name": {"보통": "6월 항쟁 동행 취재 (1987)"},
208
+ # Turn 1 (박종철 고문치사 사건)
209
+ "event_june_t1_assignment": {"보통": "서울대생 박종철 군이 조사 중 사망하는 사건이 발생했습니다. 경찰 '책상을 탁 치니 억 하고 죽었다' 발표했으, 고문 의혹 강하게기되고 있습니다. 사건의 진실 규명 단서와 주변 반응을 취재하십시오."},
210
+ "event_june_t1_source": {"보통": "[배경 정보] 1987년 1월 14일, 박종철 군은 치안본부 남영동 대공분실에서 조 받던 중 사망했습니다. 경찰 석연치 않은 사인 발표는 국민적 공분을 사고 있으며, 재야단체와 종교 중심으로 진상 규명 요구가 확산되고 있습니다. 전두환 정권의 도덕성에 큰 타격이 예상됩니다."},
211
+ "action_june_t1_opt1_text": {"보통": "사건 담당 경찰서치안본부 주변 취재, 관계자 비공식 접촉 시도"},
212
+ "action_june_t1_opt2_text": {"보통": "박종철 유족 서울대 동료 학생 인터뷰"},
213
+ "action_june_t1_opt3_text": {"보통": "부 참관 의사 또의료계 전 익명 인터뷰 (사인 규명)"},
214
+ "info_june_t1_opt1_got": {"보통": "정보: 치안본부 관계자(익명), '윗선에서 을 빨리 덮으라는 지시가 있었다. 단순 쇼크사로 처리하라는 압력이 상당하다' 귀띔. (키워드: 경찰 윗선 압력, 사건은폐 지시, 쇼크사 조작)"},
215
+ "info_june_t1_opt2_got": {"보통": "정보: 박종철 아버지는 '철아, 가그래이... 아부지는 할 말없데이...'라며 오열. 동료 학생들은 '고문살인 책임자를 처벌하라' 분노. (키워드: 박종철유족 슬픔, 고문살인 규탄, 학생 분노)"},
216
+ "info_june_t1_opt3_got": {"보통": "정보: 부검의 황적준 박사(실언급 가능성 낮으나 게임적 허용), '부검 결과 목 부위 압박 흔적과 폐에서 포음이 들리는 등 쇼크사와는 거리가 멀다. 물고문 가능성배제할 수 없'고 조심스럽게 언급. (키워드: 부검의 증언, 물문 의혹, 경찰발표 반박)"},
217
+ # (이하 6월 항쟁 턴들 생략)
218
 
219
  "article_tone_fact_based": {"보통": "객관적 사실 전달 위주"},
220
  "article_tone_critical": {"보통": "정부/기관 비판적 논조"},
 
224
  "glossary_419_revolution_free_party": {"보통": "**자유당 (自由黨):** 1951년 이승만 대통령을 중심으로 창당된 정당. 1960년 4.19 혁명으로 이승만 대통령이 하야하면서 사실상 해체되었다."},
225
  "glossary_419_revolution_315_election": {"보통": "**3.15 부정선거:** 1960년 3월 15일 실시된 제4대 대통령 및 제5대 부통령 선거에서 자유당 정권이 이승만-이기붕 후보를 당선시키기 위해 자행한 대규모 부정선거. 4.19 혁명의 직접적인 도화선이 되었다."},
226
  "glossary_419_revolution_kimjuyeol": {"보통": "**김주열 (金朱烈):** 1943~1960. 마산상업고등학교 학생. 3.15 마산 시위 중 실종되었다가 4월 11일 눈에 최루탄이 박힌 시신으로 발견되어 4.19 혁명을 격화시키는 계기가 되었다."},
227
+ "glossary_419_revolution_gyeongmudae": {"보통": "**경무대 (景武臺):** 현재 청와대 자리에 있던 조선시대 건물로, 대한민국 정부 수립 후 대통령 관저 및 집무실로 사용되었다. 4.19 혁명 당시 시위대가 경무대로 향하자 경찰이 발포하여 많은 사상자가 발생했다."},
228
+ "glossary_419_revolution_martial_law": {"보통": "**계엄령 (戒嚴令):** 전시, 사변 또는 이에 준하는 국가비상사태 시 법률이 정하는 바에 따라 군사권을 발동하여 치안을 유지할 수 있는 국가긴급권의 하나. 4.19 혁명 당시 정부는 주요 도시에 계엄령을 선포했다."},
229
+
230
  "glossary_518_gwangju_new_military": {"보통": "**신군부 (新軍部):** 1979년 10.26 사건 이후 12.12 군사반란을 통해 권력을 장악한 전두환, 노태우 등 하나회 중심의 군부 세력."},
231
+ "glossary_518_gwangju_517_measure": {"보통": "**5.17 비상계엄 전국 확대 조치:** 1980년 5월 17일 신군부가 비상계엄을 전국으로 확대하고 정치활동 금지, 대학 휴교, 언론 검열 강화 등을 단행한 조치. 5.18 광주 민주화운동의 직접적인 배경이 되었다."},
232
+ "glossary_518_gwangju_paratroopers": {"보통": "**공수부대 (공수특전여단):** 5.18 광주 민주화운동 당시 광주에 투입되어 학생과 시민들을 과잉 진압한 부대. 잔혹한 진압 방식으로 많은 사상자를 발생시켰다."},
233
+ "glossary_518_gwangju_geumnamno": {"보통": "**금남로 (錦南路):** 광주광역시의 중심 도로. 5.18 광주 민주화운동 기간 동안 대규모 시위와 계엄군과의 충돌이 벌어진 핵심 장소였다."},
234
+ "glossary_518_gwangju_citizens_army": {"보통": "**시민군 (市民軍):** 5.18 광주 민주화운동 당시 계엄군의 폭력적인 진압에 맞서 스스로 무장한 광주 시민들. 도청을 중심으로 항쟁을 벌였다."},
235
+ "glossary_518_gwangju_sangmuchungjeong": {"보통": "**상무충정작전 (尙武忠正作戰):** 1980년 5월 27일 새벽 계엄군이 광주 시내로 재진입하여 전남도청 등을 장악한 군사작전명. 이 작전으로 5.18 광주 민주화운동은 비극적으로 진압되었다."},
236
+
237
  "glossary_june_struggle_parkjongchul": {"보통": "**박종철 (朴鍾哲):** 1965~1987. 서울대학교 언어학과 학생. 1987년 1월 경찰의 고문으로 사망하여 6월 민주 항쟁의 도화선이 되었다."},
238
+ "glossary_june_struggle_413_measure": {"보통": "**4.13 호헌 조치 (四一三護憲措置):** 1987년 4월 13일 전두환 대통령이 대통령 직선제 개헌 요구를 거부하고 현행 헌법(간선제)을 고수하겠다고 발표한 조치. 국민적 저항을 불러일으켰다."},
239
+ "glossary_june_struggle_leehanyeol": {"보통": "**이한열 (李韓烈):** 1966~1987. 연세대학교 경영학과 학생. 1987년 6월 9일 시위 중 경찰이 쏜 최루탄에 맞아 중태에 빠졌다가 7월 5일 사망했다. 그의 죽음은 6월 항쟁을 더욱 확산시키는 계기가 되었다."},
240
+ "glossary_june_struggle_610_rally": {"보통": "**6.10 국민대회:** 1987년 6월 10일 '박종철군 고문살인 은폐 규탄 및 민주헌법 쟁취 범국민대회'라는 이름으로 전국적으로 열린 대규모 시위. 6월 항쟁의 본격적인 시작을 알렸다."},
241
+ "glossary_june_struggle_myeongdong_cathedral": {"보통": "**명동성당 (明洞聖堂):** 서울 명동에 위치한 천주교 서울대교구 주교좌 성당. 6월 항쟁 당시 시위대의 주요 농성 장소이자 민주화 운동의 상징적 공간이 되었다."},
242
+ "glossary_june_struggle_necktie_brigade": {"보통": "**넥타이 부대:** 6월 항쟁 당시 시위에 참여한 일반 사무직 회사원들을 일컫는 말. 중산층의 시위 참여를 상징하며 항쟁의 대중적 확산을 보여주었다."},
243
+ "glossary_june_struggle_629_declaration": {"보통": "**6.29 선언:** 1987년 6월 29일 당시 민정당 대표위원 노태우가 발표한 시국 수습 특별 선언. 대통령 직선제 개헌, 김대중 사면복권 등 8개항의 민주화 조치를 내용으로 하며, 6월 항쟁의 실질적인 승리를 의미했다."},
244
  }
245
 
246
  # --- 어휘 조정 함수 ---
 
267
  'game_year': scenario_settings['start_year'],
268
  'status': {
269
  'press_freedom': scenario_settings['initial_press_freedom'],
270
+ 'article_score_total': 0,
271
+ 'article_count': 0,
272
  'reporter_safety': scenario_settings['initial_reporter_safety'],
273
  'public_trust': scenario_settings['initial_public_trust'],
274
  },
 
282
  "4.19_revolution": [
283
  {"turn": 1, "assignment_key": "event_419_t1_assignment", "source_key": "event_419_t1_source",
284
  "options": [
285
+ {"action_key": "action_419_t1_opt1_text", "cost_freedom_risk": 10, "safety_risk": 20, "info_key": "info_419_t1_opt1_got"},
286
+ {"action_key": "action_419_t1_opt2_text", "cost_freedom_risk": 5, "safety_risk": 5, "info_key": "info_419_t1_opt2_got"},
287
+ {"action_key": "action_419_t1_opt3_text", "cost_freedom_risk": 5, "safety_risk": 10, "info_key": "info_419_t1_opt3_got"},
288
+ ], "article_writing_phase": True},
 
289
  {"turn": 2, "assignment_key": "event_419_t2_assignment", "source_key": "event_419_t2_source",
290
  "options": [
291
+ {"action_key": "action_419_t2_opt1_text", "cost_freedom_risk": 15, "safety_risk": 25, "info_key": "info_419_t2_opt1_got"},
292
+ {"action_key": "action_419_t2_opt2_text", "cost_freedom_risk": 10, "safety_risk": 15, "info_key": "info_419_t2_opt2_got"},
293
+ {"action_key": "action_419_t2_opt3_text", "cost_freedom_risk": 20, "safety_risk": 10, "info_key": "info_419_t2_opt3_got"},
294
+ ], "article_writing_phase": True},
295
+ {"turn": 3, "assignment_key": "event_419_t3_assignment", "source_key": "event_419_t3_source",
296
+ "options": [
297
+ {"action_key": "action_419_t3_opt1_text", "cost_freedom_risk": 10, "safety_risk": 20, "info_key": "info_419_t3_opt1_got"},
298
+ {"action_key": "action_419_t3_opt2_text", "cost_freedom_risk": 15, "safety_risk": 5, "info_key": "info_419_t3_opt2_got"},
299
+ {"action_key": "action_419_t3_opt3_text", "cost_freedom_risk": 5, "safety_risk": 10, "info_key": "info_419_t3_opt3_got"},
300
+ ], "article_writing_phase": True},
301
+ {"turn": 4, "assignment_key": "event_419_t4_assignment", "source_key": "event_419_t4_source",
302
+ "options": [
303
+ {"action_key": "action_419_t4_opt1_text", "cost_freedom_risk": 25, "safety_risk": 35, "info_key": "info_419_t4_opt1_got"},
304
+ {"action_key": "action_419_t4_opt2_text", "cost_freedom_risk": 5, "safety_risk": 10, "info_key": "info_419_t4_opt2_got"},
305
+ {"action_key": "action_419_t4_opt3_text", "cost_freedom_risk": 10, "safety_risk": 15, "info_key": "info_419_t4_opt3_got"},
306
+ ], "article_writing_phase": True},
307
+ {"turn": 5, "assignment_key": "event_419_t5_assignment", "source_key": "event_419_t5_source",
308
+ "options": [
309
+ {"action_key": "action_419_t5_opt1_text", "cost_freedom_risk": 10, "safety_risk": 15, "info_key": "info_419_t5_opt1_got"},
310
+ {"action_key": "action_419_t5_opt2_text", "cost_freedom_risk": 15, "safety_risk": 20, "info_key": "info_419_t5_opt2_got"},
311
+ {"action_key": "action_419_t5_opt3_text", "cost_freedom_risk": 20, "safety_risk": 5, "info_key": "info_419_t5_opt3_got"},
312
+ ], "article_writing_phase": True},
313
+ {"turn": 6, "assignment_key": "event_419_t6_assignment", "source_key": "event_419_t6_source",
314
+ "options": [
315
+ {"action_key": "action_419_t6_opt1_text", "cost_freedom_risk": 5, "safety_risk": 10, "info_key": "info_419_t6_opt1_got"},
316
+ {"action_key": "action_419_t6_opt2_text", "cost_freedom_risk": 10, "safety_risk": 5, "info_key": "info_419_t6_opt2_got"},
317
+ {"action_key": "action_419_t6_opt3_text", "cost_freedom_risk": 5, "safety_risk": 5, "info_key": "info_419_t6_opt3_got"},
318
+ ], "article_writing_phase": True},
319
+ {"turn": 7, "assignment_key": "event_419_t7_assignment", "source_key": "event_419_t7_source",
320
+ "options": [
321
+ {"action_key": "action_419_t7_opt1_text", "cost_freedom_risk": 10, "safety_risk": 10, "info_key": "info_419_t7_opt1_got"},
322
+ {"action_key": "action_419_t7_opt2_text", "cost_freedom_risk": 15, "safety_risk": 5, "info_key": "info_419_t7_opt2_got"},
323
+ {"action_key": "action_419_t7_opt3_text", "cost_freedom_risk": 5, "safety_risk": 5, "info_key": "info_419_t7_opt3_got"},
324
+ ], "article_writing_phase": True},
325
+ {"turn": 8, "assignment_key": "event_419_t8_assignment", "source_key": "event_419_t8_source",
326
+ "options": [ # 이 턴은 정보 획득 선택지가 의미 없을 수 있으나, 형식상 유지
327
+ {"action_key": "action_419_t8_opt1_text", "cost_freedom_risk": 0, "safety_risk": 0, "info_key": "info_419_t8_opt1_got"},
328
+ {"action_key": "action_419_t8_opt2_text", "cost_freedom_risk": 0, "safety_risk": 0, "info_key": "info_419_t8_opt2_got"},
329
+ {"action_key": "action_419_t8_opt3_text", "cost_freedom_risk": 0, "safety_risk": 0, "info_key": "info_419_t8_opt3_got"},
330
+ ], "article_writing_phase": True, "is_final_turn_event": True}, # 마지막 턴 표시
331
  ],
332
  "5.18_gwangju": [
333
  {"turn": 1, "assignment_key": "event_518_t1_assignment", "source_key": "event_518_t1_source",
334
  "options": [
335
+ {"action_key": "action_518_t1_opt1_text", "cost_freedom_risk": 15, "safety_risk": 25, "info_key": "info_518_t1_opt1_got"},
336
+ {"action_key": "action_518_t1_opt2_text", "cost_freedom_risk": 10, "safety_risk": 20, "info_key": "info_518_t1_opt2_got"},
337
+ {"action_key": "action_518_t1_opt3_text", "cost_freedom_risk": 20, "safety_risk": 30, "info_key": "info_518_t1_opt3_got"},
338
+ ], "article_writing_phase": False}, # 초기에는 정보 수집만
339
+ {"turn": 2, "assignment_key": "event_518_t2_assignment", "source_key": "event_518_t2_source",
340
+ "options": [
341
+ {"action_key": "action_518_t2_opt1_text", "cost_freedom_risk": 30, "safety_risk": 40, "info_key": "info_518_t2_opt1_got"},
342
+ {"action_key": "action_518_t2_opt2_text", "cost_freedom_risk": 15, "safety_risk": 20, "info_key": "info_518_t2_opt2_got"},
343
+ {"action_key": "action_518_t2_opt3_text", "cost_freedom_risk": 10, "safety_risk": 15, "info_key": "info_518_t2_opt3_got"},
344
+ ], "article_writing_phase": True},
345
+ # (이하 5.18 턴들 추가 필요 - 최대 8턴까지)
346
+ # 예시: 마지막 턴
347
+ {"turn": 8, "assignment_key": "event_518_t8_assignment", "source_key": "event_518_t8_source", # 도청 진압 이후 상황
348
+ "options": [
349
+ {"action_key": "action_518_t8_opt1_text", "cost_freedom_risk": 25, "safety_risk": 30, "info_key": "info_518_t8_opt1_got"},
350
+ {"action_key": "action_518_t8_opt2_text", "cost_freedom_risk": 30, "safety_risk": 20, "info_key": "info_518_t8_opt2_got"},
351
+ ], "article_writing_phase": True, "is_final_turn_event": True},
352
  ],
353
  "june_struggle": [
354
  {"turn": 1, "assignment_key": "event_june_t1_assignment", "source_key": "event_june_t1_source",
355
  "options": [
356
+ {"action_key": "action_june_t1_opt1_text", "cost_freedom_risk": 10, "safety_risk": 15, "info_key": "info_june_t1_opt1_got"},
357
+ {"action_key": "action_june_t1_opt2_text", "cost_freedom_risk": 5, "safety_risk": 10, "info_key": "info_june_t1_opt2_got"},
358
+ {"action_key": "action_june_t1_opt3_text", "cost_freedom_risk": 5, "safety_risk": 5, "info_key": "info_june_t1_opt3_got"},
359
+ ], "article_writing_phase": True},
360
+ # (이하 6월 항쟁 턴들 추가 필요 - 최대 7턴까지)
361
+ # 예시: 마지막 턴
362
+ {"turn": 7, "assignment_key": "event_june_t7_assignment", "source_key": "event_june_t7_source", # 6.29 선언
363
+ "options": [
364
+ {"action_key": "action_june_t7_opt1_text", "cost_freedom_risk": 0, "safety_risk": 0, "info_key": "info_june_t7_opt1_got"},
365
+ {"action_key": "action_june_t7_opt2_text", "cost_freedom_risk": 0, "safety_risk": 0, "info_key": "info_june_t7_opt2_got"},
366
+ ], "article_writing_phase": True, "is_final_turn_event": True},
367
  ]
368
  }
 
369
  # --- 다음 취재 지시 가져오기 ---
370
  def get_next_assignment(scenario_key, current_turn, game_state):
371
  scenario_assignments = HISTORICAL_ASSIGNMENTS.get(scenario_key, [])
 
379
  "source_text": get_text_for_assignment(assignment_data.get("source_key", "")),
380
  "options": [],
381
  "article_writing_phase": assignment_data.get("article_writing_phase", False),
382
+ "is_final_turn_event": assignment_data.get("is_final_turn_event", False) # 마지막 턴인지 여부
383
  }
384
  for opt_data in assignment_data.get("options", []):
385
  final_assignment["options"].append({
386
  "action_text": get_text_for_assignment(opt_data["action_key"]),
387
  "cost_freedom_risk": opt_data.get("cost_freedom_risk", 0),
388
  "safety_risk": opt_data.get("safety_risk", 0),
 
389
  "info_key": opt_data.get("info_key", "")
390
  })
391
 
 
411
  status['reporter_safety'] -= safety_loss
412
  game_state['event_log'].append(get_text_for_processing("log_safety_loss").format(loss=safety_loss))
413
 
 
 
 
 
 
 
414
  info_text = ""
415
  if selected_option.get("info_key"):
416
  info_text = get_text_for_processing(selected_option["info_key"])
 
437
  for idx, note in enumerate(game_state['reporter_notebook']):
438
  st.markdown(f"- {note}")
439
  else:
440
+ st.caption("아직 취재한 내용이 없습니다. 취재 활동을 통해 정보를 수집하세요.")
441
  st.markdown("---")
442
 
443
  with st.form(key="article_form"):
444
+ article_headline = st.text_input(get_text_func("article_headline_label"), placeholder="예: 3.15 부정선거, 그 추악한 실태 고발")
445
+ article_body_summary = st.text_area(get_text_func("article_body_label"), height=150, placeholder="예: 오늘 치러진 정부통령 선거에서 자유당은 조직적인 부정행위를 자행했다. 투표함 바꿔치기, 공개투표 강요 등 민주주의를 유린하는 행태가 곳곳에서 목격되었다...")
446
 
447
  article_tones = {
448
  "fact_based": get_text_func("article_tone_fact_based"),
 
463
  st.warning(get_text_func("warning_empty_article"))
464
  return None
465
 
466
+ if not game_state['reporter_notebook']: # 노트 내용 없으면 제출 불가
467
+ st.warning("취재 노트에 내용이 없습니다. 최소한의 취재 정보가 있어야 기사를 작성할 수 있습니다.")
468
+ return None
469
+
470
  return {
471
  "turn": game_state['current_turn'],
472
  "headline": article_headline,
 
476
  }
477
  return None
478
 
479
+ # --- 기사 평가 및 데스크 피드백 (OpenAI 사용) ---
480
+ def evaluate_article_and_get_feedback_openai(article, game_state, assignment_data):
481
  status = game_state['status']
482
  vocab_level = SCENARIOS[game_state['scenario_key']]["vocab_level"]
483
  get_text_func = lambda k: get_text(k, vocab_level)
484
 
485
+ current_assignment_text = get_text_func(assignment_data["assignment_key"])
486
+ reporter_notes_str = "\n".join([f"- {note}" for note in article['raw_notes']])
487
+ article_tone_text = get_text_func(f"article_tone_{article['tone']}")
488
+
489
+ prompt = f"""
490
+ 당신은 대한민국 현대사를 다루는 신문사의 베테랑 편집장입니다. 신입 기자가 아래와 같은 취재 지시를 받고, 수집한 노트 내용을 바탕으로 기사를 작성했습니다.
491
+
492
+ [취재 지시 내용]
493
+ {current_assignment_text}
494
+
495
+ [기자가 수집한 노트 내용]
496
+ {reporter_notes_str}
497
+
498
+ [기자가 작성한 기사]
499
+ - 제목: {article['headline']}
500
+ - 본문 요약: {article['body_summary']}
501
+ - 선택한 논조: {article_tone_text}
502
+
503
+ [평가 지침]
504
+ 1. **사실 부합도 중요 정보 포함 여부:** 기사가 취재 노트의 핵심 정보와 취재 지시의 주요 사항을 얼마나 잘 반영하고 있는가? 중요한 사실이 누락되거나 왜곡되지는 않았는가? (40점 만점)
505
+ 2. **논리성 명확성:** 기사의 내용이 논리적으로 전개되며, 독자가 이해하기 쉽게 명확하게 작성되었는가? (30점 만점)
506
+ 3. **논조의 적절성 및 영향력:** 선택한 논조가 현재 상황과 기사 내용에 적절하며, 사회에 긍정적 또는 부정적 영향을 미칠 가능성은 어떠한가? (기자의 안전도 고려) (30점 만점)
507
+
508
+ 평가 지침에 따라 항목별로 간략한 코멘트와 함께 점수를 부여고, 총점(100점 만점)과 종합적인 피드백(2-3문장)을 제공해주십시오.
509
+ 피드백은 기자가 발전할 수 있도록 구체적이고 건설적인 내용으로 작성해주세요.
510
+
511
+ 출력 형식은 다음과 같이 JSON 형태로 제공해주세요:
512
+ {{
513
+ "score_fact": 점수(숫자),
514
+ "comment_fact": "코멘트(문자열)",
515
+ "score_logic": 점수(숫자),
516
+ "comment_logic": "코멘트(문자열)",
517
+ "score_tone": 점수(숫자),
518
+ "comment_tone": "코멘트(문자열)",
519
+ "total_score": 총점(숫자),
520
+ "overall_feedback": "종합 피드백(문자열)"
521
+ }}
522
+ """
523
+ try:
524
+ with st.spinner("AI 편집장이 기사를 평가 중입니다..."):
525
+ response = client.chat.completions.create(
526
+ model="gpt-4o-mini", # 또는 gpt-4o
527
+ messages=[{"role": "user", "content": prompt}],
528
+ response_format={"type": "json_object"},
529
+ temperature=0.5,
530
+ max_tokens=600 # 토큰 수 넉넉하게
531
+ )
532
+ ai_evaluation_str = response.choices[0].message.content
533
+ ai_evaluation = json.loads(ai_evaluation_str)
534
+
535
+ total_score = ai_evaluation.get("total_score", 0)
536
+ overall_feedback = ai_evaluation.get("overall_feedback", "AI 평가를 가���오지 못했습니다.")
537
+
538
+ except Exception as e:
539
+ st.error(get_text_func("error_openai_api").format(error=str(e)))
540
+ total_score = random.randint(40, 70)
541
+ overall_feedback = "AI 편집장 시스템 오류로 자동 평가되었습니다. 내용은 훌륭하지만, 조금 더 팩트 체크에 신경 써주세요."
542
 
543
+ trust_change = (total_score - 50) // 5
544
  status['public_trust'] = max(0, min(100, status['public_trust'] + trust_change))
 
 
545
 
546
+ if article['tone'] == "critical" and total_score > 60:
547
+ freedom_change = -random.randint(5, 10)
548
+ safety_change = -random.randint(3, 8)
549
+ status['press_freedom'] = max(0, min(100, status['press_freedom'] + freedom_change))
550
+ status['reporter_safety'] = max(0, min(100, status['reporter_safety'] + safety_change))
551
+ if freedom_change != 0: game_state['event_log'].append(get_text_func("log_freedom_change_article").format(change=freedom_change))
552
+ if safety_change != 0: game_state['event_log'].append(get_text_func("log_safety_change_article").format(change=safety_change))
553
+
554
+ status['article_score_total'] += total_score
555
+ status['article_count'] += 1
556
 
557
+ tone_display_text = get_text_func(f"article_tone_{article['tone']}")
558
+ game_state['event_log'].append(get_text_func("log_article_submitted").format(headline=article['headline'], tone=tone_display_text, score=total_score))
559
+ game_state['event_log'].append(get_text_func("log_desk_feedback").format(feedback=overall_feedback))
560
  if trust_change != 0: game_state['event_log'].append(get_text_func("log_trust_change").format(change=trust_change))
 
 
561
 
562
  game_state['submitted_articles'].append(article)
563
  game_state['reporter_notebook'] = []
564
 
565
+ return overall_feedback
566
 
567
  # --- UI 표시 함수들 ---
568
  def display_reporter_dashboard(game_state):
 
571
  get_text_for_ui = lambda k: get_text(k, vocab_level)
572
  status = game_state['status']
573
 
574
+ avg_score = (status['article_score_total'] / status['article_count']) if status['article_count'] > 0 else 0
575
+
576
  col1, col2, col3, col4 = st.columns(4)
577
  with col1: st.metric(get_text_for_ui("term_public_trust"), f"{status['public_trust']}%")
578
  with col2: st.metric(get_text_for_ui("term_press_freedom"), f"{status['press_freedom']}%")
579
  with col3: st.metric(get_text_for_ui("term_reporter_safety"), f"{status['reporter_safety']}%")
580
+ with col4: st.metric(get_text_for_ui("term_article_score_avg"), f"{avg_score:.1f}점")
581
+
582
 
583
  def display_reporter_notebook(game_state, vocab_level):
584
  get_text_func = lambda k: get_text(k, vocab_level)
 
662
  st.subheader("송고한 주요 기사 목록")
663
  if game_state['submitted_articles']:
664
  for article in game_state['submitted_articles']:
665
+ tone_text_key = f"article_tone_{article['tone']}"
666
+ tone_display_text = get_text_main(tone_text_key)
667
+ st.markdown(f"- **{article['headline']}** (논조: {tone_display_text}) - {article['turn']}일차 송고 (AI 점수: {game_state['submitted_articles'][-1].get('ai_score', 'N/A')}점)") # 마지막 기사 점수 표시 (개선 필요)
668
  else:
669
  st.caption("이번 취재 기간 동안 송고한 기사가 없습니다.")
670
 
 
713
  st.session_state.desk_feedback_message = None
714
  st.session_state.game_mode = 'reporter_action'
715
  st.rerun()
716
+ else: # 모든 턴 종료
717
  st.session_state.game_mode = 'assignment_over'
718
  if game_state.get('event_log') is not None:
719
  game_state['event_log'].append(get_text_main("log_assignment_over").format(scenario_name=SCENARIOS[scenario_key]['display_name']))
 
751
  assignment_data = st.session_state.current_assignment_data
752
  submitted_article = generate_article_interface(game_state, vocab_level)
753
  if submitted_article:
754
+ feedback = evaluate_article_and_get_feedback_openai(submitted_article, game_state, assignment_data)
755
  st.session_state.desk_feedback_message = feedback
756
+ st.session_state.game_mode = 'reporter_action' # 피드백 표시 후 다음날로 넘어갈 수 있도록
757
  st.rerun()
758
 
759
  if st.session_state.desk_feedback_message and st.session_state.game_mode == 'reporter_action':
760
  st.divider()
761
  st.subheader(get_text_main("desk_feedback_title"))
762
+ st.warning(st.session_state.desk_feedback_message) # AI 피드백 표시
763
 
764
+ with col_actions_notebook: # 오른쪽 컬럼
765
  st.header("활동 기록 및 다음 단계")
766
  can_proceed_to_next_day = False
767
  if st.session_state.game_mode == 'reporter_action' and st.session_state.current_assignment_data:
768
  assignment_data = st.session_state.current_assignment_data
769
  if st.session_state.actions_taken_this_turn >= ACTIONS_PER_TURN_LIMIT:
770
+ if not assignment_data.get("article_writing_phase"): # 기사 작성 없는 턴
771
  can_proceed_to_next_day = True
772
+ elif st.session_state.desk_feedback_message: # 기사 작성 후 피드백까지 받은 상태
773
  can_proceed_to_next_day = True
774
 
775
  if can_proceed_to_next_day:
 
777
  current_turn = game_state['current_turn']
778
  max_turns_for_scenario = SCENARIOS[scenario_key]['max_turns']
779
 
780
+ if current_turn >= max_turns_for_scenario or st.session_state.current_assignment_data.get("is_final_turn_event", False):
781
  st.session_state.game_mode = 'assignment_over'
782
  if game_state.get('event_log') is not None:
783
  game_state['event_log'].append(get_text_main("log_assignment_over").format(scenario_name=SCENARIOS[scenario_key]['display_name']))
 
789
 
790
  st.session_state.current_assignment_data = None
791
  st.session_state.actions_taken_this_turn = 0
792
+ # desk_feedback_message는 다음 턴 시작 시 assignment_briefing에서 초기화
793
  st.rerun()
794
 
795
  st.divider()
 
821
  keys_to_clear = ['current_scenario_key', 'game_state', 'current_assignment_data', 'actions_taken_this_turn', 'desk_feedback_message']
822
  for k_to_clear in keys_to_clear:
823
  if k_to_clear in st.session_state: del st.session_state[k_to_clear]
824
+ time.sleep(1) # 메시지 확인 시간
825
  st.rerun()
826
 
827
  if __name__ == "__main__":