JUNGU commited on
Commit
43f1594
ยท
verified ยท
1 Parent(s): f5b5cdc

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +285 -180
src/streamlit_app.py CHANGED
@@ -7,274 +7,379 @@ from scipy.stats import norm, skew
7
  import platform
8
  import os
9
  import matplotlib.font_manager as fm
 
 
10
 
11
- def set_korean_font():
12
- """ํ•œ๊ธ€ ํฐํŠธ ์„ค์ • ํ•จ์ˆ˜ - ๊ฐ•์ œ๋กœ ํฐํŠธ ์ ์šฉ"""
 
13
 
14
- # matplotlib ์บ์‹œ ํด๋ฆฌ์–ด (์ค‘์š”!)
 
 
 
15
  plt.rcdefaults()
16
 
17
- # 1. ์‚ฌ์šฉ์ž ์ง€์ • ํฐํŠธ ํŒŒ์ผ ํ™•์ธ
18
- font_filename = "NanumGaRamYeonGgoc.ttf"
19
- font_path = os.path.join(os.getcwd(), font_filename)
20
- selected_font = None
 
21
 
22
- if os.path.exists(font_path):
23
- try:
24
- # ํฐํŠธ ๋งค๋‹ˆ์ € ์บ์‹œ ํด๋ฆฌ์–ด
25
- fm.fontManager.__init__()
26
- # ํฐํŠธ ํŒŒ์ผ์„ ์‹œ์Šคํ…œ์— ๋“ฑ๋ก
27
- fm.fontManager.addfont(font_path)
28
- font_prop = fm.FontProperties(fname=font_path)
29
- selected_font = font_prop.get_name()
30
- st.sidebar.success(f"์‚ฌ์šฉ์ž ํฐํŠธ '{selected_font}' ๋กœ๋”ฉ ์„ฑ๊ณต!")
31
- except Exception as e:
32
- st.sidebar.warning(f"์‚ฌ์šฉ์ž ํฐํŠธ ๋กœ๋”ฉ ์‹คํŒจ: {e}")
33
 
34
- # 2. ์‹œ์Šคํ…œ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ•œ๊ธ€ ํฐํŠธ ์ฐพ๊ธฐ
35
- if not selected_font:
36
- # ์‹ค์ œ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํฐํŠธ๋“ค ์ฒดํฌ
37
- system_fonts = fm.findSystemFonts()
38
- korean_font_candidates = []
39
-
40
- for font_path in system_fonts:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  try:
42
- font_name = fm.FontProperties(fname=font_path).get_name()
43
- # ํ•œ๊ธ€ ํฐํŠธ ํ›„๋ณด๋“ค
44
- if any(keyword in font_name.lower() for keyword in
45
- ['nanum', 'malgun', 'gothic', 'gulim', 'dotum', 'batang']):
46
- korean_font_candidates.append(font_name)
47
- except:
 
 
48
  continue
49
-
50
- if korean_font_candidates:
51
- selected_font = korean_font_candidates[0]
52
- st.sidebar.info(f"์‹œ์Šคํ…œ ํ•œ๊ธ€ ํฐํŠธ '{selected_font}' ๋ฐœ๊ฒฌ!")
53
 
54
- # 3. ์šด์˜์ฒด์ œ๋ณ„ ๊ธฐ๋ณธ ํฐํŠธ ์„ค์ •
55
- if not selected_font:
56
- if platform.system() == 'Windows':
57
- selected_font = 'Malgun Gothic'
58
- elif platform.system() == 'Darwin': # macOS
59
- selected_font = 'AppleGothic'
60
- else: # Linux - ๋‚˜๋ˆ”ํฐํŠธ๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ Sans ์‚ฌ์šฉ
61
- selected_font = 'sans-serif'
62
-
63
- st.sidebar.warning(f"๊ธฐ๋ณธ ํฐํŠธ '{selected_font}' ์‚ฌ์šฉ")
64
-
65
- # 4. matplotlib ์„ค์ • ๊ฐ•์ œ ์ ์šฉ
66
  plt.rcParams.update({
67
- 'font.family': selected_font,
68
- 'font.sans-serif': [selected_font, 'DejaVu Sans', 'Arial'],
69
  'axes.unicode_minus': False,
70
- 'font.size': 10
 
 
 
 
71
  })
72
 
73
- # 5. ํฐํŠธ ์„ค์ • ๊ฒ€์ฆ
74
- test_fig, test_ax = plt.subplots(figsize=(1, 1))
75
- test_ax.text(0.5, 0.5, 'ํ•œ๊ธ€ํ…Œ์ŠคํŠธ', ha='center', va='center')
76
- current_font = test_ax.texts[0].get_fontname()
77
- plt.close(test_fig)
78
-
79
- st.sidebar.text(f"์ ์šฉ๋œ ํฐํŠธ: {current_font}")
80
 
81
- return selected_font
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
  def analyze_scores(df):
84
  """๋ฐ์ดํ„ฐํ”„๋ ˆ์ž„์„ ๋ฐ›์•„ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ํ‘œ์‹œํ•˜๋Š” ํ•จ์ˆ˜"""
85
- st.subheader("๋ฐ์ดํ„ฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์ƒ์œ„ 5๊ฐœ)")
86
  st.dataframe(df.head())
87
 
88
- # ์ˆซ์ž ํ˜•์‹์˜ ์—ด๋งŒ ์„ ํƒ์ง€๋กœ ์ œ๊ณตํ•˜์—ฌ ์˜ค๋ฅ˜ ๋ฐฉ์ง€
89
  numeric_columns = df.select_dtypes(include=np.number).columns.tolist()
90
  if not numeric_columns:
91
- st.error("๋ฐ์ดํ„ฐ์—์„œ ๋ถ„์„ ๊ฐ€๋Šฅํ•œ ์ˆซ์ž ํ˜•์‹์˜ ์—ด์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
92
  return
93
 
94
- score_column = st.selectbox("๋ถ„์„ํ•  ์ ์ˆ˜ ์—ด(column)์„ ์„ ํƒํ•˜์„ธ์š”:", numeric_columns)
95
 
96
  if score_column:
97
  scores = df[score_column].dropna()
98
 
99
- # ์œ ํšจํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ
100
  if len(scores) == 0:
101
- st.error("์„ ํƒํ•œ ์—ด์— ์œ ํšจํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
102
  return
103
 
104
- st.subheader(f"'{score_column}' ์ ์ˆ˜ ๋ถ„ํฌ ๋ถ„์„ ๊ฒฐ๊ณผ")
105
 
106
  # 1. ๊ธฐ์ˆ  ํ†ต๊ณ„๋Ÿ‰
107
- st.write("#### ๐Ÿ“ˆ ๊ธฐ์ˆ  ํ†ต๊ณ„๋Ÿ‰")
108
- st.table(scores.describe())
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
- # 2. ๋ถ„ํฌ ์‹œ๊ฐํ™”
111
  st.write("#### ๐ŸŽจ ์ ์ˆ˜ ๋ถ„ํฌ ์‹œ๊ฐํ™”")
112
 
113
- # ๋งค๋ฒˆ ์ƒˆ๋กœ์šด figure ์ƒ์„ฑ ์‹œ ํฐํŠธ ์„ค์ • ์žฌ์ ์šฉ
114
- plt.rcParams.update({
115
- 'font.family': plt.rcParams.get('font.family', 'sans-serif'),
116
- 'axes.unicode_minus': False
117
- })
118
-
119
- fig, ax = plt.subplots(figsize=(12, 7))
120
-
121
  try:
122
- # ํžˆ์Šคํ† ๊ทธ๋žจ๊ณผ KDE ๊ณก์„  ๊ทธ๋ฆฌ๊ธฐ
123
- sns.histplot(scores, kde=True, stat='density', alpha=0.7, ax=ax)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
  # ์ •๊ทœ๋ถ„ํฌ ๊ณก์„  ์ถ”๊ฐ€
126
  mu, std = norm.fit(scores)
127
- xmin, xmax = ax.get_xlim()
128
- x = np.linspace(xmin, xmax, 100)
129
- p = norm.pdf(x, mu, std)
130
- ax.plot(x, p, 'r-', linewidth=2, label=f'์ •๊ทœ๋ถ„ํฌ (ฮผ={mu:.1f}, ฯƒ={std:.1f})')
 
 
 
131
 
132
- # ์ œ๋ชฉ๊ณผ ๋ผ๋ฒจ ์„ค์ • (ํ•œ๊ธ€ ํฌํ•จ)
133
- ax.set_title(f'{score_column} ์ ์ˆ˜ ๋ถ„ํฌ ๋ถ„์„', fontsize=16, pad=20)
134
- ax.set_xlabel('์ ์ˆ˜', fontsize=12)
135
- ax.set_ylabel('๋ฐ€๋„', fontsize=12)
136
- ax.legend(fontsize=10)
137
- ax.grid(True, alpha=0.3)
138
 
139
- # ํ†ต๊ณ„ ์ •๋ณด ํ…์ŠคํŠธ ๋ฐ•์Šค ์ถ”๊ฐ€
140
- stats_text = f'ํ‰๊ท : {mu:.2f}\nํ‘œ์ค€ํŽธ์ฐจ: {std:.2f}\n์ƒ˜ํ”Œ ์ˆ˜: {len(scores)}'
 
 
 
 
 
 
 
141
  ax.text(0.02, 0.98, stats_text, transform=ax.transAxes,
142
- fontsize=10, verticalalignment='top',
143
- bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))
144
 
145
  plt.tight_layout()
146
  st.pyplot(fig)
147
 
148
  except Exception as e:
149
- st.error(f"๊ทธ๋ž˜ํ”„ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
150
- # ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๊ฐ„๋‹จํ•œ ํžˆ์Šคํ† ๊ทธ๋žจ์œผ๋กœ ๋Œ€์ฒด
 
 
151
  fig2, ax2 = plt.subplots(figsize=(10, 6))
152
- ax2.hist(scores, bins=20, alpha=0.7, edgecolor='black')
153
- ax2.set_title('Score Distribution (Fallback)', fontsize=14)
154
  ax2.set_xlabel('Score')
155
  ax2.set_ylabel('Frequency')
 
156
  st.pyplot(fig2)
157
  plt.close(fig2)
158
  finally:
159
- plt.close(fig) # ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€
 
160
 
161
- # 3. ์™œ๋„(Skewness) ๋ถ„์„
162
- st.write("#### ๐Ÿ“ ์™œ๋„ (Skewness) ๋ถ„์„")
163
  try:
164
  skewness = skew(scores)
165
- st.metric(label="์™œ๋„ (Skewness)", value=f"{skewness:.4f}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
- if skewness > 0.5:
168
- st.info("๐Ÿ”ด ๊ผฌ๋ฆฌ๊ฐ€ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ๊ธด ๋ถ„ํฌ (Positive Skew): ๋Œ€๋ถ€๋ถ„์˜ ํ•™์ƒ๋“ค์ด ํ‰๊ท ๋ณด๋‹ค ๋‚ฎ์€ ์ ์ˆ˜์— ๋ชฐ๋ ค์žˆ๊ณ , ์ผ๋ถ€ ๊ณ ๋“์ ์ž๋“ค์ด ํ‰๊ท ์„ ๋†’์ด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.")
169
- elif skewness < -0.5:
170
- st.info("๐Ÿ”ต ๊ผฌ๋ฆฌ๊ฐ€ ์™ผ์ชฝ์œผ๋กœ ๊ธด ๋ถ„ํฌ (Negative Skew): ๋Œ€๋ถ€๋ถ„์˜ ํ•™์ƒ๋“ค์ด ํ‰๊ท ๋ณด๋‹ค ๋†’์€ ์ ์ˆ˜์— ๋ชฐ๋ ค์žˆ๊ณ , ์ผ๋ถ€ ์ €๋“์ ์ž๋“ค์ด ํ‰๊ท ์„ ๋‚ฎ์ถ”๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.")
171
- else:
172
- st.info("๐ŸŸข ๋Œ€์นญ์— ๊ฐ€๊นŒ์šด ๋ถ„ํฌ: ์ ์ˆ˜๊ฐ€ ํ‰๊ท ์„ ์ค‘์‹ฌ์œผ๋กœ ๋น„๊ต์  ๊ณ ๋ฅด๊ฒŒ ๋ถ„ํฌ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  except Exception as e:
174
- st.error(f"์™œ๋„ ๊ณ„์‚ฐ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
175
 
176
  def main():
177
  st.set_page_config(
178
- page_title="ํ•™์ƒ ์ ์ˆ˜ ๋ถ„์„",
179
  page_icon="๐Ÿ“Š",
180
- layout="wide"
 
181
  )
182
 
183
- # ํฐํŠธ ์„ค์ •์„ ๋จผ์ € ์‹คํ–‰ํ•˜๊ณ  ์„ค์ •๋œ ํฐํŠธ๋ช… ๋ฐ›๊ธฐ
184
- selected_font = set_korean_font()
185
-
186
- st.title("ํ•™์ƒ ์ ์ˆ˜ ๋ถ„ํฌ ๋ถ„์„ ๋„๊ตฌ ๐Ÿ“Š")
187
- st.write("CSV ํŒŒ์ผ์„ ์ง์ ‘ ์—…๋กœ๋“œํ•˜๊ฑฐ๋‚˜ Google Sheets URL์„ ๋ถ™์—ฌ๋„ฃ์–ด ํ•™์ƒ ์ ์ˆ˜ ๋ถ„ํฌ๋ฅผ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค.")
188
 
189
- # ํฐํŠธ ํ…Œ์ŠคํŠธ (๋””๋ฒ„๊น…์šฉ)
190
- if st.sidebar.checkbox("ํฐํŠธ ํ…Œ์ŠคํŠธ ๋ณด๊ธฐ"):
191
- test_fig, test_ax = plt.subplots(figsize=(8, 4))
192
- test_ax.text(0.5, 0.7, f'ํ•œ๊ธ€ ํฐํŠธ ํ…Œ์ŠคํŠธ: {selected_font}',
193
- ha='center', va='center', fontsize=14)
194
- test_ax.text(0.5, 0.5, '๊ฐ€๋‚˜๋‹ค๋ผ๋งˆ๋ฐ”์‚ฌ ABCD 1234',
195
- ha='center', va='center', fontsize=12)
196
- test_ax.text(0.5, 0.3, '์ ์ˆ˜, ๋ถ„ํฌ, ๋ถ„์„, ๊ทธ๋ž˜ํ”„',
197
- ha='center', va='center', fontsize=12)
198
- test_ax.set_xlim(0, 1)
199
- test_ax.set_ylim(0, 1)
200
- test_ax.axis('off')
201
- st.pyplot(test_fig)
202
- plt.close(test_fig)
 
 
 
 
 
 
 
203
 
204
- st.write("---")
205
 
 
206
  st.sidebar.title("๐Ÿ“ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ")
207
  source_option = st.sidebar.radio(
208
- "๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ์„ ํƒํ•˜์„ธ์š”:",
209
- ("CSV ํŒŒ์ผ ์—…๋กœ๋“œ", "Google Sheets URL")
210
  )
211
 
212
  df = None
213
 
214
- if source_option == "CSV ํŒŒ์ผ ์—…๋กœ๋“œ":
215
  uploaded_file = st.sidebar.file_uploader(
216
- "CSV ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜์„ธ์š”.",
217
  type=["csv"],
218
- help="UTF-8 ๋˜๋Š” CP949 ์ธ์ฝ”๋”ฉ๋œ CSV ํŒŒ์ผ์„ ์„ ํƒํ•˜์„ธ์š”."
219
  )
220
  if uploaded_file:
221
- try:
222
- # ์—ฌ๋Ÿฌ ์ธ์ฝ”๋”ฉ ๋ฐฉ์‹์œผ๋กœ ์‹œ๋„
223
- encodings = ['utf-8-sig', 'utf-8', 'cp949', 'euc-kr']
224
- for encoding in encodings:
225
- try:
226
- df = pd.read_csv(uploaded_file, encoding=encoding)
227
- st.sidebar.success(f"ํŒŒ์ผ ์ฝ๊ธฐ ์„ฑ๊ณต! (์ธ์ฝ”๋”ฉ: {encoding})")
228
- break
229
- except UnicodeDecodeError:
230
- continue
231
-
232
- if df is None:
233
- st.error("ํŒŒ์ผ ์ธ์ฝ”๋”ฉ์„ ์ธ์‹ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. UTF-8 ๋˜๋Š” CP949๋กœ ์ €์žฅ๋œ ํŒŒ์ผ์„ ์‚ฌ์šฉํ•ด์ฃผ์„ธ์š”.")
234
-
235
- except Exception as e:
236
- st.error(f"ํŒŒ์ผ์„ ์ฝ๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {e}")
237
 
238
- elif source_option == "Google Sheets URL":
239
- st.sidebar.info("Google Sheets๋ฅผ '์›น์— ๊ฒŒ์‹œ'ํ•˜๊ณ  CSV ํ˜•์‹์˜ URL์„ ์ž…๋ ฅํ•˜์„ธ์š”.")
240
- sample_url = "https://docs.google.com/spreadsheets/d/e/2PACX-1vQ2Z8kzJq2sM7w2_9gXo-jZ-mO5o-BvC-w5p2nJ6oJ7oJ9xL-w3kZ9j5Z3kX7vN1aQ4mB1cW8jB7fR/pub?gid=0&single=true&output=csv"
241
  url = st.sidebar.text_input(
242
  "Google Sheets CSV URL",
243
- value="",
244
  placeholder="https://docs.google.com/spreadsheets/d/..."
245
  )
246
 
247
- if url:
248
- if "docs.google.com" not in url:
249
- st.sidebar.warning("์˜ฌ๋ฐ”๋ฅธ Google Sheets URL์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”.")
250
- else:
251
- try:
252
- with st.spinner("๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘..."):
253
- df = pd.read_csv(url)
254
- st.sidebar.success("Google Sheets ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์„ฑ๊ณต!")
255
- except Exception as e:
256
- st.error(f"URL๋กœ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {e}")
257
- st.warning("์˜ฌ๋ฐ”๋ฅธ Google Sheets '์›น ๊ฒŒ์‹œ' CSV URL์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”.")
258
 
259
- # ๋ฐ์ดํ„ฐ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋กœ๋“œ๋œ ๊ฒฝ์šฐ์—๋งŒ ๋ถ„์„ ํ•จ์ˆ˜ ์‹คํ–‰
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  if df is not None and not df.empty:
261
- st.success(f"๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์™„๋ฃŒ! (ํ–‰: {len(df)}, ์—ด: {len(df.columns)})")
262
  analyze_scores(df)
263
  else:
264
- st.info("๐Ÿ‘ˆ ์‚ฌ์ด๋“œ๋ฐ”์—์„œ ๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ์„ ํƒํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์™€์ฃผ์„ธ์š”.")
265
 
266
- # ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ๋ฒ„ํŠผ
267
- if st.button("๐ŸŽฒ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ๋กœ ํ…Œ์ŠคํŠธ"):
268
- np.random.seed(42)
269
- sample_data = {
270
- 'ํ•™์ƒ๋ฒˆํ˜ธ': range(1, 101),
271
- '์ˆ˜ํ•™์ ์ˆ˜': np.random.normal(75, 15, 100).clip(0, 100).round(1),
272
- '์˜์–ด์ ์ˆ˜': np.random.normal(80, 12, 100).clip(0, 100).round(1),
273
- '๊ณผํ•™์ ์ˆ˜': np.random.normal(70, 18, 100).clip(0, 100).round(1)
274
- }
275
- df = pd.DataFrame(sample_data)
276
- st.success("์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!")
277
- analyze_scores(df)
 
278
 
279
  if __name__ == '__main__':
280
  main()
 
7
  import platform
8
  import os
9
  import matplotlib.font_manager as fm
10
+ import warnings
11
+ warnings.filterwarnings('ignore')
12
 
13
+ # ์ „์—ญ ํฐํŠธ ์„ค์ • - ์•ฑ ์‹œ์ž‘ ์ฆ‰์‹œ ์‹คํ–‰
14
+ def configure_matplotlib_korean():
15
+ """matplotlib ํ•œ๊ธ€ ํฐํŠธ ๊ฐ•์ œ ์„ค์ •"""
16
 
17
+ # matplotlib ๋ฐฑ์—”๋“œ ์„ค์ •
18
+ plt.switch_backend('Agg')
19
+
20
+ # ๊ธฐ์กด ์„ค์ • ์™„์ „ ์ดˆ๊ธฐํ™”
21
  plt.rcdefaults()
22
 
23
+ # ํฐํŠธ ์บ์‹œ ์™„์ „ ์‚ญ์ œ ๋ฐ ์žฌ๊ตฌ์„ฑ
24
+ try:
25
+ fm._load_fontmanager(try_read_cache=False)
26
+ except:
27
+ pass
28
 
29
+ # ์šด์˜์ฒด์ œ๋ณ„ ํ•œ๊ธ€ ํฐํŠธ ๊ฒฝ๋กœ ์ง์ ‘ ์ง€์ •
30
+ korean_font_paths = []
 
 
 
 
 
 
 
 
 
31
 
32
+ if platform.system() == 'Windows':
33
+ korean_font_paths = [
34
+ 'C:/Windows/Fonts/malgun.ttf',
35
+ 'C:/Windows/Fonts/gulim.ttc',
36
+ 'C:/Windows/Fonts/batang.ttc',
37
+ ]
38
+ fallback_font = 'Malgun Gothic'
39
+ elif platform.system() == 'Darwin': # macOS
40
+ korean_font_paths = [
41
+ '/System/Library/Fonts/AppleGothic.ttf',
42
+ '/System/Library/Fonts/Helvetica.ttc',
43
+ ]
44
+ fallback_font = 'AppleGothic'
45
+ else: # Linux
46
+ korean_font_paths = [
47
+ '/usr/share/fonts/truetype/nanum/NanumGothic.ttf',
48
+ '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf',
49
+ ]
50
+ fallback_font = 'DejaVu Sans'
51
+
52
+ # ์‚ฌ์šฉ์ž ์ •์˜ ํฐํŠธ ํŒŒ์ผ ๊ฒฝ๋กœ๋„ ์ถ”๊ฐ€
53
+ user_font = os.path.join(os.getcwd(), "NanumGaRamYeonGgoc.ttf")
54
+ if os.path.exists(user_font):
55
+ korean_font_paths.insert(0, user_font)
56
+
57
+ selected_font_path = None
58
+ selected_font_name = fallback_font
59
+
60
+ # ์‹ค์ œ ์กด์žฌํ•˜๋Š” ํฐํŠธ ํŒŒ์ผ ์ฐพ๊ธฐ
61
+ for font_path in korean_font_paths:
62
+ if os.path.exists(font_path):
63
  try:
64
+ font_prop = fm.FontProperties(fname=font_path)
65
+ selected_font_name = font_prop.get_name()
66
+ selected_font_path = font_path
67
+
68
+ # ํฐํŠธ ๋งค๋‹ˆ์ €์— ๊ฐ•์ œ ๋“ฑ๋ก
69
+ fm.fontManager.addfont(font_path)
70
+ break
71
+ except Exception as e:
72
  continue
 
 
 
 
73
 
74
+ # matplotlib rcParams ๊ฐ•์ œ ์„ค์ •
 
 
 
 
 
 
 
 
 
 
 
75
  plt.rcParams.update({
76
+ 'font.family': 'sans-serif',
77
+ 'font.sans-serif': [selected_font_name, 'DejaVu Sans', 'Arial', 'sans-serif'],
78
  'axes.unicode_minus': False,
79
+ 'font.size': 12,
80
+ 'figure.dpi': 100,
81
+ 'savefig.dpi': 100,
82
+ 'figure.facecolor': 'white',
83
+ 'axes.facecolor': 'white'
84
  })
85
 
86
+ # ์ „์—ญ ํฐํŠธ ์†์„ฑ ๊ฐ์ฒด ์ƒ์„ฑ
87
+ if selected_font_path:
88
+ global KOREAN_FONT_PROP
89
+ KOREAN_FONT_PROP = fm.FontProperties(fname=selected_font_path)
90
+ else:
91
+ KOREAN_FONT_PROP = fm.FontProperties(family=selected_font_name)
 
92
 
93
+ return selected_font_name, selected_font_path
94
+
95
+ # ์•ฑ ์‹œ์ž‘ ์‹œ ์ฆ‰์‹œ ํฐํŠธ ์„ค์ •
96
+ FONT_NAME, FONT_PATH = configure_matplotlib_korean()
97
+ KOREAN_FONT_PROP = None
98
+
99
+ def apply_korean_font_to_plot():
100
+ """๊ฐœ๋ณ„ ํ”Œ๋กฏ์— ํ•œ๊ธ€ ํฐํŠธ ์ง์ ‘ ์ ์šฉ"""
101
+ if FONT_PATH and os.path.exists(FONT_PATH):
102
+ font_prop = fm.FontProperties(fname=FONT_PATH)
103
+ return font_prop
104
+ else:
105
+ return fm.FontProperties(family=FONT_NAME)
106
 
107
  def analyze_scores(df):
108
  """๋ฐ์ดํ„ฐํ”„๋ ˆ์ž„์„ ๋ฐ›์•„ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ํ‘œ์‹œํ•˜๋Š” ํ•จ์ˆ˜"""
109
+ st.subheader("๐Ÿ“‹ ๋ฐ์ดํ„ฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์ƒ์œ„ 5๊ฐœ)")
110
  st.dataframe(df.head())
111
 
112
+ # ์ˆซ์ž ํ˜•์‹์˜ ์—ด๋งŒ ์„ ํƒ์ง€๋กœ ์ œ๊ณต
113
  numeric_columns = df.select_dtypes(include=np.number).columns.tolist()
114
  if not numeric_columns:
115
+ st.error("โŒ ๋ฐ์ดํ„ฐ์—์„œ ๋ถ„์„ ๊ฐ€๋Šฅํ•œ ์ˆซ์ž ํ˜•์‹์˜ ์—ด์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
116
  return
117
 
118
+ score_column = st.selectbox("๐Ÿ“Š ๋ถ„์„ํ•  ์ ์ˆ˜ ์—ด(column)์„ ์„ ํƒํ•˜์„ธ์š”:", numeric_columns)
119
 
120
  if score_column:
121
  scores = df[score_column].dropna()
122
 
 
123
  if len(scores) == 0:
124
+ st.error("โŒ ์„ ํƒํ•œ ์—ด์— ์œ ํšจํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
125
  return
126
 
127
+ st.subheader(f"๐Ÿ“ˆ '{score_column}' ์ ์ˆ˜ ๋ถ„ํฌ ๋ถ„์„ ๊ฒฐ๊ณผ")
128
 
129
  # 1. ๊ธฐ์ˆ  ํ†ต๊ณ„๋Ÿ‰
130
+ st.write("#### ๐Ÿ“Š ๊ธฐ๋ณธ ํ†ต๊ณ„๋Ÿ‰")
131
+ col1, col2, col3, col4 = st.columns(4)
132
+ with col1:
133
+ st.metric("ํ‰๊ท ", f"{scores.mean():.2f}")
134
+ with col2:
135
+ st.metric("ํ‘œ์ค€ํŽธ์ฐจ", f"{scores.std():.2f}")
136
+ with col3:
137
+ st.metric("์ตœ์†Ÿ๊ฐ’", f"{scores.min():.2f}")
138
+ with col4:
139
+ st.metric("์ตœ๋Œ“๊ฐ’", f"{scores.max():.2f}")
140
+
141
+ # ์ƒ์„ธ ํ†ต๊ณ„
142
+ st.write("#### ๐Ÿ“‹ ์ƒ์„ธ ํ†ต๊ณ„๋Ÿ‰")
143
+ st.dataframe(scores.describe().to_frame().T)
144
 
145
+ # 2. ๋ถ„ํฌ ์‹œ๊ฐํ™” - ๊ฐ•ํ™”๋œ ํ•œ๊ธ€ ํฐํŠธ ์ ์šฉ
146
  st.write("#### ๐ŸŽจ ์ ์ˆ˜ ๋ถ„ํฌ ์‹œ๊ฐํ™”")
147
 
 
 
 
 
 
 
 
 
148
  try:
149
+ # ํฐํŠธ ์†์„ฑ ๊ฐ์ฒด ์ƒ์„ฑ
150
+ korean_font = apply_korean_font_to_plot()
151
+
152
+ # Figure ์ƒ์„ฑ ๋ฐ ์„ค์ •
153
+ fig, ax = plt.subplots(figsize=(14, 8))
154
+ fig.patch.set_facecolor('white')
155
+
156
+ # ํžˆ์Šคํ† ๊ทธ๋žจ ์ƒ์„ฑ
157
+ n, bins, patches = ax.hist(scores, bins=20, density=True, alpha=0.7,
158
+ color='skyblue', edgecolor='navy', linewidth=0.8)
159
+
160
+ # KDE ๊ณก์„  ์ถ”๊ฐ€
161
+ try:
162
+ from scipy.stats import gaussian_kde
163
+ kde = gaussian_kde(scores)
164
+ x_range = np.linspace(scores.min(), scores.max(), 200)
165
+ ax.plot(x_range, kde(x_range), 'orange', linewidth=3, label='์‹ค์ œ ๋ถ„ํฌ ๊ณก์„ ')
166
+ except:
167
+ pass
168
 
169
  # ์ •๊ทœ๋ถ„ํฌ ๊ณก์„  ์ถ”๊ฐ€
170
  mu, std = norm.fit(scores)
171
+ x_norm = np.linspace(scores.min(), scores.max(), 100)
172
+ y_norm = norm.pdf(x_norm, mu, std)
173
+ ax.plot(x_norm, y_norm, 'red', linewidth=2, linestyle='--',
174
+ label=f'์ •๊ทœ๋ถ„ํฌ (ํ‰๊ท ={mu:.1f}, ํ‘œ์ค€ํŽธ์ฐจ={std:.1f})')
175
+
176
+ # ํ‰๊ท ์„  ์ถ”๊ฐ€
177
+ ax.axvline(mu, color='red', linestyle=':', linewidth=2, alpha=0.8, label=f'ํ‰๊ท : {mu:.1f}')
178
 
179
+ # ์ œ๋ชฉ๊ณผ ๋ผ๋ฒจ - ํ•œ๊ธ€ ํฐํŠธ ์ง์ ‘ ์ ์šฉ
180
+ ax.set_title(f'{score_column} ์ ์ˆ˜ ๋ถ„ํฌ ๋ถ„์„', fontproperties=korean_font, fontsize=18, pad=20)
181
+ ax.set_xlabel('์ ์ˆ˜', fontproperties=korean_font, fontsize=14)
182
+ ax.set_ylabel('๋ฐ€๋„', fontproperties=korean_font, fontsize=14)
 
 
183
 
184
+ # ๋ฒ”๋ก€ ์„ค์ •
185
+ legend = ax.legend(prop=korean_font, fontsize=11, loc='upper right')
186
+ legend.get_frame().set_alpha(0.9)
187
+
188
+ # ๊ฒฉ์ž ์ถ”๊ฐ€
189
+ ax.grid(True, alpha=0.3, linestyle='-', linewidth=0.5)
190
+
191
+ # ํ†ต๊ณ„ ์ •๋ณด ๋ฐ•์Šค
192
+ stats_text = f'์ƒ˜ํ”Œ ์ˆ˜: {len(scores)}\nํ‰๊ท : {mu:.2f}\nํ‘œ์ค€ํŽธ์ฐจ: {std:.2f}\n์ตœ์†Ÿ๊ฐ’: {scores.min():.1f}\n์ตœ๋Œ“๊ฐ’: {scores.max():.1f}'
193
  ax.text(0.02, 0.98, stats_text, transform=ax.transAxes,
194
+ fontproperties=korean_font, fontsize=10, verticalalignment='top',
195
+ bbox=dict(boxstyle='round,pad=0.5', facecolor='lightblue', alpha=0.8))
196
 
197
  plt.tight_layout()
198
  st.pyplot(fig)
199
 
200
  except Exception as e:
201
+ st.error(f"โŒ ์ƒ์„ธ ๊ทธ๋ž˜ํ”„ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜: {e}")
202
+
203
+ # ๋Œ€์ฒด ๊ทธ๋ž˜ํ”„ (์˜์–ด๋งŒ ์‚ฌ์šฉ)
204
+ st.write("**Alternative Chart (English only):**")
205
  fig2, ax2 = plt.subplots(figsize=(10, 6))
206
+ ax2.hist(scores, bins=15, alpha=0.7, color='lightcoral', edgecolor='black')
207
+ ax2.set_title(f'Distribution of {score_column}', fontsize=14)
208
  ax2.set_xlabel('Score')
209
  ax2.set_ylabel('Frequency')
210
+ ax2.grid(True, alpha=0.3)
211
  st.pyplot(fig2)
212
  plt.close(fig2)
213
  finally:
214
+ if 'fig' in locals():
215
+ plt.close(fig)
216
 
217
+ # 3. ์™œ๋„ ๋ถ„์„
218
+ st.write("#### ๐Ÿ“ ๋ถ„ํฌ ํ˜•ํƒœ ๋ถ„์„ (์™œ๋„)")
219
  try:
220
  skewness = skew(scores)
221
+ col1, col2 = st.columns([1, 2])
222
+
223
+ with col1:
224
+ st.metric("์™œ๋„ (Skewness)", f"{skewness:.4f}")
225
+
226
+ with col2:
227
+ if skewness > 0.5:
228
+ st.success("๐Ÿ”ด **์–‘์˜ ์™œ๋„ (Right Skewed)**: ๋Œ€๋ถ€๋ถ„ ํ•™์ƒ์ด ๋‚ฎ์€ ์ ์ˆ˜๋Œ€์— ๋ถ„ํฌํ•˜๊ณ , ์†Œ์ˆ˜์˜ ๊ณ ๋“์ ์ž๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.")
229
+ elif skewness < -0.5:
230
+ st.success("๐Ÿ”ต **์Œ์˜ ์™œ๋„ (Left Skewed)**: ๋Œ€๋ถ€๋ถ„ ํ•™์ƒ์ด ๋†’์€ ์ ์ˆ˜๋Œ€์— ๋ถ„ํฌํ•˜๊ณ , ์†Œ์ˆ˜์˜ ์ €๋“์ ์ž๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.")
231
+ else:
232
+ st.success("๐ŸŸข **๋Œ€์นญ ๋ถ„ํฌ**: ์ ์ˆ˜๊ฐ€ ํ‰๊ท ์„ ์ค‘์‹ฌ์œผ๋กœ ๊ณ ๋ฅด๊ฒŒ ๋ถ„ํฌ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.")
233
+
234
+ except Exception as e:
235
+ st.error(f"์™œ๋„ ๊ณ„์‚ฐ ์ค‘ ์˜ค๋ฅ˜: {e}")
236
 
237
+ # 4. ์ถ”๊ฐ€ ๋ถ„์„
238
+ st.write("#### ๐Ÿ“‹ ๊ตฌ๊ฐ„๋ณ„ ๋ถ„ํฌ")
239
+
240
+ # ์ ์ˆ˜ ๊ตฌ๊ฐ„ ๋‚˜๋ˆ„๊ธฐ
241
+ if scores.max() <= 100: # 100์  ๋งŒ์  ๊ฐ€์ •
242
+ bins_labels = ['0-60', '61-70', '71-80', '81-90', '91-100']
243
+ bins_edges = [0, 60, 70, 80, 90, 100]
244
+ else:
245
+ # ๋™์  ๊ตฌ๊ฐ„ ์ƒ์„ฑ
246
+ min_score, max_score = scores.min(), scores.max()
247
+ interval = (max_score - min_score) / 5
248
+ bins_edges = [min_score + i * interval for i in range(6)]
249
+ bins_labels = [f'{bins_edges[i]:.0f}-{bins_edges[i+1]:.0f}' for i in range(5)]
250
+
251
+ try:
252
+ score_counts = pd.cut(scores, bins=bins_edges, labels=bins_labels, include_lowest=True).value_counts().sort_index()
253
+ score_percentages = (score_counts / len(scores) * 100).round(1)
254
+
255
+ result_df = pd.DataFrame({
256
+ '๊ตฌ๊ฐ„': score_counts.index,
257
+ 'ํ•™์ƒ ์ˆ˜': score_counts.values,
258
+ '๋น„์œจ (%)': score_percentages.values
259
+ })
260
+ st.dataframe(result_df)
261
+
262
  except Exception as e:
263
+ st.warning(f"๊ตฌ๊ฐ„ ๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜: {e}")
264
 
265
  def main():
266
  st.set_page_config(
267
+ page_title="ํ•™์ƒ ์ ์ˆ˜ ๋ถ„์„ ๋„๊ตฌ",
268
  page_icon="๐Ÿ“Š",
269
+ layout="wide",
270
+ initial_sidebar_state="expanded"
271
  )
272
 
273
+ # ์ œ๋ชฉ
274
+ st.title("๐Ÿ“Š ํ•™์ƒ ์ ์ˆ˜ ๋ถ„ํฌ ๋ถ„์„ ๋„๊ตฌ")
275
+ st.markdown("**CSV ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๊ฑฐ๋‚˜ Google Sheets URL์„ ์ž…๋ ฅํ•˜์—ฌ ์ ์ˆ˜ ๋ถ„ํฌ๋ฅผ ๋ถ„์„ํ•˜์„ธ์š”**")
 
 
276
 
277
+ # ํฐํŠธ ์ •๋ณด ํ‘œ์‹œ
278
+ with st.expander("๐Ÿ”ง ํฐํŠธ ์„ค์ • ์ •๋ณด"):
279
+ st.write(f"**ํ˜„์žฌ ํฐํŠธ**: {FONT_NAME}")
280
+ st.write(f"**ํฐํŠธ ๊ฒฝ๋กœ**: {FONT_PATH if FONT_PATH else '์‹œ์Šคํ…œ ๊ธฐ๋ณธ'}")
281
+
282
+ # ํฐํŠธ ํ…Œ์ŠคํŠธ
283
+ if st.button("ํฐํŠธ ํ…Œ์ŠคํŠธ"):
284
+ try:
285
+ test_fig, test_ax = plt.subplots(figsize=(8, 3))
286
+ korean_font = apply_korean_font_to_plot()
287
+ test_ax.text(0.5, 0.7, 'ํ•œ๊ธ€ ํฐํŠธ ํ…Œ์ŠคํŠธ', ha='center', va='center',
288
+ fontproperties=korean_font, fontsize=16)
289
+ test_ax.text(0.5, 0.3, '์ ์ˆ˜ ๋ถ„ํฌ ๋ถ„์„ ๊ทธ๋ž˜ํ”„', ha='center', va='center',
290
+ fontproperties=korean_font, fontsize=14)
291
+ test_ax.set_xlim(0, 1)
292
+ test_ax.set_ylim(0, 1)
293
+ test_ax.axis('off')
294
+ st.pyplot(test_fig)
295
+ plt.close(test_fig)
296
+ except Exception as e:
297
+ st.error(f"ํฐํŠธ ํ…Œ์ŠคํŠธ ์‹คํŒจ: {e}")
298
 
299
+ st.markdown("---")
300
 
301
+ # ์‚ฌ์ด๋“œ๋ฐ” - ๋ฐ์ดํ„ฐ ์ž…๋ ฅ
302
  st.sidebar.title("๐Ÿ“ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ")
303
  source_option = st.sidebar.radio(
304
+ "๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ ํƒ:",
305
+ ("๐Ÿ“ค CSV ํŒŒ์ผ ์—…๋กœ๋“œ", "๐Ÿ”— Google Sheets URL", "๐ŸŽฒ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ")
306
  )
307
 
308
  df = None
309
 
310
+ if source_option == "๐Ÿ“ค CSV ํŒŒ์ผ ์—…๋กœ๋“œ":
311
  uploaded_file = st.sidebar.file_uploader(
312
+ "CSV ํŒŒ์ผ์„ ์„ ํƒํ•˜์„ธ์š”",
313
  type=["csv"],
314
+ help="UTF-8, CP949 ๋“ฑ ๋‹ค์–‘ํ•œ ์ธ์ฝ”๋”ฉ์„ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค"
315
  )
316
  if uploaded_file:
317
+ encodings = ['utf-8-sig', 'utf-8', 'cp949', 'euc-kr', 'latin1']
318
+ for encoding in encodings:
319
+ try:
320
+ df = pd.read_csv(uploaded_file, encoding=encoding)
321
+ st.sidebar.success(f"โœ… ํŒŒ์ผ ๋กœ๋”ฉ ์„ฑ๊ณต! (์ธ์ฝ”๋”ฉ: {encoding})")
322
+ break
323
+ except UnicodeDecodeError:
324
+ continue
325
+ except Exception as e:
326
+ st.sidebar.error(f"ํŒŒ์ผ ์ฝ๊ธฐ ์˜ค๋ฅ˜: {e}")
327
+ break
328
+
329
+ if df is None:
330
+ st.sidebar.error("โŒ ํŒŒ์ผ ์ธ์ฝ”๋”ฉ์„ ์ธ์‹ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
 
 
331
 
332
+ elif source_option == "๐Ÿ”— Google Sheets URL":
333
+ st.sidebar.info("๐Ÿ’ก Google Sheets๋ฅผ '์›น์— ๊ฒŒ์‹œ'ํ•œ ํ›„ CSV URL์„ ์ž…๋ ฅํ•˜์„ธ์š”")
 
334
  url = st.sidebar.text_input(
335
  "Google Sheets CSV URL",
 
336
  placeholder="https://docs.google.com/spreadsheets/d/..."
337
  )
338
 
339
+ if url and "docs.google.com" in url:
340
+ try:
341
+ with st.spinner("๐Ÿ“ฅ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์ค‘..."):
342
+ df = pd.read_csv(url)
343
+ st.sidebar.success("โœ… Google Sheets ๋กœ๋”ฉ ์„ฑ๊ณต!")
344
+ except Exception as e:
345
+ st.sidebar.error(f"โŒ URL ๋กœ๋”ฉ ์‹คํŒจ: {e}")
346
+ elif url:
347
+ st.sidebar.warning("โš ๏ธ ์˜ฌ๋ฐ”๋ฅธ Google Sheets URL์„ ์ž…๋ ฅํ•˜์„ธ์š”")
 
 
348
 
349
+ elif source_option == "๐ŸŽฒ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ":
350
+ if st.sidebar.button("์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ"):
351
+ np.random.seed(42)
352
+ sample_size = st.sidebar.slider("์ƒ˜ํ”Œ ํฌ๊ธฐ", 50, 500, 100)
353
+
354
+ df = pd.DataFrame({
355
+ 'ํ•™์ƒ๋ฒˆํ˜ธ': range(1, sample_size + 1),
356
+ '์ˆ˜ํ•™์ ์ˆ˜': np.random.normal(75, 15, sample_size).clip(0, 100).round(1),
357
+ '์˜์–ด์ ์ˆ˜': np.random.normal(80, 12, sample_size).clip(0, 100).round(1),
358
+ '๊ณผํ•™์ ์ˆ˜': np.random.normal(70, 18, sample_size).clip(0, 100).round(1),
359
+ '๊ตญ์–ด์ ์ˆ˜': np.random.normal(77, 14, sample_size).clip(0, 100).round(1)
360
+ })
361
+ st.sidebar.success(f"โœ… {sample_size}๋ช…์˜ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ!")
362
+
363
+ # ๋ฉ”์ธ ๋ถ„์„
364
  if df is not None and not df.empty:
365
+ st.success(f"๐ŸŽ‰ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์™„๋ฃŒ! **{len(df)}๊ฐœ ํ–‰, {len(df.columns)}๊ฐœ ์—ด**")
366
  analyze_scores(df)
367
  else:
368
+ st.info("๐Ÿ‘ˆ **์‚ฌ์ด๋“œ๋ฐ”์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์„ ํƒํ•˜์„ธ์š”**")
369
 
370
+ # ๊ธฐ๋Šฅ ์•ˆ๋‚ด
371
+ st.markdown("""
372
+ ### ๐Ÿ” ์ฃผ์š” ๊ธฐ๋Šฅ
373
+ - **๐Ÿ“Š ๊ธฐ๋ณธ ํ†ต๊ณ„**: ํ‰๊ท , ํ‘œ์ค€ํŽธ์ฐจ, ์ตœ์†Ÿ๊ฐ’, ์ตœ๋Œ“๊ฐ’ ๋“ฑ
374
+ - **๐Ÿ“ˆ ๋ถ„ํฌ ์‹œ๊ฐํ™”**: ํžˆ์Šคํ† ๊ทธ๋žจ, KDE ๊ณก์„ , ์ •๊ทœ๋ถ„ํฌ ๋น„๊ต
375
+ - **๐Ÿ“ ์™œ๋„ ๋ถ„์„**: ๋ถ„ํฌ์˜ ๋น„๋Œ€์นญ์„ฑ ์ธก์ •
376
+ - **๐Ÿ“‹ ๊ตฌ๊ฐ„๋ณ„ ๋ถ„ํฌ**: ์ ์ˆ˜ ๊ตฌ๊ฐ„๋ณ„ ํ•™์ƒ ์ˆ˜ ๋ฐ ๋น„์œจ
377
+
378
+ ### ๐Ÿ“ ์ง€์› ํ˜•์‹
379
+ - **CSV ํŒŒ์ผ**: UTF-8, CP949, EUC-KR ๋“ฑ ์ž๋™ ์ธ์ฝ”๋”ฉ ๊ฐ์ง€
380
+ - **Google Sheets**: ์›น์— ๊ฒŒ์‹œ๋œ ์‹œํŠธ์˜ CSV URL
381
+ - **์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ**: ํ…Œ์ŠคํŠธ์šฉ ๊ฐ€์ƒ ์ ์ˆ˜ ๋ฐ์ดํ„ฐ
382
+ """)
383
 
384
  if __name__ == '__main__':
385
  main()