auto: 일일 백업 2026-09-11 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"setupCompletedAt": "2026-04-23T06:17:48.250Z"
|
||||
}
|
||||
@@ -105,3 +105,96 @@ LLM-LLM 자연어 통신은 프롬프트 인젝션·할루시네이션 증폭
|
||||
## 직접 고쳐 써라
|
||||
|
||||
이 문서는 출발점이다. 패턴이 보이면 직접 업데이트해라. 정비는 코디가 하지만, 이 매뉴얼의 작가는 너다.
|
||||
|
||||
## Tools
|
||||
|
||||
### Local notes (migrated from TOOLS.md)
|
||||
|
||||
# TOOLS.md - Local Notes
|
||||
|
||||
가계부 관련 파일·스크립트·발신번호 메모. 모든 경로는 `agents/budget/workspace/` 기준.
|
||||
|
||||
## Whooing
|
||||
|
||||
- 가계부 시스템: https://whooing.com
|
||||
- **웹훅 URL:** `/Users/snowoyh/.openclaw/credentials/whooing.json` 의 `webhook_url` 필드.
|
||||
형식: `https://whooing.com/webhook/s/xxxx-xxxx-xxxx-xxxx`
|
||||
- 웹훅 페이로드 종류:
|
||||
- **raw 문자**: `message=<원문>` 보내면 후잉이 알아서 파싱
|
||||
- **구조화 JSON**: `{entry_date, item, money, left, right, memo}` (left=차변, right=대변)
|
||||
- 결제 취소/승인취소도 같이 전송해야 후잉에서 -금액으로 상쇄됨.
|
||||
|
||||
## State
|
||||
|
||||
- `state/whooing_synced.json` — `{ "last_message_at": <ISO>, "last_synced_at": "ISO" }`. dedupe 기준.
|
||||
- `state/whooing_account_map.json` — 발신번호 → 후잉 매핑. `confirmed: true`인 엔트리만 후잉으로 자동 전송. `confirmed: false`는 발견 시 로그만 남기고 사용자 확인 대기.
|
||||
- `state/whooing_accounts.json` — **관리자님 후잉 계정과목 차트** (자산/부채/순자산/비용/수익 + carrier→결제수단 매핑). 구조화 입력으로 left/right 채울 때 항상 이 파일에서 정확한 계정명을 끌어쓴다. 차트에 없는 계정명은 후잉이 거부하므로 절대 추측해 보내지 말 것.
|
||||
- `state/whooing_failures.json` — 후잉이 4xx/5xx로 거절한 건. 수동 확인용.
|
||||
|
||||
## Scripts (python3)
|
||||
|
||||
- `skills/whooing-sync/scripts/whooing_sync.py` — iMessage 결제문자 → 후잉 웹훅 POST. launchd `ai.openclaw.budget.whooing-sync` 가 매시 0/15/30/45분 실행 (OpenClaw cron 아님). ⚠️ **on-demand 실행은 스크립트 직접 호출이 아니라 `launchctl kickstart -p gui/$(id -u)/ai.openclaw.budget.whooing-sync` + 로그 확인** — 내 세션은 게이트웨이 node 를 TCC responsible process 로 물고 있어 `imsg` 가 Messages DB 를 못 읽는다. FDA 필수(`/opt/homebrew/bin/imsg`). 페어 매칭 로직 내장 — 자세한 건 `skills/whooing-sync/SKILL.md`.
|
||||
- `skills/whooing-sync/scripts/imsg_cli.py` — `imsg` CLI 공용 래퍼(`run_json`/`chats`). stderr·returncode 를 판정해 FDA 누락을 파싱 에러로 둔갑시키지 않는다. `whooing_sync.py`·`gahee_reminder.py` 가 공유.
|
||||
- `skills/whooing-sync/scripts/whooing_manual.py` — iMessage 없이 한 건 직접 등록. structured(`--item/--money/--left/--right [--date] [--memo]`) 또는 raw(`--message`). structured는 `whooing_accounts.json` 차트 검증 후 POST.
|
||||
- `skills/whooing-sync/scripts/whooing_balance.py` — 후잉 OpenAPI로 자산/부채/자본 잔액 조회. 옵션: `--section-id`, `--as-of YYYY-MM-DD`, `--json`. 크리덴셜은 `credentials/whooing.json`의 `api` 블록(app_id/token/signature).
|
||||
|
||||
## 발신번호
|
||||
|
||||
`state/whooing_account_map.json`이 정답. 아래는 사람이 읽기 쉬운 요약.
|
||||
|
||||
**확인됨 (관리자님이 직접 확인):**
|
||||
|
||||
| 발신처 | 발신번호 |
|
||||
|--------|---------|
|
||||
| 하나 계좌이체 | `+8215991111` |
|
||||
| 신한은행 | `+8215778000` |
|
||||
| 신한카드 | `+8215447000` (※ 과거 오타 `+8215447200` 쓴 적 있음 — 실사용 X, 현재 confirmed=false 로 대기) |
|
||||
|
||||
**추정 (모델 사전지식 기반, 실제 메시지 들어오면 confirmed로 승격 필요):**
|
||||
|
||||
| 발신처 | 추정 발신번호 |
|
||||
|--------|--------------|
|
||||
| 삼성카드 | `+8215888900` |
|
||||
| KB국민카드 | `+8215881688` |
|
||||
| 현대카드 | `+8215776200` |
|
||||
| 롯데카드 | `+8215888100` |
|
||||
| 하나카드 | `+8218001111` |
|
||||
| 우리카드 | `+8215889955` |
|
||||
| BC카드 | `+8215884000` |
|
||||
| NH농협카드 | `+8216444000` |
|
||||
| 카카오페이 | `+8216447405` |
|
||||
| 네이버페이 | `+8215883819` |
|
||||
| 토스 | `+8215994905` |
|
||||
| KB국민은행 | `+8215889999` |
|
||||
| 우리은행 | `+8215885000` |
|
||||
| NH농협은행 | `+8216613000` |
|
||||
| 카카오뱅크 | `+8215993333` |
|
||||
|
||||
**정책:** 미확인 번호 메시지가 들어오면 무시 또는 `failures.json`에 "unmapped"로 기록만 한다. 잘못된 자동 등록을 막기 위함.
|
||||
|
||||
## 관리자님 관련
|
||||
|
||||
- 가계부 질문이 들어오면 먼저 `state/whooing_synced.json`과 `whooing_failures.json`을 본다.
|
||||
- 잔액·계정 총합은 `whooing_balance.py`로 조회한다. 후잉 OpenAPI(`bs.json`/`accounts.json`/`sections.json`) 연동 완료.
|
||||
|
||||
## Playwright MCP (브라우저 자동화)
|
||||
|
||||
OpenClaw가 `openclaw.json` 의 MCP 서버 `playwright`로 노출. 도구는 `playwright__browser_*` prefix로 약 23개.
|
||||
|
||||
- **탐색:** `navigate / navigate_back / tabs / wait_for`
|
||||
- **관찰:** `snapshot` (접근성 트리 — 클릭 대상 ref 식별에 **우선 사용**) / `take_screenshot` / `console_messages` / `network_requests` / `network_request`
|
||||
- **조작:** `click / type / fill_form / press_key / hover / select_option / drag / drop / file_upload / handle_dialog / resize / close`
|
||||
- **평가:** `evaluate` (페이지 JS 컨텍스트) / `run_code_unsafe` (Playwright 코드 직접 — **RCE 등급, 마지막 수단**)
|
||||
|
||||
**표준 흐름:** `navigate` → `snapshot`으로 ref 확보 → `click`/`type`/`fill_form` → `wait_for`로 결과 대기 → `snapshot` 또는 `network_requests`로 검증. 스크린샷은 사람 보고용이지 다음 동작 분기 근거로 쓰지 말 것.
|
||||
|
||||
**운영 특성:**
|
||||
- 기본값 `--headless --isolated`. 매 세션 쿠키·로그인 폐기.
|
||||
- 영속 로그인 필요한 사이트는 시도 전 **코디에게 `user-data-dir` 분리·등록 요청**.
|
||||
- 첫 호출 시 npx spawn 1~2초 지연.
|
||||
- 은행·카드사 공동인증서·간편비밀번호 화면이 나오면 **즉시 중단하고 관리자님께 보고**, 우회 시도 X.
|
||||
|
||||
**활용 시나리오 (골디 맥락):**
|
||||
- 후잉 웹 직접 분개 수정/삭제 — 현재 inbox·webhook 단방향 한계 보완용. **영속 로그인 필요** → 코디에게 user-data-dir 분리 요청 후 진행
|
||||
- 카드사 웹 청구서 PDF 수집 (월별 명세 검증·보조). 공동인증서·간편비번 화면 진입 시 중단
|
||||
- 우선순위는 항상 **API · iMessage 결제문자 → 후잉 webhook 자동 동기화**. 브라우저는 자동 흐름이 못 메꾸는 잔여 영역만
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
- **스케줄러는 launchd** (OpenClaw cron 아님). plist: `~/Library/LaunchAgents/ai.openclaw.budget.whooing-sync.plist`, label `ai.openclaw.budget.whooing-sync`, 매시 0/15/30/45분. 로그는 `/Users/snowoyh/.openclaw/logs/whooing-sync.{log,err.log}`. OpenClaw 의 `후잉 가계부 동기화` cron 잡은 의도적으로 `enabled: false` — LLM 세션 비용(월 ~70M 토큰) 때문에 전환된 것. **다시 enable 하지 말 것.**
|
||||
- **FDA 필수**: launchd 는 FDA 상속 안 받음. `/opt/homebrew/bin/imsg` 를 "시스템 설정 → 개인정보 보호 및 보안 → 전체 디스크 접근 권한" 에 등록해야 Messages DB 읽힘. 누락 시 `imsg chats 실행 실패` stderr + stdout "새 결제 메시지 없음" 으로 조용히 넘어감. 맥 이전·imsg 재설치 시 재등록.
|
||||
- **페어 매칭**: `whooing_sync.py` 가 동일 금액 입↔출 SMS 를 5분 윈도우(`PAIR_WINDOW_SECONDS=300`) 로 짝지어 1건의 자산↔자산 structured 이체로 합성. 짝 없는 입출금이 5분 이내(`HOLD_GRACE_SECONDS=300`) 면 hold → 다음 cron 재시도. 상세는 SKILL.md 의 "페어 매칭" 섹션.
|
||||
|
||||
- **증권 이체 매칭** (2026-09-10): 은행→증권 이체는 은행 SMS 에 **입금자명만** 찍혀 짝이 없다. `securities_match.py` 가 capital-block 직전에 키움 kt00015 에서 짝(같은 날·같은 금액·반대 방향·`is_principal_flow` 적요)을 찾아 `증권(효원) ← 하나은행(효원)` 이체로 분개한다. **평시 키움 콜 0**(자본 분개로 갈 뻔한 건이 있을 때만 4콜). 애매하면(후보 여럿·조회 실패) 매칭 안 하고 기존 알림 경로로 보낸다. 상세는 SKILL.md 의 "증권 이체 매칭" 섹션.
|
||||
- **비결제/비거래 메시지 자동 스킵**: `SKIP_PATTERNS` 에 `"인증번호"`, `"간편인증서"`, `"개설되었습니다"`, `"계좌개설"` 포함. 또한 금액 패턴(`숫자원`)이 없는 문자는 후잉 raw 폴백으로 보내지 않는다. 인증/간편인증서/계좌개설 안내 문자는 후잉 처리하지 않는다.
|
||||
|
||||
## 발신번호 주의
|
||||
@@ -27,9 +29,9 @@
|
||||
- 신한은행: `+8215778000` (1577-8000).
|
||||
- 하나 계좌이체: `+8215991111`.
|
||||
|
||||
## merchant_map 한계 (미해결)
|
||||
## merchant_map 한계 (일부 해소)
|
||||
|
||||
`exact: "방효원"` 룰은 본인 이름만으론 이체 상대계좌를 식별 못 함. 페어 매칭이 1차 방어선이지만, 한쪽 은행이 confirmed=false 거나 시간창 벗어나면 이 룰이 폴백으로 타서 **기초잔액(효원) 경유로 잘못 기록**될 수 있다. 발견 시 `whooing_manual.py` 로 역분개하거나 후잉 UI 에서 삭제 후 올바른 이체 1건으로 재등록. 근본 해결은 메모에 "카뱅/신한" 같은 목적지 키워드를 일관되게 넣고 `merchant_map.contains` 에 등록하는 것.
|
||||
`exact: "방효원"` 룰은 본인 이름만으론 이체 상대계좌를 식별 못 함. ⚠️ **상대가 증권 계좌인 경우는 2026-09-10 증권 이체 매칭으로 해소**됐다(키움 kt00015 가 짝을 알려준다). 아래는 그 외 상대계좌(다른 은행·타인 송금) 이야기다. 페어 매칭이 1차 방어선이지만, 한쪽 은행이 confirmed=false 거나 시간창 벗어나면 이 룰이 폴백으로 타서 **기초잔액(효원) 경유로 잘못 기록**될 수 있다. 발견 시 `whooing_manual.py` 로 역분개하거나 후잉 UI 에서 삭제 후 올바른 이체 1건으로 재등록. 근본 해결은 메모에 "카뱅/신한" 같은 목적지 키워드를 일관되게 넣고 `merchant_map.contains` 에 등록하는 것.
|
||||
|
||||
## 우선 룰 (whooing_overrides.json) — 2026-04-29 도입
|
||||
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
# TOOLS.md - Local Notes
|
||||
|
||||
가계부 관련 파일·스크립트·발신번호 메모. 모든 경로는 `agents/budget/workspace/` 기준.
|
||||
|
||||
## Whooing
|
||||
|
||||
- 가계부 시스템: https://whooing.com
|
||||
- **웹훅 URL:** `/Users/snowoyh/.openclaw/credentials/whooing.json` 의 `webhook_url` 필드.
|
||||
형식: `https://whooing.com/webhook/s/xxxx-xxxx-xxxx-xxxx`
|
||||
- 웹훅 페이로드 종류:
|
||||
- **raw 문자**: `message=<원문>` 보내면 후잉이 알아서 파싱
|
||||
- **구조화 JSON**: `{entry_date, item, money, left, right, memo}` (left=차변, right=대변)
|
||||
- 결제 취소/승인취소도 같이 전송해야 후잉에서 -금액으로 상쇄됨.
|
||||
|
||||
## State
|
||||
|
||||
- `state/whooing_synced.json` — `{ "last_message_at": <ISO>, "last_synced_at": "ISO" }`. dedupe 기준.
|
||||
- `state/whooing_account_map.json` — 발신번호 → 후잉 매핑. `confirmed: true`인 엔트리만 후잉으로 자동 전송. `confirmed: false`는 발견 시 로그만 남기고 사용자 확인 대기.
|
||||
- `state/whooing_accounts.json` — **관리자님 후잉 계정과목 차트** (자산/부채/순자산/비용/수익 + carrier→결제수단 매핑). 구조화 입력으로 left/right 채울 때 항상 이 파일에서 정확한 계정명을 끌어쓴다. 차트에 없는 계정명은 후잉이 거부하므로 절대 추측해 보내지 말 것.
|
||||
- `state/whooing_failures.json` — 후잉이 4xx/5xx로 거절한 건. 수동 확인용.
|
||||
|
||||
## Scripts (python3)
|
||||
|
||||
- `skills/whooing-sync/scripts/whooing_sync.py` — iMessage 결제문자 → 후잉 웹훅 POST. launchd `ai.openclaw.budget.whooing-sync` 가 매시 0/15/30/45분 실행 (OpenClaw cron 아님). ⚠️ **on-demand 실행은 스크립트 직접 호출이 아니라 `launchctl kickstart -p gui/$(id -u)/ai.openclaw.budget.whooing-sync` + 로그 확인** — 내 세션은 게이트웨이 node 를 TCC responsible process 로 물고 있어 `imsg` 가 Messages DB 를 못 읽는다. FDA 필수(`/opt/homebrew/bin/imsg`). 페어 매칭 로직 내장 — 자세한 건 `skills/whooing-sync/SKILL.md`.
|
||||
- `skills/whooing-sync/scripts/imsg_cli.py` — `imsg` CLI 공용 래퍼(`run_json`/`chats`). stderr·returncode 를 판정해 FDA 누락을 파싱 에러로 둔갑시키지 않는다. `whooing_sync.py`·`gahee_reminder.py` 가 공유.
|
||||
- `skills/whooing-sync/scripts/whooing_manual.py` — iMessage 없이 한 건 직접 등록. structured(`--item/--money/--left/--right [--date] [--memo]`) 또는 raw(`--message`). structured는 `whooing_accounts.json` 차트 검증 후 POST.
|
||||
- `skills/whooing-sync/scripts/whooing_balance.py` — 후잉 OpenAPI로 자산/부채/자본 잔액 조회. 옵션: `--section-id`, `--as-of YYYY-MM-DD`, `--json`. 크리덴셜은 `credentials/whooing.json`의 `api` 블록(app_id/token/signature).
|
||||
|
||||
## 발신번호
|
||||
|
||||
`state/whooing_account_map.json`이 정답. 아래는 사람이 읽기 쉬운 요약.
|
||||
|
||||
**확인됨 (관리자님이 직접 확인):**
|
||||
|
||||
| 발신처 | 발신번호 |
|
||||
|--------|---------|
|
||||
| 하나 계좌이체 | `+8215991111` |
|
||||
| 신한은행 | `+8215778000` |
|
||||
| 신한카드 | `+8215447000` (※ 과거 오타 `+8215447200` 쓴 적 있음 — 실사용 X, 현재 confirmed=false 로 대기) |
|
||||
|
||||
**추정 (모델 사전지식 기반, 실제 메시지 들어오면 confirmed로 승격 필요):**
|
||||
|
||||
| 발신처 | 추정 발신번호 |
|
||||
|--------|--------------|
|
||||
| 삼성카드 | `+8215888900` |
|
||||
| KB국민카드 | `+8215881688` |
|
||||
| 현대카드 | `+8215776200` |
|
||||
| 롯데카드 | `+8215888100` |
|
||||
| 하나카드 | `+8218001111` |
|
||||
| 우리카드 | `+8215889955` |
|
||||
| BC카드 | `+8215884000` |
|
||||
| NH농협카드 | `+8216444000` |
|
||||
| 카카오페이 | `+8216447405` |
|
||||
| 네이버페이 | `+8215883819` |
|
||||
| 토스 | `+8215994905` |
|
||||
| KB국민은행 | `+8215889999` |
|
||||
| 우리은행 | `+8215885000` |
|
||||
| NH농협은행 | `+8216613000` |
|
||||
| 카카오뱅크 | `+8215993333` |
|
||||
|
||||
**정책:** 미확인 번호 메시지가 들어오면 무시 또는 `failures.json`에 "unmapped"로 기록만 한다. 잘못된 자동 등록을 막기 위함.
|
||||
|
||||
## 관리자님 관련
|
||||
|
||||
- 가계부 질문이 들어오면 먼저 `state/whooing_synced.json`과 `whooing_failures.json`을 본다.
|
||||
- 잔액·계정 총합은 `whooing_balance.py`로 조회한다. 후잉 OpenAPI(`bs.json`/`accounts.json`/`sections.json`) 연동 완료.
|
||||
|
||||
## Playwright MCP (브라우저 자동화)
|
||||
|
||||
OpenClaw가 `openclaw.json` 의 MCP 서버 `playwright`로 노출. 도구는 `playwright__browser_*` prefix로 약 23개.
|
||||
|
||||
- **탐색:** `navigate / navigate_back / tabs / wait_for`
|
||||
- **관찰:** `snapshot` (접근성 트리 — 클릭 대상 ref 식별에 **우선 사용**) / `take_screenshot` / `console_messages` / `network_requests` / `network_request`
|
||||
- **조작:** `click / type / fill_form / press_key / hover / select_option / drag / drop / file_upload / handle_dialog / resize / close`
|
||||
- **평가:** `evaluate` (페이지 JS 컨텍스트) / `run_code_unsafe` (Playwright 코드 직접 — **RCE 등급, 마지막 수단**)
|
||||
|
||||
**표준 흐름:** `navigate` → `snapshot`으로 ref 확보 → `click`/`type`/`fill_form` → `wait_for`로 결과 대기 → `snapshot` 또는 `network_requests`로 검증. 스크린샷은 사람 보고용이지 다음 동작 분기 근거로 쓰지 말 것.
|
||||
|
||||
**운영 특성:**
|
||||
- 기본값 `--headless --isolated`. 매 세션 쿠키·로그인 폐기.
|
||||
- 영속 로그인 필요한 사이트는 시도 전 **코디에게 `user-data-dir` 분리·등록 요청**.
|
||||
- 첫 호출 시 npx spawn 1~2초 지연.
|
||||
- 은행·카드사 공동인증서·간편비밀번호 화면이 나오면 **즉시 중단하고 관리자님께 보고**, 우회 시도 X.
|
||||
|
||||
**활용 시나리오 (골디 맥락):**
|
||||
- 후잉 웹 직접 분개 수정/삭제 — 현재 inbox·webhook 단방향 한계 보완용. **영속 로그인 필요** → 코디에게 user-data-dir 분리 요청 후 진행
|
||||
- 카드사 웹 청구서 PDF 수집 (월별 명세 검증·보조). 공동인증서·간편비번 화면 진입 시 중단
|
||||
- 우선순위는 항상 **API · iMessage 결제문자 → 후잉 webhook 자동 동기화**. 브라우저는 자동 흐름이 못 메꾸는 잔여 영역만
|
||||
@@ -0,0 +1 @@
|
||||
- 하나은행 2026-09-10 09:27 `방효원` 3,000,000원 출금은 관리자님 확인으로 주식계좌 이체였다. 후잉에 `증권(효원) ← 하나은행(효원)`, item `증권이체`, memo 원문으로 수동 등록했고 응답 200/done 확인. 등록 후 하나은행 후잉 잔액 `35,689,680원`이 SMS 잔액과 일치해 `state/whooing_failures.json`의 `capital_blocked` 보류 건을 비웠다.
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"setupCompletedAt": "2026-04-23T06:17:48.250Z"
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
name: monthly-settlement
|
||||
description: 매월 1일 후잉 잔액을 스냅샷으로 저장하고 전월 스냅샷과 비교해 자산 변동 리포트를 생성·발송한다. "이번달 결산", "월간 결산", "자산 변동 요약" 요청에 사용. 매월 1일 05:00 KST cron(agent: budget) 자동 실행.
|
||||
---
|
||||
|
||||
# monthly-settlement
|
||||
|
||||
매월 1일에 후잉 잔액을 스냅샷으로 저장하고, 전월 스냅샷과 비교해 자산 변동을 리포트한다.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
name: whooing-sync
|
||||
description: iMessage 카드·은행 결제 알림을 후잉(whooing.com) 가계부로 자동 동기화한다. 발신번호→계정 매핑(whooing_account_map.json), 결제 취소 감액 처리, 미매핑 발신처 보고, 수동 분개 등록(whooing_manual.py), 잔액 조회(whooing_balance.py)를 포함. "가계부 동기화", "후잉 동기화", "결제내역 정리", "이번달 지출 얼마야?" 같은 요청에 사용. 기본 운용은 launchd가 매시 0/15/30/45분 자동 실행.
|
||||
---
|
||||
|
||||
# whooing-sync
|
||||
|
||||
iMessage에 들어오는 카드/은행 결제 알림을 후잉(whooing.com) 웹훅으로 자동 전송한다.
|
||||
@@ -51,7 +56,7 @@ launchd/cron 이 이 줄을 그대로 결과로 받는다.
|
||||
- **실패 알림** — 후잉 웹훅이 거절(HTTP 에러, 본문 `fail` / `Error :`)한 건. 실패 1건당 1메시지, 4건 이상이면 앞 3건 + 초과분 요약. `_format_sync_failure()` 포맷.
|
||||
- **raw 폴백 알림** — 후잉은 200 받았지만 structured 매칭 실패로 raw 모드로 넘어간 건. sync 사이클당 **1메시지**(최대 3건 나열, 초과 시 카운트). parser / carrier_to_account / merchant_map 보완 신호. `_format_raw_fallback()` 포맷.
|
||||
|
||||
- **기초잔액(자본) 분개 보류 알림** (2026-09-07) — 분개의 좌·우 어느 쪽이든 `순자산` 계정(`기초잔액(효원)`/`기초잔액(가희)`)이면 **후잉에 넣지 않고 알림만** 보낸다(관리자님 지시). `_format_capital_blocked()` 포맷, 기록은 `whooing_failures.json` 의 `capital_blocked` 키.
|
||||
- **기초잔액(자본) 분개 보류 알림** (2026-09-07) — 분개의 좌·우 어느 쪽이든 `순자산` 계정(`기초잔액(효원)`/`기초잔액(가희)`)이면 **후잉에 넣지 않고 알림만** 보낸다(관리자님 지시). ⚠️ 2026-09-10 부터 **여기 오기 전에 증권 이체 매칭을 먼저 시도**한다(아래 「증권 이체 매칭」) — 짝을 찾으면 자본이 아니라 자산↔자산 이체로 분개되고, 이 알림은 짝을 못 찾은 건에만 남는다. `_format_capital_blocked()` 포맷, 기록은 `whooing_failures.json` 의 `capital_blocked` 키.
|
||||
- 배경: `merchant_map` 의 `방효원` exact 룰(`left: 기초잔액(효원)`, "본인 명의 자체이체")은 **짝이 되는 출금 SMS 가 같이 와서 상계되는 것**을 전제한다. 짝이 없으면 자본 한쪽만 남아 **순자산이 조용히 부풀려진다.** 2026-09-07 카카오뱅크 879,993 실사고 — 처제 대납금 상환인데 입금자명이 "방효원"이라 이 룰에 걸렸고, 금액이 08/27 클럽호핑과 정확히 일치해서 겨우 눈에 걸렸다(어중간한 금액이면 지나갔다).
|
||||
- ⚠️ **커서는 `latest_skip` 으로 진행시킨다** — 재시도해도 같은 룰이 같은 결과를 내므로 여기서 커서를 막으면 **이 메시지 뒤의 모든 거래가 영구히 멈춘다.** `blocked_at` 을 세우지 않는 이유.
|
||||
- 후잉엔 아무것도 안 들어가므로 그 계좌의 **잔액 불일치 알림이 뒤이어 뜬다** — 알림을 놓쳐도 두 번째 그물이 있다.
|
||||
@@ -158,6 +163,30 @@ node@22 가 2026-07-20 에 `22.22.2` → `22.23.1` 로 올라가며 **바이너
|
||||
- 한쪽 은행이 confirmed=false → SMS 수집 안 됨. 메모 키워드("카뱅오픈방효원")가 `merchant_map.contains` 와 맞으면 withdrawal-only structured 로 처리. 아니면 raw.
|
||||
- 시간창 벗어남 / 단독 입출금 / 외부 송금 → 각자 개별 처리.
|
||||
|
||||
## 증권 이체 매칭 (은행 SMS ↔ 키움 kt00015)
|
||||
|
||||
**은행에서 증권으로 이체하면 은행 SMS 에는 입금자명(=본인 이름)만 찍혀 목적지를 알 수 없다.** 그래서 `방효원` exact 룰에 걸려 `기초잔액 ← 은행` 이 되고, 위 자본 분개 가드에 막혀 **미반영**된다. 반대편(증권 입금)은 **SMS 가 아예 없어** 월 1회 `securities_balance` reconcile 의 차액으로만 나타나는데, 그 차액은 전부 `주식평가수익` 으로 분개되므로 **이체가 손익으로 둔갑**한다. 양쪽에서 동시에 새던 구멍이다.
|
||||
|
||||
2026-09-10 부터 그 짝을 **키움 kt00015 입출금 내역에서 찾아** `증권(효원) ← 하나은행(효원)` 이체 한 건으로 만든다. 모듈 `scripts/securities_match.py`, 진입점은 `whooing_sync.try_securities_transfer()`.
|
||||
|
||||
- **호출 시점은 capital-block 직전뿐** — 평시 키움 콜 0. 자본 분개로 갈 뻔한 건이 있는 사이클에만 계좌 수만큼(4콜) 조회한다. 그래서 15분 주기에 상시 조회가 붙지 않는다.
|
||||
- **매칭 조건**: 같은 날짜 + 금액 일치 + 방향 반대(은행 출금↔증권 IN) + `investment_principal.is_principal_flow(적요)`. 적요 필터가 **배당·이자·수익분배금·공모주환불금을 걸러낸다** — 그것들은 이체가 아니라 수익이라 은행 SMS 와 짝이 될 수 없다.
|
||||
- ⚠️ **애매하면 매칭하지 않는다** — 후보가 여럿이면 시각 근접(`TIME_TIEBREAK_SECONDS` 600초)으로 좁히고, 그래도 하나로 안 줄면 포기하고 기존 알림 경로로 보낸다. 틀린 분개보다 사람이 보는 편이 낫다. 조회가 한 계좌라도 실패하면 **전체를 포기**한다(일부만 보고 판단하면 오매칭).
|
||||
- ⚠️ **증권 계좌 간 대체는 후보에서 뺀다**(`_drop_internal_transfers`) — 가희_일반→가희_ISA 같은 내부 이동은 후잉에선 같은 `증권(가희)` 자산이라 분개 대상이 아닌데, 금액이 같으면 은행 SMS 와 오매칭될 수 있다. 서로 다른 계좌에 같은 금액의 IN/OUT 이 함께 있으면 제외(2026-04-23 가희 100만원 실사례로 검증).
|
||||
- ⚠️ **kt00015 행에는 고유 ID 가 없다** — 중복 분개 방지 키는 `날짜|계좌|IN,OUT|금액|시각` 합성이고 `state/whooing_securities_matched.json` 에 남는다(180일 후 정리). **POST 성공 후에만 기록**한다 — 실패하면 다음 사이클에 다시 짝으로 잡혀야 한다.
|
||||
- 양방향 지원: 은행 출금→`증권 ← 은행`, 은행 입금(증권에서 뺀 돈)→`은행 ← 증권`.
|
||||
- 부수효과: 이체가 실시간으로 들어가므로 **월 1회 reconcile 차액에 순수 평가손익만 남는다.**
|
||||
|
||||
출력 예시:
|
||||
```
|
||||
✅ [securities] 하나 2026-09-10T00:27:44Z 200 | 증권(효원) ← 하나은행(효원) 3,000,000원 (증권 일반 이체)
|
||||
```
|
||||
|
||||
진단 CLI (매칭만 확인, 분개 없음):
|
||||
```bash
|
||||
python3 scripts/securities_match.py 20260910 3000000 withdrawal 2026-09-10T00:27:44.111Z
|
||||
```
|
||||
|
||||
### merchant_map 주의사항
|
||||
|
||||
`state/whooing_merchant_map.json` exact 룰에 `"방효원": { "left": "기초잔액(효원)" }` 가 등록돼 있다. 페어 미성립 시 이 룰로 폴백해서 **기초잔액을 경유한 잘못된 분개**가 기록될 수 있다. 한쪽 carrier 가 confirmed=false 일 때 구멍이 크다.
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
"""은행 이체 SMS ↔ 증권 계좌 입출금(kt00015) 짝 맞추기.
|
||||
|
||||
은행에서 증권으로 이체하면 은행 SMS 에는 **입금자명(=본인 이름)만** 찍혀서
|
||||
어디로 갔는지 알 수 없다. 그래서 `방효원` 같은 자체이체 룰에 걸려
|
||||
`기초잔액 ← 은행` 이 되고, 자본 분개 금지 가드에 막혀 **미반영**된다.
|
||||
반대편(증권 입금)은 SMS 가 아예 없어 월 1회 reconcile 차액으로만 나타나는데,
|
||||
그 차액은 전부 `주식평가수익` 으로 분개되므로 **이체가 손익으로 둔갑**한다.
|
||||
|
||||
이 모듈은 그 짝을 키움 kt00015 입출금 내역에서 찾아
|
||||
`증권(효원) ← 하나은행(효원)` 이체 한 건으로 만들어 준다.
|
||||
|
||||
⚠️ **capital-block 직전에만 호출한다** — 평시 키움 콜 0. 이체가 실제로 있었던
|
||||
사이클에만 계좌 수만큼(4콜) 조회한다.
|
||||
⚠️ **애매하면 매칭하지 않는다** — 후보가 여럿이면 포기하고 기존 알림 경로로
|
||||
보낸다. 틀린 분개보다 사람이 보는 편이 낫다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
KST = ZoneInfo("Asia/Seoul")
|
||||
ROOT = Path("/Users/snowoyh/.openclaw")
|
||||
STOCK_SCRIPTS = ROOT / "agents" / "stock" / "workspace" / "scripts"
|
||||
MATCHED_FILE = ROOT / "agents" / "budget" / "workspace" / "state" / "whooing_securities_matched.json"
|
||||
|
||||
# 증권 계좌 라벨 → 후잉 자산 계정명. 라벨 prefix 규칙은 send_balance_to_budget.py 와 동일.
|
||||
GAHEE_PREFIX = "가희"
|
||||
OWNER_ASSET = {"self": "증권(효원)", "gahee": "증권(가희)"}
|
||||
|
||||
# 은행 SMS 종류 → 기대되는 증권 쪽 방향.
|
||||
OPPOSITE = {"withdrawal": "IN", "deposit": "OUT"}
|
||||
|
||||
# 시각까지 일치하는 후보를 우선한다 (오픈뱅킹은 즉시 반영이라 실측 12초 차).
|
||||
# 후보가 여럿일 때만 쓰는 tie-breaker 이지 1차 필터가 아니다 —
|
||||
# 증권사 처리가 늦어 분 단위로 벌어지는 경우가 있어 창으로 잘라내면 놓친다.
|
||||
TIME_TIEBREAK_SECONDS = 600
|
||||
|
||||
|
||||
def _owner_of(label: str) -> str:
|
||||
return "gahee" if label.startswith(GAHEE_PREFIX) else "self"
|
||||
|
||||
|
||||
def row_key(label: str, row: dict) -> str:
|
||||
"""kt00015 행에는 고유 ID 가 없다. 중복 분개 방지용 합성 키.
|
||||
같은 날 같은 금액을 두 번 이체하면 time 으로 갈린다."""
|
||||
return f"{row['date']}|{label}|{row['io_tp']}|{row['amount']}|{row.get('time') or ''}"
|
||||
|
||||
|
||||
def _load_matched() -> dict:
|
||||
try:
|
||||
with MATCHED_FILE.open(encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
|
||||
|
||||
def mark_matched(key: str, meta: dict) -> None:
|
||||
"""분개 성공 후 호출. POST 성공한 뒤에만 기록해야 재시도 여지가 남는다."""
|
||||
data = _load_matched()
|
||||
data[key] = {**meta, "matched_at": datetime.now(KST).isoformat()}
|
||||
# 오래된 항목 정리 (180일) — 무한 증식 방지.
|
||||
cutoff = (datetime.now(KST) - timedelta(days=180)).strftime("%Y%m%d")
|
||||
data = {k: v for k, v in data.items() if k.split("|", 1)[0] >= cutoff}
|
||||
MATCHED_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = MATCHED_FILE.with_suffix(".tmp")
|
||||
with tmp.open("w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
os.replace(tmp, MATCHED_FILE)
|
||||
|
||||
|
||||
def _fetch_day(base_dt: str) -> dict[str, list[dict]] | None:
|
||||
"""전 계좌의 그날 입출금. 조회 실패는 None (= 판단 보류, 매칭 안 함)."""
|
||||
sys.path.insert(0, str(STOCK_SCRIPTS))
|
||||
try:
|
||||
import kiwoom_client as kc
|
||||
import investment_principal as ip
|
||||
except Exception as e: # noqa: BLE001 — import 실패해도 후잉 동기화는 계속돼야 한다
|
||||
sys.stderr.write(f"securities_match: 키움 모듈 import 실패: {e}\n")
|
||||
return None
|
||||
|
||||
out: dict[str, list[dict]] = {}
|
||||
for acc in kc.list_accounts():
|
||||
label = acc["label"]
|
||||
try:
|
||||
rows = kc.get_cash_flow(label, base_dt=base_dt)
|
||||
except Exception as e: # noqa: BLE001
|
||||
sys.stderr.write(f"securities_match: {label} kt00015 조회 실패: {e}\n")
|
||||
return None # 일부만 보고 판단하면 오매칭 — 전부 성공해야 진행
|
||||
out[label] = [r for r in rows if ip.is_principal_flow(r["rmrk"])]
|
||||
return out
|
||||
|
||||
|
||||
def _drop_internal_transfers(day: dict[str, list[dict]]) -> dict[str, list[dict]]:
|
||||
"""증권 계좌 간 대체(예: 가희_일반 → 가희_ISA)를 후보에서 뺀다.
|
||||
후잉에선 같은 자산이라 분개 대상이 아닌데, 금액이 같으면 은행 SMS 와
|
||||
오매칭될 수 있다. 서로 다른 계좌에 같은 금액의 IN/OUT 이 함께 있으면 제외."""
|
||||
ins = {(lb, r["amount"]) for lb, rows in day.items() for r in rows if r["io_tp"] == "IN"}
|
||||
outs = {(lb, r["amount"]) for lb, rows in day.items() for r in rows if r["io_tp"] == "OUT"}
|
||||
paired = {a for lb, a in ins} & {a for lb, a in outs}
|
||||
if not paired:
|
||||
return day
|
||||
cleaned: dict[str, list[dict]] = {}
|
||||
for lb, rows in day.items():
|
||||
keep = []
|
||||
for r in rows:
|
||||
if r["amount"] in paired and any(
|
||||
other != lb and any(
|
||||
o["amount"] == r["amount"] and o["io_tp"] != r["io_tp"] for o in day[other]
|
||||
)
|
||||
for other in day
|
||||
):
|
||||
continue
|
||||
keep.append(r)
|
||||
cleaned[lb] = keep
|
||||
return cleaned
|
||||
|
||||
|
||||
def _sms_time(created_at_utc: str | None) -> datetime | None:
|
||||
if not created_at_utc:
|
||||
return None
|
||||
try:
|
||||
return datetime.fromisoformat(created_at_utc.replace("Z", "+00:00")).astimezone(KST)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def find_match(entry_date: str, amount: int, bank_kind: str,
|
||||
created_at_utc: str | None = None) -> dict | None:
|
||||
"""은행 SMS 한 건에 대응하는 증권 입출금 행을 찾는다.
|
||||
|
||||
entry_date: 'YYYYMMDD' (은행 SMS 기준일)
|
||||
amount: 원 (양수)
|
||||
bank_kind: 'withdrawal' (은행→증권) | 'deposit' (증권→은행)
|
||||
created_at_utc: SMS 수신 시각 ISO — 후보가 여럿일 때 tie-break 에만 쓴다
|
||||
|
||||
반환 (매칭 성공 시): {label, asset, row, key}
|
||||
반환 None: 후보 없음 / 후보 여럿 / 이미 처리됨 / 조회 실패
|
||||
"""
|
||||
want_io = OPPOSITE.get(bank_kind)
|
||||
if want_io is None:
|
||||
return None
|
||||
|
||||
day = _fetch_day(entry_date)
|
||||
if day is None:
|
||||
return None
|
||||
day = _drop_internal_transfers(day)
|
||||
|
||||
matched = _load_matched()
|
||||
cands = [
|
||||
{"label": lb, "row": r, "key": row_key(lb, r)}
|
||||
for lb, rows in day.items()
|
||||
for r in rows
|
||||
if r["io_tp"] == want_io and r["amount"] == amount and row_key(lb, r) not in matched
|
||||
]
|
||||
if not cands:
|
||||
return None
|
||||
if len(cands) > 1:
|
||||
# 시각이 가까운 후보가 정확히 하나면 그걸 고른다. 아니면 포기.
|
||||
sms_at = _sms_time(created_at_utc)
|
||||
near = []
|
||||
if sms_at is not None:
|
||||
for c in cands:
|
||||
t = c["row"].get("time")
|
||||
if not t:
|
||||
continue
|
||||
try:
|
||||
rt = datetime.strptime(f"{entry_date} {t}", "%Y%m%d %H:%M:%S").replace(tzinfo=KST)
|
||||
except ValueError:
|
||||
continue
|
||||
if abs((rt - sms_at).total_seconds()) <= TIME_TIEBREAK_SECONDS:
|
||||
near.append(c)
|
||||
if len(near) != 1:
|
||||
sys.stderr.write(
|
||||
f"securities_match: {entry_date} {amount:,}원 {want_io} 후보 {len(cands)}건 — 매칭 포기\n")
|
||||
return None
|
||||
cands = near
|
||||
|
||||
c = cands[0]
|
||||
c["asset"] = OWNER_ASSET[_owner_of(c["label"])]
|
||||
return c
|
||||
|
||||
|
||||
def build_payload(match: dict, bank_asset: str, entry_date: str, amount: int,
|
||||
bank_kind: str, balance: int | None = None) -> dict:
|
||||
"""후잉 이체 payload. 은행→증권이면 left=증권, 증권→은행이면 left=은행."""
|
||||
if bank_kind == "withdrawal":
|
||||
left, right = match["asset"], bank_asset
|
||||
else:
|
||||
left, right = bank_asset, match["asset"]
|
||||
memo = f"{match['label']} {match['row']['rmrk']}"
|
||||
if balance is not None:
|
||||
memo += f" / 잔액 : {balance:,}원"
|
||||
return {
|
||||
"entry_date": entry_date,
|
||||
"money": amount,
|
||||
"item": "이체",
|
||||
"left": left,
|
||||
"right": right,
|
||||
"memo": memo,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 진단용: python3 securities_match.py YYYYMMDD 금액 withdrawal|deposit
|
||||
if len(sys.argv) < 4:
|
||||
print(__doc__)
|
||||
print("usage: securities_match.py YYYYMMDD AMOUNT withdrawal|deposit [SMS_ISO_UTC]")
|
||||
raise SystemExit(2)
|
||||
m = find_match(sys.argv[1], int(sys.argv[2]), sys.argv[3],
|
||||
sys.argv[4] if len(sys.argv) > 4 else None)
|
||||
if m is None:
|
||||
print("매칭 없음")
|
||||
raise SystemExit(1)
|
||||
print(json.dumps(m, ensure_ascii=False, indent=2))
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user