seawolf2357 commited on
Commit
1cbfadc
·
verified ·
1 Parent(s): a746f28

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -1277
app.py DELETED
@@ -1,1277 +0,0 @@
1
- """
2
- 기업마당 AI 분석기 - 메인 애플리케이션
3
- - 벡터 DB 캐시 통합으로 빠른 로딩
4
- - KST 10:00/22:00 자동 동기화
5
- """
6
- import gradio as gr
7
- import pandas as pd
8
- import json
9
- import os
10
- import tempfile
11
- from pathlib import Path
12
- from typing import Optional, Tuple, List, Dict, Generator
13
- from datetime import datetime
14
-
15
- from utils import (
16
- CATEGORY_CODES, REGION_LIST, SIDO_LIST, ORG_TYPE_OPTIONS, SORT_OPTIONS, STATUS_OPTIONS,
17
- COMPANY_TYPE_OPTIONS, CORP_TYPE_OPTIONS, COMPANY_SIZE_OPTIONS, INDUSTRY_MAJOR_OPTIONS,
18
- CORE_INDUSTRY_OPTIONS, NATIONAL_STRATEGIC_TECH, CREDIT_GRADE_OPTIONS, TCB_GRADE_OPTIONS,
19
- ISO_CERT_OPTIONS, extract_region_from_text, extract_region_from_hashtags,
20
- classify_org_type, parse_deadline, is_ongoing, calculate_age, calculate_company_age
21
- )
22
-
23
- from file_api import (
24
- fetch_all_from_api, fetch_with_cache, download_file, extract_text_from_file, extract_zip_files,
25
- call_groq_api_stream, fetch_announcement_detail, CACHE_AVAILABLE
26
- )
27
-
28
- # 캐시 시스템 임포트
29
- if CACHE_AVAILABLE:
30
- from cache_db import (
31
- initialize_cache_system, get_sync_status, manual_sync,
32
- get_cached_announcements, get_cache
33
- )
34
-
35
-
36
- # ============================================================
37
- # 공고 조회 함수 (캐시 통합)
38
- # ============================================================
39
- def fetch_announcements(keyword="", category="전체", region="전체(지역)", org_type="전체",
40
- sort_by="등록일순", status_filter="진행중", page=1, rows=20) -> Tuple[pd.DataFrame, str]:
41
- """기업마당 API로 공고 목록 조회 (캐시 우선)"""
42
- try:
43
- # ⭐ 항상 캐시 우선 사용 (필터가 있어도 캐시에서 필터링 - 빠름!)
44
- items, status_prefix = fetch_with_cache(category, region, keyword)
45
-
46
- if not items:
47
- return pd.DataFrame(), f"⚠️ 검색 결과가 없습니다. {status_prefix}"
48
-
49
- data = []
50
- for item in items:
51
- if not isinstance(item, dict):
52
- continue
53
- author = item.get("author", "") or item.get("jrsdInsttNm", "")
54
- title = item.get("title", "") or item.get("pblancNm", "")
55
- exec_org = item.get("excInsttNm", "") or ""
56
- hash_tags = item.get("hashTags", "")
57
-
58
- item_region = extract_region_from_hashtags(hash_tags)
59
- if not item_region:
60
- item_region = extract_region_from_text(title)
61
- if not item_region:
62
- item_region = extract_region_from_text(author)
63
-
64
- item_org_type = classify_org_type(author)
65
- req_dt = item.get("reqstDt", "") or item.get("reqstBeginEndDe", "")
66
- item_ongoing = is_ongoing(req_dt)
67
- pub_date = item.get("pubDate", "") or item.get("creatPnttm", "") or ""
68
- if pub_date and len(str(pub_date)) >= 10:
69
- pub_date = str(pub_date)[:10]
70
-
71
- link = item.get("link", "") or item.get("pblancUrl", "")
72
- pblanc_id = item.get("seq", "") or item.get("pblancId", "")
73
-
74
- # 일반 첨부파일 (서식, 양식 등)
75
- attachments = []
76
- file_url = item.get("flpthNm", "") or ""
77
- file_name = item.get("fileNm", "") or ""
78
- if file_url and file_name:
79
- # 여러 파일이 @로 구분되어 있을 수 있음
80
- urls = file_url.split("@") if "@" in file_url else [file_url]
81
- names = file_name.split("@") if "@" in file_name else [file_name]
82
- for i, (url, name) in enumerate(zip(urls, names)):
83
- url = url.strip()
84
- name = name.strip()
85
- if url and name:
86
- ext = Path(name).suffix.lower()[1:] if Path(name).suffix else "unknown"
87
- attachments.append({"url": url, "filename": name, "type": ext})
88
-
89
- # ⭐ 본문출력파일 (공고 본문 PDF/HWP) - AI 분석용 핵심 파일
90
- print_file = None
91
- print_url = item.get("printFlpthNm", "") or ""
92
- print_name = item.get("printFileNm", "") or ""
93
- if print_url and print_name:
94
- print_url = print_url.strip()
95
- print_name = print_name.strip()
96
- ext = Path(print_name).suffix.lower()[1:] if Path(print_name).suffix else "unknown"
97
- print_file = {"url": print_url, "filename": print_name, "type": ext}
98
-
99
- description = item.get("description", "") or item.get("bsnsSumryCn", "")
100
- if description:
101
- import re
102
- description = re.sub(r'<[^>]+>', '', description).strip()
103
-
104
- deadline = parse_deadline(req_dt)
105
- row = {
106
- "지원분야": item.get("lcategory", "") or item.get("pldirSportRealmLclasCodeNm", ""),
107
- "지원사업명": title, "신청기간": req_dt, "소관부처": author,
108
- "수행기관": exec_org, "등록일": pub_date, "조회수": item.get("inqireCo", "") or "",
109
- "상세링크": link, "공고ID": pblanc_id,
110
- "사업개요": description[:200] + "..." if len(description) > 200 else description,
111
- "첨부파일": attachments, # 일반 첨부파일 (서식, 양식)
112
- "본문출력파일": print_file, # ⭐ AI 분석용 핵심 파일
113
- "지원대상": item.get("trgetNm", ""),
114
- "문의처": item.get("refrncNm", ""), "신청URL": item.get("rceptEngnHmpgUrl", ""),
115
- "_org_type": item_org_type, "_ongoing": item_ongoing, "_deadline": deadline,
116
- "_pub_date": pub_date, "_region": item_region,
117
- }
118
- data.append(row)
119
-
120
- if not data:
121
- return pd.DataFrame(), f"⚠️ 검색 결과가 없습니다. {status_prefix}"
122
-
123
- df = pd.DataFrame(data)
124
- total_before_filter = len(df)
125
-
126
- if org_type == "중앙부처":
127
- df = df[df["_org_type"] == "중앙부처"]
128
- elif org_type == "지자체":
129
- df = df[df["_org_type"] == "지자체"]
130
- if region and region != "전체(지역)":
131
- df = df[df["_region"] == region]
132
- if status_filter == "진행중":
133
- df = df[df["_ongoing"] == True]
134
-
135
- if sort_by == "등록일순":
136
- df = df.sort_values(by="_pub_date", ascending=False)
137
- elif sort_by == "마감일순":
138
- df = df.sort_values(by="_deadline", ascending=True, na_position='last')
139
-
140
- if len(df) == 0:
141
- return pd.DataFrame(), f"⚠️ 필터 조건에 맞는 결과가 없습니다. (전체 {total_before_filter}건 중)"
142
-
143
- total_filtered = len(df)
144
- start_idx = (page - 1) * rows
145
- end_idx = start_idx + rows
146
- df_page = df.iloc[start_idx:end_idx].copy()
147
- df_page.insert(0, "번호", range(total_filtered - start_idx, total_filtered - start_idx - len(df_page), -1))
148
-
149
- # ⭐ 원본 데이터 보존 (AI 분석용)
150
- df_full = df_page.copy()
151
-
152
- # 표시용 DataFrame 생성
153
- internal_cols = [c for c in df_page.columns if c.startswith("_")]
154
- df_display = df_page.drop(columns=internal_cols)
155
-
156
- # 표시용에서 불필요한 컬럼 숨기기
157
- hide_cols = ["공고ID", "첨부파일", "본문출력파일", "사업개요", "문의처", "신청URL"]
158
- for col in hide_cols:
159
- if col in df_display.columns:
160
- df_display = df_display.drop(columns=[col])
161
-
162
- status = f"✅ {len(df_page)}건 표시 (페이지 {page}) | 필터 결과: {total_filtered}건 | 수집: {total_before_filter}건"
163
- if status_prefix:
164
- status = f"{status_prefix} | {status}"
165
-
166
- # 표시용 df, 상태, 원본 df 반환
167
- return df_display, status, df_full
168
- except Exception as e:
169
- import traceback
170
- return pd.DataFrame(), f"❌ 오류: {str(e)[:80]}", pd.DataFrame()
171
-
172
-
173
- # ============================================================
174
- # AI 분석 함수
175
- # ============================================================
176
- def analyze_announcement(detail_url, project_name, print_file=None, api_description="", progress=gr.Progress()):
177
- """공고 본문출력파일을 다운로드하고 AI로 분석
178
-
179
- Args:
180
- detail_url: 공고 상세 링크
181
- project_name: 공고명
182
- print_file: 본문출력파일 정보 (dict: url, filename, type) - API에서 온 경우
183
- api_description: 사업개요
184
- """
185
- if not detail_url:
186
- yield "❌ 분석할 공고를 선택해주세요."
187
- return
188
-
189
- output = f"# 📄 {project_name}\n\n---\n\n"
190
- all_text = f"## 공고명: {project_name}\n\n"
191
-
192
- if api_description:
193
- all_text += f"### 사업개요:\n{api_description}\n\n"
194
- output += f"📋 **사업개요**\n{api_description}\n\n"
195
- yield output
196
-
197
- # ⭐ 본문출력파일이 없으면 상세 페이지에서 추출 시도
198
- if not print_file or not isinstance(print_file, dict) or not print_file.get("url"):
199
- progress(0.1, desc="상세 페이지에서 본문출력파일 검색 중...")
200
- output += "🔍 **상세 페이지에서 본문출력파일 검색 중...**\n"
201
- yield output
202
-
203
- try:
204
- content, attachments, scraped_print_file = fetch_announcement_detail(detail_url)
205
-
206
- if scraped_print_file:
207
- print_file = scraped_print_file
208
- output += f" ✅ 본문출력파일 발견: `{print_file.get('filename')}`\n\n"
209
- yield output
210
- else:
211
- output += f" ⚠️ 본문출력파일을 찾지 못했습니다.\n"
212
- if attachments:
213
- output += f" 📎 일반 첨부파일 {len(attachments)}개 발견:\n"
214
- for att in attachments:
215
- output += f" - {att.get('filename')}\n"
216
- output += "\n"
217
- yield output
218
- except Exception as e:
219
- output += f" ❌ 상세 페이지 조회 실패: {str(e)}\n\n"
220
- yield output
221
-
222
- extracted_text = None
223
-
224
- # ⭐ 본문출력파일 분석 (핵심!)
225
- if print_file and isinstance(print_file, dict) and print_file.get("url"):
226
- output += f"📄 **본문출력파일 발견**\n"
227
- output += f" - 파일명: `{print_file.get('filename', '알 수 없음')}`\n"
228
- output += f" - 형식: {print_file.get('type', 'unknown').upper()}\n\n"
229
- yield output
230
-
231
- with tempfile.TemporaryDirectory() as tmp_dir:
232
- progress(0.2, desc="본문출력파일 다운로드 중...")
233
- output += f"📥 다운로드 중...\n"
234
- yield output
235
-
236
- file_path, error = download_file(print_file['url'], tmp_dir, print_file.get('filename'))
237
-
238
- if error:
239
- output += f" - ⚠️ 다운로드 실패: {error}\n"
240
- yield output
241
- elif file_path:
242
- progress(0.5, desc="텍스트 추출 중...")
243
- output += f" - ✅ 다운로드 완료\n"
244
- yield output
245
-
246
- # 텍스트 추출
247
- text, err = extract_text_from_file(file_path)
248
- if text:
249
- extracted_text = text
250
- output += f" - ✅ 텍스트 추출 성공 ({len(text):,} 글자)\n\n"
251
- all_text += f"### 본문출력파일 내용:\n\n{text[:8000]}\n\n"
252
- else:
253
- output += f" - ⚠️ 텍스트 추출 실패: {err}\n"
254
- yield output
255
- else:
256
- output += "⚠️ **본문출력파일이 없습니다.**\n"
257
- output += "사업개요만으로 분석을 진행합니다.\n\n"
258
- yield output
259
-
260
- if len(all_text) < 100 and not extracted_text:
261
- output += "\n❌ **분석할 내용이 충분하지 않습니다.**\n"
262
- output += "본문출력파일이 없거나 텍스트 추출에 실패했습니다.\n"
263
- yield output
264
- return
265
-
266
- output += f"\n📊 **분석 준비 완료** (총 {len(all_text):,}자)\n\n---\n\n## 🤖 AI 분석 결과\n\n"
267
- yield output
268
-
269
- progress(0.7, desc="AI 분석 중...")
270
- system_prompt = """당신은 정부 지원사업 공고 분석 전문가입니다.
271
- 주어진 공고 내용을 분석하여 다음 항목을 명확하게 정리해주세요:
272
- - 사업명, 주관기관, 지원 목적
273
- - 신청 자격 요건, 제외 대상
274
- - 지원 금액/규모, 지원 항목/내용
275
- - 신청 기간, 신청 방법, 제출 서류
276
- - 중요 유의사항, 제한 사항
277
- - 이 사업의 핵심 포인트를 3줄로 요약"""
278
-
279
- messages = [
280
- {"role": "system", "content": system_prompt},
281
- {"role": "user", "content": f"다음 지원사업 공고를 분석해주세요:\n\n{all_text[:15000]}"}
282
- ]
283
-
284
- for chunk in call_groq_api_stream(messages):
285
- output += chunk
286
- yield output
287
-
288
- output += "\n\n---\n✅ **분석 완료**"
289
- yield output
290
-
291
-
292
- # ============================================================
293
- # 맞춤 과제 매칭 함수
294
- # ============================================================
295
- def analyze_uploaded_documents(files, progress=gr.Progress()):
296
- """업로드된 문서들을 분석하여 기업 정보 추출"""
297
- if not files:
298
- yield "❌ 분석할 파일을 업로드해주세요."
299
- return
300
-
301
- output = "# 📄 업로드 문서 분석 결과\n\n"
302
- all_extracted_text = []
303
-
304
- for i, file in enumerate(files):
305
- progress((i + 1) / len(files), desc=f"파일 분석 중... ({i+1}/{len(files)})")
306
- filename = os.path.basename(file.name) if hasattr(file, 'name') else f"파일_{i+1}"
307
- output += f"## 📎 {filename}\n\n"
308
- yield output
309
-
310
- try:
311
- text, error = extract_text_from_file(file.name if hasattr(file, 'name') else file)
312
- if text:
313
- all_extracted_text.append({"filename": filename, "text": text})
314
- preview = text[:500] + "..." if len(text) > 500 else text
315
- output += f"✅ 텍스트 추출 성공 ({len(text):,}자)\n\n```\n{preview}\n```\n\n"
316
- else:
317
- output += f"⚠️ 텍스트 추출 실패: {error}\n\n"
318
- except Exception as e:
319
- output += f"❌ 오류: {str(e)}\n\n"
320
- yield output
321
-
322
- if all_extracted_text:
323
- output += "---\n\n## 🤖 AI 기업정보 추출\n\n"
324
- yield output
325
-
326
- combined_text = "\n\n".join([f"[{item['filename']}]\n{item['text'][:3000]}" for item in all_extracted_text])
327
-
328
- system_prompt = """당신은 기업 서류 분석 전문가입니다.
329
- 주어진 문서들에서 다음 정보를 추출해주세요:
330
- 1. 사업�� 정보 (사업자등록번호, 법인등록번호, 상호, 대표자, 설립일, 주소, 업종)
331
- 2. 재무 정보 (자본금, 매출액, 영업이익, 당기순이익)
332
- 3. 인력 정보 (상시근로자 수, 4대보험 가입자 수)
333
- 4. 인증/등록 정보 (부설연구소, 벤처기업 인증 등)
334
- 5. 기타 특이사항
335
- JSON 형식으로 정리해주세요."""
336
-
337
- messages = [
338
- {"role": "system", "content": system_prompt},
339
- {"role": "user", "content": f"다음 기업 서류들을 분석해주세요:\n\n{combined_text[:12000]}"}
340
- ]
341
-
342
- for chunk in call_groq_api_stream(messages):
343
- output += chunk
344
- yield output
345
-
346
- output += "\n\n---\n✅ **문서 분석 완료**"
347
- yield output
348
-
349
-
350
- def match_announcements_with_profile(profile_data, announcements_df, progress=gr.Progress()):
351
- """기업 프로필과 공고를 매칭"""
352
- if not profile_data:
353
- yield "❌ 기업 프로필을 먼저 입력해주세요."
354
- return
355
-
356
- if announcements_df is None or (isinstance(announcements_df, pd.DataFrame) and announcements_df.empty):
357
- yield "❌ 매칭할 공고 데이터가 없습니다. 먼저 공고를 검색해주세요."
358
- return
359
-
360
- output = "# 🎯 맞춤 과제 매칭 결과\n\n"
361
- output += "## 📋 입력된 기업 프로필\n\n"
362
- output += f"```json\n{json.dumps(profile_data, ensure_ascii=False, indent=2)[:2000]}\n```\n\n"
363
- output += "---\n\n## 🔍 AI 매칭 분석 중...\n\n"
364
- yield output
365
-
366
- announcements_text = ""
367
- df_to_use = announcements_df if isinstance(announcements_df, pd.DataFrame) else pd.DataFrame()
368
- for idx, row in df_to_use.head(20).iterrows():
369
- announcements_text += f"""
370
- ### {row.get('지원사업명', '')}
371
- - 지원분야: {row.get('지원분야', '')}
372
- - 소관부처: {row.get('소관부처', '')}
373
- - 신청기간: {row.get('신청기간', '')}
374
- - 지원대상: {row.get('지원대상', '')}
375
- ---
376
- """
377
-
378
- system_prompt = """당신은 정부 지원사업 매칭 전문가입니다.
379
- 기업 프로필과 공고 목록을 분석하여 신청 가능한 과제를 추천해주세요.
380
- 각 공고에 대해:
381
- - ✅ 적합: 신청 자격 충족
382
- - ⚠️ 확인필요: 일부 조건 확인 필요
383
- - ❌ 부적합: 자격 미달
384
- 추천 순위와 이유를 설명해주세요."""
385
-
386
- messages = [
387
- {"role": "system", "content": system_prompt},
388
- {"role": "user", "content": f"기업 프로필:\n{json.dumps(profile_data, ensure_ascii=False)}\n\n공고 목록:\n{announcements_text[:8000]}"}
389
- ]
390
-
391
- for chunk in call_groq_api_stream(messages):
392
- output += chunk
393
- yield output
394
-
395
- output += "\n\n---\n✅ **매칭 분석 완료**"
396
- yield output
397
-
398
-
399
- # ============================================================
400
- # 캐시 관리 함수
401
- # ============================================================
402
- def get_cache_info():
403
- """캐시 상태 정보 반환"""
404
- if not CACHE_AVAILABLE:
405
- return "⚠️ 캐시 시스템 미사용 (API 직접 호출)"
406
-
407
- status = get_sync_status()
408
- info = f"""📦 **캐시 상태**
409
- - 총 캐시: {status.get('total_count', 0):,}건
410
- - 마지막 동기화: {status.get('last_sync', '없음')}
411
- - ChromaDB: {'✅ 사용' if status.get('chromadb_available') else '❌ 미사용'}
412
- - 스케줄러: {'✅ 활성' if status.get('scheduler_available') else '❌ 비활성'}
413
- """
414
- return info
415
-
416
-
417
- def do_manual_sync():
418
- """수동 동기화 실행"""
419
- if not CACHE_AVAILABLE:
420
- return "⚠️ 캐시 시스템을 사용할 수 없습니다."
421
- return manual_sync()
422
-
423
-
424
- # ============================================================
425
- # CSS 스타일 - Neumorphism Design
426
- # ============================================================
427
- CUSTOM_CSS = """
428
- @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap');
429
-
430
- /* ═══════════════════════════════════════════════════════════
431
- 🔘 NEUMORPHISM 핵심 변수
432
- ═══════════════════════════════════════════════════════════ */
433
- :root {
434
- --neu-bg: #e0e5ec;
435
- --neu-shadow-dark: #a3b1c6;
436
- --neu-shadow-light: #ffffff;
437
- --neu-text: #5a6677;
438
- --neu-text-dark: #3d4856;
439
- --neu-accent: #6d7b8d;
440
- --neu-primary: #4a7dbd;
441
- --neu-success: #48bb78;
442
- --neu-warning: #ed8936;
443
- }
444
-
445
- /* ═══════════════════════════════════════════════════════════
446
- 📦 기본 컨테이너
447
- ═══════════════════════════════════════════════════════════ */
448
- .gradio-container {
449
- font-family: 'Noto Sans KR', sans-serif !important;
450
- background: var(--neu-bg) !important;
451
- max-width: 100% !important;
452
- width: 100% !important;
453
- margin: 0 auto !important;
454
- min-height: 100vh;
455
- padding: 20px !important;
456
- }
457
-
458
- body, .dark {
459
- background: var(--neu-bg) !important;
460
- }
461
-
462
- /* ═══════════════════════════════════════════════════════════
463
- 📐 전체 폭 강제 (탭 전환 시 폭 변화 방지)
464
- ═══════════════════════════════════════════════════════════ */
465
- .gr-tabs, .tabs, .tabitem, .tab-content {
466
- width: 100% !important;
467
- max-width: 100% !important;
468
- }
469
-
470
- .gr-tab-item, .tab-nav button {
471
- flex-shrink: 0 !important;
472
- }
473
-
474
- .gr-row, .gr-column, .row, .column {
475
- width: 100% !important;
476
- max-width: 100% !important;
477
- }
478
-
479
- .gr-block, .block {
480
- width: 100% !important;
481
- max-width: 100% !important;
482
- }
483
-
484
- /* 탭 패널 내부 컨텐츠 */
485
- .gr-tabitem > div, .tabitem > div {
486
- width: 100% !important;
487
- max-width: 100% !important;
488
- }
489
-
490
- /* 컨테이너 내부 요소들 */
491
- .contain {
492
- width: 100% !important;
493
- max-width: 100% !important;
494
- }
495
-
496
- #component-0 {
497
- width: 100% !important;
498
- max-width: 100% !important;
499
- }
500
-
501
- /* ═══════════════════════════════════════════════════════════
502
- 🎯 헤더 배너 (Neumorphism)
503
- ═══════════════════════════════════════════════════════════ */
504
- .header-banner {
505
- background: var(--neu-bg);
506
- color: var(--neu-text-dark);
507
- padding: 28px 32px;
508
- border-radius: 24px;
509
- margin-bottom: 24px;
510
- box-shadow:
511
- 12px 12px 24px var(--neu-shadow-dark),
512
- -12px -12px 24px var(--neu-shadow-light);
513
- }
514
-
515
- .header-banner h1 {
516
- color: var(--neu-text-dark) !important;
517
- text-shadow: 2px 2px 4px var(--neu-shadow-light), -1px -1px 3px rgba(0,0,0,0.1);
518
- }
519
-
520
- .header-banner p {
521
- color: var(--neu-text) !important;
522
- }
523
-
524
- /* ═══════════════════════════════════════════════════════════
525
- 🏷️ 배지 스타일
526
- ═══════════════════════════════════════════════════════════ */
527
- .feature-badge {
528
- display: inline-block;
529
- background: var(--neu-bg);
530
- padding: 8px 16px;
531
- border-radius: 50px;
532
- font-size: 12px;
533
- font-weight: 600;
534
- margin: 4px 4px 0 0;
535
- color: var(--neu-text);
536
- box-shadow:
537
- 4px 4px 8px var(--neu-shadow-dark),
538
- -4px -4px 8px var(--neu-shadow-light);
539
- }
540
-
541
- /* ═══════════════════════════════════════════════════════════
542
- 📋 섹션 헤더
543
- ═══════════════════════════════════════════════════════════ */
544
- .section-header {
545
- background: var(--neu-bg);
546
- color: var(--neu-text-dark);
547
- padding: 14px 20px;
548
- border-radius: 15px;
549
- margin: 16px 0 12px 0;
550
- font-weight: 700;
551
- box-shadow:
552
- 6px 6px 12px var(--neu-shadow-dark),
553
- -6px -6px 12px var(--neu-shadow-light);
554
- }
555
-
556
- /* ═══════════════════════════════════════════════════════════
557
- 🔘 버튼 스타일
558
- ═══════════════════════════════════════════════════════════ */
559
- .gr-button, button.primary, button.secondary {
560
- background: var(--neu-bg) !important;
561
- border: none !important;
562
- border-radius: 50px !important;
563
- padding: 12px 28px !important;
564
- color: var(--neu-text-dark) !important;
565
- font-weight: 600 !important;
566
- box-shadow:
567
- 6px 6px 12px var(--neu-shadow-dark),
568
- -6px -6px 12px var(--neu-shadow-light) !important;
569
- transition: all 0.2s ease !important;
570
- }
571
-
572
- .gr-button:hover, button.primary:hover, button.secondary:hover {
573
- box-shadow:
574
- 4px 4px 8px var(--neu-shadow-dark),
575
- -4px -4px 8px var(--neu-shadow-light) !important;
576
- transform: translateY(-1px);
577
- }
578
-
579
- .gr-button:active, button.primary:active, button.secondary:active {
580
- box-shadow:
581
- inset 4px 4px 8px var(--neu-shadow-dark),
582
- inset -4px -4px 8px var(--neu-shadow-light) !important;
583
- transform: translateY(0);
584
- }
585
-
586
- /* 🟢 분석 버튼 */
587
- .analyze-btn button, .analyze-btn {
588
- background: linear-gradient(145deg, #52c992, #3ea87a) !important;
589
- color: white !important;
590
- box-shadow:
591
- 6px 6px 12px var(--neu-shadow-dark),
592
- -6px -6px 12px var(--neu-shadow-light) !important;
593
- }
594
-
595
- .analyze-btn:hover button, .analyze-btn:hover {
596
- background: linear-gradient(145deg, #3ea87a, #359968) !important;
597
- }
598
-
599
- /* 🟣 매칭 버튼 */
600
- .match-btn button, .match-btn {
601
- background: linear-gradient(145deg, #9f7aea, #805ad5) !important;
602
- color: white !important;
603
- box-shadow:
604
- 6px 6px 12px var(--neu-shadow-dark),
605
- -6px -6px 12px var(--neu-shadow-light) !important;
606
- }
607
-
608
- /* 🟠 동기화 버튼 */
609
- .sync-btn button, .sync-btn {
610
- background: linear-gradient(145deg, #f6ad55, #ed8936) !important;
611
- color: white !important;
612
- box-shadow:
613
- 4px 4px 8px var(--neu-shadow-dark),
614
- -4px -4px 8px var(--neu-shadow-light) !important;
615
- }
616
-
617
- /* ═══════════════════════════════════════════════════════════
618
- 📝 입력 필드 (Inset 효과)
619
- ═══════════════════════════════════════════════════════════ */
620
- .gr-textbox textarea, .gr-textbox input,
621
- input[type="text"], input[type="number"], textarea,
622
- .gr-dropdown select {
623
- background: var(--neu-bg) !important;
624
- border: none !important;
625
- border-radius: 15px !important;
626
- padding: 14px 18px !important;
627
- color: var(--neu-text-dark) !important;
628
- box-shadow:
629
- inset 4px 4px 8px var(--neu-shadow-dark),
630
- inset -4px -4px 8px var(--neu-shadow-light) !important;
631
- }
632
-
633
- .gr-textbox textarea:focus, .gr-textbox input:focus,
634
- input[type="text"]:focus, textarea:focus {
635
- outline: none !important;
636
- box-shadow:
637
- inset 6px 6px 12px var(--neu-shadow-dark),
638
- inset -6px -6px 12px var(--neu-shadow-light) !important;
639
- }
640
-
641
- /* ═══════════════════════════════════════════════════════════
642
- 📊 데이터프레임 테이블
643
- ═══════════════════════════════════════════════════════════ */
644
- .gr-dataframe, .dataframe, table {
645
- background: var(--neu-bg) !important;
646
- border-radius: 20px !important;
647
- overflow: hidden;
648
- box-shadow:
649
- 8px 8px 16px var(--neu-shadow-dark),
650
- -8px -8px 16px var(--neu-shadow-light) !important;
651
- }
652
-
653
- .gr-dataframe th, .dataframe th {
654
- background: var(--neu-bg) !important;
655
- color: var(--neu-text-dark) !important;
656
- font-weight: 700 !important;
657
- padding: 14px 12px !important;
658
- border-bottom: 2px solid var(--neu-shadow-dark) !important;
659
- }
660
-
661
- .gr-dataframe td, .dataframe td {
662
- background: var(--neu-bg) !important;
663
- color: var(--neu-text) !important;
664
- padding: 12px !important;
665
- border-bottom: 1px solid rgba(163, 177, 198, 0.3) !important;
666
- }
667
-
668
- .gr-dataframe tr:hover td, .dataframe tr:hover td {
669
- background: rgba(74, 125, 189, 0.1) !important;
670
- }
671
-
672
- /* ═══════════════════════════════════════════════════════════
673
- 📁 탭 스타일
674
- ═══════════════════════════════════════════════════════════ */
675
- .gr-tabs, .tabs {
676
- background: var(--neu-bg) !important;
677
- }
678
-
679
- .gr-tab-item, .tab-nav button {
680
- background: var(--neu-bg) !important;
681
- border: none !important;
682
- border-radius: 15px 15px 0 0 !important;
683
- padding: 12px 24px !important;
684
- color: var(--neu-text) !important;
685
- font-weight: 600 !important;
686
- margin-right: 4px !important;
687
- box-shadow:
688
- 4px -4px 8px var(--neu-shadow-dark),
689
- -4px -4px 8px var(--neu-shadow-light) !important;
690
- }
691
-
692
- .gr-tab-item.selected, .tab-nav button.selected {
693
- background: var(--neu-bg) !important;
694
- color: var(--neu-primary) !important;
695
- box-shadow:
696
- inset 3px 3px 6px var(--neu-shadow-dark),
697
- inset -3px -3px 6px var(--neu-shadow-light) !important;
698
- }
699
-
700
- /* ═══════════════════════════════════════════════════════════
701
- ✅ 체크박스
702
- ═══════════════════════════════════════════════════════════ */
703
- .gr-checkbox input[type="checkbox"] {
704
- appearance: none;
705
- width: 24px;
706
- height: 24px;
707
- background: var(--neu-bg);
708
- border-radius: 8px;
709
- box-shadow:
710
- inset 3px 3px 6px var(--neu-shadow-dark),
711
- inset -3px -3px 6px var(--neu-shadow-light);
712
- cursor: pointer;
713
- }
714
-
715
- .gr-checkbox input[type="checkbox"]:checked {
716
- background: linear-gradient(145deg, #48bb78, #38a169);
717
- box-shadow:
718
- 3px 3px 6px var(--neu-shadow-dark),
719
- -3px -3px 6px var(--neu-shadow-light);
720
- }
721
-
722
- /* ═══════════════════════════════════════════════════════════
723
- 📋 캐시 정보 박스
724
- ═══════════════════════════════════════════════════════════ */
725
- .cache-info {
726
- background: var(--neu-bg) !important;
727
- border: none !important;
728
- border-radius: 15px !important;
729
- padding: 16px 20px !important;
730
- margin-bottom: 16px !important;
731
- font-size: 13px;
732
- color: var(--neu-text) !important;
733
- box-shadow:
734
- inset 4px 4px 8px var(--neu-shadow-dark),
735
- inset -4px -4px 8px var(--neu-shadow-light) !important;
736
- }
737
-
738
- /* ═══════════════════════════════════════════════════════════
739
- 📄 카드/패널 스타일
740
- ═══════════════════════════════════════════════════════════ */
741
- .gr-panel, .gr-box, .gr-form {
742
- background: var(--neu-bg) !important;
743
- border: none !important;
744
- border-radius: 20px !important;
745
- padding: 20px !important;
746
- box-shadow:
747
- 8px 8px 16px var(--neu-shadow-dark),
748
- -8px -8px 16px var(--neu-shadow-light) !important;
749
- }
750
-
751
- /* ═══════════════════════════════════════════════════════════
752
- 📊 라벨 스타일
753
- ═══════════════════════════════════════════════════════════ */
754
- label, .gr-label {
755
- color: var(--neu-text-dark) !important;
756
- font-weight: 600 !important;
757
- text-shadow: 1px 1px 2px var(--neu-shadow-light);
758
- }
759
-
760
- /* ═══════════════════════════════════════════════════════════
761
- 🔗 링크 스타일
762
- ═══════════════════════════════════════════════════════════ */
763
- a {
764
- color: var(--neu-primary) !important;
765
- text-decoration: none !important;
766
- font-weight: 600;
767
- transition: all 0.2s ease;
768
- }
769
-
770
- a:hover {
771
- color: #3a6aa8 !important;
772
- text-shadow: 0 0 8px rgba(74, 125, 189, 0.3);
773
- }
774
-
775
- /* ═══════════════════════════════════════════════════════════
776
- 📱 스크롤바 스타일
777
- ═══════════════════════════════════════════════════════════ */
778
- ::-webkit-scrollbar {
779
- width: 10px;
780
- height: 10px;
781
- }
782
-
783
- ::-webkit-scrollbar-track {
784
- background: var(--neu-bg);
785
- border-radius: 10px;
786
- box-shadow: inset 2px 2px 4px var(--neu-shadow-dark);
787
- }
788
-
789
- ::-webkit-scrollbar-thumb {
790
- background: linear-gradient(145deg, #d1d9e6, #b8c0cc);
791
- border-radius: 10px;
792
- box-shadow:
793
- 2px 2px 4px var(--neu-shadow-dark),
794
- -2px -2px 4px var(--neu-shadow-light);
795
- }
796
-
797
- /* ═══════════════════════════════════════════════════════════
798
- 🎨 마크다운 출력 영역
799
- ═══════════════════════════════════════════════════════════ */
800
- .gr-markdown {
801
- background: var(--neu-bg) !important;
802
- border-radius: 15px !important;
803
- padding: 20px !important;
804
- color: var(--neu-text-dark) !important;
805
- box-shadow:
806
- inset 4px 4px 8px var(--neu-shadow-dark),
807
- inset -4px -4px 8px var(--neu-shadow-light) !important;
808
- }
809
-
810
- .gr-markdown h1, .gr-markdown h2, .gr-markdown h3 {
811
- color: var(--neu-text-dark) !important;
812
- text-shadow: 2px 2px 4px var(--neu-shadow-light);
813
- }
814
-
815
- /* ═══════════════════════════════════════════════════════════
816
- ✨ 애니메이션
817
- ═══════════════════════════════════════════════════════════ */
818
- @keyframes pulse-neu {
819
- 0%, 100% {
820
- box-shadow:
821
- 8px 8px 16px var(--neu-shadow-dark),
822
- -8px -8px 16px var(--neu-shadow-light);
823
- }
824
- 50% {
825
- box-shadow:
826
- 12px 12px 24px var(--neu-shadow-dark),
827
- -12px -12px 24px var(--neu-shadow-light);
828
- }
829
- }
830
-
831
- .loading {
832
- animation: pulse-neu 1.5s ease-in-out infinite;
833
- }
834
- """
835
-
836
-
837
- # ============================================================
838
- # 메인 인터페이스
839
- # ============================================================
840
- def create_interface():
841
- with gr.Blocks(title="기업마당 AI 분석기", css=CUSTOM_CSS) as demo:
842
- gr.HTML("""
843
- <div class="header-banner">
844
- <div style="display: flex; align-items: center; gap: 20px;">
845
- <div style="width: 64px; height: 64px; background: linear-gradient(145deg, #f0f5fa, #d1d9e6); border-radius: 18px; display: flex; align-items: center; justify-content: center; font-size: 32px; box-shadow: 6px 6px 12px #a3b1c6, -6px -6px 12px #ffffff;">🏢</div>
846
- <div>
847
- <h1 style="margin: 0; font-size: 26px; font-weight: 700;">기업마당 지원사업 AI 분석기</h1>
848
- <p style="margin: 6px 0 0 0; font-size: 14px;">공고 검색 · 첨부파일 자동 분석 · AI 요약 · 맞춤 과제 추출</p>
849
- <div style="margin-top: 12px;">
850
- <span class="feature-badge">📄 HWP/HWPX 지원</span>
851
- <span class="feature-badge">🤖 AI 분석</span>
852
- <span class="feature-badge">🎯 맞춤 매칭</span>
853
- <span class="feature-badge">⚡ 빠른 캐시</span>
854
- </div>
855
- </div>
856
- </div>
857
- </div>
858
- """)
859
-
860
- # 상태 변수
861
- selected_url = gr.State("")
862
- selected_name = gr.State("")
863
- selected_print_file = gr.State(None) # ⭐ 본문출력파일 (dict)
864
- selected_description = gr.State("")
865
- current_df = gr.State(value=pd.DataFrame())
866
- company_profile = gr.State(value={})
867
-
868
- with gr.Tabs():
869
- # 탭 1: 공고 검색
870
- with gr.Tab("🔍 공고 검색"):
871
- # 캐시 상태 표시
872
- with gr.Row():
873
- cache_status = gr.Markdown(value=get_cache_info(), elem_classes=["cache-info"])
874
- sync_btn = gr.Button("🔄 수동 동기화", size="sm", elem_classes=["sync-btn"])
875
-
876
- with gr.Row():
877
- keyword_input = gr.Textbox(label="🔍 검색어", placeholder="예: AI, 스타트업, R&D", scale=3)
878
- category_dropdown = gr.Dropdown(label="📂 지원분야", choices=list(CATEGORY_CODES.keys()), value="전체", scale=1)
879
- region_dropdown = gr.Dropdown(label="📍 지역", choices=REGION_LIST, value="전체(지역)", scale=1)
880
- with gr.Row():
881
- org_type_dropdown = gr.Dropdown(label="🏛️ 기관유형", choices=ORG_TYPE_OPTIONS, value="전체", scale=1)
882
- sort_dropdown = gr.Dropdown(label="📊 정렬", choices=SORT_OPTIONS, value="등록일순", scale=1)
883
- status_dropdown = gr.Dropdown(label="📌 공고상태", choices=STATUS_OPTIONS, value="진행중", scale=1)
884
- with gr.Row():
885
- page_input = gr.Number(label="📄 페이지", value=1, minimum=1, scale=1)
886
- rows_dropdown = gr.Dropdown(label="📊 표시개수", choices=[10, 15, 20, 30, 50], value=20, scale=1)
887
- search_btn = gr.Button("🔎 검색", variant="primary", scale=2)
888
-
889
- status_output = gr.Textbox(label="📊 조회 결과", interactive=False)
890
- results_output = gr.Dataframe(label="📋 공고 목록 (행 클릭으로 선택)", wrap=True, interactive=False)
891
- with gr.Row():
892
- prev_btn = gr.Button("◀️ 이전", size="sm")
893
- next_btn = gr.Button("다음 ▶️", size="sm")
894
- open_link_btn = gr.Button("🔗 선택 공고 열기", size="sm", variant="secondary")
895
-
896
- # 선택된 공고 링크를 표시하는 HTML (JavaScript로 새 탭 열기)
897
- link_output = gr.HTML(value="<div style='padding: 10px; color: #5a6677;'>📌 공고를 선택하면 여기에 링크가 표시됩니다</div>")
898
-
899
- # 탭 2: AI 분석
900
- with gr.Tab("🤖 AI 분석"):
901
- with gr.Row(equal_height=False):
902
- with gr.Column(scale=1, min_width=300):
903
- selected_info = gr.Textbox(label="📌 선택된 공고", placeholder="공고 검색 탭에서 선택", lines=5, interactive=False)
904
- analyze_btn = gr.Button("🚀 AI 분석 시작", variant="primary", size="lg", elem_classes=["analyze-btn"])
905
- with gr.Column(scale=3, min_width=600):
906
- analysis_output = gr.Markdown(value="### 📊 분석 결과\n\n*공고를 선택하고 분석 버튼을 클릭하세요*", height=600)
907
-
908
- # 탭 3: 맞춤 과제 추출
909
- with gr.Tab("🎯 맞춤 과제 추출"):
910
- gr.HTML("""
911
- <div style="background: var(--neu-bg, #e0e5ec); color: #3d4856; padding: 24px 28px; border-radius: 20px; margin-bottom: 24px; box-shadow: 8px 8px 16px #a3b1c6, -8px -8px 16px #ffffff;">
912
- <h2 style="margin: 0 0 8px 0; text-shadow: 2px 2px 4px #ffffff;">🎯 나만의 맞춤 과제 추출</h2>
913
- <p style="margin: 0; color: #5a6677;">기업 정보를 입력하고 문서를 업로드하면 AI가 신청 가능한 과제를 자동으로 매칭해드립니다.</p>
914
- </div>
915
- """)
916
-
917
- with gr.Tabs():
918
- # 서브탭 1: 기업 기본정보
919
- with gr.Tab("1️⃣ 기업 기본정보"):
920
- with gr.Row():
921
- with gr.Column():
922
- gr.HTML('<div class="section-header">📋 사업자 정보</div>')
923
- biz_number = gr.Textbox(label="사업자등록번호", placeholder="000-00-00000")
924
- corp_number = gr.Textbox(label="법인등록번호", placeholder="000000-0000000")
925
- company_name = gr.Textbox(label="상호/법인명", placeholder="(주)회사명")
926
- establish_date = gr.Textbox(label="설립일자", placeholder="YYYY-MM-DD")
927
- company_type = gr.Dropdown(label="기업형태", choices=COMPANY_TYPE_OPTIONS, value="법인사업자")
928
- corp_type = gr.Dropdown(label="법인 종류", choices=CORP_TYPE_OPTIONS, value="주식회사")
929
- company_size = gr.Dropdown(label="기업규모", choices=COMPANY_SIZE_OPTIONS, value="소기업")
930
-
931
- with gr.Column():
932
- gr.HTML('<div class="section-header">🏆 인증 현황</div>')
933
- venture_cert = gr.Checkbox(label="벤처기업 인증")
934
- innobiz_cert = gr.Checkbox(label="이노비즈 인증")
935
- mainbiz_cert = gr.Checkbox(label="메인비즈 인증")
936
- sme_cert = gr.Checkbox(label="중소기업확인서 보유")
937
- small_biz_cert = gr.Checkbox(label="소상공인확인서 보유")
938
- social_venture = gr.Checkbox(label="소셜벤처")
939
- startup_flag = gr.Checkbox(label="스타트업")
940
-
941
- with gr.Column():
942
- gr.HTML('<div class="section-header">📍 소재지 정보</div>')
943
- hq_sido = gr.Dropdown(label="본사 소재지 (시/도)", choices=SIDO_LIST, value="서울특별시")
944
- hq_sigungu = gr.Textbox(label="본사 소재지 (시/군/구)", placeholder="예: 강남구")
945
- innovation_city = gr.Checkbox(label="혁신도시 입주")
946
- industrial_complex = gr.Checkbox(label="산업단지 입주")
947
- free_zone = gr.Checkbox(label="규제자유특구 소재")
948
- non_capital = gr.Checkbox(label="비수도권 (지방기업)")
949
-
950
- gr.HTML('<div class="section-header">📊 업종 정보</div>')
951
- industry_major = gr.Dropdown(label="주업종 (대분류)", choices=INDUSTRY_MAJOR_OPTIONS, value="C. 제조업")
952
- is_manufacturing = gr.Checkbox(label="제조업 여부")
953
- is_knowledge_service = gr.Checkbox(label="지식서비스업 여부")
954
-
955
- # 서브탭 2: 대표자/인력 정보
956
- with gr.Tab("2️⃣ 대표자/인력 정보"):
957
- with gr.Row():
958
- with gr.Column():
959
- gr.HTML('<div class="section-header">👤 대표자 정보</div>')
960
- ceo_name = gr.Textbox(label="대표자명", placeholder="홍길동")
961
- ceo_gender = gr.Radio(label="대표자 성별", choices=["남성", "여성"], value="남성")
962
- ceo_birthdate = gr.Textbox(label="대표자 생년월일", placeholder="YYYY-MM-DD")
963
- youth_ceo = gr.Checkbox(label="청년창업자 (만39세 미만)")
964
- senior_ceo = gr.Checkbox(label="시니어창업자 (만40세 이상)")
965
- women_company = gr.Checkbox(label="���성기업확인서 보유")
966
- disabled_company = gr.Checkbox(label="장애인기업확인서 보유")
967
-
968
- with gr.Column():
969
- gr.HTML('<div class="section-header">👥 고용 현황</div>')
970
- insurance_employees = gr.Number(label="4대보험 가입자 수", value=0, minimum=0)
971
- regular_employees = gr.Number(label="상시근로자 수", value=0, minimum=0)
972
- youth_employees = gr.Number(label="청년고용 인원", value=0, minimum=0)
973
- female_ratio = gr.Slider(label="여성고용 비율 (%)", minimum=0, maximum=100, value=0)
974
- new_hire_plan = gr.Number(label="신규채용 계획 (명)", value=0, minimum=0)
975
-
976
- with gr.Column():
977
- gr.HTML('<div class="section-header">🔬 연구인력/역량</div>')
978
- rd_personnel = gr.Number(label="연구인력 수", value=0, minimum=0)
979
- phd_researchers = gr.Number(label="박사급 연구원", value=0, minimum=0)
980
- research_center = gr.Checkbox(label="기업부설연구소 등록")
981
- rd_dept = gr.Checkbox(label="연구개발전담부서 등록")
982
- patent_count = gr.Number(label="보유 특허 수", value=0, minimum=0)
983
-
984
- # 서브탭 3: 재무 정보
985
- with gr.Tab("3️⃣ 재무 정보"):
986
- with gr.Row():
987
- with gr.Column():
988
- gr.HTML('<div class="section-header">💰 매출 및 수익</div>')
989
- revenue_current = gr.Number(label="최근년도 매출액 (백만원)", value=0, minimum=0)
990
- revenue_prev = gr.Number(label="전년도 매출액 (백만원)", value=0, minimum=0)
991
- operating_profit = gr.Number(label="영업이익 (백만원)", value=0)
992
- net_income = gr.Number(label="당기순이익 (백만원)", value=0)
993
- export_amount = gr.Number(label="수출액 (천달러)", value=0, minimum=0)
994
-
995
- with gr.Column():
996
- gr.HTML('<div class="section-header">📊 재무건전성</div>')
997
- capital = gr.Number(label="자본금 (백만원)", value=0, minimum=0)
998
- total_assets = gr.Number(label="자산총계 (백만원)", value=0, minimum=0)
999
- debt_ratio = gr.Slider(label="부채비율 (%)", minimum=0, maximum=500, value=0)
1000
- credit_grade = gr.Dropdown(label="신용등급", choices=CREDIT_GRADE_OPTIONS, value="미평가")
1001
- tcb_grade = gr.Dropdown(label="TCB 등급", choices=TCB_GRADE_OPTIONS, value="미평가")
1002
- capital_impairment = gr.Checkbox(label="자본잠식 여부")
1003
-
1004
- with gr.Column():
1005
- gr.HTML('<div class="section-header">🔬 R&D 투자</div>')
1006
- rd_investment = gr.Number(label="연간 R&D 투자액 (백만원)", value=0, minimum=0)
1007
- gov_project_exp = gr.Checkbox(label="정부과제 수행 경험")
1008
- gov_support_3yr = gr.Number(label="최근 3년 정부지원금 (백만원)", value=0, minimum=0)
1009
-
1010
- # 서브탭 4: 기술분야/제한사항
1011
- with gr.Tab("4️⃣ 기술분야/제한사항"):
1012
- with gr.Row():
1013
- with gr.Column():
1014
- gr.HTML('<div class="section-header">🔬 기술 분야</div>')
1015
- core_industry = gr.CheckboxGroup(label="10대 핵심산업", choices=CORE_INDUSTRY_OPTIONS)
1016
- strategic_tech = gr.CheckboxGroup(label="12대 국가전략기술", choices=NATIONAL_STRATEGIC_TECH)
1017
- green_tech = gr.Checkbox(label="녹색기술 분야")
1018
- digital_transform = gr.Checkbox(label="디지털전환 분야")
1019
- defense_industry = gr.Checkbox(label="국방/방산 분야")
1020
-
1021
- with gr.Column():
1022
- gr.HTML('<div class="section-header">📜 인증/ISO</div>')
1023
- iso_certs = gr.CheckboxGroup(label="ISO 인증", choices=ISO_CERT_OPTIONS)
1024
- gmp_cert = gr.Checkbox(label="GMP 인증")
1025
-
1026
- with gr.Column():
1027
- gr.HTML('<div class="section-header">⚠️ 결격사유 확인</div>')
1028
- tax_delinquent = gr.Checkbox(label="국세 체납")
1029
- local_tax_delinquent = gr.Checkbox(label="지방세 체납")
1030
- gov_project_fail = gr.Checkbox(label="정부과제 불성실")
1031
- bankruptcy = gr.Checkbox(label="휴/폐업 이력")
1032
- financial_default = gr.Checkbox(label="금융기관 연체")
1033
-
1034
- # 서브탭 5: 문서 업로드 및 매칭
1035
- with gr.Tab("5️⃣ 문서 업로드 & 매칭"):
1036
- gr.HTML("""
1037
- <div style="background: var(--neu-bg, #e0e5ec); border-radius: 15px; padding: 18px 22px; margin-bottom: 16px; box-shadow: inset 4px 4px 8px #a3b1c6, inset -4px -4px 8px #ffffff;">
1038
- <h3 style="margin: 0 0 8px 0; color: #3d4856;">📁 문서 업로드</h3>
1039
- <p style="margin: 0; color: #5a6677;">사업자등록증, 등기부등본, 재무제표, 중소기업확인서 등을 업로드하면 AI가 자동으로 정보를 추출합니다.</p>
1040
- </div>
1041
- """)
1042
-
1043
- with gr.Row():
1044
- with gr.Column(scale=1):
1045
- file_upload = gr.File(
1046
- label="📎 문서 업로드 (HWP, PDF, TXT, XLSX)",
1047
- file_count="multiple",
1048
- file_types=[".hwp", ".hwpx", ".pdf", ".txt", ".xlsx", ".xls"]
1049
- )
1050
- analyze_docs_btn = gr.Button("📄 문서 분석", variant="secondary", size="lg")
1051
-
1052
- with gr.Column(scale=2):
1053
- doc_analysis_output = gr.Markdown(value="### 📄 문서 분석 결과\n\n*문서를 업로드하고 분석 버튼을 클릭하세요*", height=400)
1054
-
1055
- gr.HTML('<hr style="margin: 24px 0;">')
1056
-
1057
- with gr.Row():
1058
- save_profile_btn = gr.Button("💾 프로필 저장", variant="secondary", size="lg")
1059
- match_btn = gr.Button("🎯 맞춤 과제 매칭 시작", variant="primary", size="lg", elem_classes=["match-btn"])
1060
-
1061
- profile_status = gr.Textbox(label="프로필 저장 상태", interactive=False)
1062
- match_output = gr.Markdown(value="### 🎯 매칭 결과\n\n*프로필을 저장하고 매칭 버튼을 클릭하세요*", height=500)
1063
-
1064
- # ============================================================
1065
- # 이벤트 핸들러
1066
- # ============================================================
1067
- def search_fn(keyword, category, region, org_type, sort_by, status_filter, page, rows):
1068
- display_df, status, full_df = fetch_announcements(keyword or "", category or "전체", region or "전체(지역)",
1069
- org_type or "전체", sort_by or "등록일순",
1070
- status_filter or "진행중", int(page) if page else 1, int(rows) if rows else 20)
1071
- return display_df, status, full_df
1072
-
1073
- def prev_fn(page, keyword, category, region, org_type, sort_by, status_filter, rows):
1074
- new_page = max(1, int(page) - 1) if page else 1
1075
- display_df, status, full_df = fetch_announcements(keyword or "", category or "전체", region or "전체(지역)",
1076
- org_type or "전체", sort_by or "등록일순",
1077
- status_filter or "진행중", new_page, int(rows) if rows else 20)
1078
- return display_df, status, full_df, new_page
1079
-
1080
- def next_fn(page, keyword, category, region, org_type, sort_by, status_filter, rows):
1081
- new_page = int(page) + 1 if page else 2
1082
- display_df, status, full_df = fetch_announcements(keyword or "", category or "전체", region or "전체(지역)",
1083
- org_type or "전체", sort_by or "등록일순",
1084
- status_filter or "진행중", new_page, int(rows) if rows else 20)
1085
- return display_df, status, full_df, new_page
1086
-
1087
- def on_row_select(evt: gr.SelectData, df):
1088
- if evt.index[0] < len(df):
1089
- row = df.iloc[evt.index[0]]
1090
- url = row.get("상세링크", "")
1091
- name = row.get("지원사업명", "")
1092
- attachments = row.get("첨부파일", [])
1093
- print_file = row.get("본문출력파일", None) # ⭐ AI 분석용 핵심 파일
1094
- description = row.get("사업개요", "")
1095
-
1096
- # URL 정규화
1097
- full_url = url
1098
- if url and url.startswith('/'):
1099
- full_url = f"https://www.bizinfo.go.kr{url}"
1100
-
1101
- # 정보 표시 (AI 분석 탭용)
1102
- info_parts = [f"📌 {name}", f"🔗 {full_url}"]
1103
-
1104
- # 본문출력파일 (AI 분석 대상)
1105
- if print_file:
1106
- info_parts.append(f"\n\n📄 **본문출력파일 (AI 분석 대상)**:")
1107
- info_parts.append(f" - {print_file.get('filename', '파일')}")
1108
-
1109
- # 일반 첨부파일 (서식, 양식)
1110
- if attachments and len(attachments) > 0:
1111
- info_parts.append(f"\n\n📎 기타 첨부파일 {len(attachments)}개:")
1112
- for att in attachments:
1113
- info_parts.append(f" - {att.get('filename', '파일')}")
1114
-
1115
- info = "\n".join(info_parts)
1116
-
1117
- # 링크 HTML (첫 번째 탭용)
1118
- if full_url:
1119
- link_html = f'''
1120
- <div style="background: var(--neu-bg, #e0e5ec); padding: 16px 20px; border-radius: 15px;
1121
- box-shadow: inset 4px 4px 8px #a3b1c6, inset -4px -4px 8px #ffffff;">
1122
- <div style="font-weight: 700; color: #3d4856; margin-bottom: 8px;">📌 {name}</div>
1123
- <a href="{full_url}" target="_blank"
1124
- style="display: inline-block; background: linear-gradient(145deg, #4a7dbd, #3a6aa8);
1125
- color: white !important; padding: 10px 20px; border-radius: 25px;
1126
- text-decoration: none; font-weight: 600; margin-top: 8px;
1127
- box-shadow: 4px 4px 8px #a3b1c6, -4px -4px 8px #ffffff;">
1128
- 🔗 기업마당에서 상세보기
1129
- </a>
1130
- </div>
1131
- '''
1132
- else:
1133
- link_html = "<div style='padding: 10px; color: #5a6677;'>⚠️ 링크 정보가 없습니다</div>"
1134
-
1135
- return url, name, print_file, description, info, link_html
1136
- return "", "", None, "", "", "<div style='padding: 10px; color: #5a6677;'>📌 공고를 선택하세요</div>"
1137
-
1138
- def save_profile_fn(biz_num, corp_num, comp_name, est_date, comp_type, corp_tp, comp_size,
1139
- venture, innobiz, mainbiz, sme, small_biz, social, startup,
1140
- sido, sigungu, innov_city, ind_complex, free_z, non_cap,
1141
- ind_major, is_manu, is_know,
1142
- ceo_nm, ceo_gen, ceo_birth, youth, senior, women, disabled,
1143
- ins_emp, reg_emp, youth_emp, fem_ratio, new_hire,
1144
- rd_per, phd, res_ctr, rd_dep, patent,
1145
- rev_cur, rev_prev, op_profit, net_inc, export,
1146
- cap, assets, debt, credit, tcb, impair,
1147
- rd_inv, gov_exp, gov_sup,
1148
- core_ind, strat_tech, green, digital, defense,
1149
- iso, gmp,
1150
- tax_del, local_tax, gov_fail, bankrupt, fin_def):
1151
-
1152
- profile = {
1153
- "사업자정보": {
1154
- "사업자등록번호": biz_num, "법인등록번호": corp_num, "상호": comp_name,
1155
- "설립일자": est_date, "기업형태": comp_type, "법인종류": corp_tp, "기업규모": comp_size
1156
- },
1157
- "인증현황": {
1158
- "벤처기업": venture, "이노비즈": innobiz, "메인비즈": mainbiz,
1159
- "중소기업확인서": sme, "소상공인확인서": small_biz, "소셜벤처": social, "스타트업": startup
1160
- },
1161
- "소재지": {
1162
- "시도": sido, "시군구": sigungu, "혁신도시": innov_city,
1163
- "산업단지": ind_complex, "규제자유특구": free_z, "비수도권": non_cap
1164
- },
1165
- "업종": {"대분류": ind_major, "제조업": is_manu, "지식서비스업": is_know},
1166
- "대표자": {
1167
- "이름": ceo_nm, "성별": ceo_gen, "생년월일": ceo_birth,
1168
- "청년창업자": youth, "시니어창업자": senior, "여성기업": women, "장애인기업": disabled
1169
- },
1170
- "고용현황": {
1171
- "4대보험가입자": ins_emp, "상시근로자": reg_emp, "청년고용": youth_emp,
1172
- "여성비율": fem_ratio, "신규채용계획": new_hire
1173
- },
1174
- "연구역량": {
1175
- "연구인력": rd_per, "박사급": phd, "부설연구소": res_ctr,
1176
- "전담부서": rd_dep, "특허수": patent
1177
- },
1178
- "재무정보": {
1179
- "매출액_당해": rev_cur, "매출액_전년": rev_prev, "영업이익": op_profit,
1180
- "당기순이익": net_inc, "수출액": export, "자본금": cap, "자산총계": assets,
1181
- "부채비율": debt, "신용등급": credit, "TCB등급": tcb, "자본잠식": impair
1182
- },
1183
- "R&D투자": {"연간투자액": rd_inv, "정부과제경험": gov_exp, "최근3년지원금": gov_sup},
1184
- "기술분야": {
1185
- "핵심산업": core_ind, "국가전략기술": strat_tech,
1186
- "녹색기술": green, "디지털전환": digital, "국방방산": defense
1187
- },
1188
- "인증": {"ISO": iso, "GMP": gmp},
1189
- "결격사유": {
1190
- "국세체납": tax_del, "지방세체납": local_tax, "불성실이력": gov_fail,
1191
- "휴폐업": bankrupt, "금융연체": fin_def
1192
- }
1193
- }
1194
- return profile, "✅ 프로필이 저장되었습니다."
1195
-
1196
- def sync_and_update():
1197
- result = do_manual_sync()
1198
- info = get_cache_info()
1199
- return info, result
1200
-
1201
- # 이벤트 연결
1202
- search_btn.click(fn=search_fn, inputs=[keyword_input, category_dropdown, region_dropdown, org_type_dropdown,
1203
- sort_dropdown, status_dropdown, page_input, rows_dropdown],
1204
- outputs=[results_output, status_output, current_df])
1205
-
1206
- keyword_input.submit(fn=search_fn, inputs=[keyword_input, category_dropdown, region_dropdown, org_type_dropdown,
1207
- sort_dropdown, status_dropdown, page_input, rows_dropdown],
1208
- outputs=[results_output, status_output, current_df])
1209
-
1210
- # 수동 동기화 버튼
1211
- sync_btn.click(fn=sync_and_update, outputs=[cache_status, status_output])
1212
-
1213
- # 페이지네이션 이벤트
1214
- prev_btn.click(fn=prev_fn, inputs=[page_input, keyword_input, category_dropdown, region_dropdown,
1215
- org_type_dropdown, sort_dropdown, status_dropdown, rows_dropdown],
1216
- outputs=[results_output, status_output, current_df, page_input])
1217
-
1218
- next_btn.click(fn=next_fn, inputs=[page_input, keyword_input, category_dropdown, region_dropdown,
1219
- org_type_dropdown, sort_dropdown, status_dropdown, rows_dropdown],
1220
- outputs=[results_output, status_output, current_df, page_input])
1221
-
1222
- # 행 선택 시 link_output도 업데이트
1223
- results_output.select(fn=on_row_select, inputs=[current_df],
1224
- outputs=[selected_url, selected_name, selected_print_file, selected_description, selected_info, link_output])
1225
-
1226
- # 바로가기 버튼 클릭 시 - JavaScript로 새 탭 열기
1227
- def open_selected_link(url):
1228
- if url:
1229
- full_url = url if url.startswith('http') else f"https://www.bizinfo.go.kr{url}"
1230
- return f'''
1231
- <script>window.open("{full_url}", "_blank");</script>
1232
- <div style="padding: 10px; color: #48bb78;">✅ 새 탭에서 열렸습니다</div>
1233
- '''
1234
- return "<div style='padding: 10px; color: #ed8936;'>⚠️ 먼저 공고를 선택하세요</div>"
1235
-
1236
- open_link_btn.click(fn=open_selected_link, inputs=[selected_url], outputs=[link_output])
1237
-
1238
- analyze_btn.click(fn=analyze_announcement, inputs=[selected_url, selected_name, selected_print_file, selected_description],
1239
- outputs=[analysis_output])
1240
-
1241
- analyze_docs_btn.click(fn=analyze_uploaded_documents, inputs=[file_upload], outputs=[doc_analysis_output])
1242
-
1243
- save_profile_btn.click(
1244
- fn=save_profile_fn,
1245
- inputs=[biz_number, corp_number, company_name, establish_date, company_type, corp_type, company_size,
1246
- venture_cert, innobiz_cert, mainbiz_cert, sme_cert, small_biz_cert, social_venture, startup_flag,
1247
- hq_sido, hq_sigungu, innovation_city, industrial_complex, free_zone, non_capital,
1248
- industry_major, is_manufacturing, is_knowledge_service,
1249
- ceo_name, ceo_gender, ceo_birthdate, youth_ceo, senior_ceo, women_company, disabled_company,
1250
- insurance_employees, regular_employees, youth_employees, female_ratio, new_hire_plan,
1251
- rd_personnel, phd_researchers, research_center, rd_dept, patent_count,
1252
- revenue_current, revenue_prev, operating_profit, net_income, export_amount,
1253
- capital, total_assets, debt_ratio, credit_grade, tcb_grade, capital_impairment,
1254
- rd_investment, gov_project_exp, gov_support_3yr,
1255
- core_industry, strategic_tech, green_tech, digital_transform, defense_industry,
1256
- iso_certs, gmp_cert,
1257
- tax_delinquent, local_tax_delinquent, gov_project_fail, bankruptcy, financial_default],
1258
- outputs=[company_profile, profile_status]
1259
- )
1260
-
1261
- match_btn.click(fn=match_announcements_with_profile, inputs=[company_profile, current_df], outputs=[match_output])
1262
-
1263
- return demo
1264
-
1265
-
1266
- # ============================================================
1267
- # 앱 시작
1268
- # ============================================================
1269
- if __name__ == "__main__":
1270
- # 캐시 시스템 초기화 (백그라운드 스케줄러 시작)
1271
- if CACHE_AVAILABLE:
1272
- print("🚀 캐시 시스템 초기화 중...")
1273
- status = initialize_cache_system()
1274
- print(f"✅ 캐시 상태: {status}")
1275
-
1276
- demo = create_interface()
1277
- demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)