Initial commit: OpenClaw 워크스페이스 버전관리 시작
설정·스크립트·스킬·문서·큐레이션 메모리 추적. 시크릿(credentials/identity)·런타임 상태(state/logs/sessions/sqlite)· 백업(clobbered/bak)·dream 캐시는 .gitignore로 제외. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
"""carrier별 결제/입출금 SMS 파서.
|
||||
|
||||
Python 3.9 호환을 위해 union type은 문자열로 표기.
|
||||
|
||||
각 파서는 (raw_text, created_at_iso) -> "dict | None".
|
||||
반환 dict 필드:
|
||||
- kind: "withdrawal" | "deposit" | "card_approval" | "card_cancel"
|
||||
- entry_date: "YYYYMMDD"
|
||||
- amount: int (원, 양수)
|
||||
- merchant: str (가맹점/메모/송수신처)
|
||||
- raw: 원문 그대로
|
||||
파싱 실패 시 None을 반환해 raw 폴백 처리하도록 한다.
|
||||
"""
|
||||
import re
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
KST = ZoneInfo("Asia/Seoul")
|
||||
|
||||
|
||||
def _iso_to_kst_date(iso: str) -> str:
|
||||
"""imsg created_at(UTC ISO) -> KST YYYYMMDD."""
|
||||
try:
|
||||
dt = datetime.fromisoformat(iso.replace("Z", "+00:00")).astimezone(KST)
|
||||
return dt.strftime("%Y%m%d")
|
||||
except Exception:
|
||||
return datetime.now(KST).strftime("%Y%m%d")
|
||||
|
||||
|
||||
def _normalize(text: str) -> str:
|
||||
"""전각 공백·전각 영문 → 일반화."""
|
||||
return text.replace("\u3000", " ").replace("\xa0", " ").strip()
|
||||
|
||||
|
||||
HANA_RE = re.compile(
|
||||
r"\[Web발신\]\s*\n"
|
||||
r"하나,(?P<md>\d{2}/\d{2})\s+(?P<hm>\d{2}:\d{2})\s*\n"
|
||||
r"(?P<acct>\S+)\s*\n"
|
||||
r"(?P<kind>입금|출금)(?P<amount>[\d,]+)원\s*\n?"
|
||||
r"(?P<merchant>[^\n]*)"
|
||||
r"(?:\s*\n잔액(?P<balance>[\d,]+)원)?",
|
||||
)
|
||||
|
||||
|
||||
def parse_hana_bank(raw: str, created_at: str) -> "dict | None":
|
||||
text = raw.strip()
|
||||
m = HANA_RE.match(text)
|
||||
if not m:
|
||||
return None
|
||||
kind = "deposit" if m.group("kind") == "입금" else "withdrawal"
|
||||
amount = int(m.group("amount").replace(",", ""))
|
||||
merchant = _normalize(m.group("merchant")) or "(메모없음)"
|
||||
result = {
|
||||
"kind": kind,
|
||||
"entry_date": _iso_to_kst_date(created_at),
|
||||
"amount": amount,
|
||||
"merchant": merchant,
|
||||
"raw": raw,
|
||||
}
|
||||
if m.group("balance"):
|
||||
result["balance"] = int(m.group("balance").replace(",", ""))
|
||||
return result
|
||||
|
||||
|
||||
# 신한카드 신형식: "신한카드(6847)승인 방*원님 아파트 관리비 249,710원 정상승인 되었습니다."
|
||||
# entry_date 는 SMS created_at 기반(_iso_to_kst_date)이라 메시지 내 날짜 추출 불필요.
|
||||
SHINHAN_APPROVE_RE = re.compile(
|
||||
r"신한(?:카드)?(?:\(\d+\))?\s*(?P<kind>승인|매입취소|승인취소)\s+"
|
||||
r"(?P<who>\S+?님)\s+"
|
||||
r"(?P<merchant>.+?)\s+"
|
||||
r"(?P<amount>[\d,]+)\s*원",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
SHINHAN_BANK_RE = re.compile(
|
||||
r"\[Web발신\]\s*\n"
|
||||
r"신한\s*(?P<md>\d{2}/\d{2})\s+(?P<hm>\d{2}:\d{2})\s*\n"
|
||||
r"(?P<acct>\S+)\s*\n"
|
||||
r"(?P<kind>입금|출금)\s+(?P<amount>[\d,]+)\s*\n"
|
||||
r"잔액\s+(?P<balance>[\d,]+)\s*\n?"
|
||||
r"(?P<merchant>.*)",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def parse_shinhan_bank(raw: str, created_at: str) -> "dict | None":
|
||||
text = raw.strip()
|
||||
m = SHINHAN_BANK_RE.match(text)
|
||||
if not m:
|
||||
return None
|
||||
kind = "deposit" if m.group("kind") == "입금" else "withdrawal"
|
||||
amount = int(m.group("amount").replace(",", ""))
|
||||
merchant = _normalize(m.group("merchant")).split("\n")[0].strip() or "(메모없음)"
|
||||
result = {
|
||||
"kind": kind,
|
||||
"entry_date": _iso_to_kst_date(created_at),
|
||||
"amount": amount,
|
||||
"merchant": merchant,
|
||||
"raw": raw,
|
||||
}
|
||||
if m.group("balance"):
|
||||
result["balance"] = int(m.group("balance").replace(",", ""))
|
||||
return result
|
||||
|
||||
|
||||
def parse_shinhan_card(raw: str, created_at: str) -> "dict | None":
|
||||
text = _normalize(raw).replace("[Web발신]", "")
|
||||
m = SHINHAN_APPROVE_RE.search(text)
|
||||
if not m:
|
||||
return None
|
||||
kind_raw = m.group("kind")
|
||||
kind = "card_cancel" if "취소" in kind_raw else "card_approval"
|
||||
amount = int(m.group("amount").replace(",", ""))
|
||||
merchant = m.group("merchant").strip().split("\n")[0]
|
||||
merchant = re.sub(r"\s+", " ", merchant).strip() or "(가맹점없음)"
|
||||
return {
|
||||
"kind": kind,
|
||||
"entry_date": _iso_to_kst_date(created_at),
|
||||
"amount": amount,
|
||||
"merchant": merchant,
|
||||
"raw": raw,
|
||||
}
|
||||
|
||||
|
||||
KAKAOBANK_RE = re.compile(
|
||||
r"\[Web발신\]\s*\[카카오뱅크\]\s+"
|
||||
r"(?P<acct>\S+)\s+"
|
||||
r"(?P<md>\d{2}/\d{2})\s+(?P<hm>\d{2}:\d{2})\s+"
|
||||
r"(?P<kind>입금|출금)\s+(?P<amount>[\d,]+)원\s+"
|
||||
r"(?P<merchant>[^\n]+?)"
|
||||
r"(?:\s*\n잔액\s+(?P<balance>[\d,]+)원)?\s*$",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def parse_kakao_bank(raw: str, created_at: str) -> "dict | None":
|
||||
text = _normalize(raw)
|
||||
m = KAKAOBANK_RE.search(text)
|
||||
if not m:
|
||||
return None
|
||||
kind = "deposit" if m.group("kind") == "입금" else "withdrawal"
|
||||
amount = int(m.group("amount").replace(",", ""))
|
||||
merchant = _normalize(m.group("merchant")).split("\n")[0].strip() or "(메모없음)"
|
||||
result = {
|
||||
"kind": kind,
|
||||
"entry_date": _iso_to_kst_date(created_at),
|
||||
"amount": amount,
|
||||
"merchant": merchant,
|
||||
"raw": raw,
|
||||
}
|
||||
if m.group("balance"):
|
||||
result["balance"] = int(m.group("balance").replace(",", ""))
|
||||
return result
|
||||
|
||||
|
||||
# 현대카드 승인 형식 (취소는 별도 샘플 확보 후 추가):
|
||||
# 현대카드 블루멤버스 승인
|
||||
# 방*원
|
||||
# 5,170원 일시불
|
||||
# 04/25 11:34
|
||||
# 쿠팡
|
||||
# 누적1,466,011원
|
||||
HYUNDAI_CARD_APPROVE_RE = re.compile(
|
||||
r"^\s*현대카드[^\n]*승인\s*\n"
|
||||
r"(?P<who>[^\n]+)\s*\n"
|
||||
r"(?P<amount>[\d,]+)\s*원[^\n]*\n"
|
||||
r"\d{2}/\d{2}\s+\d{2}:\d{2}\s*\n"
|
||||
r"(?P<merchant>[^\n]+)"
|
||||
r"(?:\s*\n누적\s*(?P<cumulative>[\d,]+)\s*원)?",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
# RCS/MAAP 형식은 한 줄로 들어온다:
|
||||
# [Web발신] 현대카드 블루멤버스 승인 방*원 84,000원 일시불 05/16 11:57 네이버페이 누적820,000원
|
||||
HYUNDAI_CARD_APPROVE_INLINE_RE = re.compile(
|
||||
r"현대카드[^\n]*승인\s+"
|
||||
r"(?P<who>\S+)\s+"
|
||||
r"(?P<amount>[\d,]+)\s*원[^\n]*?"
|
||||
r"\d{2}/\d{2}\s+\d{2}:\d{2}\s+"
|
||||
r"(?P<merchant>.+?)"
|
||||
r"(?:\s+누적\s*(?P<cumulative>[\d,]+)\s*원?)?\s*$",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
|
||||
def parse_hyundai_card(raw: str, created_at: str) -> "dict | None":
|
||||
text = _normalize(raw).replace("[Web발신]", "").strip()
|
||||
m = HYUNDAI_CARD_APPROVE_RE.search(text) or HYUNDAI_CARD_APPROVE_INLINE_RE.search(text)
|
||||
if not m:
|
||||
return None
|
||||
amount = int(m.group("amount").replace(",", ""))
|
||||
merchant = re.sub(r"\s+", " ", m.group("merchant").strip()) or "(가맹점없음)"
|
||||
result = {
|
||||
"kind": "card_approval",
|
||||
"entry_date": _iso_to_kst_date(created_at),
|
||||
"amount": amount,
|
||||
"merchant": merchant,
|
||||
"raw": raw,
|
||||
}
|
||||
if m.group("cumulative"):
|
||||
result["cumulative"] = int(m.group("cumulative").replace(",", ""))
|
||||
return result
|
||||
|
||||
|
||||
PARSERS = {
|
||||
"+8215991111": parse_hana_bank,
|
||||
"+8215447200": parse_shinhan_card,
|
||||
"+8215778000": parse_shinhan_bank,
|
||||
"+8215993333": parse_kakao_bank,
|
||||
"+8215776200": parse_hyundai_card,
|
||||
"15776200@botplatform.maapservice.com": parse_hyundai_card,
|
||||
}
|
||||
|
||||
|
||||
def parse(sender: str, raw: str, created_at: str) -> "dict | None":
|
||||
fn = PARSERS.get(sender)
|
||||
if fn is None:
|
||||
return None
|
||||
try:
|
||||
return fn(raw, created_at)
|
||||
except Exception:
|
||||
return None
|
||||
Reference in New Issue
Block a user