github-actions[bot] commited on
Commit
645a5e6
Β·
1 Parent(s): 4f784f5

sync from 029f8ff

Browse files
Files changed (3) hide show
  1. README.md +2 -2
  2. app.py +330 -425
  3. requirements.txt +1 -1
README.md CHANGED
@@ -3,8 +3,8 @@ title: DartLab
3
  emoji: πŸ“Š
4
  colorFrom: red
5
  colorTo: yellow
6
- sdk: gradio
7
- sdk_version: "6.10.0"
8
  app_file: app.py
9
  pinned: true
10
  license: mit
 
3
  emoji: πŸ“Š
4
  colorFrom: red
5
  colorTo: yellow
6
+ sdk: streamlit
7
+ sdk_version: "1.45.1"
8
  app_file: app.py
9
  pinned: true
10
  license: mit
app.py CHANGED
@@ -1,517 +1,422 @@
1
- """DartLab Gradio Demo β€” DART/EDGAR κ³΅μ‹œ 뢄석."""
2
 
3
  from __future__ import annotations
4
 
5
  import gc
6
  import os
7
  import re
8
- import threading
9
  from collections import OrderedDict
10
 
11
- import gradio as gr
12
  import pandas as pd
 
13
 
14
  import dartlab
15
 
16
  # ── μ„€μ • ──────────────────────────────────────────────
17
 
18
  _MAX_CACHE = 2
19
- _companyCache: OrderedDict = OrderedDict()
20
- _HAS_AI = bool(os.environ.get("OPENAI_API_KEY"))
21
-
22
- if _HAS_AI:
23
- dartlab.llm.configure(provider="openai", api_key=os.environ["OPENAI_API_KEY"])
24
-
25
  _LOGO_URL = "https://raw.githubusercontent.com/eddmpython/dartlab/master/.github/assets/logo.png"
26
  _BLOG_URL = "https://eddmpython.github.io/dartlab/blog/dartlab-easy-start/"
27
  _DOCS_URL = "https://eddmpython.github.io/dartlab/docs/getting-started/quickstart"
28
  _COLAB_URL = "https://colab.research.google.com/github/eddmpython/dartlab/blob/master/notebooks/showcase/01_quickstart.ipynb"
29
  _REPO_URL = "https://github.com/eddmpython/dartlab"
30
 
 
31
 
32
- # ── μœ ν‹Έ ──────────────────────────────────────────────
33
-
34
-
35
- def _toPandas(df):
36
- """Polars/pandas DataFrame β†’ pandas λ³€ν™˜."""
37
- if df is None:
38
- return None
39
- if hasattr(df, "to_pandas"):
40
- return df.to_pandas()
41
- return df
42
-
43
-
44
- def _formatNumbers(df):
45
- """숫자 μ»¬λŸΌμ— μ²œλ‹¨μœ„ κ΅¬λΆ„μž 적용, μ†Œμˆ˜μ  있으면 μ†Œμˆ˜ 2자리."""
46
- if df is None or not isinstance(df, pd.DataFrame) or df.empty:
47
- return df
48
- result = df.copy()
49
- for col in result.columns:
50
- if pd.api.types.is_numeric_dtype(result[col]):
51
- result[col] = result[col].apply(
52
- lambda x: _fmtNum(x) if pd.notna(x) else ""
53
- )
54
- return result
55
-
56
-
57
- def _fmtNum(x):
58
- """숫자 β†’ μ²œλ‹¨μœ„ κ΅¬λΆ„μž λ¬Έμžμ—΄."""
59
- if x != x: # NaN
60
- return ""
61
- if isinstance(x, float):
62
- if x == int(x):
63
- return f"{int(x):,}"
64
- return f"{x:,.2f}"
65
- return f"{int(x):,}"
66
-
67
-
68
- def _getCompany(code: str):
69
- """μΊμ‹œλœ Company λ°˜ν™˜, μ΅œλŒ€ 2개 μœ μ§€."""
70
- code = code.strip()
71
- if code in _companyCache:
72
- _companyCache.move_to_end(code)
73
- return _companyCache[code]
74
- while len(_companyCache) >= _MAX_CACHE:
75
- _companyCache.popitem(last=False)
76
- gc.collect()
77
- c = dartlab.Company(code)
78
- _companyCache[code] = c
79
- return c
80
-
81
-
82
- # ── ν”„λ¦¬λ‘œλ“œ ──────────────────────────────────────────
83
-
84
-
85
- def _warmup():
86
- """λ°±κ·ΈλΌμš΄λ“œ listing μΊμ‹œ μ›Œλ°μ—…."""
87
- try:
88
- dartlab.search("μ‚Όμ„±μ „μž")
89
- except Exception:
90
- pass
91
-
92
-
93
- threading.Thread(target=_warmup, daemon=True).start()
94
-
95
-
96
- # ── ν•Έλ“€λŸ¬: 톡합 뢄석 ────────────────────────────────
97
-
98
-
99
- def handleAnalyze(query: str):
100
- """μ’…λͺ©μ½”λ“œ λ˜λŠ” νšŒμ‚¬λͺ… β†’ 기업정보 + μž¬λ¬΄μ œν‘œ + topics."""
101
- empty = ("", "", None, gr.update(choices=[], value=None), None, "")
102
-
103
- if not query or not query.strip():
104
- return ("⚠️ μ’…λͺ©μ½”λ“œ λ˜λŠ” νšŒμ‚¬λͺ…을 μž…λ ₯ν•˜μ„Έμš”.", *empty[1:])
105
-
106
- query = query.strip()
107
-
108
- # μ’…λͺ©μ½”λ“œ νŒλ³„: 6자리 숫자 λ˜λŠ” 영문 티컀
109
- if re.match(r"^\d{6}$", query) or re.match(r"^[A-Z]{1,5}$", query):
110
- code = query
111
- else:
112
- try:
113
- results = dartlab.search(query)
114
- if results is None or len(results) == 0:
115
- return (
116
- f"⚠️ '{query}' 검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€.\n\nμ’…λͺ©μ½”λ“œ(예: 005930)λ‚˜ 티컀(예: AAPL)λ₯Ό 직접 μž…λ ₯ν•΄λ³΄μ„Έμš”.",
117
- *empty[1:],
118
- )
119
- code = str(results[0, "stockCode"])
120
- except Exception as e:
121
- return (f"⚠️ 검색 μ‹€νŒ¨: {e}", *empty[1:])
122
-
123
- try:
124
- c = _getCompany(code)
125
- except Exception as e:
126
- return (f"⚠️ κΈ°μ—… λ‘œλ“œ μ‹€νŒ¨: {e}", *empty[1:])
127
-
128
- # 기본정보 β€” μΉ΄λ“œ μŠ€νƒ€μΌ
129
- info = f"### 🏒 {c.corpName}\n"
130
- info += f"| ν•­λͺ© | κ°’ |\n|---|---|\n"
131
- info += f"| **μ’…λͺ©μ½”λ“œ** | `{c.stockCode}` |\n"
132
- info += f"| **μ‹œμž₯** | {c.market} |\n"
133
- if hasattr(c, "currency") and c.currency:
134
- info += f"| **톡화** | {c.currency} |\n"
135
-
136
- # topics
137
- topics = []
138
- try:
139
- topics = list(c.topics) if c.topics else []
140
- except Exception:
141
- pass
142
-
143
- # IS κΈ°λ³Έ λ‘œλ“œ
144
- finance = None
145
- try:
146
- raw = _toPandas(c.IS)
147
- finance = _formatNumbers(raw)
148
- except Exception:
149
- pass
150
-
151
- topicUpdate = gr.update(
152
- choices=topics,
153
- value=topics[0] if topics else None,
154
- )
155
-
156
- return info, code, finance, topicUpdate, None, ""
157
-
158
-
159
- def handleFinance(code: str, sheet: str):
160
- """μž¬λ¬΄μ œν‘œ μ‹œνŠΈ μ „ν™˜."""
161
- if not code or not code.strip():
162
- return None
163
- try:
164
- c = _getCompany(code)
165
- except Exception:
166
- return None
167
-
168
- lookup = {"IS": "IS", "BS": "BS", "CF": "CF", "ratios": "ratios"}
169
- attr = lookup.get(sheet, "IS")
170
- try:
171
- result = getattr(c, attr, None)
172
- raw = _toPandas(result)
173
- return _formatNumbers(raw)
174
- except Exception:
175
- return None
176
-
177
-
178
- def handleShow(code: str, topic: str):
179
- """sections show β†’ DataFrame λ˜λŠ” Markdown."""
180
- if not code or not topic:
181
- return None, ""
182
- try:
183
- c = _getCompany(code)
184
- result = c.show(topic)
185
- except Exception as e:
186
- return None, f"⚠️ 쑰회 μ‹€νŒ¨: {e}"
187
-
188
- if result is None:
189
- return None, "데이터 μ—†μŒ"
190
- if hasattr(result, "to_pandas"):
191
- raw = _toPandas(result)
192
- return _formatNumbers(raw), ""
193
- return None, str(result)
194
-
195
-
196
- # ── ν•Έλ“€λŸ¬: AI Chat ───────────────────────────────────
197
-
198
-
199
- def handleChat(message: str, history: list, code: str):
200
- """AI 뢄석 λŒ€ν™”."""
201
- if not _HAS_AI:
202
- history = history + [
203
- {"role": "user", "content": message},
204
- {
205
- "role": "assistant",
206
- "content": "⚠️ OPENAI_API_KEYκ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.\n\nHuggingFace Spaces Settings β†’ Variables and secretsμ—μ„œ μ„€μ •ν•˜μ„Έμš”.",
207
- },
208
- ]
209
- return history, ""
210
-
211
- stockCode = code.strip() if code else None
212
- if not stockCode:
213
- history = history + [
214
- {"role": "user", "content": message},
215
- {"role": "assistant", "content": "⚠️ λ¨Όμ € μƒλ‹¨μ—μ„œ μ’…λͺ©μ„ λΆ„μ„ν•˜μ„Έμš”."},
216
- ]
217
- return history, ""
218
-
219
- try:
220
- query = f"{stockCode} {message}" if stockCode else message
221
- answer = dartlab.ask(query, stream=False, raw=False)
222
- except Exception as e:
223
- answer = f"뢄석 μ‹€νŒ¨: {e}"
224
-
225
- history = history + [
226
- {"role": "user", "content": message},
227
- {"role": "assistant", "content": answer if answer else "응닡 μ—†μŒ"},
228
- ]
229
- return history, ""
230
-
231
 
232
- # ── UI ────────────────────────────────────────────────
233
 
234
- _CSS = """
235
- /* ── dartlab 닀크 ν…Œλ§ˆ ── */
236
- .gradio-container {
237
- background: #0a0d16 !important;
238
- max-width: 880px !important;
239
- margin: 0 auto !important;
 
 
 
 
 
240
  }
241
 
242
  /* 헀더 */
243
  .dl-header {
244
  text-align: center;
245
- padding: 2rem 0 1rem;
246
  }
247
  .dl-header img {
248
- display: inline-block;
249
  border-radius: 50%;
250
  box-shadow: 0 0 40px rgba(234,70,71,0.3);
251
  }
252
  .dl-header h1 {
253
- color: #ea4647 !important;
254
- font-size: 2.4rem !important;
255
- margin: 0.6rem 0 0.2rem !important;
256
- font-weight: 800 !important;
257
  letter-spacing: -0.02em;
258
  }
259
  .dl-header .tagline {
260
  color: #94a3b8;
261
  font-size: 1.05rem;
262
- margin: 0 0 0.3rem;
263
  }
264
  .dl-header .sub {
265
  color: #64748b;
266
  font-size: 0.85rem;
267
- margin: 0;
268
  }
269
 
270
- /* μ„Ήμ…˜ 라벨 */
271
  .dl-section {
272
- color: #ea4647 !important;
273
- font-weight: 700 !important;
274
- font-size: 1.05rem !important;
275
- margin: 1.2rem 0 0.4rem !important;
276
- border-bottom: 1px solid #1e2433;
277
- padding-bottom: 0.3rem;
278
  }
279
 
280
- /* 기업정보 μΉ΄λ“œ */
281
- .dl-info .prose {
282
  background: #0f1219;
283
  border: 1px solid #1e2433;
284
- border-radius: 8px;
285
- padding: 1rem 1.5rem;
 
286
  }
287
- .dl-info table {
288
- width: auto !important;
 
 
289
  }
290
- .dl-info th, .dl-info td {
291
- padding: 0.3rem 1rem 0.3rem 0 !important;
292
- border: none !important;
 
293
  }
294
-
295
- /* 데이터 ν…Œμ΄λΈ” κ°•ν™” */
296
- .dl-table table {
297
- font-variant-numeric: tabular-nums !important;
298
  }
299
- .dl-table th {
300
- background: #141824 !important;
301
- color: #cbd5e1 !important;
302
- font-weight: 600 !important;
303
- text-align: center !important;
304
- padding: 0.5rem 0.8rem !important;
305
- white-space: nowrap !important;
306
- border-bottom: 2px solid #ea4647 !important;
307
  }
308
- .dl-table td {
309
- padding: 0.4rem 0.8rem !important;
310
- text-align: right !important;
311
- white-space: nowrap !important;
312
- font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace !important;
313
- font-size: 0.88rem !important;
314
  }
315
- .dl-table td:first-child {
316
- text-align: left !important;
317
- font-family: inherit !important;
318
- font-weight: 500 !important;
319
- color: #e2e8f0 !important;
 
 
320
  }
321
- .dl-table tr:hover td {
322
- background: rgba(234,70,71,0.04) !important;
 
 
323
  }
324
 
325
  /* ν‘Έν„° */
326
  .dl-footer {
327
  text-align: center;
328
  padding: 2rem 0 1rem;
329
- color: #475569;
330
- font-size: 0.85rem;
331
  border-top: 1px solid #1e2433;
332
  margin-top: 2rem;
 
 
333
  }
334
  .dl-footer a {
335
- color: #94a3b8 !important;
336
  text-decoration: none;
337
- margin: 0 0.6rem;
338
- transition: color 0.2s;
339
  }
340
  .dl-footer a:hover {
341
- color: #ea4647 !important;
342
  }
343
 
344
- /* μž…λ ₯ μ˜μ—­ κ°•μ‘° */
345
- .dl-input input {
346
- font-size: 1.1rem !important;
347
- text-align: center !important;
348
- }
349
-
350
- /* μ•ˆλ‚΄ ν…μŠ€νŠΈ */
351
- .dl-guide {
352
  text-align: center;
353
- color: #64748b;
354
- font-size: 0.9rem;
355
- margin: 0.3rem 0 0;
356
  }
357
- """
358
-
359
- _THEME = gr.themes.Base(
360
- primary_hue=gr.themes.Color(
361
- c50="#fef2f2", c100="#fee2e2", c200="#fecaca", c300="#fca5a5",
362
- c400="#f87171", c500="#ea4647", c600="#dc2626", c700="#c83232",
363
- c800="#991b1b", c900="#7f1d1d", c950="#450a0a",
364
- ),
365
- neutral_hue=gr.themes.Color(
366
- c50="#f8fafc", c100="#f1f5f9", c200="#e2e8f0", c300="#cbd5e1",
367
- c400="#94a3b8", c500="#64748b", c600="#475569", c700="#334155",
368
- c800="#1e293b", c900="#0f172a", c950="#0a0d16",
369
- ),
370
- font=("system-ui", "-apple-system", "sans-serif"),
371
- ).set(
372
- body_background_fill="#0a0d16",
373
- body_text_color="#f1f5f9",
374
- block_background_fill="#0f1219",
375
- block_border_color="#1e2433",
376
- input_background_fill="#1a1f2b",
377
- button_primary_background_fill="#ea4647",
378
- button_primary_text_color="#ffffff",
379
- button_secondary_background_fill="#1a1f2b",
380
- button_secondary_text_color="#f1f5f9",
381
- button_secondary_border_color="#1e2433",
382
- )
383
 
384
 
385
- with gr.Blocks(
386
- title="DartLab β€” μ’…λͺ©μ½”λ“œ ν•˜λ‚˜λ‘œ κΈ°μ—… 뢄석",
387
- theme=_THEME,
388
- css=_CSS,
389
- ) as demo:
390
 
391
- # ── 헀더 ──
392
- gr.HTML(f"""
393
- <div class="dl-header">
394
- <img src="{_LOGO_URL}" width="96" height="96" alt="DartLab">
395
- <h1>DartLab</h1>
396
- <p class="tagline">μ’…λͺ©μ½”λ“œ ν•˜λ‚˜. κΈ°μ—…μ˜ 전체 이야기.</p>
397
- <p class="sub">DART Β· EDGAR κ³΅μ‹œ 데이터λ₯Ό κ΅¬μ‘°ν™”ν•˜μ—¬ μ œκ³΅ν•©λ‹ˆλ‹€</p>
398
- </div>
399
- """)
400
-
401
- # ── 곡유 state ──
402
- codeState = gr.State("")
403
-
404
- # ── μ’…λͺ© μž…λ ₯ ──
405
- gr.HTML('<p class="dl-guide">μ’…λͺ©μ½”λ“œ(005930) λ˜λŠ” νšŒμ‚¬λͺ…(μ‚Όμ„±μ „μž)을 μž…λ ₯ν•˜μ„Έμš”</p>')
406
- with gr.Row():
407
- queryInput = gr.Textbox(
408
- label="",
409
- placeholder="005930, μ‚Όμ„±μ „μž, AAPL ...",
410
- scale=4,
411
- elem_classes="dl-input",
412
- show_label=False,
413
- )
414
- analyzeBtn = gr.Button("λΆ„μ„ν•˜κΈ°", scale=1, variant="primary", size="lg")
415
 
416
- # ── κΈ°μ—… 정보 ──
417
- companyInfo = gr.Markdown("", elem_classes="dl-info")
 
 
 
 
 
418
 
419
- # ── μž¬λ¬΄μ œν‘œ ──
420
- gr.Markdown("### πŸ“Š μž¬λ¬΄μ œν‘œ", elem_classes="dl-section")
421
- gr.HTML('<p class="dl-guide">IS(μ†μ΅κ³„μ‚°μ„œ) Β· BS(μž¬λ¬΄μƒνƒœν‘œ) Β· CF(ν˜„κΈˆνλ¦„ν‘œ) Β· ratios(μž¬λ¬΄λΉ„μœ¨)</p>')
422
- sheetSelect = gr.Dropdown(
423
- choices=["IS", "BS", "CF", "ratios"],
424
- value="IS",
425
- label="μ‹œνŠΈ 선택",
426
- scale=1,
427
- )
428
- financeTable = gr.DataFrame(
429
- label="μž¬λ¬΄μ œν‘œ",
430
- interactive=False,
431
- elem_classes="dl-table",
432
- wrap=True,
433
- )
434
 
435
- # ── Sections ──
436
- gr.Markdown("### πŸ“‹ κ³΅μ‹œ 데이터 (Sections)", elem_classes="dl-section")
437
- gr.HTML('<p class="dl-guide">topic을 μ„ νƒν•˜λ©΄ ν•΄λ‹Ή κ³΅μ‹œ ν•­λͺ©μ˜ 기간별 데이터λ₯Ό 확인할 수 μžˆμŠ΅λ‹ˆλ‹€</p>')
438
- topicSelect = gr.Dropdown(
439
- choices=[],
440
- label="Topic 선택",
441
- interactive=True,
442
- )
443
- sectionTable = gr.DataFrame(
444
- label="Section 데이터",
445
- interactive=False,
446
- elem_classes="dl-table",
447
- wrap=True,
448
- )
449
- sectionText = gr.Markdown("")
450
-
451
- # ── AI Chat (μ ‘νž˜) ──
452
- with gr.Accordion("πŸ€– AI 뢄석 (OpenAI API ν•„μš”)", open=False):
453
- if not _HAS_AI:
454
- gr.Markdown(
455
- "**AI 뢄석을 μ‚¬μš©ν•˜λ €λ©΄** HuggingFace Spaces Settings β†’ "
456
- "Variables and secretsμ—μ„œ `OPENAI_API_KEY`λ₯Ό μ„€μ •ν•˜μ„Έμš”."
 
 
 
 
 
 
 
 
 
 
 
 
457
  )
458
- chatbot = gr.Chatbot(label="AI 뢄석", height=400)
459
- with gr.Row():
460
- chatInput = gr.Textbox(
461
- label="질문",
462
- placeholder="μž¬λ¬΄κ±΄μ „μ„± λΆ„μ„ν•΄μ€˜, λ°°λ‹Ή 이상 μ§•ν›„ μ°Ύμ•„μ€˜ ...",
463
- scale=5,
464
  )
465
- chatBtn = gr.Button("전솑", scale=1, variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
 
467
- chatBtn.click(
468
- handleChat,
469
- inputs=[chatInput, chatbot, codeState],
470
- outputs=[chatbot, chatInput],
471
- )
472
- chatInput.submit(
473
- handleChat,
474
- inputs=[chatInput, chatbot, codeState],
475
- outputs=[chatbot, chatInput],
476
- )
477
 
478
- # ── ν‘Έν„° ──
479
- gr.HTML(f"""
480
- <div class="dl-footer">
481
- <a href="{_BLOG_URL}">πŸ“– 초보자 κ°€μ΄λ“œ</a> Β·
482
- <a href="{_DOCS_URL}">πŸ“˜ 곡식 λ¬Έμ„œ</a> Β·
483
- <a href="{_COLAB_URL}">πŸ”¬ Colab</a> Β·
484
- <a href="{_REPO_URL}">⭐ GitHub</a>
485
- <br><span style="color:#334155; font-size:0.8rem; margin-top:0.5rem; display:inline-block;">
486
- pip install dartlab Β· Python 3.12+
487
- </span>
488
- </div>
489
- """)
490
 
491
- # ── 이벀트 바인딩 ──
492
- analyzeBtn.click(
493
- handleAnalyze,
494
- inputs=queryInput,
495
- outputs=[companyInfo, codeState, financeTable, topicSelect, sectionTable, sectionText],
496
- )
497
- queryInput.submit(
498
- handleAnalyze,
499
- inputs=queryInput,
500
- outputs=[companyInfo, codeState, financeTable, topicSelect, sectionTable, sectionText],
501
- )
502
 
503
- sheetSelect.change(
504
- handleFinance,
505
- inputs=[codeState, sheetSelect],
506
- outputs=financeTable,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
  )
 
 
 
 
 
508
 
509
- topicSelect.change(
510
- handleShow,
511
- inputs=[codeState, topicSelect],
512
- outputs=[sectionTable, sectionText],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
  )
514
 
 
 
 
 
 
 
 
 
515
 
516
- if __name__ == "__main__":
517
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DartLab Streamlit Demo β€” DART/EDGAR κ³΅μ‹œ 뢄석."""
2
 
3
  from __future__ import annotations
4
 
5
  import gc
6
  import os
7
  import re
 
8
  from collections import OrderedDict
9
 
 
10
  import pandas as pd
11
+ import streamlit as st
12
 
13
  import dartlab
14
 
15
  # ── μ„€μ • ──────────────────────────────────────────────
16
 
17
  _MAX_CACHE = 2
 
 
 
 
 
 
18
  _LOGO_URL = "https://raw.githubusercontent.com/eddmpython/dartlab/master/.github/assets/logo.png"
19
  _BLOG_URL = "https://eddmpython.github.io/dartlab/blog/dartlab-easy-start/"
20
  _DOCS_URL = "https://eddmpython.github.io/dartlab/docs/getting-started/quickstart"
21
  _COLAB_URL = "https://colab.research.google.com/github/eddmpython/dartlab/blob/master/notebooks/showcase/01_quickstart.ipynb"
22
  _REPO_URL = "https://github.com/eddmpython/dartlab"
23
 
24
+ # ── νŽ˜μ΄μ§€ μ„€μ • ──────────────────────────────────────
25
 
26
+ st.set_page_config(
27
+ page_title="DartLab β€” μ’…λͺ©μ½”λ“œ ν•˜λ‚˜λ‘œ κΈ°μ—… 뢄석",
28
+ page_icon="πŸ“Š",
29
+ layout="centered",
30
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
+ # ── μ»€μŠ€ν…€ CSS ────────────────────────────────────────
33
 
34
+ st.markdown("""
35
+ <style>
36
+ /* 닀크 λΈŒλžœλ”© */
37
+ [data-testid="stAppViewContainer"] {
38
+ background: #0a0d16;
39
+ }
40
+ [data-testid="stHeader"] {
41
+ background: #0a0d16;
42
+ }
43
+ [data-testid="stSidebar"] {
44
+ background: #0f1219;
45
  }
46
 
47
  /* 헀더 */
48
  .dl-header {
49
  text-align: center;
50
+ padding: 1.5rem 0 1rem;
51
  }
52
  .dl-header img {
 
53
  border-radius: 50%;
54
  box-shadow: 0 0 40px rgba(234,70,71,0.3);
55
  }
56
  .dl-header h1 {
57
+ color: #ea4647;
58
+ font-size: 2.4rem;
59
+ font-weight: 800;
60
+ margin: 0.5rem 0 0.15rem;
61
  letter-spacing: -0.02em;
62
  }
63
  .dl-header .tagline {
64
  color: #94a3b8;
65
  font-size: 1.05rem;
66
+ margin: 0;
67
  }
68
  .dl-header .sub {
69
  color: #64748b;
70
  font-size: 0.85rem;
71
+ margin: 0.2rem 0 0;
72
  }
73
 
74
+ /* μ„Ήμ…˜ 제λͺ© */
75
  .dl-section {
76
+ color: #ea4647;
77
+ font-weight: 700;
78
+ font-size: 1.15rem;
79
+ border-bottom: 2px solid #ea4647;
80
+ padding-bottom: 0.35rem;
81
+ margin: 1.5rem 0 0.8rem;
82
  }
83
 
84
+ /* κΈ°μ—…μΉ΄λ“œ */
85
+ .dl-card {
86
  background: #0f1219;
87
  border: 1px solid #1e2433;
88
+ border-radius: 10px;
89
+ padding: 1.2rem 1.5rem;
90
+ margin: 1rem 0;
91
  }
92
+ .dl-card h2 {
93
+ color: #f1f5f9;
94
+ font-size: 1.4rem;
95
+ margin: 0 0 0.8rem;
96
  }
97
+ .dl-card .meta {
98
+ display: flex;
99
+ gap: 2rem;
100
+ flex-wrap: wrap;
101
  }
102
+ .dl-card .meta-item {
103
+ display: flex;
104
+ flex-direction: column;
 
105
  }
106
+ .dl-card .meta-label {
107
+ color: #64748b;
108
+ font-size: 0.75rem;
109
+ text-transform: uppercase;
110
+ letter-spacing: 0.05em;
 
 
 
111
  }
112
+ .dl-card .meta-value {
113
+ color: #e2e8f0;
114
+ font-size: 1.1rem;
115
+ font-weight: 600;
 
 
116
  }
117
+
118
+ /* μ•ˆλ‚΄ ν…μŠ€νŠΈ */
119
+ .dl-guide {
120
+ color: #64748b;
121
+ font-size: 0.88rem;
122
+ text-align: center;
123
+ margin: 0.2rem 0 0.8rem;
124
  }
125
+
126
+ /* 데이터 ν…Œμ΄λΈ” β€” column_config둜 μ²˜λ¦¬ν•˜λ―€λ‘œ μ΅œμ†Œν•œλ§Œ */
127
+ [data-testid="stDataFrame"] {
128
+ font-variant-numeric: tabular-nums;
129
  }
130
 
131
  /* ν‘Έν„° */
132
  .dl-footer {
133
  text-align: center;
134
  padding: 2rem 0 1rem;
 
 
135
  border-top: 1px solid #1e2433;
136
  margin-top: 2rem;
137
+ color: #475569;
138
+ font-size: 0.85rem;
139
  }
140
  .dl-footer a {
141
+ color: #94a3b8;
142
  text-decoration: none;
143
+ margin: 0 0.5rem;
 
144
  }
145
  .dl-footer a:hover {
146
+ color: #ea4647;
147
  }
148
 
149
+ /* 빈 μƒνƒœ μ•ˆλ‚΄ */
150
+ .dl-empty {
 
 
 
 
 
 
151
  text-align: center;
152
+ color: #475569;
153
+ padding: 2rem;
154
+ font-size: 1rem;
155
  }
156
+ </style>
157
+ """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
 
160
+ # ── μœ ν‹Έ ──────────────────────────────────────────────
 
 
 
 
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
+ def _toPandas(df):
164
+ """Polars/pandas DataFrame β†’ pandas λ³€ν™˜."""
165
+ if df is None:
166
+ return None
167
+ if hasattr(df, "to_pandas"):
168
+ return df.to_pandas()
169
+ return df
170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
+ @st.cache_resource(max_entries=_MAX_CACHE)
173
+ def _getCompany(code: str):
174
+ """μΊμ‹œλœ Company λ°˜ν™˜."""
175
+ gc.collect()
176
+ return dartlab.Company(code)
177
+
178
+
179
+ def _resolveCode(query: str) -> tuple[str | None, str | None]:
180
+ """쿼리 β†’ (μ’…λͺ©μ½”λ“œ, μ—λŸ¬λ©”μ‹œμ§€)."""
181
+ query = query.strip()
182
+ if not query:
183
+ return None, None
184
+
185
+ if re.match(r"^\d{6}$", query) or re.match(r"^[A-Z]{1,5}$", query):
186
+ return query, None
187
+
188
+ try:
189
+ results = dartlab.search(query)
190
+ if results is None or len(results) == 0:
191
+ return None, f"'{query}' 검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€. μ’…λͺ©μ½”λ“œ(예: 005930)λ₯Ό 직접 μž…λ ₯ν•΄λ³΄μ„Έμš”."
192
+ return str(results[0, "stockCode"]), None
193
+ except Exception as e:
194
+ return None, f"검색 μ‹€νŒ¨: {e}"
195
+
196
+
197
+ def _buildColumnConfig(df: pd.DataFrame) -> dict:
198
+ """숫자 μ»¬λŸΌμ— μ²œλ‹¨μœ„ κ΅¬λΆ„μž 포맷 적용."""
199
+ config = {}
200
+ if df is None or df.empty:
201
+ return config
202
+ for col in df.columns:
203
+ if pd.api.types.is_integer_dtype(df[col]):
204
+ config[col] = st.column_config.NumberColumn(
205
+ col, format="%d",
206
  )
207
+ elif pd.api.types.is_float_dtype(df[col]):
208
+ config[col] = st.column_config.NumberColumn(
209
+ col, format="%.2f",
 
 
 
210
  )
211
+ return config
212
+
213
+
214
+ def _showDataFrame(df: pd.DataFrame, key: str = ""):
215
+ """DataFrame을 ν¬λ§·νŒ…ν•΄μ„œ ν‘œμ‹œ."""
216
+ if df is None or df.empty:
217
+ st.markdown('<p class="dl-empty">데이터 μ—†μŒ</p>', unsafe_allow_html=True)
218
+ return
219
+ st.dataframe(
220
+ df,
221
+ column_config=_buildColumnConfig(df),
222
+ use_container_width=True,
223
+ hide_index=True,
224
+ key=key or None,
225
+ )
226
 
 
 
 
 
 
 
 
 
 
 
227
 
228
+ # ── ν”„λ¦¬λ‘œλ“œ ──────────────────────────────────────────
229
+
230
+ @st.cache_resource
231
+ def _warmup():
232
+ """listing μΊμ‹œ μ›Œλ°μ—…."""
233
+ try:
234
+ dartlab.search("μ‚Όμ„±μ „μž")
235
+ except Exception:
236
+ pass
237
+ return True
238
+
239
+ _warmup()
240
 
 
 
 
 
 
 
 
 
 
 
 
241
 
242
+ # ── 헀더 ──────────────────────────────────────────────
243
+
244
+ st.markdown(f"""
245
+ <div class="dl-header">
246
+ <img src="{_LOGO_URL}" width="88" height="88" alt="DartLab">
247
+ <h1>DartLab</h1>
248
+ <p class="tagline">μ’…λͺ©μ½”λ“œ ν•˜λ‚˜. κΈ°μ—…μ˜ 전체 이야기.</p>
249
+ <p class="sub">DART Β· EDGAR κ³΅μ‹œ 데이터λ₯Ό κ΅¬μ‘°ν™”ν•˜μ—¬ μ œκ³΅ν•©λ‹ˆλ‹€</p>
250
+ </div>
251
+ """, unsafe_allow_html=True)
252
+
253
+
254
+ # ── μ’…λͺ© μž…λ ₯ ─────────────────────────────────────────
255
+
256
+ col1, col2 = st.columns([4, 1])
257
+ with col1:
258
+ query = st.text_input(
259
+ "μ’…λͺ©μ½”λ“œ λ˜λŠ” νšŒμ‚¬λͺ…",
260
+ placeholder="005930, μ‚Όμ„±μ „μž, AAPL ...",
261
+ label_visibility="collapsed",
262
  )
263
+ with col2:
264
+ analyzeClicked = st.button("λΆ„μ„ν•˜κΈ°", type="primary", use_container_width=True)
265
+
266
+ st.markdown('<p class="dl-guide">μ’…λͺ©μ½”λ“œ(005930) λ˜λŠ” νšŒμ‚¬λͺ…(μ‚Όμ„±μ „μž)을 μž…λ ₯ν•˜κ³  λΆ„μ„ν•˜κΈ°λ₯Ό ν΄λ¦­ν•˜μ„Έμš”</p>', unsafe_allow_html=True)
267
+
268
 
269
+ # ── 메인 둜직 ─────────────────────────────────────────
270
+
271
+ # μ„Έμ…˜ μƒνƒœ μ΄ˆκΈ°ν™”
272
+ if "code" not in st.session_state:
273
+ st.session_state.code = ""
274
+
275
+ if analyzeClicked and query:
276
+ code, err = _resolveCode(query)
277
+ if err:
278
+ st.error(err)
279
+ elif code:
280
+ st.session_state.code = code
281
+
282
+ code = st.session_state.code
283
+
284
+ if code:
285
+ try:
286
+ c = _getCompany(code)
287
+ except Exception as e:
288
+ st.error(f"κΈ°μ—… λ‘œλ“œ μ‹€νŒ¨: {e}")
289
+ st.stop()
290
+
291
+ # ── κΈ°μ—… μΉ΄λ“œ ──
292
+ currency = ""
293
+ if hasattr(c, "currency") and c.currency:
294
+ currency = c.currency
295
+
296
+ st.markdown(f"""
297
+ <div class="dl-card">
298
+ <h2>🏒 {c.corpName}</h2>
299
+ <div class="meta">
300
+ <div class="meta-item">
301
+ <span class="meta-label">μ’…λͺ©μ½”λ“œ</span>
302
+ <span class="meta-value">{c.stockCode}</span>
303
+ </div>
304
+ <div class="meta-item">
305
+ <span class="meta-label">μ‹œμž₯</span>
306
+ <span class="meta-value">{c.market}</span>
307
+ </div>
308
+ {"<div class='meta-item'><span class='meta-label'>톡화</span><span class='meta-value'>" + currency + "</span></div>" if currency else ""}
309
+ </div>
310
+ </div>
311
+ """, unsafe_allow_html=True)
312
+
313
+ # ── μž¬λ¬΄μ œν‘œ ──
314
+ st.markdown('<div class="dl-section">πŸ“Š μž¬λ¬΄μ œν‘œ</div>', unsafe_allow_html=True)
315
+ st.markdown('<p class="dl-guide">IS(μ†μ΅κ³„μ‚°μ„œ) Β· BS(μž¬λ¬΄μƒνƒœν‘œ) Β· CF(ν˜„κΈˆνλ¦„ν‘œ) Β· ratios(μž¬λ¬΄λΉ„μœ¨)</p>', unsafe_allow_html=True)
316
+
317
+ sheetTab = st.radio(
318
+ "μ‹œνŠΈ 선택",
319
+ ["IS", "BS", "CF", "ratios"],
320
+ horizontal=True,
321
+ label_visibility="collapsed",
322
  )
323
 
324
+ finDf = None
325
+ try:
326
+ raw = getattr(c, sheetTab, None)
327
+ finDf = _toPandas(raw)
328
+ except Exception:
329
+ pass
330
+
331
+ _showDataFrame(finDf, key="finance")
332
 
333
+ # ── Sections ──
334
+ topics = []
335
+ try:
336
+ topics = list(c.topics) if c.topics else []
337
+ except Exception:
338
+ pass
339
+
340
+ if topics:
341
+ st.markdown('<div class="dl-section">πŸ“‹ κ³΅μ‹œ 데이터 (Sections)</div>', unsafe_allow_html=True)
342
+ st.markdown('<p class="dl-guide">topic을 μ„ νƒν•˜λ©΄ ν•΄λ‹Ή κ³΅μ‹œ ν•­λͺ©μ˜ 기간별 데이터λ₯Ό ν‘œμ‹œν•©λ‹ˆλ‹€</p>', unsafe_allow_html=True)
343
+
344
+ selectedTopic = st.selectbox(
345
+ "Topic 선택",
346
+ topics,
347
+ label_visibility="collapsed",
348
+ )
349
+
350
+ if selectedTopic:
351
+ secDf = None
352
+ secText = ""
353
+ try:
354
+ result = c.show(selectedTopic)
355
+ if result is not None:
356
+ if hasattr(result, "to_pandas"):
357
+ secDf = _toPandas(result)
358
+ else:
359
+ secText = str(result)
360
+ except Exception as e:
361
+ secText = f"쑰회 μ‹€νŒ¨: {e}"
362
+
363
+ if secDf is not None:
364
+ _showDataFrame(secDf, key="section")
365
+ elif secText:
366
+ st.markdown(secText)
367
+
368
+ # ── AI Chat ──
369
+ with st.expander("πŸ€– AI 뢄석 (OpenAI API ν•„μš”)", expanded=False):
370
+ hasAi = bool(os.environ.get("OPENAI_API_KEY"))
371
+
372
+ if not hasAi:
373
+ st.info("AI 뢄석을 μ‚¬μš©ν•˜λ €λ©΄ HuggingFace Spaces Settings β†’ Variables and secretsμ—μ„œ `OPENAI_API_KEY`λ₯Ό μ„€μ •ν•˜μ„Έμš”.")
374
+ else:
375
+ if "messages" not in st.session_state:
376
+ st.session_state.messages = []
377
+
378
+ for msg in st.session_state.messages:
379
+ with st.chat_message(msg["role"]):
380
+ st.markdown(msg["content"])
381
+
382
+ if prompt := st.chat_input("μž¬λ¬΄κ±΄μ „μ„± λΆ„μ„ν•΄μ€˜, λ°°λ‹Ή 이상 μ§•ν›„ μ°Ύμ•„μ€˜ ..."):
383
+ st.session_state.messages.append({"role": "user", "content": prompt})
384
+ with st.chat_message("user"):
385
+ st.markdown(prompt)
386
+
387
+ with st.chat_message("assistant"):
388
+ try:
389
+ q = f"{code} {prompt}"
390
+ answer = dartlab.ask(q, stream=False, raw=False)
391
+ st.markdown(answer or "응닡 μ—†μŒ")
392
+ st.session_state.messages.append({"role": "assistant", "content": answer or "응닡 μ—†μŒ"})
393
+ except Exception as e:
394
+ errMsg = f"뢄석 μ‹€νŒ¨: {e}"
395
+ st.markdown(errMsg)
396
+ st.session_state.messages.append({"role": "assistant", "content": errMsg})
397
+
398
+ else:
399
+ # λ―Έμž…λ ₯ μƒνƒœ
400
+ st.markdown("""
401
+ <div class="dl-empty">
402
+ <p style="font-size: 1.2rem; color: #94a3b8;">μ’…λͺ©μ½”λ“œλ₯Ό μž…λ ₯ν•˜κ³  <strong>λΆ„μ„ν•˜κΈ°</strong>λ₯Ό ν΄λ¦­ν•˜μ„Έμš”</p>
403
+ <p style="color: #475569; margin-top: 0.5rem;">
404
+ μ˜ˆμ‹œ: <code>005930</code> (μ‚Όμ„±μ „μž) Β· <code>000660</code> (SKν•˜μ΄λ‹‰μŠ€) Β· <code>AAPL</code> (Apple)
405
+ </p>
406
+ </div>
407
+ """, unsafe_allow_html=True)
408
+
409
+
410
+ # ── ν‘Έν„° ──────────────────────────────────────────────
411
+
412
+ st.markdown(f"""
413
+ <div class="dl-footer">
414
+ <a href="{_BLOG_URL}">πŸ“– 초보자 κ°€μ΄λ“œ</a> Β·
415
+ <a href="{_DOCS_URL}">πŸ“˜ 곡식 λ¬Έμ„œ</a> Β·
416
+ <a href="{_COLAB_URL}">πŸ”¬ Colab</a> Β·
417
+ <a href="{_REPO_URL}">⭐ GitHub</a>
418
+ <br><span style="color:#334155; font-size:0.8rem; margin-top:0.5rem; display:inline-block;">
419
+ pip install dartlab Β· Python 3.12+
420
+ </span>
421
+ </div>
422
+ """, unsafe_allow_html=True)
requirements.txt CHANGED
@@ -1,2 +1,2 @@
1
  dartlab>=0.7.8
2
- gradio>=6.0,<7
 
1
  dartlab>=0.7.8
2
+ streamlit>=1.45,<2