Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
AI 기반 상권 분석 시스템 -
|
| 3 |
Dataset: https://huggingface.co/datasets/ginipick/market
|
| 4 |
"""
|
| 5 |
import gradio as gr
|
|
@@ -168,34 +168,15 @@ class MarketAnalyzer:
|
|
| 168 |
"""포괄적인 인사이트 생성"""
|
| 169 |
insights = []
|
| 170 |
|
| 171 |
-
# 1. 업종별 점포 수 (상권업종중분류)
|
| 172 |
insights.append(self._create_top_categories_chart())
|
| 173 |
-
|
| 174 |
-
# 2. 대분류별 분포 (파이 차트)
|
| 175 |
insights.append(self._create_major_category_pie())
|
| 176 |
-
|
| 177 |
-
# 3. 층별 분포 상세 분석
|
| 178 |
insights.append(self._create_floor_analysis())
|
| 179 |
-
|
| 180 |
-
# 4. 지역별 업종 다양성 지수
|
| 181 |
insights.append(self._create_diversity_index())
|
| 182 |
-
|
| 183 |
-
# 5. 프랜차이즈 vs 개인사업자 분석
|
| 184 |
insights.append(self._create_franchise_analysis())
|
| 185 |
-
|
| 186 |
-
# 6. 업종별 층 선호도
|
| 187 |
insights.append(self._create_floor_preference())
|
| 188 |
-
|
| 189 |
-
# 7. 시군구별 상권 밀집도 TOP 20
|
| 190 |
insights.append(self._create_district_density())
|
| 191 |
-
|
| 192 |
-
# 8. 업종 상관관계 (같은 지역에 자주 나타나는 업종)
|
| 193 |
insights.append(self._create_category_correlation())
|
| 194 |
-
|
| 195 |
-
# 9. 소분류 트렌드 (상위 20개)
|
| 196 |
insights.append(self._create_subcategory_trends())
|
| 197 |
-
|
| 198 |
-
# 10. 지역별 특화 업종
|
| 199 |
insights.append(self._create_regional_specialization())
|
| 200 |
|
| 201 |
return insights
|
|
@@ -244,7 +225,6 @@ class MarketAnalyzer:
|
|
| 244 |
floor_data = self.df['층정보_숫자'].dropna()
|
| 245 |
floor_counts = floor_data.value_counts().sort_index()
|
| 246 |
|
| 247 |
-
# 지하, 1층, 2층 이상으로 그룹화
|
| 248 |
underground = floor_counts[floor_counts.index < 0].sum()
|
| 249 |
first_floor = floor_counts.get(1, 0)
|
| 250 |
upper_floors = floor_counts[floor_counts.index > 1].sum()
|
|
@@ -273,9 +253,8 @@ class MarketAnalyzer:
|
|
| 273 |
if '시군구명' not in self.df.columns or '상권업종중분류명' not in self.df.columns:
|
| 274 |
return None
|
| 275 |
|
| 276 |
-
# 각 시군구별 업종 다양성 계산 (업종 수 / 전체 점포 수)
|
| 277 |
diversity_data = []
|
| 278 |
-
for district in self.df['시군구명'].unique()[:20]:
|
| 279 |
district_df = self.df[self.df['시군구명'] == district]
|
| 280 |
num_categories = district_df['상권업종중분류명'].nunique()
|
| 281 |
total_stores = len(district_df)
|
|
@@ -307,7 +286,6 @@ class MarketAnalyzer:
|
|
| 307 |
if '브랜드명' not in self.df.columns:
|
| 308 |
return None
|
| 309 |
|
| 310 |
-
# 브랜드명이 있으면 프랜차이즈로 간주
|
| 311 |
franchise_count = self.df['브랜드명'].notna().sum()
|
| 312 |
individual_count = self.df['브랜드명'].isna().sum()
|
| 313 |
|
|
@@ -333,7 +311,6 @@ class MarketAnalyzer:
|
|
| 333 |
if '층정보_숫자' not in self.df.columns or '상권업종중분류명' not in self.df.columns:
|
| 334 |
return None
|
| 335 |
|
| 336 |
-
# 상위 10개 업종 선택
|
| 337 |
top_categories = self.df['상권업종중분류명'].value_counts().head(10).index
|
| 338 |
floor_pref_data = []
|
| 339 |
|
|
@@ -390,21 +367,17 @@ class MarketAnalyzer:
|
|
| 390 |
return {'type': 'plot', 'data': fig, 'title': '지역 밀집도 분석'}
|
| 391 |
|
| 392 |
def _create_category_correlation(self) -> Dict:
|
| 393 |
-
"""업종 상관관계
|
| 394 |
if '시군구명' not in self.df.columns or '상권업종중분류명' not in self.df.columns:
|
| 395 |
return None
|
| 396 |
|
| 397 |
-
# 상위 10개 업종만 선택
|
| 398 |
top_categories = self.df['상권업종중분류명'].value_counts().head(10).index.tolist()
|
| 399 |
-
|
| 400 |
-
# 각 시군구별로 업종 존재 여부 매트릭스 생성
|
| 401 |
districts = self.df['시군구명'].unique()
|
| 402 |
correlation_matrix = np.zeros((len(top_categories), len(top_categories)))
|
| 403 |
|
| 404 |
for i, cat1 in enumerate(top_categories):
|
| 405 |
for j, cat2 in enumerate(top_categories):
|
| 406 |
if i != j:
|
| 407 |
-
# 두 업종이 같은 시군구에 존재하는 횟수
|
| 408 |
coexist_count = 0
|
| 409 |
for district in districts:
|
| 410 |
district_df = self.df[self.df['시군구명'] == district]
|
|
@@ -456,7 +429,6 @@ class MarketAnalyzer:
|
|
| 456 |
if '시도명' not in self.df.columns or '상권업종중분류명' not in self.df.columns:
|
| 457 |
return None
|
| 458 |
|
| 459 |
-
# 각 시도별 상위 3개 업종
|
| 460 |
specialization_data = []
|
| 461 |
for region in self.df['시도명'].unique():
|
| 462 |
region_df = self.df[self.df['시도명'] == region]
|
|
@@ -490,7 +462,6 @@ class MarketAnalyzer:
|
|
| 490 |
|
| 491 |
m = folium.Map(location=[center_lat, center_lon], zoom_start=11, tiles='OpenStreetMap')
|
| 492 |
|
| 493 |
-
# 히트맵
|
| 494 |
heat_data = [[row['위��'], row['경도']] for _, row in df_sample.iterrows()]
|
| 495 |
HeatMap(heat_data, radius=15, blur=25, max_zoom=13).add_to(m)
|
| 496 |
|
|
@@ -522,7 +493,6 @@ class LLMQueryProcessor:
|
|
| 522 |
"""Fireworks AI 기반 자연어 처리 (스트리밍 지원 + 웹검색)"""
|
| 523 |
|
| 524 |
def __init__(self, api_key: str = None):
|
| 525 |
-
# 환경변수에서 API 키 가져오기
|
| 526 |
self.api_key = api_key or os.getenv("FIREWORKS_API_KEY")
|
| 527 |
self.base_url = "https://api.fireworks.ai/inference/v1/chat/completions"
|
| 528 |
|
|
@@ -532,7 +502,6 @@ class LLMQueryProcessor:
|
|
| 532 |
def process_query_stream(self, query: str, data_context: Dict, chat_history: List = None, web_search_results: str = None):
|
| 533 |
"""자연어 쿼리 처리 (스트리밍 모드) - 웹검색 결과 포함"""
|
| 534 |
|
| 535 |
-
# 웹검색 결과가 있으면 컨텍스트에 추가
|
| 536 |
web_context = ""
|
| 537 |
if web_search_results and "⚠️" not in web_search_results:
|
| 538 |
web_context = f"""
|
|
@@ -551,7 +520,8 @@ class LLMQueryProcessor:
|
|
| 551 |
|
| 552 |
구체적인 숫자와 비율로 정량적 분석을 제공하세요.
|
| 553 |
창업, 투자, 경쟁 분석 관점에서 실용적 인사이트를 제공하세요.
|
| 554 |
-
웹 검색 결과가 제공된 경우 최신 트렌드와 함께 분석하세요.
|
|
|
|
| 555 |
|
| 556 |
messages = [{"role": "system", "content": system_prompt}]
|
| 557 |
if chat_history:
|
|
@@ -563,7 +533,7 @@ class LLMQueryProcessor:
|
|
| 563 |
"max_tokens": 4800,
|
| 564 |
"temperature": 0.7,
|
| 565 |
"messages": messages,
|
| 566 |
-
"stream": True
|
| 567 |
}
|
| 568 |
|
| 569 |
headers = {
|
|
@@ -577,16 +547,15 @@ class LLMQueryProcessor:
|
|
| 577 |
headers=headers,
|
| 578 |
json=payload,
|
| 579 |
timeout=60,
|
| 580 |
-
stream=True
|
| 581 |
)
|
| 582 |
|
| 583 |
if response.status_code == 200:
|
| 584 |
-
# 스트리밍 응답 처리
|
| 585 |
for line in response.iter_lines():
|
| 586 |
if line:
|
| 587 |
line_text = line.decode('utf-8')
|
| 588 |
if line_text.startswith('data: '):
|
| 589 |
-
data_str = line_text[6:]
|
| 590 |
if data_str.strip() == '[DONE]':
|
| 591 |
break
|
| 592 |
try:
|
|
@@ -639,15 +608,16 @@ def load_data(regions):
|
|
| 639 |
|
| 640 |
app_state.analyzer = MarketAnalyzer(df)
|
| 641 |
|
| 642 |
-
# 기본 통계
|
| 643 |
stats = f"""
|
| 644 |
✅ **데이터 로드 완료!**
|
| 645 |
-
|
| 646 |
-
📊 **통계**
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
|
|
|
|
|
|
| 651 |
"""
|
| 652 |
|
| 653 |
return stats, gr.update(visible=True), gr.update(visible=True), gr.update(visible=True)
|
|
@@ -670,7 +640,6 @@ def generate_insights():
|
|
| 670 |
else:
|
| 671 |
result.append(None)
|
| 672 |
|
| 673 |
-
# 부족한 차트는 None으로 채우기
|
| 674 |
while len(result) < 11:
|
| 675 |
result.append(None)
|
| 676 |
|
|
@@ -678,7 +647,7 @@ def generate_insights():
|
|
| 678 |
|
| 679 |
|
| 680 |
def chat_respond(message, history):
|
| 681 |
-
"""챗봇 응답 (스트리밍 모드 + 웹검색)
|
| 682 |
if app_state.analyzer is None:
|
| 683 |
yield history + [[message, "❌ 먼저 데이터를 로드해주세요!"]]
|
| 684 |
return
|
|
@@ -686,39 +655,31 @@ def chat_respond(message, history):
|
|
| 686 |
data_context = app_state.analyzer.analyze_for_llm()
|
| 687 |
|
| 688 |
try:
|
| 689 |
-
# LLM 프로세서 초기화
|
| 690 |
if app_state.llm_processor is None:
|
| 691 |
app_state.llm_processor = LLMQueryProcessor()
|
| 692 |
|
| 693 |
-
# Brave Search 클라이언트 초기화
|
| 694 |
if app_state.brave_client is None:
|
| 695 |
try:
|
| 696 |
app_state.brave_client = BraveSearchClient()
|
| 697 |
except:
|
| 698 |
app_state.brave_client = None
|
| 699 |
|
| 700 |
-
# 🔍 자동 웹검색 수행
|
| 701 |
web_results = None
|
| 702 |
if app_state.brave_client and app_state.brave_client.api_key:
|
| 703 |
-
# 검색 쿼리 생성 (사용자 질문에서 핵심 키워드 추출)
|
| 704 |
search_query = f"한국 상권 창업 트렌드 {message}"
|
| 705 |
web_results = app_state.brave_client.search(search_query, count=3)
|
| 706 |
|
| 707 |
-
# 대화 히스토리 구성
|
| 708 |
chat_hist = []
|
| 709 |
for user_msg, bot_msg in history:
|
| 710 |
chat_hist.append({"role": "user", "content": user_msg})
|
| 711 |
chat_hist.append({"role": "assistant", "content": bot_msg})
|
| 712 |
|
| 713 |
-
# 새 메시지 추가
|
| 714 |
history = history + [[message, ""]]
|
| 715 |
|
| 716 |
-
# 웹검색 상태 표시
|
| 717 |
if web_results and "⚠️" not in web_results:
|
| 718 |
history[-1][1] = "🔍 웹 검색 중...\n\n"
|
| 719 |
yield history
|
| 720 |
|
| 721 |
-
# 스트리밍 응답 (웹검색 결과 포함)
|
| 722 |
full_response = ""
|
| 723 |
for chunk in app_state.llm_processor.process_query_stream(message, data_context, chat_hist, web_results):
|
| 724 |
full_response += chunk
|
|
@@ -726,7 +687,6 @@ def chat_respond(message, history):
|
|
| 726 |
yield history
|
| 727 |
|
| 728 |
except ValueError as e:
|
| 729 |
-
# API 키가 없는 경우 기본 통계 제공
|
| 730 |
response = f"""📊 **기본 데이터 분석 결과**
|
| 731 |
|
| 732 |
**전체 현황**
|
|
@@ -738,119 +698,520 @@ def chat_respond(message, history):
|
|
| 738 |
환경변수를 설정하세요:
|
| 739 |
```bash
|
| 740 |
export FIREWORKS_API_KEY="your_api_key_here"
|
| 741 |
-
export BRAVE_API_KEY="your_brave_api_key_here"
|
| 742 |
-
```
|
| 743 |
-
|
| 744 |
-
또는 Hugging Face Space에서는 Settings > Variables 에서 설정하세요."""
|
| 745 |
|
| 746 |
history = history + [[message, response]]
|
| 747 |
yield history
|
| 748 |
|
| 749 |
|
| 750 |
# ============================================================================
|
| 751 |
-
#
|
| 752 |
# ============================================================================
|
| 753 |
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 758 |
|
| 759 |
-
|
| 760 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 761 |
|
| 762 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 763 |
gr.HTML("""
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
padding: 10px 20px;
|
| 779 |
-
border-radius: 25px;
|
| 780 |
-
text-decoration: none;
|
| 781 |
-
font-weight: 600;
|
| 782 |
-
font-size: 0.95em;
|
| 783 |
-
transition: all 0.3s ease;
|
| 784 |
-
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
| 785 |
-
position: relative;
|
| 786 |
-
overflow: hidden;
|
| 787 |
-
}
|
| 788 |
-
|
| 789 |
-
.badge::before {
|
| 790 |
-
content: '';
|
| 791 |
-
position: absolute;
|
| 792 |
-
top: 0;
|
| 793 |
-
left: -100%;
|
| 794 |
-
width: 100%;
|
| 795 |
-
height: 100%;
|
| 796 |
-
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
| 797 |
-
transition: left 0.5s;
|
| 798 |
-
}
|
| 799 |
-
|
| 800 |
-
.badge:hover::before {
|
| 801 |
-
left: 100%;
|
| 802 |
-
}
|
| 803 |
-
|
| 804 |
-
.badge:hover {
|
| 805 |
-
transform: translateY(-3px);
|
| 806 |
-
box-shadow: 0 6px 25px rgba(0,0,0,0.3);
|
| 807 |
-
}
|
| 808 |
-
|
| 809 |
-
.badge-kakao {
|
| 810 |
-
background: linear-gradient(135deg, #FEE500 0%, #FFEB3B 100%);
|
| 811 |
-
color: #3C1E1E;
|
| 812 |
-
}
|
| 813 |
-
|
| 814 |
-
.badge-kakao:hover {
|
| 815 |
-
background: linear-gradient(135deg, #FFD700 0%, #FFC107 100%);
|
| 816 |
-
}
|
| 817 |
-
|
| 818 |
-
.badge-ginigen {
|
| 819 |
-
background: linear-gradient(135deg, #00D9FF 0%, #0099FF 100%);
|
| 820 |
-
color: white;
|
| 821 |
-
}
|
| 822 |
-
|
| 823 |
-
.badge-ginigen:hover {
|
| 824 |
-
background: linear-gradient(135deg, #00C4E6 0%, #0080E6 100%);
|
| 825 |
-
}
|
| 826 |
-
|
| 827 |
-
.badge-icon {
|
| 828 |
-
font-size: 1.2em;
|
| 829 |
-
}
|
| 830 |
-
</style>
|
| 831 |
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 835 |
<span>오픈채팅 바로가기</span>
|
| 836 |
</a>
|
| 837 |
-
<a href="https://ginigen.ai" target="_blank" class="badge badge-
|
| 838 |
-
<span
|
| 839 |
<span>나노 바나나 애드온 무료 서비스</span>
|
| 840 |
</a>
|
| 841 |
</div>
|
| 842 |
""")
|
| 843 |
-
|
| 844 |
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 848 |
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
|
| 855 |
region_select = gr.CheckboxGroup(
|
| 856 |
choices=list(MarketDataLoader.REGIONS.keys()),
|
|
@@ -858,12 +1219,22 @@ with gr.Blocks(title="AI 상권 분석 시스템 Pro", theme=gr.themes.Soft()) a
|
|
| 858 |
label="📍 분석 지역 선택 (최대 5개 권장)"
|
| 859 |
)
|
| 860 |
|
| 861 |
-
load_btn = gr.Button(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 862 |
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 866 |
with gr.Tabs() as tabs:
|
|
|
|
| 867 |
with gr.Tab("📊 인사이트 대시보드", id=0) as tab1:
|
| 868 |
insights_content = gr.Column(visible=False)
|
| 869 |
|
|
@@ -875,38 +1246,42 @@ with gr.Blocks(title="AI 상권 분석 시스템 Pro", theme=gr.themes.Soft()) a
|
|
| 875 |
gr.Markdown("### 📈 10가지 심층 상권 인사이트")
|
| 876 |
|
| 877 |
with gr.Row():
|
| 878 |
-
chart1 = gr.Plot(label="업종별 점포 수")
|
| 879 |
-
chart2 = gr.Plot(label="대분류 분포")
|
| 880 |
|
| 881 |
with gr.Row():
|
| 882 |
-
chart3 = gr.Plot(label="층별 분포")
|
| 883 |
-
chart4 = gr.Plot(label="업종 다양성")
|
| 884 |
|
| 885 |
with gr.Row():
|
| 886 |
-
chart5 = gr.Plot(label="프랜차이즈 분석")
|
| 887 |
-
chart6 = gr.Plot(label="층 선호도")
|
| 888 |
|
| 889 |
with gr.Row():
|
| 890 |
-
chart7 = gr.Plot(label="지역 밀집도")
|
| 891 |
-
chart8 = gr.Plot(label="업종 상관관계")
|
| 892 |
|
| 893 |
with gr.Row():
|
| 894 |
-
chart9 = gr.Plot(label="소분류 트렌드")
|
| 895 |
-
chart10 = gr.Plot(label="지역 특화")
|
| 896 |
|
| 897 |
-
|
|
|
|
| 898 |
chat_content = gr.Column(visible=False)
|
| 899 |
|
| 900 |
with chat_content:
|
| 901 |
gr.Markdown("""
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
""")
|
| 908 |
|
| 909 |
-
chatbot = gr.Chatbot(
|
|
|
|
|
|
|
|
|
|
| 910 |
|
| 911 |
with gr.Row():
|
| 912 |
msg_input = gr.Textbox(
|
|
@@ -914,15 +1289,37 @@ with gr.Blocks(title="AI 상권 분석 시스템 Pro", theme=gr.themes.Soft()) a
|
|
| 914 |
show_label=False,
|
| 915 |
scale=4
|
| 916 |
)
|
| 917 |
-
submit_btn = gr.Button("전송", variant="primary", scale=1)
|
| 918 |
|
| 919 |
-
# 샘플 버튼들
|
| 920 |
with gr.Row():
|
| 921 |
-
sample_btn1 = gr.Button("강남
|
| 922 |
-
sample_btn2 = gr.Button("치킨집 포화 지역?", size="sm")
|
| 923 |
-
sample_btn3 = gr.Button("1층
|
| 924 |
-
sample_btn4 = gr.Button("프랜차이즈 점유율?", size="sm")
|
| 925 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 926 |
# 이벤트 핸들러
|
| 927 |
load_btn.click(
|
| 928 |
fn=load_data,
|
|
@@ -933,7 +1330,7 @@ with gr.Blocks(title="AI 상권 분석 시스템 Pro", theme=gr.themes.Soft()) a
|
|
| 933 |
outputs=[map_output, chart1, chart2, chart3, chart4, chart5, chart6, chart7, chart8, chart9, chart10]
|
| 934 |
)
|
| 935 |
|
| 936 |
-
# 챗봇 이벤트
|
| 937 |
submit_btn.click(
|
| 938 |
fn=chat_respond,
|
| 939 |
inputs=[msg_input, chatbot],
|
|
@@ -959,49 +1356,11 @@ with gr.Blocks(title="AI 상권 분석 시스템 Pro", theme=gr.themes.Soft()) a
|
|
| 959 |
yield result
|
| 960 |
return handler
|
| 961 |
|
| 962 |
-
sample_btn1.click(fn=create_sample_click("강남에서 카페 창업?"), inputs=[chatbot], outputs=[chatbot])
|
| 963 |
-
sample_btn2.click(fn=create_sample_click("치킨집 포화 지역?"), inputs=[chatbot], outputs=[chatbot])
|
| 964 |
-
sample_btn3.click(fn=create_sample_click("1층이 유리한 업종?"), inputs=[chatbot], outputs=[chatbot])
|
| 965 |
-
sample_btn4.click(fn=create_sample_click("프랜차이즈 점유율?"), inputs=[chatbot], outputs=[chatbot])
|
| 966 |
-
|
| 967 |
-
gr.Markdown("""
|
| 968 |
-
---
|
| 969 |
-
### 📖 사용 가이드
|
| 970 |
-
1. 지역 선택 → 2. 데이터 로드 → 3. 10가지 인사이트 확인 또는 AI에게 질문
|
| 971 |
-
|
| 972 |
-
### 🔑 AI 챗봇 + 웹검색 활성화 방법
|
| 973 |
-
환경변수 설정:
|
| 974 |
-
```bash
|
| 975 |
-
export FIREWORKS_API_KEY="your_api_key_here"
|
| 976 |
-
export BRAVE_API_KEY="your_brave_api_key_here" # 웹검색 기능용 (선택)
|
| 977 |
-
```
|
| 978 |
-
|
| 979 |
-
Hugging Face Space에서는:
|
| 980 |
-
1. Settings 메뉴 클릭
|
| 981 |
-
2. Variables 탭 선택
|
| 982 |
-
3. New variable 추가:
|
| 983 |
-
- `FIREWORKS_API_KEY` (필수 - AI 분석용)
|
| 984 |
-
- `BRAVE_API_KEY` (선택 - 웹검색 활성화용)
|
| 985 |
-
|
| 986 |
-
🌐 **웹검색 기능**: BRAVE_API_KEY 설정 시 자동으로 최신 상권 트렌드를 검색하여 답변에 반영합니다!
|
| 987 |
-
|
| 988 |
-
### 📊 제공되는 10가지 분석
|
| 989 |
-
1. **업종별 점포 수**: 가장 많은 업종 TOP 15
|
| 990 |
-
2. **대분류 분포**: 소매/음식/서비스 등 대분류 비율
|
| 991 |
-
3. **층별 분포**: 지하/1층/상층 입지 분석
|
| 992 |
-
4. **업종 다양성**: 지역별 업종 다양성 지수
|
| 993 |
-
5. **프랜차이즈 분석**: 개인 vs 프랜차이즈 비율
|
| 994 |
-
6. **층 선호도**: 업종별 선호 층수
|
| 995 |
-
7. **지역 밀집도**: 점포 수 상위 지역
|
| 996 |
-
8. **업종 상관관계**: 같이 나타나는 업종 패턴
|
| 997 |
-
9. **소분류 트렌드**: 세부 업종 분포
|
| 998 |
-
10. **지역 특화**: 각 지역의 특화 업종
|
| 999 |
-
|
| 1000 |
-
💡 **Tip**: API 키 없이도 10가지 시각화 분석과 기본 통계를 확인할 수 있습니다!
|
| 1001 |
-
|
| 1002 |
-
⚡ **NEW!** 챗봇이 이제 실시간 스트리밍으로 응답합니다!
|
| 1003 |
-
🔍 **NEW!** Brave Search 웹검색으로 최신 상권 트렌드를 자동 반영합니다!
|
| 1004 |
-
""")
|
| 1005 |
|
| 1006 |
# 실행
|
| 1007 |
if __name__ == "__main__":
|
|
|
|
| 1 |
"""
|
| 2 |
+
AI 기반 상권 분석 시스템 - Comic Classic Theme 버전
|
| 3 |
Dataset: https://huggingface.co/datasets/ginipick/market
|
| 4 |
"""
|
| 5 |
import gradio as gr
|
|
|
|
| 168 |
"""포괄적인 인사이트 생성"""
|
| 169 |
insights = []
|
| 170 |
|
|
|
|
| 171 |
insights.append(self._create_top_categories_chart())
|
|
|
|
|
|
|
| 172 |
insights.append(self._create_major_category_pie())
|
|
|
|
|
|
|
| 173 |
insights.append(self._create_floor_analysis())
|
|
|
|
|
|
|
| 174 |
insights.append(self._create_diversity_index())
|
|
|
|
|
|
|
| 175 |
insights.append(self._create_franchise_analysis())
|
|
|
|
|
|
|
| 176 |
insights.append(self._create_floor_preference())
|
|
|
|
|
|
|
| 177 |
insights.append(self._create_district_density())
|
|
|
|
|
|
|
| 178 |
insights.append(self._create_category_correlation())
|
|
|
|
|
|
|
| 179 |
insights.append(self._create_subcategory_trends())
|
|
|
|
|
|
|
| 180 |
insights.append(self._create_regional_specialization())
|
| 181 |
|
| 182 |
return insights
|
|
|
|
| 225 |
floor_data = self.df['층정보_숫자'].dropna()
|
| 226 |
floor_counts = floor_data.value_counts().sort_index()
|
| 227 |
|
|
|
|
| 228 |
underground = floor_counts[floor_counts.index < 0].sum()
|
| 229 |
first_floor = floor_counts.get(1, 0)
|
| 230 |
upper_floors = floor_counts[floor_counts.index > 1].sum()
|
|
|
|
| 253 |
if '시군구명' not in self.df.columns or '상권업종중분류명' not in self.df.columns:
|
| 254 |
return None
|
| 255 |
|
|
|
|
| 256 |
diversity_data = []
|
| 257 |
+
for district in self.df['시군구명'].unique()[:20]:
|
| 258 |
district_df = self.df[self.df['시군구명'] == district]
|
| 259 |
num_categories = district_df['상권업종중분류명'].nunique()
|
| 260 |
total_stores = len(district_df)
|
|
|
|
| 286 |
if '브랜드명' not in self.df.columns:
|
| 287 |
return None
|
| 288 |
|
|
|
|
| 289 |
franchise_count = self.df['브랜드명'].notna().sum()
|
| 290 |
individual_count = self.df['브랜드명'].isna().sum()
|
| 291 |
|
|
|
|
| 311 |
if '층정보_숫자' not in self.df.columns or '상권업종중분류명' not in self.df.columns:
|
| 312 |
return None
|
| 313 |
|
|
|
|
| 314 |
top_categories = self.df['상권업종중분류명'].value_counts().head(10).index
|
| 315 |
floor_pref_data = []
|
| 316 |
|
|
|
|
| 367 |
return {'type': 'plot', 'data': fig, 'title': '지역 밀집도 분석'}
|
| 368 |
|
| 369 |
def _create_category_correlation(self) -> Dict:
|
| 370 |
+
"""업종 상관관계"""
|
| 371 |
if '시군구명' not in self.df.columns or '상권업종중분류명' not in self.df.columns:
|
| 372 |
return None
|
| 373 |
|
|
|
|
| 374 |
top_categories = self.df['상권업종중분류명'].value_counts().head(10).index.tolist()
|
|
|
|
|
|
|
| 375 |
districts = self.df['시군구명'].unique()
|
| 376 |
correlation_matrix = np.zeros((len(top_categories), len(top_categories)))
|
| 377 |
|
| 378 |
for i, cat1 in enumerate(top_categories):
|
| 379 |
for j, cat2 in enumerate(top_categories):
|
| 380 |
if i != j:
|
|
|
|
| 381 |
coexist_count = 0
|
| 382 |
for district in districts:
|
| 383 |
district_df = self.df[self.df['시군구명'] == district]
|
|
|
|
| 429 |
if '시도명' not in self.df.columns or '상권업종중분류명' not in self.df.columns:
|
| 430 |
return None
|
| 431 |
|
|
|
|
| 432 |
specialization_data = []
|
| 433 |
for region in self.df['시도명'].unique():
|
| 434 |
region_df = self.df[self.df['시도명'] == region]
|
|
|
|
| 462 |
|
| 463 |
m = folium.Map(location=[center_lat, center_lon], zoom_start=11, tiles='OpenStreetMap')
|
| 464 |
|
|
|
|
| 465 |
heat_data = [[row['위��'], row['경도']] for _, row in df_sample.iterrows()]
|
| 466 |
HeatMap(heat_data, radius=15, blur=25, max_zoom=13).add_to(m)
|
| 467 |
|
|
|
|
| 493 |
"""Fireworks AI 기반 자연어 처리 (스트리밍 지원 + 웹검색)"""
|
| 494 |
|
| 495 |
def __init__(self, api_key: str = None):
|
|
|
|
| 496 |
self.api_key = api_key or os.getenv("FIREWORKS_API_KEY")
|
| 497 |
self.base_url = "https://api.fireworks.ai/inference/v1/chat/completions"
|
| 498 |
|
|
|
|
| 502 |
def process_query_stream(self, query: str, data_context: Dict, chat_history: List = None, web_search_results: str = None):
|
| 503 |
"""자연어 쿼리 처리 (스트리밍 모드) - 웹검색 결과 포함"""
|
| 504 |
|
|
|
|
| 505 |
web_context = ""
|
| 506 |
if web_search_results and "⚠️" not in web_search_results:
|
| 507 |
web_context = f"""
|
|
|
|
| 520 |
|
| 521 |
구체적인 숫자와 비율로 정량적 분석을 제공하세요.
|
| 522 |
창업, 투자, 경쟁 분석 관점에서 실용적 인사이트를 제공하세요.
|
| 523 |
+
웹 검색 결과가 제공된 경우 최신 트렌드와 함께 분석하세요.
|
| 524 |
+
반드시 한국어로 답변하세요."""
|
| 525 |
|
| 526 |
messages = [{"role": "system", "content": system_prompt}]
|
| 527 |
if chat_history:
|
|
|
|
| 533 |
"max_tokens": 4800,
|
| 534 |
"temperature": 0.7,
|
| 535 |
"messages": messages,
|
| 536 |
+
"stream": True
|
| 537 |
}
|
| 538 |
|
| 539 |
headers = {
|
|
|
|
| 547 |
headers=headers,
|
| 548 |
json=payload,
|
| 549 |
timeout=60,
|
| 550 |
+
stream=True
|
| 551 |
)
|
| 552 |
|
| 553 |
if response.status_code == 200:
|
|
|
|
| 554 |
for line in response.iter_lines():
|
| 555 |
if line:
|
| 556 |
line_text = line.decode('utf-8')
|
| 557 |
if line_text.startswith('data: '):
|
| 558 |
+
data_str = line_text[6:]
|
| 559 |
if data_str.strip() == '[DONE]':
|
| 560 |
break
|
| 561 |
try:
|
|
|
|
| 608 |
|
| 609 |
app_state.analyzer = MarketAnalyzer(df)
|
| 610 |
|
|
|
|
| 611 |
stats = f"""
|
| 612 |
✅ **데이터 로드 완료!**
|
| 613 |
+
{'=' * 40}
|
| 614 |
+
📊 **분석 통계**
|
| 615 |
+
• 총 점포: {len(df):,}개
|
| 616 |
+
• 분석 지역: {', '.join(regions)}
|
| 617 |
+
• 업종 수: {df['상권업종중분류명'].nunique()}개
|
| 618 |
+
• 대분류: {df['상권업종대분류명'].nunique()}개
|
| 619 |
+
{'=' * 40}
|
| 620 |
+
💡 이제 인사이트를 확인하거나 AI에게 질문하세요!
|
| 621 |
"""
|
| 622 |
|
| 623 |
return stats, gr.update(visible=True), gr.update(visible=True), gr.update(visible=True)
|
|
|
|
| 640 |
else:
|
| 641 |
result.append(None)
|
| 642 |
|
|
|
|
| 643 |
while len(result) < 11:
|
| 644 |
result.append(None)
|
| 645 |
|
|
|
|
| 647 |
|
| 648 |
|
| 649 |
def chat_respond(message, history):
|
| 650 |
+
"""챗봇 응답 (스트리밍 모드 + 웹검색)"""
|
| 651 |
if app_state.analyzer is None:
|
| 652 |
yield history + [[message, "❌ 먼저 데이터를 로드해주세요!"]]
|
| 653 |
return
|
|
|
|
| 655 |
data_context = app_state.analyzer.analyze_for_llm()
|
| 656 |
|
| 657 |
try:
|
|
|
|
| 658 |
if app_state.llm_processor is None:
|
| 659 |
app_state.llm_processor = LLMQueryProcessor()
|
| 660 |
|
|
|
|
| 661 |
if app_state.brave_client is None:
|
| 662 |
try:
|
| 663 |
app_state.brave_client = BraveSearchClient()
|
| 664 |
except:
|
| 665 |
app_state.brave_client = None
|
| 666 |
|
|
|
|
| 667 |
web_results = None
|
| 668 |
if app_state.brave_client and app_state.brave_client.api_key:
|
|
|
|
| 669 |
search_query = f"한국 상권 창업 트렌드 {message}"
|
| 670 |
web_results = app_state.brave_client.search(search_query, count=3)
|
| 671 |
|
|
|
|
| 672 |
chat_hist = []
|
| 673 |
for user_msg, bot_msg in history:
|
| 674 |
chat_hist.append({"role": "user", "content": user_msg})
|
| 675 |
chat_hist.append({"role": "assistant", "content": bot_msg})
|
| 676 |
|
|
|
|
| 677 |
history = history + [[message, ""]]
|
| 678 |
|
|
|
|
| 679 |
if web_results and "⚠️" not in web_results:
|
| 680 |
history[-1][1] = "🔍 웹 검색 중...\n\n"
|
| 681 |
yield history
|
| 682 |
|
|
|
|
| 683 |
full_response = ""
|
| 684 |
for chunk in app_state.llm_processor.process_query_stream(message, data_context, chat_hist, web_results):
|
| 685 |
full_response += chunk
|
|
|
|
| 687 |
yield history
|
| 688 |
|
| 689 |
except ValueError as e:
|
|
|
|
| 690 |
response = f"""📊 **기본 데이터 분석 결과**
|
| 691 |
|
| 692 |
**전체 현황**
|
|
|
|
| 698 |
환경변수를 설정하세요:
|
| 699 |
```bash
|
| 700 |
export FIREWORKS_API_KEY="your_api_key_here"
|
| 701 |
+
export BRAVE_API_KEY="your_brave_api_key_here"
|
| 702 |
+
```"""
|
|
|
|
|
|
|
| 703 |
|
| 704 |
history = history + [[message, response]]
|
| 705 |
yield history
|
| 706 |
|
| 707 |
|
| 708 |
# ============================================================================
|
| 709 |
+
# 🎨 Comic Classic Theme CSS
|
| 710 |
# ============================================================================
|
| 711 |
|
| 712 |
+
css = """
|
| 713 |
+
/* ===== 🎨 Google Fonts Import ===== */
|
| 714 |
+
@import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&family=Noto+Sans+KR:wght@400;700&display=swap');
|
| 715 |
+
|
| 716 |
+
/* ===== 🎨 Comic Classic 배경 - 빈티지 페이퍼 + 도트 패턴 ===== */
|
| 717 |
+
.gradio-container {
|
| 718 |
+
background-color: #FEF9C3 !important;
|
| 719 |
+
background-image:
|
| 720 |
+
radial-gradient(#1F2937 1px, transparent 1px) !important;
|
| 721 |
+
background-size: 20px 20px !important;
|
| 722 |
+
min-height: 100vh !important;
|
| 723 |
+
font-family: 'Noto Sans KR', 'Comic Neue', cursive, sans-serif !important;
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
/* ===== 허깅페이스 상단 요소 숨김 ===== */
|
| 727 |
+
.huggingface-space-header,
|
| 728 |
+
#space-header,
|
| 729 |
+
.space-header,
|
| 730 |
+
[class*="space-header"],
|
| 731 |
+
.svelte-1ed2p3z,
|
| 732 |
+
.space-header-badge,
|
| 733 |
+
.header-badge,
|
| 734 |
+
[data-testid="space-header"],
|
| 735 |
+
.svelte-kqij2n,
|
| 736 |
+
.svelte-1ax1toq,
|
| 737 |
+
.embed-container > div:first-child {
|
| 738 |
+
display: none !important;
|
| 739 |
+
visibility: hidden !important;
|
| 740 |
+
height: 0 !important;
|
| 741 |
+
width: 0 !important;
|
| 742 |
+
overflow: hidden !important;
|
| 743 |
+
opacity: 0 !important;
|
| 744 |
+
pointer-events: none !important;
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
/* ===== Footer 완전 숨김 ===== */
|
| 748 |
+
footer,
|
| 749 |
+
.footer,
|
| 750 |
+
.gradio-container footer,
|
| 751 |
+
.built-with,
|
| 752 |
+
[class*="footer"],
|
| 753 |
+
.gradio-footer,
|
| 754 |
+
.main-footer,
|
| 755 |
+
div[class*="footer"],
|
| 756 |
+
.show-api,
|
| 757 |
+
.built-with-gradio,
|
| 758 |
+
a[href*="gradio.app"],
|
| 759 |
+
a[href*="huggingface.co/spaces"] {
|
| 760 |
+
display: none !important;
|
| 761 |
+
visibility: hidden !important;
|
| 762 |
+
height: 0 !important;
|
| 763 |
+
padding: 0 !important;
|
| 764 |
+
margin: 0 !important;
|
| 765 |
+
}
|
| 766 |
+
|
| 767 |
+
/* ===== 메인 컨테이너 ===== */
|
| 768 |
+
#col-container {
|
| 769 |
+
max-width: 1400px;
|
| 770 |
+
margin: 0 auto;
|
| 771 |
+
}
|
| 772 |
+
|
| 773 |
+
/* ===== 🎨 헤더 타이틀 - 코믹 스타일 ===== */
|
| 774 |
+
.header-text h1 {
|
| 775 |
+
font-family: 'Bangers', cursive !important;
|
| 776 |
+
color: #1F2937 !important;
|
| 777 |
+
font-size: 3.2rem !important;
|
| 778 |
+
font-weight: 400 !important;
|
| 779 |
+
text-align: center !important;
|
| 780 |
+
margin-bottom: 0.5rem !important;
|
| 781 |
+
text-shadow:
|
| 782 |
+
4px 4px 0px #FACC15,
|
| 783 |
+
6px 6px 0px #1F2937 !important;
|
| 784 |
+
letter-spacing: 3px !important;
|
| 785 |
+
-webkit-text-stroke: 2px #1F2937 !important;
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
/* ===== 🎨 서브타이틀 ===== */
|
| 789 |
+
.subtitle {
|
| 790 |
+
text-align: center !important;
|
| 791 |
+
font-family: 'Noto Sans KR', 'Comic Neue', cursive !important;
|
| 792 |
+
font-size: 1.1rem !important;
|
| 793 |
+
color: #1F2937 !important;
|
| 794 |
+
margin-bottom: 1.5rem !important;
|
| 795 |
+
font-weight: 700 !important;
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
/* ===== 🎨 카드/패널 - 만화 프레임 스타일 ===== */
|
| 799 |
+
.gr-panel,
|
| 800 |
+
.gr-box,
|
| 801 |
+
.gr-form,
|
| 802 |
+
.block,
|
| 803 |
+
.gr-group {
|
| 804 |
+
background: #FFFFFF !important;
|
| 805 |
+
border: 3px solid #1F2937 !important;
|
| 806 |
+
border-radius: 8px !important;
|
| 807 |
+
box-shadow: 6px 6px 0px #1F2937 !important;
|
| 808 |
+
transition: all 0.2s ease !important;
|
| 809 |
+
}
|
| 810 |
+
|
| 811 |
+
.gr-panel:hover,
|
| 812 |
+
.block:hover {
|
| 813 |
+
transform: translate(-2px, -2px) !important;
|
| 814 |
+
box-shadow: 8px 8px 0px #1F2937 !important;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
/* ===== 🎨 입력 필드 (Textbox) ===== */
|
| 818 |
+
textarea,
|
| 819 |
+
input[type="text"],
|
| 820 |
+
input[type="number"] {
|
| 821 |
+
background: #FFFFFF !important;
|
| 822 |
+
border: 3px solid #1F2937 !important;
|
| 823 |
+
border-radius: 8px !important;
|
| 824 |
+
color: #1F2937 !important;
|
| 825 |
+
font-family: 'Noto Sans KR', 'Comic Neue', cursive !important;
|
| 826 |
+
font-size: 1rem !important;
|
| 827 |
+
font-weight: 700 !important;
|
| 828 |
+
transition: all 0.2s ease !important;
|
| 829 |
+
}
|
| 830 |
+
|
| 831 |
+
textarea:focus,
|
| 832 |
+
input[type="text"]:focus,
|
| 833 |
+
input[type="number"]:focus {
|
| 834 |
+
border-color: #3B82F6 !important;
|
| 835 |
+
box-shadow: 4px 4px 0px #3B82F6 !important;
|
| 836 |
+
outline: none !important;
|
| 837 |
+
}
|
| 838 |
+
|
| 839 |
+
textarea::placeholder {
|
| 840 |
+
color: #9CA3AF !important;
|
| 841 |
+
font-weight: 400 !important;
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
/* ===== 🎨 Primary 버튼 - 코믹 블루 ===== */
|
| 845 |
+
.gr-button-primary,
|
| 846 |
+
button.primary,
|
| 847 |
+
.gr-button.primary {
|
| 848 |
+
background: #3B82F6 !important;
|
| 849 |
+
border: 3px solid #1F2937 !important;
|
| 850 |
+
border-radius: 8px !important;
|
| 851 |
+
color: #FFFFFF !important;
|
| 852 |
+
font-family: 'Noto Sans KR', 'Bangers', cursive !important;
|
| 853 |
+
font-weight: 700 !important;
|
| 854 |
+
font-size: 1.2rem !important;
|
| 855 |
+
letter-spacing: 1px !important;
|
| 856 |
+
padding: 14px 28px !important;
|
| 857 |
+
box-shadow: 5px 5px 0px #1F2937 !important;
|
| 858 |
+
transition: all 0.1s ease !important;
|
| 859 |
+
text-shadow: 1px 1px 0px #1F2937 !important;
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
.gr-button-primary:hover,
|
| 863 |
+
button.primary:hover,
|
| 864 |
+
.gr-button.primary:hover {
|
| 865 |
+
background: #2563EB !important;
|
| 866 |
+
transform: translate(-2px, -2px) !important;
|
| 867 |
+
box-shadow: 7px 7px 0px #1F2937 !important;
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
+
.gr-button-primary:active,
|
| 871 |
+
button.primary:active,
|
| 872 |
+
.gr-button.primary:active {
|
| 873 |
+
transform: translate(3px, 3px) !important;
|
| 874 |
+
box-shadow: 2px 2px 0px #1F2937 !important;
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
/* ===== 🎨 Secondary 버튼 - 코믹 레드 ===== */
|
| 878 |
+
.gr-button-secondary,
|
| 879 |
+
button.secondary {
|
| 880 |
+
background: #EF4444 !important;
|
| 881 |
+
border: 3px solid #1F2937 !important;
|
| 882 |
+
border-radius: 8px !important;
|
| 883 |
+
color: #FFFFFF !important;
|
| 884 |
+
font-family: 'Noto Sans KR', 'Bangers', cursive !important;
|
| 885 |
+
font-weight: 700 !important;
|
| 886 |
+
font-size: 1rem !important;
|
| 887 |
+
letter-spacing: 1px !important;
|
| 888 |
+
box-shadow: 4px 4px 0px #1F2937 !important;
|
| 889 |
+
transition: all 0.1s ease !important;
|
| 890 |
+
text-shadow: 1px 1px 0px #1F2937 !important;
|
| 891 |
+
}
|
| 892 |
+
|
| 893 |
+
.gr-button-secondary:hover,
|
| 894 |
+
button.secondary:hover {
|
| 895 |
+
background: #DC2626 !important;
|
| 896 |
+
transform: translate(-2px, -2px) !important;
|
| 897 |
+
box-shadow: 6px 6px 0px #1F2937 !important;
|
| 898 |
+
}
|
| 899 |
+
|
| 900 |
+
/* ===== 🎨 Small 버튼 ===== */
|
| 901 |
+
button.sm,
|
| 902 |
+
.gr-button-sm {
|
| 903 |
+
background: #10B981 !important;
|
| 904 |
+
border: 2px solid #1F2937 !important;
|
| 905 |
+
border-radius: 6px !important;
|
| 906 |
+
color: #FFFFFF !important;
|
| 907 |
+
font-family: 'Noto Sans KR', cursive !important;
|
| 908 |
+
font-weight: 700 !important;
|
| 909 |
+
font-size: 0.9rem !important;
|
| 910 |
+
padding: 8px 16px !important;
|
| 911 |
+
box-shadow: 3px 3px 0px #1F2937 !important;
|
| 912 |
+
transition: all 0.1s ease !important;
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
button.sm:hover,
|
| 916 |
+
.gr-button-sm:hover {
|
| 917 |
+
background: #059669 !important;
|
| 918 |
+
transform: translate(-1px, -1px) !important;
|
| 919 |
+
box-shadow: 4px 4px 0px #1F2937 !important;
|
| 920 |
+
}
|
| 921 |
+
|
| 922 |
+
/* ===== 🎨 로그 출력 영역 ===== */
|
| 923 |
+
.info-log textarea {
|
| 924 |
+
background: #1F2937 !important;
|
| 925 |
+
color: #10B981 !important;
|
| 926 |
+
font-family: 'Courier New', monospace !important;
|
| 927 |
+
font-size: 0.9rem !important;
|
| 928 |
+
font-weight: 400 !important;
|
| 929 |
+
border: 3px solid #10B981 !important;
|
| 930 |
+
border-radius: 8px !important;
|
| 931 |
+
box-shadow: 4px 4px 0px #10B981 !important;
|
| 932 |
+
}
|
| 933 |
+
|
| 934 |
+
/* ===== 🎨 아코디언 - 말풍선 스타일 ===== */
|
| 935 |
+
.gr-accordion {
|
| 936 |
+
background: #FACC15 !important;
|
| 937 |
+
border: 3px solid #1F2937 !important;
|
| 938 |
+
border-radius: 8px !important;
|
| 939 |
+
box-shadow: 4px 4px 0px #1F2937 !important;
|
| 940 |
+
}
|
| 941 |
+
|
| 942 |
+
.gr-accordion-header {
|
| 943 |
+
color: #1F2937 !important;
|
| 944 |
+
font-family: 'Noto Sans KR', 'Comic Neue', cursive !important;
|
| 945 |
+
font-weight: 700 !important;
|
| 946 |
+
font-size: 1.1rem !important;
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
/* ===== 🎨 체크박스 그룹 ===== */
|
| 950 |
+
.gr-checkbox-group {
|
| 951 |
+
background: #FFFFFF !important;
|
| 952 |
+
border: 3px solid #1F2937 !important;
|
| 953 |
+
border-radius: 8px !important;
|
| 954 |
+
padding: 10px !important;
|
| 955 |
+
}
|
| 956 |
+
|
| 957 |
+
input[type="checkbox"] {
|
| 958 |
+
accent-color: #3B82F6 !important;
|
| 959 |
+
width: 18px !important;
|
| 960 |
+
height: 18px !important;
|
| 961 |
+
}
|
| 962 |
+
|
| 963 |
+
/* ===== 🎨 탭 스타일 ===== */
|
| 964 |
+
.gr-tab-nav {
|
| 965 |
+
background: #FACC15 !important;
|
| 966 |
+
border: 3px solid #1F2937 !important;
|
| 967 |
+
border-radius: 8px 8px 0 0 !important;
|
| 968 |
+
box-shadow: 4px 4px 0px #1F2937 !important;
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
.gr-tab-nav button {
|
| 972 |
+
font-family: 'Noto Sans KR', 'Comic Neue', cursive !important;
|
| 973 |
+
font-weight: 700 !important;
|
| 974 |
+
color: #1F2937 !important;
|
| 975 |
+
border: none !important;
|
| 976 |
+
padding: 12px 20px !important;
|
| 977 |
+
}
|
| 978 |
+
|
| 979 |
+
.gr-tab-nav button.selected {
|
| 980 |
+
background: #3B82F6 !important;
|
| 981 |
+
color: #FFFFFF !important;
|
| 982 |
+
border-radius: 6px 6px 0 0 !important;
|
| 983 |
+
}
|
| 984 |
+
|
| 985 |
+
/* ===== 🎨 챗봇 스타일 ===== */
|
| 986 |
+
.gr-chatbot {
|
| 987 |
+
background: #FFFFFF !important;
|
| 988 |
+
border: 3px solid #1F2937 !important;
|
| 989 |
+
border-radius: 8px !important;
|
| 990 |
+
box-shadow: 6px 6px 0px #1F2937 !important;
|
| 991 |
+
}
|
| 992 |
+
|
| 993 |
+
.gr-chatbot .message {
|
| 994 |
+
font-family: 'Noto Sans KR', sans-serif !important;
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
/* ===== 🎨 라벨 스타일 ===== */
|
| 998 |
+
label,
|
| 999 |
+
.gr-input-label,
|
| 1000 |
+
.gr-block-label {
|
| 1001 |
+
color: #1F2937 !important;
|
| 1002 |
+
font-family: 'Noto Sans KR', 'Comic Neue', cursive !important;
|
| 1003 |
+
font-weight: 700 !important;
|
| 1004 |
+
font-size: 1rem !important;
|
| 1005 |
+
}
|
| 1006 |
+
|
| 1007 |
+
/* ===== 🎨 Markdown 스타일 ===== */
|
| 1008 |
+
.gr-markdown {
|
| 1009 |
+
font-family: 'Noto Sans KR', 'Comic Neue', cursive !important;
|
| 1010 |
+
color: #1F2937 !important;
|
| 1011 |
+
}
|
| 1012 |
+
|
| 1013 |
+
.gr-markdown h1,
|
| 1014 |
+
.gr-markdown h2,
|
| 1015 |
+
.gr-markdown h3 {
|
| 1016 |
+
font-family: 'Bangers', 'Noto Sans KR', cursive !important;
|
| 1017 |
+
color: #1F2937 !important;
|
| 1018 |
+
text-shadow: 2px 2px 0px #FACC15 !important;
|
| 1019 |
+
}
|
| 1020 |
+
|
| 1021 |
+
/* ===== 🎨 Plot 영역 ===== */
|
| 1022 |
+
.gr-plot {
|
| 1023 |
+
border: 3px solid #1F2937 !important;
|
| 1024 |
+
border-radius: 8px !important;
|
| 1025 |
+
box-shadow: 4px 4px 0px #1F2937 !important;
|
| 1026 |
+
background: #FFFFFF !important;
|
| 1027 |
+
}
|
| 1028 |
+
|
| 1029 |
+
/* ===== 🎨 HTML 영역 (지도) ===== */
|
| 1030 |
+
.gr-html {
|
| 1031 |
+
border: 4px solid #1F2937 !important;
|
| 1032 |
+
border-radius: 8px !important;
|
| 1033 |
+
box-shadow: 6px 6px 0px #FACC15 !important;
|
| 1034 |
+
overflow: hidden !important;
|
| 1035 |
+
}
|
| 1036 |
+
|
| 1037 |
+
/* ===== 🎨 스크롤바 - 코믹 스타일 ===== */
|
| 1038 |
+
::-webkit-scrollbar {
|
| 1039 |
+
width: 12px;
|
| 1040 |
+
height: 12px;
|
| 1041 |
+
}
|
| 1042 |
+
|
| 1043 |
+
::-webkit-scrollbar-track {
|
| 1044 |
+
background: #FEF9C3;
|
| 1045 |
+
border: 2px solid #1F2937;
|
| 1046 |
+
}
|
| 1047 |
+
|
| 1048 |
+
::-webkit-scrollbar-thumb {
|
| 1049 |
+
background: #3B82F6;
|
| 1050 |
+
border: 2px solid #1F2937;
|
| 1051 |
+
border-radius: 0px;
|
| 1052 |
+
}
|
| 1053 |
+
|
| 1054 |
+
::-webkit-scrollbar-thumb:hover {
|
| 1055 |
+
background: #EF4444;
|
| 1056 |
+
}
|
| 1057 |
+
|
| 1058 |
+
/* ===== 🎨 선택 하이라이트 ===== */
|
| 1059 |
+
::selection {
|
| 1060 |
+
background: #FACC15;
|
| 1061 |
+
color: #1F2937;
|
| 1062 |
+
}
|
| 1063 |
+
|
| 1064 |
+
/* ===== 🎨 링크 스타일 ===== */
|
| 1065 |
+
a {
|
| 1066 |
+
color: #3B82F6 !important;
|
| 1067 |
+
text-decoration: none !important;
|
| 1068 |
+
font-weight: 700 !important;
|
| 1069 |
+
}
|
| 1070 |
+
|
| 1071 |
+
a:hover {
|
| 1072 |
+
color: #EF4444 !important;
|
| 1073 |
+
}
|
| 1074 |
+
|
| 1075 |
+
/* ===== 🎨 Row/Column 간격 ===== */
|
| 1076 |
+
.gr-row {
|
| 1077 |
+
gap: 1.5rem !important;
|
| 1078 |
+
}
|
| 1079 |
+
|
| 1080 |
+
.gr-column {
|
| 1081 |
+
gap: 1rem !important;
|
| 1082 |
+
}
|
| 1083 |
+
|
| 1084 |
+
/* ===== 🎨 Badge 스타일 ===== */
|
| 1085 |
+
.badge-container {
|
| 1086 |
+
display: flex;
|
| 1087 |
+
justify-content: center;
|
| 1088 |
+
gap: 15px;
|
| 1089 |
+
flex-wrap: wrap;
|
| 1090 |
+
margin: 20px 0;
|
| 1091 |
+
}
|
| 1092 |
+
|
| 1093 |
+
.comic-badge {
|
| 1094 |
+
display: inline-flex;
|
| 1095 |
+
align-items: center;
|
| 1096 |
+
gap: 8px;
|
| 1097 |
+
padding: 12px 24px;
|
| 1098 |
+
border: 3px solid #1F2937;
|
| 1099 |
+
border-radius: 8px;
|
| 1100 |
+
text-decoration: none;
|
| 1101 |
+
font-weight: 700;
|
| 1102 |
+
font-size: 1em;
|
| 1103 |
+
transition: all 0.2s ease;
|
| 1104 |
+
box-shadow: 4px 4px 0px #1F2937;
|
| 1105 |
+
font-family: 'Noto Sans KR', sans-serif;
|
| 1106 |
+
}
|
| 1107 |
+
|
| 1108 |
+
.comic-badge:hover {
|
| 1109 |
+
transform: translate(-2px, -2px);
|
| 1110 |
+
box-shadow: 6px 6px 0px #1F2937;
|
| 1111 |
+
}
|
| 1112 |
+
|
| 1113 |
+
.comic-badge-yellow {
|
| 1114 |
+
background: #FACC15;
|
| 1115 |
+
color: #1F2937;
|
| 1116 |
+
}
|
| 1117 |
+
|
| 1118 |
+
.comic-badge-blue {
|
| 1119 |
+
background: #3B82F6;
|
| 1120 |
+
color: #FFFFFF;
|
| 1121 |
+
}
|
| 1122 |
+
|
| 1123 |
+
.comic-badge-green {
|
| 1124 |
+
background: #10B981;
|
| 1125 |
+
color: #FFFFFF;
|
| 1126 |
+
}
|
| 1127 |
+
|
| 1128 |
+
/* ===== 반응형 조정 ===== */
|
| 1129 |
+
@media (max-width: 768px) {
|
| 1130 |
+
.header-text h1 {
|
| 1131 |
+
font-size: 2rem !important;
|
| 1132 |
+
text-shadow:
|
| 1133 |
+
3px 3px 0px #FACC15,
|
| 1134 |
+
4px 4px 0px #1F2937 !important;
|
| 1135 |
+
}
|
| 1136 |
|
| 1137 |
+
.gr-button-primary,
|
| 1138 |
+
button.primary {
|
| 1139 |
+
padding: 12px 20px !important;
|
| 1140 |
+
font-size: 1rem !important;
|
| 1141 |
+
}
|
| 1142 |
+
|
| 1143 |
+
.gr-panel,
|
| 1144 |
+
.block {
|
| 1145 |
+
box-shadow: 4px 4px 0px #1F2937 !important;
|
| 1146 |
+
}
|
| 1147 |
+
}
|
| 1148 |
+
|
| 1149 |
+
/* ===== 🎨 다크모드 비활성화 ===== */
|
| 1150 |
+
@media (prefers-color-scheme: dark) {
|
| 1151 |
+
.gradio-container {
|
| 1152 |
+
background-color: #FEF9C3 !important;
|
| 1153 |
+
}
|
| 1154 |
+
}
|
| 1155 |
+
"""
|
| 1156 |
|
| 1157 |
+
|
| 1158 |
+
# ============================================================================
|
| 1159 |
+
# Gradio UI
|
| 1160 |
+
# ============================================================================
|
| 1161 |
+
|
| 1162 |
+
with gr.Blocks(title="AI 상권 분석 시스템", css=css) as demo:
|
| 1163 |
+
|
| 1164 |
+
# HOME Badge
|
| 1165 |
gr.HTML("""
|
| 1166 |
+
<div style="text-align: center; margin: 20px 0 10px 0;">
|
| 1167 |
+
<a href="https://www.humangen.ai" target="_blank" style="text-decoration: none;">
|
| 1168 |
+
<img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge" alt="HOME">
|
| 1169 |
+
</a>
|
| 1170 |
+
</div>
|
| 1171 |
+
""")
|
| 1172 |
+
|
| 1173 |
+
# Header Title
|
| 1174 |
+
gr.Markdown(
|
| 1175 |
+
"""
|
| 1176 |
+
# 🏪 AI 상권 분석 시스템 PRO 📊
|
| 1177 |
+
""",
|
| 1178 |
+
elem_classes="header-text"
|
| 1179 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1180 |
|
| 1181 |
+
gr.Markdown(
|
| 1182 |
+
"""
|
| 1183 |
+
<p class="subtitle">⚡ 전국 상가(상권) 데이터 실시간 분석 | 스트리밍 + 웹검색 🔍 | 10가지 심층 인사이트 🚀</p>
|
| 1184 |
+
""",
|
| 1185 |
+
)
|
| 1186 |
+
|
| 1187 |
+
# 배지
|
| 1188 |
+
gr.HTML("""
|
| 1189 |
+
<div class="badge-container">
|
| 1190 |
+
<a href="https://open.kakao.com/o/peIe8KWh" target="_blank" class="comic-badge comic-badge-yellow">
|
| 1191 |
+
<span>💬</span>
|
| 1192 |
<span>오픈채팅 바로가기</span>
|
| 1193 |
</a>
|
| 1194 |
+
<a href="https://ginigen.ai" target="_blank" class="comic-badge comic-badge-blue">
|
| 1195 |
+
<span>🍌</span>
|
| 1196 |
<span>나노 바나나 애드온 무료 서비스</span>
|
| 1197 |
</a>
|
| 1198 |
</div>
|
| 1199 |
""")
|
|
|
|
| 1200 |
|
| 1201 |
+
# API 상태
|
| 1202 |
+
api_status = "✅ 설정됨" if os.getenv("FIREWORKS_API_KEY") else "⚠️ 미설정"
|
| 1203 |
+
brave_status = "✅ 활성화" if os.getenv("BRAVE_API_KEY") else "⚠️ 비활성화"
|
| 1204 |
+
|
| 1205 |
+
with gr.Row(equal_height=False):
|
| 1206 |
+
# 왼쪽 컬럼 - 설정
|
| 1207 |
+
with gr.Column(scale=1, min_width=300):
|
| 1208 |
+
gr.Markdown("### ⚙️ 분석 설정")
|
| 1209 |
|
| 1210 |
+
gr.Markdown(f"""
|
| 1211 |
+
**🔑 API 상태**
|
| 1212 |
+
- Fireworks AI: {api_status}
|
| 1213 |
+
- Brave Search: {brave_status}
|
| 1214 |
+
""")
|
| 1215 |
|
| 1216 |
region_select = gr.CheckboxGroup(
|
| 1217 |
choices=list(MarketDataLoader.REGIONS.keys()),
|
|
|
|
| 1219 |
label="📍 분석 지역 선택 (최대 5개 권장)"
|
| 1220 |
)
|
| 1221 |
|
| 1222 |
+
load_btn = gr.Button(
|
| 1223 |
+
"📊 데이터 로드하기!",
|
| 1224 |
+
variant="primary",
|
| 1225 |
+
size="lg"
|
| 1226 |
+
)
|
| 1227 |
|
| 1228 |
+
with gr.Accordion("📜 로드 상태", open=True):
|
| 1229 |
+
status_box = gr.Markdown(
|
| 1230 |
+
"👈 지역을 선택하고 데이터를 로드하세요!",
|
| 1231 |
+
elem_classes="info-log"
|
| 1232 |
+
)
|
| 1233 |
+
|
| 1234 |
+
# 오른쪽 컬럼 - 메인 콘텐츠
|
| 1235 |
+
with gr.Column(scale=3, min_width=600):
|
| 1236 |
with gr.Tabs() as tabs:
|
| 1237 |
+
# 탭 1: 인사이트 대시보드
|
| 1238 |
with gr.Tab("📊 인사이트 대시보드", id=0) as tab1:
|
| 1239 |
insights_content = gr.Column(visible=False)
|
| 1240 |
|
|
|
|
| 1246 |
gr.Markdown("### 📈 10가지 심층 상권 인사이트")
|
| 1247 |
|
| 1248 |
with gr.Row():
|
| 1249 |
+
chart1 = gr.Plot(label="🏆 업종별 점포 수")
|
| 1250 |
+
chart2 = gr.Plot(label="📊 대분류 분포")
|
| 1251 |
|
| 1252 |
with gr.Row():
|
| 1253 |
+
chart3 = gr.Plot(label="🏢 층별 분포")
|
| 1254 |
+
chart4 = gr.Plot(label="🎨 업종 다양성")
|
| 1255 |
|
| 1256 |
with gr.Row():
|
| 1257 |
+
chart5 = gr.Plot(label="🏪 프랜차이즈 분석")
|
| 1258 |
+
chart6 = gr.Plot(label="📍 층 선호도")
|
| 1259 |
|
| 1260 |
with gr.Row():
|
| 1261 |
+
chart7 = gr.Plot(label="🔥 지역 밀집도")
|
| 1262 |
+
chart8 = gr.Plot(label="🔗 업종 상관관계")
|
| 1263 |
|
| 1264 |
with gr.Row():
|
| 1265 |
+
chart9 = gr.Plot(label="🔍 소분류 트렌드")
|
| 1266 |
+
chart10 = gr.Plot(label="🎯 지역 특화")
|
| 1267 |
|
| 1268 |
+
# 탭 2: AI 챗봇
|
| 1269 |
+
with gr.Tab("🤖 AI 분석 챗봇 ⚡🔍", id=1) as tab2:
|
| 1270 |
chat_content = gr.Column(visible=False)
|
| 1271 |
|
| 1272 |
with chat_content:
|
| 1273 |
gr.Markdown("""
|
| 1274 |
+
### 💡 예시 질문
|
| 1275 |
+
강남에서 카페 창업? | 치킨집 포화 지역? | 1층이 유리한 업종? | 프랜차이즈 점유율?
|
| 1276 |
+
|
| 1277 |
+
⚡ **스트리밍**: AI 응답이 실시간으로 표시됩니다!
|
| 1278 |
+
🔍 **웹검색**: 최신 상권 트렌드를 자동 반영합니다!
|
| 1279 |
""")
|
| 1280 |
|
| 1281 |
+
chatbot = gr.Chatbot(
|
| 1282 |
+
height=450,
|
| 1283 |
+
label="AI 상권 분석 어시스턴트"
|
| 1284 |
+
)
|
| 1285 |
|
| 1286 |
with gr.Row():
|
| 1287 |
msg_input = gr.Textbox(
|
|
|
|
| 1289 |
show_label=False,
|
| 1290 |
scale=4
|
| 1291 |
)
|
| 1292 |
+
submit_btn = gr.Button("🚀 전송", variant="primary", scale=1)
|
| 1293 |
|
|
|
|
| 1294 |
with gr.Row():
|
| 1295 |
+
sample_btn1 = gr.Button("☕ 강남 카페 창업?", size="sm")
|
| 1296 |
+
sample_btn2 = gr.Button("🍗 치킨집 포화 지역?", size="sm")
|
| 1297 |
+
sample_btn3 = gr.Button("🏢 1층 유리한 업종?", size="sm")
|
| 1298 |
+
sample_btn4 = gr.Button("🏪 프랜차이즈 점유율?", size="sm")
|
| 1299 |
|
| 1300 |
+
# 사용 가이드
|
| 1301 |
+
gr.Markdown("""
|
| 1302 |
+
---
|
| 1303 |
+
### 📖 사용 가이드
|
| 1304 |
+
1️⃣ 지역 선택 → 2️⃣ 데이터 로드 → 3️⃣ 10가지 인사이트 확인 또는 AI에게 질문!
|
| 1305 |
+
|
| 1306 |
+
### 📊 제공되는 10가지 분석
|
| 1307 |
+
| 분석 항목 | 설명 |
|
| 1308 |
+
|----------|------|
|
| 1309 |
+
| 🏆 업종별 점포 수 | 가장 많은 업종 TOP 15 |
|
| 1310 |
+
| 📊 대분류 분포 | 소매/음식/서비스 등 비율 |
|
| 1311 |
+
| 🏢 층별 분포 | 지하/1층/상층 입지 분석 |
|
| 1312 |
+
| 🎨 업종 다양성 | 지역별 업종 다양성 지수 |
|
| 1313 |
+
| 🏪 프랜차이즈 분석 | 개인 vs 프랜차이즈 비율 |
|
| 1314 |
+
| 📍 층 선호도 | 업종별 선호 층수 |
|
| 1315 |
+
| 🔥 지역 밀집도 | 점포 수 상위 지역 |
|
| 1316 |
+
| 🔗 업종 상관관계 | 같이 나타나는 업종 패턴 |
|
| 1317 |
+
| 🔍 소분류 트렌드 | 세부 업종 분포 |
|
| 1318 |
+
| 🎯 지역 특화 | 각 지역의 특화 업종 |
|
| 1319 |
+
|
| 1320 |
+
💡 **Tip**: API 키 없이도 10가지 시각화 분석과 기본 ���계를 확인할 수 있습니다!
|
| 1321 |
+
""")
|
| 1322 |
+
|
| 1323 |
# 이벤트 핸들러
|
| 1324 |
load_btn.click(
|
| 1325 |
fn=load_data,
|
|
|
|
| 1330 |
outputs=[map_output, chart1, chart2, chart3, chart4, chart5, chart6, chart7, chart8, chart9, chart10]
|
| 1331 |
)
|
| 1332 |
|
| 1333 |
+
# 챗봇 이벤트
|
| 1334 |
submit_btn.click(
|
| 1335 |
fn=chat_respond,
|
| 1336 |
inputs=[msg_input, chatbot],
|
|
|
|
| 1356 |
yield result
|
| 1357 |
return handler
|
| 1358 |
|
| 1359 |
+
sample_btn1.click(fn=create_sample_click("강남에서 카페 창업하려면 어떻게 해야 하나요?"), inputs=[chatbot], outputs=[chatbot])
|
| 1360 |
+
sample_btn2.click(fn=create_sample_click("치킨집이 가장 포화된 지역은 어디인가요?"), inputs=[chatbot], outputs=[chatbot])
|
| 1361 |
+
sample_btn3.click(fn=create_sample_click("1층이 유리한 업종은 무엇인가요?"), inputs=[chatbot], outputs=[chatbot])
|
| 1362 |
+
sample_btn4.click(fn=create_sample_click("프랜차이즈 점유율이 높은 업종은?"), inputs=[chatbot], outputs=[chatbot])
|
| 1363 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1364 |
|
| 1365 |
# 실행
|
| 1366 |
if __name__ == "__main__":
|