feat: Flask 애플리케이션 모듈화 및 웹 대시보드 구현
- Flask Blueprint 아키텍처로 전환 (dashboard, upload, backup, status) - app.py 681줄 95줄로 축소 (86% 감소) - HTML 템플릿 모듈화 (base.html + 기능별 templates) - CSS/JS 파일 분리 (common + 기능별 파일) - 대시보드 기능 추가 (통계, 주간 예보, 방문객 추이) - 파일 업로드 웹 인터페이스 구현 - 백업/복구 관리 UI 구현 - Docker 배포 환경 개선 - .gitignore 업데이트 (uploads, backups, cache 등)
This commit is contained in:
90
lib/ga4.py
90
lib/ga4.py
@ -1,7 +1,7 @@
|
||||
# ga4.py
|
||||
# ga4.py
|
||||
'''
|
||||
퍼스트가든 구글 애널리틱스 API를 활용해 관련 데이터를 DB에 저장함
|
||||
병렬 처리를 통해 처리 속도 향상
|
||||
병렬 처리를 통해 처리 속도 향상 (내부 병렬은 유지하되 에러/재시도 보강)
|
||||
'''
|
||||
|
||||
import sys, os
|
||||
@ -9,6 +9,7 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
import yaml
|
||||
import pprint
|
||||
import traceback
|
||||
from datetime import datetime, timedelta
|
||||
from dateutil.parser import parse
|
||||
from google.analytics.data import BetaAnalyticsDataClient
|
||||
@ -38,25 +39,33 @@ def load_config():
|
||||
# GA4 클라이언트 초기화
|
||||
# ------------------------
|
||||
def init_ga_client(service_account_file):
|
||||
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = service_account_file
|
||||
print(f"[INFO] GA4 클라이언트 초기화 - 인증파일: {service_account_file}")
|
||||
return BetaAnalyticsDataClient()
|
||||
try:
|
||||
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = service_account_file
|
||||
print(f"[INFO] GA4 클라이언트 초기화 - 인증파일: {service_account_file}")
|
||||
return BetaAnalyticsDataClient()
|
||||
except Exception as e:
|
||||
print(f"[ERROR] GA4 클라이언트 초기화 실패: {e}")
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
# ------------------------
|
||||
# config.yaml에 최대 rows 저장
|
||||
# ------------------------
|
||||
def update_config_file_with_max_rows(max_rows):
|
||||
with open(CONFIG_PATH, encoding="utf-8") as f:
|
||||
config = yaml.safe_load(f)
|
||||
try:
|
||||
with open(CONFIG_PATH, encoding="utf-8") as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
if "ga4" not in config:
|
||||
config["ga4"] = {}
|
||||
config["ga4"]["max_rows_per_request"] = max_rows
|
||||
if "ga4" not in config:
|
||||
config["ga4"] = {}
|
||||
config["ga4"]["max_rows_per_request"] = int(max_rows)
|
||||
|
||||
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
||||
yaml.dump(config, f, allow_unicode=True)
|
||||
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
||||
yaml.dump(config, f, allow_unicode=True)
|
||||
|
||||
print(f"[INFO] config.yaml에 max_rows_per_request = {max_rows} 저장 완료")
|
||||
print(f"[INFO] config.yaml에 max_rows_per_request = {max_rows} 저장 완료")
|
||||
except Exception as e:
|
||||
print(f"[WARN] config.yaml 업데이트 실패: {e}")
|
||||
|
||||
# ------------------------
|
||||
# GA4 API로 최대 rows 감지
|
||||
@ -71,10 +80,13 @@ def detect_max_rows_supported(client, property_id):
|
||||
limit=100000
|
||||
)
|
||||
response = client.run_report(request)
|
||||
print(f"[INFO] 최대 rows 감지: {len(response.rows)} rows 수신됨.")
|
||||
return len(response.rows)
|
||||
nrows = len(response.rows)
|
||||
print(f"[INFO] 최대 rows 감지: {nrows} rows 수신됨.")
|
||||
return nrows
|
||||
except Exception as e:
|
||||
print(f"[WARNING] 최대 rows 감지 실패: {e}")
|
||||
traceback.print_exc()
|
||||
# 안전한 기본값 반환
|
||||
return 10000
|
||||
|
||||
# ------------------------
|
||||
@ -82,21 +94,31 @@ def detect_max_rows_supported(client, property_id):
|
||||
# ------------------------
|
||||
def fetch_report(client, property_id, start_date, end_date, dimensions, metrics, limit=10000):
|
||||
print(f"[INFO] fetch_report 호출 - 기간: {start_date} ~ {end_date}, dims={dimensions}, metrics={metrics}")
|
||||
request = RunReportRequest(
|
||||
property=f"properties/{property_id}",
|
||||
dimensions=[Dimension(name=d) for d in dimensions],
|
||||
metrics=[Metric(name=m) for m in metrics],
|
||||
date_ranges=[DateRange(start_date=start_date, end_date=end_date)],
|
||||
limit=limit,
|
||||
)
|
||||
response = client.run_report(request)
|
||||
print(f"[INFO] GA4 리포트 응답 받음: {len(response.rows)} rows")
|
||||
return response
|
||||
try:
|
||||
request = RunReportRequest(
|
||||
property=f"properties/{property_id}",
|
||||
dimensions=[Dimension(name=d) for d in dimensions],
|
||||
metrics=[Metric(name=m) for m in metrics],
|
||||
date_ranges=[DateRange(start_date=start_date, end_date=end_date)],
|
||||
limit=limit,
|
||||
)
|
||||
response = client.run_report(request)
|
||||
print(f"[INFO] GA4 리포트 응답 받음: {len(response.rows)} rows")
|
||||
return response
|
||||
except Exception as e:
|
||||
print(f"[ERROR] GA4 fetch_report 실패: {e}")
|
||||
traceback.print_exc()
|
||||
# 빈 응답 형태 반환하는 대신 None 반환해서 호출부가 처리하도록 함
|
||||
return None
|
||||
|
||||
# ------------------------
|
||||
# 응답 데이터를 DB에 저장
|
||||
# ------------------------
|
||||
def save_report_to_db(engine, table, response, dimension_names, metric_names, debug=False):
|
||||
if response is None:
|
||||
print("[INFO] 저장할 응답 없음 (None)")
|
||||
return
|
||||
|
||||
with engine.begin() as conn:
|
||||
for row in response.rows:
|
||||
dims = row.dimension_values
|
||||
@ -137,6 +159,7 @@ def save_report_to_db(engine, table, response, dimension_names, metric_names, de
|
||||
print(f"[DB ERROR] 중복 오류 또는 기타: {e}")
|
||||
except Exception as e:
|
||||
print(f"[DB ERROR] 저장 실패: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
# ------------------------
|
||||
# 테이블에서 마지막 날짜 조회
|
||||
@ -175,7 +198,6 @@ def determine_date_range(table, config_start, config_end, force_update, engine):
|
||||
else:
|
||||
actual_start = config_start
|
||||
|
||||
# 시작일이 종료일보다 뒤에 있으면 자동 교체
|
||||
if actual_start > actual_end:
|
||||
print(f"[WARN] 시작일({actual_start})이 종료일({actual_end})보다 뒤에 있습니다. 날짜를 교환하여 수집을 계속합니다.")
|
||||
actual_start, actual_end = actual_end, actual_start
|
||||
@ -201,10 +223,10 @@ def process_dimension_metric(engine, client, property_id, config, table, dims, m
|
||||
end_str = end_dt.strftime('%Y-%m-%d')
|
||||
print(f"[INFO] GA4 데이터 조회: {start_str} ~ {end_str}")
|
||||
response = fetch_report(client, property_id, start_str, end_str, dimensions=dims, metrics=mets, limit=max_rows)
|
||||
if len(response.rows) > 0:
|
||||
if response and len(response.rows) > 0:
|
||||
save_report_to_db(engine, table, response, dimension_names=dims, metric_names=mets, debug=debug)
|
||||
else:
|
||||
print(f"[INFO] 해당 기간 {start_str} ~ {end_str} 데이터 없음")
|
||||
print(f"[INFO] 해당 기간 {start_str} ~ {end_str} 데이터 없음 또는 요청 실패")
|
||||
|
||||
# ------------------------
|
||||
# 메인 진입점 (병렬 처리 포함)
|
||||
@ -224,12 +246,19 @@ def main():
|
||||
return
|
||||
|
||||
engine = db.engine
|
||||
client = init_ga_client(service_account_file)
|
||||
try:
|
||||
client = init_ga_client(service_account_file)
|
||||
except Exception:
|
||||
print("[ERROR] GA4 클라이언트 초기화 실패로 종료합니다.")
|
||||
return
|
||||
|
||||
max_rows = ga4_cfg.get("max_rows_per_request")
|
||||
if not max_rows:
|
||||
max_rows = detect_max_rows_supported(client, property_id)
|
||||
update_config_file_with_max_rows(max_rows)
|
||||
try:
|
||||
update_config_file_with_max_rows(max_rows)
|
||||
except Exception:
|
||||
pass
|
||||
print(f"[INFO] 설정된 max_rows_per_request = {max_rows}")
|
||||
|
||||
tasks = [
|
||||
@ -253,6 +282,7 @@ def main():
|
||||
print(f"[INFO] 태스크 {i} 완료")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 태스크 {i} 실패: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
print("[INFO] GA4 데이터 수집 및 저장 완료")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user