jeongsoo's picture
fix
fa4a66c
import streamlit as st
import json
import os
import uuid
import glob
from datetime import datetime
import numpy as np
import platform
import networkx as nx
import plotly.graph_objects as go
from sklearn.metrics.pairwise import cosine_similarity
import plotly
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
from sklearn.manifold import TSNE
import warnings
warnings.filterwarnings('ignore')
# --- (이전 μ½”λ“œλŠ” 동일) ---
# νŽ˜μ΄μ§€ μ„€μ •
st.set_page_config(
page_title="ν•œκ΅­μ–΄ 단어 의미 λ„€νŠΈμ›Œν¬ μ‹œκ°ν™”",
page_icon="πŸ”€",
layout="wide"
)
# 폴더 경둜 μ„€μ •
DATA_FOLDER = 'data'
UPLOAD_FOLDER = 'uploads'
# 폴더 생성
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
# μ„Έμ…˜ μƒνƒœ μ΄ˆκΈ°ν™”
if 'model' not in st.session_state:
st.session_state.model = None
if 'embeddings_cache' not in st.session_state:
st.session_state.embeddings_cache = {}
if 'graph_cache' not in st.session_state:
st.session_state.graph_cache = {}
if 'data_files' not in st.session_state:
st.session_state.data_files = {}
if 'selected_files' not in st.session_state:
st.session_state.selected_files = [] # 리슀트둜 μ΄ˆκΈ°ν™”
if 'threshold' not in st.session_state:
st.session_state.threshold = 0.7
if 'generate_clicked' not in st.session_state:
st.session_state.generate_clicked = False
if 'fig' not in st.session_state:
st.session_state.fig = None
# --- (ν•¨μˆ˜ μ •μ˜ 뢀뢄은 동일: set_korean_font, load_words_from_json, ...) ---
# --- ν•œκΈ€ 폰트 μ„€μ • ν•¨μˆ˜ ---
def set_korean_font():
"""
ν˜„μž¬ μš΄μ˜μ²΄μ œμ— λ§žλŠ” ν•œκΈ€ 폰트λ₯Ό matplotlib 및 Plotly용으둜 μ„€μ • μ‹œλ„ν•˜κ³ ,
Plotlyμ—μ„œ μ‚¬μš©ν•  폰트 이름을 λ°˜ν™˜ν•©λ‹ˆλ‹€.
"""
system_name = platform.system()
plotly_font_name = None # Plotlyμ—μ„œ μ‚¬μš©ν•  폰트 이름
# Matplotlib 폰트 μ„€μ •
if system_name == "Windows":
font_name = "Malgun Gothic"
plotly_font_name = "Malgun Gothic"
elif system_name == "Darwin": # MacOS
font_name = "AppleGothic"
plotly_font_name = "AppleGothic"
elif system_name == "Linux":
# Linuxμ—μ„œ μ„ ν˜Έν•˜λŠ” ν•œκΈ€ 폰트 경둜 λ˜λŠ” 이름 μ„€μ •
font_path = "/usr/share/fonts/truetype/nanum/NanumGothic.ttf"
plotly_font_name_linux = "NanumGothic" # PlotlyλŠ” 폰트 '이름'을 주둜 μ‚¬μš©
if os.path.exists(font_path):
prop = fm.FontProperties(fname=font_path)
fm.fontManager.addfont(font_path) # μ‹œμŠ€ν…œμ— 폰트 μΆ”κ°€ (ν•„μš”ν•  수 있음)
font_name = prop.get_name()
plotly_font_name = plotly_font_name_linux
else:
# μ‹œμŠ€ν…œμ—μ„œ 'Nanum' 포함 폰트 μ°ΎκΈ° μ‹œλ„
try:
available_fonts = [f.name for f in fm.fontManager.ttflist]
nanum_fonts = [name for name in available_fonts if 'Nanum' in name]
if nanum_fonts:
font_name = nanum_fonts[0]
# Plotlyμ—μ„œ μ‚¬μš©ν•  이름도 λΉ„μŠ·ν•˜κ²Œ μ„€μ • (μ •ν™•ν•œ 이름은 μ‹œμŠ€ν…œλ§ˆλ‹€ λ‹€λ₯Ό 수 있음)
plotly_font_name = font_name if 'Nanum' in font_name else plotly_font_name_linux
else:
# λ‹€λ₯Έ OS 폰트 μ‹œλ„ (Linuxμ—μ„œ λ“œλ¬Όμ§€λ§Œ)
if "Malgun Gothic" in available_fonts:
font_name = "Malgun Gothic"
plotly_font_name = "Malgun Gothic"
elif "AppleGothic" in available_fonts:
font_name = "AppleGothic"
plotly_font_name = "AppleGothic"
else:
font_name = None
except Exception as e:
print(f"Linux font search error: {e}")
font_name = None
if not font_name:
font_name = None
plotly_font_name = None # Plotly도 κΈ°λ³Έκ°’ μ‚¬μš©
else: # 기타 OS
font_name = None
plotly_font_name = None
# Matplotlib 폰트 μ„€μ • 적용
if font_name:
try:
plt.rc('font', family=font_name)
plt.rc('axes', unicode_minus=False)
print(f"Matplotlib font set to: {font_name}")
except Exception as e:
print(f"Failed to set Matplotlib font '{font_name}': {e}")
plt.rcdefaults()
plt.rc('axes', unicode_minus=False)
else:
print("No suitable Korean font found for Matplotlib. Using default.")
plt.rcdefaults()
plt.rc('axes', unicode_minus=False)
if not plotly_font_name:
plotly_font_name = 'sans-serif' # Plotly κΈ°λ³Έκ°’ μ§€μ •
print(f"Plotly font name to use: {plotly_font_name}")
return plotly_font_name # Plotlyμ—μ„œ μ‚¬μš©ν•  폰트 이름 λ°˜ν™˜
# --- 데이터 λ‘œλ“œ ν•¨μˆ˜ ---
def load_words_from_json(filepath):
""" JSON νŒŒμΌμ—μ„œ 'word' ν•„λ“œλ§Œ 리슀트둜 λ‘œλ“œν•©λ‹ˆλ‹€. """
try:
with open(filepath, 'r', encoding='utf-8') as f:
data = json.load(f)
# dataκ°€ 리슀트 ν˜•νƒœλΌκ³  κ°€μ •
if isinstance(data, list):
words = [item.get('word', '') for item in data if isinstance(item, dict) and item.get('word')] # dict ν˜•νƒœμ΄κ³  'word' ν‚€κ°€ μžˆλŠ”μ§€ 확인
# 빈 λ¬Έμžμ—΄ 제거
words = [word for word in words if word]
if not words:
st.warning(f"κ²½κ³ : 파일 '{os.path.basename(filepath)}'μ—μ„œ 'word' ν‚€λ₯Ό κ°€μ§„ μœ νš¨ν•œ 데이터λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")
return None
return words
else:
st.error(f"였λ₯˜: 파일 '{os.path.basename(filepath)}'의 μ΅œμƒμœ„ ν˜•μ‹μ΄ λ¦¬μŠ€νŠΈκ°€ μ•„λ‹™λ‹ˆλ‹€.")
return None
except FileNotFoundError:
st.error(f"였λ₯˜: 파일 '{filepath}'λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")
return None
except json.JSONDecodeError as e:
st.error(f"였λ₯˜: 파일 '{os.path.basename(filepath)}'의 JSON ν˜•μ‹μ΄ 잘λͺ»λ˜μ—ˆμŠ΅λ‹ˆλ‹€. 였λ₯˜: {e}")
return None
except Exception as e:
st.error(f"'{os.path.basename(filepath)}' 데이터 λ‘œλ”© 쀑 였λ₯˜ λ°œμƒ: {e}")
return None
def scan_data_files():
"""데이터 ν΄λ”μ—μ„œ μ‚¬μš© κ°€λŠ₯ν•œ λͺ¨λ“  JSON νŒŒμΌμ„ μŠ€μΊ”ν•˜κ³  정보λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€."""
data_files = {}
# κΈ°λ³Έ 데이터 폴더 μŠ€μΊ”
try:
for file_path in glob.glob(os.path.join(DATA_FOLDER, '*.json')):
file_id = f"default_{os.path.basename(file_path)}" # 고유 ID 생성 방식 λ³€κ²½
file_name = os.path.basename(file_path)
words = load_words_from_json(file_path)
if words: # wordsκ°€ None이 μ•„λ‹ˆκ³  λΉ„μ–΄μžˆμ§€ μ•Šμ€ 경우
data_files[file_id] = {
'path': file_path,
'name': file_name,
'word_count': len(words),
'type': 'default',
'sample_words': words[:5] # μƒ˜ν”Œ 단어 수 μ‘°μ • κ°€λŠ₯
}
except Exception as e:
st.error(f"κΈ°λ³Έ 데이터 폴더 μŠ€μΊ” 쀑 였λ₯˜: {e}")
# μ—…λ‘œλ“œ 폴더 μŠ€μΊ”
try:
for file_path in glob.glob(os.path.join(UPLOAD_FOLDER, '*.json')):
file_id = f"uploaded_{os.path.basename(file_path)}" # 고유 ID 생성 방식 λ³€κ²½
file_name = os.path.basename(file_path)
words = load_words_from_json(file_path)
if words: # wordsκ°€ None이 μ•„λ‹ˆκ³  λΉ„μ–΄μžˆμ§€ μ•Šμ€ 경우
data_files[file_id] = {
'path': file_path,
'name': file_name,
'word_count': len(words),
'type': 'uploaded',
'sample_words': words[:5] # μƒ˜ν”Œ 단어 수 μ‘°μ • κ°€λŠ₯
}
except Exception as e:
st.error(f"μ—…λ‘œλ“œ 폴더 μŠ€μΊ” 쀑 였λ₯˜: {e}")
return data_files
def merge_word_lists(file_ids):
"""μ„ νƒλœ νŒŒμΌλ“€μ—μ„œ 단어λ₯Ό λ‘œλ“œν•˜κ³  쀑볡 μ œκ±°ν•˜μ—¬ λ³‘ν•©ν•©λ‹ˆλ‹€."""
all_words = []
if not file_ids:
return []
# data_files μƒνƒœκ°€ μ΅œμ‹ μΈμ§€ 확인 (μ—…λ‘œλ“œ/μ‚­μ œ ν›„ ν•„μš”ν•  수 있음)
current_data_files = st.session_state.get('data_files', {})
for file_id in file_ids:
if file_id in current_data_files:
file_path = current_data_files[file_id]['path']
words = load_words_from_json(file_path)
if words:
all_words.extend(words)
else:
st.warning(f"μ„ νƒλœ 파일 ID '{file_id}'λ₯Ό ν˜„μž¬ 파일 λͺ©λ‘μ—μ„œ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. λͺ©λ‘μ„ μƒˆλ‘œκ³ μΉ¨ν•©λ‹ˆλ‹€.")
# 파일 λͺ©λ‘μ„ λ‹€μ‹œ μŠ€μΊ”ν•˜κ³  μž¬μ‹œλ„ (선택적)
st.session_state.data_files = scan_data_files()
if file_id in st.session_state.data_files:
words = load_words_from_json(st.session_state.data_files[file_id]['path'])
if words: all_words.extend(words)
else:
st.error(f"파일 '{file_id}'λ₯Ό μ—¬μ „νžˆ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")
# 쀑볡 제거 및 μ •λ ¬
unique_words = sorted(list(set(all_words)))
return unique_words
def encode_words(words, normalize=True):
"""단어 λͺ©λ‘μ„ μž„λ² λ”©μœΌλ‘œ λ³€ν™˜ν•©λ‹ˆλ‹€. (κ°œμ„ λœ TF-IDF μŠ€νƒ€μΌ μž„λ² λ”©)"""
if not words:
return np.array([])
embeddings = []
# 전체 단어에 λ‚˜νƒ€λ‚˜λŠ” λͺ¨λ“  고유 문자둜 μ–΄νœ˜ ꡬ성
unique_chars = set(char for word in words for char in word)
char_to_idx = {char: i for i, char in enumerate(sorted(list(unique_chars)))}
dim = len(char_to_idx)
if dim == 0: # 단어가 μ•„μ˜ˆ μ—†λŠ” 경우
return np.array([])
for word in words:
embed = np.zeros(dim)
word_len = len(word)
if word_len == 0: # 빈 λ¬Έμžμ—΄ 처리
embeddings.append(embed)
continue
# TF (Term Frequency): 단어 λ‚΄ 문자 λΉˆλ„
tf = {}
for char in word:
if char in char_to_idx:
tf[char] = tf.get(char, 0) + 1
for char, count in tf.items():
if char in char_to_idx:
# TF 계산 (μ—¬κΈ°μ„œλŠ” λ‹¨μˆœ λΉˆλ„ μ‚¬μš©, ν•„μš”μ‹œ log μŠ€μΌ€μΌλ§ λ“± 적용 κ°€λŠ₯)
embed[char_to_idx[char]] = count / word_len # 단어 길이둜 μ •κ·œν™”
# L2 μ •κ·œν™” (Cosine Similarityλ₯Ό μœ„ν•΄ 유용)
if normalize:
norm = np.linalg.norm(embed)
if norm > 0:
embed = embed / norm
embeddings.append(embed)
return np.array(embeddings)
def generate_graph(file_ids, similarity_threshold=0.7):
"""μ—¬λŸ¬ νŒŒμΌμ—μ„œ 단어λ₯Ό λ‘œλ“œν•˜κ³  κ·Έλž˜ν”„λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€."""
if not file_ids:
st.error("κ·Έλž˜ν”„λ₯Ό 생성할 파일이 μ„ νƒλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
return None
# μΊμ‹œ ν‚€ 생성 (파일 ID λ¦¬μŠ€νŠΈμ™€ μž„κ³„κ°’ μ‘°ν•©, μˆœμ„œ 보μž₯)
cache_key = f"{'-'.join(sorted(file_ids))}_{similarity_threshold}"
if cache_key in st.session_state.graph_cache:
# μΊμ‹œλœ κ²°κ³Ό λ°˜ν™˜
return st.session_state.graph_cache[cache_key]
# ν•œκΈ€ 폰트 μ„€μ •
plotly_font = set_korean_font()
# μ„ νƒλœ νŒŒμΌλ“€μ—μ„œ 단어 λ‘œλ“œ 및 병합
word_list = merge_word_lists(file_ids)
if not word_list:
st.error("μ„ νƒλœ νŒŒμΌμ—μ„œ μœ νš¨ν•œ 단어λ₯Ό λ‘œλ“œν•  수 μ—†μŠ΅λ‹ˆλ‹€.")
return None
if len(word_list) < 2:
st.warning("κ·Έλž˜ν”„λ₯Ό μƒμ„±ν•˜λ €λ©΄ μ΅œμ†Œ 2개 μ΄μƒμ˜ 고유 단어가 ν•„μš”ν•©λ‹ˆλ‹€.")
return None
# μž„λ² λ”© 생성
embeddings = None
with st.spinner('단어 μž„λ² λ”© 생성 쀑...'):
# μΊμ‹œ 확인 (파일 ID 기반)
embedding_cache_key = '-'.join(sorted(file_ids))
if embedding_cache_key in st.session_state.embeddings_cache:
word_list_cached, embeddings = st.session_state.embeddings_cache[embedding_cache_key]
# μΊμ‹œλœ 단어 λͺ©λ‘κ³Ό ν˜„μž¬ 단어 λͺ©λ‘μ΄ λ‹€λ₯΄λ©΄ μž¬μƒμ„±
if sorted(word_list_cached) != sorted(word_list):
embeddings = encode_words(word_list, normalize=True)
st.session_state.embeddings_cache[embedding_cache_key] = (word_list, embeddings)
else:
embeddings = encode_words(word_list, normalize=True)
st.session_state.embeddings_cache[embedding_cache_key] = (word_list, embeddings)
if embeddings is None or embeddings.shape[0] == 0 or embeddings.shape[1] == 0:
st.error("단어 μž„λ² λ”© 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.")
return None
# 3D μ’Œν‘œ 생성 - t-SNE μ‚¬μš©
embeddings_3d = None
with st.spinner('단어 μ’Œν‘œ 계산 쀑 (t-SNE)...'):
# t-SNE νŒŒλΌλ―Έν„° μ„€μ • (데이터 크기에 따라 동적 쑰절)
n_samples = embeddings.shape[0]
# perplexityλŠ” n_samples - 1 보닀 μž‘μ•„μ•Ό 함
effective_perplexity = min(30, max(5, n_samples - 1)) # μ΅œμ†Œ 5, μ΅œλŒ€ 30 λ˜λŠ” μƒ˜ν”Œμˆ˜-1
# 반볡 횟수
max_iter = max(250, min(1000, n_samples * 5)) # μƒ˜ν”Œ μˆ˜μ— 따라 μ‘°μ ˆν•˜λ˜ μ΅œμ†Œ/μ΅œλŒ€κ°’ μ„€μ •
# ν•™μŠ΅λ₯ 
learning_rate = max(10, min(200, n_samples / 12)) if n_samples > 12 else 'auto' # μƒ˜ν”Œ 수 기반, λ„ˆλ¬΄ μž‘μœΌλ©΄ auto
if n_samples <= 3: # t-SNEλŠ” μ΅œμ†Œ 4개 μƒ˜ν”Œ ꢌμž₯
st.warning(f"t-SNEλŠ” μ΅œμ†Œ 4개의 단어가 ν•„μš”ν•©λ‹ˆλ‹€ (ν˜„μž¬ {n_samples}개). PCAλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.")
from sklearn.decomposition import PCA
pca = PCA(n_components=min(3, n_samples), random_state=42) # μ΅œλŒ€ 3차원 λ˜λŠ” μƒ˜ν”Œ 수
embeddings_3d_pca = pca.fit_transform(embeddings)
# 3μ°¨μ›μœΌλ‘œ λ§žμΆ”κΈ° (λΆ€μ‘±ν•˜λ©΄ 0으둜 채움)
embeddings_3d = np.zeros((n_samples, 3))
embeddings_3d[:, :embeddings_3d_pca.shape[1]] = embeddings_3d_pca
else:
try:
# max_iter λ³€μˆ˜ 동적 계산 및 ν• λ‹Ή
max_iter = max(250, min(1000, n_samples * 5)) # <--- 이 쀄을 μ‹€μ œ μ½”λ“œλ‘œ μΆ”κ°€/ν™œμ„±ν™”
tsne = TSNE(n_components=3, random_state=42,
perplexity=effective_perplexity,
n_iter=max_iter, # 이제 μ •μ˜λœ max_iter μ‚¬μš©
init='pca',
learning_rate=learning_rate,
n_jobs=-1)
embeddings_3d = tsne.fit_transform(embeddings)
except Exception as e:
st.error(f"t-SNE μ‹€ν–‰ 쀑 였λ₯˜ λ°œμƒ: {e}. PCA둜 λŒ€μ²΄ν•©λ‹ˆλ‹€.")
from sklearn.decomposition import PCA
pca = PCA(n_components=3, random_state=42)
embeddings_3d = pca.fit_transform(embeddings)
if embeddings_3d is None:
st.error("단어 μ’Œν‘œ 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.")
return None
# μœ μ‚¬λ„ 계산 및 μ—£μ§€ μ •μ˜
edges = []
edge_weights = []
with st.spinner('단어 κ°„ μœ μ‚¬λ„ 계산 및 μ—°κ²°(μ—£μ§€) 생성 쀑...'):
# μœ μ‚¬λ„ ν–‰λ ¬ 계산
similarity_matrix = cosine_similarity(embeddings)
# μž„κ³„κ°’ 이상인 μ—£μ§€λ§Œ μΆ”κ°€
for i in range(n_samples):
for j in range(i + 1, n_samples): # 쀑볡 및 자기 μžμ‹  μ—°κ²° λ°©μ§€
similarity = similarity_matrix[i, j]
if similarity >= similarity_threshold: # λ“±ν˜Έ 포함 (μž„κ³„κ°’κ³Ό 같아도 μ—°κ²°)
edges.append((word_list[i], word_list[j]))
edge_weights.append(similarity)
# NetworkX κ·Έλž˜ν”„ 생성
G = nx.Graph()
# λ…Έλ“œ μΆ”κ°€ (단어와 3D μ’Œν‘œ)
for i, word in enumerate(word_list):
G.add_node(word, pos=(embeddings_3d[i, 0], embeddings_3d[i, 1], embeddings_3d[i, 2]))
# 엣지와 κ°€μ€‘μΉ˜ μΆ”κ°€
for edge, weight in zip(edges, edge_weights):
# self-loop λ°©μ§€ (이둠상 μœ„ λ‘œμ§μ—μ„œ λ°œμƒ μ•ˆ 함)
if edge[0] != edge[1]:
G.add_edge(edge[0], edge[1], weight=weight)
# Plotly κ·Έλž˜ν”„ 생성
edge_x, edge_y, edge_z = [], [], []
if G.number_of_edges() > 0:
for edge in G.edges():
try:
pos0 = G.nodes[edge[0]]['pos']
pos1 = G.nodes[edge[1]]['pos']
edge_x.extend([pos0[0], pos1[0], None]) # None은 μ„  끊기
edge_y.extend([pos0[1], pos1[1], None])
edge_z.extend([pos0[2], pos1[2], None])
except KeyError as e:
st.warning(f"μ—£μ§€ 생성 쀑 λ…Έλ“œ ν‚€ 였λ₯˜: {e}. ν•΄λ‹Ή μ—£μ§€λ₯Ό κ±΄λ„ˆ<0xEB><0x84>λ‹ˆλ‹€.")
continue # λ¬Έμ œκ°€ μžˆλŠ” μ—£μ§€λŠ” κ±΄λ„ˆλœ€
# μ—£μ§€ 트레이슀
edge_trace = go.Scatter3d(
x=edge_x, y=edge_y, z=edge_z,
mode='lines',
line=dict(width=1, color='#888'),
hoverinfo='none' # μ—£μ§€μ—λŠ” ν˜Έλ²„ 정보 μ—†μŒ
)
# λ…Έλ“œ μ’Œν‘œ 및 ν…μŠ€νŠΈ 정보
node_x, node_y, node_z, node_text = [], [], [], []
node_adjacencies = [] # μ—°κ²° 수 (degree)
node_hover_text = [] # ν˜Έλ²„ ν…μŠ€νŠΈ
nodes_data = []
for node in G.nodes():
try:
pos = G.nodes[node]['pos']
degree = G.degree(node) # λ…Έλ“œμ˜ μ—°κ²° 수 계산
nodes_data.append({
'x': pos[0], 'y': pos[1], 'z': pos[2],
'text': node,
'degree': degree,
'hover_text': f'{node}<br>μ—°κ²° 수: {degree}'
})
except KeyError:
st.warning(f"λ…Έλ“œ '{node}' 처리 쀑 'pos' ν‚€ 였λ₯˜. ν•΄λ‹Ή λ…Έλ“œλ₯Ό κ±΄λ„ˆ<0xEB><0x84>λ‹ˆλ‹€.")
continue # μœ„μΉ˜ 정보 μ—†λŠ” λ…Έλ“œ κ±΄λ„ˆλœ€
# λ…Έλ“œ 데이터가 μžˆμ„ κ²½μš°μ—λ§Œ 처리
if nodes_data:
# λ…Έλ“œ 크기λ₯Ό μ—°κ²° μˆ˜μ— 따라 쑰절 (μ˜ˆμ‹œ: 둜그 μŠ€μΌ€μΌλ§)
degrees = np.array([data['degree'] for data in nodes_data])
# 둜그 μŠ€μΌ€μΌλ§ 적용 (0인 경우 λŒ€λΉ„ +1), μ΅œλŒ€/μ΅œμ†Œ 크기 μ œν•œ
node_sizes = np.log1p(degrees) * 3 + 6 # κΈ°λ³Έ 크기 6, μ—°κ²° λ§Žμ„μˆ˜λ‘ 컀짐
node_sizes = np.clip(node_sizes, 5, 20) # μ΅œμ†Œ 5, μ΅œλŒ€ 20
# λ…Έλ“œ 데이터 뢄리
node_x = [data['x'] for data in nodes_data]
node_y = [data['y'] for data in nodes_data]
node_z = [data['z'] for data in nodes_data]
node_text = [data['text'] for data in nodes_data]
node_hover_text = [data['hover_text'] for data in nodes_data]
# λ…Έλ“œ 트레이슀
node_trace = go.Scatter3d(
x=node_x, y=node_y, z=node_z,
mode='markers+text', # λ§ˆμ»€μ™€ ν…μŠ€νŠΈ ν•¨κ»˜ ν‘œμ‹œ
text=node_text, # λ…Έλ“œ μœ„μ— ν‘œμ‹œλ  ν…μŠ€νŠΈ
hovertext=node_hover_text, # 마우슀 μ˜¬λ Έμ„ λ•Œ ν‘œμ‹œλ  ν…μŠ€νŠΈ
hoverinfo='text', # ν˜Έλ²„ μ‹œ hovertext만 ν‘œμ‹œ
textposition='top center', # ν…μŠ€νŠΈ μœ„μΉ˜
textfont=dict(
size=10,
color='black',
family=plotly_font # μ„€μ •λœ ν•œκΈ€ 폰트 μ‚¬μš©
),
marker=dict(
size=node_sizes, # μ—°κ²° μˆ˜μ— 따라 크기 쑰절된 리슀트
color=node_z, # ZμΆ• κ°’μœΌλ‘œ 색상 λ§€ν•‘
colorscale='Viridis', # 색상 μŠ€μΌ€μΌ
opacity=0.9,
colorbar=dict(thickness=15, title='Node Depth (Z)', xanchor='left', titleside='right')
)
)
else:
# λ…Έλ“œ 데이터가 μ—†μœΌλ©΄ 빈 트레이슀 생성
node_trace = go.Scatter3d(x=[], y=[], z=[], mode='markers')
# μ‚¬μš©λœ 파일 이름 λͺ©λ‘ 생성
file_names_used = []
if 'data_files' in st.session_state:
file_names_used = [st.session_state.data_files[fid]['name'] for fid in file_ids if fid in st.session_state.data_files]
file_info_str = ", ".join(file_names_used) if file_names_used else "μ•Œ 수 μ—†μŒ"
# λ ˆμ΄μ•„μ›ƒ μ„€μ •
layout = go.Layout(
title=dict(
text=f'<b>μ–΄νœ˜ 의미 μœ μ‚¬μ„± 기반 3D κ·Έλž˜ν”„</b><br>Threshold: {similarity_threshold:.2f} | 데이터: {file_info_str}',
font=dict(size=16, family=plotly_font),
x=0.5, # 제λͺ© 쀑앙 μ •λ ¬
xanchor='center'
),
showlegend=False, # λ²”λ‘€ μˆ¨κΉ€
margin=dict(l=10, r=10, b=10, t=80), # μ—¬λ°± 쑰절 (제λͺ© 곡간 확보)
scene=dict(
xaxis=dict(
title='TSNE-1', showticklabels=False, # μΆ• 눈금 μˆ¨κΉ€
backgroundcolor="rgb(240, 240, 240)", gridcolor="white", zerolinecolor="white"
),
yaxis=dict(
title='TSNE-2', showticklabels=False,
backgroundcolor="rgb(240, 240, 240)", gridcolor="white", zerolinecolor="white"
),
zaxis=dict(
title='TSNE-3', showticklabels=False,
backgroundcolor="rgb(240, 240, 240)", gridcolor="white", zerolinecolor="white"
),
aspectratio=dict(x=1, y=1, z=0.8), # κ°€λ‘œμ„Έλ‘œλΉ„ 쑰절
camera=dict(
eye=dict(x=1.2, y=1.2, z=0.8) # 초기 카메라 μ‹œμ 
)
),
# ν˜Έλ²„ λͺ¨λ“œ μ„€μ • (κ°€μž₯ κ°€κΉŒμš΄ 데이터 포인트 λ˜λŠ” 톡합)
hovermode='closest'
)
# Figure 객체 생성
fig = go.Figure(data=[edge_trace, node_trace], layout=layout)
# κ²°κ³Ό μΊμ‹œ μ €μž₯
st.session_state.graph_cache[cache_key] = fig
return fig
def handle_uploaded_file(uploaded_file):
"""μ—…λ‘œλ“œλœ νŒŒμΌμ„ μ²˜λ¦¬ν•˜κ³  데이터 파일 λͺ©λ‘μ— μΆ”κ°€ν•©λ‹ˆλ‹€."""
if uploaded_file is not None:
# 파일λͺ… μ•ˆμ „ 처리 (uuid μ‚¬μš© ꢌμž₯) 및 μ €μž₯ 경둜
# original_name = uploaded_file.name
unique_id = str(uuid.uuid4()) # 고유 ID 생성
# file_extension = os.path.splitext(original_name)[1]
# file_name = f"{unique_id}{file_extension}" # 고유 ID둜 파일λͺ… 생성
file_name = f"{unique_id}_{uploaded_file.name}" # 원본 이름 일뢀 포함 (선택적)
file_path = os.path.join(UPLOAD_FOLDER, file_name)
try:
# 파일 μ €μž₯
with open(file_path, 'wb') as f:
f.write(uploaded_file.getbuffer())
st.info(f"파일 '{uploaded_file.name}' ({file_name}) μ €μž₯ μ™„λ£Œ. λ‚΄μš© 검증 쀑...")
# μ—…λ‘œλ“œλœ 파일 검증 (단어 λ‘œλ“œ μ‹œλ„)
words = load_words_from_json(file_path)
if words is None or not words : # λ‘œλ“œ μ‹€νŒ¨ λ˜λŠ” 빈 리슀트
try:
os.remove(file_path) # μœ νš¨ν•˜μ§€ μ•ŠμœΌλ©΄ 파일 μ‚­μ œ
st.error(f"μ—…λ‘œλ“œλœ 파일 '{uploaded_file.name}'μ—μ„œ μœ νš¨ν•œ 'word' 데이터λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. 파일 ν˜•μ‹(UTF-8 인코딩 JSON λ°°μ—΄, 각 객체에 'word' ν‚€)을 ν™•μΈν•΄μ£Όμ„Έμš”. 파일이 μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
except OSError as e:
st.error(f"μœ νš¨ν•˜μ§€ μ•Šμ€ νŒŒμΌμ„ μ‚­μ œν•˜λŠ” 쀑 였λ₯˜ λ°œμƒ: {e}")
return None # μ‹€νŒ¨ μ‹œ None λ°˜ν™˜
st.success(f"파일 '{uploaded_file.name}' 검증 μ™„λ£Œ. {len(words)}개의 단어λ₯Ό μ°Ύμ•˜μŠ΅λ‹ˆλ‹€.")
# 데이터 파일 λ‹€μ‹œ μŠ€μΊ”ν•˜μ—¬ μƒˆ 파일 정보 포함 (μ„Έμ…˜ μƒνƒœ μ—…λ°μ΄νŠΈ)
st.session_state.data_files = scan_data_files()
# μƒˆ νŒŒμΌμ— ν•΄λ‹Ήν•˜λŠ” file_id μ°ΎκΈ° (scan_data_filesμ—μ„œ μƒμ„±λœ ID μ‚¬μš©)
new_file_id = f"uploaded_{file_name}" # scan_data_files와 λ™μΌν•œ 둜직으둜 ID 생성
if new_file_id in st.session_state.data_files:
return new_file_id # 성곡 μ‹œ 파일 ID λ°˜ν™˜
else:
st.error("파일 λͺ©λ‘ μ—…λ°μ΄νŠΈ 후에도 μƒˆ 파일 IDλ₯Ό μ°Ύμ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.")
return None
except Exception as e:
st.error(f"파일 μ—…λ‘œλ“œ 및 처리 쀑 였λ₯˜ λ°œμƒ: {e}")
# 였λ₯˜ λ°œμƒ μ‹œ μ—…λ‘œλ“œλœ 파일 μ‚­μ œ μ‹œλ„
try:
if os.path.exists(file_path):
os.remove(file_path)
except OSError as del_e:
st.warning(f"였λ₯˜ λ°œμƒ ν›„ 파일 μ‚­μ œ μ‹€νŒ¨: {del_e}")
return None # μ‹€νŒ¨ μ‹œ None λ°˜ν™˜
def delete_file(file_id):
"""νŒŒμΌμ„ μ‚­μ œν•©λ‹ˆλ‹€."""
if file_id not in st.session_state.get('data_files', {}):
st.error('μ‚­μ œν•  νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.')
return False
file_info = st.session_state.data_files[file_id]
# μ—…λ‘œλ“œλœ 파일만 μ‚­μ œ ν—ˆμš©
if file_info.get('type') != 'uploaded':
st.error('κΈ°λ³Έ 데이터 νŒŒμΌμ€ μ‚­μ œν•  수 μ—†μŠ΅λ‹ˆλ‹€.')
return False
file_path = file_info.get('path')
file_name = file_info.get('name', 'μ•Œ 수 μ—†μŒ')
if not file_path:
st.error(f"파일 '{file_name}'의 경둜 정보가 μ—†μŠ΅λ‹ˆλ‹€.")
return False
try:
# 파일 μ‹œμŠ€ν…œμ—μ„œ 파일 μ‚­μ œ
if os.path.exists(file_path):
os.remove(file_path)
st.info(f"파일 μ‹œμŠ€ν…œμ—μ„œ '{file_name}' μ‚­μ œ μ™„λ£Œ.")
else:
st.warning(f"파일 μ‹œμŠ€ν…œμ— '{file_name}'({file_path})이(κ°€) 이미 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")
# μ„Έμ…˜ μƒνƒœμ—μ„œ 파일 정보 제거
del st.session_state.data_files[file_id]
# κ΄€λ ¨ μΊμ‹œ ν•­λͺ© μ‚­μ œ (κ·Έλž˜ν”„, μž„λ² λ”©)
keys_to_remove_graph = [k for k in st.session_state.graph_cache if file_id in k]
for key in keys_to_remove_graph:
del st.session_state.graph_cache[key]
keys_to_remove_embed = [k for k in st.session_state.embeddings_cache if file_id in k]
for key in keys_to_remove_embed:
del st.session_state.embeddings_cache[key]
# ν˜„μž¬ μ„ νƒλœ 파일 λͺ©λ‘μ—μ„œλ„ 제거
if file_id in st.session_state.selected_files:
st.session_state.selected_files.remove(file_id)
st.success(f"파일 '{file_name}' κ΄€λ ¨ 정보 및 μΊμ‹œκ°€ μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
return True
except Exception as e:
st.error(f"파일 μ‚­μ œ 쀑 였λ₯˜ λ°œμƒ: {e}")
return False
def clear_cache():
"""κ·Έλž˜ν”„ 및 μž„λ² λ”© μΊμ‹œλ₯Ό μ΄ˆκΈ°ν™”ν•©λ‹ˆλ‹€."""
st.session_state.graph_cache = {}
st.session_state.embeddings_cache = {}
st.session_state.fig = None # ν˜„μž¬ ν‘œμ‹œμ€‘μΈ κ·Έλž˜ν”„λ„ μ΄ˆκΈ°ν™”
st.success('κ·Έλž˜ν”„ 및 μž„λ² λ”© μΊμ‹œκ°€ μ΄ˆκΈ°ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€.')
# st.experimental_rerun() # μΊμ‹œ 클리어 ν›„ UI κ°±μ‹ 
# --- μ•± μ‹€ν–‰ μ‹œμž‘ ---
# 데이터 파일 μŠ€μΊ” (μ•± μ‹œμž‘ μ‹œ λ˜λŠ” ν•„μš” μ‹œ)
if 'data_files' not in st.session_state or not st.session_state.data_files:
st.session_state.data_files = scan_data_files()
# 타이틀 및 μ†Œκ°œ
st.title('ν•œκ΅­μ–΄ 단어 의미 λ„€νŠΈμ›Œν¬ μ‹œκ°ν™”')
st.markdown("""
이 λ„κ΅¬λŠ” 제곡된 JSON νŒŒμΌμ—μ„œ ν•œκ΅­μ–΄ 단어 λͺ©λ‘μ„ 읽어듀여, 단어 κ°„μ˜ 의미적 μœ μ‚¬μ„±(μ—¬κΈ°μ„œλŠ” 문자 ꡬ성 기반 μœ μ‚¬μ„±)을 κ³„μ‚°ν•˜κ³ ,
κ·Έ 관계λ₯Ό μΈν„°λž™ν‹°λΈŒν•œ 3D λ„€νŠΈμ›Œν¬ κ·Έλž˜ν”„λ‘œ μ‹œκ°ν™”ν•©λ‹ˆλ‹€.
""")
# --- μ‚¬μ΄λ“œλ°” μ„€μ • ---
st.sidebar.title('βš™οΈ μ„€μ • 및 μ œμ–΄')
# μž„κ³„κ°’ μ„€μ •
threshold = st.sidebar.slider(
'μœ μ‚¬λ„ μž„κ³„κ°’ (Similarity Threshold)',
min_value=0.1,
max_value=0.95, # μ΅œλŒ€κ°’ μ•½κ°„ 늘림
value=st.session_state.threshold,
step=0.05,
help='이 값보닀 μœ μ‚¬λ„κ°€ 높은 λ‹¨μ–΄λ“€λ§Œ μ—°κ²°μ„ (μ—£μ§€)으둜 μ΄μ–΄μ§‘λ‹ˆλ‹€. 값이 λ†’μ„μˆ˜λ‘ 연결이 더 μ—„κ²©ν•΄μ§‘λ‹ˆλ‹€.'
)
# μŠ¬λΌμ΄λ” 값이 λ³€κ²½λ˜λ©΄ μ„Έμ…˜ μƒνƒœ μ—…λ°μ΄νŠΈ (콜백 μ‚¬μš©μ΄ 더 효율적일 수 있음)
if threshold != st.session_state.threshold:
st.session_state.threshold = threshold
st.session_state.fig = None # μž„κ³„κ°’ λ³€κ²½ μ‹œ ν˜„μž¬ κ·Έλž˜ν”„ μ΄ˆκΈ°ν™” (μž¬μƒμ„± ν•„μš” μ•Œλ¦Ό)
st.session_state.generate_clicked = False # 클릭 μƒνƒœλ„ 리셋
st.sidebar.divider()
# 파일 μ—…λ‘œλ“œ
st.sidebar.header('πŸ“„ 파일 μ—…λ‘œλ“œ')
uploaded_file = st.sidebar.file_uploader(
"JSON 파일 μ—…λ‘œλ“œ",
type=['json'],
help="단어 λͺ©λ‘μ΄ ν¬ν•¨λœ JSON νŒŒμΌμ„ μ—…λ‘œλ“œν•˜μ„Έμš”. ν˜•μ‹: [{'word': '단어1'}, {'word': '단어2'}, ...]"
)
if uploaded_file is not None:
# μ—…λ‘œλ“œ λ²„νŠΌ λŒ€μ‹  파일이 있으면 λ°”λ‘œ 처리 μ‹œλ„ (μ‚¬μš©μž κ²½ν—˜ κ°œμ„ )
# if st.sidebar.button('μ—…λ‘œλ“œ 처리', key='upload_button'): # λ²„νŠΌ 제거
with st.spinner("μ—…λ‘œλ“œλœ 파일 처리 쀑..."):
new_file_id = handle_uploaded_file(uploaded_file)
if new_file_id:
st.sidebar.success(f"파일 '{uploaded_file.name}' μ—…λ‘œλ“œ 및 처리 μ™„λ£Œ!")
# μƒˆλ‘œ μ—…λ‘œλ“œλœ νŒŒμΌμ„ μžλ™μœΌλ‘œ 선택 λͺ©λ‘μ— μΆ”κ°€ν•˜κ³  선택 μƒνƒœλ‘œ λ§Œλ“¦
if new_file_id not in st.session_state.selected_files:
st.session_state.selected_files.append(new_file_id)
# 슀크립트 μž¬μ‹€ν–‰ν•˜μ—¬ UI μ—…λ°μ΄νŠΈ
# st.experimental_rerun()
else:
# handle_uploaded_file λ‚΄λΆ€μ—μ„œ 였λ₯˜ λ©”μ‹œμ§€ ν‘œμ‹œλ¨
pass
# μ—…λ‘œλ“œ μœ„μ ― μ΄ˆκΈ°ν™”λ₯Ό μœ„ν•΄ None ν• λ‹Ή (선택적)
# uploaded_file = None # μ΄λ ‡κ²Œ ν•˜λ©΄ 파일 선택 창이 λ‹€μ‹œ λ‚˜νƒ€λ‚¨, ν•„μš”μ— 따라 쑰절
st.sidebar.divider()
# 파일 선택 μ˜μ—­
st.sidebar.header('πŸ—‚οΈ 데이터 파일 선택')
if st.session_state.data_files:
# μ‚¬μš©ν•  파일 선택 μ²΄ν¬λ°•μŠ€
st.sidebar.markdown("**μ‚¬μš©ν•  νŒŒμΌμ„ μ„ νƒν•˜μ„Έμš” (닀쀑 선택 κ°€λŠ₯):**")
# 선택 μƒνƒœ 관리λ₯Ό μœ„ν•œ μž„μ‹œ 리슀트
selected_files_temp = []
# 파일 λͺ©λ‘ μ •λ ¬ (μ΄λ¦„μˆœ)
sorted_file_ids = sorted(st.session_state.data_files.keys(), key=lambda fid: st.session_state.data_files[fid]['name'])
# 각 νŒŒμΌμ— λŒ€ν•œ μ²΄ν¬λ°•μŠ€ 및 정보 ν‘œμ‹œ
for file_id in sorted_file_ids:
if file_id not in st.session_state.data_files: continue # μ‚­μ œλœ 경우 κ±΄λ„ˆλ›°κΈ°
file_info = st.session_state.data_files[file_id]
file_label = f"{file_info['name']} ({file_info['word_count']} 단어)"
file_type_tag = "[κΈ°λ³Έ]" if file_info['type'] == 'default' else "[μ—…λ‘œλ“œ]"
label_full = f"{file_label} {file_type_tag}"
# ν˜„μž¬ 파일이 μ„ νƒλ˜μ—ˆλŠ”μ§€ 확인 (μ„Έμ…˜ μƒνƒœ κΈ°μ€€)
is_selected = file_id in st.session_state.selected_files
# μ²΄ν¬λ°•μŠ€ 생성
checkbox_key = f"cb_{file_id}" # 고유 ν‚€
# μ²΄ν¬λ°•μŠ€ κ°’ λ³€κ²½ μ‹œ 콜백 μ‚¬μš© λŒ€μ‹ , 루프 ν›„ 비ꡐ λ°©μ‹μœΌλ‘œ 처리
if st.sidebar.checkbox(label_full, value=is_selected, key=checkbox_key):
# 체크된 경우 μž„μ‹œ λ¦¬μŠ€νŠΈμ— μΆ”κ°€
selected_files_temp.append(file_id)
# μƒ˜ν”Œ 단어 및 μ‚­μ œ λ²„νŠΌ (μ—…λ‘œλ“œλœ νŒŒμΌμ—λ§Œ)
with st.sidebar.expander("파일 정보 보기", expanded=False):
st.markdown(f"**μƒ˜ν”Œ 단어:** `{'`, `'.join(file_info['sample_words'])}`")
if file_info['type'] == 'uploaded':
delete_button_key = f"del_{file_id}"
if st.button('πŸ—‘οΈ 이 파일 μ‚­μ œ', key=delete_button_key, help=f"'{file_info['name']}' νŒŒμΌμ„ 영ꡬ적으둜 μ‚­μ œν•©λ‹ˆλ‹€."):
with st.spinner(f"'{file_info['name']}' μ‚­μ œ 쀑..."):
if delete_file(file_id):
# μ‚­μ œ 성곡 μ‹œ, selected_files_tempμ—μ„œλ„ 제거 (ν•„μˆ˜)
if file_id in selected_files_temp:
selected_files_temp.remove(file_id)
# data_files μƒνƒœκ°€ λ³€κ²½λ˜μ—ˆμœΌλ―€λ‘œ μž¬μ‹€ν–‰ ν•„μš”
# st.experimental_rerun()
else:
st.error("파일 μ‚­μ œμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.")
# st.sidebar.markdown("---") # ꡬ뢄선 제거 λ˜λŠ” μŠ€νƒ€μΌ μ‘°μ •
# --- μ€‘μš”: 선택 μƒνƒœ μ—…λ°μ΄νŠΈ ---
# ν˜„μž¬ μ²΄ν¬λ°•μŠ€ μƒνƒœ(selected_files_temp)와 μ„Έμ…˜ μƒνƒœ(st.session_state.selected_files)κ°€ λ‹€λ₯Ό λ•Œλ§Œ μ—…λ°μ΄νŠΈ
# μˆœμ„œμ— 상관없이 λΉ„κ΅ν•˜κΈ° μœ„ν•΄ μ •λ ¬ ν›„ 비ꡐ
if sorted(selected_files_temp) != sorted(st.session_state.selected_files):
st.session_state.selected_files = selected_files_temp
st.session_state.fig = None # 파일 선택 λ³€κ²½ μ‹œ κ·Έλž˜ν”„ μ΄ˆκΈ°ν™”
st.session_state.generate_clicked = False # 클릭 μƒνƒœλ„ 리셋
# 선택 λ³€κ²½ μ‹œ λ°”λ‘œ μž¬μ‹€ν–‰ν•˜μ—¬ UI 반영 (μ„ νƒμ μ΄μ§€λ§Œ μ‚¬μš©μž κ²½ν—˜ κ°œμ„ )
# st.experimental_rerun()
st.sidebar.divider()
# κ·Έλž˜ν”„ 생성 λ²„νŠΌ
# μ„ νƒλœ 파일이 μžˆμ„ λ•Œλ§Œ ν™œμ„±ν™”
if st.session_state.selected_files:
if st.sidebar.button('πŸ“Š κ·Έλž˜ν”„ 생성/μ—…λ°μ΄νŠΈ', key='generate_button', type="primary"):
# λ²„νŠΌ 클릭 μ‹œ, generate_clicked ν”Œλž˜κ·Έ μ„€μ •
# μ„ νƒλœ 파일이 μžˆλŠ”μ§€ λ‹€μ‹œ ν•œλ²ˆ 확인 (ν˜Ήμ‹œ λͺ¨λ₯Ό λ™μ‹œμ„± 문제 λ°©μ§€)
if st.session_state.selected_files:
st.session_state.generate_clicked = True
# μ—¬κΈ°μ„œ st.experimental_rerun() 호좜 제거! λ²„νŠΌ 클릭 μ‹œ μžλ™μœΌλ‘œ μž¬μ‹€ν–‰λ¨
else:
st.sidebar.warning('κ·Έλž˜ν”„λ₯Ό 생성할 νŒŒμΌμ„ λ¨Όμ € μ„ νƒν•΄μ£Όμ„Έμš”.')
st.session_state.generate_clicked = False # λ§Œμ•½μ„ μœ„ν•΄ 리셋
else:
st.sidebar.warning('κ·Έλž˜ν”„λ₯Ό μƒμ„±ν•˜λ €λ©΄ μ΅œμ†Œ 1개 μ΄μƒμ˜ νŒŒμΌμ„ μ„ νƒν•΄μ£Όμ„Έμš”.')
else:
st.sidebar.warning('μ‚¬μš© κ°€λŠ₯ν•œ 데이터 파일이 μ—†μŠ΅λ‹ˆλ‹€. νŒŒμΌμ„ μ—…λ‘œλ“œν•˜κ±°λ‚˜ `data` 폴더에 JSON νŒŒμΌμ„ μΆ”κ°€ν•˜μ„Έμš”.')
# μΊμ‹œ μ΄ˆκΈ°ν™” λ²„νŠΌ (항상 ν‘œμ‹œ)
if st.sidebar.button('πŸ”„ μΊμ‹œ μ΄ˆκΈ°ν™”', key='clear_cache_button'):
clear_cache()
# --- 메인 μ½˜ν…μΈ  μ˜μ—­ ---
st.header("πŸ“ˆ 3D 단어 λ„€νŠΈμ›Œν¬ μ‹œκ°ν™”")
# κ·Έλž˜ν”„ ν‘œμ‹œ 둜직
# 1. μ„ νƒλœ 파일이 μžˆμ–΄μ•Ό 함
# 2. 'κ·Έλž˜ν”„ 생성' λ²„νŠΌμ΄ ν΄λ¦­λ˜μ—ˆκ±°λ‚˜ (generate_clicked == True)
# 3. 이미 μƒμ„±λœ κ·Έλž˜ν”„κ°€ μ„Έμ…˜ μƒνƒœμ— μžˆμ–΄μ•Ό 함 (st.session_state.fig is not None)
if st.session_state.selected_files:
# κ·Έλž˜ν”„λ₯Ό 생성해야 ν•˜λŠ” 쑰건 : λ²„νŠΌ 클릭 ν”Œλž˜κ·Έκ°€ True μ΄κ±°λ‚˜, μž„κ³„κ°’/νŒŒμΌμ„ νƒ λ³€κ²½μœΌλ‘œ figκ°€ None이 된 경우
should_generate_graph = st.session_state.generate_clicked or \
(st.session_state.fig is None and st.session_state.selected_files) # 파일 선택 ν›„ figκ°€ 없을 λ•Œ
if should_generate_graph:
with st.spinner('κ·Έλž˜ν”„ 생성 쀑... μž μ‹œλ§Œ κΈ°λ‹€λ €μ£Όμ„Έμš”.'):
try:
# generate_graph ν•¨μˆ˜ 호좜
fig = generate_graph(st.session_state.selected_files, st.session_state.threshold)
# μ„±κ³΅μ μœΌλ‘œ μƒμ„±λ˜λ©΄ μ„Έμ…˜ μƒνƒœμ— μ €μž₯
st.session_state.fig = fig
# 생성 μ™„λ£Œ ν›„ 클릭 ν”Œλž˜κ·Έ 리셋
st.session_state.generate_clicked = False
except Exception as e:
st.error(f"κ·Έλž˜ν”„ 생성 쀑 였λ₯˜ λ°œμƒ: {e}")
st.session_state.fig = None # 였λ₯˜ λ°œμƒ μ‹œ fig μ΄ˆκΈ°ν™”
st.session_state.generate_clicked = False # ν”Œλž˜κ·Έ 리셋
# μƒμ„±λœ κ·Έλž˜ν”„κ°€ μ„Έμ…˜ μƒνƒœμ— 있으면 ν‘œμ‹œ
if st.session_state.get('fig') is not None:
st.plotly_chart(st.session_state.fig, use_container_width=True)
# ν˜„μž¬ κ·Έλž˜ν”„ 정보 ν‘œμ‹œ
try:
selected_file_names = [st.session_state.data_files[fid]['name'] for fid in st.session_state.selected_files if fid in st.session_state.data_files]
total_word_count = sum(st.session_state.data_files[fid]['word_count'] for fid in st.session_state.selected_files if fid in st.session_state.data_files)
# μ‹€μ œ κ·Έλž˜ν”„μ˜ λ…Έλ“œ/μ—£μ§€ 수 κ°€μ Έμ˜€κΈ° (fig 객체 뢄석 ν•„μš”)
num_nodes = len(st.session_state.fig.data[1].x) if len(st.session_state.fig.data) > 1 and hasattr(st.session_state.fig.data[1], 'x') else 0
num_edges = len(st.session_state.fig.data[0].x) // 3 if len(st.session_state.fig.data) > 0 and hasattr(st.session_state.fig.data[0], 'x') and st.session_state.fig.data[0].x else 0
st.info(f"""
**ν˜„μž¬ κ·Έλž˜ν”„ 정보**
- **데이터 파일:** {', '.join(selected_file_names)}
- **고유 단어 수 (λ…Έλ“œ):** {num_nodes} 개
- **μ—°κ²°μ„  수 (μ—£μ§€):** {num_edges} 개 (μœ μ‚¬λ„ β‰₯ {st.session_state.threshold:.2f})
""")
except Exception as info_e:
st.warning(f"κ·Έλž˜ν”„ 정보 ν‘œμ‹œ 쀑 였λ₯˜: {info_e}")
# μ‚¬μš© μ„€λͺ…
with st.expander("πŸ’‘ κ·Έλž˜ν”„ μ‘°μž‘ 방법"):
st.markdown("""
- **ν™•λŒ€/μΆ•μ†Œ:** 마우슀 휠 슀크둀 λ˜λŠ” ν„°μΉ˜μŠ€ν¬λ¦°μ—μ„œ 두 손가락 μ‚¬μš©
- **νšŒμ „:** 마우슀 μ™Όμͺ½ λ²„νŠΌ λˆ„λ₯Έ μƒνƒœλ‘œ λ“œλž˜κ·Έ
- **이동 (Pan):** 마우슀 였λ₯Έμͺ½ λ²„νŠΌ λˆ„λ₯Έ μƒνƒœλ‘œ λ“œλž˜κ·Έ λ˜λŠ” Shift + μ™Όμͺ½ λ²„νŠΌ λ“œλž˜κ·Έ
- **단어 정보 확인:** 마우슀 μ»€μ„œλ₯Ό 단어(마컀) μœ„μ— 올리면 단어 이름과 μ—°κ²°λœ λ‹€λ₯Έ λ‹¨μ–΄μ˜ 수λ₯Ό λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€.
- **νˆ΄λ°” μ‚¬μš©:** κ·Έλž˜ν”„ 우츑 μƒλ‹¨μ˜ νˆ΄λ°” μ•„μ΄μ½˜μ„ μ‚¬μš©ν•˜μ—¬ λ‹€μ–‘ν•œ 보기 μ˜΅μ…˜(λ‹€μš΄λ‘œλ“œ, ν™•λŒ€/μΆ•μ†Œ μ˜μ—­ μ§€μ • λ“±)을 ν™œμš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
""")
elif not should_generate_graph and not st.session_state.selected_files:
st.info("πŸ‘ˆ μ‚¬μ΄λ“œλ°”μ—μ„œ 뢄석할 데이터 νŒŒμΌμ„ μ„ νƒν•΄μ£Όμ„Έμš”.")
elif not should_generate_graph and st.session_state.selected_files and st.session_state.fig is None:
# νŒŒμΌμ€ μ„ νƒν–ˆμ§€λ§Œ 아직 생성 λ²„νŠΌ μ•ˆ λˆ„λ¦„ or 생성 μ‹€νŒ¨
st.info("πŸ‘ˆ μ‚¬μ΄λ“œλ°”μ—μ„œ 'πŸ“Š κ·Έλž˜ν”„ 생성/μ—…λ°μ΄νŠΈ' λ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ μ‹œκ°ν™”λ₯Ό μ‹œμž‘ν•˜μ„Έμš”.")
elif not st.session_state.data_files:
st.warning("ν‘œμ‹œν•  데이터 파일이 μ—†μŠ΅λ‹ˆλ‹€. νŒŒμΌμ„ μ—…λ‘œλ“œν•˜κ±°λ‚˜ `data` 폴더에 μœ νš¨ν•œ JSON νŒŒμΌμ„ μΆ”κ°€ν•˜μ„Έμš”.")
else:
# data_filesλŠ” μžˆμ§€λ§Œ selected_filesκ°€ μ—†λŠ” 경우
st.info("πŸ‘ˆ μ‚¬μ΄λ“œλ°”μ—μ„œ 뢄석할 데이터 νŒŒμΌμ„ μ„ νƒν•΄μ£Όμ„Έμš”.")
# --- ν•˜λ‹¨ 정보 μ„Ήμ…˜ ---
st.divider()
with st.expander("ℹ️ 이 μ‹œκ°ν™” 도ꡬ에 λŒ€ν•˜μ—¬"):
st.markdown("""
이 λ„κ΅¬λŠ” λ‹€μŒκ³Ό 같은 과정을 톡해 ν•œκ΅­μ–΄ 단어 λ„€νŠΈμ›Œν¬λ₯Ό μ‹œκ°ν™”ν•©λ‹ˆλ‹€:
1. **데이터 λ‘œλ”©:** μ‚¬μš©μžκ°€ μ œκ³΅ν•œ JSON νŒŒμΌμ—μ„œ 'word' ν•„λ“œλ₯Ό κ°€μ§„ 단어 λͺ©λ‘μ„ μΆ”μΆœν•©λ‹ˆλ‹€.
2. **단어 μž„λ² λ”©:** 각 단어λ₯Ό 고차원 벑터 곡간에 ν‘œν˜„ν•©λ‹ˆλ‹€. ν˜„μž¬λŠ” **문자 ꡬ성 기반 TF-IDF μŠ€νƒ€μΌ μž„λ² λ”©**을 μ‚¬μš©ν•˜μ—¬, 단어λ₯Ό μ΄λ£¨λŠ” λ¬Έμžλ“€μ˜ λΉˆλ„λ₯Ό 기반으둜 벑터λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€. (μΆ”ν›„ Word2Vec, FastText λ“± 사전 ν›ˆλ ¨λœ λͺ¨λΈ μ‚¬μš© κ°€λŠ₯)
3. **차원 μΆ•μ†Œ:** 고차원 μž„λ² λ”© 벑터λ₯Ό μ‹œκ°ν™” κ°€λŠ₯ν•œ 3차원 κ³΅κ°„μœΌλ‘œ μΆ•μ†Œν•©λ‹ˆλ‹€. **t-SNE(t-Distributed Stochastic Neighbor Embedding)** μ•Œκ³ λ¦¬μ¦˜μ„ μ‚¬μš©ν•˜μ—¬ λ³΅μž‘ν•œ 데이터 ꡬ쑰λ₯Ό μœ μ§€ν•˜λ©΄μ„œ 차원을 μ€„μž…λ‹ˆλ‹€. (단어 μˆ˜κ°€ 적을 경우 PCA μ‚¬μš©)
4. **μœ μ‚¬λ„ 계산:** 3D κ³΅κ°„μœΌλ‘œ μΆ•μ†Œν•˜κΈ° μ „μ˜ 원본 μž„λ² λ”© 벑터 κ°„μ˜ **코사인 μœ μ‚¬λ„(Cosine Similarity)**λ₯Ό κ³„μ‚°ν•˜μ—¬ 단어 쌍의 의미적(μ—¬κΈ°μ„œλŠ” ꡬ성적) μœ μ‚¬μ„±μ„ μΈ‘μ •ν•©λ‹ˆλ‹€.
5. **κ·Έλž˜ν”„ 생성:** μ„€μ •λœ **μœ μ‚¬λ„ μž„κ³„κ°’(Threshold)** 이상인 단어 μŒλ“€μ„ μ—°κ²°μ„ (μ—£μ§€)으둜 이어 λ„€νŠΈμ›Œν¬ κ·Έλž˜ν”„λ₯Ό κ΅¬μ„±ν•©λ‹ˆλ‹€. 각 λ‹¨μ–΄λŠ” λ…Έλ“œ(점)둜 ν‘œμ‹œλ©λ‹ˆλ‹€.
6. **3D μ‹œκ°ν™”:** **Plotly 라이브러리**λ₯Ό μ‚¬μš©ν•˜μ—¬ μƒμ„±λœ λ„€νŠΈμ›Œν¬ κ·Έλž˜ν”„λ₯Ό μΈν„°λž™ν‹°λΈŒν•œ 3D 곡간에 μ‹œκ°ν™”ν•©λ‹ˆλ‹€. λ…Έλ“œμ˜ μœ„μΉ˜λŠ” t-SNE κ²°κ³Ό μ’Œν‘œλ₯Ό λ”°λ₯΄λ©°, μƒ‰μƒμ΄λ‚˜ ν¬κΈ°λŠ” ZμΆ• κ°’μ΄λ‚˜ μ—°κ²° 수(degree) 등을 λ°˜μ˜ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
이λ₯Ό 톡해 단어듀이 μ„œλ‘œ μ–Όλ§ˆλ‚˜ μœ μ‚¬ν•œμ§€μ— 따라 ꡰ집을 μ΄λ£¨κ±°λ‚˜ μ—°κ²°λ˜λŠ” νŒ¨ν„΄μ„ μ‹œκ°μ μœΌλ‘œ 탐색할 수 μžˆμŠ΅λ‹ˆλ‹€.
""")
with st.expander("πŸ“‹ JSON 파일 ν˜•μ‹ μ•ˆλ‚΄"):
st.markdown("""
μ—…λ‘œλ“œν•˜κ±°λ‚˜ `data` 폴더에 λ„£λŠ” JSON νŒŒμΌμ€ **UTF-8 인코딩**이어야 ν•˜λ©°, λ‹€μŒκ³Ό 같은 ν˜•μ‹μ„ 따라야 ν•©λ‹ˆλ‹€:
```json
[
{
"word": "학ꡐ"
},
{
"word": "μ„ μƒλ‹˜"
},
{
"word": "학생"
},
{
"word": "ꡐ싀"
},
{
"word": "컴퓨터",
"description": "이 ν•„λ“œλŠ” λ¬΄μ‹œλ©λ‹ˆλ‹€"
}
]
```
- 파일의 μ΅œμƒμœ„ κ΅¬μ‘°λŠ” **λ°°μ—΄(List)**이어야 ν•©λ‹ˆλ‹€ (`[...]`).
- λ°°μ—΄μ˜ 각 μš”μ†ŒλŠ” **객체(Dictionary)**μ—¬μ•Ό ν•©λ‹ˆλ‹€ (`{...}`).
- 각 κ°μ²΄λŠ” λ°˜λ“œμ‹œ `"word"`λΌλŠ” ν‚€λ₯Ό 포함해야 ν•˜λ©°, κ·Έ 값은 뢄석할 **ν•œκ΅­μ–΄ 단어 λ¬Έμžμ—΄**이어야 ν•©λ‹ˆλ‹€.
- `"word"` μ™Έμ˜ λ‹€λ₯Έ ν‚€κ°€ μžˆμ–΄λ„ λ¬΄λ°©ν•˜λ‚˜, ν˜„μž¬ λ²„μ „μ—μ„œλŠ” μ‚¬μš©λ˜μ§€ μ•Šκ³  λ¬΄μ‹œλ©λ‹ˆλ‹€.
- 파일 인코딩이 UTF-8이 μ•„λ‹Œ 경우 ν•œκΈ€μ΄ κΉ¨μ§€κ±°λ‚˜ 였λ₯˜κ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.
""")