Update app.py
Browse files
app.py
CHANGED
@@ -12,13 +12,6 @@ import glob
|
|
12 |
from datetime import datetime
|
13 |
from gradio_client import Client
|
14 |
|
15 |
-
# 환경변수 로드를 위한 dotenv 사용
|
16 |
-
try:
|
17 |
-
from dotenv import load_dotenv
|
18 |
-
load_dotenv()
|
19 |
-
except ImportError:
|
20 |
-
pass
|
21 |
-
|
22 |
# 로깅 설정
|
23 |
logging.basicConfig(
|
24 |
level=logging.INFO,
|
@@ -31,29 +24,9 @@ logging.basicConfig(
|
|
31 |
|
32 |
logger = logging.getLogger(__name__)
|
33 |
|
34 |
-
# API 클라이언트 설정
|
35 |
-
def
|
36 |
-
|
37 |
-
endpoint = os.getenv('API_ENDPOINT', '').strip()
|
38 |
-
|
39 |
-
# 디버깅을 위한 환경변수 체크 (실제 값은 로그에 남기지 않음)
|
40 |
-
if not endpoint:
|
41 |
-
logger.error("API_ENDPOINT 환경변수가 설정되지 않았습니다.")
|
42 |
-
logger.info("사용 가능한 환경변수들:")
|
43 |
-
for key in os.environ.keys():
|
44 |
-
if 'API' in key.upper():
|
45 |
-
logger.info(f" {key}: {'설정됨' if os.environ[key] else '비어있음'}")
|
46 |
-
raise ValueError("API_ENDPOINT 환경변수가 설정되지 않았습니다.")
|
47 |
-
|
48 |
-
if endpoint.startswith('#') or len(endpoint) == 0:
|
49 |
-
logger.error("API_ENDPOINT 환경변수가 올바르지 않은 형식입니다.")
|
50 |
-
raise ValueError("API_ENDPOINT 환경변수가 올바르게 설정되지 않았습니다.")
|
51 |
-
|
52 |
-
# 엔드포인트 형식 검증
|
53 |
-
if '/' not in endpoint:
|
54 |
-
logger.error("API_ENDPOINT는 'username/repo-name' 형식이어야 합니다.")
|
55 |
-
raise ValueError("API_ENDPOINT 형식이 올바르지 않습니다.")
|
56 |
-
|
57 |
return Client(endpoint)
|
58 |
|
59 |
# 세션별 임시 파일 관리를 위한 딕셔너리
|
@@ -63,7 +36,6 @@ session_data = {}
|
|
63 |
def cleanup_huggingface_temp_folders():
|
64 |
"""허깅페이스 임시 폴더 초기 정리"""
|
65 |
try:
|
66 |
-
# 일반적인 임시 디렉토리들
|
67 |
temp_dirs = [
|
68 |
tempfile.gettempdir(),
|
69 |
"/tmp",
|
@@ -79,7 +51,6 @@ def cleanup_huggingface_temp_folders():
|
|
79 |
for temp_dir in temp_dirs:
|
80 |
if os.path.exists(temp_dir):
|
81 |
try:
|
82 |
-
# 기존 세션 파일들 정리
|
83 |
session_files = glob.glob(os.path.join(temp_dir, "session_*.xlsx"))
|
84 |
session_files.extend(glob.glob(os.path.join(temp_dir, "session_*.csv")))
|
85 |
session_files.extend(glob.glob(os.path.join(temp_dir, "*keyword*.xlsx")))
|
@@ -89,7 +60,6 @@ def cleanup_huggingface_temp_folders():
|
|
89 |
|
90 |
for file_path in session_files:
|
91 |
try:
|
92 |
-
# 파일이 1시간 이상 오래된 경우만 삭제
|
93 |
if os.path.getmtime(file_path) < time.time() - 3600:
|
94 |
os.remove(file_path)
|
95 |
cleanup_count += 1
|
@@ -102,7 +72,6 @@ def cleanup_huggingface_temp_folders():
|
|
102 |
|
103 |
logger.info(f"✅ 허깅페이스 임시 폴더 초기 정리 완료 - {cleanup_count}개 파일 삭제")
|
104 |
|
105 |
-
# Gradio 캐시 폴더도 정리
|
106 |
try:
|
107 |
gradio_temp_dir = os.path.join(os.getcwd(), "gradio_cached_examples")
|
108 |
if os.path.exists(gradio_temp_dir):
|
@@ -117,17 +86,14 @@ def cleanup_huggingface_temp_folders():
|
|
117 |
def setup_clean_temp_environment():
|
118 |
"""깨끗한 임시 환경 설정"""
|
119 |
try:
|
120 |
-
# 1. 기존 임시 파일들 정리
|
121 |
cleanup_huggingface_temp_folders()
|
122 |
|
123 |
-
|
124 |
-
app_temp_dir = os.path.join(tempfile.gettempdir(), "control_tower_app")
|
125 |
if os.path.exists(app_temp_dir):
|
126 |
shutil.rmtree(app_temp_dir, ignore_errors=True)
|
127 |
os.makedirs(app_temp_dir, exist_ok=True)
|
128 |
|
129 |
-
|
130 |
-
os.environ['CONTROL_TOWER_TEMP'] = app_temp_dir
|
131 |
|
132 |
logger.info(f"✅ 애플리케이션 전용 임시 디렉토리 설정: {app_temp_dir}")
|
133 |
|
@@ -139,7 +105,7 @@ def setup_clean_temp_environment():
|
|
139 |
|
140 |
def get_app_temp_dir():
|
141 |
"""애플리케이션 전용 임시 디렉토리 반환"""
|
142 |
-
return os.environ.get('
|
143 |
|
144 |
def get_session_id():
|
145 |
"""세션 ID 생성"""
|
@@ -175,11 +141,10 @@ def cleanup_old_sessions():
|
|
175 |
sessions_to_remove = []
|
176 |
|
177 |
for session_id, data in session_data.items():
|
178 |
-
if current_time - data.get('last_activity', 0) > 3600:
|
179 |
sessions_to_remove.append(session_id)
|
180 |
|
181 |
for session_id in sessions_to_remove:
|
182 |
-
# 파일 정리
|
183 |
if session_id in session_temp_files:
|
184 |
for file_path in session_temp_files[session_id]:
|
185 |
try:
|
@@ -190,7 +155,6 @@ def cleanup_old_sessions():
|
|
190 |
logger.error(f"오래된 세션 파일 삭제 오류: {e}")
|
191 |
del session_temp_files[session_id]
|
192 |
|
193 |
-
# 세션 데이터 정리
|
194 |
if session_id in session_data:
|
195 |
del session_data[session_id]
|
196 |
logger.info(f"오래된 세션 데이터 삭제: {session_id[:8]}...")
|
@@ -206,12 +170,10 @@ def create_session_temp_file(session_id, suffix='.xlsx'):
|
|
206 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
207 |
random_suffix = str(random.randint(1000, 9999))
|
208 |
|
209 |
-
# 애플리케이션 전용 임시 디렉토리 사용
|
210 |
temp_dir = get_app_temp_dir()
|
211 |
filename = f"session_{session_id[:8]}_{timestamp}_{random_suffix}{suffix}"
|
212 |
temp_file_path = os.path.join(temp_dir, filename)
|
213 |
|
214 |
-
# 빈 파일 생성
|
215 |
with open(temp_file_path, 'w') as f:
|
216 |
pass
|
217 |
|
@@ -219,11 +181,11 @@ def create_session_temp_file(session_id, suffix='.xlsx'):
|
|
219 |
return temp_file_path
|
220 |
|
221 |
def wrapper_modified(keyword, korean_only, apply_main_keyword_option, exclude_zero_volume, session_id):
|
222 |
-
"""키워드 검색 및 처리 래퍼 함수 (API
|
223 |
update_session_activity(session_id)
|
224 |
|
225 |
try:
|
226 |
-
client =
|
227 |
result = client.predict(
|
228 |
keyword=keyword,
|
229 |
korean_only=korean_only,
|
@@ -232,10 +194,8 @@ def wrapper_modified(keyword, korean_only, apply_main_keyword_option, exclude_ze
|
|
232 |
api_name="/process_search_results"
|
233 |
)
|
234 |
|
235 |
-
# API 결과 처리
|
236 |
table_html, cat_choices, vol_choices, selected_cat, download_file = result
|
237 |
|
238 |
-
# 로컬 파일로 다운로드 파일 복사
|
239 |
local_file = None
|
240 |
if download_file:
|
241 |
local_file = create_session_temp_file(session_id, '.xlsx')
|
@@ -245,7 +205,7 @@ def wrapper_modified(keyword, korean_only, apply_main_keyword_option, exclude_ze
|
|
245 |
gr.update(value=table_html),
|
246 |
gr.update(choices=cat_choices),
|
247 |
gr.update(choices=vol_choices),
|
248 |
-
None,
|
249 |
gr.update(choices=cat_choices, value=selected_cat),
|
250 |
local_file,
|
251 |
gr.update(visible=True),
|
@@ -256,7 +216,7 @@ def wrapper_modified(keyword, korean_only, apply_main_keyword_option, exclude_ze
|
|
256 |
except Exception as e:
|
257 |
logger.error(f"API 호출 오류: {e}")
|
258 |
return (
|
259 |
-
gr.update(value="<p>검색
|
260 |
gr.update(choices=["전체 보기"]),
|
261 |
gr.update(choices=["전체"]),
|
262 |
None,
|
@@ -268,11 +228,11 @@ def wrapper_modified(keyword, korean_only, apply_main_keyword_option, exclude_ze
|
|
268 |
)
|
269 |
|
270 |
def analyze_with_auto_download(analysis_keywords, selected_category, state_df, session_id):
|
271 |
-
"""카테고리 일치 분석 실행 및 자동 다운로드 (API
|
272 |
update_session_activity(session_id)
|
273 |
|
274 |
try:
|
275 |
-
client =
|
276 |
result = client.predict(
|
277 |
analysis_keywords=analysis_keywords,
|
278 |
selected_category=selected_category,
|
@@ -281,7 +241,6 @@ def analyze_with_auto_download(analysis_keywords, selected_category, state_df, s
|
|
281 |
|
282 |
analysis_result, download_file = result
|
283 |
|
284 |
-
# 로컬 파일로 다운로드 파일 복사
|
285 |
local_file = None
|
286 |
if download_file:
|
287 |
local_file = create_session_temp_file(session_id, '.xlsx')
|
@@ -294,11 +253,11 @@ def analyze_with_auto_download(analysis_keywords, selected_category, state_df, s
|
|
294 |
return "분석 중 오류가 발생했습니다. 다시 시도해주세요.", None, gr.update(visible=False)
|
295 |
|
296 |
def filter_and_sort_table(df, selected_cat, keyword_sort, total_volume_sort, usage_count_sort, selected_volume_range, exclude_zero_volume, session_id):
|
297 |
-
"""테이블 필터링 및 정렬 함수 (API
|
298 |
update_session_activity(session_id)
|
299 |
|
300 |
try:
|
301 |
-
client =
|
302 |
result = client.predict(
|
303 |
selected_cat=selected_cat,
|
304 |
keyword_sort=keyword_sort,
|
@@ -320,7 +279,7 @@ def update_category_selection(selected_cat, session_id):
|
|
320 |
update_session_activity(session_id)
|
321 |
|
322 |
try:
|
323 |
-
client =
|
324 |
result = client.predict(
|
325 |
selected_cat=selected_cat,
|
326 |
api_name="/update_category_selection"
|
@@ -336,7 +295,6 @@ def reset_interface(session_id):
|
|
336 |
"""인터페이스 리셋 함수 - 세션별 데이터 초기화"""
|
337 |
update_session_activity(session_id)
|
338 |
|
339 |
-
# 세션별 임시 파일 정리
|
340 |
if session_id in session_temp_files:
|
341 |
for file_path in session_temp_files[session_id]:
|
342 |
try:
|
@@ -348,60 +306,22 @@ def reset_interface(session_id):
|
|
348 |
session_temp_files[session_id] = []
|
349 |
|
350 |
try:
|
351 |
-
client =
|
352 |
-
result = client.predict(
|
353 |
-
api_name="/reset_interface"
|
354 |
-
)
|
355 |
-
|
356 |
-
# API 결과를 그대로 반환
|
357 |
return result
|
358 |
|
359 |
except Exception as e:
|
360 |
logger.error(f"리셋 API 호출 오류: {e}")
|
361 |
return (
|
362 |
-
"",
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
"", # HTML 테이블
|
367 |
-
["전체 보기"], # 카테고리 필터
|
368 |
-
"전체 보기", # 카테고리 필터 선택
|
369 |
-
["전체"], # 검색량 구간 필터
|
370 |
-
"전체", # 검색량 구간 선택
|
371 |
-
"정렬 없음", # 총검색량 정렬
|
372 |
-
"정렬 없음", # 키워드 사용횟수 정렬
|
373 |
-
["전체 보기"], # 분석할 카테고리
|
374 |
-
"전체 보기", # 분석할 카테고리 선택
|
375 |
-
"", # 키워드 입력
|
376 |
-
"", # 분석 결과
|
377 |
-
None # 다운로드 파일
|
378 |
)
|
379 |
|
380 |
-
# 래퍼 함수들
|
381 |
def search_with_loading(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id):
|
382 |
update_session_activity(session_id)
|
383 |
-
|
384 |
-
try:
|
385 |
-
client = get_api_client()
|
386 |
-
result = client.predict(
|
387 |
-
keyword=keyword,
|
388 |
-
korean_only=korean_only,
|
389 |
-
apply_main_keyword=apply_main_keyword,
|
390 |
-
exclude_zero_volume=exclude_zero_volume,
|
391 |
-
api_name="/search_with_loading"
|
392 |
-
)
|
393 |
-
|
394 |
-
return (
|
395 |
-
gr.update(visible=True),
|
396 |
-
gr.update(visible=False)
|
397 |
-
)
|
398 |
-
|
399 |
-
except Exception as e:
|
400 |
-
logger.error(f"검색 로딩 API 호출 오류: {e}")
|
401 |
-
return (
|
402 |
-
gr.update(visible=False),
|
403 |
-
gr.update(visible=True)
|
404 |
-
)
|
405 |
|
406 |
def process_search_results(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id):
|
407 |
update_session_activity(session_id)
|
@@ -431,49 +351,30 @@ def process_search_results(keyword, korean_only, apply_main_keyword, exclude_zer
|
|
431 |
|
432 |
def analyze_with_loading(analysis_keywords, selected_category, state_df, session_id):
|
433 |
update_session_activity(session_id)
|
434 |
-
|
435 |
-
try:
|
436 |
-
client = get_api_client()
|
437 |
-
result = client.predict(
|
438 |
-
analysis_keywords=analysis_keywords,
|
439 |
-
selected_category=selected_category,
|
440 |
-
api_name="/analyze_with_loading"
|
441 |
-
)
|
442 |
-
|
443 |
-
return gr.update(visible=True)
|
444 |
-
|
445 |
-
except Exception as e:
|
446 |
-
logger.error(f"분석 로딩 API 호출 오류: {e}")
|
447 |
-
return gr.update(visible=False)
|
448 |
|
449 |
def process_analyze_results(analysis_keywords, selected_category, state_df, session_id):
|
450 |
update_session_activity(session_id)
|
451 |
results = analyze_with_auto_download(analysis_keywords, selected_category, state_df, session_id)
|
452 |
return results + (gr.update(visible=False),)
|
453 |
|
454 |
-
# 세션 정리 스케줄러
|
455 |
def start_session_cleanup_scheduler():
|
456 |
"""세션 정리 스케줄러 시작"""
|
457 |
def cleanup_scheduler():
|
458 |
while True:
|
459 |
-
time.sleep(600)
|
460 |
cleanup_old_sessions()
|
461 |
-
# 추가로 허깅페이스 임시 폴더도 주기적 정리
|
462 |
cleanup_huggingface_temp_folders()
|
463 |
|
464 |
threading.Thread(target=cleanup_scheduler, daemon=True).start()
|
465 |
|
466 |
def cleanup_on_startup():
|
467 |
"""애플리케이션 시작 시 전체 정리"""
|
468 |
-
logger.info("🧹
|
469 |
|
470 |
-
# 1. 허깅페이스 임시 폴더 정리
|
471 |
cleanup_huggingface_temp_folders()
|
472 |
-
|
473 |
-
# 2. 깨끗한 임시 환경 설정
|
474 |
app_temp_dir = setup_clean_temp_environment()
|
475 |
|
476 |
-
# 3. 전역 변수 초기화
|
477 |
global session_temp_files, session_data
|
478 |
session_temp_files.clear()
|
479 |
session_data.clear()
|
@@ -482,7 +383,6 @@ def cleanup_on_startup():
|
|
482 |
|
483 |
return app_temp_dir
|
484 |
|
485 |
-
# Gradio 인터페이스 생성
|
486 |
def create_app():
|
487 |
fontawesome_html = """
|
488 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
@@ -490,7 +390,6 @@ def create_app():
|
|
490 |
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap">
|
491 |
"""
|
492 |
|
493 |
-
# CSS 파일 로드
|
494 |
try:
|
495 |
with open('style.css', 'r', encoding='utf-8') as f:
|
496 |
custom_css = f.read()
|
@@ -504,40 +403,24 @@ def create_app():
|
|
504 |
)) as demo:
|
505 |
gr.HTML(fontawesome_html)
|
506 |
|
507 |
-
# 세션 ID 상태 (각 사용자별로 고유)
|
508 |
session_id = gr.State(get_session_id)
|
509 |
-
|
510 |
-
# 키워드 상태 관리
|
511 |
keyword_state = gr.State("")
|
512 |
|
513 |
-
# 입력 섹션
|
514 |
with gr.Column(elem_classes="custom-frame fade-in"):
|
515 |
gr.HTML('<div class="section-title"><i class="fas fa-search"></i> 검색 입력</div>')
|
516 |
|
517 |
with gr.Row():
|
518 |
with gr.Column(scale=1):
|
519 |
-
keyword = gr.Textbox(
|
520 |
-
label="메인 키워드",
|
521 |
-
placeholder="예: 오징어"
|
522 |
-
)
|
523 |
with gr.Column(scale=1):
|
524 |
-
search_btn = gr.Button(
|
525 |
-
"메인키워드 분석",
|
526 |
-
elem_classes="custom-button"
|
527 |
-
)
|
528 |
|
529 |
with gr.Accordion("옵션 설정", open=False):
|
530 |
with gr.Row():
|
531 |
with gr.Column(scale=1):
|
532 |
-
korean_only = gr.Checkbox(
|
533 |
-
label="한글만 추출",
|
534 |
-
value=True
|
535 |
-
)
|
536 |
with gr.Column(scale=1):
|
537 |
-
exclude_zero_volume = gr.Checkbox(
|
538 |
-
label="검색량 0 키워드 제외",
|
539 |
-
value=False
|
540 |
-
)
|
541 |
|
542 |
with gr.Row():
|
543 |
with gr.Column(scale=1):
|
@@ -549,7 +432,6 @@ def create_app():
|
|
549 |
with gr.Column(scale=1):
|
550 |
gr.HTML("")
|
551 |
|
552 |
-
# 진행 상태 표시 섹션
|
553 |
with gr.Column(elem_classes="custom-frame fade-in", visible=False) as progress_section:
|
554 |
gr.HTML('<div class="section-title"><i class="fas fa-spinner"></i> 분석 진행 상태</div>')
|
555 |
progress_html = gr.HTML("""
|
@@ -564,7 +446,6 @@ def create_app():
|
|
564 |
</div>
|
565 |
""")
|
566 |
|
567 |
-
# 메인키워드 분석 결과 섹션
|
568 |
with gr.Column(elem_classes="custom-frame fade-in") as main_keyword_section:
|
569 |
gr.HTML('<div class="section-title"><i class="fas fa-table"></i> 메인키워드 분석 결과</div>')
|
570 |
|
@@ -629,7 +510,6 @@ def create_app():
|
|
629 |
gr.HTML("<div class='data-container' id='table_container'></div>")
|
630 |
table_output = gr.HTML(elem_classes="fade-in")
|
631 |
|
632 |
-
# 카테고리 분석 섹션
|
633 |
with gr.Column(elem_classes="custom-frame fade-in", visible=False) as category_analysis_section:
|
634 |
gr.HTML('<div class="section-title"><i class="fas fa-chart-bar"></i> 키워드 분석</div>')
|
635 |
|
@@ -649,7 +529,6 @@ def create_app():
|
|
649 |
interactive=True
|
650 |
)
|
651 |
|
652 |
-
# 실행 섹션
|
653 |
with gr.Column(elem_classes="execution-section", visible=False) as execution_section:
|
654 |
gr.HTML('<div class="section-title"><i class="fas fa-play-circle"></i> 실행</div>')
|
655 |
with gr.Row():
|
@@ -664,22 +543,16 @@ def create_app():
|
|
664 |
elem_classes=["execution-button", "secondary-button"]
|
665 |
)
|
666 |
|
667 |
-
# 분석 결과 출력 섹션
|
668 |
with gr.Column(elem_classes="custom-frame fade-in", visible=False) as analysis_output_section:
|
669 |
gr.HTML('<div class="section-title"><i class="fas fa-list-ul"></i> 분석 결과 요약</div>')
|
670 |
|
671 |
analysis_result = gr.HTML(elem_classes="fade-in")
|
672 |
|
673 |
with gr.Row():
|
674 |
-
download_output = gr.File(
|
675 |
-
label="키워드 목록 다운로드",
|
676 |
-
visible=True
|
677 |
-
)
|
678 |
|
679 |
-
# 상태 저장용 변수
|
680 |
state_df = gr.State()
|
681 |
|
682 |
-
# 이벤트 연결 - 모든 함수에 session_id 추가
|
683 |
search_btn.click(
|
684 |
fn=search_with_loading,
|
685 |
inputs=[keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id],
|
@@ -696,7 +569,6 @@ def create_app():
|
|
696 |
]
|
697 |
)
|
698 |
|
699 |
-
# 필터 및 정렬 변경 이벤트 연결 - session_id 추가
|
700 |
category_filter.change(
|
701 |
fn=filter_and_sort_table,
|
702 |
inputs=[
|
@@ -753,7 +625,6 @@ def create_app():
|
|
753 |
outputs=[table_output]
|
754 |
)
|
755 |
|
756 |
-
# 카테고리 분석 버튼 이벤트 - session_id 추가
|
757 |
analyze_btn.click(
|
758 |
fn=analyze_with_loading,
|
759 |
inputs=[analysis_keywords, selected_category, state_df, session_id],
|
@@ -764,7 +635,6 @@ def create_app():
|
|
764 |
outputs=[analysis_result, download_output, analysis_output_section, progress_section]
|
765 |
)
|
766 |
|
767 |
-
# 리셋 버튼 이벤트 연결 - session_id 추가
|
768 |
reset_btn.click(
|
769 |
fn=reset_interface,
|
770 |
inputs=[session_id],
|
@@ -773,54 +643,42 @@ def create_app():
|
|
773 |
table_output, category_filter, category_filter,
|
774 |
search_volume_filter, search_volume_filter,
|
775 |
total_volume_sort, usage_count_sort,
|
776 |
-
selected_category, selected_category,
|
777 |
-
analysis_keywords, analysis_result, download_output
|
|
|
|
|
778 |
]
|
779 |
)
|
780 |
|
781 |
return demo
|
782 |
|
783 |
if __name__ == "__main__":
|
784 |
-
|
785 |
-
logger.info("🚀 컨트롤 타워 애플리케이션 시작...")
|
786 |
|
787 |
-
# 1. 첫 번째: 허깅페이스 임시 폴더 정리 및 환경 설정
|
788 |
app_temp_dir = cleanup_on_startup()
|
789 |
-
|
790 |
-
# 2. 세션 정리 스케줄러 시작
|
791 |
start_session_cleanup_scheduler()
|
792 |
|
793 |
-
|
794 |
-
try:
|
795 |
-
client = get_api_client()
|
796 |
-
logger.info("✅ API 클라이언트 연결 확인 완료")
|
797 |
-
except Exception as e:
|
798 |
-
logger.error(f"❌ API 클라이언트 연결 실패: {e}")
|
799 |
-
raise
|
800 |
-
|
801 |
-
logger.info("===== 컨트롤 타워 Application Startup at %s =====", time.strftime("%Y-%m-%d %H:%M:%S"))
|
802 |
logger.info(f"📁 임시 파일 저장 위치: {app_temp_dir}")
|
803 |
|
804 |
-
# ========== 앱 실행 ==========
|
805 |
try:
|
806 |
app = create_app()
|
807 |
app.launch(
|
808 |
-
share=False,
|
809 |
-
server_name="0.0.0.0",
|
810 |
-
server_port=7860,
|
811 |
-
max_threads=40,
|
812 |
-
auth=None,
|
813 |
-
show_error=True,
|
814 |
-
quiet=False,
|
815 |
-
favicon_path=None,
|
816 |
-
ssl_verify=False
|
817 |
)
|
818 |
except Exception as e:
|
819 |
logger.error(f"애플리케이션 실행 실패: {e}")
|
820 |
raise
|
821 |
finally:
|
822 |
-
|
823 |
-
logger.info("🧹 컨트롤 타워 애플리케이션 종료 - 최종 정리 작업...")
|
824 |
try:
|
825 |
cleanup_huggingface_temp_folders()
|
826 |
if os.path.exists(app_temp_dir):
|
|
|
12 |
from datetime import datetime
|
13 |
from gradio_client import Client
|
14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
# 로깅 설정
|
16 |
logging.basicConfig(
|
17 |
level=logging.INFO,
|
|
|
24 |
|
25 |
logger = logging.getLogger(__name__)
|
26 |
|
27 |
+
# API 클라이언트 설정 (환경변수로 숨김)
|
28 |
+
def get_client():
|
29 |
+
endpoint = os.getenv('API_ENDPOINT')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
return Client(endpoint)
|
31 |
|
32 |
# 세션별 임시 파일 관리를 위한 딕셔너리
|
|
|
36 |
def cleanup_huggingface_temp_folders():
|
37 |
"""허깅페이스 임시 폴더 초기 정리"""
|
38 |
try:
|
|
|
39 |
temp_dirs = [
|
40 |
tempfile.gettempdir(),
|
41 |
"/tmp",
|
|
|
51 |
for temp_dir in temp_dirs:
|
52 |
if os.path.exists(temp_dir):
|
53 |
try:
|
|
|
54 |
session_files = glob.glob(os.path.join(temp_dir, "session_*.xlsx"))
|
55 |
session_files.extend(glob.glob(os.path.join(temp_dir, "session_*.csv")))
|
56 |
session_files.extend(glob.glob(os.path.join(temp_dir, "*keyword*.xlsx")))
|
|
|
60 |
|
61 |
for file_path in session_files:
|
62 |
try:
|
|
|
63 |
if os.path.getmtime(file_path) < time.time() - 3600:
|
64 |
os.remove(file_path)
|
65 |
cleanup_count += 1
|
|
|
72 |
|
73 |
logger.info(f"✅ 허깅페이스 임시 폴더 초기 정리 완료 - {cleanup_count}개 파일 삭제")
|
74 |
|
|
|
75 |
try:
|
76 |
gradio_temp_dir = os.path.join(os.getcwd(), "gradio_cached_examples")
|
77 |
if os.path.exists(gradio_temp_dir):
|
|
|
86 |
def setup_clean_temp_environment():
|
87 |
"""깨끗한 임시 환경 설정"""
|
88 |
try:
|
|
|
89 |
cleanup_huggingface_temp_folders()
|
90 |
|
91 |
+
app_temp_dir = os.path.join(tempfile.gettempdir(), "keyword_app")
|
|
|
92 |
if os.path.exists(app_temp_dir):
|
93 |
shutil.rmtree(app_temp_dir, ignore_errors=True)
|
94 |
os.makedirs(app_temp_dir, exist_ok=True)
|
95 |
|
96 |
+
os.environ['KEYWORD_APP_TEMP'] = app_temp_dir
|
|
|
97 |
|
98 |
logger.info(f"✅ 애플리케이션 전용 임시 디렉토리 설정: {app_temp_dir}")
|
99 |
|
|
|
105 |
|
106 |
def get_app_temp_dir():
|
107 |
"""애플리케이션 전용 임시 디렉토리 반환"""
|
108 |
+
return os.environ.get('KEYWORD_APP_TEMP', tempfile.gettempdir())
|
109 |
|
110 |
def get_session_id():
|
111 |
"""세션 ID 생성"""
|
|
|
141 |
sessions_to_remove = []
|
142 |
|
143 |
for session_id, data in session_data.items():
|
144 |
+
if current_time - data.get('last_activity', 0) > 3600:
|
145 |
sessions_to_remove.append(session_id)
|
146 |
|
147 |
for session_id in sessions_to_remove:
|
|
|
148 |
if session_id in session_temp_files:
|
149 |
for file_path in session_temp_files[session_id]:
|
150 |
try:
|
|
|
155 |
logger.error(f"오래된 세션 파일 삭제 오류: {e}")
|
156 |
del session_temp_files[session_id]
|
157 |
|
|
|
158 |
if session_id in session_data:
|
159 |
del session_data[session_id]
|
160 |
logger.info(f"오래된 세션 데이터 삭제: {session_id[:8]}...")
|
|
|
170 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
171 |
random_suffix = str(random.randint(1000, 9999))
|
172 |
|
|
|
173 |
temp_dir = get_app_temp_dir()
|
174 |
filename = f"session_{session_id[:8]}_{timestamp}_{random_suffix}{suffix}"
|
175 |
temp_file_path = os.path.join(temp_dir, filename)
|
176 |
|
|
|
177 |
with open(temp_file_path, 'w') as f:
|
178 |
pass
|
179 |
|
|
|
181 |
return temp_file_path
|
182 |
|
183 |
def wrapper_modified(keyword, korean_only, apply_main_keyword_option, exclude_zero_volume, session_id):
|
184 |
+
"""키워드 검색 및 처리 래퍼 함수 (API 사용)"""
|
185 |
update_session_activity(session_id)
|
186 |
|
187 |
try:
|
188 |
+
client = get_client()
|
189 |
result = client.predict(
|
190 |
keyword=keyword,
|
191 |
korean_only=korean_only,
|
|
|
194 |
api_name="/process_search_results"
|
195 |
)
|
196 |
|
|
|
197 |
table_html, cat_choices, vol_choices, selected_cat, download_file = result
|
198 |
|
|
|
199 |
local_file = None
|
200 |
if download_file:
|
201 |
local_file = create_session_temp_file(session_id, '.xlsx')
|
|
|
205 |
gr.update(value=table_html),
|
206 |
gr.update(choices=cat_choices),
|
207 |
gr.update(choices=vol_choices),
|
208 |
+
None,
|
209 |
gr.update(choices=cat_choices, value=selected_cat),
|
210 |
local_file,
|
211 |
gr.update(visible=True),
|
|
|
216 |
except Exception as e:
|
217 |
logger.error(f"API 호출 오류: {e}")
|
218 |
return (
|
219 |
+
gr.update(value="<p>검색 결과가 없습니다. 다른 키워드로 시도해보세요.</p>"),
|
220 |
gr.update(choices=["전체 보기"]),
|
221 |
gr.update(choices=["전체"]),
|
222 |
None,
|
|
|
228 |
)
|
229 |
|
230 |
def analyze_with_auto_download(analysis_keywords, selected_category, state_df, session_id):
|
231 |
+
"""카테고리 일치 분석 실행 및 자동 다운로드 (API 사용)"""
|
232 |
update_session_activity(session_id)
|
233 |
|
234 |
try:
|
235 |
+
client = get_client()
|
236 |
result = client.predict(
|
237 |
analysis_keywords=analysis_keywords,
|
238 |
selected_category=selected_category,
|
|
|
241 |
|
242 |
analysis_result, download_file = result
|
243 |
|
|
|
244 |
local_file = None
|
245 |
if download_file:
|
246 |
local_file = create_session_temp_file(session_id, '.xlsx')
|
|
|
253 |
return "분석 중 오류가 발생했습니다. 다시 시도해주세요.", None, gr.update(visible=False)
|
254 |
|
255 |
def filter_and_sort_table(df, selected_cat, keyword_sort, total_volume_sort, usage_count_sort, selected_volume_range, exclude_zero_volume, session_id):
|
256 |
+
"""테이블 필터링 및 정렬 함수 (API 사용)"""
|
257 |
update_session_activity(session_id)
|
258 |
|
259 |
try:
|
260 |
+
client = get_client()
|
261 |
result = client.predict(
|
262 |
selected_cat=selected_cat,
|
263 |
keyword_sort=keyword_sort,
|
|
|
279 |
update_session_activity(session_id)
|
280 |
|
281 |
try:
|
282 |
+
client = get_client()
|
283 |
result = client.predict(
|
284 |
selected_cat=selected_cat,
|
285 |
api_name="/update_category_selection"
|
|
|
295 |
"""인터페이스 리셋 함수 - 세션별 데이터 초기화"""
|
296 |
update_session_activity(session_id)
|
297 |
|
|
|
298 |
if session_id in session_temp_files:
|
299 |
for file_path in session_temp_files[session_id]:
|
300 |
try:
|
|
|
306 |
session_temp_files[session_id] = []
|
307 |
|
308 |
try:
|
309 |
+
client = get_client()
|
310 |
+
result = client.predict(api_name="/reset_interface")
|
|
|
|
|
|
|
|
|
311 |
return result
|
312 |
|
313 |
except Exception as e:
|
314 |
logger.error(f"리셋 API 호출 오류: {e}")
|
315 |
return (
|
316 |
+
"", True, False, "메인키워드 적용", "", ["전체 보기"], "전체 보기",
|
317 |
+
["전체"], "전체", "정렬 없음", "정렬 없음", None, ["전체 보기"],
|
318 |
+
"전체 보기", "", "", None, gr.update(visible=False),
|
319 |
+
gr.update(visible=False), ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
320 |
)
|
321 |
|
|
|
322 |
def search_with_loading(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id):
|
323 |
update_session_activity(session_id)
|
324 |
+
return (gr.update(visible=True), gr.update(visible=False))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
325 |
|
326 |
def process_search_results(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id):
|
327 |
update_session_activity(session_id)
|
|
|
351 |
|
352 |
def analyze_with_loading(analysis_keywords, selected_category, state_df, session_id):
|
353 |
update_session_activity(session_id)
|
354 |
+
return gr.update(visible=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
355 |
|
356 |
def process_analyze_results(analysis_keywords, selected_category, state_df, session_id):
|
357 |
update_session_activity(session_id)
|
358 |
results = analyze_with_auto_download(analysis_keywords, selected_category, state_df, session_id)
|
359 |
return results + (gr.update(visible=False),)
|
360 |
|
|
|
361 |
def start_session_cleanup_scheduler():
|
362 |
"""세션 정리 스케줄러 시작"""
|
363 |
def cleanup_scheduler():
|
364 |
while True:
|
365 |
+
time.sleep(600)
|
366 |
cleanup_old_sessions()
|
|
|
367 |
cleanup_huggingface_temp_folders()
|
368 |
|
369 |
threading.Thread(target=cleanup_scheduler, daemon=True).start()
|
370 |
|
371 |
def cleanup_on_startup():
|
372 |
"""애플리케이션 시작 시 전체 정리"""
|
373 |
+
logger.info("🧹 애플리케이션 시작 - 초기 정리 작업 시작...")
|
374 |
|
|
|
375 |
cleanup_huggingface_temp_folders()
|
|
|
|
|
376 |
app_temp_dir = setup_clean_temp_environment()
|
377 |
|
|
|
378 |
global session_temp_files, session_data
|
379 |
session_temp_files.clear()
|
380 |
session_data.clear()
|
|
|
383 |
|
384 |
return app_temp_dir
|
385 |
|
|
|
386 |
def create_app():
|
387 |
fontawesome_html = """
|
388 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
|
|
390 |
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap">
|
391 |
"""
|
392 |
|
|
|
393 |
try:
|
394 |
with open('style.css', 'r', encoding='utf-8') as f:
|
395 |
custom_css = f.read()
|
|
|
403 |
)) as demo:
|
404 |
gr.HTML(fontawesome_html)
|
405 |
|
|
|
406 |
session_id = gr.State(get_session_id)
|
|
|
|
|
407 |
keyword_state = gr.State("")
|
408 |
|
|
|
409 |
with gr.Column(elem_classes="custom-frame fade-in"):
|
410 |
gr.HTML('<div class="section-title"><i class="fas fa-search"></i> 검색 입력</div>')
|
411 |
|
412 |
with gr.Row():
|
413 |
with gr.Column(scale=1):
|
414 |
+
keyword = gr.Textbox(label="메인 키워드", placeholder="예: 오징어")
|
|
|
|
|
|
|
415 |
with gr.Column(scale=1):
|
416 |
+
search_btn = gr.Button("메인키워드 분석", elem_classes="custom-button")
|
|
|
|
|
|
|
417 |
|
418 |
with gr.Accordion("옵션 설정", open=False):
|
419 |
with gr.Row():
|
420 |
with gr.Column(scale=1):
|
421 |
+
korean_only = gr.Checkbox(label="한글만 추출", value=True)
|
|
|
|
|
|
|
422 |
with gr.Column(scale=1):
|
423 |
+
exclude_zero_volume = gr.Checkbox(label="검색량 0 키워드 제외", value=False)
|
|
|
|
|
|
|
424 |
|
425 |
with gr.Row():
|
426 |
with gr.Column(scale=1):
|
|
|
432 |
with gr.Column(scale=1):
|
433 |
gr.HTML("")
|
434 |
|
|
|
435 |
with gr.Column(elem_classes="custom-frame fade-in", visible=False) as progress_section:
|
436 |
gr.HTML('<div class="section-title"><i class="fas fa-spinner"></i> 분석 진행 상태</div>')
|
437 |
progress_html = gr.HTML("""
|
|
|
446 |
</div>
|
447 |
""")
|
448 |
|
|
|
449 |
with gr.Column(elem_classes="custom-frame fade-in") as main_keyword_section:
|
450 |
gr.HTML('<div class="section-title"><i class="fas fa-table"></i> 메인키워드 분석 결과</div>')
|
451 |
|
|
|
510 |
gr.HTML("<div class='data-container' id='table_container'></div>")
|
511 |
table_output = gr.HTML(elem_classes="fade-in")
|
512 |
|
|
|
513 |
with gr.Column(elem_classes="custom-frame fade-in", visible=False) as category_analysis_section:
|
514 |
gr.HTML('<div class="section-title"><i class="fas fa-chart-bar"></i> 키워드 분석</div>')
|
515 |
|
|
|
529 |
interactive=True
|
530 |
)
|
531 |
|
|
|
532 |
with gr.Column(elem_classes="execution-section", visible=False) as execution_section:
|
533 |
gr.HTML('<div class="section-title"><i class="fas fa-play-circle"></i> 실행</div>')
|
534 |
with gr.Row():
|
|
|
543 |
elem_classes=["execution-button", "secondary-button"]
|
544 |
)
|
545 |
|
|
|
546 |
with gr.Column(elem_classes="custom-frame fade-in", visible=False) as analysis_output_section:
|
547 |
gr.HTML('<div class="section-title"><i class="fas fa-list-ul"></i> 분석 결과 요약</div>')
|
548 |
|
549 |
analysis_result = gr.HTML(elem_classes="fade-in")
|
550 |
|
551 |
with gr.Row():
|
552 |
+
download_output = gr.File(label="키워드 목록 다운로드", visible=True)
|
|
|
|
|
|
|
553 |
|
|
|
554 |
state_df = gr.State()
|
555 |
|
|
|
556 |
search_btn.click(
|
557 |
fn=search_with_loading,
|
558 |
inputs=[keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id],
|
|
|
569 |
]
|
570 |
)
|
571 |
|
|
|
572 |
category_filter.change(
|
573 |
fn=filter_and_sort_table,
|
574 |
inputs=[
|
|
|
625 |
outputs=[table_output]
|
626 |
)
|
627 |
|
|
|
628 |
analyze_btn.click(
|
629 |
fn=analyze_with_loading,
|
630 |
inputs=[analysis_keywords, selected_category, state_df, session_id],
|
|
|
635 |
outputs=[analysis_result, download_output, analysis_output_section, progress_section]
|
636 |
)
|
637 |
|
|
|
638 |
reset_btn.click(
|
639 |
fn=reset_interface,
|
640 |
inputs=[session_id],
|
|
|
643 |
table_output, category_filter, category_filter,
|
644 |
search_volume_filter, search_volume_filter,
|
645 |
total_volume_sort, usage_count_sort,
|
646 |
+
state_df, selected_category, selected_category,
|
647 |
+
analysis_keywords, analysis_result, download_output,
|
648 |
+
keyword_analysis_section, analysis_output_section,
|
649 |
+
keyword_state
|
650 |
]
|
651 |
)
|
652 |
|
653 |
return demo
|
654 |
|
655 |
if __name__ == "__main__":
|
656 |
+
logger.info("🚀 메인키워드 분석 애플리케이션 시작...")
|
|
|
657 |
|
|
|
658 |
app_temp_dir = cleanup_on_startup()
|
|
|
|
|
659 |
start_session_cleanup_scheduler()
|
660 |
|
661 |
+
logger.info("===== 멀티유저 메인키워드 분석 Application Startup at %s =====", time.strftime("%Y-%m-%d %H:%M:%S"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
662 |
logger.info(f"📁 임시 파일 저장 위치: {app_temp_dir}")
|
663 |
|
|
|
664 |
try:
|
665 |
app = create_app()
|
666 |
app.launch(
|
667 |
+
share=False,
|
668 |
+
server_name="0.0.0.0",
|
669 |
+
server_port=7860,
|
670 |
+
max_threads=40,
|
671 |
+
auth=None,
|
672 |
+
show_error=True,
|
673 |
+
quiet=False,
|
674 |
+
favicon_path=None,
|
675 |
+
ssl_verify=False
|
676 |
)
|
677 |
except Exception as e:
|
678 |
logger.error(f"애플리케이션 실행 실패: {e}")
|
679 |
raise
|
680 |
finally:
|
681 |
+
logger.info("🧹 애플리케이션 종료 - 최종 정리 작업...")
|
|
|
682 |
try:
|
683 |
cleanup_huggingface_temp_folders()
|
684 |
if os.path.exists(app_temp_dir):
|