dev-yuje commited on
Commit
38d9fac
·
1 Parent(s): 1570976

feat: 대시보드 무의미한 지표 제거 및 기업/기술 실제 개요 정의 리스트 제공

Browse files
Files changed (1) hide show
  1. app.py +79 -95
app.py CHANGED
@@ -134,15 +134,12 @@ def get_db_stats() -> Dict[str, Any]:
134
  """Neo4j 데이터베이스로부터 실시간 지식 그래프 통계 및 요약을 안전하게 조회합니다.
135
 
136
  Returns:
137
- Dict[str, Any]: 기사 건수, 기업 수, 기술 수, 서비스 수, 분야 수, 벡터 청크 수 및 세부 목록
138
  """
139
  stats: Dict[str, Any] = {
140
  "articles": 0,
141
  "companies": 0,
142
  "technologies": 0,
143
- "services": 0,
144
- "fields": 0,
145
- "chunks": 0,
146
  "companies_list": [],
147
  "techs_list": [],
148
  "recent_articles": [],
@@ -164,30 +161,24 @@ def get_db_stats() -> Dict[str, Any]:
164
  if res_techs:
165
  stats["technologies"] = res_techs["cnt"]
166
 
167
- res_services = session.run("MATCH (s:AIService) RETURN count(s) as cnt").single()
168
- if res_services:
169
- stats["services"] = res_services["cnt"]
170
-
171
- res_fields = session.run("MATCH (f:AIField) RETURN count(f) as cnt").single()
172
- if res_fields:
173
- stats["fields"] = res_fields["cnt"]
174
-
175
- res_chunks = session.run("MATCH (c:Content) RETURN count(c) as cnt").single()
176
- if res_chunks:
177
- stats["chunks"] = res_chunks["cnt"]
178
-
179
- # 2. 기업 및 기술 목록 조회 (상위 15개)
180
- res_comp_list = session.run("MATCH (c:AICompany) RETURN c.name as name LIMIT 15")
181
- stats["companies_list"] = [r["name"] for r in res_comp_list]
182
 
183
- res_tech_list = session.run("MATCH (t:AITechnology) RETURN t.name as name LIMIT 15")
184
- stats["techs_list"] = [r["name"] for r in res_tech_list]
 
 
 
185
 
186
- # 3. 최근 기사 목록 조회 (최근 5개)
187
  res_art_list = session.run(
188
  "MATCH (a:Article) "
189
  "RETURN a.title as title, a.published_date as date, a.url as url "
190
- "ORDER BY a.published_date DESC LIMIT 5"
191
  )
192
  stats["recent_articles"] = [
193
  {"title": r["title"], "date": r["date"], "url": r["url"]}
@@ -200,23 +191,33 @@ def get_db_stats() -> Dict[str, Any]:
200
 
201
  def build_stats_html(stats: Dict[str, Any]) -> str:
202
  """조회된 지식 그래프 통계 정보들을 바탕으로 미려하고 컴팩트한 대시보드용 HTML을 생성합니다."""
203
- # 1. 기업 뱃지 HTML 생성 (상위 최대 8개)
204
- comp_badges: str = ""
205
- for c in stats.get("companies_list", [])[:8]:
206
- comp_badges += f'<span class="badge-item">{c}</span>'
207
- if not comp_badges:
208
- comp_badges = '<span style="font-size:10px; color:#94a3b8;">등록된 기업이 없습니다.</span>'
209
-
210
- # 2. 기술 뱃지 HTML 생성 (상위 최대 8개)
211
- tech_badges: str = ""
212
- for t in stats.get("techs_list", [])[:8]:
213
- tech_badges += f'<span class="badge-item tech-badge">{t}</span>'
214
- if not tech_badges:
215
- tech_badges = '<span style="font-size:10px; color:#94a3b8;">등록된 기술 없습니다.</span>'
 
 
 
 
 
 
 
 
 
 
216
 
217
  # 3. 최근 기사 리스트 HTML 생성 (최대 3개)
218
  news_list_html: str = ""
219
- for a in stats.get("recent_articles", [])[:3]:
220
  title = a["title"]
221
  url = a["url"] if a["url"] and str(a["url"]).lower() != "nan" else "#"
222
  target = 'target="_blank"' if url != "#" else ""
@@ -239,31 +240,27 @@ def build_stats_html(stats: Dict[str, Any]) -> str:
239
 
240
  <div class="stats-grid">
241
  <div class="stat-card">
242
- <div class="stat-lbl">📰 뉴스</div>
243
  <div class="stat-val">{stats['articles']}건</div>
244
  </div>
245
  <div class="stat-card">
246
- <div class="stat-lbl">🏢 기업</div>
247
  <div class="stat-val">{stats['companies']}개</div>
248
  </div>
249
  <div class="stat-card">
250
- <div class="stat-lbl">💡 기술</div>
251
  <div class="stat-val">{stats['technologies']}개</div>
252
  </div>
253
- <div class="stat-card">
254
- <div class="stat-lbl">🧩 벡터 Chunks</div>
255
- <div class="stat-val" style="color: #059669;">{stats['chunks']}개</div>
256
- </div>
257
  </div>
258
 
259
- <div class="section-subtitle">🏢 주요 분석 기업</div>
260
- <div class="badge-container">
261
- {comp_badges}
262
  </div>
263
 
264
- <div class="section-subtitle">💡 주요 핵심 기술</div>
265
- <div class="badge-container">
266
- {tech_badges}
267
  </div>
268
 
269
  <div class="section-subtitle">📰 최신 뉴스 피드</div>
@@ -311,15 +308,15 @@ custom_css: str = """
311
  /* 통계 그리드 및 카드 */
312
  .stats-grid {
313
  display: grid;
314
- grid-template-columns: repeat(2, 1fr);
315
- gap: 8px;
316
  margin-bottom: 12px;
317
  }
318
  .stat-card {
319
  background: white;
320
  border: 1px solid #e2e8f0;
321
  border-radius: 6px;
322
- padding: 6px 8px;
323
  text-align: center;
324
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
325
  transition: all 0.2s ease-in-out;
@@ -334,7 +331,7 @@ custom_css: str = """
334
  color: #f1f5f9;
335
  }
336
  .stat-val {
337
- font-size: 14px;
338
  font-weight: 800;
339
  color: #4f46e5;
340
  margin-top: 2px;
@@ -343,7 +340,7 @@ custom_css: str = """
343
  color: #818cf8;
344
  }
345
  .stat-lbl {
346
- font-size: 10px;
347
  color: #64748b;
348
  font-weight: 500;
349
  }
@@ -351,55 +348,42 @@ custom_css: str = """
351
  color: #94a3b8;
352
  }
353
 
354
- /* 뱃지 리스트 스타일 */
355
- .badge-container {
356
  display: flex;
357
- flex-wrap: wrap;
358
  gap: 4px;
359
  margin-bottom: 10px;
360
  }
361
- .badge-item {
362
- background-color: #e0e7ff;
363
- color: #3730a3;
364
- font-size: 10px;
365
- font-weight: 600;
366
- padding: 2px 8px;
367
- border-radius: 12px;
368
- border: 1px solid #c7d2fe;
369
- transition: all 0.2s;
370
- }
371
- .badge-item:hover {
372
- background-color: #4f46e5;
373
- color: white;
374
- transform: scale(1.05);
375
- }
376
- .dark .badge-item {
377
- background-color: #1e1b4b;
378
- color: #c7d2fe;
379
- border-color: #312e81;
380
  }
381
- .dark .badge-item:hover {
382
- background-color: #818cf8;
383
- color: #0f172a;
384
  }
385
-
386
- .tech-badge {
387
- background-color: #ecfdf5;
388
- color: #065f46;
389
- border-color: #a7f3d0;
390
  }
391
- .tech-badge:hover {
392
- background-color: #10b981;
393
- color: white;
394
  }
395
- .dark .tech-badge {
396
- background-color: #064e3b;
397
- color: #a7f3d0;
398
- border-color: #065f46;
399
  }
400
- .dark .tech-badge:hover {
401
- background-color: #34d399;
402
- color: #064e3b;
403
  }
404
 
405
  /* 최근 뉴스 타임라인 및 스크롤바 스타일 */
 
134
  """Neo4j 데이터베이스로부터 실시간 지식 그래프 통계 및 요약을 안전하게 조회합니다.
135
 
136
  Returns:
137
+ Dict[str, Any]: 기사 건수, 기업 수, 기술 수, 세부 설명 목록
138
  """
139
  stats: Dict[str, Any] = {
140
  "articles": 0,
141
  "companies": 0,
142
  "technologies": 0,
 
 
 
143
  "companies_list": [],
144
  "techs_list": [],
145
  "recent_articles": [],
 
161
  if res_techs:
162
  stats["technologies"] = res_techs["cnt"]
163
 
164
+ # 2. 기업 기술 목록 & 설명 조회 (상위 5개)
165
+ res_comp_list = session.run(
166
+ "MATCH (c:AICompany) "
167
+ "RETURN c.name as name, COALESCE(c.description, '최근 주목받는 AI 핵심 기업') as desc LIMIT 5"
168
+ )
169
+ stats["companies_list"] = [{"name": r["name"], "desc": r["desc"]} for r in res_comp_list]
 
 
 
 
 
 
 
 
 
170
 
171
+ res_tech_list = session.run(
172
+ "MATCH (t:AITechnology) "
173
+ "RETURN t.name as name, COALESCE(t.description, 'AI 혁신 기술 인프라') as desc LIMIT 5"
174
+ )
175
+ stats["techs_list"] = [{"name": r["name"], "desc": r["desc"]} for r in res_tech_list]
176
 
177
+ # 3. 최근 기사 목록 조회 (최근 3개)
178
  res_art_list = session.run(
179
  "MATCH (a:Article) "
180
  "RETURN a.title as title, a.published_date as date, a.url as url "
181
+ "ORDER BY a.published_date DESC LIMIT 3"
182
  )
183
  stats["recent_articles"] = [
184
  {"title": r["title"], "date": r["date"], "url": r["url"]}
 
191
 
192
  def build_stats_html(stats: Dict[str, Any]) -> str:
193
  """조회된 지식 그래프 통계 정보들을 바탕으로 미려하고 컴팩트한 대시보드용 HTML을 생성합니다."""
194
+ # 1. 기업 리스트 HTML 생성
195
+ comp_html: str = ""
196
+ for c in stats.get("companies_list", []):
197
+ comp_html += f"""
198
+ <div class="definition-item">
199
+ <span class="definition-name">🏢 {c['name']}</span>
200
+ <span class="definition-desc">{c['desc']}</span>
201
+ </div>
202
+ """
203
+ if not comp_html:
204
+ comp_html = '<div style="font-size:10px; color:#94a3b8;">등록된 기업이 없습니다.</div>'
205
+
206
+ # 2. 기술 리스트 HTML 생성
207
+ tech_html: str = ""
208
+ for t in stats.get("techs_list", []):
209
+ tech_html += f"""
210
+ <div class="definition-item">
211
+ <span class="definition-name" style="color: #059669;">💡 {t['name']}</span>
212
+ <span class="definition-desc">{t['desc']}</span>
213
+ </div>
214
+ """
215
+ if not tech_html:
216
+ tech_html = '<div style="font-size:10px; color:#94a3b8;">등록된 기술이 없습니다.</div>'
217
 
218
  # 3. 최근 기사 리스트 HTML 생성 (최대 3개)
219
  news_list_html: str = ""
220
+ for a in stats.get("recent_articles", []):
221
  title = a["title"]
222
  url = a["url"] if a["url"] and str(a["url"]).lower() != "nan" else "#"
223
  target = 'target="_blank"' if url != "#" else ""
 
240
 
241
  <div class="stats-grid">
242
  <div class="stat-card">
243
+ <div class="stat-lbl">📰 뉴스 기사</div>
244
  <div class="stat-val">{stats['articles']}건</div>
245
  </div>
246
  <div class="stat-card">
247
+ <div class="stat-lbl">🏢 핵심 기업</div>
248
  <div class="stat-val">{stats['companies']}개</div>
249
  </div>
250
  <div class="stat-card">
251
+ <div class="stat-lbl">💡 주요 기술</div>
252
  <div class="stat-val">{stats['technologies']}개</div>
253
  </div>
 
 
 
 
254
  </div>
255
 
256
+ <div class="section-subtitle">🏢 주요 분석 기업 및 개요</div>
257
+ <div class="definition-list">
258
+ {comp_html}
259
  </div>
260
 
261
+ <div class="section-subtitle">💡 주요 핵심 기술 및 정의</div>
262
+ <div class="definition-list">
263
+ {tech_html}
264
  </div>
265
 
266
  <div class="section-subtitle">📰 최신 뉴스 피드</div>
 
308
  /* 통계 그리드 및 카드 */
309
  .stats-grid {
310
  display: grid;
311
+ grid-template-columns: repeat(3, 1fr);
312
+ gap: 6px;
313
  margin-bottom: 12px;
314
  }
315
  .stat-card {
316
  background: white;
317
  border: 1px solid #e2e8f0;
318
  border-radius: 6px;
319
+ padding: 6px 4px;
320
  text-align: center;
321
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
322
  transition: all 0.2s ease-in-out;
 
331
  color: #f1f5f9;
332
  }
333
  .stat-val {
334
+ font-size: 13px;
335
  font-weight: 800;
336
  color: #4f46e5;
337
  margin-top: 2px;
 
340
  color: #818cf8;
341
  }
342
  .stat-lbl {
343
+ font-size: 9px;
344
  color: #64748b;
345
  font-weight: 500;
346
  }
 
348
  color: #94a3b8;
349
  }
350
 
351
+ /* 주요 기업 및 기술 정의 리스트 스타일 */
352
+ .definition-list {
353
  display: flex;
354
+ flex-direction: column;
355
  gap: 4px;
356
  margin-bottom: 10px;
357
  }
358
+ .definition-item {
359
+ background-color: white;
360
+ border: 1px solid #e2e8f0;
361
+ border-radius: 5px;
362
+ padding: 4px 6px;
363
+ display: flex;
364
+ flex-direction: column;
365
+ gap: 1px;
366
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.01);
 
 
 
 
 
 
 
 
 
 
367
  }
368
+ .dark .definition-item {
369
+ background-color: #1e293b;
370
+ border-color: #334155;
371
  }
372
+ .definition-name {
373
+ font-size: 10px;
374
+ font-weight: 700;
375
+ color: #4f46e5;
 
376
  }
377
+ .dark .definition-name {
378
+ color: #818cf8;
 
379
  }
380
+ .definition-desc {
381
+ font-size: 9px;
382
+ color: #475569;
383
+ line-height: 1.3;
384
  }
385
+ .dark .definition-desc {
386
+ color: #cbd5e1;
 
387
  }
388
 
389
  /* 최근 뉴스 타임라인 및 스크롤바 스타일 */