feat: 대시보드 무의미한 지표 제거 및 기업/기술 실제 개요 정의 리스트 제공
Browse files
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 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 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(
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
-
# 3. 최근 기사 목록 조회 (최근
|
| 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
|
| 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. 기업
|
| 204 |
-
|
| 205 |
-
for c in stats.get("companies_list", [])
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
|
| 217 |
# 3. 최근 기사 리스트 HTML 생성 (최대 3개)
|
| 218 |
news_list_html: str = ""
|
| 219 |
-
for a in stats.get("recent_articles", [])
|
| 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="
|
| 261 |
-
{
|
| 262 |
</div>
|
| 263 |
|
| 264 |
-
<div class="section-subtitle">💡 주요 핵심 기술</div>
|
| 265 |
-
<div class="
|
| 266 |
-
{
|
| 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(
|
| 315 |
-
gap:
|
| 316 |
margin-bottom: 12px;
|
| 317 |
}
|
| 318 |
.stat-card {
|
| 319 |
background: white;
|
| 320 |
border: 1px solid #e2e8f0;
|
| 321 |
border-radius: 6px;
|
| 322 |
-
padding: 6px
|
| 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:
|
| 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:
|
| 347 |
color: #64748b;
|
| 348 |
font-weight: 500;
|
| 349 |
}
|
|
@@ -351,55 +348,42 @@ custom_css: str = """
|
|
| 351 |
color: #94a3b8;
|
| 352 |
}
|
| 353 |
|
| 354 |
-
/*
|
| 355 |
-
.
|
| 356 |
display: flex;
|
| 357 |
-
flex-
|
| 358 |
gap: 4px;
|
| 359 |
margin-bottom: 10px;
|
| 360 |
}
|
| 361 |
-
.
|
| 362 |
-
background-color:
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 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 .
|
| 382 |
-
background-color: #
|
| 383 |
-
color: #
|
| 384 |
}
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
color: #
|
| 389 |
-
border-color: #a7f3d0;
|
| 390 |
}
|
| 391 |
-
.
|
| 392 |
-
|
| 393 |
-
color: white;
|
| 394 |
}
|
| 395 |
-
.
|
| 396 |
-
|
| 397 |
-
color: #
|
| 398 |
-
|
| 399 |
}
|
| 400 |
-
.dark .
|
| 401 |
-
|
| 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 |
/* 최근 뉴스 타임라인 및 스크롤바 스타일 */
|