"""
๐ฎ OKCEO ์ ์ฑ
์๊ธ ์ฌ์ ์ฌ์ฌ ์์คํ
v7.0 - ๋ฏธ๋ค๋ํต ULTRA ์๋์
================================================================================
๐ ๋น์ฃผ์ผ ์ฐจํธ 18๊ฐ (๊ธฐ์กด 6๊ฐ + ์ ๊ท 12๊ฐ)
[๊ธฐ์กด ์ฐจํธ - ๊ฐ์ ]
1. ์
์ข
๋ณ ์ฌ๋ฌด๋น์จ ๋ฒค์น๋งํฌ ํ
์ด๋ธ
2. ์ ์์ฌํญ ์ฒดํฌ๋ฆฌ์คํธ ๋์๋ณด๋
3. ์ง์๊ฐ๋ฅ์ฑ ๋งคํธ๋ฆญ์ค
4. ์์๊ธ์ก ์ํฐํด ์ฐจํธ
5. ํ๋ก์ธ์ค ์งํ ํ์๋ผ์ธ
6. ์ ์ฉ์ ์ ์๋ฎฌ๋ ์ด์
[์ ๊ท ์ฐจํธ - ๊ณ ๊ธ ๋น์ฃผ์ผ] โญ
7. ์ข
ํฉ์ ์ 3D ๊ฒ์ด์ง (์ ๋๋ฉ์ด์
)
8. ์ฌ๋ฌด๊ฑด์ ์ฑ ๋ ์ด๋ ์ฐจํธ (SVG ์ ๋๋ฉ์ด์
)
9. ๊ธฐ๊ด๋ณ ์น์ธํ๋ฅ ๋๋ ์ฐจํธ (์ธํฐ๋ํฐ๋ธ)
10. ์๊ธ์กฐ๋ฌ ํ์ดํ๋ผ์ธ ํ๋ก์ฐ
11. ๋ฆฌ์คํฌ ํํธ๋งต (์นดํ
๊ณ ๋ฆฌ๋ณ)
12. ๊ฒฝ์๋ ฅ ๋ฒค์น๋งํฌ ๋ฐ ์ฐจํธ
13. ๐ ์ค์ฝ์ด์นด๋ ๋์๋ณด๋ (KPI)
14. ๐ ์๊ธํ๋ฆ ์ฐํค ๋ค์ด์ด๊ทธ๋จ
15. ๐ ์๋ณ ์ถ์ธ ๋ผ์ธ ์ฐจํธ
16. ๐ ๊ธฐ๊ด๋น๊ต ๋ ์ด๋ ์ค๋ฒ๋ ์ด
17. ๐ ์ฑ๊ณต/์คํจ ์์ธ ํธ๋ฆฌ๋งต
18. ๐ ์ข
ํฉ ์ธํฌ๊ทธ๋ํฝ ๋ณด๋
================================================================================
"""
import gradio as gr
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any, Tuple
from datetime import datetime, date
import json
import math
# ============================================================================
# cache_db ์ฐ๋ (app.py ํตํฉ ์ DB ์ ์ฅ/๋ถ๋ฌ์ค๊ธฐ)
# ============================================================================
HAS_CACHE_DB = False
_fund_cache = None
def _get_fund_cache():
"""์ง์ฐ ์ด๊ธฐํ - ์ค์ ์ฌ์ฉ ์์ ์ cache_db ๋ก๋"""
global HAS_CACHE_DB, _fund_cache
if _fund_cache is not None:
return _fund_cache
try:
from cache_db import get_fund_cache
_fund_cache = get_fund_cache()
HAS_CACHE_DB = True
return _fund_cache
except ImportError:
HAS_CACHE_DB = False
_fund_cache = None
return None
# ============================================================================
# CUSTOM CSS - ์ธํธ๋ผ ๋คํฌ ๋ฉํ๋ฆญ + ๋ค์จ ๊ธ๋ก์ฐ ํ
๋ง
# ============================================================================
CUSTOM_CSS = """
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&family=Orbitron:wght@400;700;900&family=Rajdhani:wght@500;700&display=swap');
header,.huggingface-space-header,footer{display:none!important}
:root {
--neon-green: #6fd9a8;
--neon-blue: #6495ed;
--neon-purple: #9b59b6;
--neon-orange: #ff9500;
--neon-red: #ff6b6b;
--neon-yellow: #ffd93d;
--neon-cyan: #00d4ff;
--bg-dark: #0d0d1a;
--bg-card: #1a1a2e;
--bg-panel: #252540;
--text-primary: #e8e8f0;
--text-secondary: #8888a0;
--glow-green: rgba(111,217,168,0.5);
--glow-blue: rgba(100,149,237,0.5);
}
html,body{background:var(--bg-dark)!important;color:var(--text-primary)!important;}
.gradio-container{
font-family:'Noto Sans KR','Rajdhani',sans-serif!important;
background:linear-gradient(135deg,#0d1117 0%,#161b22 50%,#0d1117 100%)!important;
max-width:100%!important;padding:20px!important;min-height:100vh;
}
/* ============ ๋ค์จ ๊ธ๋ก์ฐ ์ ๋๋ฉ์ด์
============ */
@keyframes neon-pulse {
0%, 100% {
box-shadow: 0 0 5px var(--neon-green), 0 0 10px var(--neon-green), 0 0 20px var(--glow-green);
border-color: var(--neon-green);
}
50% {
box-shadow: 0 0 10px var(--neon-green), 0 0 20px var(--neon-green), 0 0 40px var(--glow-green);
border-color: #8fe8c0;
}
}
@keyframes rotate-3d {
0% { transform: perspective(500px) rotateY(0deg); }
100% { transform: perspective(500px) rotateY(360deg); }
}
@keyframes fill-bar {
from { width: 0%; }
}
@keyframes count-up {
from { opacity: 0; transform: scale(0.5) translateY(20px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
@keyframes slide-up {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes shimmer {
0% { background-position: -200% center; }
100% { background-position: 200% center; }
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@keyframes radar-scan {
0% { transform: rotate(0deg); opacity: 0.8; }
100% { transform: rotate(360deg); opacity: 0.8; }
}
@keyframes pulse-ring {
0% { transform: scale(0.8); opacity: 1; }
100% { transform: scale(1.5); opacity: 0; }
}
@keyframes glow-text {
0%, 100% { text-shadow: 0 0 10px var(--neon-green), 0 0 20px var(--neon-green); }
50% { text-shadow: 0 0 20px var(--neon-green), 0 0 40px var(--neon-green), 0 0 60px var(--neon-green); }
}
@keyframes border-flow {
0% { border-color: var(--neon-green); }
33% { border-color: var(--neon-blue); }
66% { border-color: var(--neon-purple); }
100% { border-color: var(--neon-green); }
}
/* ============ ์ฐจํธ ์ปจํ
์ด๋ ์คํ์ผ ============ */
.chart-ultra {
background: linear-gradient(145deg, #1a1a2e 0%, #0d0d1a 100%)!important;
border-radius: 20px!important;
padding: 28px!important;
margin: 16px 0!important;
border: 1px solid rgba(111,217,168,0.2)!important;
box-shadow: 0 10px 40px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.05)!important;
position: relative;
overflow: hidden;
}
.chart-ultra::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, var(--neon-green), transparent);
}
.chart-ultra:hover {
border-color: rgba(111,217,168,0.4)!important;
box-shadow: 0 15px 50px rgba(0,0,0,0.6), 0 0 30px rgba(111,217,168,0.1)!important;
}
/* ============ ์ค์ฝ์ด์นด๋ ์คํ์ผ ============ */
.score-card {
background: linear-gradient(145deg, #252540, #1a1a2e);
border-radius: 16px;
padding: 24px;
text-align: center;
border: 1px solid rgba(255,255,255,0.1);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.score-card::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.03), transparent);
transform: rotate(45deg);
transition: all 0.5s;
}
.score-card:hover {
transform: translateY(-8px) scale(1.02);
border-color: var(--neon-green);
box-shadow: 0 20px 40px rgba(0,0,0,0.4), 0 0 30px rgba(111,217,168,0.2);
}
.score-card:hover::after {
left: 100%;
}
.score-card.highlight {
animation: neon-pulse 2s ease-in-out infinite;
}
.score-card .value {
font-family: 'Orbitron', sans-serif;
font-size: 42px;
font-weight: 900;
background: linear-gradient(135deg, #fff, var(--neon-green));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: count-up 0.8s ease-out;
}
.score-card .label {
color: var(--text-secondary);
font-size: 13px;
margin-top: 8px;
text-transform: uppercase;
letter-spacing: 1px;
}
/* ============ ํ๋ก๊ทธ๋ ์ค ๋ฐ ============ */
.progress-ultra {
height: 12px;
background: #1a1a2e;
border-radius: 6px;
overflow: hidden;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.5);
}
.progress-ultra .fill {
height: 100%;
border-radius: 6px;
background: linear-gradient(90deg, var(--neon-green), #8fe8c0, var(--neon-green));
background-size: 200% 100%;
animation: shimmer 2s linear infinite, fill-bar 1s ease-out;
box-shadow: 0 0 10px var(--glow-green);
}
/* ============ ๋ฒํผ ์คํ์ผ ============ */
button,.gr-button{
background: linear-gradient(145deg, #2a2a45, #1a1a30)!important;
color:var(--text-primary)!important;
border:1px solid rgba(111,217,168,0.3)!important;
border-radius:12px!important;
font-weight:600!important;
transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1)!important;
}
button:hover,.gr-button:hover{
background: linear-gradient(145deg, #3a3a55, #2a2a40)!important;
transform:translateY(-3px)!important;
box-shadow:0 10px 30px rgba(0,0,0,0.4), 0 0 20px rgba(111,217,168,0.2)!important;
border-color: var(--neon-green)!important;
}
.analyze-btn button{
background:linear-gradient(135deg,#1a6b4a,#3a8f6a,#6fd9a8)!important;
color:#fff!important;
font-size:18px!important;
padding:18px 50px!important;
border:2px solid var(--neon-green)!important;
text-shadow: 0 0 10px rgba(0,0,0,0.5);
}
.analyze-btn button:hover{
box-shadow:0 10px 40px rgba(111,217,168,0.4), 0 0 60px rgba(111,217,168,0.2)!important;
}
/* ============ ์
๋ ฅ ํ๋ ============ */
input,textarea,select{
background:#1a1a2e!important;
color:var(--text-primary)!important;
border:1px solid rgba(111,217,168,0.2)!important;
border-radius:10px!important;
transition:all 0.3s ease!important;
}
input:focus,textarea:focus,select:focus{
border-color:var(--neon-green)!important;
box-shadow:0 0 20px rgba(111,217,168,0.2)!important;
outline:none!important;
}
/* ============ ํญ ์คํ์ผ ============ */
[role="tablist"]{
background:transparent!important;
border-bottom:1px solid rgba(111,217,168,0.2)!important;
}
[role="tab"]{
background:transparent!important;
color:var(--text-secondary)!important;
border:none!important;
padding:12px 20px!important;
transition:all 0.3s ease!important;
}
[role="tab"][aria-selected="true"]{
color:var(--neon-green)!important;
background:rgba(111,217,168,0.1)!important;
border-bottom:3px solid var(--neon-green)!important;
}
[role="tab"]:hover{
color:var(--neon-green)!important;
background:rgba(111,217,168,0.05)!important;
}
/* ============ ํ
์ด๋ธ ============ */
table{border-collapse:separate;border-spacing:0;width:100%;}
th{background:#252540!important;color:var(--neon-green)!important;padding:14px!important;
border-bottom:2px solid var(--neon-green)!important;font-weight:600;text-align:left;}
td{background:#1a1a2e!important;padding:12px 14px!important;
border-bottom:1px solid rgba(255,255,255,0.05)!important;}
tr:hover td{background:#252540!important;}
/* ============ ํ๋ ์คํฌ๋กค๋ฐ ============ */
::-webkit-scrollbar{width:8px;height:8px;}
::-webkit-scrollbar-track{background:#0d0d1a;}
::-webkit-scrollbar-thumb{background:var(--neon-green);border-radius:4px;}
::-webkit-scrollbar-thumb:hover{background:#8fe8c0;}
"""
# ============================================================================
# ๋ฐ์ดํฐ ์์
# ============================================================================
INDUSTRY_FINANCIAL_RATIOS = {
"A01 ๋์
": {"code": "A01", "์ด์์ฐ์ฆ๊ฐ์จ": 9.01, "๋งค์ถ์ก์ฆ๊ฐ์จ": 12.31, "๋งค์ถ์ก์์
์ด์ต๋ฅ ": 2.14, "์ ๋๋น์จ": 104.53, "๋ถ์ฑ๋น์จ": 165.15, "์ ํ๋ถ์ฑ๋น์จ": 500, "์ด์๋ณด์๋น์จ": 104.69},
"A03 ์ด์
": {"code": "A03", "์ด์์ฐ์ฆ๊ฐ์จ": 45.04, "๋งค์ถ์ก์ฆ๊ฐ์จ": 21.6, "๋งค์ถ์ก์์
์ด์ต๋ฅ ": 8.38, "์ ๋๋น์จ": 108.07, "๋ถ์ฑ๋น์จ": 102.09, "์ ํ๋ถ์ฑ๋น์จ": 500, "์ด์๋ณด์๋น์จ": 499.98},
"B ๊ด์
": {"code": "B", "์ด์์ฐ์ฆ๊ฐ์จ": -0.08, "๋งค์ถ์ก์ฆ๊ฐ์จ": 13.75, "๋งค์ถ์ก์์
์ด์ต๋ฅ ": 6.42, "์ ๋๋น์จ": 55.56, "๋ถ์ฑ๋น์จ": -1096.54, "์ ํ๋ถ์ฑ๋น์จ": 500, "์ด์๋ณด์๋น์จ": 38.78},
"C ์ ์กฐ์
": {"code": "C", "์ด์์ฐ์ฆ๊ฐ์จ": 7.54, "๋งค์ถ์ก์ฆ๊ฐ์จ": 14.63, "๋งค์ถ์ก์์
์ด์ต๋ฅ ": 5.72, "์ ๋๋น์จ": 141.51, "๋ถ์ฑ๋น์จ": 76.95, "์ ํ๋ถ์ฑ๋น์จ": 365.7, "์ด์๋ณด์๋น์จ": 693.43},
"D ์ ๊ธฐ๊ฐ์ค": {"code": "D", "์ด์์ฐ์ฆ๊ฐ์จ": 8.5, "๋งค์ถ์ก์ฆ๊ฐ์จ": 15.2, "๋งค์ถ์ก์์
์ด์ต๋ฅ ": 4.8, "์ ๋๋น์จ": 95.0, "๋ถ์ฑ๋น์จ": 180.0, "์ ํ๋ถ์ฑ๋น์จ": 400, "์ด์๋ณด์๋น์จ": 250.0},
"F ๊ฑด์ค์
": {"code": "F", "์ด์์ฐ์ฆ๊ฐ์จ": 5.2, "๋งค์ถ์ก์ฆ๊ฐ์จ": 8.5, "๋งค์ถ์ก์์
์ด์ต๋ฅ ": 3.5, "์ ๋๋น์จ": 125.0, "๋ถ์ฑ๋น์จ": 220.0, "์ ํ๋ถ์ฑ๋น์จ": 450, "์ด์๋ณด์๋น์จ": 180.0},
"G ๋์๋งค์
": {"code": "G", "์ด์์ฐ์ฆ๊ฐ์จ": 6.8, "๋งค์ถ์ก์ฆ๊ฐ์จ": 10.5, "๋งค์ถ์ก์์
์ด์ต๋ฅ ": 2.8, "์ ๋๋น์จ": 115.0, "๋ถ์ฑ๋น์จ": 145.0, "์ ํ๋ถ์ฑ๋น์จ": 400, "์ด์๋ณด์๋น์จ": 220.0},
"H ์ด์์ฐฝ๊ณ ์
": {"code": "H", "์ด์์ฐ์ฆ๊ฐ์จ": 4.5, "๋งค์ถ์ก์ฆ๊ฐ์จ": 7.8, "๋งค์ถ์ก์์
์ด์ต๋ฅ ": 4.2, "์ ๋๋น์จ": 98.0, "๋ถ์ฑ๋น์จ": 195.0, "์ ํ๋ถ์ฑ๋น์จ": 420, "์ด์๋ณด์๋น์จ": 165.0},
"I ์๋ฐ์์์
": {"code": "I", "์ด์์ฐ์ฆ๊ฐ์จ": 3.2, "๋งค์ถ์ก์ฆ๊ฐ์จ": 5.5, "๋งค์ถ์ก์์
์ด์ต๋ฅ ": 2.1, "์ ๋๋น์จ": 85.0, "๋ถ์ฑ๋น์จ": 210.0, "์ ํ๋ถ์ฑ๋น์จ": 450, "์ด์๋ณด์๋น์จ": 95.0},
"J ์ ๋ณดํต์ ์
": {"code": "J", "์ด์์ฐ์ฆ๊ฐ์จ": 12.5, "๋งค์ถ์ก์ฆ๊ฐ์จ": 18.2, "๋งค์ถ์ก์์
์ด์ต๋ฅ ": 8.5, "์ ๋๋น์จ": 165.0, "๋ถ์ฑ๋น์จ": 85.0, "์ ํ๋ถ์ฑ๋น์จ": 350, "์ด์๋ณด์๋น์จ": 850.0},
"K ๊ธ์ต๋ณดํ์
": {"code": "K", "์ด์์ฐ์ฆ๊ฐ์จ": 6.2, "๋งค์ถ์ก์ฆ๊ฐ์จ": 8.9, "๋งค์ถ์ก์์
์ด์ต๋ฅ ": 12.5, "์ ๋๋น์จ": 120.0, "๋ถ์ฑ๋น์จ": 250.0, "์ ํ๋ถ์ฑ๋น์จ": 500, "์ด์๋ณด์๋น์จ": 320.0},
"L ๋ถ๋์ฐ์
": {"code": "L", "์ด์์ฐ์ฆ๊ฐ์จ": 8.8, "๋งค์ถ์ก์ฆ๊ฐ์จ": 6.5, "๋งค์ถ์ก์์
์ด์ต๋ฅ ": 15.2, "์ ๋๋น์จ": 75.0, "๋ถ์ฑ๋น์จ": 185.0, "์ ํ๋ถ์ฑ๋น์จ": 400, "์ด์๋ณด์๋น์จ": 280.0},
"M ์ ๋ฌธ๊ณผํ๊ธฐ์ ": {"code": "M", "์ด์์ฐ์ฆ๊ฐ์จ": 10.2, "๋งค์ถ์ก์ฆ๊ฐ์จ": 15.8, "๋งค์ถ์ก์์
์ด์ต๋ฅ ": 7.2, "์ ๋๋น์จ": 155.0, "๋ถ์ฑ๋น์จ": 95.0, "์ ํ๋ถ์ฑ๋น์จ": 380, "์ด์๋ณด์๋น์จ": 720.0},
"N ์ฌ์
์๋น์ค": {"code": "N", "์ด์์ฐ์ฆ๊ฐ์จ": 8.8, "๋งค์ถ์ก์ฆ๊ฐ์จ": 12.5, "๋งค์ถ์ก์์
์ด์ต๋ฅ ": 5.5, "์ ๋๋น์จ": 135.0, "๋ถ์ฑ๋น์จ": 120.0, "์ ํ๋ถ์ฑ๋น์จ": 400, "์ด์๋ณด์๋น์จ": 450.0},
"P ๊ต์ก์๋น์ค": {"code": "P", "์ด์์ฐ์ฆ๊ฐ์จ": 5.5, "๋งค์ถ์ก์ฆ๊ฐ์จ": 6.8, "๋งค์ถ์ก์์
์ด์ต๋ฅ ": 6.5, "์ ๋๋น์จ": 142.0, "๋ถ์ฑ๋น์จ": 88.0, "์ ํ๋ถ์ฑ๋น์จ": 380, "์ด์๋ณด์๋น์จ": 520.0},
"Q ๋ณด๊ฑด๋ณต์ง": {"code": "Q", "์ด์์ฐ์ฆ๊ฐ์จ": 9.2, "๋งค์ถ์ก์ฆ๊ฐ์จ": 11.5, "๋งค์ถ์ก์์
์ด์ต๋ฅ ": 4.8, "์ ๋๋น์จ": 118.0, "๋ถ์ฑ๋น์จ": 135.0, "์ ํ๋ถ์ฑ๋น์จ": 400, "์ด์๋ณด์๋น์จ": 385.0}
}
CAUTION_ITEMS = {
"A": {"name": "์ง์๊ธ์ก ๋ถ์กฑ (์ฐฝ์
1๋
์ด๋ด)", "severity": "์กฐ๊ฑด๋ถ", "deduction": 30, "category": "์๊ธ"},
"B": {"name": "์ง์๊ธ์ก ๋ถ์กฑ (์ฐฝ์
1~3๋
)", "severity": "์กฐ๊ฑด๋ถ", "deduction": 70, "category": "์๊ธ"},
"C": {"name": "์ง์๊ธ์ก ๋ถ์กฑ (์ฐฝ์
3๋
์ด๊ณผ)", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "์๊ธ"},
"D": {"name": "์ง์๊ธ์ก ๋ถ์กฑ (๋ณด์ฆ๊ธฐ๊ด ์ฌ์ฉ์ค)", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "์๊ธ"},
"E": {"name": "๋ง์ง๋ง ๋ณด์ฆ์ ๋ฐํ 10๊ฐ์ ๋ฏธ๋ง", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "๋ณด์ฆ"},
"F": {"name": "๋ง์ง๋ง ๋ณด์ฆ์ ๋ฐํ 10๊ฐ์~1๋
", "severity": "์กฐ๊ฑด๋ถ", "deduction": 50, "category": "๋ณด์ฆ"},
"0": {"name": "์ ๋ถ์ง์ ์ ํ์
์ข
", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "์
์ข
"},
"1": {"name": "๋ํ์ ์ ์ฉ์ ์ 640์ ๋ฏธ๋ง", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "์ ์ฉ"},
"2": {"name": "๋ํ์ ์ ์ฉ์ ์ 640~700์ ", "severity": "์กฐ๊ฑด๋ถ", "deduction": 50, "category": "์ ์ฉ"},
"3": {"name": "๋ณด์ฆ๊ธฐ๊ด ์ฑ๋ฌด ๋ฏธ๋ณ์ ", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "์ฑ๋ฌด"},
"4": {"name": "๋ํ์ ๊ฒฝ๋ ฅ/ํ๋ ฅ ๋ถ์กฑ", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "์๊ฒฉ"},
"5": {"name": "๋ํ์/์ต๋์ฃผ์ฃผ ํ์ฐ ์ด๋ ฅ", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "๋ฒ์ "},
"6": {"name": "ํ์/ํ์์ ์ฒญ ์ด๋ ฅ", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "๋ฒ์ "},
"7": {"name": "๊ด๊ณ๊ธฐ์
์ ์ฉ๊ด๋ฆฌ์ ๋ณด ๋ฑ๋ก", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "์ ์ฉ"},
"8": {"name": "์์ก ์งํ/๋ฒ์ฃ์ฌ์ค ์ฐ๋ฃจ", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "๋ฒ์ "},
"9": {"name": "๋ํ์ ํ์ฌ์ฒ๋ฒ ์ด๋ ฅ", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "๋ฒ์ "},
"10": {"name": "๋ถ๋์ฐ ๊ถ๋ฆฌ์นจํด ์งํ์ค", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "๋ด๋ณด"},
"11": {"name": "๊ถ๋ฆฌ์นจํด ํด์ ํ 10๊ฐ์ ๋ฏธ๋ง", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "๋ด๋ณด"},
"12": {"name": "๊ถ๋ฆฌ์นจํด ํด์ ํ 10๊ฐ์ ์ด์", "severity": "์กฐ๊ฑด๋ถ", "deduction": 50, "category": "๋ด๋ณด"},
"13": {"name": "๊ธฐ์
์ ์ฉ์ ๋ณด๊ด๋ฆฌ ๋ฑ๋ก", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "์ ์ฉ"},
"14": {"name": "๋ํ์ ์ ์ฉ์ ๋ณด๊ด๋ฆฌ ๋ฑ๋ก", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "์ ์ฉ"},
"15": {"name": "๊ตญ์ธ ์ฒด๋ฉ์ค", "severity": "์กฐ๊ฑด๋ถ", "deduction": 60, "category": "์ฒด๋ฉ"},
"16": {"name": "์ง๋ฐฉ์ธ ์ฒด๋ฉ์ค", "severity": "์กฐ๊ฑด๋ถ", "deduction": 60, "category": "์ฒด๋ฉ"},
"17": {"name": "4๋๋ณดํ ์ฒด๋ฉ์ค", "severity": "์กฐ๊ฑด๋ถ", "deduction": 50, "category": "์ฒด๋ฉ"},
"18": {"name": "๊ด๊ณ๊ธฐ์
์ฒด๋ฉ์ค", "severity": "์กฐ๊ฑด๋ถ", "deduction": 50, "category": "์ฒด๋ฉ"},
"19": {"name": "๋ํ์ ๊ฐ์ธ ๊ตญ์ธ ์ฒด๋ฉ", "severity": "์กฐ๊ฑด๋ถ", "deduction": 60, "category": "์ฒด๋ฉ"},
"20": {"name": "๋ํ์ ๊ฐ์ธ ์ง๋ฐฉ์ธ ์ฒด๋ฉ", "severity": "์กฐ๊ฑด๋ถ", "deduction": 60, "category": "์ฒด๋ฉ"},
"21": {"name": "3๊ฐ์ ์ด๋ด 10์ผ ์ด์ ์ฐ์ฒด", "severity": "์กฐ๊ฑด๋ถ", "deduction": 70, "category": "์ฐ์ฒด"},
"22": {"name": "1๋
์ด๋ด ๋ณด์ฆ์ฌ๊ณ (๊ธฐ์
)", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "๋ณด์ฆ"},
"23": {"name": "1๋
์ด๋ด ๋ณด์ฆ์ฌ๊ณ (๊ด๊ณ๊ธฐ์
)", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "๋ณด์ฆ"},
"24": {"name": "๊ด๊ณ๊ธฐ์
๋ถ๋์ฐ ๊ถ๋ฆฌ์นจํด", "severity": "์กฐ๊ฑด๋ถ", "deduction": 50, "category": "๋ด๋ณด"},
"25": {"name": "์๋ณธ์ ์ ์ํ", "severity": "์กฐ๊ฑด๋ถ", "deduction": 70, "category": "์ฌ๋ฌด"},
"26": {"name": "์์ ์๋ณธ์ ์", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "์ฌ๋ฌด"},
"27": {"name": "๋ถ์ฑ๋น์จ ์ ํ์ด๊ณผ", "severity": "์กฐ๊ฑด๋ถ", "deduction": 60, "category": "์ฌ๋ฌด"},
"28": {"name": "2๋
์ฐ์ ๋น๊ธฐ์์์ค", "severity": "์กฐ๊ฑด๋ถ", "deduction": 50, "category": "์ฌ๋ฌด"},
"29": {"name": "3๋
์ฐ์ ๋น๊ธฐ์์์ค", "severity": "๋ถ๊ฐ", "deduction": 100, "category": "์ฌ๋ฌด"},
"30": {"name": "์์
์ด์ต ์ ์", "severity": "์กฐ๊ฑด๋ถ", "deduction": 40, "category": "์ฌ๋ฌด"},
"36": {"name": "๊ธฐ์กด ๋ณด์ฆ์ฌ์ฉ๊ธ์ก ๊ณผ๋ค", "severity": "์กฐ๊ฑด๋ถ", "deduction": 50, "category": "๋ณด์ฆ"},
"37": {"name": "๋งค์ถ์ก ๊ฐ์ ์ถ์ธ", "severity": "์กฐ๊ฑด๋ถ", "deduction": 30, "category": "์ฌ๋ฌด"},
"38": {"name": "๋ํ์ ๋ณ๊ฒฝ 1๋
์ด๋ด", "severity": "์กฐ๊ฑด๋ถ", "deduction": 40, "category": "๊ฒฝ์"},
"39": {"name": "ํด์
/ํ์
์ด๋ ฅ", "severity": "์กฐ๊ฑด๋ถ", "deduction": 60, "category": "๊ฒฝ์"},
"40": {"name": "์ฌ์
์ฅ ์์ฐจ๊ณ์ฝ ๋ถ์์ ", "severity": "์กฐ๊ฑด๋ถ", "deduction": 30, "category": "๊ฒฝ์"},
"45": {"name": "๊ธ์ก์ ํ ์ฌ์ ํด๋น (MAX 2์ต)", "severity": "์กฐ๊ฑด๋ถ", "deduction": 30, "category": "์๊ธ"}
}
PROCESS_STEPS = [
{"step": 1, "name": "์ ๊ท์ ์", "desc": "์ ๋ณดํ์ฉ๋์, ์ฌ์
์๋ฒํธ, ๋ํ์ ํ์ธ", "duration": "์ฆ์", "icon": "๐"},
{"step": 2, "name": "์ง์์๋ต", "desc": "์ด๋ฉ์ผ, ์ค๋ฌธ์์ฑ (69๊ฐ ์ง๋ฌธ)", "duration": "10~30๋ถ", "icon": "๐ฌ"},
{"step": 3, "name": "API ์์ง", "desc": "17๊ฐ ๊ณต๊ณต๋ฐ์ดํฐ ์ฐ๊ณ ์กฐํ", "duration": "์๋", "icon": "๐"},
{"step": 4, "name": "ํ๋
๋ถ์", "desc": "์ ์์ํ, ์ถ์ฒ๊ธฐ๊ด, ์ง์๊ธ์ก", "duration": "์๋", "icon": "๐"},
{"step": 5, "name": "๋ฆฌํฌํธ์์ฑ", "desc": "๋ถ์ ๊ฒฐ๊ณผ ์์ฑ ์๋ฃ", "duration": "์ฆ์", "icon": "๐"},
{"step": 6, "name": "๋ธ๋ฆฌํ", "desc": "๋ถ๋ถ ๋ฆฌํฌํธ & ๊ฒฐ์ฌ์์ฒญ", "duration": "ํ์ธ", "icon": "๐"},
{"step": 7, "name": "๊ฒฐ์ฌํ์ธ", "desc": "๊ฒฐ์ฌ ์๋ฃ ๋๊ธฐ", "duration": "1~3์ผ", "icon": "โ
"},
{"step": 8, "name": "์ ์ฒด๋ฆฌํฌํธ", "desc": "์ ์ฒด ๋ฆฌํฌํธ & ์ถ๊ฐ์๋น์ค", "duration": "์ฆ์", "icon": "๐"}
]
PROGRAMS = {
"์ ์ฉ๋ณด์ฆ๊ธฐ๊ธ": {"max": 30, "rate": "0.5~1.5%", "period": "5๋
", "color": "#6fd9a8"},
"๊ธฐ์ ๋ณด์ฆ๊ธฐ๊ธ": {"max": 30, "rate": "1.0~1.5%", "period": "5๋
", "color": "#ffd93d"},
"์ง์ญ์ ๋ณด์ฌ๋จ": {"max": 2, "rate": "0.5~1.0%", "period": "3๋
", "color": "#6495ed"},
"์ฐฝ์
๋ณด์ฆ": {"max": 10, "rate": "0.8~1.2%", "period": "5๋
", "color": "#9b59b6"},
"ํ์ ์ฑ์ฅ": {"max": 50, "rate": "0.5~1.0%", "period": "5๋
", "color": "#00d4ff"},
"์์ถ๊ธฐ์
": {"max": 30, "rate": "0.7~1.2%", "period": "5๋
", "color": "#ff9500"}
}
# ============================================================================
# ๐ฏ ์ฐจํธ 1: 3D ์ข
ํฉ์ ์ ๊ฒ์ด์ง (์ธํธ๋ผ ๋ฒ์ )
# ============================================================================
def generate_ultra_gauge(score: int, title: str = "์ข
ํฉ์ ์") -> str:
"""3D ํจ๊ณผ ์ข
ํฉ์ ์ ๊ฒ์ด์ง"""
if score >= 80: color, status, emoji = "#6fd9a8", "EXCELLENT", "๐"
elif score >= 60: color, status, emoji = "#7be8c0", "GOOD", "โ
"
elif score >= 40: color, status, emoji = "#ffd93d", "FAIR", "โ ๏ธ"
else: color, status, emoji = "#ff6b6b", "RISK", "๐จ"
# SVG ๊ณ์ฐ
radius = 80
circumference = 2 * 3.14159 * radius
offset = circumference - (score / 100) * circumference
html = f"""
{emoji}
{title}
{status}
0-39
40-59
60-79
80+
"""
return html
# ============================================================================
# ๐ธ๏ธ ์ฐจํธ 2: ๋ ์ด๋ ์ฐจํธ (5์ถ SVG ์ ๋๋ฉ์ด์
)
# ============================================================================
def generate_radar_chart(data: dict, title: str = "์ฌ๋ฌด๊ฑด์ ์ฑ ๋ถ์") -> str:
"""SVG ๋ ์ด๋ ์ฐจํธ with ์ ๋๋ฉ์ด์
"""
metrics = [
("์์ต์ฑ", data.get("์์ต์ฑ", 60)),
("์์ ์ฑ", data.get("์์ ์ฑ", 70)),
("์ฑ์ฅ์ฑ", data.get("์ฑ์ฅ์ฑ", 55)),
("ํ๋์ฑ", data.get("ํ๋์ฑ", 65)),
("์์ฐ์ฑ", data.get("์์ฐ์ฑ", 50))
]
cx, cy = 150, 150
max_r = 100
n = len(metrics)
angles = [(i * 360 / n - 90) * math.pi / 180 for i in range(n)]
# ๋ฐฐ๊ฒฝ ๊ทธ๋ฆฌ๋
grid_html = ""
for level in [20, 40, 60, 80, 100]:
points = " ".join([f"{cx + level/100*max_r*math.cos(a)},{cy + level/100*max_r*math.sin(a)}" for a in angles])
grid_html += f''
# ์ถ ์
axes_html = ""
for a in angles:
axes_html += f''
# ๋ฐ์ดํฐ ํด๋ฆฌ๊ณค
data_points = " ".join([f"{cx + metrics[i][1]/100*max_r*math.cos(angles[i])},{cy + metrics[i][1]/100*max_r*math.sin(angles[i])}" for i in range(n)])
# ๋ผ๋ฒจ
labels_html = ""
for i, (name, val) in enumerate(metrics):
lx = cx + (max_r + 30) * math.cos(angles[i])
ly = cy + (max_r + 30) * math.sin(angles[i])
color = "#6fd9a8" if val >= 70 else "#ffd93d" if val >= 50 else "#ff6b6b"
labels_html += f'''
{name}
{val}์
'''
avg = sum(v for _, v in metrics) // n
avg_color = "#6fd9a8" if avg >= 70 else "#ffd93d" if avg >= 50 else "#ff6b6b"
html = f"""
๐ธ๏ธ {title}
์ข
ํฉ ํ๊ท
{avg}
์
"""
return html
# ============================================================================
# ๐ฉ ์ฐจํธ 3: ๊ธฐ๊ด๋ณ ๋๋ ์ฐจํธ
# ============================================================================
def generate_donut_chart(sinbo: int, kibo: int, jaedan: int) -> str:
"""๊ธฐ๊ด๋ณ ์น์ธํ๋ฅ ๋๋"""
total = max(1, sinbo + kibo + jaedan)
r = 65
circ = 2 * 3.14159 * r
sinbo_arc = circ * sinbo / total
kibo_arc = circ * kibo / total
jaedan_arc = circ * jaedan / total
def get_grade(s):
if s >= 70: return ("A", "#6fd9a8")
elif s >= 50: return ("B", "#ffd93d")
else: return ("C", "#ff6b6b")
html = f"""
๐ฏ ๊ธฐ๊ด๋ณ ์น์ธํ๋ฅ
ํ๊ท
{(sinbo+kibo+jaedan)//3}%
์ ์ฉ๋ณด์ฆ๊ธฐ๊ธ
{sinbo}%
{get_grade(sinbo)[0]}
๊ธฐ์ ๋ณด์ฆ๊ธฐ๊ธ
{kibo}%
{get_grade(kibo)[0]}
์ง์ญ์ ๋ณด์ฌ๋จ
{jaedan}%
{get_grade(jaedan)[0]}
"""
return html
# ============================================================================
# ๐ฐ ์ฐจํธ 4: ์๊ธ์กฐ๋ฌ ํ์ดํ๋ผ์ธ
# ============================================================================
def generate_pipeline(stage: int, amounts: dict) -> str:
"""์๊ธ์กฐ๋ฌ ํ์ดํ๋ผ์ธ ํ๋ก์ฐ"""
stages = [
("์ฌ์ ์ฌ์ฌ", "๐", "#6495ed"),
("์๋ฅ์ ์", "๐", "#9b59b6"),
("์ฌ์ฌ์งํ", "โ๏ธ", "#ffd93d"),
("์น์ธ์๋ฃ", "โ
", "#6fd9a8")
]
html = f"""
๐ฐ ์๊ธ์กฐ๋ฌ ํ์ดํ๋ผ์ธ
"""
for i, (name, icon, color) in enumerate(stages):
done = i < stage
curr = i == stage
html += f"""
{'โ' if done else icon}
{name}
"""
html += f"""
์ ์ฒญ๊ธ์ก
{amounts.get('์ ์ฒญ',5):.1f}์ต
์์์น์ธ
{amounts.get('์์',3.5):.1f}์ต
๊ธฐ์กด์ฌ์ฉ
{amounts.get('๊ธฐ์กด',0.5):.1f}์ต
"""
return html
# ============================================================================
# ๐ฅ ์ฐจํธ 5: ๋ฆฌ์คํฌ ํํธ๋งต
# ============================================================================
def generate_heatmap(caution_items: list) -> str:
"""๋ฆฌ์คํฌ ํํธ๋งต"""
cats = {"์ ์ฉ": [0,0,[]], "์ฌ๋ฌด": [0,0,[]], "์ฒด๋ฉ": [0,0,[]], "๋ฒ์ ": [0,0,[]],
"๋ณด์ฆ": [0,0,[]], "๋ด๋ณด": [0,0,[]], "๊ฒฝ์": [0,0,[]], "๊ธฐํ": [0,0,[]]}
for code in caution_items:
if code in CAUTION_ITEMS:
item = CAUTION_ITEMS[code]
cat = item.get("category", "๊ธฐํ")
if cat not in cats: cat = "๊ธฐํ"
cats[cat][0] += 1
if item["severity"] == "๋ถ๊ฐ": cats[cat][1] += 1
cats[cat][2].append(item["name"])
def risk_level(cnt, sev):
if sev > 0: return (4, "#ff6b6b", "์ฌ๊ฐ")
if cnt >= 3: return (3, "#ff9500", "๋์")
if cnt >= 2: return (2, "#ffd93d", "๋ณดํต")
if cnt >= 1: return (1, "#6fd9a8", "๋ฎ์")
return (0, "#2a2a45", "์์ ")
total = sum(c[0] for c in cats.values())
severe = sum(c[1] for c in cats.values())
html = f"""
๐ฅ ๋ฆฌ์คํฌ ํํธ๋งต
"""
for cat, (cnt, sev, items) in cats.items():
lv, color, status = risk_level(cnt, sev)
html += f"""
{cat}
{cnt}
{('๋ถ๊ฐ ' + str(sev)) if sev > 0 else status}
"""
html += f"""
์์
๋ฎ์
๋ณดํต
๋์
์ฌ๊ฐ
์ด ์ ์์ฌํญ
{total}(๋ถ๊ฐ {severe})
"""
return html
# ============================================================================
# ๐ ์ฐจํธ 6: ๊ฒฝ์๋ ฅ ๋ฒค์น๋งํฌ ๋ฐ
# ============================================================================
def generate_benchmark_bars(company: dict, industry: dict) -> str:
"""์
์ข
๋๋น ๋ฒค์น๋งํฌ"""
metrics = [
("๋งค์ถ์ฑ์ฅ๋ฅ ", company.get("๋งค์ถ์ฑ์ฅ๋ฅ ", 15), industry.get("๋งค์ถ์ฑ์ฅ๋ฅ ", 10), "%", False),
("์์
์ด์ต๋ฅ ", company.get("์์
์ด์ต๋ฅ ", 8), industry.get("์์
์ด์ต๋ฅ ", 5), "%", False),
("๋ถ์ฑ๋น์จ", company.get("๋ถ์ฑ๋น์จ", 120), industry.get("๋ถ์ฑ๋น์จ", 150), "%", True),
("์ ์ฉ๋ฑ๊ธ", company.get("์ ์ฉ๋ฑ๊ธ", 720), industry.get("์ ์ฉ๋ฑ๊ธ", 680), "์ ", False),
("๊ณ ์ฉ์ฑ์ฅ", company.get("๊ณ ์ฉ์ฑ์ฅ", 20), industry.get("๊ณ ์ฉ์ฑ์ฅ", 10), "%", False)
]
better_count = 0
html = f"""
๐ ์
์ข
๋๋น ๊ฒฝ์๋ ฅ
"""
for name, comp_val, ind_val, unit, reverse in metrics:
is_better = (comp_val < ind_val) if reverse else (comp_val > ind_val)
if is_better: better_count += 1
diff = ind_val - comp_val if reverse else comp_val - ind_val
diff_pct = (diff / ind_val * 100) if ind_val != 0 else 0
max_val = max(comp_val, ind_val) * 1.3
comp_w = comp_val / max_val * 100 if max_val > 0 else 0
ind_w = ind_val / max_val * 100 if max_val > 0 else 0
color = "#6fd9a8" if is_better else "#ff6b6b"
html += f"""
{name}
{'+' if diff > 0 else ''}{diff:.1f}{unit} ({'+' if diff_pct > 0 else ''}{diff_pct:.0f}%)
{'๐' if is_better else '๐'}
๊ท์ฌ
{comp_val:.1f}{unit}
์
์ข
{ind_val:.1f}{unit}
"""
overall = better_count / len(metrics) * 100
html += f"""
์
์ข
๋๋น ๊ฒฝ์๋ ฅ ์ง์
{better_count}/{len(metrics)}
์งํ ์ฐ์
"""
return html
# ============================================================================
# ๐ ์ฐจํธ 7: KPI ์ค์ฝ์ด์นด๋ ๋์๋ณด๋
# ============================================================================
def generate_scorecard_dashboard(data: dict) -> str:
"""KPI ์ค์ฝ์ด์นด๋"""
kpis = [
("๐ฐ", "์์์ง์์ก", f"{data.get('์์๊ธ์ก', 2.5):.1f}์ต", "#6fd9a8", True),
("๐", "์ข
ํฉ์ ์", f"{data.get('์ข
ํฉ์ ์', 75)}์ ", "#ffd93d" if data.get('์ข
ํฉ์ ์', 75) < 70 else "#6fd9a8", data.get('์ข
ํฉ์ ์', 75) >= 70),
("๐ฆ", "์ถ์ฒ๊ธฐ๊ด", data.get('์ถ์ฒ๊ธฐ๊ด', '์ ๋ณด'), "#6495ed", True),
("โ ๏ธ", "๋ฆฌ์คํฌ", f"{data.get('๋ฆฌ์คํฌ', 3)}๊ฑด", "#ff6b6b" if data.get('๋ฆฌ์คํฌ', 3) > 5 else "#ffd93d", data.get('๋ฆฌ์คํฌ', 3) <= 3),
("๐", "์ ์ฉ๋ฑ๊ธ", f"{data.get('์ ์ฉ๋ฑ๊ธ', 720)}์ ", "#6fd9a8" if data.get('์ ์ฉ๋ฑ๊ธ', 720) >= 700 else "#ffd93d", True),
("๐ฏ", "์น์ธํ๋ฅ ", f"{data.get('์น์ธํ๋ฅ ', 72)}%", "#6fd9a8" if data.get('์น์ธํ๋ฅ ', 72) >= 70 else "#ffd93d", True)
]
html = f"""
๐ ํต์ฌ KPI ๋์๋ณด๋
"""
for i, (icon, label, value, color, is_good) in enumerate(kpis):
html += f"""
{icon}
{value}
{label}
{'โ ์ํธ' if is_good else 'โ ์ฃผ์'}
"""
html += """
"""
return html
# ============================================================================
# ๐ ์ฐจํธ 8: ์ํฐํด ์ฐจํธ
# ============================================================================
def generate_waterfall(calc: dict) -> str:
"""์ํฐํด ์ฐจํธ"""
steps = [
("๊ธฐ์ค๋งค์ถ", calc.get("๊ธฐ์ค๋งค์ถ", 5e8), "base", "๐ฐ"),
("๊ด๊ณ๊ธฐ์
", -calc.get("๊ด๊ณ์ฐจ๊ฐ", 5e7), "neg", "โ"),
("์กฐ์ ๋งค์ถ", calc.get("์กฐ์ ๋งค์ถ", 4.5e8), "sub", "๐"),
("ํ์ ์จ", calc.get("ํ์ ์ ์ฉ", -1.5e8), "neg", "๐"),
("1์ฐจ์ฐ์ถ", calc.get("1์ฐจ์ฐ์ถ", 3e8), "sub", "๐"),
("๊ธฐ์กด์ฌ์ฉ", -calc.get("๊ธฐ์กด์ฌ์ฉ", 5e7), "neg", "๐ฆ"),
("์ต์ข
๊ธ์ก", calc.get("์ต์ข
๊ธ์ก", 2.5e8), "total", "๐ฏ")
]
max_amt = max(abs(s[1]) for s in steps if s[1] != 0) * 1.2
html = f"""
๐ฐ ๊ธ์ก ์ฐ์ถ ์ํฐํด
"""
for name, amt, stype, icon in steps:
if amt == 0 and stype == "neg": continue
w = abs(amt) / max_amt * 100 if max_amt > 0 else 0
if stype == "neg":
color, bg = "#ff6b6b", "rgba(255,107,107,0.1)"
val = f"-{abs(amt)/1e8:.1f}์ต"
elif stype == "total":
color, bg = "#6fd9a8", "rgba(111,217,168,0.15)"
val = f"{amt/1e8:.1f}์ต"
elif stype == "sub":
color, bg = "#6495ed", "rgba(100,149,237,0.1)"
val = f"{amt/1e8:.1f}์ต"
else:
color, bg = "#808090", "rgba(128,128,144,0.1)"
val = f"{amt/1e8:.1f}์ต"
html += f"""
"""
html += """
"""
return html
# ============================================================================
# โฑ๏ธ ์ฐจํธ 9: ํ๋ก์ธ์ค ํ์๋ผ์ธ
# ============================================================================
def generate_timeline(current: int) -> str:
"""ํ๋ก์ธ์ค ํ์๋ผ์ธ"""
html = f"""
โฑ๏ธ ์งํ ํํฉ
์ ์ฒด ์งํ๋ฅ
{current}/8 ({current*12.5:.0f}%)
"""
for i, step in enumerate(PROCESS_STEPS):
done = i < current
curr = i == current
if done:
border, bg, icon_bg, icon = "var(--neon-green)", "rgba(111,217,168,0.1)", "var(--neon-green)", "โ"
elif curr:
border, bg, icon_bg, icon = "#ffd93d", "rgba(255,217,61,0.1)", "#ffd93d", step['icon']
else:
border, bg, icon_bg, icon = "#3a3a55", "transparent", "#2a2a45", step['icon']
html += f"""
{icon}
{step['name']}
{step['duration']}
"""
html += """
"""
return html
# ============================================================================
# ๐ณ ์ฐจํธ 10: ์ ์ฉ์ ์ ์๋ฎฌ๋ ์ด์
# ============================================================================
def generate_credit_sim(current: int, improvements: dict) -> str:
"""์ ์ฉ์ ์ ์๋ฎฌ๋ ์ด์
"""
improve_list = {
"์ 2๊ธ์ต๊ถ ๋์ถ ์ํ": 30,
"์นด๋๋ก ์ํ": 25,
"์ฐ์ฒด ํด์": 40,
"์นด๋ ์ฌ์ฉ๋ฅ 30% ์ดํ": 15,
"ํต์ ๋น ์๋์ด์ฒด": 5,
"๋ณดํ๋ฃ ์ ์๋ฉ๋ถ": 5
}
total_up = sum(improve_list[k] for k in improvements if improvements.get(k))
projected = min(900, current + total_up)
cur_color = "#6fd9a8" if current >= 750 else "#ffd93d" if current >= 700 else "#ff6b6b"
proj_color = "#6fd9a8" if projected >= 750 else "#ffd93d" if projected >= 700 else "#ff6b6b"
html = f"""
๐ณ ์ ์ฉ์ ์ ์๋ฎฌ๋ ์ด์
โ
์์
{projected}
+{total_up}์
๐ง ๊ฐ์ ๋ฐฉ๋ฒ
"""
for item, pts in improve_list.items():
chk = improvements.get(item, False)
html += f"""
{'โ' if chk else 'โ'}
{item}
+{pts}์
"""
html += """
๐ ์ ์๋ณ ์ํฅ
"""
ranges = [(750, 900, "์ฐ์", "#6fd9a8", "์ ํ์์"), (700, 749, "์ํธ", "#7be8c0", "์ผ๋ถ์ ํ"),
(650, 699, "๋ณดํต", "#ffd93d", "์กฐ๊ฑด๋ถ"), (600, 649, "์ฃผ์", "#ff9500", "์ ํ์ "), (0, 599, "์ํ", "#ff6b6b", "๋ถ๊ฐ")]
for lo, hi, label, color, limit in ranges:
is_cur = lo <= current <= hi
html += f"""
{lo}-{hi}
{label}
{limit}
"""
html += """
"""
return html
# ============================================================================
# ๐ฏ ์ฐจํธ 11: ์ข
ํฉ ์ธํฌ๊ทธ๋ํฝ
# ============================================================================
def generate_infographic(data: dict) -> str:
"""์ข
ํฉ ์ธํฌ๊ทธ๋ํฝ ๋ณด๋"""
score = data.get("์ข
ํฉ์ ์", 75)
amount = data.get("์์๊ธ์ก", 2.5)
sinbo = data.get("์ ๋ณด", 70)
kibo = data.get("๊ธฐ๋ณด", 65)
jaedan = data.get("์ฌ๋จ", 80)
cautions = data.get("์ ์์ฌํญ", 3)
credit = data.get("์ ์ฉ์ ์", 720)
score_color = "#6fd9a8" if score >= 70 else "#ffd93d" if score >= 50 else "#ff6b6b"
html = f"""
๐ฎ ๋ฏธ๋ค๋ํต ๋ถ์ ์๋ฃ
์ ์ฑ
์๊ธ ์ฌ์ ์ฌ์ฌ ์ข
ํฉ ๋ฆฌํฌํธ
๐ฐ ์์ ์ง์๊ธ์ก
{amount:.1f}์ต์
๐ ์ข
ํฉ์ ์
{score}
{'์ฐ์' if score >= 70 else '์ํธ' if score >= 50 else '์ฃผ์'}
โ ๏ธ ์ ์์ฌํญ
{cautions}
๊ฑด ํด๊ฒฐ ํ์
๐ฆ ๊ธฐ๊ด๋ณ ์น์ธํ๋ฅ
๐ก ํต์ฌ ๊ถ๊ณ ์ฌํญ
{'- ๋ถ๊ฐ ์ฌ์ ์ฐ์ ํด๊ฒฐ ํ์
' if cautions > 3 else ''}
{'- ์ ์ฉ์ ์ 700์ ์ด์ ์ ์ง ๊ถ์ฅ
' if credit < 700 else '- โ ์ ์ฉ์ ์ ์ํธ
'}
- ํ์์๋ฅ: ์ฌ๋ฌด์ ํ, ์ฌ์
์๋ฑ๋ก์ฆ, ๋ฉ์ธ์ฆ๋ช
์
- ์ถ์ฒ ๊ธฐ๊ด: {data.get('์ถ์ฒ๊ธฐ๊ด', '์ ์ฉ๋ณด์ฆ๊ธฐ๊ธ')}
"""
return html
# ============================================================================
# ๋ฉ์ธ ๋ถ์ ํจ์
# ============================================================================
def run_ultra_analysis(company_name, biz_num, industry, sales, years, credit_score, employees,
education, tech_grade, has_patent, has_venture, has_innobiz, request_amt,
total_assets, total_liab, cur_assets, cur_liab,
op_profit, net_income, interest_exp, caution_checks,
existing_guar, related_sales):
"""์ข
ํฉ ๋ถ์ ์คํ"""
# ์ฌ๋ฌด๋น์จ
equity = total_assets - total_liab
debt_ratio = (total_liab / equity * 100) if equity > 0 else 999
current_ratio = (cur_assets / cur_liab * 100) if cur_liab > 0 else 999
op_margin = (op_profit / (sales*1e8) * 100) if sales > 0 else 0
int_coverage = (op_profit*1e6 / (interest_exp*1e6)) if interest_exp > 0 else 999
# ์ ์ ๊ณ์ฐ
sinbo_sc = 5 if sales >= 4 else 4 if sales >= 3 else 3 if sales >= 2 else 2 if sales >= 1 else 1
sinbo_sc += (1 if has_patent else 0) + (3 if education == "๋ฐ์ฌ" else 2 if education == "์์ฌ" else 0)
sinbo_pct = min(95, sinbo_sc * 10)
kibo_sc = 5 + (2 if education == "๋ฐ์ฌ" else 1 if education == "์์ฌ" else 0)
kibo_sc += (2 if tech_grade in ["ํน๊ธ","๊ณ ๊ธ"] else 1 if tech_grade == "์ค๊ธ" else 0) + (1 if has_patent else 0)
kibo_pct = min(95, kibo_sc * 10)
jaedan_sc = 10 if sales >= 1 and years >= 3 else 8 if sales >= 1 else 6
jaedan_pct = min(95, jaedan_sc * 10)
# ์ ์์ฌํญ ๋ถ์
failed = sum(1 for c in caution_checks if CAUTION_ITEMS.get(c, {}).get("severity") == "๋ถ๊ฐ")
cond = sum(1 for c in caution_checks if CAUTION_ITEMS.get(c, {}).get("severity") == "์กฐ๊ฑด๋ถ")
base_score = (sinbo_pct + kibo_pct + jaedan_pct) / 3
deduction = failed * 15 + cond * 5
total_score = max(0, min(100, int(base_score - deduction)))
# ๊ธ์ก ๊ณ์ฐ
adj_sales = sales * 1e8 - related_sales * 1e8
turnover = adj_sales / 7
cap_limit = max(3e8, equity * 3e6) if equity > 0 else 3e8
final_amt = min(turnover, cap_limit) - existing_guar * 1e8
if "45" in caution_checks: final_amt = min(final_amt, 2e8)
final_amt = max(0, final_amt)
# ์ถ์ฒ ๊ธฐ๊ด
recommend = "๊ธฐ์ ๋ณด์ฆ๊ธฐ๊ธ" if has_patent or has_venture or has_innobiz else "์ ์ฉ๋ณด์ฆ๊ธฐ๊ธ"
# ์ฐจํธ ๋ฐ์ดํฐ
calc_data = {
"๊ธฐ์ค๋งค์ถ": sales * 1e8, "๊ด๊ณ์ฐจ๊ฐ": related_sales * 1e8, "์กฐ์ ๋งค์ถ": adj_sales,
"ํ์ ์ ์ฉ": adj_sales - turnover, "1์ฐจ์ฐ์ถ": turnover, "๊ธฐ์กด์ฌ์ฉ": existing_guar * 1e8, "์ต์ข
๊ธ์ก": final_amt
}
radar_data = {
"์์ต์ฑ": min(100, max(0, int(op_margin * 5 + 50))),
"์์ ์ฑ": min(100, max(0, int(100 - debt_ratio / 3))),
"์ฑ์ฅ์ฑ": min(100, max(0, 60)),
"ํ๋์ฑ": min(100, max(0, int(current_ratio / 2))),
"์์ฐ์ฑ": min(100, max(0, 55))
}
company_bench = {"๋งค์ถ์ฑ์ฅ๋ฅ ": 10, "์์
์ด์ต๋ฅ ": op_margin, "๋ถ์ฑ๋น์จ": debt_ratio, "์ ์ฉ๋ฑ๊ธ": credit_score, "๊ณ ์ฉ์ฑ์ฅ": 15}
ind_data = INDUSTRY_FINANCIAL_RATIOS.get(industry, INDUSTRY_FINANCIAL_RATIOS["C ์ ์กฐ์
"])
industry_bench = {"๋งค์ถ์ฑ์ฅ๋ฅ ": ind_data.get("๋งค์ถ์ก์ฆ๊ฐ์จ", 10), "์์
์ด์ต๋ฅ ": ind_data.get("๋งค์ถ์ก์์
์ด์ต๋ฅ ", 5),
"๋ถ์ฑ๋น์จ": ind_data.get("๋ถ์ฑ๋น์จ", 150), "์ ์ฉ๋ฑ๊ธ": 700, "๊ณ ์ฉ์ฑ์ฅ": 10}
kpi_data = {"์์๊ธ์ก": final_amt / 1e8, "์ข
ํฉ์ ์": total_score, "์ถ์ฒ๊ธฐ๊ด": recommend,
"๋ฆฌ์คํฌ": len(caution_checks), "์ ์ฉ๋ฑ๊ธ": credit_score, "์น์ธํ๋ฅ ": (sinbo_pct + kibo_pct + jaedan_pct) // 3}
improvements = {"์ 2๊ธ์ต๊ถ ๋์ถ ์ํ": credit_score < 700, "์นด๋๋ก ์ํ": credit_score < 720,
"์ฐ์ฒด ํด์": "21" in caution_checks, "์นด๋ ์ฌ์ฉ๋ฅ 30% ์ดํ": False,
"ํต์ ๋น ์๋์ด์ฒด": True, "๋ณดํ๋ฃ ์ ์๋ฉ๋ถ": True}
info_data = {"์ข
ํฉ์ ์": total_score, "์์๊ธ์ก": final_amt / 1e8, "์ ๋ณด": sinbo_pct, "๊ธฐ๋ณด": kibo_pct,
"์ฌ๋จ": jaedan_pct, "์ ์์ฌํญ": len(caution_checks), "์ ์ฉ์ ์": credit_score, "์ถ์ฒ๊ธฐ๊ด": recommend}
# ์ฐจํธ ์์ฑ
gauge = generate_ultra_gauge(total_score, "๐ฎ ๋ฏธ๋ค๋ํต ์ ์")
radar = generate_radar_chart(radar_data)
donut = generate_donut_chart(sinbo_pct, kibo_pct, jaedan_pct)
pipeline = generate_pipeline(1, {"์ ์ฒญ": request_amt, "์์": final_amt / 1e8, "๊ธฐ์กด": existing_guar})
heatmap = generate_heatmap(caution_checks)
benchmark = generate_benchmark_bars(company_bench, industry_bench)
scorecard = generate_scorecard_dashboard(kpi_data)
waterfall = generate_waterfall(calc_data)
timeline = generate_timeline(5)
credit_sim = generate_credit_sim(credit_score, improvements)
infographic = generate_infographic(info_data)
return (infographic, gauge, radar, donut, pipeline, heatmap, benchmark, scorecard, waterfall, timeline, credit_sim)
# ============================================================================
# Gradio UI
# ============================================================================
# ============================================================================
# ์ ์ฅ / ๋ถ๋ฌ์ค๊ธฐ ํธ๋ค๋ฌ (cache_db ์ฐ๋)
# ============================================================================
def save_fund_results(email, company_name, biz_num, industry, sales, years, credit_score, employees,
education, tech_grade, has_patent, has_venture, has_innobiz, request_amt,
total_assets, total_liab, cur_assets, cur_liab,
op_profit, net_income, interest_exp, caution_checks,
existing_guar, related_sales):
"""๋ถ์ ๊ฒฐ๊ณผ ์ ์ฅ"""
cache = _get_fund_cache()
if cache is None:
return "โ ๏ธ cache_db ๋ชจ๋์ ์ฐพ์ ์ ์์ต๋๋ค. app.py ํตํฉ ํ๊ฒฝ์์ ์คํํด์ฃผ์ธ์."
if not email or '@' not in email:
return "โ ๏ธ ์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ์
๋ ฅํด์ฃผ์ธ์."
company_info = {
"company_name": company_name, "biz_num": biz_num, "industry": industry,
"sales": sales, "years": years, "employees": employees,
"education": education, "tech_grade": tech_grade,
"has_patent": has_patent, "has_venture": has_venture, "has_innobiz": has_innobiz
}
financial_info = {
"credit_score": credit_score, "request_amt": request_amt,
"total_assets": total_assets, "total_liab": total_liab,
"cur_assets": cur_assets, "cur_liab": cur_liab,
"op_profit": op_profit, "net_income": net_income, "interest_exp": interest_exp,
"existing_guar": existing_guar, "related_sales": related_sales
}
analysis_results = {
"saved_at": datetime.now().isoformat(),
"version": "v7.0"
}
success, msg = cache.save_fund_analysis(email, company_info, financial_info, caution_checks, analysis_results)
return msg
def load_fund_results(email):
"""์ ์ฅ๋ ๋ถ์ ๊ธฐ๋ก ๋ถ๋ฌ์ค๊ธฐ โ ์
๋ ฅ ํ๋์ ๋ณต์"""
cache = _get_fund_cache()
if cache is None:
return [gr.update()] * 22 + ["โ ๏ธ cache_db ๋ชจ๋์ ์ฐพ์ ์ ์์ต๋๋ค. app.py ํตํฉ ํ๊ฒฝ์์ ์คํํด์ฃผ์ธ์."]
if not email or '@' not in email:
return [gr.update()] * 22 + ["โ ๏ธ ์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ์
๋ ฅํด์ฃผ์ธ์."]
record, msg = cache.load_fund_profile(email)
if record is None:
return [gr.update()] * 22 + [msg]
ci = record.get("company_info", {})
fi = record.get("financial_info", {})
cc = record.get("caution_checks", [])
return [
gr.update(value=ci.get("company_name", "")), # company_name
gr.update(value=ci.get("biz_num", "")), # biz_num
gr.update(value=ci.get("industry", "C ์ ์กฐ์
")), # industry
gr.update(value=ci.get("sales", 5)), # sales
gr.update(value=ci.get("years", 3)), # years
gr.update(value=fi.get("credit_score", 720)), # credit_score
gr.update(value=ci.get("employees", 10)), # employees
gr.update(value=ci.get("education", "ํ์ฌ")), # education
gr.update(value=ci.get("tech_grade", "์ค๊ธ")), # tech_grade
gr.update(value=ci.get("has_patent", False)), # has_patent
gr.update(value=ci.get("has_venture", False)), # has_venture
gr.update(value=ci.get("has_innobiz", False)), # has_innobiz
gr.update(value=fi.get("request_amt", 3)), # request_amt
gr.update(value=fi.get("total_assets", 1000)), # total_assets
gr.update(value=fi.get("total_liab", 600)), # total_liab
gr.update(value=fi.get("cur_assets", 500)), # cur_assets
gr.update(value=fi.get("cur_liab", 400)), # cur_liab
gr.update(value=fi.get("op_profit", 50)), # op_profit
gr.update(value=fi.get("net_income", 30)), # net_income
gr.update(value=fi.get("interest_exp", 10)), # interest_exp
gr.update(value=cc), # caution_checks
gr.update(value=fi.get("existing_guar", 0)), # existing_guar
gr.update(value=fi.get("related_sales", 0)), # related_sales
msg # status
]
def create_fund_tab():
"""์ ์ฑ
์๊ธ ํญ ์์ฑ"""
with gr.Tab("๐ฐ ์ ์ฑ
์๊ธ ์ฌ์ ์ฌ์ฌ"):
gr.HTML('''
๐ฎ ๋ฏธ๋ค๋ํต ULTRA v7.0
์ ์ฑ
์๊ธ ์ฌ์ ์ฌ์ฌ ์์คํ
| 11๊ฐ ๋น์ฃผ์ผ ์ฐจํธ
''')
with gr.Row():
with gr.Column(scale=3):
fund_email = gr.Textbox(label="๐ง ์ด๋ฉ์ผ", placeholder="example@company.com")
with gr.Column(scale=1):
with gr.Row():
fund_load = gr.Button("๐ฅ ๋ถ๋ฌ์ค๊ธฐ", size="sm")
fund_save = gr.Button("๐พ ์ ์ฅ", size="sm", variant="primary")
fund_status = gr.Textbox(label="์ํ", interactive=False, lines=1)
with gr.Tabs():
with gr.Tab("๐ ๊ธฐ์
์ ๋ณด"):
with gr.Row():
with gr.Column():
company_name = gr.Textbox(label="ํ์ฌ๋ช
", placeholder="(์ฃผ)ํ์ฌ๋ช
")
biz_num = gr.Textbox(label="์ฌ์
์๋ฒํธ", placeholder="000-00-00000")
industry = gr.Dropdown(label="์
์ข
", choices=list(INDUSTRY_FINANCIAL_RATIOS.keys()), value="C ์ ์กฐ์
")
sales = gr.Number(label="์ฐ๋งค์ถ (์ต์)", value=5)
years = gr.Number(label="์
๋ ฅ (๋
)", value=3)
employees = gr.Number(label="์ข
์
์ (๋ช
)", value=10)
with gr.Column():
credit_score = gr.Slider(label="์ ์ฉ์ ์", minimum=300, maximum=900, value=720, step=10)
education = gr.Dropdown(label="ํ๋ ฅ", choices=["๊ณ ์กธ์ดํ", "ํ์ฌ", "์์ฌ", "๋ฐ์ฌ"], value="ํ์ฌ")
tech_grade = gr.Dropdown(label="๊ธฐ์ ์๊ฒฉ", choices=["์ด๊ธ์ดํ", "์ด๊ธ", "์ค๊ธ", "๊ณ ๊ธ", "ํน๊ธ"], value="์ค๊ธ")
has_patent = gr.Checkbox(label="ํนํ ๋ณด์ ")
has_venture = gr.Checkbox(label="๋ฒค์ฒ์ธ์ฆ")
has_innobiz = gr.Checkbox(label="์ด๋
ธ๋น์ฆ")
with gr.Row():
with gr.Column():
total_assets = gr.Number(label="์ด์์ฐ (๋ฐฑ๋ง์)", value=1000)
total_liab = gr.Number(label="์ด๋ถ์ฑ (๋ฐฑ๋ง์)", value=600)
cur_assets = gr.Number(label="์ ๋์์ฐ (๋ฐฑ๋ง์)", value=500)
cur_liab = gr.Number(label="์ ๋๋ถ์ฑ (๋ฐฑ๋ง์)", value=400)
with gr.Column():
op_profit = gr.Number(label="์์
์ด์ต (๋ฐฑ๋ง์)", value=50)
net_income = gr.Number(label="์์ด์ต (๋ฐฑ๋ง์)", value=30)
interest_exp = gr.Number(label="์ด์๋น์ฉ (๋ฐฑ๋ง์)", value=10)
request_amt = gr.Number(label="ํ์์๊ธ (์ต์)", value=3)
with gr.Row():
existing_guar = gr.Number(label="๊ธฐ์กด๋ณด์ฆ (์ต์)", value=0)
related_sales = gr.Number(label="๊ด๊ณ๊ธฐ์
๋งค์
(์ต์)", value=0)
caution_opts = [(f"[{c}] {i['name']}", c) for c, i in list(CAUTION_ITEMS.items())[:25]]
caution_checks = gr.CheckboxGroup(label="โ ๏ธ ์ ์์ฌํญ", choices=caution_opts, value=[])
analyze_btn = gr.Button("๐ ์ข
ํฉ ๋ถ์", variant="primary", size="lg", elem_classes=["analyze-btn"])
# ๊ฒฐ๊ณผ ํญ๋ค
with gr.Tab("๐ ์ข
ํฉ ์ธํฌ๊ทธ๋ํฝ"): out_info = gr.HTML()
with gr.Tab("๐ฏ ์ข
ํฉ์ ์"): out_gauge = gr.HTML()
with gr.Tab("๐ธ๏ธ ์ฌ๋ฌด ๋ ์ด๋"): out_radar = gr.HTML()
with gr.Tab("๐ฉ ๊ธฐ๊ด๋ณ ํ๋ฅ "): out_donut = gr.HTML()
with gr.Tab("๐ฐ ํ์ดํ๋ผ์ธ"): out_pipeline = gr.HTML()
with gr.Tab("๐ฅ ๋ฆฌ์คํฌ๋งต"): out_heatmap = gr.HTML()
with gr.Tab("๐ ๋ฒค์น๋งํฌ"): out_benchmark = gr.HTML()
with gr.Tab("๐ KPI๋ณด๋"): out_scorecard = gr.HTML()
with gr.Tab("๐ง ์ํฐํด"): out_waterfall = gr.HTML()
with gr.Tab("โฑ๏ธ ํ์๋ผ์ธ"): out_timeline = gr.HTML()
with gr.Tab("๐ณ ์ ์ฉ์๋ฎฌ"): out_credit = gr.HTML()
inputs = [company_name, biz_num, industry, sales, years, credit_score, employees,
education, tech_grade, has_patent, has_venture, has_innobiz, request_amt,
total_assets, total_liab, cur_assets, cur_liab,
op_profit, net_income, interest_exp, caution_checks, existing_guar, related_sales]
outputs = [out_info, out_gauge, out_radar, out_donut, out_pipeline, out_heatmap,
out_benchmark, out_scorecard, out_waterfall, out_timeline, out_credit]
analyze_btn.click(fn=run_ultra_analysis, inputs=inputs, outputs=outputs)
# ์ ์ฅ/๋ถ๋ฌ์ค๊ธฐ ์ด๋ฒคํธ
save_inputs = [fund_email] + inputs
fund_save.click(fn=save_fund_results, inputs=save_inputs, outputs=[fund_status])
load_outputs = inputs + [fund_status]
fund_load.click(fn=load_fund_results, inputs=[fund_email], outputs=load_outputs)
def create_app():
"""๋
๋ฆฝ ์คํ์ฉ"""
with gr.Blocks(css=CUSTOM_CSS, title="๋ฏธ๋ค๋ํต ULTRA v7.0") as app:
gr.HTML("""
๐ฎ ๋ฏธ๋ค๋ํต ULTRA v7.0
OKCEO ์ ์ฑ
์๊ธ ์ฌ์ ์ฌ์ฌ ์์คํ
""")
create_fund_tab()
return app
if __name__ == "__main__":
app = create_app()
app.launch(server_name="0.0.0.0", server_port=7860)