markets / app.py
seawolf2357's picture
Update app.py
cef3bdf verified
"""
AI 기반 μƒκΆŒ 뢄석 μ‹œμŠ€ν…œ - Comic Classic Theme 버전
Dataset: https://huggingface.co/datasets/ginipick/market
"""
import gradio as gr
import pandas as pd
import numpy as np
from typing import Dict, List, Tuple
import json
from datasets import load_dataset
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import folium
from folium.plugins import HeatMap, MarkerCluster
import requests
from collections import Counter
import re
import os
import time
# ============================================================================
# Brave 웹검색 ν΄λΌμ΄μ–ΈνŠΈ
# ============================================================================
class BraveSearchClient:
"""Brave Search API ν΄λΌμ΄μ–ΈνŠΈ"""
def __init__(self, api_key: str = None):
self.api_key = api_key or os.getenv("BRAVE_API_KEY")
self.base_url = "https://api.search.brave.com/res/v1/web/search"
def search(self, query: str, count: int = 5) -> str:
"""μ›Ή 검색 μˆ˜ν–‰"""
if not self.api_key:
return "⚠️ Brave Search API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."
headers = {
"Accept": "application/json",
"X-Subscription-Token": self.api_key
}
params = {
"q": query,
"count": count,
"text_decorations": False,
"search_lang": "ko"
}
try:
response = requests.get(self.base_url, headers=headers, params=params, timeout=10)
if response.status_code == 200:
data = response.json()
results = []
if 'web' in data and 'results' in data['web']:
for item in data['web']['results'][:count]:
title = item.get('title', '')
description = item.get('description', '')
url = item.get('url', '')
results.append(f"πŸ“„ **{title}**\n{description}\nπŸ”— {url}")
return "\n\n".join(results) if results else "검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€."
else:
return f"⚠️ 검색 μ‹€νŒ¨: {response.status_code}"
except Exception as e:
return f"⚠️ 검색 였λ₯˜: {str(e)}"
# ============================================================================
# 데이터 λ‘œλ” 클래슀
# ============================================================================
class MarketDataLoader:
"""ν—ˆκΉ…νŽ˜μ΄μŠ€ μƒκΆŒ 데이터 λ‘œλ”"""
REGIONS = {
'μ„œμšΈ': 'μ„œμšΈ_202506', 'κ²½κΈ°': 'κ²½κΈ°_202506', 'λΆ€μ‚°': 'λΆ€μ‚°_202506',
'λŒ€κ΅¬': 'λŒ€κ΅¬_202506', '인천': '인천_202506', 'κ΄‘μ£Ό': 'κ΄‘μ£Ό_202506',
'λŒ€μ „': 'λŒ€μ „_202506', 'μšΈμ‚°': 'μšΈμ‚°_202506', 'μ„Έμ’…': 'μ„Έμ’…_202506',
'경남': '경남_202506', '경뢁': '경뢁_202506', '전남': '전남_202506',
'전뢁': '전뢁_202506', '좩남': '좩남_202506', '좩뢁': '좩뢁_202506',
'강원': '강원_202506', '제주': '제주_202506'
}
# μ—…μ’… λΆ„λ₯˜ λ§€ν•‘
CATEGORY_MAPPING = {
'G2': 'μ†Œλ§€μ—…',
'I1': 'μˆ™λ°•μ—…',
'I2': 'μŒμ‹μ μ—…',
'L1': '뢀동산업',
'M1': 'μ „λ¬Έ/κ³Όν•™/기술',
'N1': '사업지원/μž„λŒ€',
'P1': 'κ΅μœ‘μ„œλΉ„μŠ€',
'Q1': '보건의료',
'R1': '예술/슀포츠/μ—¬κ°€',
'S2': '수리/κ°œμΈμ„œλΉ„μŠ€'
}
@staticmethod
def load_region_data(region: str, sample_size: int = 30000) -> pd.DataFrame:
"""지역별 데이터 λ‘œλ“œ"""
try:
file_name = f"μ†Œμƒκ³΅μΈμ‹œμž₯μ§„ν₯곡단_상가(μƒκΆŒ)정보_{MarketDataLoader.REGIONS[region]}.csv"
dataset = load_dataset("ginipick/market", data_files=file_name, split="train")
df = dataset.to_pandas()
if len(df) > sample_size:
df = df.sample(n=sample_size, random_state=42)
return df
except Exception as e:
print(f"데이터 λ‘œλ“œ μ‹€νŒ¨: {str(e)}")
return pd.DataFrame()
@staticmethod
def load_multiple_regions(regions: List[str], sample_per_region: int = 30000) -> pd.DataFrame:
"""μ—¬λŸ¬ μ§€μ—­ 데이터 λ‘œλ“œ"""
dfs = []
for region in regions:
df = MarketDataLoader.load_region_data(region, sample_per_region)
if not df.empty:
dfs.append(df)
if dfs:
return pd.concat(dfs, ignore_index=True)
return pd.DataFrame()
# ============================================================================
# μƒκΆŒ 뢄석 클래슀
# ============================================================================
class MarketAnalyzer:
"""μƒκΆŒ 데이터 뢄석 μ—”μ§„"""
def __init__(self, df: pd.DataFrame):
self.df = df
self.prepare_data()
def prepare_data(self):
"""데이터 μ „μ²˜λ¦¬"""
if '경도' in self.df.columns:
self.df['경도'] = pd.to_numeric(self.df['경도'], errors='coerce')
if 'μœ„λ„' in self.df.columns:
self.df['μœ„λ„'] = pd.to_numeric(self.df['μœ„λ„'], errors='coerce')
self.df = self.df.dropna(subset=['경도', 'μœ„λ„'])
# μΈ΅ 정보 μ •μ œ
if '측정보' in self.df.columns:
self.df['측정보_숫자'] = self.df['측정보'].apply(self._parse_floor)
def _parse_floor(self, floor_str):
"""μΈ΅ 정보λ₯Ό 숫자둜 λ³€ν™˜"""
if pd.isna(floor_str):
return None
floor_str = str(floor_str)
if 'μ§€ν•˜' in floor_str or 'B' in floor_str:
match = re.search(r'\d+', floor_str)
return -int(match.group()) if match else -1
elif '1μΈ΅' in floor_str or floor_str == '1':
return 1
else:
match = re.search(r'\d+', floor_str)
return int(match.group()) if match else None
def get_comprehensive_insights(self) -> List[Dict]:
"""포괄적인 μΈμ‚¬μ΄νŠΈ 생성"""
insights = []
insights.append(self._create_top_categories_chart())
insights.append(self._create_major_category_pie())
insights.append(self._create_floor_analysis())
insights.append(self._create_diversity_index())
insights.append(self._create_franchise_analysis())
insights.append(self._create_floor_preference())
insights.append(self._create_district_density())
insights.append(self._create_category_correlation())
insights.append(self._create_subcategory_trends())
insights.append(self._create_regional_specialization())
return insights
def _create_top_categories_chart(self) -> Dict:
"""업쒅별 점포 수 차트"""
if 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' not in self.df.columns:
return None
top_categories = self.df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].value_counts().head(15)
fig = px.bar(
x=top_categories.values,
y=top_categories.index,
orientation='h',
labels={'x': '점포 수', 'y': 'μ—…μ’…'},
title='πŸ† μƒμœ„ μ—…μ’… TOP 15',
color=top_categories.values,
color_continuous_scale='blues'
)
fig.update_layout(showlegend=False, height=500)
return {'type': 'plot', 'data': fig, 'title': '업쒅별 점포 수 뢄석'}
def _create_major_category_pie(self) -> Dict:
"""λŒ€λΆ„λ₯˜λ³„ 뢄포"""
if 'μƒκΆŒμ—…μ’…λŒ€λΆ„λ₯˜μ½”λ“œ' not in self.df.columns:
return None
major_counts = self.df['μƒκΆŒμ—…μ’…λŒ€λΆ„λ₯˜μ½”λ“œ'].value_counts()
labels = [MarketDataLoader.CATEGORY_MAPPING.get(code, code) for code in major_counts.index]
fig = px.pie(
values=major_counts.values,
names=labels,
title='πŸ“Š μ—…μ’… λŒ€λΆ„λ₯˜ 뢄포',
hole=0.4,
color_discrete_sequence=px.colors.qualitative.Set3
)
fig.update_traces(textposition='inside', textinfo='percent+label')
return {'type': 'plot', 'data': fig, 'title': 'λŒ€λΆ„λ₯˜λ³„ μƒκΆŒ ꡬ성'}
def _create_floor_analysis(self) -> Dict:
"""측별 뢄포 상세 뢄석"""
if '측정보_숫자' not in self.df.columns:
return None
floor_data = self.df['측정보_숫자'].dropna()
floor_counts = floor_data.value_counts().sort_index()
underground = floor_counts[floor_counts.index < 0].sum()
first_floor = floor_counts.get(1, 0)
upper_floors = floor_counts[floor_counts.index > 1].sum()
fig = go.Figure(data=[
go.Bar(
x=['μ§€ν•˜', '1μΈ΅', '2μΈ΅ 이상'],
y=[underground, first_floor, upper_floors],
text=[f'{underground:,}<br>({underground/len(floor_data)*100:.1f}%)',
f'{first_floor:,}<br>({first_floor/len(floor_data)*100:.1f}%)',
f'{upper_floors:,}<br>({upper_floors/len(floor_data)*100:.1f}%)'],
textposition='auto',
marker_color=['#e74c3c', '#3498db', '#95a5a6']
)
])
fig.update_layout(
title='🏒 측별 점포 뢄포 (μ§€ν•˜ vs 1μΈ΅ vs 상측)',
xaxis_title='μΈ΅ ꡬ뢄',
yaxis_title='점포 수',
height=400
)
return {'type': 'plot', 'data': fig, 'title': '측별 μž…μ§€ 뢄석'}
def _create_diversity_index(self) -> Dict:
"""지역별 μ—…μ’… λ‹€μ–‘μ„± μ§€μˆ˜"""
if 'μ‹œκ΅°κ΅¬λͺ…' not in self.df.columns or 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' not in self.df.columns:
return None
diversity_data = []
for district in self.df['μ‹œκ΅°κ΅¬λͺ…'].unique()[:20]:
district_df = self.df[self.df['μ‹œκ΅°κ΅¬λͺ…'] == district]
num_categories = district_df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].nunique()
total_stores = len(district_df)
diversity_score = (num_categories / total_stores) * 100
diversity_data.append({
'μ§€μ—­': district,
'λ‹€μ–‘μ„±μ§€μˆ˜': diversity_score,
'μ—…μ’…μˆ˜': num_categories,
'점포수': total_stores
})
diversity_df = pd.DataFrame(diversity_data).sort_values('λ‹€μ–‘μ„±μ§€μˆ˜', ascending=False)
fig = px.bar(
diversity_df,
x='λ‹€μ–‘μ„±μ§€μˆ˜',
y='μ§€μ—­',
orientation='h',
title='🎨 지역별 μ—…μ’… λ‹€μ–‘μ„± μ§€μˆ˜ (μ—…μ’… 수 / 점포 수 Γ— 100)',
labels={'λ‹€μ–‘μ„±μ§€μˆ˜': 'λ‹€μ–‘μ„± μ§€μˆ˜', 'μ§€μ—­': 'μ‹œκ΅°κ΅¬'},
color='λ‹€μ–‘μ„±μ§€μˆ˜',
color_continuous_scale='viridis'
)
fig.update_layout(height=500)
return {'type': 'plot', 'data': fig, 'title': 'μƒκΆŒ λ‹€μ–‘μ„± 뢄석'}
def _create_franchise_analysis(self) -> Dict:
"""ν”„λžœμ°¨μ΄μ¦ˆ vs κ°œμΈμ‚¬μ—…μž 뢄석"""
if 'λΈŒλžœλ“œλͺ…' not in self.df.columns:
return None
franchise_count = self.df['λΈŒλžœλ“œλͺ…'].notna().sum()
individual_count = self.df['λΈŒλžœλ“œλͺ…'].isna().sum()
fig = go.Figure(data=[
go.Pie(
labels=['κ°œμΈμ‚¬μ—…μž', 'ν”„λžœμ°¨μ΄μ¦ˆ'],
values=[individual_count, franchise_count],
hole=0.4,
marker_colors=['#3498db', '#e74c3c'],
textinfo='label+percent+value',
texttemplate='%{label}<br>%{value:,}개<br>(%{percent})'
)
])
fig.update_layout(
title='πŸͺ κ°œμΈμ‚¬μ—…μž vs ν”„λžœμ°¨μ΄μ¦ˆ λΉ„μœ¨',
height=400
)
return {'type': 'plot', 'data': fig, 'title': 'μ‚¬μ—…μž μœ ν˜• 뢄석'}
def _create_floor_preference(self) -> Dict:
"""업쒅별 μΈ΅ μ„ ν˜Έλ„"""
if '측정보_숫자' not in self.df.columns or 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' not in self.df.columns:
return None
top_categories = self.df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].value_counts().head(10).index
floor_pref_data = []
for category in top_categories:
cat_df = self.df[self.df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'] == category]
floor_dist = cat_df['측정보_숫자'].dropna()
if len(floor_dist) > 0:
underground = (floor_dist < 0).sum()
first_floor = (floor_dist == 1).sum()
upper_floors = (floor_dist > 1).sum()
floor_pref_data.append({
'μ—…μ’…': category,
'μ§€ν•˜': underground,
'1μΈ΅': first_floor,
'2μΈ΅ 이상': upper_floors
})
pref_df = pd.DataFrame(floor_pref_data)
fig = go.Figure()
fig.add_trace(go.Bar(name='μ§€ν•˜', x=pref_df['μ—…μ’…'], y=pref_df['μ§€ν•˜'], marker_color='#e74c3c'))
fig.add_trace(go.Bar(name='1μΈ΅', x=pref_df['μ—…μ’…'], y=pref_df['1μΈ΅'], marker_color='#3498db'))
fig.add_trace(go.Bar(name='2μΈ΅ 이상', x=pref_df['μ—…μ’…'], y=pref_df['2μΈ΅ 이상'], marker_color='#95a5a6'))
fig.update_layout(
title='🏒 업쒅별 μΈ΅ μ„ ν˜Έλ„ (μƒμœ„ 10개 μ—…μ’…)',
xaxis_title='μ—…μ’…',
yaxis_title='점포 수',
barmode='stack',
height=500,
xaxis_tickangle=-45
)
return {'type': 'plot', 'data': fig, 'title': '측별 μ„ ν˜Έλ„ 뢄석'}
def _create_district_density(self) -> Dict:
"""μ‹œκ΅°κ΅¬λ³„ μƒκΆŒ 밀집도"""
if 'μ‹œκ΅°κ΅¬λͺ…' not in self.df.columns:
return None
district_counts = self.df['μ‹œκ΅°κ΅¬λͺ…'].value_counts().head(20)
fig = px.bar(
x=district_counts.values,
y=district_counts.index,
orientation='h',
title='πŸ“ μ‹œκ΅°κ΅¬λ³„ 점포 밀집도 TOP 20',
labels={'x': '점포 수', 'y': 'μ‹œκ΅°κ΅¬'},
color=district_counts.values,
color_continuous_scale='reds'
)
fig.update_layout(showlegend=False, height=600)
return {'type': 'plot', 'data': fig, 'title': 'μ§€μ—­ 밀집도 뢄석'}
def _create_category_correlation(self) -> Dict:
"""μ—…μ’… 상관관계"""
if 'μ‹œκ΅°κ΅¬λͺ…' not in self.df.columns or 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' not in self.df.columns:
return None
top_categories = self.df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].value_counts().head(10).index.tolist()
districts = self.df['μ‹œκ΅°κ΅¬λͺ…'].unique()
correlation_matrix = np.zeros((len(top_categories), len(top_categories)))
for i, cat1 in enumerate(top_categories):
for j, cat2 in enumerate(top_categories):
if i != j:
coexist_count = 0
for district in districts:
district_df = self.df[self.df['μ‹œκ΅°κ΅¬λͺ…'] == district]
has_cat1 = cat1 in district_df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].values
has_cat2 = cat2 in district_df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].values
if has_cat1 and has_cat2:
coexist_count += 1
correlation_matrix[i][j] = coexist_count
fig = go.Figure(data=go.Heatmap(
z=correlation_matrix,
x=top_categories,
y=top_categories,
colorscale='Blues',
text=np.round(correlation_matrix, 1),
texttemplate='%{text}',
textfont={"size": 10}
))
fig.update_layout(
title='πŸ”— μ—…μ’… 상관관계 맀트릭슀 (같은 μ§€μ—­ λ™μ‹œ μΆœν˜„μœ¨)',
xaxis_title='μ—…μ’…',
yaxis_title='μ—…μ’…',
height=600,
xaxis_tickangle=-45
)
return {'type': 'plot', 'data': fig, 'title': 'μ—…μ’… 곡쑴 뢄석'}
def _create_subcategory_trends(self) -> Dict:
"""μ†ŒλΆ„λ₯˜ νŠΈλ Œλ“œ"""
if 'μƒκΆŒμ—…μ’…μ†ŒλΆ„λ₯˜λͺ…' not in self.df.columns:
return None
subcat_counts = self.df['μƒκΆŒμ—…μ’…μ†ŒλΆ„λ₯˜λͺ…'].value_counts().head(20)
fig = px.treemap(
names=subcat_counts.index,
parents=[''] * len(subcat_counts),
values=subcat_counts.values,
title='πŸ” μ†ŒλΆ„λ₯˜ μ—…μ’… νŠΈλ Œλ“œ TOP 20',
color=subcat_counts.values,
color_continuous_scale='greens'
)
fig.update_layout(height=600)
return {'type': 'plot', 'data': fig, 'title': 'μ„ΈλΆ€ μ—…μ’… 뢄석'}
def _create_regional_specialization(self) -> Dict:
"""지역별 νŠΉν™” μ—…μ’…"""
if 'μ‹œλ„λͺ…' not in self.df.columns or 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' not in self.df.columns:
return None
specialization_data = []
for region in self.df['μ‹œλ„λͺ…'].unique():
region_df = self.df[self.df['μ‹œλ„λͺ…'] == region]
top_categories = region_df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].value_counts().head(3)
for category, count in top_categories.items():
specialization_data.append({
'μ§€μ—­': region,
'νŠΉν™”μ—…μ’…': category,
'점포수': count
})
spec_df = pd.DataFrame(specialization_data)
fig = px.sunburst(
spec_df,
path=['μ§€μ—­', 'νŠΉν™”μ—…μ’…'],
values='점포수',
title='🎯 지역별 νŠΉν™” μ—…μ’… (각 μ§€μ—­ TOP 3)',
color='점포수',
color_continuous_scale='oranges'
)
fig.update_layout(height=700)
return {'type': 'plot', 'data': fig, 'title': 'μ§€μ—­ νŠΉν™” 뢄석'}
def create_density_map(self, sample_size: int = 1000) -> str:
"""점포 밀집도 지도 생성"""
df_sample = self.df.sample(n=min(sample_size, len(self.df)), random_state=42)
center_lat = df_sample['μœ„λ„'].mean()
center_lon = df_sample['경도'].mean()
m = folium.Map(location=[center_lat, center_lon], zoom_start=11, tiles='OpenStreetMap')
heat_data = [[row['μœ„λ„'], row['경도']] for _, row in df_sample.iterrows()]
HeatMap(heat_data, radius=15, blur=25, max_zoom=13).add_to(m)
return m._repr_html_()
def analyze_for_llm(self) -> Dict:
"""LLM μ»¨ν…μŠ€νŠΈμš© 뢄석 데이터"""
context = {
'총_점포_수': len(self.df),
'μ§€μ—­_수': self.df['μ‹œλ„λͺ…'].nunique() if 'μ‹œλ„λͺ…' in self.df.columns else 0,
'μ—…μ’…_수': self.df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].nunique() if 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' in self.df.columns else 0,
}
if 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' in self.df.columns:
context['μƒμœ„_μ—…μ’…_5'] = self.df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].value_counts().head(5).to_dict()
if '측정보_숫자' in self.df.columns:
first_floor_ratio = (self.df['측정보_숫자'] == 1).sum() / len(self.df) * 100
context['1μΈ΅_λΉ„μœ¨'] = f"{first_floor_ratio:.1f}%"
return context
# ============================================================================
# LLM 쿼리 ν”„λ‘œμ„Έμ„œ (슀트리밍 지원 + 웹검색)
# ============================================================================
class LLMQueryProcessor:
"""Fireworks AI 기반 μžμ—°μ–΄ 처리 (슀트리밍 지원 + 웹검색)"""
def __init__(self, api_key: str = None):
self.api_key = api_key or os.getenv("FIREWORKS_API_KEY")
self.base_url = "https://api.fireworks.ai/inference/v1/chat/completions"
if not self.api_key:
raise ValueError("❌ FIREWORKS_API_KEY ν™˜κ²½λ³€μˆ˜λ₯Ό μ„€μ •ν•˜κ±°λ‚˜ API ν‚€λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”!")
def process_query_stream(self, query: str, data_context: Dict, chat_history: List = None, web_search_results: str = None):
"""μžμ—°μ–΄ 쿼리 처리 (슀트리밍 λͺ¨λ“œ) - 웹검색 κ²°κ³Ό 포함"""
web_context = ""
if web_search_results and "⚠️" not in web_search_results:
web_context = f"""
🌐 **μ΅œμ‹  μ›Ή 검색 정보**
{web_search_results}
μœ„ μ›Ή 검색 κ²°κ³Όλ₯Ό μ°Έκ³ ν•˜μ—¬ μ΅œμ‹  정보와 νŠΈλ Œλ“œλ₯Ό λ°˜μ˜ν•΄μ£Όμ„Έμš”.
"""
system_prompt = f"""당신은 ν•œκ΅­ μƒκΆŒ 데이터 뢄석 μ „λ¬Έκ°€μž…λ‹ˆλ‹€.
πŸ“Š **ν˜„μž¬ 뢄석 데이터**
{json.dumps(data_context, ensure_ascii=False, indent=2)}
{web_context}
ꡬ체적인 μˆ«μžμ™€ λΉ„μœ¨λ‘œ μ •λŸ‰μ  뢄석을 μ œκ³΅ν•˜μ„Έμš”.
μ°½μ—…, 투자, 경쟁 뢄석 κ΄€μ μ—μ„œ μ‹€μš©μ  μΈμ‚¬μ΄νŠΈλ₯Ό μ œκ³΅ν•˜μ„Έμš”.
μ›Ή 검색 κ²°κ³Όκ°€ 제곡된 경우 μ΅œμ‹  νŠΈλ Œλ“œμ™€ ν•¨κ»˜ λΆ„μ„ν•˜μ„Έμš”.
λ°˜λ“œμ‹œ ν•œκ΅­μ–΄λ‘œ λ‹΅λ³€ν•˜μ„Έμš”."""
messages = [{"role": "system", "content": system_prompt}]
if chat_history:
messages.extend(chat_history[-6:])
messages.append({"role": "user", "content": query})
payload = {
"model": "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507",
"max_tokens": 4800,
"temperature": 0.7,
"messages": messages,
"stream": True
}
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
try:
response = requests.post(
self.base_url,
headers=headers,
json=payload,
timeout=60,
stream=True
)
if response.status_code == 200:
for line in response.iter_lines():
if line:
line_text = line.decode('utf-8')
if line_text.startswith('data: '):
data_str = line_text[6:]
if data_str.strip() == '[DONE]':
break
try:
data = json.loads(data_str)
if 'choices' in data and len(data['choices']) > 0:
delta = data['choices'][0].get('delta', {})
content = delta.get('content', '')
if content:
yield content
except json.JSONDecodeError:
continue
else:
yield f"⚠️ API 였λ₯˜: {response.status_code}"
except requests.exceptions.Timeout:
yield "⚠️ API 응닡 μ‹œκ°„ 초과. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”."
except requests.exceptions.ConnectionError:
yield "⚠️ λ„€νŠΈμ›Œν¬ μ—°κ²° 였λ₯˜. 인터넷 연결을 ν™•μΈν•΄μ£Όμ„Έμš”."
except Exception as e:
yield f"❌ 였λ₯˜: {str(e)}"
# ============================================================================
# μ „μ—­ μƒνƒœ
# ============================================================================
class AppState:
def __init__(self):
self.analyzer = None
self.llm_processor = None
self.brave_client = None
self.chat_history = []
app_state = AppState()
# ============================================================================
# Gradio μΈν„°νŽ˜μ΄μŠ€ ν•¨μˆ˜
# ============================================================================
def load_data(regions):
"""데이터 λ‘œλ“œ"""
if not regions:
return "❌ μ΅œμ†Œ 1개 지역을 μ„ νƒν•΄μ£Όμ„Έμš”!", None, None, None
try:
df = MarketDataLoader.load_multiple_regions(regions, sample_per_region=30000)
if df.empty:
return "❌ 데이터 λ‘œλ“œ μ‹€νŒ¨!", None, None, None
app_state.analyzer = MarketAnalyzer(df)
stats = f"""
βœ… **데이터 λ‘œλ“œ μ™„λ£Œ!**
{'=' * 40}
πŸ“Š **뢄석 톡계**
β€’ 총 점포: {len(df):,}개
β€’ 뢄석 μ§€μ—­: {', '.join(regions)}
β€’ μ—…μ’… 수: {df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].nunique()}개
β€’ λŒ€λΆ„λ₯˜: {df['μƒκΆŒμ—…μ’…λŒ€λΆ„λ₯˜λͺ…'].nunique()}개
{'=' * 40}
πŸ’‘ 이제 μΈμ‚¬μ΄νŠΈλ₯Ό ν™•μΈν•˜κ±°λ‚˜ AIμ—κ²Œ μ§ˆλ¬Έν•˜μ„Έμš”!
"""
return stats, gr.update(visible=True), gr.update(visible=True), gr.update(visible=True)
except Exception as e:
return f"❌ 였λ₯˜: {str(e)}", None, None, None
def generate_insights():
"""μΈμ‚¬μ΄νŠΈ 생성"""
if app_state.analyzer is None:
return [None] * 11
insights = app_state.analyzer.get_comprehensive_insights()
map_html = app_state.analyzer.create_density_map(sample_size=2000)
result = [map_html]
for insight in insights:
if insight and insight['type'] == 'plot':
result.append(insight['data'])
else:
result.append(None)
while len(result) < 11:
result.append(None)
return result[:11]
def chat_respond(message, history):
"""챗봇 응닡 (슀트리밍 λͺ¨λ“œ + 웹검색)"""
if app_state.analyzer is None:
yield history + [[message, "❌ λ¨Όμ € 데이터λ₯Ό λ‘œλ“œν•΄μ£Όμ„Έμš”!"]]
return
data_context = app_state.analyzer.analyze_for_llm()
try:
if app_state.llm_processor is None:
app_state.llm_processor = LLMQueryProcessor()
if app_state.brave_client is None:
try:
app_state.brave_client = BraveSearchClient()
except:
app_state.brave_client = None
web_results = None
if app_state.brave_client and app_state.brave_client.api_key:
search_query = f"ν•œκ΅­ μƒκΆŒ μ°½μ—… νŠΈλ Œλ“œ {message}"
web_results = app_state.brave_client.search(search_query, count=3)
chat_hist = []
for user_msg, bot_msg in history:
chat_hist.append({"role": "user", "content": user_msg})
chat_hist.append({"role": "assistant", "content": bot_msg})
history = history + [[message, ""]]
if web_results and "⚠️" not in web_results:
history[-1][1] = "πŸ” μ›Ή 검색 쀑...\n\n"
yield history
full_response = ""
for chunk in app_state.llm_processor.process_query_stream(message, data_context, chat_hist, web_results):
full_response += chunk
history[-1][1] = full_response
yield history
except ValueError as e:
response = f"""πŸ“Š **κΈ°λ³Έ 데이터 뢄석 κ²°κ³Ό**
**전체 ν˜„ν™©**
- 총 점포 수: {data_context['총_점포_수']:,}개
- μ—…μ’… μ’…λ₯˜: {data_context['μ—…μ’…_수']}개
- 1μΈ΅ λΉ„μœ¨: {data_context.get('1μΈ΅_λΉ„μœ¨', 'N/A')}
⚠️ **AI 뢄석 μ‚¬μš© 방법**
ν™˜κ²½λ³€μˆ˜λ₯Ό μ„€μ •ν•˜μ„Έμš”:
```bash
export FIREWORKS_API_KEY="your_api_key_here"
export BRAVE_API_KEY="your_brave_api_key_here"
```"""
history = history + [[message, response]]
yield history
# ============================================================================
# 🎨 Comic Classic Theme CSS
# ============================================================================
css = """
/* ===== 🎨 Google Fonts Import ===== */
@import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&family=Noto+Sans+KR:wght@400;700&display=swap');
/* ===== 🎨 Comic Classic λ°°κ²½ - λΉˆν‹°μ§€ 페이퍼 + λ„νŠΈ νŒ¨ν„΄ ===== */
.gradio-container {
background-color: #FEF9C3 !important;
background-image:
radial-gradient(#1F2937 1px, transparent 1px) !important;
background-size: 20px 20px !important;
min-height: 100vh !important;
font-family: 'Noto Sans KR', 'Comic Neue', cursive, sans-serif !important;
}
/* ===== ν—ˆκΉ…νŽ˜μ΄μŠ€ 상단 μš”μ†Œ μˆ¨κΉ€ ===== */
.huggingface-space-header,
#space-header,
.space-header,
[class*="space-header"],
.svelte-1ed2p3z,
.space-header-badge,
.header-badge,
[data-testid="space-header"],
.svelte-kqij2n,
.svelte-1ax1toq,
.embed-container > div:first-child {
display: none !important;
visibility: hidden !important;
height: 0 !important;
width: 0 !important;
overflow: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}
/* ===== Footer μ™„μ „ μˆ¨κΉ€ ===== */
footer,
.footer,
.gradio-container footer,
.built-with,
[class*="footer"],
.gradio-footer,
.main-footer,
div[class*="footer"],
.show-api,
.built-with-gradio,
a[href*="gradio.app"],
a[href*="huggingface.co/spaces"] {
display: none !important;
visibility: hidden !important;
height: 0 !important;
padding: 0 !important;
margin: 0 !important;
}
/* ===== 메인 μ»¨ν…Œμ΄λ„ˆ ===== */
#col-container {
max-width: 1400px;
margin: 0 auto;
}
/* ===== 🎨 헀더 타이틀 - μ½”λ―Ή μŠ€νƒ€μΌ ===== */
.header-text h1 {
font-family: 'Bangers', cursive !important;
color: #1F2937 !important;
font-size: 3.2rem !important;
font-weight: 400 !important;
text-align: center !important;
margin-bottom: 0.5rem !important;
text-shadow:
4px 4px 0px #FACC15,
6px 6px 0px #1F2937 !important;
letter-spacing: 3px !important;
-webkit-text-stroke: 2px #1F2937 !important;
}
/* ===== 🎨 μ„œλΈŒνƒ€μ΄ν‹€ ===== */
.subtitle {
text-align: center !important;
font-family: 'Noto Sans KR', 'Comic Neue', cursive !important;
font-size: 1.1rem !important;
color: #1F2937 !important;
margin-bottom: 1.5rem !important;
font-weight: 700 !important;
}
/* ===== 🎨 μΉ΄λ“œ/νŒ¨λ„ - λ§Œν™” ν”„λ ˆμž„ μŠ€νƒ€μΌ ===== */
.gr-panel,
.gr-box,
.gr-form,
.block,
.gr-group {
background: #FFFFFF !important;
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
box-shadow: 6px 6px 0px #1F2937 !important;
transition: all 0.2s ease !important;
}
.gr-panel:hover,
.block:hover {
transform: translate(-2px, -2px) !important;
box-shadow: 8px 8px 0px #1F2937 !important;
}
/* ===== 🎨 μž…λ ₯ ν•„λ“œ (Textbox) ===== */
textarea,
input[type="text"],
input[type="number"] {
background: #FFFFFF !important;
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
color: #1F2937 !important;
font-family: 'Noto Sans KR', 'Comic Neue', cursive !important;
font-size: 1rem !important;
font-weight: 700 !important;
transition: all 0.2s ease !important;
}
textarea:focus,
input[type="text"]:focus,
input[type="number"]:focus {
border-color: #3B82F6 !important;
box-shadow: 4px 4px 0px #3B82F6 !important;
outline: none !important;
}
textarea::placeholder {
color: #9CA3AF !important;
font-weight: 400 !important;
}
/* ===== 🎨 Primary λ²„νŠΌ - μ½”λ―Ή 블루 ===== */
.gr-button-primary,
button.primary,
.gr-button.primary {
background: #3B82F6 !important;
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
color: #FFFFFF !important;
font-family: 'Noto Sans KR', 'Bangers', cursive !important;
font-weight: 700 !important;
font-size: 1.2rem !important;
letter-spacing: 1px !important;
padding: 14px 28px !important;
box-shadow: 5px 5px 0px #1F2937 !important;
transition: all 0.1s ease !important;
text-shadow: 1px 1px 0px #1F2937 !important;
}
.gr-button-primary:hover,
button.primary:hover,
.gr-button.primary:hover {
background: #2563EB !important;
transform: translate(-2px, -2px) !important;
box-shadow: 7px 7px 0px #1F2937 !important;
}
.gr-button-primary:active,
button.primary:active,
.gr-button.primary:active {
transform: translate(3px, 3px) !important;
box-shadow: 2px 2px 0px #1F2937 !important;
}
/* ===== 🎨 Secondary λ²„νŠΌ - μ½”λ―Ή λ ˆλ“œ ===== */
.gr-button-secondary,
button.secondary {
background: #EF4444 !important;
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
color: #FFFFFF !important;
font-family: 'Noto Sans KR', 'Bangers', cursive !important;
font-weight: 700 !important;
font-size: 1rem !important;
letter-spacing: 1px !important;
box-shadow: 4px 4px 0px #1F2937 !important;
transition: all 0.1s ease !important;
text-shadow: 1px 1px 0px #1F2937 !important;
}
.gr-button-secondary:hover,
button.secondary:hover {
background: #DC2626 !important;
transform: translate(-2px, -2px) !important;
box-shadow: 6px 6px 0px #1F2937 !important;
}
/* ===== 🎨 Small λ²„νŠΌ ===== */
button.sm,
.gr-button-sm {
background: #10B981 !important;
border: 2px solid #1F2937 !important;
border-radius: 6px !important;
color: #FFFFFF !important;
font-family: 'Noto Sans KR', cursive !important;
font-weight: 700 !important;
font-size: 0.9rem !important;
padding: 8px 16px !important;
box-shadow: 3px 3px 0px #1F2937 !important;
transition: all 0.1s ease !important;
}
button.sm:hover,
.gr-button-sm:hover {
background: #059669 !important;
transform: translate(-1px, -1px) !important;
box-shadow: 4px 4px 0px #1F2937 !important;
}
/* ===== 🎨 둜그 좜λ ₯ μ˜μ—­ ===== */
.info-log textarea {
background: #1F2937 !important;
color: #10B981 !important;
font-family: 'Courier New', monospace !important;
font-size: 0.9rem !important;
font-weight: 400 !important;
border: 3px solid #10B981 !important;
border-radius: 8px !important;
box-shadow: 4px 4px 0px #10B981 !important;
}
/* ===== 🎨 μ•„μ½”λ””μ–Έ - 말풍선 μŠ€νƒ€μΌ ===== */
.gr-accordion {
background: #FACC15 !important;
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
box-shadow: 4px 4px 0px #1F2937 !important;
}
.gr-accordion-header {
color: #1F2937 !important;
font-family: 'Noto Sans KR', 'Comic Neue', cursive !important;
font-weight: 700 !important;
font-size: 1.1rem !important;
}
/* ===== 🎨 μ²΄ν¬λ°•μŠ€ κ·Έλ£Ή ===== */
.gr-checkbox-group {
background: #FFFFFF !important;
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
padding: 10px !important;
}
input[type="checkbox"] {
accent-color: #3B82F6 !important;
width: 18px !important;
height: 18px !important;
}
/* ===== 🎨 νƒ­ μŠ€νƒ€μΌ ===== */
.gr-tab-nav {
background: #FACC15 !important;
border: 3px solid #1F2937 !important;
border-radius: 8px 8px 0 0 !important;
box-shadow: 4px 4px 0px #1F2937 !important;
}
.gr-tab-nav button {
font-family: 'Noto Sans KR', 'Comic Neue', cursive !important;
font-weight: 700 !important;
color: #1F2937 !important;
border: none !important;
padding: 12px 20px !important;
}
.gr-tab-nav button.selected {
background: #3B82F6 !important;
color: #FFFFFF !important;
border-radius: 6px 6px 0 0 !important;
}
/* ===== 🎨 챗봇 μŠ€νƒ€μΌ ===== */
.gr-chatbot {
background: #FFFFFF !important;
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
box-shadow: 6px 6px 0px #1F2937 !important;
}
.gr-chatbot .message {
font-family: 'Noto Sans KR', sans-serif !important;
}
/* ===== 🎨 라벨 μŠ€νƒ€μΌ ===== */
label,
.gr-input-label,
.gr-block-label {
color: #1F2937 !important;
font-family: 'Noto Sans KR', 'Comic Neue', cursive !important;
font-weight: 700 !important;
font-size: 1rem !important;
}
/* ===== 🎨 Markdown μŠ€νƒ€μΌ ===== */
.gr-markdown {
font-family: 'Noto Sans KR', 'Comic Neue', cursive !important;
color: #1F2937 !important;
}
.gr-markdown h1,
.gr-markdown h2,
.gr-markdown h3 {
font-family: 'Bangers', 'Noto Sans KR', cursive !important;
color: #1F2937 !important;
text-shadow: 2px 2px 0px #FACC15 !important;
}
/* ===== 🎨 Plot μ˜μ—­ ===== */
.gr-plot {
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
box-shadow: 4px 4px 0px #1F2937 !important;
background: #FFFFFF !important;
}
/* ===== 🎨 HTML μ˜μ—­ (지도) ===== */
.gr-html {
border: 4px solid #1F2937 !important;
border-radius: 8px !important;
box-shadow: 6px 6px 0px #FACC15 !important;
overflow: hidden !important;
}
/* ===== 🎨 μŠ€ν¬λ‘€λ°” - μ½”λ―Ή μŠ€νƒ€μΌ ===== */
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track {
background: #FEF9C3;
border: 2px solid #1F2937;
}
::-webkit-scrollbar-thumb {
background: #3B82F6;
border: 2px solid #1F2937;
border-radius: 0px;
}
::-webkit-scrollbar-thumb:hover {
background: #EF4444;
}
/* ===== 🎨 선택 ν•˜μ΄λΌμ΄νŠΈ ===== */
::selection {
background: #FACC15;
color: #1F2937;
}
/* ===== 🎨 링크 μŠ€νƒ€μΌ ===== */
a {
color: #3B82F6 !important;
text-decoration: none !important;
font-weight: 700 !important;
}
a:hover {
color: #EF4444 !important;
}
/* ===== 🎨 Row/Column 간격 ===== */
.gr-row {
gap: 1.5rem !important;
}
.gr-column {
gap: 1rem !important;
}
/* ===== 🎨 Badge μŠ€νƒ€μΌ ===== */
.badge-container {
display: flex;
justify-content: center;
gap: 15px;
flex-wrap: wrap;
margin: 20px 0;
}
.comic-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
border: 3px solid #1F2937;
border-radius: 8px;
text-decoration: none;
font-weight: 700;
font-size: 1em;
transition: all 0.2s ease;
box-shadow: 4px 4px 0px #1F2937;
font-family: 'Noto Sans KR', sans-serif;
}
.comic-badge:hover {
transform: translate(-2px, -2px);
box-shadow: 6px 6px 0px #1F2937;
}
.comic-badge-yellow {
background: #FACC15;
color: #1F2937;
}
.comic-badge-blue {
background: #3B82F6;
color: #FFFFFF;
}
.comic-badge-green {
background: #10B981;
color: #FFFFFF;
}
/* ===== λ°˜μ‘ν˜• μ‘°μ • ===== */
@media (max-width: 768px) {
.header-text h1 {
font-size: 2rem !important;
text-shadow:
3px 3px 0px #FACC15,
4px 4px 0px #1F2937 !important;
}
.gr-button-primary,
button.primary {
padding: 12px 20px !important;
font-size: 1rem !important;
}
.gr-panel,
.block {
box-shadow: 4px 4px 0px #1F2937 !important;
}
}
/* ===== 🎨 닀크λͺ¨λ“œ λΉ„ν™œμ„±ν™” ===== */
@media (prefers-color-scheme: dark) {
.gradio-container {
background-color: #FEF9C3 !important;
}
}
"""
# ============================================================================
# Gradio UI
# ============================================================================
with gr.Blocks(title="AI μƒκΆŒ 뢄석 μ‹œμŠ€ν…œ", css=css) as demo:
# HOME Badge
gr.HTML("""
<div style="text-align: center; margin: 20px 0 10px 0;">
<a href="https://www.humangen.ai" target="_blank" style="text-decoration: none;">
<img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge" alt="HOME">
</a>
</div>
""")
# Header Title
gr.Markdown(
"""
# πŸͺ AI μƒκΆŒ 뢄석 μ‹œμŠ€ν…œ PRO πŸ“Š
""",
elem_classes="header-text"
)
gr.Markdown(
"""
<p class="subtitle">⚑ μ „κ΅­ 상가(μƒκΆŒ) 데이터 μ‹€μ‹œκ°„ 뢄석 | 슀트리밍 + 웹검색 πŸ” | 10κ°€μ§€ 심측 μΈμ‚¬μ΄νŠΈ πŸš€</p>
""",
)
# λ°°μ§€
gr.HTML("""
<div class="badge-container">
<a href="https://open.kakao.com/o/peIe8KWh" target="_blank" class="comic-badge comic-badge-yellow">
<span>πŸ’¬</span>
<span>μ˜€ν”ˆμ±„νŒ… λ°”λ‘œκ°€κΈ°</span>
</a>
<a href="https://ginigen.ai" target="_blank" class="comic-badge comic-badge-blue">
<span>🍌</span>
<span>λ‚˜λ…Έ λ°”λ‚˜λ‚˜ μ• λ“œμ˜¨ 무료 μ„œλΉ„μŠ€</span>
</a>
</div>
""")
# API μƒνƒœ
api_status = "βœ… 섀정됨" if os.getenv("FIREWORKS_API_KEY") else "⚠️ λ―Έμ„€μ •"
brave_status = "βœ… ν™œμ„±ν™”" if os.getenv("BRAVE_API_KEY") else "⚠️ λΉ„ν™œμ„±ν™”"
with gr.Row(equal_height=False):
# μ™Όμͺ½ 컬럼 - μ„€μ •
with gr.Column(scale=1, min_width=300):
gr.Markdown("### βš™οΈ 뢄석 μ„€μ •")
gr.Markdown(f"""
**πŸ”‘ API μƒνƒœ**
- Fireworks AI: {api_status}
- Brave Search: {brave_status}
""")
region_select = gr.CheckboxGroup(
choices=list(MarketDataLoader.REGIONS.keys()),
value=['μ„œμšΈ'],
label="πŸ“ 뢄석 μ§€μ—­ 선택 (μ΅œλŒ€ 5개 ꢌμž₯)"
)
load_btn = gr.Button(
"πŸ“Š 데이터 λ‘œλ“œν•˜κΈ°!",
variant="primary",
size="lg"
)
with gr.Accordion("πŸ“œ λ‘œλ“œ μƒνƒœ", open=True):
status_box = gr.Markdown(
"πŸ‘ˆ 지역을 μ„ νƒν•˜κ³  데이터λ₯Ό λ‘œλ“œν•˜μ„Έμš”!",
elem_classes="info-log"
)
# 였λ₯Έμͺ½ 컬럼 - 메인 μ½˜ν…μΈ 
with gr.Column(scale=3, min_width=600):
with gr.Tabs() as tabs:
# νƒ­ 1: μΈμ‚¬μ΄νŠΈ λŒ€μ‹œλ³΄λ“œ
with gr.Tab("πŸ“Š μΈμ‚¬μ΄νŠΈ λŒ€μ‹œλ³΄λ“œ", id=0) as tab1:
insights_content = gr.Column(visible=False)
with insights_content:
gr.Markdown("### πŸ—ΊοΈ 점포 밀집도 히트맡")
map_output = gr.HTML()
gr.Markdown("---")
gr.Markdown("### πŸ“ˆ 10κ°€μ§€ 심측 μƒκΆŒ μΈμ‚¬μ΄νŠΈ")
with gr.Row():
chart1 = gr.Plot(label="πŸ† 업쒅별 점포 수")
chart2 = gr.Plot(label="πŸ“Š λŒ€λΆ„λ₯˜ 뢄포")
with gr.Row():
chart3 = gr.Plot(label="🏒 측별 뢄포")
chart4 = gr.Plot(label="🎨 μ—…μ’… λ‹€μ–‘μ„±")
with gr.Row():
chart5 = gr.Plot(label="πŸͺ ν”„λžœμ°¨μ΄μ¦ˆ 뢄석")
chart6 = gr.Plot(label="πŸ“ μΈ΅ μ„ ν˜Έλ„")
with gr.Row():
chart7 = gr.Plot(label="πŸ”₯ μ§€μ—­ 밀집도")
chart8 = gr.Plot(label="πŸ”— μ—…μ’… 상관관계")
with gr.Row():
chart9 = gr.Plot(label="πŸ” μ†ŒλΆ„λ₯˜ νŠΈλ Œλ“œ")
chart10 = gr.Plot(label="🎯 μ§€μ—­ νŠΉν™”")
# νƒ­ 2: AI 챗봇
with gr.Tab("πŸ€– AI 뢄석 챗봇 βš‘πŸ”", id=1) as tab2:
chat_content = gr.Column(visible=False)
with chat_content:
gr.Markdown("""
### πŸ’‘ μ˜ˆμ‹œ 질문
κ°•λ‚¨μ—μ„œ 카페 μ°½μ—…? | μΉ˜ν‚¨μ§‘ 포화 μ§€μ—­? | 1측이 μœ λ¦¬ν•œ μ—…μ’…? | ν”„λžœμ°¨μ΄μ¦ˆ 점유율?
⚑ **슀트리밍**: AI 응닡이 μ‹€μ‹œκ°„μœΌλ‘œ ν‘œμ‹œλ©λ‹ˆλ‹€!
πŸ” **웹검색**: μ΅œμ‹  μƒκΆŒ νŠΈλ Œλ“œλ₯Ό μžλ™ λ°˜μ˜ν•©λ‹ˆλ‹€!
""")
chatbot = gr.Chatbot(
height=450,
label="AI μƒκΆŒ 뢄석 μ–΄μ‹œμŠ€ν„΄νŠΈ"
)
with gr.Row():
msg_input = gr.Textbox(
placeholder="무엇이든 λ¬Όμ–΄λ³΄μ„Έμš”! (예: κ°•λ‚¨μ—μ„œ 카페 μ°½μ—…ν•˜λ €λ©΄?)",
show_label=False,
scale=4
)
submit_btn = gr.Button("πŸš€ 전솑", variant="primary", scale=1)
with gr.Row():
sample_btn1 = gr.Button("β˜• 강남 카페 μ°½μ—…?", size="sm")
sample_btn2 = gr.Button("πŸ— μΉ˜ν‚¨μ§‘ 포화 μ§€μ—­?", size="sm")
sample_btn3 = gr.Button("🏒 1μΈ΅ μœ λ¦¬ν•œ μ—…μ’…?", size="sm")
sample_btn4 = gr.Button("πŸͺ ν”„λžœμ°¨μ΄μ¦ˆ 점유율?", size="sm")
# μ‚¬μš© κ°€μ΄λ“œ
gr.Markdown("""
---
### πŸ“– μ‚¬μš© κ°€μ΄λ“œ
1️⃣ μ§€μ—­ 선택 β†’ 2️⃣ 데이터 λ‘œλ“œ β†’ 3️⃣ 10κ°€μ§€ μΈμ‚¬μ΄νŠΈ 확인 λ˜λŠ” AIμ—κ²Œ 질문!
### πŸ“Š μ œκ³΅λ˜λŠ” 10κ°€μ§€ 뢄석
| 뢄석 ν•­λͺ© | μ„€λͺ… |
|----------|------|
| πŸ† 업쒅별 점포 수 | κ°€μž₯ λ§Žμ€ μ—…μ’… TOP 15 |
| πŸ“Š λŒ€λΆ„λ₯˜ 뢄포 | μ†Œλ§€/μŒμ‹/μ„œλΉ„μŠ€ λ“± λΉ„μœ¨ |
| 🏒 측별 뢄포 | μ§€ν•˜/1μΈ΅/상측 μž…μ§€ 뢄석 |
| 🎨 μ—…μ’… λ‹€μ–‘μ„± | 지역별 μ—…μ’… λ‹€μ–‘μ„± μ§€μˆ˜ |
| πŸͺ ν”„λžœμ°¨μ΄μ¦ˆ 뢄석 | 개인 vs ν”„λžœμ°¨μ΄μ¦ˆ λΉ„μœ¨ |
| πŸ“ μΈ΅ μ„ ν˜Έλ„ | 업쒅별 μ„ ν˜Έ 측수 |
| πŸ”₯ μ§€μ—­ 밀집도 | 점포 수 μƒμœ„ μ§€μ—­ |
| πŸ”— μ—…μ’… 상관관계 | 같이 λ‚˜νƒ€λ‚˜λŠ” μ—…μ’… νŒ¨ν„΄ |
| πŸ” μ†ŒλΆ„λ₯˜ νŠΈλ Œλ“œ | μ„ΈλΆ€ μ—…μ’… 뢄포 |
| 🎯 μ§€μ—­ νŠΉν™” | 각 μ§€μ—­μ˜ νŠΉν™” μ—…μ’… |
πŸ’‘ **Tip**: API ν‚€ 없이도 10κ°€μ§€ μ‹œκ°ν™” 뢄석과 κΈ°λ³Έ 톡계λ₯Ό 확인할 수 μžˆμŠ΅λ‹ˆλ‹€!
""")
# 이벀트 ν•Έλ“€λŸ¬
load_btn.click(
fn=load_data,
inputs=[region_select],
outputs=[status_box, insights_content, chat_content, tab1]
).then(
fn=generate_insights,
outputs=[map_output, chart1, chart2, chart3, chart4, chart5, chart6, chart7, chart8, chart9, chart10]
)
# 챗봇 이벀트
submit_btn.click(
fn=chat_respond,
inputs=[msg_input, chatbot],
outputs=[chatbot]
).then(
fn=lambda: "",
outputs=[msg_input]
)
msg_input.submit(
fn=chat_respond,
inputs=[msg_input, chatbot],
outputs=[chatbot]
).then(
fn=lambda: "",
outputs=[msg_input]
)
# μƒ˜ν”Œ λ²„νŠΌ 이벀트
def create_sample_click(text):
def handler(history):
for result in chat_respond(text, history or []):
yield result
return handler
sample_btn1.click(fn=create_sample_click("κ°•λ‚¨μ—μ„œ 카페 μ°½μ—…ν•˜λ €λ©΄ μ–΄λ–»κ²Œ ν•΄μ•Ό ν•˜λ‚˜μš”?"), inputs=[chatbot], outputs=[chatbot])
sample_btn2.click(fn=create_sample_click("μΉ˜ν‚¨μ§‘μ΄ κ°€μž₯ ν¬ν™”λœ 지역은 μ–΄λ””μΈκ°€μš”?"), inputs=[chatbot], outputs=[chatbot])
sample_btn3.click(fn=create_sample_click("1측이 μœ λ¦¬ν•œ 업쒅은 λ¬΄μ—‡μΈκ°€μš”?"), inputs=[chatbot], outputs=[chatbot])
sample_btn4.click(fn=create_sample_click("ν”„λžœμ°¨μ΄μ¦ˆ 점유율이 높은 업쒅은?"), inputs=[chatbot], outputs=[chatbot])
# μ‹€ν–‰
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860, share=False)