from __future__ import annotations import base64 import hashlib import hmac import json import os import sys import time import urllib.parse import urllib.request import uuid from datetime import datetime from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) import scripts.refresh_hanmac_wehago_ledgers as refresh import scripts.wehago_data_download_2022_work as wehago import scripts.wehago_ledger_api_download as api_download SOURCE_RUNS = { 2022: "20260615_194953", 2023: "20260615_195024", 2024: "20260615_195215", 2025: "20260615_195405", } DIRECT_GISU_BY_YEAR = dict(refresh.GISU_BY_YEAR) LEDGER_API_URL = "https://api.wehago.com/smarta/sabk0107/jungi_slip/" LEDGER_API_PATH = "/smarta/sabk0107/jungi_slip/" def wsl_unc(path: str) -> Path: if os.name == "nt" and path.startswith("/home/"): return Path(r"\\wsl.localhost\Ubuntu" + path.replace("/", "\\")) return Path(path) def report_entries(report: dict[str, Any]) -> list[dict[str, Any]]: reports = report.get("reports") if isinstance(reports, list): return reports if isinstance(reports, dict): return list(reports.values()) return [report] def failed_codes_from_report(report_path: Path) -> list[str]: report = json.loads(report_path.read_text(encoding="utf-8")) entries = report_entries(report) if not entries: return [] validation = entries[0].get("validation", {}) provenance = validation.get("api_provenance") or validation.get("api_progress") or {} return [ str(item["account_code"]) for item in provenance.get("download_failures", []) if item.get("account_code") ] def cookie_map(driver: Any) -> dict[str, str]: cookies = {item["name"]: item.get("value", "") for item in driver.get_cookies()} if "AUTH_A_TOKEN" not in cookies or "wehago_s" not in cookies: script_cookies = driver.execute_script("return document.cookie || '';") or "" for part in str(script_cookies).split(";"): if "=" not in part: continue key, value = part.strip().split("=", 1) cookies.setdefault(key, value) missing = [key for key in ("AUTH_A_TOKEN", "wehago_s") if not cookies.get(key)] if missing: raise RuntimeError(f"WEHAGO 인증 쿠키를 찾지 못했습니다: {missing}") return cookies def wehago_sign(wehago_s: str, path_search: str, timestamp: str, transaction_id: str) -> str: secret = base64.b64encode(hashlib.sha256((wehago_s + timestamp).encode("utf-8")).digest()).decode("ascii") digest = hmac.new( secret.encode("utf-8"), (path_search + timestamp + transaction_id).encode("utf-8"), hashlib.sha256, ).digest() return base64.b64encode(digest).decode("ascii") def ledger_payload(year: int, gisu: int, account: wehago.Account, wehago_s: str, timestamp: str) -> dict[str, str]: code = f"{account.code}00" return { "start_slip_date": f"{year}0101", "end_slip_date": f"{year}1231", "from_search_date": f"{year}0101", "to_search_date": f"{year}1231", "from_cd_acctit": code, "to_cd_acctit": code, "cd_details": "", "gb_code": "0", "gb_semok": "0", "gisu": str(gisu), "gubn_mon": "", "gubn_total": "", "gubn_balance": "0", "balance_color": "0", "sort": "1", "mn_gubun": "0", "mn_start": "0", "mn_end": "9999999999999999", "count_check": "1", "timestamp": timestamp, "cno": "1173867", "ccode": "biz202103030006368", "user_id": "b21344", "ym_insa": "2026", "wehago_s": wehago_s, "oldview": "0" if year == 2025 else "1", "locale": "ko", } def fetch_ledger_rows(year: int, account: wehago.Account, cookies: dict[str, str]) -> list[dict[str, Any]]: timestamp = str(int(time.time())) transaction_id = uuid.uuid4().hex[:10] payload = ledger_payload(year, DIRECT_GISU_BY_YEAR[year], account, cookies["wehago_s"], timestamp) body = urllib.parse.urlencode(payload).encode("utf-8") headers = { "Authorization": f"Bearer {cookies['AUTH_A_TOKEN']}", "Content-Type": "application/x-www-form-urlencoded", "timestamp": timestamp, "transaction-id": transaction_id, "wehago-sign": wehago_sign(cookies["wehago_s"], LEDGER_API_PATH, timestamp, transaction_id), "client-id": "smarta", "service": "smarta", "cno": "1173867", "Origin": "https://smarta.wehago.com", "Referer": "https://smarta.wehago.com/", } request = urllib.request.Request(LEDGER_API_URL, data=body, headers=headers, method="POST") with urllib.request.urlopen(request, timeout=30) as response: rows = json.loads(response.read().decode("utf-8")) if not isinstance(rows, list): raise RuntimeError(f"{account.code}: API 응답이 목록이 아닙니다: {type(rows).__name__}") return rows def main() -> int: base = wsl_unc("/home/b17301/WEHAGO_DB/data_download/hanmac_refresh") canonical_base = wsl_unc("/home/b17301/WEHAGO_DB/data_download/hanmac") run_root = base / f"retry_failed_direct_{datetime.now():%Y%m%d_%H%M%S}" run_root.mkdir(parents=True, exist_ok=True) wehago.CHROME_DEBUGGER_ADDRESS = "127.0.0.1:9225" wehago.DOWNLOAD_DIR = run_root driver = wehago.build_driver(run_root) try: cookies = cookie_map(driver) finally: driver.quit() summary: dict[str, Any] = {} for year, source_run in SOURCE_RUNS.items(): failed_codes = failed_codes_from_report(base / source_run / "refresh_report.json") canonical_dir = canonical_base / str(year) by_code = {account.code: account for account in wehago.discover_downloaded_accounts(canonical_dir)} accounts = [by_code[code] for code in failed_codes if code in by_code] staging_dir = run_root / "staging" / str(year) staging_dir.mkdir(parents=True, exist_ok=True) manifest: list[dict[str, Any]] = [] failures: list[dict[str, str]] = [] print(f"YEAR {year}: direct API retry accounts={len(accounts)} staging={staging_dir}", flush=True) for index, account in enumerate(accounts, start=1): try: rows = fetch_ledger_rows(year, account, cookies) api_download.validate_api_rows(account, rows) target = staging_dir / account.safe_filename api_download.write_api_rows(target, account, rows) manifest.append( { "account_code": account.code, "account_name": account.name, "api_request_code": f"{account.code}00", "api_response_rows": len(rows), } ) print(f"[{year} {index}/{len(accounts)}] OK {account.code} rows={len(rows)}", flush=True) except Exception as exc: reason = f"{type(exc).__name__}: {exc}" failures.append({"account_code": account.code, "account_name": account.name, "reason": reason}) print(f"[{year} {index}/{len(accounts)}] FAIL {account.code}: {reason}", flush=True) api_download.write_progress( staging_dir, { "status": "completed_with_failures" if failures else "completed", "completed": len(manifest), "total": len(accounts), "accounts": manifest, "failures": failures, }, ) validation = refresh.validate_staging(staging_dir) summary[str(year)] = { "requested": len(accounts), "downloaded": len(manifest), "failures": len(failures), "failed_codes": [item["account_code"] for item in failures], "staging": str(staging_dir), "validation": validation, } (run_root / "retry_summary.json").write_text( json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8", ) print(f"YEAR {year}: downloaded={len(manifest)} failures={len(failures)}", flush=True) print(f"SUMMARY_PATH {run_root / 'retry_summary.json'}", flush=True) return 0 if __name__ == "__main__": raise SystemExit(main())