JUNGU commited on
Commit
b89f81e
·
verified ·
1 Parent(s): f787b99

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +140 -263
src/streamlit_app.py CHANGED
@@ -11,38 +11,14 @@ from collections import Counter
11
  import json
12
  import os
13
  from datetime import datetime, timedelta
14
- import openai
15
  from dotenv import load_dotenv
16
  import traceback
17
  import plotly.graph_objects as go
18
  import schedule
19
  import threading
20
  import matplotlib.pyplot as plt
21
-
22
- # /tmp 경로 설정
23
- TMP_DIR = "/tmp"
24
- SAVED_ARTICLES_PATH = os.path.join(TMP_DIR, "saved_articles.json")
25
- SCHEDULED_NEWS_DIR = os.path.join(TMP_DIR, "scheduled_news")
26
-
27
- # NLTK 데이터 경로 설정 (현재 디렉토리)
28
- NLTK_DATA_DIR = "nltk_data"
29
-
30
- # NLTK 데이터 경로 추가
31
- nltk.data.path.insert(0, NLTK_DATA_DIR)
32
-
33
- # 필요한 NLTK 데이터 확인
34
- required_nltk_data = {
35
- 'punkt': 'tokenizers/punkt',
36
- 'stopwords': 'corpora/stopwords'
37
- }
38
-
39
- for data_name, data_path in required_nltk_data.items():
40
- try:
41
- nltk.data.find(data_path)
42
- except LookupError:
43
- st.error(f"NLTK 데이터 '{data_name}'가 필요합니다. 다음 명령어로 다운로드하세요:")
44
- st.code(f"python -c \"import nltk; nltk.download('{data_name}', download_dir='nltk_data')\"")
45
- st.stop()
46
 
47
  # 워드클라우드 추가
48
  try:
@@ -68,73 +44,68 @@ global_scheduler_state = SchedulerState()
68
  if 'openai_api_key' not in st.session_state:
69
  st.session_state.openai_api_key = None
70
 
71
- # API 키 로드 (허깅페이스 환경변수 우선, 다음으로 Streamlit secrets, 그 다음 .env 파일)
72
- if st.session_state.openai_api_key is None:
73
- st.session_state.openai_api_key = os.getenv('OPENAI_API_KEY') # Hugging Face
74
- if st.session_state.openai_api_key is None:
75
- try:
76
- if 'OPENAI_API_KEY' in st.secrets: # Streamlit Cloud
77
- st.session_state.openai_api_key = st.secrets['OPENAI_API_KEY']
78
- except Exception: # st.secrets가 존재하지 않는 환경 (로컬 등)
79
- pass
80
- if st.session_state.openai_api_key is None:
81
- load_dotenv() # 로컬 .env 파일
82
- st.session_state.openai_api_key = os.getenv('OPENAI_API_KEY')
83
 
84
- # OpenAI API 키 설정
85
- # openai.api_key 설정은 각 API 호출 직전에 st.session_state.openai_api_key 사용하도록 변경하거나,
86
- # 시작 시점에 한 번 설정합니다. 여기서는 후자를 선택합니다.
87
- if st.session_state.openai_api_key:
88
  openai.api_key = st.session_state.openai_api_key
89
- else:
90
- # UI 초기에는 키가 없을 수 있으므로, 나중에 입력 openai.api_key가 설정되도록 유도
91
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
  # 페이지 설정
94
  st.set_page_config(page_title="뉴스 기사 도구", page_icon="📰", layout="wide")
95
 
96
- # 사이드바 메뉴 설정
97
- st.sidebar.title("뉴스 기사 도구")
98
- menu = st.sidebar.radio(
99
- "메뉴 선택",
100
- ["뉴스 기사 크롤링", "기사 분석하기", "새 기사 생성하기", "뉴스 기사 예약하기"]
101
- )
102
-
103
- # 디렉토리 생성 함수
104
- def ensure_directory(directory):
105
- try:
106
- os.makedirs(directory, mode=0o777, exist_ok=True)
107
- # 디렉토리 권한 설정
108
- os.chmod(directory, 0o777)
109
- except Exception as e:
110
- st.error(f"디렉토리 생성 중 오류 발생: {str(e)}")
111
- return False
112
- return True
113
 
114
  # 저장된 기사를 불러오는 함수
115
  def load_saved_articles():
116
- try:
117
- ensure_directory(TMP_DIR)
118
- if os.path.exists(SAVED_ARTICLES_PATH):
119
- with open(SAVED_ARTICLES_PATH, 'r', encoding='utf-8') as f:
120
- return json.load(f)
121
- except Exception as e:
122
- st.error(f"기사 로드 중 오류 발생: {str(e)}")
123
- return []
124
  return []
125
 
126
  # 기사를 저장하는 함수
127
  def save_articles(articles):
128
- try:
129
- ensure_directory(TMP_DIR)
130
- with open(SAVED_ARTICLES_PATH, 'w', encoding='utf-8') as f:
131
- json.dump(articles, f, ensure_ascii=False, indent=2)
132
- # 파일 권한 설정
133
- os.chmod(SAVED_ARTICLES_PATH, 0o666)
134
- except Exception as e:
135
- st.error(f"기사 저장 중 오류 발생: {str(e)}")
136
- return False
137
- return True
138
 
139
  @st.cache_data
140
  def crawl_naver_news(keyword, num_articles=5):
@@ -221,12 +192,22 @@ def get_article_content(url):
221
  except Exception as e:
222
  return f"오류 발생: {str(e)}"
223
 
224
- # NLTK를 이용한 키워드 분석
225
  def analyze_keywords(text, top_n=10):
226
- # 한국어 불용어 목록 (직접 정의해야 합니다)
227
  korean_stopwords = ['이', '그', '저', '것', '및', '등', '를', '을', '에', '에서', '의', '으로', '로']
228
 
229
- tokens = word_tokenize(text)
 
 
 
 
 
 
 
 
 
 
230
  tokens = [word for word in tokens if word.isalnum() and len(word) > 1 and word not in korean_stopwords]
231
 
232
  word_count = Counter(tokens)
@@ -288,45 +269,46 @@ def extract_keywords_for_wordcloud(text, top_n=50):
288
 
289
 
290
  # 워드 클라우드 생성 함수
291
-
292
  def generate_wordcloud(keywords_dict):
293
  if not WordCloud:
294
  st.warning("워드클라우드 설치안되어 있습니다.")
295
  return None
296
  try:
297
- # 프로젝트 루트에 NanumGothic.ttf가 있다고 가정
298
- font_path = "NanumGothic.ttf"
299
-
300
- # 로컬에 폰트 파일이 있는지 확인, 없으면 기본으로 시도
301
- if not os.path.exists(font_path):
302
- st.warning(f"폰트 파일({font_path})을 찾을 수 없습니다. 기본 폰트로 워드클라우드를 생성합니다. 한글이 깨질 수 있습니다.")
303
- # font_path = None # 또는 시스템 기본 폰트 경로를 지정 (플랫폼마다 다름)
304
- # WordCloud 생성자에서 font_path를 None으로 두면 시스템 기본값을 시도하거나, 아예 빼고 호출
305
- wc = WordCloud(
306
- width=800,
307
- height=400,
308
- background_color='white',
309
- colormap='viridis',
310
- max_font_size=150,
311
- random_state=42
312
- ).generate_from_frequencies(keywords_dict)
313
- else:
314
- wc= WordCloud(
315
- font_path=font_path,
316
- width=800,
317
- height=400,
318
- background_color = 'white',
319
- colormap = 'viridis',
320
- max_font_size=150,
321
- random_state=42
322
- ).generate_from_frequencies(keywords_dict)
 
 
 
 
323
 
324
  return wc
325
 
326
  except Exception as e:
327
- st.error(f"워드클라우드 생성 중 오류 발생: {str(e)}")
328
- # traceback.print_exc() # 디버깅 시 사용
329
- st.warning("워드클라우드 생성에 실패했습니다. 폰트 문제일 수 있습니다. NanumGothic.ttf 파일이 프로젝트 루트에 있는지 확인해주세요.")
330
  return None
331
 
332
  # 뉴스 분석 함수
@@ -353,13 +335,13 @@ def analyze_news_content(news_df):
353
  results['top_keywords'] = []
354
  return results
355
 
356
- # OpenAI API를 이용한 새 기사 생성
357
  def generate_article(original_content, prompt_text):
358
- if not st.session_state.openai_api_key:
359
- return "오류: OpenAI API 키가 설정되지 않았습니다. 사이드바에서 키를 입력하거나 환경 변수를 설정해주세요."
360
- openai.api_key = st.session_state.openai_api_key
361
  try:
362
- response = openai.chat.completions.create(
 
 
 
363
  model="gpt-4.1-mini",
364
  messages=[
365
  {"role": "system", "content": "당신은 전문적인 뉴스 기자입니다. 주어진 내용을 바탕으로 새로운 기사를 작성해주세요."},
@@ -367,22 +349,22 @@ def generate_article(original_content, prompt_text):
367
  ],
368
  max_tokens=2000
369
  )
370
- return response.choices[0].message.content
371
  except Exception as e:
372
  return f"기사 생성 오류: {str(e)}"
373
 
374
- # OpenAI API를 이용한 이미지 생성
375
  def generate_image(prompt):
376
- if not st.session_state.openai_api_key:
377
- return "오류: OpenAI API 키가 설정되지 않았습니다. 사이드바에서 키를 입력하거나 환경 변수를 설정해주세요."
378
- openai.api_key = st.session_state.openai_api_key
379
  try:
380
- response = openai.images.generate(
381
- model="gpt-image-1",
382
- prompt=prompt
 
 
 
 
383
  )
384
- image_base64=response.data[0].b64_json
385
- return f"data:image/png;base64,{image_base64}"
386
  except Exception as e:
387
  return f"이미지 생성 오류: {str(e)}"
388
 
@@ -413,21 +395,12 @@ def perform_news_task(task_type, keyword, num_articles, file_prefix):
413
  time.sleep(0.5) # 서버 부하 방지
414
 
415
  # 결과 저장
416
- if not ensure_directory(SCHEDULED_NEWS_DIR):
417
- print(f"스케줄된 뉴스 디렉토리 생성 실패")
418
- return
419
-
420
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
421
- filename = os.path.join(SCHEDULED_NEWS_DIR, f"{file_prefix}_{task_type}_{timestamp}.json")
422
 
423
- try:
424
- with open(filename, 'w', encoding='utf-8') as f:
425
- json.dump(articles, f, ensure_ascii=False, indent=2)
426
- # 파일 권한 설정
427
- os.chmod(filename, 0o666)
428
- except Exception as e:
429
- print(f"파일 저장 중 오류 발생: {e}")
430
- return
431
 
432
  global_scheduler_state.last_run = datetime.now()
433
  print(f"{datetime.now()} - {task_type} 뉴스 기사 수집 완료: {keyword}")
@@ -563,7 +536,7 @@ if menu == "뉴스 기사 크롤링":
563
  st.write(f"**요약:** {article['description']}")
564
  st.write(f"**링크:** {article['link']}")
565
  st.write("**본문 미리보기:**")
566
- st.write(article['content'][:300] + "...")
567
 
568
  elif menu == "기사 분석하기":
569
  st.header("기사 분석하기")
@@ -598,7 +571,6 @@ elif menu == "기사 분석하기":
598
  keyword_tab1, keyword_tab2 = st.tabs(["키워드 빈도", "워드클라우드"])
599
 
600
  with keyword_tab1:
601
-
602
  keywords = analyze_keywords(selected_article['content'])
603
 
604
  # 시각화
@@ -633,7 +605,14 @@ elif menu == "기사 분석하기":
633
  # 텍스트 통계 계산
634
  word_count = len(re.findall(r'\b\w+\b', content))
635
  char_count = len(content)
636
- sentence_count = len(re.split(r'[.!?]+', content))
 
 
 
 
 
 
 
637
  avg_word_length = sum(len(word) for word in re.findall(r'\b\w+\b', content)) / word_count if word_count > 0 else 0
638
  avg_sentence_length = word_count / sentence_count if sentence_count > 0 else 0
639
 
@@ -653,127 +632,31 @@ elif menu == "기사 분석하기":
653
  with col2:
654
  st.metric("평균 문장 길이", f"{avg_sentence_length:.1f}단어")
655
 
656
- # 텍스트 복잡성 점수 (간단한 예시)
657
  complexity_score = min(10, (avg_sentence_length / 10) * 5 + (avg_word_length / 5) * 5)
658
  st.progress(complexity_score / 10)
659
  st.write(f"텍스트 복잡성 점수: {complexity_score:.1f}/10")
660
-
661
- # 출현 빈도 막대 그래프
662
- st.subheader("품사별 분포 (한국어/영어 지원)")
663
- try:
664
- # KoNLPy 설치 확인
665
- try:
666
- from konlpy.tag import Okt
667
- konlpy_installed = True
668
- except ImportError:
669
- konlpy_installed = False
670
- st.warning("한국어 형태소 분석을 위해 KoNLPy를 설치해주세요: pip install konlpy")
671
-
672
- # 영어 POS tagger 준비
673
- from nltk import pos_tag
674
- try:
675
- nltk.data.find('taggers/averaged_perceptron_tagger')
676
- except LookupError:
677
- nltk.download('averaged_perceptron_tagger')
678
-
679
- # Try using the correct resource name as shown in the error message
680
- try:
681
- nltk.data.find('averaged_perceptron_tagger_eng')
682
- except LookupError:
683
- nltk.download('averaged_perceptron_tagger_eng')
684
-
685
- # 언어 감지 (간단한 방식)
686
- is_korean = bool(re.search(r'[가-힣]', content))
687
-
688
- if is_korean and konlpy_installed:
689
- # 한국어 형태소 분석
690
- okt = Okt()
691
- tagged = okt.pos(content)
692
-
693
- # 한국어 품사 매핑
694
- pos_dict = {
695
- 'Noun': '명사', 'NNG': '명사', 'NNP': '고유명사',
696
- 'Verb': '동사', 'VV': '동사', 'VA': '형용사',
697
- 'Adjective': '형용사',
698
- 'Adverb': '부사',
699
- 'Josa': '조사', 'Punctuation': '구두점',
700
- 'Determiner': '관형사', 'Exclamation': '감탄사'
701
- }
702
-
703
- pos_counts = {'명사': 0, '동사': 0, '형용사': 0, '부사': 0, '조사': 0, '구두점': 0, '관형사': 0, '감탄사': 0, '기타': 0}
704
-
705
- for _, pos in tagged:
706
- if pos in pos_dict:
707
- pos_counts[pos_dict[pos]] += 1
708
- elif pos.startswith('N'): # 기타 명사류
709
- pos_counts['명사'] += 1
710
- elif pos.startswith('V'): # 기타 동사류
711
- pos_counts['동사'] += 1
712
- else:
713
- pos_counts['기타'] += 1
714
-
715
- else:
716
- # 영어 POS 태깅
717
- tokens = word_tokenize(content.lower())
718
- tagged = pos_tag(tokens)
719
-
720
- # 영어 품사 매핑
721
- pos_dict = {
722
- 'NN': '명사', 'NNS': '명사', 'NNP': '고유명사', 'NNPS': '고유명사',
723
- 'VB': '동사', 'VBD': '동사', 'VBG': '동사', 'VBN': '동사', 'VBP': '동사', 'VBZ': '동사',
724
- 'JJ': '형용사', 'JJR': '형용사', 'JJS': '형용사',
725
- 'RB': '부사', 'RBR': '부사', 'RBS': '부사'
726
- }
727
-
728
- pos_counts = {'명사': 0, '동사': 0, '형용사': 0, '부사': 0, '기타': 0}
729
-
730
- for _, pos in tagged:
731
- if pos in pos_dict:
732
- pos_counts[pos_dict[pos]] += 1
733
- else:
734
- pos_counts['기타'] += 1
735
-
736
- # 결과 시각화
737
- pos_df = pd.DataFrame({
738
- '품사': list(pos_counts.keys()),
739
- '빈도': list(pos_counts.values())
740
- })
741
-
742
- st.bar_chart(pos_df.set_index('품사'))
743
-
744
- if is_korean:
745
- st.info("한국어 텍스트가 감지되었습니다.")
746
- else:
747
- st.info("영어 텍스트가 감지되었습니다.")
748
- except Exception as e:
749
- st.error(f"품사 분석 중 오류 발생: {str(e)}")
750
- st.error(traceback.format_exc())
751
 
752
  elif analysis_type == "감정 분석":
753
  if st.button("감정 분석하기"):
754
  if st.session_state.openai_api_key:
755
  with st.spinner("기사의 감정을 분석 중입니다..."):
756
  try:
757
- # 감정 분석 API 호출 전에 확인 및 설정
758
- if not openai.api_key:
759
- if st.session_state.openai_api_key:
760
- openai.api_key = st.session_state.openai_api_key
761
- else:
762
- st.error("OpenAI API 키가 설정되지 않았습니다.")
763
- st.stop()
764
-
765
- response = openai.chat.completions.create(
766
  model="gpt-4.1-mini",
767
  messages=[
768
  {"role": "system", "content": "당신은 텍스트의 감정과 논조를 분석하는 전문가입니다. 다음 뉴스 기사의 감정과 논조를 분석하고, '긍정적', '부정적', '중립적' 중 하나로 분류해 주세요. 또한 기사에서 드러나는 핵심 감정 키워드를 5개 추출하고, 각 키워드별로 1-10 사이의 강도 점수를 매겨주세요. JSON 형식으로 다음과 같이 응답해주세요: {'sentiment': '긍정적/부정적/중립적', 'reason': '이유 설명...', 'keywords': [{'word': '키워드1', 'score': 8}, {'word': '키워드2', 'score': 7}, ...]}"},
769
  {"role": "user", "content": f"다음 뉴스 기사를 분석해 주세요:\n\n제목: {selected_article['title']}\n\n내용: {selected_article['content'][:1500]}"}
770
  ],
771
- max_tokens=800,
772
- response_format={"type": "json_object"}
773
  )
774
 
775
- # JSON 파싱
776
- analysis_result = json.loads(response.choices[0].message.content)
777
 
778
  # 결과 시각화
779
  st.subheader("감정 분석 결과")
@@ -960,7 +843,6 @@ elif menu == "새 기사 생성하기":
960
 
961
  if st.button("새 기사 생성하기"):
962
  if st.session_state.openai_api_key:
963
- # openai.api_key = st.session_state.openai_api_key # 이미 상단에서 설정됨 또는 각 함수 호출 시 설정
964
  with st.spinner("기사를 생성 중입니다..."):
965
  new_article = generate_article(selected_article['content'], prompt_text)
966
 
@@ -979,16 +861,9 @@ elif menu == "새 기사 생성하기":
979
  """
980
 
981
  # 이미지 생성
982
- # 이미지 생성 API 호출 전에 키 확인 및 설정
983
- if not openai.api_key:
984
- if st.session_state.openai_api_key:
985
- openai.api_key = st.session_state.openai_api_key
986
- else:
987
- st.error("OpenAI API 키가 설정되지 않았습니다.")
988
- st.stop()
989
  image_url = generate_image(image_prompt)
990
 
991
- if image_url and not image_url.startswith("이미지 생성 오류") and not image_url.startswith("오류: OpenAI API 키가 설정되지 않았습니다."):
992
  st.subheader("생성된 이미지:")
993
  st.image(image_url)
994
  else:
@@ -1010,6 +885,8 @@ elif menu == "새 기사 생성하기":
1010
  else:
1011
  st.warning("OpenAI API 키를 사이드바에서 설정해주세요.")
1012
 
 
 
1013
  elif menu == "뉴스 기사 예약하기":
1014
  st.header("뉴스 기사 예약하기")
1015
 
@@ -1157,13 +1034,13 @@ elif menu == "뉴스 기사 예약하기":
1157
  )
1158
 
1159
  # 수집된 파일 보기
1160
- if os.path.exists(SCHEDULED_NEWS_DIR):
1161
- files = [f for f in os.listdir(SCHEDULED_NEWS_DIR) if f.endswith('.json')]
1162
  if files:
1163
  st.subheader("수집된 파일 열기")
1164
- selected_file = st.selectbox("파일 선택", files, index=len(files)-1 if files else 0) # files가 비어있을 경우 대비
1165
  if selected_file and st.button("파일 내용 보기"):
1166
- with open(os.path.join(SCHEDULED_NEWS_DIR, selected_file), 'r', encoding='utf-8') as f:
1167
  articles = json.load(f)
1168
 
1169
  st.write(f"**파일명:** {selected_file}")
@@ -1179,4 +1056,4 @@ elif menu == "뉴스 기사 예약하기":
1179
 
1180
  # 푸터
1181
  st.markdown("---")
1182
- st.markdown("© 뉴스 기사 도구 @conanssam")
 
11
  import json
12
  import os
13
  from datetime import datetime, timedelta
14
+ import openai # 구 버전 방식 사용
15
  from dotenv import load_dotenv
16
  import traceback
17
  import plotly.graph_objects as go
18
  import schedule
19
  import threading
20
  import matplotlib.pyplot as plt
21
+ import kss # KoNLPy 대신 KSS 사용
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  # 워드클라우드 추가
24
  try:
 
44
  if 'openai_api_key' not in st.session_state:
45
  st.session_state.openai_api_key = None
46
 
47
+ # 여러 방법으로 API 키 로드 시도
48
+ load_dotenv() # .env 파일에서 로드 시도
 
 
 
 
 
 
 
 
 
 
49
 
50
+ # 1. 환경 변수에서 API 키 확인
51
+ if os.environ.get('OPENAI_API_KEY'):
52
+ st.session_state.openai_api_key = os.environ.get('OPENAI_API_KEY')
 
53
  openai.api_key = st.session_state.openai_api_key
54
+
55
+ # 2. Streamlit secrets에서 API확인 (try-except로 오류 방지)
56
+ if not st.session_state.openai_api_key:
57
+ try:
58
+ if 'OPENAI_API_KEY' in st.secrets:
59
+ st.session_state.openai_api_key = st.secrets['OPENAI_API_KEY']
60
+ openai.api_key = st.session_state.openai_api_key
61
+ except Exception as e:
62
+ pass # secrets 파일이 없어도 오류 발생하지 않음
63
+
64
+ # NLTK 데이터 경로 설정 - 현재 워크스페이스의 nltk_data 사용
65
+ nltk_data_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'nltk_data')
66
+ nltk.data.path.insert(0, nltk_data_path)
67
+
68
+ # 필요한 NLTK 데이터 확인
69
+ try:
70
+ nltk.data.find('tokenizers/punkt')
71
+ except LookupError:
72
+ nltk.download('punkt', download_dir=nltk_data_path)
73
+
74
+ try:
75
+ nltk.data.find('corpora/stopwords')
76
+ except LookupError:
77
+ nltk.download('stopwords', download_dir=nltk_data_path)
78
 
79
  # 페이지 설정
80
  st.set_page_config(page_title="뉴스 기사 도구", page_icon="📰", layout="wide")
81
 
82
+ # 사이드바에 API 키 입력 필드 추가
83
+ with st.sidebar:
84
+ st.title("뉴스 기사 도구")
85
+ menu = st.radio(
86
+ "메뉴 선택",
87
+ ["뉴스 기사 크롤링", "기사 분석하기", "새 기사 생성하기", "뉴스 기사 예약하기"]
88
+ )
89
+
90
+ st.divider()
91
+ api_key = st.text_input("OpenAI API 키 입력", type="password")
92
+ if api_key:
93
+ st.session_state.openai_api_key = api_key
94
+ openai.api_key = api_key
95
+ st.success("API 키가 설정되었습니다!")
 
 
 
96
 
97
  # 저장된 기사를 불러오는 함수
98
  def load_saved_articles():
99
+ if os.path.exists('/tmp/saved_articles/articles.json'):
100
+ with open('/tmp/saved_articles/articles.json', 'r', encoding='utf-8') as f:
101
+ return json.load(f)
 
 
 
 
 
102
  return []
103
 
104
  # 기사를 저장하는 함수
105
  def save_articles(articles):
106
+ os.makedirs('/tmp/saved_articles', exist_ok=True)
107
+ with open('/tmp/saved_articles/articles.json', 'w', encoding='utf-8') as f:
108
+ json.dump(articles, f, ensure_ascii=False, indent=2)
 
 
 
 
 
 
 
109
 
110
  @st.cache_data
111
  def crawl_naver_news(keyword, num_articles=5):
 
192
  except Exception as e:
193
  return f"오류 발생: {str(e)}"
194
 
195
+ # NLTK를 이용한 키워드 분석 (KSS 활용)
196
  def analyze_keywords(text, top_n=10):
197
+ # 한국어 불용어 목록
198
  korean_stopwords = ['이', '그', '저', '것', '및', '등', '를', '을', '에', '에서', '의', '으로', '로']
199
 
200
+ # KSS를 사용한 문장 분리 및 토큰화
201
+ try:
202
+ sentences = kss.split_sentences(text)
203
+ tokens = []
204
+ for sentence in sentences:
205
+ # 간단한 토큰화 (공백 기준)
206
+ tokens.extend(sentence.split())
207
+ except:
208
+ # KSS 실패시 기본 토큰화
209
+ tokens = text.split()
210
+
211
  tokens = [word for word in tokens if word.isalnum() and len(word) > 1 and word not in korean_stopwords]
212
 
213
  word_count = Counter(tokens)
 
269
 
270
 
271
  # 워드 클라우드 생성 함수
 
272
  def generate_wordcloud(keywords_dict):
273
  if not WordCloud:
274
  st.warning("워드클라우드 설치안되어 있습니다.")
275
  return None
276
  try:
277
+ wc= WordCloud(
278
+ width=800,
279
+ height=400,
280
+ background_color = 'white',
281
+ colormap = 'viridis',
282
+ max_font_size=150,
283
+ random_state=42
284
+ ).generate_from_frequencies(keywords_dict)
285
+
286
+ try:
287
+ possible_font_paths=["NanumGothic.ttf", "이름"]
288
+
289
+ font_path = None
290
+ for path in possible_font_paths:
291
+ if os.path.exists(path):
292
+ font_path = path
293
+ break
294
+
295
+ if font_path:
296
+ wc= WordCloud(
297
+ font_path=font_path,
298
+ width=800,
299
+ height=400,
300
+ background_color = 'white',
301
+ colormap = 'viridis',
302
+ max_font_size=150,
303
+ random_state=42
304
+ ).generate_from_frequencies(keywords_dict)
305
+ except Exception as e:
306
+ print(f"오류발생 {str(e)}")
307
 
308
  return wc
309
 
310
  except Exception as e:
311
+ st.error(f"오류발생 {str(e)}")
 
 
312
  return None
313
 
314
  # 뉴스 분석 함수
 
335
  results['top_keywords'] = []
336
  return results
337
 
338
+ # OpenAI API를 이용한 새 기사 생성 (구 버전 방식)
339
  def generate_article(original_content, prompt_text):
 
 
 
340
  try:
341
+ if not st.session_state.openai_api_key:
342
+ return "OpenAI API 키가 설정되지 않았습니다."
343
+
344
+ response = openai.ChatCompletion.create(
345
  model="gpt-4.1-mini",
346
  messages=[
347
  {"role": "system", "content": "당신은 전문적인 뉴스 기자입니다. 주어진 내용을 바탕으로 새로운 기사를 작성해주세요."},
 
349
  ],
350
  max_tokens=2000
351
  )
352
+ return response.choices[0].message['content']
353
  except Exception as e:
354
  return f"기사 생성 오류: {str(e)}"
355
 
356
+ # OpenAI API를 이용한 이미지 생성 (구 버전 방식)
357
  def generate_image(prompt):
 
 
 
358
  try:
359
+ if not st.session_state.openai_api_key:
360
+ return "OpenAI API 키가 설정되지 않았습니다."
361
+
362
+ response = openai.Image.create(
363
+ prompt=prompt,
364
+ n=1,
365
+ size="1024x1024"
366
  )
367
+ return response['data'][0]['url']
 
368
  except Exception as e:
369
  return f"이미지 생성 오류: {str(e)}"
370
 
 
395
  time.sleep(0.5) # 서버 부하 방지
396
 
397
  # 결과 저장
398
+ os.makedirs('/tmp/scheduled_news', exist_ok=True)
 
 
 
399
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
400
+ filename = f"/tmp/scheduled_news/{file_prefix}_{task_type}_{timestamp}.json"
401
 
402
+ with open(filename, 'w', encoding='utf-8') as f:
403
+ json.dump(articles, f, ensure_ascii=False, indent=2)
 
 
 
 
 
 
404
 
405
  global_scheduler_state.last_run = datetime.now()
406
  print(f"{datetime.now()} - {task_type} 뉴스 기사 수집 완료: {keyword}")
 
536
  st.write(f"**요약:** {article['description']}")
537
  st.write(f"**링크:** {article['link']}")
538
  st.write("**본문 미리보기:**")
539
+ st.write(article['content'][:300] + "..." if len(article['content']) > 300 else article['content'])
540
 
541
  elif menu == "기사 분석하기":
542
  st.header("기사 분석하기")
 
571
  keyword_tab1, keyword_tab2 = st.tabs(["키워드 빈도", "워드클라우드"])
572
 
573
  with keyword_tab1:
 
574
  keywords = analyze_keywords(selected_article['content'])
575
 
576
  # 시각화
 
605
  # 텍스트 통계 계산
606
  word_count = len(re.findall(r'\b\w+\b', content))
607
  char_count = len(content)
608
+ try:
609
+ # KSS로 문장 분리
610
+ sentences = kss.split_sentences(content)
611
+ sentence_count = len(sentences)
612
+ except:
613
+ # KSS 실패시 기본 문장 분리
614
+ sentence_count = len(re.split(r'[.!?]+', content))
615
+
616
  avg_word_length = sum(len(word) for word in re.findall(r'\b\w+\b', content)) / word_count if word_count > 0 else 0
617
  avg_sentence_length = word_count / sentence_count if sentence_count > 0 else 0
618
 
 
632
  with col2:
633
  st.metric("평균 문장 길이", f"{avg_sentence_length:.1f}단어")
634
 
635
+ # 텍스트 복잡성 점수
636
  complexity_score = min(10, (avg_sentence_length / 10) * 5 + (avg_word_length / 5) * 5)
637
  st.progress(complexity_score / 10)
638
  st.write(f"텍스트 복잡성 점수: {complexity_score:.1f}/10")
639
+
640
+ # 품사 분석 부분 제거 (KoNLPy 의존성 제거)
641
+ st.info("상세 품사 분석은 현재 지원되지 않습니다.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
 
643
  elif analysis_type == "감정 분석":
644
  if st.button("감정 분석하기"):
645
  if st.session_state.openai_api_key:
646
  with st.spinner("기사의 감정을 분석 중입니다..."):
647
  try:
648
+ # 감정 분석 프롬프트 설정 (구 버전 방식)
649
+ response = openai.ChatCompletion.create(
 
 
 
 
 
 
 
650
  model="gpt-4.1-mini",
651
  messages=[
652
  {"role": "system", "content": "당신은 텍스트의 감정과 논조를 분석하는 전문가입니다. 다음 뉴스 기사의 감정과 논조를 분석하고, '긍정적', '부정적', '중립적' 중 하나로 분류해 주세요. 또한 기사에서 드러나는 핵심 감정 키워드를 5개 추출하고, 각 키워드별로 1-10 사이의 강도 점수를 매겨주세요. JSON 형식으로 다음과 같이 응답해주세요: {'sentiment': '긍정적/부정적/중립적', 'reason': '이유 설명...', 'keywords': [{'word': '키워드1', 'score': 8}, {'word': '키워드2', 'score': 7}, ...]}"},
653
  {"role": "user", "content": f"다음 뉴스 기사를 분석해 주세요:\n\n제목: {selected_article['title']}\n\n내용: {selected_article['content'][:1500]}"}
654
  ],
655
+ max_tokens=800
 
656
  )
657
 
658
+ # JSON 파싱 (구 버전 방식)
659
+ analysis_result = json.loads(response.choices[0].message['content'])
660
 
661
  # 결과 시각화
662
  st.subheader("감정 분석 결과")
 
843
 
844
  if st.button("새 기사 생성하기"):
845
  if st.session_state.openai_api_key:
 
846
  with st.spinner("기사를 생성 중입니다..."):
847
  new_article = generate_article(selected_article['content'], prompt_text)
848
 
 
861
  """
862
 
863
  # 이미지 생성
 
 
 
 
 
 
 
864
  image_url = generate_image(image_prompt)
865
 
866
+ if image_url and not image_url.startswith("이미지 생성 오류"):
867
  st.subheader("생성된 이미지:")
868
  st.image(image_url)
869
  else:
 
885
  else:
886
  st.warning("OpenAI API 키를 사이드바에서 설정해주세요.")
887
 
888
+
889
+
890
  elif menu == "뉴스 기사 예약하기":
891
  st.header("뉴스 기사 예약하기")
892
 
 
1034
  )
1035
 
1036
  # 수집된 파일 보기
1037
+ if os.path.exists('/tmp/scheduled_news'):
1038
+ files = [f for f in os.listdir('/tmp/scheduled_news') if f.endswith('.json')]
1039
  if files:
1040
  st.subheader("수집된 파일 열기")
1041
+ selected_file = st.selectbox("파일 선택", files, index=len(files)-1)
1042
  if selected_file and st.button("파일 내용 보기"):
1043
+ with open(os.path.join('/tmp/scheduled_news', selected_file), 'r', encoding='utf-8') as f:
1044
  articles = json.load(f)
1045
 
1046
  st.write(f"**파일명:** {selected_file}")
 
1056
 
1057
  # 푸터
1058
  st.markdown("---")
1059
+ st.markdown("© 뉴스 기사 도구 @conanssam")