auto: 일일 백업 2026-09-11 02:00

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hyowons
2026-09-11 02:00:03 +09:00
parent 92d0d96368
commit b4f510d1ad
188 changed files with 4782 additions and 3353 deletions
@@ -25,6 +25,7 @@ import imsg_cli
import notify
import whooing_balance
import gahee_reminder
import securities_match
# 후잉 웹훅은 HTTP 200을 반환하면서 본문에 "fail"/"Error : ..." 를 줄 수 있어 본문 검증 필수.
SUCCESS_BODY = "done"
@@ -753,6 +754,32 @@ def detect_pairs(candidates, accounts):
return pairs, used
def try_securities_transfer(parsed, cand, accounts):
"""자본 분개로 갈 뻔한 은행 SMS 가 실은 증권 이체인지 확인.
은행 SMS 에는 입금자명(본인 이름)만 찍혀 목적지를 알 수 없지만, 반대편은
키움 kt00015 에 남는다. 짝을 찾으면 자본이 아니라 자산↔자산 이체로 분개한다.
반환: {payload, key, label} | None (짝 없음·애매·조회 실패)
"""
if not parsed or parsed.get("kind") not in ("withdrawal", "deposit"):
return None
bank_asset = accounts.get("carrier_to_account", {}).get(cand["info"].get("carrier"))
if not bank_asset:
return None # 은행 자산 계정을 모르면 이체를 구성할 수 없다
entry_date = parsed.get("entry_date")
amount = parsed.get("amount")
if not entry_date or not amount:
return None
match = securities_match.find_match(
entry_date, amount, parsed["kind"], cand.get("created_at"))
if match is None:
return None
payload = securities_match.build_payload(
match, bank_asset, entry_date, amount, parsed["kind"], parsed.get("balance"))
return {"payload": payload, "key": match["key"], "label": match["label"]}
def post_to_whooing(webhook_url, payload, dry_run=False):
"""payload는 dict. 반환: (ok: bool, status: int, body: str).
HTTP 2xx + body가 'done' 으로 시작해야 성공. 그 외(HTTP 200 + 'fail' 포함) 모두 실패."""
@@ -879,6 +906,7 @@ def main():
sent_transfer = 0
sent_structured = 0
sent_raw = 0
sent_securities = 0
sent_cancel = 0
failed = 0
new_failures = []
@@ -994,24 +1022,45 @@ def main():
# 처제 대납금 상환이었는데 입금자명이 "방효원"이라 자체이체 룰에 걸렸다).
# ⚠️ 커서는 latest_skip 으로 진행시킨다 — 재시도해도 같은 룰이 같은 결과를 내므로
# 여기서 커서를 막으면 이 메시지 뒤의 모든 거래가 영구히 멈춘다.
sec_key = None
if mode == "structured" and (
structured.get("left") in capital_accounts
or structured.get("right") in capital_accounts
):
capital_blocked.append({
"label": c["info"]["label"],
"created_at": c["created_at"],
"payload": payload,
"blocked_at": datetime.now(KST).isoformat(),
})
if c["created_at"] > (latest_skip or ""):
latest_skip = c["created_at"]
print(f" ⛔ [capital-block] {c['info']['label']} {c['created_at']} | {display} — 미반영, 알림만")
continue
# 자본으로 갈 뻔한 건이 실은 증권 이체일 수 있다. 은행 SMS 는 입금자명만
# 주지만 반대편이 키움 kt00015 에 남으므로, 짝을 찾으면 자본이 아니라
# 자산↔자산 이체로 분개한다. 짝이 없으면 기존대로 미반영+알림.
sec = try_securities_transfer(parsed, c, accounts)
if sec:
payload = {k: str(v) for k, v in sec["payload"].items()}
mode = "securities"
sec_key = sec["key"]
display = (f"{sec['payload']['left']} ← {sec['payload']['right']} "
f"{sec['payload']['money']:,}원 (증권 {sec['label']} 이체)")
else:
capital_blocked.append({
"label": c["info"]["label"],
"created_at": c["created_at"],
"payload": payload,
"blocked_at": datetime.now(KST).isoformat(),
})
if c["created_at"] > (latest_skip or ""):
latest_skip = c["created_at"]
print(f" ⛔ [capital-block] {c['info']['label']} {c['created_at']} | {display} — 미반영, 알림만")
continue
ok, status, body = post_to_whooing(webhook_url, payload, dry_run=args.dry_run)
if ok:
if mode == "structured":
if mode == "securities":
sent_securities += 1
# POST 성공 후에만 기록한다 — 실패하면 다음에 다시 짝으로 잡혀야 한다.
if not args.dry_run:
securities_match.mark_matched(sec_key, {
"sms_at": c["created_at"],
"money": int(payload["money"]),
"entry": display,
})
elif mode == "structured":
sent_structured += 1
carrier_key = c["info"].get("carrier")
if carrier_key:
@@ -1353,9 +1402,9 @@ def main():
save_json(SYNC_STATE_FILE, sync_state)
if blocked_at:
print(f"⚠️ 후잉 동기화: transfer {sent_transfer}건, structured {sent_structured}건, raw {sent_raw}건, 취소감액 {sent_cancel}건, 실패 {failed}건 — {blocked_at} 에서 중단(다음 cron 재시도)")
print(f"⚠️ 후잉 동기화: transfer {sent_transfer}건, structured {sent_structured}건, 증권이체 {sent_securities}건, raw {sent_raw}건, 취소감액 {sent_cancel}건, 실패 {failed}건 — {blocked_at} 에서 중단(다음 cron 재시도)")
else:
print(f"✅ 후잉 동기화: transfer {sent_transfer}건, structured {sent_structured}건, raw {sent_raw}건, 취소감액 {sent_cancel}건, 실패 {failed}건 (last={final_latest or latest})")
print(f"✅ 후잉 동기화: transfer {sent_transfer}건, structured {sent_structured}건, 증권이체 {sent_securities}건, raw {sent_raw}건, 취소감액 {sent_cancel}건, 실패 {failed}건 (last={final_latest or latest})")
# 가희 잔액 리마인더 & 답신 자동 분개 (격리 — 실패해도 결제 sync 결과는 위에서 이미 출력됨)
run_gahee_reminder()