feat: 예치금 시스템 추가 (은행 SMS 자동 매칭)
- manage_deposit Function Calling 추가 (잔액조회, 입금신고, 거래내역, 취소) - Email Worker로 은행 SMS 파싱 (하나/KB/신한 지원) - 양방향 자동 매칭: 사용자 신고 ↔ 은행 알림 - D1 테이블: user_deposits, deposit_transactions, bank_notifications - 관리자 전용: 대기목록 조회, 입금 확인/거절 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
30
CLAUDE.md
30
CLAUDE.md
@@ -38,15 +38,19 @@ Telegram Webhook → Security Validation → Command/Message Router
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Core Services**:
|
**Core Services**:
|
||||||
- `openai-service.ts` - GPT-4o-mini + Function Calling (6개 도구: weather, search, time, calculate, lookup_docs, manage_domain)
|
- `openai-service.ts` - GPT-4o-mini + Function Calling (7개 도구: weather, search, time, calculate, lookup_docs, manage_domain, manage_deposit)
|
||||||
- `summary-service.ts` - 메시지 버퍼링 + 20개마다 프로필 추출 (슬라이딩 윈도우 3개)
|
- `summary-service.ts` - 메시지 버퍼링 + 20개마다 프로필 추출 (슬라이딩 윈도우 3개)
|
||||||
- `security.ts` - Webhook 검증 (timing-safe comparison, timestamp validation, rate limiting 30req/min)
|
- `security.ts` - Webhook 검증 (timing-safe comparison, timestamp validation, rate limiting 30req/min)
|
||||||
- `commands.ts` - 봇 명령어 핸들러 (/start, /help, /profile, /reset, /context, /stats, /debug)
|
- `commands.ts` - 봇 명령어 핸들러 (/start, /help, /profile, /reset, /context, /stats, /debug)
|
||||||
|
- `index.ts` - Email Worker 핸들러 (SMS → 메일 파싱, 은행 알림 자동 매칭)
|
||||||
|
|
||||||
**Data Layer** (D1 SQLite):
|
**Data Layer** (D1 SQLite):
|
||||||
- `users` - telegram_id 기반 사용자
|
- `users` - telegram_id 기반 사용자
|
||||||
- `message_buffer` - 롤링 대화 기록
|
- `message_buffer` - 롤링 대화 기록
|
||||||
- `summaries` - 프로필 버전 관리 (generation 추적)
|
- `summaries` - 프로필 버전 관리 (generation 추적)
|
||||||
|
- `user_deposits` - 예치금 계정 (잔액 관리)
|
||||||
|
- `deposit_transactions` - 예치금 거래 내역 (pending/confirmed/rejected)
|
||||||
|
- `bank_notifications` - 은행 SMS 파싱 결과 (자동 매칭용)
|
||||||
|
|
||||||
**AI Fallback**: OpenAI 미설정 시 Workers AI (Llama 3.1 8B) 자동 전환
|
**AI Fallback**: OpenAI 미설정 시 Workers AI (Llama 3.1 8B) 자동 전환
|
||||||
|
|
||||||
@@ -65,6 +69,7 @@ Telegram Webhook → Security Validation → Command/Message Router
|
|||||||
- `MAX_SUMMARIES_PER_USER`: 유지할 프로필 버전 수 (기본 3)
|
- `MAX_SUMMARIES_PER_USER`: 유지할 프로필 버전 수 (기본 3)
|
||||||
- `DOMAIN_AGENT_ID`: OpenAI Assistant ID (도메인 관리 에이전트)
|
- `DOMAIN_AGENT_ID`: OpenAI Assistant ID (도메인 관리 에이전트)
|
||||||
- `DOMAIN_OWNER_ID`: 도메인 관리 권한 Telegram ID (소유권 검증용)
|
- `DOMAIN_OWNER_ID`: 도메인 관리 권한 Telegram ID (소유권 검증용)
|
||||||
|
- `DEPOSIT_ADMIN_ID`: 예치금 관리 권한 Telegram ID (입금 확인/거절)
|
||||||
|
|
||||||
## External Integrations
|
## External Integrations
|
||||||
|
|
||||||
@@ -74,3 +79,26 @@ Telegram Webhook → Security Validation → Command/Message Router
|
|||||||
- **wttr.in**: 날씨 API
|
- **wttr.in**: 날씨 API
|
||||||
- **DuckDuckGo**: 웹 검색 API
|
- **DuckDuckGo**: 웹 검색 API
|
||||||
- **Vault**: `vault.anvil.it.com`에서 API 키 중앙 관리
|
- **Vault**: `vault.anvil.it.com`에서 API 키 중앙 관리
|
||||||
|
- **Email Workers**: SMS → 메일 수신 → 은행 알림 파싱 (하나/KB/신한 지원)
|
||||||
|
|
||||||
|
## Deposit System
|
||||||
|
|
||||||
|
**예치금 자동 매칭 흐름**:
|
||||||
|
```
|
||||||
|
[사용자 신고] "홍길동 50000원 입금"
|
||||||
|
↓
|
||||||
|
bank_notifications 테이블 검색 (입금자명 + 금액 매칭)
|
||||||
|
↓
|
||||||
|
매칭 성공 → confirmed | 매칭 실패 → pending 대기
|
||||||
|
|
||||||
|
[은행 SMS 수신] Email Worker
|
||||||
|
↓
|
||||||
|
SMS 파싱 → bank_notifications 저장
|
||||||
|
↓
|
||||||
|
deposit_transactions 검색 (pending 상태 + 입금자명 + 금액)
|
||||||
|
↓
|
||||||
|
매칭 성공 → confirmed + 잔액 증가 | 매칭 실패 → 알림만 저장
|
||||||
|
```
|
||||||
|
|
||||||
|
**입금 계좌**: 하나은행 427-910018-27104 (주식회사 아이언클래드)
|
||||||
|
- Vault 경로: `secret/companies/ironclad-corp`
|
||||||
|
|||||||
105
README.md
105
README.md
@@ -7,10 +7,11 @@
|
|||||||
1. [개요](#개요)
|
1. [개요](#개요)
|
||||||
2. [아키텍처](#아키텍처)
|
2. [아키텍처](#아키텍처)
|
||||||
3. [Function Calling](#function-calling)
|
3. [Function Calling](#function-calling)
|
||||||
4. [프로젝트 구조](#프로젝트-구조)
|
4. [예치금 시스템](#예치금-시스템)
|
||||||
5. [배포 가이드](#배포-가이드)
|
5. [프로젝트 구조](#프로젝트-구조)
|
||||||
6. [보안 설정](#보안-설정)
|
6. [배포 가이드](#배포-가이드)
|
||||||
7. [봇 명령어](#봇-명령어)
|
7. [보안 설정](#보안-설정)
|
||||||
|
8. [봇 명령어](#봇-명령어)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -20,9 +21,11 @@
|
|||||||
|
|
||||||
- **OpenAI GPT-4o-mini**: 고품질 AI 응답 및 Function Calling 지원
|
- **OpenAI GPT-4o-mini**: 고품질 AI 응답 및 Function Calling 지원
|
||||||
- **사용자 프로필**: 대화에서 사용자의 관심사, 목표, 맥락을 추출하여 프로필 구축
|
- **사용자 프로필**: 대화에서 사용자의 관심사, 목표, 맥락을 추출하여 프로필 구축
|
||||||
- **Function Calling**: 날씨, 검색, 시간, 계산, **문서 조회**, **도메인 관리** 등 AI가 자동으로 도구 호출
|
- **Function Calling**: 날씨, 검색, 시간, 계산, **문서 조회**, **도메인 관리**, **예치금 관리** 등 AI가 자동으로 도구 호출
|
||||||
- **Context7 연동**: 프로그래밍 라이브러리 공식 문서 실시간 조회
|
- **Context7 연동**: 프로그래밍 라이브러리 공식 문서 실시간 조회
|
||||||
- **Domain Agent**: OpenAI Assistants API 기반 도메인 관리 에이전트 연동
|
- **Domain Agent**: OpenAI Assistants API 기반 도메인 관리 에이전트 연동
|
||||||
|
- **예치금 시스템**: 은행 입금 자동 감지 + 사용자 신고 매칭으로 자동 충전
|
||||||
|
- **Email Worker**: SMS → 메일 → 자동 파싱으로 입금 알림 처리
|
||||||
- **무한 컨텍스트**: 슬라이딩 윈도우(3개)로 프로필 유지, 무제한 대화 기억
|
- **무한 컨텍스트**: 슬라이딩 윈도우(3개)로 프로필 유지, 무제한 대화 기억
|
||||||
- **개인화 응답**: 프로필 기반으로 맞춤형 AI 응답 제공
|
- **개인화 응답**: 프로필 기반으로 맞춤형 AI 응답 제공
|
||||||
- **폴백 지원**: OpenAI 미설정 시 Workers AI(Llama)로 자동 전환
|
- **폴백 지원**: OpenAI 미설정 시 Workers AI(Llama)로 자동 전환
|
||||||
@@ -37,6 +40,7 @@
|
|||||||
| **Context7** | 라이브러리 문서 조회 API |
|
| **Context7** | 라이브러리 문서 조회 API |
|
||||||
| **Domain Agent** | 도메인 관리 (OpenAI Assistants) |
|
| **Domain Agent** | 도메인 관리 (OpenAI Assistants) |
|
||||||
| **Namecheap API** | 도메인 조회/관리 백엔드 |
|
| **Namecheap API** | 도메인 조회/관리 백엔드 |
|
||||||
|
| **Email Workers** | SMS → 메일 파싱 (입금 알림) |
|
||||||
| **Workers AI** | 폴백용 (Llama 3.1 8B) |
|
| **Workers AI** | 폴백용 (Llama 3.1 8B) |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -60,10 +64,11 @@
|
|||||||
│ (Function Call) │ 도구 호출 자동 판단
|
│ (Function Call) │ 도구 호출 자동 판단
|
||||||
└──────────────────┘
|
└──────────────────┘
|
||||||
│
|
│
|
||||||
┌───┴───┬───────┬───────┬───────┬───────┐
|
┌───┴───┬───────┬───────┬───────┬───────┬───────┐
|
||||||
▼ ▼ ▼ ▼ ▼ ▼
|
▼ ▼ ▼ ▼ ▼ ▼ ▼
|
||||||
[날씨] [검색] [시간] [계산] [문서] [도메인] → 외부 API
|
[날씨] [검색] [시간] [계산] [문서] [도메인] [예치금] → 외부 API/D1
|
||||||
│ │ │ │ │ │
|
│ │ │ │ │ │ │
|
||||||
|
│ │ │ │ │ │ └── D1 (자동 매칭)
|
||||||
│ │ │ │ │ └── Domain Agent (Assistants API)
|
│ │ │ │ │ └── Domain Agent (Assistants API)
|
||||||
│ │ │ │ │ ↓
|
│ │ │ │ │ ↓
|
||||||
│ │ │ │ └── Context7 API Namecheap API
|
│ │ │ │ └── Context7 API Namecheap API
|
||||||
@@ -126,6 +131,7 @@ OpenAI Function Calling을 통해 AI가 자동으로 필요한 도구를 호출
|
|||||||
| **계산** | "123 * 456", "100의 20%" | 내장 |
|
| **계산** | "123 * 456", "100의 20%" | 내장 |
|
||||||
| **문서** | "React hooks 사용법", "OpenAI API 예제" | Context7 |
|
| **문서** | "React hooks 사용법", "OpenAI API 예제" | Context7 |
|
||||||
| **도메인** | "도메인 목록", "anvil.it.com 네임서버" | Domain Agent (소유자 전용) |
|
| **도메인** | "도메인 목록", "anvil.it.com 네임서버" | Domain Agent (소유자 전용) |
|
||||||
|
| **예치금** | "잔액 확인", "충전하고 싶어", "10000원 입금했어" | D1 + Email Worker |
|
||||||
|
|
||||||
### 동작 방식
|
### 동작 방식
|
||||||
|
|
||||||
@@ -147,6 +153,83 @@ OpenAI: 날씨 데이터를 자연어로 응답 생성
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 예치금 시스템
|
||||||
|
|
||||||
|
은행 계좌 입금 기반 예치금 충전 시스템입니다. 사용자 신고와 은행 SMS 알림을 양방향으로 자동 매칭합니다.
|
||||||
|
|
||||||
|
### 입금 계좌
|
||||||
|
|
||||||
|
| 은행 | 계좌번호 | 예금주 |
|
||||||
|
|------|----------|--------|
|
||||||
|
| 하나은행 | 427-910018-27104 | 주식회사 아이언클래드 |
|
||||||
|
|
||||||
|
> **Vault 경로**: `secret/companies/ironclad-corp` @ `vault.anvil.it.com`
|
||||||
|
|
||||||
|
### 자동 매칭 흐름
|
||||||
|
|
||||||
|
```
|
||||||
|
[시나리오 1: 사용자가 먼저 신고]
|
||||||
|
|
||||||
|
사용자: "홍길동 50000원 입금했어"
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────┐
|
||||||
|
│ bank_notifications│ ← 기존 은행 알림 확인
|
||||||
|
└──────────────────┘
|
||||||
|
│
|
||||||
|
├── 매칭 발견 → 즉시 confirmed + 잔액 증가
|
||||||
|
│
|
||||||
|
└── 매칭 없음 → pending 상태로 대기
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
[시나리오 2: 은행 SMS가 먼저 도착]
|
||||||
|
|
||||||
|
은행 SMS → Email Worker 수신
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────┐
|
||||||
|
│ SMS 파싱 │ ← 입금자명, 금액, 은행 추출
|
||||||
|
│ (하나/KB/신한) │
|
||||||
|
└──────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────┐
|
||||||
|
│ deposit_transactions│ ← 대기중 입금 확인
|
||||||
|
└──────────────────┘
|
||||||
|
│
|
||||||
|
├── 매칭 발견 → 즉시 confirmed + 잔액 증가
|
||||||
|
│
|
||||||
|
└── 매칭 없음 → bank_notifications에 저장 (나중에 매칭)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 지원 기능
|
||||||
|
|
||||||
|
| 기능 | 설명 | 권한 |
|
||||||
|
|------|------|------|
|
||||||
|
| `잔액 조회` | 현재 예치금 잔액 확인 | 모든 사용자 |
|
||||||
|
| `계좌 안내` | 입금 계좌 정보 표시 | 모든 사용자 |
|
||||||
|
| `입금 신고` | 입금자명 + 금액으로 충전 요청 | 모든 사용자 |
|
||||||
|
| `거래 내역` | 최근 거래 내역 조회 | 모든 사용자 |
|
||||||
|
| `입금 취소` | 대기중 입금 취소 | 모든 사용자 |
|
||||||
|
| `대기 목록` | 미처리 입금 목록 조회 | 관리자 전용 |
|
||||||
|
| `수동 확인` | 입금 수동 확정 처리 | 관리자 전용 |
|
||||||
|
| `입금 거절` | 입금 요청 거절 | 관리자 전용 |
|
||||||
|
|
||||||
|
### Email Worker 설정
|
||||||
|
|
||||||
|
Cloudflare Email Routing으로 SMS를 메일로 전달받아 파싱합니다.
|
||||||
|
|
||||||
|
1. **Dashboard 설정**: Email > Email Routing > Routes
|
||||||
|
2. **라우팅**: `deposit@your-domain.com` → Worker: `telegram-summary-bot`
|
||||||
|
|
||||||
|
지원 은행 SMS 패턴:
|
||||||
|
- 하나은행: `[하나은행] 01/16 14:30 입금 50,000원 홍길동 잔액 1,234,567원`
|
||||||
|
- KB국민: `[KB] 입금 50,000원 01/16 14:30 홍길동`
|
||||||
|
- 신한: `[신한] 01/16 입금 50,000원 홍길동`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 프로젝트 구조
|
## 프로젝트 구조
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -194,6 +277,9 @@ database_id = "c285bb5b-888b-405d-b36f-475ae5aed20e"
|
|||||||
- `users` - 사용자 정보
|
- `users` - 사용자 정보
|
||||||
- `message_buffer` - 메시지 임시 저장
|
- `message_buffer` - 메시지 임시 저장
|
||||||
- `summaries` - 프로필 저장
|
- `summaries` - 프로필 저장
|
||||||
|
- `user_deposits` - 예치금 계정
|
||||||
|
- `deposit_transactions` - 예치금 거래 내역
|
||||||
|
- `bank_notifications` - 은행 입금 알림 (SMS 파싱)
|
||||||
|
|
||||||
### 3. Secrets 설정
|
### 3. Secrets 설정
|
||||||
|
|
||||||
@@ -305,6 +391,7 @@ MAX_SUMMARIES_PER_USER = "3"
|
|||||||
N8N_WEBHOOK_URL = "https://n8n.anvil.it.com"
|
N8N_WEBHOOK_URL = "https://n8n.anvil.it.com"
|
||||||
DOMAIN_AGENT_ID = "asst_MzPFKoqt7V4w6bc0UwcXU4ob"
|
DOMAIN_AGENT_ID = "asst_MzPFKoqt7V4w6bc0UwcXU4ob"
|
||||||
DOMAIN_OWNER_ID = "821596605"
|
DOMAIN_OWNER_ID = "821596605"
|
||||||
|
DEPOSIT_ADMIN_ID = "821596605"
|
||||||
|
|
||||||
[[d1_databases]]
|
[[d1_databases]]
|
||||||
binding = "DB"
|
binding = "DB"
|
||||||
|
|||||||
42
schema.sql
42
schema.sql
@@ -45,9 +45,51 @@ CREATE TABLE IF NOT EXISTS user_domains (
|
|||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- 예치금 계정 테이블
|
||||||
|
CREATE TABLE IF NOT EXISTS user_deposits (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL UNIQUE,
|
||||||
|
balance INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 은행 입금 알림 테이블 (SMS → 메일 → 파싱)
|
||||||
|
CREATE TABLE IF NOT EXISTS bank_notifications (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
bank_name TEXT,
|
||||||
|
depositor_name TEXT NOT NULL,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
balance_after INTEGER,
|
||||||
|
transaction_time DATETIME,
|
||||||
|
raw_message TEXT,
|
||||||
|
matched_transaction_id INTEGER,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (matched_transaction_id) REFERENCES deposit_transactions(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 예치금 거래 내역 테이블
|
||||||
|
CREATE TABLE IF NOT EXISTS deposit_transactions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('deposit', 'withdrawal', 'refund')),
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'confirmed', 'rejected', 'cancelled')),
|
||||||
|
depositor_name TEXT,
|
||||||
|
description TEXT,
|
||||||
|
confirmed_at DATETIME,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
|
||||||
-- 인덱스
|
-- 인덱스
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_domains_user ON user_domains(user_id);
|
CREATE INDEX IF NOT EXISTS idx_user_domains_user ON user_domains(user_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_domains_domain ON user_domains(domain);
|
CREATE INDEX IF NOT EXISTS idx_user_domains_domain ON user_domains(domain);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_deposits_user ON user_deposits(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_transactions_user ON deposit_transactions(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_transactions_status ON deposit_transactions(status, created_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bank_notifications_match ON bank_notifications(depositor_name, amount, matched_transaction_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_buffer_user ON message_buffer(user_id);
|
CREATE INDEX IF NOT EXISTS idx_buffer_user ON message_buffer(user_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_buffer_chat ON message_buffer(user_id, chat_id);
|
CREATE INDEX IF NOT EXISTS idx_buffer_chat ON message_buffer(user_id, chat_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_summary_user ON summaries(user_id, chat_id);
|
CREATE INDEX IF NOT EXISTS idx_summary_user ON summaries(user_id, chat_id);
|
||||||
|
|||||||
193
src/index.ts
193
src/index.ts
@@ -1,4 +1,4 @@
|
|||||||
import { Env, TelegramUpdate } from './types';
|
import { Env, TelegramUpdate, EmailMessage, BankNotification } from './types';
|
||||||
import { validateWebhookRequest, checkRateLimit } from './security';
|
import { validateWebhookRequest, checkRateLimit } from './security';
|
||||||
import { sendMessage, sendMessageWithKeyboard, setWebhook, getWebhookInfo, sendChatAction } from './telegram';
|
import { sendMessage, sendMessageWithKeyboard, setWebhook, getWebhookInfo, sendChatAction } from './telegram';
|
||||||
import {
|
import {
|
||||||
@@ -207,4 +207,195 @@ Documentation: https://github.com/your-repo
|
|||||||
|
|
||||||
return new Response('Not Found', { status: 404 });
|
return new Response('Not Found', { status: 404 });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Email 핸들러 (SMS → 메일 → 파싱)
|
||||||
|
async email(message: EmailMessage, env: Env): Promise<void> {
|
||||||
|
try {
|
||||||
|
// 이메일 본문 읽기
|
||||||
|
const rawEmail = await new Response(message.raw).text();
|
||||||
|
console.log('[Email] 수신:', message.from, 'Size:', message.rawSize);
|
||||||
|
|
||||||
|
// SMS 내용 파싱
|
||||||
|
const notification = parseBankSMS(rawEmail);
|
||||||
|
if (!notification) {
|
||||||
|
console.log('[Email] 은행 SMS 파싱 실패:', rawEmail.slice(0, 200));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Email] 파싱 결과:', notification);
|
||||||
|
|
||||||
|
// DB에 저장
|
||||||
|
const insertResult = await env.DB.prepare(
|
||||||
|
`INSERT INTO bank_notifications (bank_name, depositor_name, amount, balance_after, transaction_time, raw_message)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)`
|
||||||
|
).bind(
|
||||||
|
notification.bankName,
|
||||||
|
notification.depositorName,
|
||||||
|
notification.amount,
|
||||||
|
notification.balanceAfter || null,
|
||||||
|
notification.transactionTime?.toISOString() || null,
|
||||||
|
notification.rawMessage
|
||||||
|
).run();
|
||||||
|
|
||||||
|
const notificationId = insertResult.meta.last_row_id;
|
||||||
|
console.log('[Email] 알림 저장 완료, ID:', notificationId);
|
||||||
|
|
||||||
|
// 자동 매칭 시도
|
||||||
|
const matched = await tryAutoMatch(env.DB, notificationId, notification);
|
||||||
|
|
||||||
|
// 관리자에게 알림
|
||||||
|
if (env.BOT_TOKEN && env.DEPOSIT_ADMIN_ID) {
|
||||||
|
const statusMsg = matched
|
||||||
|
? `✅ 자동 매칭 완료! (거래 #${matched.transactionId})`
|
||||||
|
: '⏳ 매칭 대기 중 (사용자 입금 신고 필요)';
|
||||||
|
|
||||||
|
await sendMessage(
|
||||||
|
env.BOT_TOKEN,
|
||||||
|
parseInt(env.DEPOSIT_ADMIN_ID),
|
||||||
|
`🏦 <b>입금 알림</b>\n\n` +
|
||||||
|
`은행: ${notification.bankName}\n` +
|
||||||
|
`입금자: ${notification.depositorName}\n` +
|
||||||
|
`금액: ${notification.amount.toLocaleString()}원\n` +
|
||||||
|
`${notification.balanceAfter ? `잔액: ${notification.balanceAfter.toLocaleString()}원\n` : ''}` +
|
||||||
|
`\n${statusMsg}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Email] 처리 오류:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 은행 SMS 파싱 함수
|
||||||
|
function parseBankSMS(content: string): BankNotification | null {
|
||||||
|
// 이메일에서 SMS 본문 추출 (여러 줄에 걸쳐 있을 수 있음)
|
||||||
|
const text = content.replace(/\r\n/g, '\n').replace(/=\n/g, '');
|
||||||
|
|
||||||
|
// 하나은행 패턴: [하나은행] 01/16 14:30 입금 50,000원 홍길동 잔액 1,234,567원
|
||||||
|
const hanaPattern = /\[하나은행\]\s*(\d{1,2}\/\d{1,2})\s*(\d{1,2}:\d{2})?\s*입금\s*([\d,]+)원\s*(\S+?)(?:\s+잔액\s*([\d,]+)원)?/;
|
||||||
|
const hanaMatch = text.match(hanaPattern);
|
||||||
|
if (hanaMatch) {
|
||||||
|
const [, date, time, amountStr, depositor, balanceStr] = hanaMatch;
|
||||||
|
return {
|
||||||
|
bankName: '하나은행',
|
||||||
|
depositorName: depositor,
|
||||||
|
amount: parseInt(amountStr.replace(/,/g, '')),
|
||||||
|
balanceAfter: balanceStr ? parseInt(balanceStr.replace(/,/g, '')) : undefined,
|
||||||
|
transactionTime: parseDateTime(date, time),
|
||||||
|
rawMessage: text.slice(0, 500),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// KB국민은행 패턴: [KB] 입금 50,000원 01/16 14:30 홍길동
|
||||||
|
const kbPattern = /\[KB\]\s*입금\s*([\d,]+)원\s*(\d{1,2}\/\d{1,2})?\s*(\d{1,2}:\d{2})?\s*(\S+)/;
|
||||||
|
const kbMatch = text.match(kbPattern);
|
||||||
|
if (kbMatch) {
|
||||||
|
const [, amountStr, date, time, depositor] = kbMatch;
|
||||||
|
return {
|
||||||
|
bankName: 'KB국민은행',
|
||||||
|
depositorName: depositor,
|
||||||
|
amount: parseInt(amountStr.replace(/,/g, '')),
|
||||||
|
transactionTime: date ? parseDateTime(date, time) : undefined,
|
||||||
|
rawMessage: text.slice(0, 500),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 신한은행 패턴: [신한] 01/16 입금 50,000원 홍길동
|
||||||
|
const shinhanPattern = /\[신한\]\s*(\d{1,2}\/\d{1,2})?\s*입금\s*([\d,]+)원\s*(\S+)/;
|
||||||
|
const shinhanMatch = text.match(shinhanPattern);
|
||||||
|
if (shinhanMatch) {
|
||||||
|
const [, date, amountStr, depositor] = shinhanMatch;
|
||||||
|
return {
|
||||||
|
bankName: '신한은행',
|
||||||
|
depositorName: depositor,
|
||||||
|
amount: parseInt(amountStr.replace(/,/g, '')),
|
||||||
|
transactionTime: date ? parseDateTime(date) : undefined,
|
||||||
|
rawMessage: text.slice(0, 500),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 일반 입금 패턴: 입금 50,000원 홍길동 또는 홍길동 50,000원 입금
|
||||||
|
const genericPattern1 = /입금\s*([\d,]+)원?\s*(\S{2,10})/;
|
||||||
|
const genericPattern2 = /(\S{2,10})\s*([\d,]+)원?\s*입금/;
|
||||||
|
|
||||||
|
const genericMatch1 = text.match(genericPattern1);
|
||||||
|
if (genericMatch1) {
|
||||||
|
return {
|
||||||
|
bankName: '알수없음',
|
||||||
|
depositorName: genericMatch1[2],
|
||||||
|
amount: parseInt(genericMatch1[1].replace(/,/g, '')),
|
||||||
|
rawMessage: text.slice(0, 500),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const genericMatch2 = text.match(genericPattern2);
|
||||||
|
if (genericMatch2) {
|
||||||
|
return {
|
||||||
|
bankName: '알수없음',
|
||||||
|
depositorName: genericMatch2[1],
|
||||||
|
amount: parseInt(genericMatch2[2].replace(/,/g, '')),
|
||||||
|
rawMessage: text.slice(0, 500),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 날짜/시간 파싱
|
||||||
|
function parseDateTime(dateStr: string, timeStr?: string): Date {
|
||||||
|
const now = new Date();
|
||||||
|
const [month, day] = dateStr.split('/').map(Number);
|
||||||
|
const year = now.getFullYear();
|
||||||
|
|
||||||
|
let hours = 0, minutes = 0;
|
||||||
|
if (timeStr) {
|
||||||
|
[hours, minutes] = timeStr.split(':').map(Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(year, month - 1, day, hours, minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 자동 매칭 시도
|
||||||
|
async function tryAutoMatch(
|
||||||
|
db: D1Database,
|
||||||
|
notificationId: number,
|
||||||
|
notification: BankNotification
|
||||||
|
): Promise<{ transactionId: number } | null> {
|
||||||
|
// 매칭 조건: 입금자명 + 금액이 일치하는 pending 거래
|
||||||
|
const pendingTx = await db.prepare(
|
||||||
|
`SELECT dt.id, dt.user_id, dt.amount
|
||||||
|
FROM deposit_transactions dt
|
||||||
|
WHERE dt.status = 'pending'
|
||||||
|
AND dt.type = 'deposit'
|
||||||
|
AND dt.depositor_name = ?
|
||||||
|
AND dt.amount = ?
|
||||||
|
ORDER BY dt.created_at ASC
|
||||||
|
LIMIT 1`
|
||||||
|
).bind(notification.depositorName, notification.amount).first<{
|
||||||
|
id: number;
|
||||||
|
user_id: number;
|
||||||
|
amount: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
if (!pendingTx) {
|
||||||
|
console.log('[AutoMatch] 매칭되는 pending 거래 없음');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[AutoMatch] 매칭 발견:', pendingTx);
|
||||||
|
|
||||||
|
// 트랜잭션: 거래 확정 + 잔액 증가 + 알림 매칭 업데이트
|
||||||
|
await db.batch([
|
||||||
|
db.prepare(
|
||||||
|
"UPDATE deposit_transactions SET status = 'confirmed', confirmed_at = CURRENT_TIMESTAMP WHERE id = ?"
|
||||||
|
).bind(pendingTx.id),
|
||||||
|
db.prepare(
|
||||||
|
'UPDATE user_deposits SET balance = balance + ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ?'
|
||||||
|
).bind(pendingTx.amount, pendingTx.user_id),
|
||||||
|
db.prepare(
|
||||||
|
'UPDATE bank_notifications SET matched_transaction_id = ? WHERE id = ?'
|
||||||
|
).bind(pendingTx.id, notificationId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { transactionId: pendingTx.id };
|
||||||
|
}
|
||||||
|
|||||||
@@ -131,6 +131,36 @@ const tools = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'manage_deposit',
|
||||||
|
description: '예치금을 관리합니다. 잔액 조회, 입금 신고(충전 요청), 거래 내역 조회, 입금 확인(관리자) 등을 수행할 수 있습니다. 사용자가 "입금했어", "송금했어", "충전하고 싶어", "10000원 입금" 등의 말을 하면 이 도구를 사용합니다.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
action: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['balance', 'request_deposit', 'transactions', 'cancel', 'pending_list', 'confirm', 'reject', 'account_info'],
|
||||||
|
description: '수행할 작업: balance(잔액조회), request_deposit(입금신고/충전요청-입금했다고 말할때도 사용), account_info(입금계좌안내), transactions(거래내역), cancel(입금취소), pending_list(대기목록-관리자), confirm(입금확인-관리자), reject(입금거절-관리자)',
|
||||||
|
},
|
||||||
|
amount: {
|
||||||
|
type: 'number',
|
||||||
|
description: '금액 (충전 요청 시 필수)',
|
||||||
|
},
|
||||||
|
depositor_name: {
|
||||||
|
type: 'string',
|
||||||
|
description: '입금자명 (충전 요청 시 필수)',
|
||||||
|
},
|
||||||
|
transaction_id: {
|
||||||
|
type: 'number',
|
||||||
|
description: '거래 ID (확인/거절/취소 시 필수)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['action'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Namecheap API 호출 (allowedDomains로 필터링)
|
// Namecheap API 호출 (allowedDomains로 필터링)
|
||||||
@@ -438,6 +468,291 @@ async function executeTool(name: string, args: Record<string, string>, env?: Env
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'manage_deposit': {
|
||||||
|
const action = args.action;
|
||||||
|
const amount = args.amount ? parseInt(args.amount) : 0;
|
||||||
|
const depositorName = args.depositor_name;
|
||||||
|
const transactionId = args.transaction_id ? parseInt(args.transaction_id) : 0;
|
||||||
|
|
||||||
|
if (!telegramUserId || !db) {
|
||||||
|
return '🚫 예치금 기능을 사용할 수 없습니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAdmin = telegramUserId === env?.DEPOSIT_ADMIN_ID;
|
||||||
|
|
||||||
|
// 사용자 조회 또는 생성
|
||||||
|
let user = await db.prepare(
|
||||||
|
'SELECT id FROM users WHERE telegram_id = ?'
|
||||||
|
).bind(telegramUserId).first<{ id: number }>();
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return '🚫 사용자 정보를 찾을 수 없습니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 예치금 계정 조회 또는 생성
|
||||||
|
let deposit = await db.prepare(
|
||||||
|
'SELECT id, balance FROM user_deposits WHERE user_id = ?'
|
||||||
|
).bind(user.id).first<{ id: number; balance: number }>();
|
||||||
|
|
||||||
|
if (!deposit) {
|
||||||
|
await db.prepare(
|
||||||
|
'INSERT INTO user_deposits (user_id, balance) VALUES (?, 0)'
|
||||||
|
).bind(user.id).run();
|
||||||
|
deposit = { id: 0, balance: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'balance': {
|
||||||
|
return `💰 현재 예치금 잔액: ${deposit.balance.toLocaleString()}원`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'account_info': {
|
||||||
|
return `🏦 <b>입금 계좌 안내</b>
|
||||||
|
|
||||||
|
은행: 하나은행
|
||||||
|
계좌: 427-910018-27104
|
||||||
|
예금주: 주식회사 아이언클래드
|
||||||
|
|
||||||
|
입금 후 입금자명과 금액을 알려주세요.
|
||||||
|
예: "홍길동 10000원 입금했어"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'request_deposit': {
|
||||||
|
if (!amount || amount <= 0) {
|
||||||
|
return '❌ 충전 금액을 입력해주세요. (예: 10000원 충전)';
|
||||||
|
}
|
||||||
|
if (!depositorName) {
|
||||||
|
return '❌ 입금자명을 입력해주세요. (예: 홍길동 10000원 입금)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 먼저 매칭되는 은행 알림이 있는지 확인
|
||||||
|
const bankNotification = await db.prepare(
|
||||||
|
`SELECT id, amount FROM bank_notifications
|
||||||
|
WHERE depositor_name = ? AND amount = ? AND matched_transaction_id IS NULL
|
||||||
|
ORDER BY created_at DESC LIMIT 1`
|
||||||
|
).bind(depositorName, amount).first<{ id: number; amount: number }>();
|
||||||
|
|
||||||
|
if (bankNotification) {
|
||||||
|
// 은행 알림이 이미 있으면 바로 확정 처리
|
||||||
|
const result = await db.prepare(
|
||||||
|
`INSERT INTO deposit_transactions (user_id, type, amount, status, depositor_name, description, confirmed_at)
|
||||||
|
VALUES (?, 'deposit', ?, 'confirmed', ?, '자동 매칭 확정', CURRENT_TIMESTAMP)`
|
||||||
|
).bind(user.id, amount, depositorName).run();
|
||||||
|
|
||||||
|
const txId = result.meta.last_row_id;
|
||||||
|
|
||||||
|
// 잔액 증가 + 알림 매칭 업데이트
|
||||||
|
await db.batch([
|
||||||
|
db.prepare(
|
||||||
|
'UPDATE user_deposits SET balance = balance + ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ?'
|
||||||
|
).bind(amount, user.id),
|
||||||
|
db.prepare(
|
||||||
|
'UPDATE bank_notifications SET matched_transaction_id = ? WHERE id = ?'
|
||||||
|
).bind(txId, bankNotification.id),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 업데이트된 잔액 조회
|
||||||
|
const newDeposit = await db.prepare(
|
||||||
|
'SELECT balance FROM user_deposits WHERE user_id = ?'
|
||||||
|
).bind(user.id).first<{ balance: number }>();
|
||||||
|
|
||||||
|
return `✅ 입금이 확인되었습니다!
|
||||||
|
|
||||||
|
📋 <b>거래 번호:</b> #${txId}
|
||||||
|
💵 <b>입금액:</b> ${amount.toLocaleString()}원
|
||||||
|
👤 <b>입금자:</b> ${depositorName}
|
||||||
|
💰 <b>현재 잔액:</b> ${newDeposit?.balance.toLocaleString() || 0}원
|
||||||
|
|
||||||
|
🎉 은행 알림과 자동 매칭되어 즉시 충전되었습니다.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 은행 알림이 없으면 pending 거래 생성
|
||||||
|
const result = await db.prepare(
|
||||||
|
`INSERT INTO deposit_transactions (user_id, type, amount, status, depositor_name, description)
|
||||||
|
VALUES (?, 'deposit', ?, 'pending', ?, '사용자 입금 요청')`
|
||||||
|
).bind(user.id, amount, depositorName).run();
|
||||||
|
|
||||||
|
const txId = result.meta.last_row_id;
|
||||||
|
|
||||||
|
return `✅ 입금 요청이 등록되었습니다.
|
||||||
|
|
||||||
|
📋 <b>요청 번호:</b> #${txId}
|
||||||
|
💵 <b>금액:</b> ${amount.toLocaleString()}원
|
||||||
|
👤 <b>입금자명:</b> ${depositorName}
|
||||||
|
|
||||||
|
🏦 <b>입금 계좌 안내</b>
|
||||||
|
은행: 하나은행
|
||||||
|
계좌: 427-910018-27104
|
||||||
|
예금주: 주식회사 아이언클래드
|
||||||
|
|
||||||
|
⏳ 은행 입금 확인 후 자동으로 충전됩니다.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'transactions': {
|
||||||
|
const transactions = await db.prepare(
|
||||||
|
`SELECT id, type, amount, status, depositor_name, created_at
|
||||||
|
FROM deposit_transactions
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 10`
|
||||||
|
).bind(user.id).all<{
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
amount: number;
|
||||||
|
status: string;
|
||||||
|
depositor_name: string;
|
||||||
|
created_at: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
if (!transactions.results?.length) {
|
||||||
|
return '📜 거래 내역이 없습니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusEmoji: Record<string, string> = {
|
||||||
|
pending: '⏳',
|
||||||
|
confirmed: '✅',
|
||||||
|
rejected: '❌',
|
||||||
|
cancelled: '🚫',
|
||||||
|
};
|
||||||
|
const typeLabel: Record<string, string> = {
|
||||||
|
deposit: '입금',
|
||||||
|
withdrawal: '출금',
|
||||||
|
refund: '환불',
|
||||||
|
};
|
||||||
|
|
||||||
|
const lines = transactions.results.map(tx => {
|
||||||
|
const emoji = statusEmoji[tx.status] || '❓';
|
||||||
|
const type = typeLabel[tx.type] || tx.type;
|
||||||
|
const date = new Date(tx.created_at).toLocaleDateString('ko-KR');
|
||||||
|
return `${emoji} #${tx.id} | ${type} ${tx.amount.toLocaleString()}원 | ${date}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return `📜 <b>최근 거래 내역</b>\n\n${lines.join('\n')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'cancel': {
|
||||||
|
if (!transactionId) {
|
||||||
|
return '❌ 취소할 거래 번호를 입력해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const tx = await db.prepare(
|
||||||
|
'SELECT id, status, user_id FROM deposit_transactions WHERE id = ?'
|
||||||
|
).bind(transactionId).first<{ id: number; status: string; user_id: number }>();
|
||||||
|
|
||||||
|
if (!tx) {
|
||||||
|
return '❌ 거래를 찾을 수 없습니다.';
|
||||||
|
}
|
||||||
|
if (tx.user_id !== user.id && !isAdmin) {
|
||||||
|
return '🚫 본인의 거래만 취소할 수 있습니다.';
|
||||||
|
}
|
||||||
|
if (tx.status !== 'pending') {
|
||||||
|
return '❌ 대기 중인 거래만 취소할 수 있습니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.prepare(
|
||||||
|
"UPDATE deposit_transactions SET status = 'cancelled' WHERE id = ?"
|
||||||
|
).bind(transactionId).run();
|
||||||
|
|
||||||
|
return `✅ 거래 #${transactionId}이 취소되었습니다.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 관리자 전용 기능
|
||||||
|
case 'pending_list': {
|
||||||
|
if (!isAdmin) {
|
||||||
|
return '🚫 관리자 권한이 필요합니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = await db.prepare(
|
||||||
|
`SELECT dt.id, dt.amount, dt.depositor_name, dt.created_at, u.telegram_id, u.username
|
||||||
|
FROM deposit_transactions dt
|
||||||
|
JOIN users u ON dt.user_id = u.id
|
||||||
|
WHERE dt.status = 'pending' AND dt.type = 'deposit'
|
||||||
|
ORDER BY dt.created_at ASC`
|
||||||
|
).all<{
|
||||||
|
id: number;
|
||||||
|
amount: number;
|
||||||
|
depositor_name: string;
|
||||||
|
created_at: string;
|
||||||
|
telegram_id: string;
|
||||||
|
username: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
if (!pending.results?.length) {
|
||||||
|
return '✅ 대기 중인 입금 요청이 없습니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = pending.results.map(p => {
|
||||||
|
const date = new Date(p.created_at).toLocaleString('ko-KR');
|
||||||
|
const user = p.username || p.telegram_id;
|
||||||
|
return `#${p.id} | ${p.depositor_name} | ${p.amount.toLocaleString()}원 | @${user} | ${date}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return `⏳ <b>대기 중인 입금 요청</b>\n\n${lines.join('\n')}\n\n입금 확인: "입금 확인 #번호"\n입금 거절: "입금 거절 #번호"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'confirm': {
|
||||||
|
if (!isAdmin) {
|
||||||
|
return '🚫 관리자 권한이 필요합니다.';
|
||||||
|
}
|
||||||
|
if (!transactionId) {
|
||||||
|
return '❌ 확인할 거래 번호를 입력해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const tx = await db.prepare(
|
||||||
|
'SELECT id, user_id, amount, status FROM deposit_transactions WHERE id = ?'
|
||||||
|
).bind(transactionId).first<{ id: number; user_id: number; amount: number; status: string }>();
|
||||||
|
|
||||||
|
if (!tx) {
|
||||||
|
return '❌ 거래를 찾을 수 없습니다.';
|
||||||
|
}
|
||||||
|
if (tx.status !== 'pending') {
|
||||||
|
return '❌ 대기 중인 거래만 확인할 수 있습니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 트랜잭션: 상태 변경 + 잔액 증가
|
||||||
|
await db.batch([
|
||||||
|
db.prepare(
|
||||||
|
"UPDATE deposit_transactions SET status = 'confirmed', confirmed_at = CURRENT_TIMESTAMP WHERE id = ?"
|
||||||
|
).bind(transactionId),
|
||||||
|
db.prepare(
|
||||||
|
'UPDATE user_deposits SET balance = balance + ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ?'
|
||||||
|
).bind(tx.amount, tx.user_id),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return `✅ 입금 확인 완료!\n\n거래 #${transactionId}\n금액: ${tx.amount.toLocaleString()}원`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'reject': {
|
||||||
|
if (!isAdmin) {
|
||||||
|
return '🚫 관리자 권한이 필요합니다.';
|
||||||
|
}
|
||||||
|
if (!transactionId) {
|
||||||
|
return '❌ 거절할 거래 번호를 입력해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const tx = await db.prepare(
|
||||||
|
'SELECT id, status FROM deposit_transactions WHERE id = ?'
|
||||||
|
).bind(transactionId).first<{ id: number; status: string }>();
|
||||||
|
|
||||||
|
if (!tx) {
|
||||||
|
return '❌ 거래를 찾을 수 없습니다.';
|
||||||
|
}
|
||||||
|
if (tx.status !== 'pending') {
|
||||||
|
return '❌ 대기 중인 거래만 거절할 수 있습니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.prepare(
|
||||||
|
"UPDATE deposit_transactions SET status = 'rejected' WHERE id = ?"
|
||||||
|
).bind(transactionId).run();
|
||||||
|
|
||||||
|
return `❌ 입금 거절 완료 (거래 #${transactionId})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return '❓ 알 수 없는 작업입니다. 잔액 조회, 충전, 거래 내역 등을 요청해주세요.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case 'manage_domain': {
|
case 'manage_domain': {
|
||||||
const query = args.query;
|
const query = args.query;
|
||||||
console.log('[manage_domain] 시작:', { query, telegramUserId, hasDb: !!db });
|
console.log('[manage_domain] 시작:', { query, telegramUserId, hasDb: !!db });
|
||||||
|
|||||||
22
src/types.ts
22
src/types.ts
@@ -10,6 +10,7 @@ export interface Env {
|
|||||||
DOMAIN_AGENT_ID?: string;
|
DOMAIN_AGENT_ID?: string;
|
||||||
NAMECHEAP_API_KEY?: string;
|
NAMECHEAP_API_KEY?: string;
|
||||||
DOMAIN_OWNER_ID?: string;
|
DOMAIN_OWNER_ID?: string;
|
||||||
|
DEPOSIT_ADMIN_ID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IntentAnalysis {
|
export interface IntentAnalysis {
|
||||||
@@ -69,3 +70,24 @@ export interface ConversationContext {
|
|||||||
recentMessages: BufferedMessage[];
|
recentMessages: BufferedMessage[];
|
||||||
totalMessages: number;
|
totalMessages: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cloudflare Email Workers 타입
|
||||||
|
export interface EmailMessage {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
headers: Headers;
|
||||||
|
raw: ReadableStream;
|
||||||
|
rawSize: number;
|
||||||
|
setReject(reason: string): void;
|
||||||
|
forward(to: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 은행 알림 파싱 결과
|
||||||
|
export interface BankNotification {
|
||||||
|
bankName: string;
|
||||||
|
depositorName: string;
|
||||||
|
amount: number;
|
||||||
|
balanceAfter?: number;
|
||||||
|
transactionTime?: Date;
|
||||||
|
rawMessage: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,12 +11,18 @@ MAX_SUMMARIES_PER_USER = "3"
|
|||||||
N8N_WEBHOOK_URL = "https://n8n.anvil.it.com"
|
N8N_WEBHOOK_URL = "https://n8n.anvil.it.com"
|
||||||
DOMAIN_AGENT_ID = "asst_MzPFKoqt7V4w6bc0UwcXU4ob"
|
DOMAIN_AGENT_ID = "asst_MzPFKoqt7V4w6bc0UwcXU4ob"
|
||||||
DOMAIN_OWNER_ID = "821596605"
|
DOMAIN_OWNER_ID = "821596605"
|
||||||
|
DEPOSIT_ADMIN_ID = "821596605"
|
||||||
|
|
||||||
[[d1_databases]]
|
[[d1_databases]]
|
||||||
binding = "DB"
|
binding = "DB"
|
||||||
database_name = "telegram-conversations"
|
database_name = "telegram-conversations"
|
||||||
database_id = "c285bb5b-888b-405d-b36f-475ae5aed20e"
|
database_id = "c285bb5b-888b-405d-b36f-475ae5aed20e"
|
||||||
|
|
||||||
|
# Email Worker 설정 (SMS → 메일 수신)
|
||||||
|
# Cloudflare Dashboard에서 Email Routing 설정 필요:
|
||||||
|
# 1. Email > Email Routing > Routes
|
||||||
|
# 2. deposit@your-domain.com → Worker: telegram-summary-bot
|
||||||
|
|
||||||
# Secrets (wrangler secret put 으로 설정):
|
# Secrets (wrangler secret put 으로 설정):
|
||||||
# - BOT_TOKEN: Telegram Bot Token
|
# - BOT_TOKEN: Telegram Bot Token
|
||||||
# - WEBHOOK_SECRET: Webhook 검증용 시크릿
|
# - WEBHOOK_SECRET: Webhook 검증용 시크릿
|
||||||
|
|||||||
Reference in New Issue
Block a user