227 lines
8.3 KiB
Python
227 lines
8.3 KiB
Python
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())
|