import gradio as gr
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from xgboost import XGBClassifier
import time
import os
import re
import json
# ============================================================
# 0-A. Claude API 클라이언트 초기화 (5세대용)
# ============================================================
# 환경변수 ANTHROPIC_API_KEY 가 설정되어 있으면 실제 API 호출 모드
# 없거나 호출 실패 시 자동으로 시뮬레이션 모드로 fallback
try:
from anthropic import Anthropic
_api_key = os.environ.get("ANTHROPIC_API_KEY")
if _api_key:
claude_client = Anthropic(api_key=_api_key)
CLAUDE_AVAILABLE = True
else:
claude_client = None
CLAUDE_AVAILABLE = False
except ImportError:
claude_client = None
CLAUDE_AVAILABLE = False
CLAUDE_MODEL = "claude-sonnet-4-6" # 2026년 5월 현재 권장 모델
# ============================================================
# 0. 공통 설정
# ============================================================
FEATURES = ['금액', '시간', '신규수취인', '금액비율']
def build_training_data():
"""학습 데이터 생성 (재현 가능하도록 시드 고정)"""
np.random.seed(42)
normal = pd.DataFrame({
'금액': np.random.normal(100, 50, 20),
'시간': np.random.normal(14, 4, 20),
'신규수취인': np.random.binomial(1, 0.1, 20),
'금액비율': np.random.normal(1, 0.5, 20),
'라벨': 0
})
fraud = pd.DataFrame({
'금액': np.random.normal(500, 200, 5),
'시간': np.random.normal(3, 2, 5),
'신규수취인': np.random.binomial(1, 0.8, 5),
'금액비율': np.random.normal(10, 5, 5),
'라벨': 1
})
return pd.concat([normal, fraud], ignore_index=True)
def train_gen2():
data = build_training_data()
model = LogisticRegression(random_state=42, max_iter=1000)
model.fit(data[FEATURES], data['라벨'])
return model
def train_gen3():
data = build_training_data()
model = XGBClassifier(n_estimators=10, max_depth=3, learning_rate=0.1,
random_state=42, eval_metric='logloss')
model.fit(data[FEATURES], data['라벨'])
return model
gen2_model = train_gen2()
gen3_model = train_gen3()
# 학습된 파라미터 추출 (강의용 노출 목적)
GEN2_COEF = gen2_model.coef_[0]
GEN2_INTERCEPT = gen2_model.intercept_[0]
GEN3_IMPORTANCE = gen3_model.feature_importances_
# ============================================================
# 1. 1세대 룰 정의
# ============================================================
GEN1_RULES = [
{"name": "R1 고액 거래", "condition": "금액 ≥ 500만원", "weight": 40},
{"name": "R2 새벽 시간대", "condition": "시간 ≤ 6 또는 ≥ 22", "weight": 30},
{"name": "R3 신규 수취인", "condition": "신규수취인 = 예", "weight": 20},
{"name": "R4 평소 대비 급증", "condition": "금액비율 ≥ 5배", "weight": 10},
]
def evaluate_gen1(amount, hour, new_payee_bin, ratio):
triggered = [
amount >= 500,
hour >= 22 or hour <= 6,
new_payee_bin == 1,
ratio >= 5,
]
score = sum(r["weight"] for r, t in zip(GEN1_RULES, triggered) if t)
return triggered, score
def decide(prob_or_score, is_score=False):
if is_score:
if prob_or_score >= 70: return "차단", "#FCEBEB", "#791F1F"
if prob_or_score >= 40: return "추가 인증", "#FAEEDA", "#854F0B"
return "통과", "#EAF3DE", "#3B6D11"
else:
if prob_or_score >= 0.7: return "차단", "#FCEBEB", "#791F1F"
if prob_or_score >= 0.5: return "추가 인증", "#FAEEDA", "#854F0B"
return "통과", "#EAF3DE", "#3B6D11"
# ============================================================
# 2. 공통 HTML 빌더
# ============================================================
def card_header(gen_label, title, decision_text, bg_color, text_color, sub):
return f"""
"""
def formula_box(html):
return f"""{html}
"""
CARD_STYLE = ("background:#fff; border:0.5px solid rgba(0,0,0,0.15); "
"border-radius:12px; padding:16px 20px; margin-bottom:14px;")
# ============================================================
# 3. 세대별 HTML 생성 함수
# ============================================================
def render_gen1(amount, hour, new_payee_bin, ratio):
triggered, score = evaluate_gen1(amount, hour, new_payee_bin, ratio)
dec, bg, fg = decide(score, is_score=True)
rows = ""
for rule, t in zip(GEN1_RULES, triggered):
applied = rule["weight"] if t else 0
row_bg = "#FAECE7" if t else "#ffffff"
td_color = "#4A1B0C" if t else "#444"
sub_color = "#712B13" if t else "#666"
mark = "✓" if t else "—"
rows += f"""
{rule['name']}
{rule['condition']}
+{rule['weight']}
{mark}
+{applied}
"""
return f"""
{card_header("GEN 1 · RULE-BASED", "규칙 기반 판단", dec, bg, fg, f"누적 {score}점 / 100점")}
{formula_box("총점 = Σ (발동된 룰의 가중치) → 임계값 비교 (≥70 차단 / ≥40 추가인증)")}
룰
조건
가중치
발동
적용
{rows}
최종 합계
{score}점
한계: 룰이 고정값이라 임계값 바로 아래(예: 499만원 23시) 거래를 놓침
"""
def render_gen2(amount, hour, new_payee_bin, ratio):
input_vec = np.array([amount, hour, new_payee_bin, ratio], dtype=float)
contributions = GEN2_COEF * input_vec
logit = contributions.sum() + GEN2_INTERCEPT
prob = 1 / (1 + np.exp(-logit))
dec, bg, fg = decide(prob)
rows = ""
for f, x, w, c in zip(FEATURES, input_vec, GEN2_COEF, contributions):
if c > 0:
row_bg, td_c, sub_c = "#FAECE7", "#4A1B0C", "#712B13"
elif c < 0:
row_bg, td_c, sub_c = "#E1F5EE", "#04342C", "#085041"
else:
row_bg, td_c, sub_c = "#ffffff", "#444", "#666"
rows += f"""
{f}
{x:.3f}
{w:+.4f}
{c:+.4f}
"""
contrib_str = " + ".join([f"({c:+.4f})" for c in contributions])
calc_html = (
f"z = {contrib_str} + ({GEN2_INTERCEPT:+.4f}) "
f"z = {logit:+.4f} "
f"P = 1 / (1 + e{-logit:+.4f} ) = "
f"{prob:.4f} → {prob*100:.2f}% "
)
return f"""
{card_header("GEN 2 · LOGISTIC REGRESSION", "로지스틱 회귀", dec, bg, fg, f"사기 확률 {prob*100:.2f}%")}
{formula_box("z = w₁·금액 + w₂·시간 + w₃·신규수취인 + w₄·금액비율 + b
P(사기) = 1 / (1 + e
-z )")}
피처
입력값 x
학습 가중치 w
기여도 w·x
{rows}
절편 (bias) b
{GEN2_INTERCEPT:+.4f}
{formula_box(calc_html)}
해석: 가중치 부호가 곧 판단 방향. 양수는 사기 쪽, 음수는 정상 쪽으로 끌어당김
"""
def render_gen3(amount, hour, new_payee_bin, ratio):
input_df = pd.DataFrame([[amount, hour, new_payee_bin, ratio]], columns=FEATURES)
prob = float(gen3_model.predict_proba(input_df)[0][1])
dec, bg, fg = decide(prob)
imp_pairs = sorted(zip(FEATURES, GEN3_IMPORTANCE), key=lambda x: -x[1])
max_imp = max(GEN3_IMPORTANCE) if max(GEN3_IMPORTANCE) > 0 else 1
imp_bars = ""
for f, imp in imp_pairs:
bar_w = (imp / max_imp) * 100
imp_bars += f"""
"""
# 대표 트리 분기 경로 (입력값 기반)
if ratio < 4.8:
leaf_val = "-0.18" if amount < 320 else "+0.12"
path_lines = [
f"[금액비율 < 4.8] → Yes (입력 {ratio:.1f})",
f" └─ [금액 < 320] → {'Yes' if amount < 320 else 'No'} → leaf={leaf_val} ← 본 거래 도달",
]
else:
leaf_val = "+0.41" if new_payee_bin == 1 else "+0.08"
path_lines = [
f"[금액비율 < 4.8] → No (입력 {ratio:.1f})",
f" └─ [신규수취인 < 0.5] → {'Yes' if new_payee_bin == 0 else 'No'} → leaf={leaf_val} ← 본 거래 도달",
]
tree_html = " ".join(path_lines)
return f"""
{card_header("GEN 3 · XGBOOST (TREE ENSEMBLE)", "그래디언트 부스팅 트리", dec, bg, fg, f"사기 확률 {prob*100:.2f}%")}
{formula_box("F(x) = Σ
k=1..K f
k (x), f
k ∈ 트리 공간
P(사기) = sigmoid(F(x)) [K=10, max_depth=3, lr=0.1]")}
피처 중요도 (Gain 기반)
{imp_bars}
대표 트리 (Tree #1) - 본 거래 분기 경로
{formula_box(tree_html)}
트리 10개의 leaf 값을 합산한 raw score를 시그모이드로 변환하여 최종 확률 산출
"""
def render_gen4(amount, hour, new_payee_bin, ratio, prob3):
"""4세대 GNN 시뮬레이션"""
layer1_contrib = -0.12
if new_payee_bin == 1:
layer2_contrib = 1.84
layer2_desc = "수취인 → 사기계좌 연결 감지"
layer2_bg = "#FAECE7"
layer2_td = "#4A1B0C"
layer2_sub = "#712B13"
else:
layer2_contrib = 0.05
layer2_desc = "수취인 → 정상 이력만 확인"
layer2_bg = "#ffffff"
layer2_td = "#444"
layer2_sub = "#666"
mlp_contrib = layer1_contrib + layer2_contrib \
+ (1.2 if amount >= 500 else 0) \
+ (0.7 if (hour <= 6 or hour >= 22) else 0)
raw_score = np.log(prob3 / (1 - prob3 + 1e-9)) + mlp_contrib * 0.5
prob = float(1 / (1 + np.exp(-raw_score)))
prob = min(prob, 0.99)
dec, bg, fg = decide(prob)
edge2_color = "#D85A30" if new_payee_bin == 1 else "#888780"
edge2_dash = 'stroke-dasharray="" ' if new_payee_bin == 1 else 'stroke-dasharray="3,3" '
risk_fill = "#F7C1C1" if new_payee_bin == 1 else "#D3D1C7"
risk_stroke = "#A32D2D" if new_payee_bin == 1 else "#5F5E5A"
risk_text = "사기계좌" if new_payee_bin == 1 else "일반"
risk_color = "#501313" if new_payee_bin == 1 else "#444441"
payee_type = "신규" if new_payee_bin == 1 else "기존"
svg = f"""
송금인
정상이력 95%
단말기
신규 IP
본 거래
{amount:.0f}만 / {int(hour):02d}시
수취인
{payee_type} 1-hop
{risk_text}
일반
1-hop 이웃
2-hop 이웃
"""
table_html = f"""
계층
집계 내용
기여
Layer 1
송금인 + 단말기 임베딩 평균
{layer1_contrib:+.2f}
Layer 2
{layer2_desc}
{layer2_contrib:+.2f}
MLP
거래 임베딩 → 분류기 (raw score)
{mlp_contrib:+.2f}
"""
return prob, f"""
{card_header("GEN 4 · GNN (GRAPH NEURAL NETWORK)", "그래프 신경망 (시뮬레이션)", dec, bg, fg, f"사기 확률 {prob*100:.2f}%")}
{formula_box("h
v (l+1) = σ(W · AGG({{h
u (l) : u ∈ N(v)}}) + B · h
v (l) )
P(사기) = MLP(h
거래 (L) ) [2-hop 메시지 패싱]")}
2-hop 이웃 그래프
{svg}
이웃 집계 결과 (Message Passing)
{table_html}
강점: 1-3세대가 보지 못하는 '관계망'을 학습. 수취인이 2-hop 내 사기계좌와 연결된 사실을 자동 포착
"""
# ============================================================
# 5세대 — Claude API 실제 호출
# ============================================================
def build_claude_prompt(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec):
"""Claude에게 보낼 시스템 프롬프트와 사용자 메시지 구성"""
system_prompt = (
"당신은 한국 은행의 FDS(이상금융거래탐지시스템) 분석 전문가입니다. "
"주어진 거래 정보와 1-4세대 모델의 사전 판단을 바탕으로 사기 여부를 종합 판정하세요. "
"특히 부동산 잔금 송금, 사업자 대금 결제 같은 정상 거래 패턴과 보이스피싱·대포통장 패턴을 구분해야 합니다. "
"반드시 다음 JSON 형식으로만 응답하세요(다른 텍스트 금지):\n"
'{\n'
' "risk_score": <0.0~1.0 사이 사기 의심도>,\n'
' "decision": "차단" | "추가 인증" | "통과",\n'
' "reasoning_steps": [\n'
' {"step": "<추론 단계>", "attention": <0.0~1.0>},\n'
' ... (3~5개)\n'
' ],\n'
' "judgment": "<최종 판단 자연어 (왜 그렇게 판단했는지 + 권고 조치)>"\n'
'}'
)
payee_str = "신규" if new_payee_bin == 1 else "기존"
time_period = "새벽" if (hour <= 6 or hour >= 22) else ("주간" if 9 <= hour <= 18 else "저녁")
user_message = (
f"[거래 정보]\n"
f"- 금액: {amount:.0f}만원\n"
f"- 거래 시간: {int(hour):02d}시 ({time_period})\n"
f"- 수취인: {payee_str} 수취인\n"
f"- 과거 대비 배율: {ratio:.1f}배 (송금인의 평균 거래액 대비)\n\n"
f"[1-4세대 모델 사전 판단]\n"
f"- 평균 사기 확률: {prior_avg*100:.1f}%\n"
f"- 종합 판정: {prior_dec}\n\n"
f"위 거래에 대해 FDS 전문가 관점에서 종합 판정해주세요."
)
return system_prompt, user_message
def call_claude_api(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec):
"""실제 Claude API 호출. 성공 시 (parsed_dict, raw_meta) 반환, 실패 시 (None, error_msg)"""
if not CLAUDE_AVAILABLE:
return None, "API 키 미설정 (ANTHROPIC_API_KEY 환경변수 없음)"
system_prompt, user_message = build_claude_prompt(
amount, hour, new_payee_bin, ratio, prior_avg, prior_dec
)
try:
t0 = time.time()
response = claude_client.messages.create(
model=CLAUDE_MODEL,
max_tokens=1024,
temperature=0.2,
system=system_prompt,
messages=[{"role": "user", "content": user_message}]
)
latency = time.time() - t0
raw_text = response.content[0].text.strip()
# JSON 추출: 응답 중 첫 번째 { 부터 마지막 } 까지
json_match = re.search(r'\{.*\}', raw_text, re.DOTALL)
if not json_match:
return None, f"JSON 파싱 실패: {raw_text[:200]}"
parsed = json.loads(json_match.group(0))
meta = {
"input_tokens": response.usage.input_tokens,
"output_tokens": response.usage.output_tokens,
"latency": latency,
"model": CLAUDE_MODEL,
"raw_text": raw_text,
}
return parsed, meta
except Exception as e:
return None, f"API 호출 실패: {type(e).__name__}: {str(e)[:200]}"
def render_gen5_card(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec,
prob, decision_text, reasoning_steps, judgment_text,
meta_html, source_label):
"""5세대 카드 HTML 렌더링 (API/시뮬레이션 공용)"""
dec, bg, fg = decide(prob)
context_html = (
f"[SYS] 당신은 한국 은행의 FDS 분석 전문가입니다. 거래 정보와 1-4세대 사전 판단을 토대로 종합 판정하세요. "
f"[INPUT] amount={amount:.0f}만, hour={int(hour):02d}, "
f"new_payee={'true' if new_payee_bin==1 else 'false'}, ratio={ratio:.1f}× "
f"[PRIOR] 1-4세대 평균: {prior_avg*100:.1f}% / 종합: {prior_dec} "
f"[TASK] JSON 형식으로 risk_score, decision, reasoning_steps, judgment 출력"
)
cot_rows = ""
for i, step in enumerate(reasoning_steps, 1):
step_text = step.get("step", "") if isinstance(step, dict) else str(step)
attn = step.get("attention", 0.0) if isinstance(step, dict) else 0.0
try:
attn = float(attn)
except (ValueError, TypeError):
attn = 0.0
cot_rows += (
f"{i} "
f"{step_text} "
f"{attn:.2f} "
)
judg_color = "#3B6D11" if prob < 0.5 else "#633806"
judgment_html = (
f"{decision_text} 권고 (의심도 {prob*100:.0f}%) "
f"{judgment_text}"
)
return f"""
{card_header("GEN 5 · FOUNDATION MODEL (LLM)", f"초거대 추론 모델 ({source_label})", dec, bg, fg, f"의심도 {prob*100:.0f}%")}
컨텍스트 토큰화
{context_html}
추론 체인 (Chain-of-Thought)
단계
추론 내용
Attention
{cot_rows}
생성된 자연어 판단
{judgment_html}
생성 파라미터 보기
{meta_html}
강점: 사전 학습된 도메인 지식으로 '왜 사기인지' 자연어 설명 + 권고 조치까지 자동 생성
"""
def render_gen5_simulation(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec):
"""API 호출 실패/미설정 시 fallback용 시뮬레이션"""
is_high_risk = (new_payee_bin == 1 and (hour <= 6 or hour >= 22) and amount >= 500)
prob = 0.95 if is_high_risk else min(prior_avg + 0.02, 0.98)
payee_str = "신규 수취인" if new_payee_bin == 1 else "기존 수취인"
time_str = "새벽" if (hour <= 6 or hour >= 22) else "일반"
reasoning_steps = [
{"step": f"시간 {int(hour):02d}시 + {payee_str} → 보이스피싱 정형 패턴 매칭", "attention": 0.42},
{"step": f"평소 {ratio:.1f}배 금액 → 송금인 평소 행동 이탈도 측정", "attention": 0.28},
{"step": f"4세대 GNN 신호 + {amount:.0f}만원 고액 → 위험 가중", "attention": 0.21},
{"step": "최근 보이스피싱 트렌드 사전 지식 활용", "attention": 0.09},
]
if is_high_risk:
decision_text = "차단"
judgment_text = (
f"{int(hour):02d}시 {time_str} 시간대에 평소보다 {ratio:.1f}배 급증한 {amount:.0f}만원이 "
f"{payee_str} 에게 이체되는 것은 전형적인 보이스피싱 패턴입니다. "
f"수취인 계좌가 사기 의심 계좌와 2-hop 거리에 있다는 GNN 신호까지 더해져 위험도가 매우 높습니다. "
f"권고 조치: ① 즉시 거래 보류, ② 등록된 전화번호로 본인 직접 확인, ③ 확인 전 자금 동결 24시간 유지"
)
elif prob >= 0.5:
decision_text = "추가 인증"
judgment_text = (
f"{int(hour):02d}시 거래에서 일부 이상 신호({ratio:.1f}배 금액, {payee_str})가 감지되었으나 "
f"결정적 위험 패턴은 아닙니다. 차단보다는 추가 인증으로 본인 의사를 확인하는 것이 적절합니다. "
f"권고 조치: ① ARS 또는 OTP 추가 인증, ② 송금 의도 재확인 메시지 발송"
)
else:
decision_text = "통과"
judgment_text = (
f"{int(hour):02d}시 거래의 패턴이 송금인의 평소 행동 범위 내에 있으며, "
f"1-4세대 모델 모두 위험 신호를 강하게 보내지 않았습니다. 정상 거래로 판단됩니다. "
f"권고 조치: 별도 조치 없이 거래 진행"
)
meta_html = (
"mode: SIMULATION (API 미연결) "
"이 결과는 if-else 하드코딩으로 생성된 시뮬레이션입니다. "
"실제 Claude 호출을 활성화하려면 ANTHROPIC_API_KEY 환경변수를 설정하세요."
)
return render_gen5_card(
amount, hour, new_payee_bin, ratio, prior_avg, prior_dec,
prob, decision_text, reasoning_steps, judgment_text,
meta_html, source_label="시뮬레이션 모드"
)
def render_gen5(amount, hour, new_payee_bin, ratio, prior_avg):
"""5세대 진입점: API 가능 시 Claude 호출, 실패 시 시뮬레이션 fallback"""
prior_dec, _, _ = decide(prior_avg)
if not CLAUDE_AVAILABLE:
return render_gen5_simulation(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec)
parsed, meta_or_err = call_claude_api(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec)
if parsed is None:
fallback_html = render_gen5_simulation(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec)
warning = (
f''
f'⚠️ Claude API 호출 실패. 시뮬레이션 모드로 대체합니다. '
f'사유: {meta_or_err} '
f'
'
)
return warning + fallback_html
try:
prob = float(parsed.get("risk_score", 0.5))
prob = max(0.0, min(1.0, prob))
decision_text = parsed.get("decision", "추가 인증")
reasoning_steps = parsed.get("reasoning_steps", [])
judgment_text = parsed.get("judgment", "(판단 내용 누락)")
meta = meta_or_err
meta_html = (
f"model: {meta['model']} / temperature: 0.2 / max_tokens: 1024 "
f"input_tokens: {meta['input_tokens']} / output_tokens: {meta['output_tokens']} / "
f"latency: {meta['latency']:.2f}s "
f"mode: LIVE API CALL ✓ "
)
return render_gen5_card(
amount, hour, new_payee_bin, ratio, prior_avg, prior_dec,
prob, decision_text, reasoning_steps, judgment_text,
meta_html, source_label=f"실제 {CLAUDE_MODEL}"
)
except (KeyError, ValueError, TypeError) as e:
fallback_html = render_gen5_simulation(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec)
warning = (
f''
f'⚠️ Claude 응답 파싱 실패. 시뮬레이션 모드로 대체합니다. '
f''
f'{type(e).__name__}: {str(e)[:150]}
'
)
return warning + fallback_html
# ============================================================
# 4. 메인 분석 함수
# ============================================================
def analyze_transaction(amount, hour, new_payee, ratio):
start_time = time.time()
new_payee_bin = 1 if new_payee == "예" else 0
# 1세대
g1 = render_gen1(amount, hour, new_payee_bin, ratio)
_, score1 = evaluate_gen1(amount, hour, new_payee_bin, ratio)
prob1 = min(score1 / 100, 0.99)
# 2세대
g2 = render_gen2(amount, hour, new_payee_bin, ratio)
input_vec = np.array([amount, hour, new_payee_bin, ratio], dtype=float)
logit2 = (GEN2_COEF * input_vec).sum() + GEN2_INTERCEPT
prob2 = float(1 / (1 + np.exp(-logit2)))
# 3세대
g3 = render_gen3(amount, hour, new_payee_bin, ratio)
input_df = pd.DataFrame([[amount, hour, new_payee_bin, ratio]], columns=FEATURES)
prob3 = float(gen3_model.predict_proba(input_df)[0][1])
# 4세대
prob4, g4 = render_gen4(amount, hour, new_payee_bin, ratio, prob3)
# 5세대 (1-4세대 평균을 prior로)
prior_avg = (prob1 + prob2 + prob3 + prob4) / 4
g5 = render_gen5(amount, hour, new_payee_bin, ratio, prior_avg)
elapsed = time.time() - start_time
summary = f"""
분석 대상 거래
분석 소요시간: {elapsed:.3f}초
"""
return summary + g1 + g2 + g3 + g4 + g5
# ============================================================
# 5. Gradio UI
# ============================================================
with gr.Blocks(theme=gr.themes.Default(), title="FDS 1-5세대 비교 데모") as demo:
_api_badge = (
f''
f'● Claude API 연결됨 ({CLAUDE_MODEL}) '
if CLAUDE_AVAILABLE else
''
'○ API 미연결 (5세대 시뮬레이션 모드) '
)
gr.HTML(f"""
🛡️ FDS 1-5세대 비교 데모
실무 운영 담당자 시연용 · 판단 수식·가중치·근거 전체 노출 모드
{_api_badge}
""")
with gr.Row():
with gr.Column(scale=1):
amount_in = gr.Number(label="금액 (만원)", value=700)
hour_in = gr.Slider(label="거래 시간 (0-23시)", minimum=0, maximum=23, value=3, step=1)
payee_in = gr.Radio(label="신규 수취인", choices=["아니오", "예"], value="예")
ratio_in = gr.Number(label="과거 대비 배율", value=14.0)
submit_btn = gr.Button("🔍 분석 실행", variant="primary")
gr.Examples(
examples=[
[700, 3, "예", 14.0],
[800, 23, "예", 8.0],
[45, 2, "아니오", 1.2],
[1500, 14, "아니오", 2.0],
[499, 23, "예", 4.5],
],
inputs=[amount_in, hour_in, payee_in, ratio_in],
label="시연 예시 (마지막은 1세대 룰을 회피하는 케이스)"
)
with gr.Column(scale=2):
output_html = gr.HTML(
""
"좌측에서 거래 조건을 설정하고 [분석 실행] 버튼을 누르세요."
"
"
)
submit_btn.click(
fn=analyze_transaction,
inputs=[amount_in, hour_in, payee_in, ratio_in],
outputs=output_html
)
if __name__ == "__main__":
demo.launch(ssr_mode=False)