| | """ |
| | 🎮 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 |
| |
|
| | |
| | |
| | |
| | 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 = """ |
| | @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"} |
| | } |
| |
|
| | |
| | |
| | |
| |
|
| | 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", "🚨" |
| | |
| | |
| | radius = 80 |
| | circumference = 2 * 3.14159 * radius |
| | offset = circumference - (score / 100) * circumference |
| | |
| | html = f""" |
| | <div class="chart-ultra" style="text-align:center;"> |
| | <div style="display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:24px;"> |
| | <span style="font-size:28px;">{emoji}</span> |
| | <h3 style="margin:0;color:var(--neon-green);font-family:'Orbitron',sans-serif;font-size:22px; |
| | animation:glow-text 2s ease-in-out infinite;">{title}</h3> |
| | </div> |
| | |
| | <div style="position:relative;width:220px;height:220px;margin:0 auto;"> |
| | <!-- 배경 글로우 --> |
| | <div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%); |
| | width:180px;height:180px;border-radius:50%; |
| | background:radial-gradient(circle,{color}15 0%,transparent 70%); |
| | animation:pulse-ring 2s ease-out infinite;"></div> |
| | |
| | <svg width="220" height="220" viewBox="0 0 220 220" style="transform:rotate(-90deg);"> |
| | <defs> |
| | <linearGradient id="gaugeGrad" x1="0%" y1="0%" x2="100%" y2="0%"> |
| | <stop offset="0%" style="stop-color:{color}"/> |
| | <stop offset="50%" style="stop-color:#fff"/> |
| | <stop offset="100%" style="stop-color:{color}"/> |
| | </linearGradient> |
| | <filter id="glow"> |
| | <feGaussianBlur stdDeviation="4" result="coloredBlur"/> |
| | <feMerge><feMergeNode in="coloredBlur"/><feMergeNode in="SourceGraphic"/></feMerge> |
| | </filter> |
| | <filter id="shadow"> |
| | <feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="#000" flood-opacity="0.5"/> |
| | </filter> |
| | </defs> |
| | |
| | <!-- 외곽 링 (3D 효과) --> |
| | <circle cx="110" cy="110" r="100" fill="none" stroke="#1a1a2e" stroke-width="20" filter="url(#shadow)"/> |
| | <circle cx="110" cy="110" r="100" fill="none" stroke="#252540" stroke-width="18"/> |
| | |
| | <!-- 배경 트랙 --> |
| | <circle cx="110" cy="110" r="{radius}" fill="none" stroke="#2a2a45" stroke-width="14"/> |
| | |
| | <!-- 진행 바 --> |
| | <circle cx="110" cy="110" r="{radius}" fill="none" |
| | stroke="url(#gaugeGrad)" stroke-width="14" stroke-linecap="round" |
| | stroke-dasharray="{circumference}" stroke-dashoffset="{offset}" |
| | filter="url(#glow)" |
| | style="transition:stroke-dashoffset 1.5s cubic-bezier(0.4, 0, 0.2, 1);"/> |
| | |
| | <!-- 포인터 점 --> |
| | <circle cx="110" cy="{110-radius}" r="8" fill="{color}" filter="url(#glow)"> |
| | <animateTransform attributeName="transform" type="rotate" |
| | from="0 110 110" to="{score*3.6} 110 110" dur="1.5s" fill="freeze"/> |
| | </circle> |
| | </svg> |
| | |
| | <!-- 중앙 값 --> |
| | <div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;"> |
| | <div style="font-family:'Orbitron',sans-serif;font-size:56px;font-weight:900; |
| | color:{color};text-shadow:0 0 30px {color}80; |
| | animation:count-up 1s ease-out;">{score}</div> |
| | <div style="font-size:14px;color:var(--text-secondary);margin-top:-5px;">/ 100</div> |
| | </div> |
| | </div> |
| | |
| | <!-- 상태 배지 --> |
| | <div style="margin-top:24px;"> |
| | <span style="display:inline-block;padding:10px 30px; |
| | background:linear-gradient(135deg,{color}20,{color}10); |
| | border:2px solid {color};border-radius:30px; |
| | color:{color};font-family:'Orbitron',sans-serif;font-weight:700;font-size:16px; |
| | box-shadow:0 0 20px {color}40;letter-spacing:2px;"> |
| | {status} |
| | </span> |
| | </div> |
| | |
| | <!-- 스케일 바 --> |
| | <div style="display:flex;justify-content:center;gap:15px;margin-top:20px;"> |
| | <span style="display:flex;align-items:center;gap:5px;font-size:11px;"> |
| | <span style="width:10px;height:10px;background:#ff6b6b;border-radius:2px;"></span> |
| | <span style="color:var(--text-secondary);">0-39</span> |
| | </span> |
| | <span style="display:flex;align-items:center;gap:5px;font-size:11px;"> |
| | <span style="width:10px;height:10px;background:#ffd93d;border-radius:2px;"></span> |
| | <span style="color:var(--text-secondary);">40-59</span> |
| | </span> |
| | <span style="display:flex;align-items:center;gap:5px;font-size:11px;"> |
| | <span style="width:10px;height:10px;background:#7be8c0;border-radius:2px;"></span> |
| | <span style="color:var(--text-secondary);">60-79</span> |
| | </span> |
| | <span style="display:flex;align-items:center;gap:5px;font-size:11px;"> |
| | <span style="width:10px;height:10px;background:#6fd9a8;border-radius:2px;"></span> |
| | <span style="color:var(--text-secondary);">80+</span> |
| | </span> |
| | </div> |
| | </div> |
| | """ |
| | return html |
| |
|
| | |
| | |
| | |
| |
|
| | 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'<polygon points="{points}" fill="none" stroke="#2a2a45" stroke-width="1"/>' |
| | |
| | |
| | axes_html = "" |
| | for a in angles: |
| | axes_html += f'<line x1="{cx}" y1="{cy}" x2="{cx + max_r*math.cos(a)}" y2="{cy + max_r*math.sin(a)}" stroke="#3a3a55" stroke-width="1"/>' |
| | |
| | |
| | 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''' |
| | <text x="{lx}" y="{ly}" fill="{color}" font-size="13" font-weight="600" |
| | text-anchor="middle" dominant-baseline="middle">{name}</text> |
| | <text x="{lx}" y="{ly + 16}" fill="var(--text-secondary)" font-size="11" |
| | text-anchor="middle">{val}점</text> |
| | ''' |
| | |
| | avg = sum(v for _, v in metrics) // n |
| | avg_color = "#6fd9a8" if avg >= 70 else "#ffd93d" if avg >= 50 else "#ff6b6b" |
| | |
| | html = f""" |
| | <div class="chart-ultra"> |
| | <h3 style="color:var(--neon-green);margin-bottom:20px;text-align:center; |
| | font-family:'Rajdhani',sans-serif;font-size:20px;">🕸️ {title}</h3> |
| | |
| | <svg width="300" height="300" viewBox="0 0 300 300" style="display:block;margin:0 auto;"> |
| | <defs> |
| | <linearGradient id="radarFill" x1="0%" y1="0%" x2="100%" y2="100%"> |
| | <stop offset="0%" style="stop-color:#6fd9a8;stop-opacity:0.6"/> |
| | <stop offset="100%" style="stop-color:#3a8f6a;stop-opacity:0.2"/> |
| | </linearGradient> |
| | <filter id="radarGlow"> |
| | <feGaussianBlur stdDeviation="3" result="blur"/> |
| | <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge> |
| | </filter> |
| | </defs> |
| | |
| | {grid_html} |
| | {axes_html} |
| | |
| | <!-- 데이터 영역 --> |
| | <polygon points="{data_points}" fill="url(#radarFill)" stroke="#6fd9a8" |
| | stroke-width="2" filter="url(#radarGlow)" |
| | style="animation:slide-up 0.8s ease-out;"> |
| | <animate attributeName="opacity" from="0" to="1" dur="1s"/> |
| | </polygon> |
| | |
| | <!-- 데이터 포인트 --> |
| | {''.join([f'<circle cx="{cx + metrics[i][1]/100*max_r*math.cos(angles[i])}" cy="{cy + metrics[i][1]/100*max_r*math.sin(angles[i])}" r="6" fill="#6fd9a8" stroke="#fff" stroke-width="2"><animate attributeName="r" from="0" to="6" dur="0.5s" begin="{i*0.1}s"/></circle>' for i in range(n)])} |
| | |
| | {labels_html} |
| | </svg> |
| | |
| | <!-- 평균 점수 --> |
| | <div style="text-align:center;margin-top:15px;padding:15px; |
| | background:linear-gradient(145deg,#252540,#1a1a2e);border-radius:12px;"> |
| | <span style="color:var(--text-secondary);font-size:14px;">종합 평균</span> |
| | <span style="font-family:'Orbitron',sans-serif;font-size:28px;font-weight:700; |
| | color:{avg_color};margin-left:15px;">{avg}</span> |
| | <span style="color:var(--text-secondary);font-size:14px;">점</span> |
| | </div> |
| | </div> |
| | """ |
| | return html |
| |
|
| | |
| | |
| | |
| |
|
| | 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""" |
| | <div class="chart-ultra"> |
| | <h3 style="color:var(--neon-green);margin-bottom:24px;text-align:center; |
| | font-family:'Rajdhani',sans-serif;font-size:20px;">🎯 기관별 승인확률</h3> |
| | |
| | <div style="display:flex;justify-content:center;align-items:center;gap:50px;flex-wrap:wrap;"> |
| | <!-- 도넛 차트 --> |
| | <div style="position:relative;width:180px;height:180px;"> |
| | <svg width="180" height="180" viewBox="0 0 180 180" style="transform:rotate(-90deg);"> |
| | <defs> |
| | <filter id="donutGlow"> |
| | <feGaussianBlur stdDeviation="3"/><feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge> |
| | </filter> |
| | </defs> |
| | <circle cx="90" cy="90" r="{r}" fill="none" stroke="#2a2a45" stroke-width="22"/> |
| | <circle cx="90" cy="90" r="{r}" fill="none" stroke="#6fd9a8" stroke-width="22" |
| | stroke-dasharray="{sinbo_arc} {circ}" stroke-dashoffset="0" filter="url(#donutGlow)"> |
| | <animate attributeName="stroke-dasharray" from="0 {circ}" to="{sinbo_arc} {circ}" dur="1s"/> |
| | </circle> |
| | <circle cx="90" cy="90" r="{r}" fill="none" stroke="#ffd93d" stroke-width="22" |
| | stroke-dasharray="{kibo_arc} {circ}" stroke-dashoffset="{-sinbo_arc}" filter="url(#donutGlow)"> |
| | <animate attributeName="stroke-dasharray" from="0 {circ}" to="{kibo_arc} {circ}" dur="1s" begin="0.3s"/> |
| | </circle> |
| | <circle cx="90" cy="90" r="{r}" fill="none" stroke="#6495ed" stroke-width="22" |
| | stroke-dasharray="{jaedan_arc} {circ}" stroke-dashoffset="{-(sinbo_arc+kibo_arc)}" filter="url(#donutGlow)"> |
| | <animate attributeName="stroke-dasharray" from="0 {circ}" to="{jaedan_arc} {circ}" dur="1s" begin="0.6s"/> |
| | </circle> |
| | </svg> |
| | <div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;"> |
| | <div style="font-size:12px;color:var(--text-secondary);">평균</div> |
| | <div style="font-family:'Orbitron',sans-serif;font-size:32px;font-weight:700;color:#fff;"> |
| | {(sinbo+kibo+jaedan)//3}% |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <!-- 범례 --> |
| | <div style="display:flex;flex-direction:column;gap:16px;"> |
| | <div style="display:flex;align-items:center;gap:15px;padding:14px 20px; |
| | background:linear-gradient(145deg,rgba(111,217,168,0.1),transparent); |
| | border-radius:12px;border-left:4px solid #6fd9a8;"> |
| | <div style="width:14px;height:14px;background:#6fd9a8;border-radius:50%;box-shadow:0 0 10px #6fd9a880;"></div> |
| | <div> |
| | <div style="font-size:12px;color:var(--text-secondary);">신용보증기금</div> |
| | <div style="font-family:'Orbitron',sans-serif;font-size:26px;font-weight:700;color:#6fd9a8;">{sinbo}%</div> |
| | </div> |
| | <span style="margin-left:auto;font-size:20px;padding:5px 12px;background:{get_grade(sinbo)[1]}30; |
| | border-radius:8px;color:{get_grade(sinbo)[1]};font-weight:700;">{get_grade(sinbo)[0]}</span> |
| | </div> |
| | |
| | <div style="display:flex;align-items:center;gap:15px;padding:14px 20px; |
| | background:linear-gradient(145deg,rgba(255,217,61,0.1),transparent); |
| | border-radius:12px;border-left:4px solid #ffd93d;"> |
| | <div style="width:14px;height:14px;background:#ffd93d;border-radius:50%;box-shadow:0 0 10px #ffd93d80;"></div> |
| | <div> |
| | <div style="font-size:12px;color:var(--text-secondary);">기술보증기금</div> |
| | <div style="font-family:'Orbitron',sans-serif;font-size:26px;font-weight:700;color:#ffd93d;">{kibo}%</div> |
| | </div> |
| | <span style="margin-left:auto;font-size:20px;padding:5px 12px;background:{get_grade(kibo)[1]}30; |
| | border-radius:8px;color:{get_grade(kibo)[1]};font-weight:700;">{get_grade(kibo)[0]}</span> |
| | </div> |
| | |
| | <div style="display:flex;align-items:center;gap:15px;padding:14px 20px; |
| | background:linear-gradient(145deg,rgba(100,149,237,0.1),transparent); |
| | border-radius:12px;border-left:4px solid #6495ed;"> |
| | <div style="width:14px;height:14px;background:#6495ed;border-radius:50%;box-shadow:0 0 10px #6495ed80;"></div> |
| | <div> |
| | <div style="font-size:12px;color:var(--text-secondary);">지역신보재단</div> |
| | <div style="font-family:'Orbitron',sans-serif;font-size:26px;font-weight:700;color:#6495ed;">{jaedan}%</div> |
| | </div> |
| | <span style="margin-left:auto;font-size:20px;padding:5px 12px;background:{get_grade(jaedan)[1]}30; |
| | border-radius:8px;color:{get_grade(jaedan)[1]};font-weight:700;">{get_grade(jaedan)[0]}</span> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | """ |
| | return html |
| |
|
| | |
| | |
| | |
| |
|
| | def generate_pipeline(stage: int, amounts: dict) -> str: |
| | """자금조달 파이프라인 플로우""" |
| | |
| | stages = [ |
| | ("사전심사", "🔍", "#6495ed"), |
| | ("서류접수", "📄", "#9b59b6"), |
| | ("심사진행", "⚖️", "#ffd93d"), |
| | ("승인완료", "✅", "#6fd9a8") |
| | ] |
| | |
| | html = f""" |
| | <div class="chart-ultra"> |
| | <h3 style="color:var(--neon-green);margin-bottom:25px;text-align:center; |
| | font-family:'Rajdhani',sans-serif;font-size:20px;">💰 자금조달 파이프라인</h3> |
| | |
| | <!-- 파이프라인 --> |
| | <div style="position:relative;padding:30px 0;"> |
| | <div style="position:absolute;top:50%;left:8%;right:8%;height:6px; |
| | background:linear-gradient(90deg,#2a2a45 0%,#6fd9a8 {stage*25}%,#2a2a45 {stage*25}%); |
| | transform:translateY(-50%);border-radius:3px;"></div> |
| | |
| | <div style="display:flex;justify-content:space-between;position:relative;"> |
| | """ |
| | |
| | for i, (name, icon, color) in enumerate(stages): |
| | done = i < stage |
| | curr = i == stage |
| | |
| | html += f""" |
| | <div style="text-align:center;flex:1;opacity:{'1' if done or curr else '0.4'}; |
| | animation:{'float 2s ease-in-out infinite' if curr else 'none'};"> |
| | <div style="width:70px;height:70px;margin:0 auto; |
| | background:{'linear-gradient(135deg,'+color+','+color+'80)' if done else '#252540' if curr else '#1a1a2e'}; |
| | border:3px solid {color if done or curr else '#3a3a55'};border-radius:50%; |
| | display:flex;align-items:center;justify-content:center;font-size:28px; |
| | box-shadow:{'0 0 30px '+color+'60' if curr else '0 0 15px '+color+'40' if done else 'none'}; |
| | transition:all 0.3s ease;"> |
| | {'✓' if done else icon} |
| | </div> |
| | <div style="margin-top:12px;font-weight:600;color:{'#fff' if done or curr else 'var(--text-secondary)'}; |
| | font-size:14px;">{name}</div> |
| | </div> |
| | """ |
| | |
| | html += f""" |
| | </div> |
| | </div> |
| | |
| | <!-- 금액 카드 --> |
| | <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:30px;"> |
| | <div class="score-card" style="border-top:4px solid #6495ed;"> |
| | <div class="label">신청금액</div> |
| | <div class="value" style="font-size:28px;color:#6495ed;">{amounts.get('신청',5):.1f}<span style="font-size:16px;">억</span></div> |
| | </div> |
| | <div class="score-card highlight" style="border-top:4px solid #6fd9a8;"> |
| | <div class="label">예상승인</div> |
| | <div class="value" style="font-size:28px;">{amounts.get('예상',3.5):.1f}<span style="font-size:16px;">억</span></div> |
| | </div> |
| | <div class="score-card" style="border-top:4px solid #ff6b6b;"> |
| | <div class="label">기존사용</div> |
| | <div class="value" style="font-size:28px;color:#ff6b6b;">{amounts.get('기존',0.5):.1f}<span style="font-size:16px;">억</span></div> |
| | </div> |
| | </div> |
| | </div> |
| | """ |
| | return html |
| |
|
| | |
| | |
| | |
| |
|
| | 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""" |
| | <div class="chart-ultra"> |
| | <h3 style="color:var(--neon-green);margin-bottom:20px;text-align:center; |
| | font-family:'Rajdhani',sans-serif;font-size:20px;">🔥 리스크 히트맵</h3> |
| | |
| | <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:20px;"> |
| | """ |
| | |
| | for cat, (cnt, sev, items) in cats.items(): |
| | lv, color, status = risk_level(cnt, sev) |
| | html += f""" |
| | <div style="background:{color}{'30' if lv > 0 else ''};padding:20px;text-align:center; |
| | border-radius:14px;border:2px solid {color if lv >= 3 else 'transparent'}; |
| | transition:all 0.3s ease;cursor:pointer; |
| | {'animation:neon-pulse 1.5s ease-in-out infinite;' if lv >= 3 else ''}" |
| | title="{', '.join(items[:3]) if items else '해당없음'}"> |
| | <div style="font-size:13px;color:{'#fff' if lv >= 2 else 'var(--text-secondary)'};font-weight:600;">{cat}</div> |
| | <div style="font-family:'Orbitron',sans-serif;font-size:32px;font-weight:700; |
| | color:{'#fff' if lv >= 2 else color};margin:10px 0;">{cnt}</div> |
| | <div style="font-size:11px;color:{'#fff' if lv >= 2 else '#808090'};">{('불가 ' + str(sev)) if sev > 0 else status}</div> |
| | </div> |
| | """ |
| | |
| | html += f""" |
| | </div> |
| | |
| | <!-- 범례 --> |
| | <div style="display:flex;justify-content:center;gap:20px;padding:15px; |
| | background:#1a1a2e;border-radius:10px;margin-bottom:20px;"> |
| | <span style="display:flex;align-items:center;gap:6px;font-size:12px;"> |
| | <span style="width:14px;height:14px;background:#2a2a45;border-radius:3px;"></span>안전 |
| | </span> |
| | <span style="display:flex;align-items:center;gap:6px;font-size:12px;"> |
| | <span style="width:14px;height:14px;background:#6fd9a8;border-radius:3px;"></span>낮음 |
| | </span> |
| | <span style="display:flex;align-items:center;gap:6px;font-size:12px;"> |
| | <span style="width:14px;height:14px;background:#ffd93d;border-radius:3px;"></span>보통 |
| | </span> |
| | <span style="display:flex;align-items:center;gap:6px;font-size:12px;"> |
| | <span style="width:14px;height:14px;background:#ff9500;border-radius:3px;"></span>높음 |
| | </span> |
| | <span style="display:flex;align-items:center;gap:6px;font-size:12px;"> |
| | <span style="width:14px;height:14px;background:#ff6b6b;border-radius:3px;"></span>심각 |
| | </span> |
| | </div> |
| | |
| | <!-- 총계 --> |
| | <div style="padding:18px;background:linear-gradient(145deg,#252540,#1a1a2e);border-radius:12px; |
| | border-left:4px solid #ff6b6b;display:flex;justify-content:space-between;align-items:center;"> |
| | <span style="color:var(--text-secondary);">총 유의사항</span> |
| | <span style="font-family:'Orbitron',sans-serif;font-size:28px;font-weight:700;color:#ff6b6b;"> |
| | {total}<span style="font-size:14px;color:var(--text-secondary);margin-left:8px;">(불가 {severe})</span> |
| | </span> |
| | </div> |
| | </div> |
| | """ |
| | return html |
| |
|
| | |
| | |
| | |
| |
|
| | 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""" |
| | <div class="chart-ultra"> |
| | <h3 style="color:var(--neon-green);margin-bottom:25px;text-align:center; |
| | font-family:'Rajdhani',sans-serif;font-size:20px;">📊 업종 대비 경쟁력</h3> |
| | |
| | <div style="display:flex;flex-direction:column;gap:18px;"> |
| | """ |
| | |
| | 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""" |
| | <div style="background:#1a1a2e;padding:18px;border-radius:14px;border-left:4px solid {color};"> |
| | <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;"> |
| | <span style="font-weight:600;color:#fff;">{name}</span> |
| | <span style="font-size:13px;color:{color};"> |
| | {'+' if diff > 0 else ''}{diff:.1f}{unit} ({'+' if diff_pct > 0 else ''}{diff_pct:.0f}%) |
| | {'👍' if is_better else '👎'} |
| | </span> |
| | </div> |
| | |
| | <div style="margin-bottom:8px;"> |
| | <div style="display:flex;align-items:center;gap:12px;margin-bottom:6px;"> |
| | <span style="width:50px;font-size:12px;color:var(--text-secondary);">귀사</span> |
| | <div style="flex:1;height:14px;background:#252540;border-radius:7px;overflow:hidden;"> |
| | <div class="progress-ultra"><div class="fill" style="width:{comp_w}%;background:linear-gradient(90deg,{color},{color}80);"></div></div> |
| | </div> |
| | <span style="width:70px;text-align:right;font-family:'Orbitron',sans-serif;font-weight:600;color:{color};">{comp_val:.1f}{unit}</span> |
| | </div> |
| | </div> |
| | |
| | <div> |
| | <div style="display:flex;align-items:center;gap:12px;"> |
| | <span style="width:50px;font-size:12px;color:#606080;">업종</span> |
| | <div style="flex:1;height:10px;background:#252540;border-radius:5px;overflow:hidden;"> |
| | <div style="width:{ind_w}%;height:100%;background:#505070;border-radius:5px;"></div> |
| | </div> |
| | <span style="width:70px;text-align:right;font-size:13px;color:#606080;">{ind_val:.1f}{unit}</span> |
| | </div> |
| | </div> |
| | </div> |
| | """ |
| | |
| | overall = better_count / len(metrics) * 100 |
| | |
| | html += f""" |
| | </div> |
| | |
| | <!-- 종합 --> |
| | <div style="margin-top:25px;padding:22px;background:linear-gradient(145deg,#1a2a30,#0d1a1f); |
| | border-radius:14px;text-align:center;border:2px solid #6fd9a830;"> |
| | <div style="font-size:14px;color:var(--text-secondary);margin-bottom:8px;">업종 대비 경쟁력 지수</div> |
| | <div style="font-family:'Orbitron',sans-serif;font-size:48px;font-weight:900; |
| | color:{'#6fd9a8' if overall >= 60 else '#ffd93d' if overall >= 40 else '#ff6b6b'}; |
| | text-shadow:0 0 30px {'#6fd9a8' if overall >= 60 else '#ffd93d' if overall >= 40 else '#ff6b6b'}60;"> |
| | {better_count}/{len(metrics)} |
| | </div> |
| | <div style="font-size:13px;color:var(--text-secondary);margin-top:5px;">지표 우위</div> |
| | </div> |
| | </div> |
| | """ |
| | return html |
| |
|
| | |
| | |
| | |
| |
|
| | 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""" |
| | <div class="chart-ultra"> |
| | <h3 style="color:var(--neon-green);margin-bottom:25px;text-align:center; |
| | font-family:'Rajdhani',sans-serif;font-size:20px;">📋 핵심 KPI 대시보드</h3> |
| | |
| | <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:18px;"> |
| | """ |
| | |
| | for i, (icon, label, value, color, is_good) in enumerate(kpis): |
| | html += f""" |
| | <div class="score-card {'highlight' if i == 0 else ''}" style="animation:slide-up 0.5s ease-out {i*0.1}s both;"> |
| | <div style="font-size:32px;margin-bottom:10px;">{icon}</div> |
| | <div class="value" style="color:{color};">{value}</div> |
| | <div class="label">{label}</div> |
| | <div style="margin-top:10px;"> |
| | <span style="display:inline-block;padding:4px 12px;background:{color}20; |
| | border-radius:12px;font-size:11px;color:{color};"> |
| | {'✓ 양호' if is_good else '⚠ 주의'} |
| | </span> |
| | </div> |
| | </div> |
| | """ |
| | |
| | html += """ |
| | </div> |
| | </div> |
| | """ |
| | return html |
| |
|
| | |
| | |
| | |
| |
|
| | 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""" |
| | <div class="chart-ultra"> |
| | <h3 style="color:var(--neon-green);margin-bottom:25px;text-align:center; |
| | font-family:'Rajdhani',sans-serif;font-size:20px;">💰 금액 산출 워터폴</h3> |
| | |
| | <div style="display:flex;flex-direction:column;gap:14px;"> |
| | """ |
| | |
| | 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""" |
| | <div style="background:{bg};padding:18px;border-radius:12px;border-left:4px solid {color}; |
| | animation:slide-up 0.4s ease-out;"> |
| | <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;"> |
| | <span style="font-weight:{'700' if stype in ['total','sub'] else '500'};color:#fff;"> |
| | {icon} {name} |
| | </span> |
| | <span style="font-family:'Orbitron',sans-serif;font-size:22px;font-weight:700;color:{color};">{val}</span> |
| | </div> |
| | <div class="progress-ultra"> |
| | <div class="fill" style="width:{w}%;background:{color};"></div> |
| | </div> |
| | </div> |
| | """ |
| | |
| | html += """ |
| | </div> |
| | </div> |
| | """ |
| | return html |
| |
|
| | |
| | |
| | |
| |
|
| | def generate_timeline(current: int) -> str: |
| | """프로세스 타임라인""" |
| | |
| | html = f""" |
| | <div class="chart-ultra"> |
| | <h3 style="color:var(--neon-green);margin-bottom:20px;text-align:center; |
| | font-family:'Rajdhani',sans-serif;font-size:20px;">⏱️ 진행 현황</h3> |
| | |
| | <!-- 진행률 바 --> |
| | <div style="margin-bottom:30px;"> |
| | <div style="display:flex;justify-content:space-between;margin-bottom:10px;"> |
| | <span style="color:var(--text-secondary);">전체 진행률</span> |
| | <span style="color:var(--neon-green);font-family:'Orbitron',sans-serif;font-weight:600;"> |
| | {current}/8 ({current*12.5:.0f}%) |
| | </span> |
| | </div> |
| | <div class="progress-ultra" style="height:16px;"> |
| | <div class="fill" style="width:{current*12.5}%;"></div> |
| | </div> |
| | </div> |
| | |
| | <!-- 단계 그리드 --> |
| | <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:14px;"> |
| | """ |
| | |
| | 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""" |
| | <div style="background:{bg};border:2px solid {border};border-radius:14px; |
| | padding:18px;text-align:center;transition:all 0.3s ease; |
| | {'animation:neon-pulse 1.5s ease-in-out infinite;' if curr else ''}"> |
| | <div style="width:50px;height:50px;margin:0 auto 12px; |
| | background:{icon_bg};border-radius:50%; |
| | display:flex;align-items:center;justify-content:center; |
| | font-size:24px;color:{'#0d0d1a' if done else '#fff'}; |
| | box-shadow:{'0 0 20px '+border+'60' if curr else 'none'};"> |
| | {icon} |
| | </div> |
| | <div style="font-size:13px;font-weight:600;color:{'var(--neon-green)' if done else '#ffd93d' if curr else 'var(--text-secondary)'};"> |
| | {step['name']} |
| | </div> |
| | <div style="font-size:11px;color:var(--text-secondary);margin-top:4px;">{step['duration']}</div> |
| | </div> |
| | """ |
| | |
| | html += """ |
| | </div> |
| | </div> |
| | """ |
| | return html |
| |
|
| | |
| | |
| | |
| |
|
| | 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""" |
| | <div class="chart-ultra"> |
| | <h3 style="color:var(--neon-green);margin-bottom:25px;text-align:center; |
| | font-family:'Rajdhani',sans-serif;font-size:20px;">💳 신용점수 시뮬레이션</h3> |
| | |
| | <div style="display:grid;grid-template-columns:1fr 1fr;gap:30px;"> |
| | <!-- 점수 비교 --> |
| | <div> |
| | <div style="display:flex;gap:20px;margin-bottom:25px;"> |
| | <div class="score-card" style="flex:1;"> |
| | <div class="label">현재</div> |
| | <div class="value" style="color:{cur_color};">{current}</div> |
| | </div> |
| | <div style="display:flex;align-items:center;font-size:28px;color:var(--neon-green);">→</div> |
| | <div class="score-card highlight" style="flex:1;"> |
| | <div class="label">예상</div> |
| | <div class="value">{projected}</div> |
| | <div style="color:var(--neon-green);font-size:14px;margin-top:5px;">+{total_up}점</div> |
| | </div> |
| | </div> |
| | |
| | <!-- 개선 방법 --> |
| | <div style="background:#1a1a2e;padding:20px;border-radius:14px;"> |
| | <h4 style="color:#ffd93d;margin:0 0 15px;">🔧 개선 방법</h4> |
| | <div style="display:flex;flex-direction:column;gap:10px;"> |
| | """ |
| | |
| | for item, pts in improve_list.items(): |
| | chk = improvements.get(item, False) |
| | html += f""" |
| | <div style="display:flex;align-items:center;gap:12px;padding:12px; |
| | background:{'rgba(111,217,168,0.1)' if chk else '#252540'}; |
| | border-radius:10px;border:1px solid {'var(--neon-green)' if chk else '#3a3a55'};"> |
| | <span style="color:{'var(--neon-green)' if chk else 'var(--text-secondary)'};font-size:18px;"> |
| | {'✓' if chk else '○'} |
| | </span> |
| | <span style="flex:1;color:#fff;font-size:13px;">{item}</span> |
| | <span style="color:var(--neon-green);font-size:12px;font-weight:600;">+{pts}점</span> |
| | </div> |
| | """ |
| | |
| | html += """ |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <!-- 등급 가이드 --> |
| | <div style="background:#1a1a2e;padding:20px;border-radius:14px;"> |
| | <h4 style="color:#fff;margin:0 0 15px;">📊 점수별 영향</h4> |
| | <div style="display:flex;flex-direction:column;gap:10px;"> |
| | """ |
| | |
| | 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""" |
| | <div style="display:flex;align-items:center;gap:12px;padding:12px; |
| | background:{'rgba(111,217,168,0.1)' if is_cur else 'transparent'}; |
| | border-radius:8px;border:1px solid {'var(--neon-green)' if is_cur else 'transparent'};"> |
| | <div style="width:14px;height:14px;background:{color};border-radius:50%;"></div> |
| | <span style="width:70px;color:#fff;">{lo}-{hi}</span> |
| | <span style="width:50px;color:{color};font-weight:600;">{label}</span> |
| | <span style="color:var(--text-secondary);font-size:12px;">{limit}</span> |
| | </div> |
| | """ |
| | |
| | html += """ |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | """ |
| | return html |
| |
|
| | |
| | |
| | |
| |
|
| | 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""" |
| | <div class="chart-ultra" style="background:linear-gradient(135deg,#0d1117 0%,#161b22 50%,#0d1117 100%);"> |
| | |
| | <!-- 헤더 --> |
| | <div style="text-align:center;margin-bottom:30px;padding:25px; |
| | background:linear-gradient(145deg,#1a2a30,#0d1a1f);border-radius:16px; |
| | border:2px solid var(--neon-green)30;"> |
| | <h2 style="margin:0 0 10px;font-family:'Orbitron',sans-serif;font-size:28px; |
| | background:linear-gradient(135deg,#6fd9a8,#fff,#6fd9a8); |
| | -webkit-background-clip:text;-webkit-text-fill-color:transparent; |
| | animation:glow-text 2s ease-in-out infinite;"> |
| | 🎮 미네랄핵 분석 완료 |
| | </h2> |
| | <p style="color:var(--text-secondary);margin:0;">정책자금 사전심사 종합 리포트</p> |
| | </div> |
| | |
| | <!-- 메인 KPI --> |
| | <div style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:20px;margin-bottom:25px;"> |
| | |
| | <!-- 예상 지원금액 (대형) --> |
| | <div class="score-card highlight" style="padding:30px;"> |
| | <div style="font-size:18px;color:var(--text-secondary);margin-bottom:10px;">💰 예상 지원금액</div> |
| | <div style="font-family:'Orbitron',sans-serif;font-size:64px;font-weight:900; |
| | background:linear-gradient(135deg,#6fd9a8,#fff); |
| | -webkit-background-clip:text;-webkit-text-fill-color:transparent;"> |
| | {amount:.1f}<span style="font-size:32px;">억원</span> |
| | </div> |
| | </div> |
| | |
| | <!-- 종합점수 --> |
| | <div class="score-card" style="border-top:4px solid {score_color};"> |
| | <div class="label">📊 종합점수</div> |
| | <div class="value" style="color:{score_color};">{score}</div> |
| | <div style="margin-top:10px;font-size:13px;color:{score_color};"> |
| | {'우수' if score >= 70 else '양호' if score >= 50 else '주의'} |
| | </div> |
| | </div> |
| | |
| | <!-- 유의사항 --> |
| | <div class="score-card" style="border-top:4px solid {'#ff6b6b' if cautions > 5 else '#ffd93d' if cautions > 0 else '#6fd9a8'};"> |
| | <div class="label">⚠️ 유의사항</div> |
| | <div class="value" style="color:{'#ff6b6b' if cautions > 5 else '#ffd93d'};">{cautions}</div> |
| | <div style="margin-top:10px;font-size:13px;color:var(--text-secondary);">건 해결 필요</div> |
| | </div> |
| | </div> |
| | |
| | <!-- 기관별 확률 바 --> |
| | <div style="background:#1a1a2e;padding:25px;border-radius:16px;margin-bottom:25px;"> |
| | <h4 style="color:var(--neon-green);margin:0 0 20px;font-size:16px;">🏦 기관별 승인확률</h4> |
| | |
| | <div style="display:flex;flex-direction:column;gap:15px;"> |
| | <div style="display:flex;align-items:center;gap:15px;"> |
| | <span style="width:100px;color:var(--text-secondary);">신용보증기금</span> |
| | <div style="flex:1;height:24px;background:#252540;border-radius:12px;overflow:hidden;"> |
| | <div style="width:{sinbo}%;height:100%;background:linear-gradient(90deg,#6fd9a8,#8fe8c0); |
| | border-radius:12px;display:flex;align-items:center;justify-content:flex-end;padding-right:10px;"> |
| | <span style="font-family:'Orbitron',sans-serif;font-size:12px;color:#0d0d1a;font-weight:700;">{sinbo}%</span> |
| | </div> |
| | </div> |
| | </div> |
| | <div style="display:flex;align-items:center;gap:15px;"> |
| | <span style="width:100px;color:var(--text-secondary);">기술보증기금</span> |
| | <div style="flex:1;height:24px;background:#252540;border-radius:12px;overflow:hidden;"> |
| | <div style="width:{kibo}%;height:100%;background:linear-gradient(90deg,#ffd93d,#ffe880); |
| | border-radius:12px;display:flex;align-items:center;justify-content:flex-end;padding-right:10px;"> |
| | <span style="font-family:'Orbitron',sans-serif;font-size:12px;color:#0d0d1a;font-weight:700;">{kibo}%</span> |
| | </div> |
| | </div> |
| | </div> |
| | <div style="display:flex;align-items:center;gap:15px;"> |
| | <span style="width:100px;color:var(--text-secondary);">지역신보재단</span> |
| | <div style="flex:1;height:24px;background:#252540;border-radius:12px;overflow:hidden;"> |
| | <div style="width:{jaedan}%;height:100%;background:linear-gradient(90deg,#6495ed,#8ab4f8); |
| | border-radius:12px;display:flex;align-items:center;justify-content:flex-end;padding-right:10px;"> |
| | <span style="font-family:'Orbitron',sans-serif;font-size:12px;color:#0d0d1a;font-weight:700;">{jaedan}%</span> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <!-- 권고사항 --> |
| | <div style="padding:20px;background:linear-gradient(145deg,#1a2a30,#0d1a1f); |
| | border-radius:14px;border-left:4px solid var(--neon-green);"> |
| | <h4 style="color:var(--neon-green);margin:0 0 15px;">💡 핵심 권고사항</h4> |
| | <ul style="margin:0;padding-left:20px;color:#fff;line-height:1.8;"> |
| | {'<li style="color:#ff6b6b;">불가 사유 우선 해결 필요</li>' if cautions > 3 else ''} |
| | {'<li>신용점수 700점 이상 유지 권장</li>' if credit < 700 else '<li style="color:#6fd9a8;">✓ 신용점수 양호</li>'} |
| | <li>필요서류: 재무제표, 사업자등록증, 납세증명서</li> |
| | <li>추천 기관: {data.get('추천기관', '신용보증기금')}</li> |
| | </ul> |
| | </div> |
| | </div> |
| | """ |
| | 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) |
| |
|
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| |
|
| | 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", "")), |
| | gr.update(value=ci.get("biz_num", "")), |
| | gr.update(value=ci.get("industry", "C 제조업")), |
| | gr.update(value=ci.get("sales", 5)), |
| | gr.update(value=ci.get("years", 3)), |
| | gr.update(value=fi.get("credit_score", 720)), |
| | gr.update(value=ci.get("employees", 10)), |
| | gr.update(value=ci.get("education", "학사")), |
| | gr.update(value=ci.get("tech_grade", "중급")), |
| | gr.update(value=ci.get("has_patent", False)), |
| | gr.update(value=ci.get("has_venture", False)), |
| | gr.update(value=ci.get("has_innobiz", False)), |
| | gr.update(value=fi.get("request_amt", 3)), |
| | gr.update(value=fi.get("total_assets", 1000)), |
| | gr.update(value=fi.get("total_liab", 600)), |
| | gr.update(value=fi.get("cur_assets", 500)), |
| | gr.update(value=fi.get("cur_liab", 400)), |
| | gr.update(value=fi.get("op_profit", 50)), |
| | gr.update(value=fi.get("net_income", 30)), |
| | gr.update(value=fi.get("interest_exp", 10)), |
| | gr.update(value=cc), |
| | gr.update(value=fi.get("existing_guar", 0)), |
| | gr.update(value=fi.get("related_sales", 0)), |
| | msg |
| | ] |
| |
|
| |
|
| | def create_fund_tab(): |
| | """정책자금 탭 생성""" |
| | |
| | with gr.Tab("💰 정책자금 사전심사"): |
| | gr.HTML(''' |
| | <div style="background:linear-gradient(135deg,#0d1117,#161b22,#0d1117);padding:35px;border-radius:20px; |
| | margin-bottom:24px;border:2px solid rgba(111,217,168,0.3);text-align:center; |
| | box-shadow:0 10px 40px rgba(0,0,0,0.5);"> |
| | <h2 style="margin:0 0 10px;font-family:'Orbitron',sans-serif; |
| | background:linear-gradient(135deg,#6fd9a8,#fff,#6fd9a8); |
| | -webkit-background-clip:text;-webkit-text-fill-color:transparent;font-size:32px; |
| | animation:glow-text 2s ease-in-out infinite;"> |
| | 🎮 미네랄핵 ULTRA v7.0 |
| | </h2> |
| | <p style="color:#8888a0;margin:0;font-size:14px;">정책자금 사전심사 시스템 | 11개 비주얼 차트</p> |
| | </div> |
| | ''') |
| | |
| | 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(""" |
| | <div style="text-align:center;padding:40px;background:linear-gradient(135deg,#0d1117,#161b22); |
| | border-radius:20px;margin-bottom:20px;border:1px solid rgba(111,217,168,0.2);"> |
| | <h1 style="font-family:'Orbitron',sans-serif;font-size:42px;margin:0; |
| | background:linear-gradient(135deg,#6fd9a8,#fff,#6fd9a8); |
| | -webkit-background-clip:text;-webkit-text-fill-color:transparent;"> |
| | 🎮 미네랄핵 ULTRA v7.0 |
| | </h1> |
| | <p style="color:#8888a0;margin:15px 0 0;font-size:16px;">OKCEO 정책자금 사전심사 시스템</p> |
| | </div> |
| | """) |
| | create_fund_tab() |
| | return app |
| |
|
| |
|
| | if __name__ == "__main__": |
| | app = create_app() |
| | app.launch(server_name="0.0.0.0", server_port=7860) |