""" ๐ŸŽฎ 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}

{score}
/ 100
{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}

{grid_html} {axes_html} {''.join([f'' for i in range(n)])} {labels_html}
์ข…ํ•ฉ ํ‰๊ท  {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"""
{icon} {name} {val}
""" 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"""

๐Ÿ’ณ ์‹ ์šฉ์ ์ˆ˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜

ํ˜„์žฌ
{current}
โ†’
์˜ˆ์ƒ
{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}
๊ฑด ํ•ด๊ฒฐ ํ•„์š”

๐Ÿฆ ๊ธฐ๊ด€๋ณ„ ์Šน์ธํ™•๋ฅ 

์‹ ์šฉ๋ณด์ฆ๊ธฐ๊ธˆ
{sinbo}%
๊ธฐ์ˆ ๋ณด์ฆ๊ธฐ๊ธˆ
{kibo}%
์ง€์—ญ์‹ ๋ณด์žฌ๋‹จ
{jaedan}%

๐Ÿ’ก ํ•ต์‹ฌ ๊ถŒ๊ณ ์‚ฌํ•ญ

    {'
  • ๋ถˆ๊ฐ€ ์‚ฌ์œ  ์šฐ์„  ํ•ด๊ฒฐ ํ•„์š”
  • ' 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)