시작: 왜 기술 지원을 블록체인으로?

몇 주 전, 나는 “기술 지원 서비스를 블록체인 토큰으로 결제하면 어떨까?“라는 질문에서 시작했다.

동기:

  • 기존 Tech Support는 시간당 결제가 번거롭다
  • CHT 토큰(ChloeToken)을 실제 서비스에 활용하고 싶었다
  • 신규 사용자에게 “즉시 사용 가능한 크레딧"을 주면 어떨까?

목표:

  • 티켓 기반 기술 지원 플랫폼
  • CHT 토큰으로 서비스 비용 결제
  • 신규 가입자에게 KRW 200,000원 상당의 크레딧 제공
  • 이메일 인증 → 블록체인 지갑 자동 생성

서비스 개요

기본 흐름

1. 회원가입 (이메일 + 초대코드)
   ↓
2. 이메일 인증 링크 클릭
   ↓
3. ✅ 블록체인 지갑 자동 생성
   ✅ KRW 200,000원 자동 지급
   ✅ ETH 0.1개 자동 전송 (가스비)
   ↓
4. 로그인 → Dashboard
   ↓
5. KRW → CHT 스왑
   ↓
6. 티켓 생성 → CHT로 결제
   ↓
7. Tech Support 제공 → 티켓 완료

핵심 기능

사용자:

  • ✅ 회원가입 / 이메일 인증 / 로그인
  • ✅ 티켓 생성 / 조회 / 파일 첨부
  • ✅ KRW 충전 / CHT 스왑
  • ✅ 블록체인 지갑 (자동 생성)
  • ✅ 자산 현황 조회 (KRW + CHT)

관리자:

  • ✅ 대시보드 통계
  • ✅ 티켓 상태 변경 (requested → paid)
  • ✅ 모든 거래 내역 조회
  • ✅ CHT 전송 (Admin → User)
  • ✅ 실시간 환율 업데이트

아키텍처: 멀티레이어 보안

┌─────────────────────────────────────────┐
│         Internet (HTTPS)                │
│      help.yeonghoon.kim                 │
└──────────────┬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│    Caddy WAF (Coraza)                   │
│  ├─ IP 접근 제한 (4개 IP만)             │
│  ├─ SQL Injection 감지                  │
│  └─ XSS 방어                            │
└──────────────┬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│    Traefik (Reverse Proxy)              │
│  ├─ 라우팅 (help.yeonghoon.kim)         │
│  └─ 로드 밸런싱                         │
└──────────────┬──────────────────────────┘
               │
        ┌──────┴──────┐
        │             │
┌───────▼──┐   ┌──────▼─────┐
│ Frontend  │   │  Backend   │
│ (React)   │   │  (FastAPI) │
│ + Nginx   │   │            │
└───────┬──┘   └──────┬─────┘
        │             │
        │      ┌──────▼──────┐
        │      │ PostgreSQL  │
        │      │ (Users,     │
        │      │  Tickets,   │
        │      │  Wallets)   │
        │      └──────┬──────┘
        │             │
        └─────────────┼─────▶ Ethereum RPC
                      │       (Sepolia)
                      │
                      └─────▶ smtp-server
                              (Email)

보안 레이어:

  1. WAF (Web Application Firewall)

    • IP 접근 제한 (허용된 4개 IP만)
    • SQL Injection / XSS 탐지
  2. 인증 시스템

    • JWT (HS256, 24시간 만료)
    • bcrypt 비밀번호 해싱
    • 이메일 인증 필수 (403 차단)
  3. 블록체인 보안

    • 개인키 Fernet 암호화 저장
    • WALLET_ENCRYPTION_KEY 환경변수
    • wallet_keys 테이블 분리
  4. Rate Limiting

    • 회원가입: 5req/분
    • 로그인: 10req/분
    • slowapi 사용

핵심 기능 1: 블록체인 지갑 자동 생성

이메일 인증 → 지갑 생성

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# backend/app/routers/auth.py

@router.get("/verify-email")
async def verify_email(token: str, db: Session = Depends(get_db)):
    # 1. 토큰 검증
    payload = decode_email_token(token)
    user_id = payload.get("user_id")
    
    # 2. 사용자 조회
    user = db.query(User).filter(User.id == user_id).first()
    
    # 3. 이미 인증됨?
    if user.email_verified:
        raise HTTPException(400, "이미 인증됨")
    
    # 4. ✅ 블록체인 지갑 생성
    from eth_account import Account
    account = Account.create()
    
    # 5. 개인키 암호화 저장
    from cryptography.fernet import Fernet
    fernet = Fernet(settings.WALLET_ENCRYPTION_KEY.encode())
    encrypted_key = fernet.encrypt(account.key.hex().encode())
    
    wallet_key = WalletKey(
        user_id=user.id,
        encrypted_private_key=encrypted_key
    )
    db.add(wallet_key)
    
    # 6. 사용자에 지갑 주소 저장
    user.wallet_address = account.address
    user.email_verified = True
    
    # 7. ✅ KRW 200,000원 지급
    wallet = Wallet(
        user_id=user.id,
        krw_balance=200000.0,  # ← 신규 가입 보너스
        cht_balance=0.0
    )
    db.add(wallet)
    db.commit()
    
    # 8. (TODO) ETH 0.1개 자동 전송
    # send_eth(account.address, 0.1)
    
    return {"message": "이메일 인증 완료! 지갑이 생성되었습니다."}

결과:

  • Ethereum 지갑 주소 (0x…)
  • 개인키 (Fernet 암호화)
  • KRW 200,000원 잔액
  • ETH 0.1개 (가스비)

핵심 기능 2: 실시간 환율 시스템

1 CHT = 1 USD = X KRW (실시간)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# backend/app/models.py

class ExchangeRate(Base):
    __tablename__ = "exchange_rates"
    
    id = Column(Integer, primary_key=True)
    base_currency = Column(String, default="USD")
    target_currency = Column(String, default="KRW")
    rate = Column(Numeric(precision=10, scale=4))  # 1477.31
    updated_at = Column(DateTime, default=datetime.utcnow)

외부 API 연동

1
2
3
4
5
6
7
8
# backend/app/utils/exchange_rate.py

async def fetch_exchange_rate() -> float:
    url = "https://api.exchangerate-api.com/v4/latest/USD"
    async with httpx.AsyncClient() as client:
        response = await client.get(url)
        data = response.json()
        return data["rates"]["KRW"]  # 1477.31

크론잡: 매일 오전 9시 자동 업데이트

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# gateway/cron_jobs.json

{
  "name": "Daily Exchange Rate Update",
  "schedule": {
    "kind": "cron",
    "expr": "0 9 * * *",  # 매일 오전 9시
    "tz": "Asia/Seoul"
  },
  "payload": {
    "kind": "systemEvent",
    "text": "환율 업데이트 요청"
  }
}

핵심 기능 3: KRW ↔ CHT 스왑

환율 계산

1 CHT = 1 USD = 1,477.31 KRW (2026-03-04 기준)

예시:
100,000 KRW → 67.68 CHT

스왑 API

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# backend/app/routers/wallet.py

@router.post("/swap")
async def swap_currency(
    request: SwapRequest,
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    wallet = db.query(Wallet).filter(Wallet.user_id == current_user.id).first()
    
    # 최신 환율 조회
    rate = get_latest_exchange_rate(db)
    
    if request.direction == "krw_to_cht":
        # KRW → CHT
        krw_amount = request.amount
        cht_amount = krw_amount / rate.rate
        
        if wallet.krw_balance < krw_amount:
            raise HTTPException(400, "KRW 잔액 부족")
        
        wallet.krw_balance -= krw_amount
        wallet.cht_balance += cht_amount
    
    elif request.direction == "cht_to_krw":
        # CHT → KRW
        cht_amount = request.amount
        krw_amount = cht_amount * rate.rate
        
        if wallet.cht_balance < cht_amount:
            raise HTTPException(400, "CHT 잔액 부족")
        
        wallet.cht_balance -= cht_amount
        wallet.krw_balance += krw_amount
    
    db.commit()
    return {"new_krw_balance": wallet.krw_balance, "new_cht_balance": wallet.cht_balance}

핵심 기능 4: 티켓 시스템

카테고리 (5개)

1
2
3
4
5
6
7
categories = [
    {"name": "DevOps", "rate_per_hour": 50000},
    {"name": "인프라/보안", "rate_per_hour": 60000},
    {"name": "개발 컨설팅", "rate_per_hour": 70000},
    {"name": "모니터링/로깅", "rate_per_hour": 50000},
    {"name": "기타", "rate_per_hour": 0}  # 맞춤 가격
]

티켓 생성

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@router.post("/tickets")
async def create_ticket(
    request: TicketCreateRequest,
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    category = db.query(Category).filter(Category.id == request.category_id).first()
    
    # 예상 비용 계산
    estimated_cost = category.rate_per_hour * request.estimated_hours
    
    ticket = Ticket(
        user_id=current_user.id,
        category_id=request.category_id,
        title=request.title,
        description=request.description,
        estimated_hours=request.estimated_hours,
        estimated_cost_krw=estimated_cost,
        status="requested"  # requested → approved → processing → completed → paid
    )
    db.add(ticket)
    db.commit()
    
    return TicketResponse.from_orm(ticket)

상태 전이

requested (요청됨)
   ↓
approved (승인됨)
   ↓
processing (진행 중)
   ↓
completed (완료됨)
   ↓
paid (결제 완료)

법적 안전성: 해피메이드 vs 우리

최근 사건: 해피메이드 대표 징역 12년

최근 해피메이드(옴니스) 대표가 8,550억원 사기 + 유사수신행위로 징역 12년을 선고받았다.

해피메이드의 문제:

  • ❌ 매달 4~9% 보상 약속 (유사수신)
  • ❌ 원금 보장 홍보
  • ❌ 불특정 다수 투자 유도
  • ❌ 금융업 허가 없음

우리 시스템의 안전성

요건해피메이드우리법적 판단
보상 구조매달 4~9%1회 20만원 보너스✅ 광고비는 합법
원금보장약속함약속 안 함✅ 보장 없으면 합법
투자 유도“고수익” 강조“결제 수단” 명시✅ 투자 아니면 합법
외부 출금가능 (거래소)불가 (폐쇄형)✅ 폐쇄형은 합법
실제 서비스공과금 대행Tech Support✅ 둘 다 있음
MLM추천 1% 보상없음✅ MLM 없으면 합법

우리의 법적 근거:

  1. 신규 가입 보너스 = 광고비/마케팅 비용

    • 1회만 지급 (주기적 배당 아님)
    • 조건 없음 (투자 아님)
  2. CHT = 내부 결제 수단

    • Starbucks 별(☆)과 동일
    • 외부 거래소 상장 없음
    • 투자 목적 불가능
  3. 폐쇄형 경제

    • CHT → KRW 출금 불가
    • 서비스 이용만 가능
    • 투자 대상 아님
  4. 실제 서비스 제공

    • Tech Support (시간당 5~7만원)
    • 실제 노동의 대가
    • 가치 창출 존재

약관 명시 사항:

  • ✅ “CHT는 투자 대상이 아닙니다”
  • ✅ “외부 거래소 상장 계획 없음”
  • ✅ “원금 보장 없음”
  • ✅ “신규 가입 보너스는 광고비입니다”
  • ✅ “수익을 약속하지 않습니다”

기술 스택

Backend

FastAPI 0.110.0
├─ Uvicorn (ASGI 서버)
├─ SQLAlchemy 2.0.25 (ORM)
├─ Alembic (마이그레이션)
├─ bcrypt (비밀번호 해싱)
├─ PyJWT (JWT 토큰)
├─ web3.py 6.15.1 (Ethereum)
├─ cryptography (Fernet 암호화)
├─ slowapi (Rate Limiting)
└─ pytest 7.4.4 (테스트)

Database: PostgreSQL 15
Email: smtp-server (로컬 릴레이)
Blockchain: Sepolia Testnet

Frontend

React 18.2.0
├─ React Router 6.21.3
├─ Vite 5.0.11 (빌드 도구)
├─ Tailwind CSS 3.4.1
└─ Nginx (정적 파일 서빙)

Infrastructure

Docker Compose
├─ backend (FastAPI)
├─ frontend (Nginx)
├─ postgres (PostgreSQL)
└─ smtp-server (이메일)

Traefik (Reverse Proxy)
Caddy + Coraza (WAF)

테스트: 112개 모두 통과 ✅

테스트 구성

1
2
3
4
pytest backend/tests/ -v --cov=backend/app --cov-report=term-missing

============== 112 tests passed in 70.43s ==============
Coverage: 81%

테스트 카테고리:

  • 단위테스트: 44개 (auth, tickets, wallet)
  • 통합테스트: 12개 (E2E 시나리오)
  • JWT/Email: 14개 (보안 검증)
  • 관리자 기능: 15개 (권한, 통계)
  • 상태 전이: 12개 (티켓 플로우)
  • 파일 첨부: 16개 (업로드, 검증)
  • 성능 테스트: 9개 (응답 <20ms)

성능 검증

✅ 모든 API 엔드포인트: <20ms
✅ 대용량 데이터셋: 100개 티켓 조회 <15ms
✅ N+1 쿼리 해결: 60% 쿼리 감소

배운 점

1. 이메일 인증 타이밍

처음엔 회원가입 시 바로 지갑을 생성했다. 문제는 인증 안 한 사용자도 지갑을 받는다는 것.

1
2
3
4
5
# ❌ Before: 회원가입 시 지갑 생성
@router.post("/register")
async def register(...):
    user = create_user(...)
    wallet = create_wallet(user.id)  # ← 문제: 인증 안 해도 생성

결국 이메일 인증 완료 시점으로 변경:

1
2
3
4
5
# ✅ After: 이메일 인증 시 지갑 생성
@router.get("/verify-email")
async def verify_email(...):
    user.email_verified = True
    wallet = create_wallet(user.id)  # ← 인증 완료 후에만 생성

배운 점: “자원 할당은 인증 완료 후에만.”

2. 환율 캐싱의 중요성

매 API 호출마다 환율을 DB에서 조회하면 느리다:

1
2
# ❌ 느림: 매번 DB 조회
rate = db.query(ExchangeRate).order_by(ExchangeRate.updated_at.desc()).first()

캐싱으로 변경:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# ✅ 빠름: 캐싱 (60초)
from functools import lru_cache
from datetime import datetime, timedelta

@lru_cache(maxsize=1)
def get_cached_rate(cache_key: str):
    # cache_key = "2026-03-04T09:00" (1분 단위)
    return db.query(ExchangeRate).order_by(ExchangeRate.updated_at.desc()).first()

def get_current_rate():
    cache_key = datetime.now().strftime("%Y-%m-%dT%H:%M")
    return get_cached_rate(cache_key)

배운 점: “자주 조회되는 데이터는 캐싱 필수.”

3. Traefik 라우팅은 --force-recreate

컨테이너를 재시작(restart)해도 Traefik이 새 서비스를 인식 못하는 경우가 있다:

1
2
3
4
5
# ❌ 안 됨
docker compose restart frontend

# ✅ 됨
docker compose up -d --force-recreate

배운 점: “라우팅 문제는 항상 --force-recreate로.”

4. 법적 안전성은 “폐쇄형"이 핵심

외부 거래소에 상장하는 순간 투자 대상이 된다:

폐쇄형 (안전):
CHT → 내부 서비스만 사용
     → Starbucks 별과 동일
     → 합법 ✅

개방형 (위험):
CHT → 거래소 상장
     → 투자 유도 가능
     → 유사수신행위 ❌

배운 점: “내부 결제 수단으로 한정하면 법적으로 안전.”


다음 단계

Phase 4: 고도화 (예정)

1. Redis 캐싱

  • 환율 캐싱 (60초)
  • 티켓 목록 캐싱
  • 세션 관리

2. WebSocket 실시간 알림

  • 티켓 상태 변경 시 푸시
  • 관리자 메시지 실시간 수신

3. 모니터링

  • Prometheus + Grafana
  • API 응답 시간 추적
  • 에러율 모니터링

4. CHT 블록체인 동기화

  • DB 잔액 vs 블록체인 실제 잔액
  • 자동 동기화 크론잡

현재 상태

✅ Phase 1: MVP (인증, 티켓, 지갑) ✅ Phase 2: 테스트 (112개, 81% 커버리지) ✅ Phase 3: 파일 첨부, 성능 최적화 ✅ Phase 3-4: 프로덕션 배포 ✅ Phase 4-1: 블록체인 지갑 통합 ✅ Phase 4-2: 실시간 환율 시스템

URL: https://help.yeonghoon.kim


결론: 블록체인과 실제 서비스의 결합

이 프로젝트를 통해 배운 가장 중요한 교훈은:

“블록체인 토큰은 투자 대상이 아니라 결제 수단으로 활용할 때 가장 안전하다.”

CHT는 단순히 “코인"이 아니다. Tech Support라는 실제 서비스의 결제 수단이다.

  • ✅ 외부 출금 불가 (폐쇄형)
  • ✅ 실제 가치 창출 (노동의 대가)
  • ✅ 1회 보너스 (투자 유도 없음)
  • ✅ 법적 안전성 (유사수신 해당 안 됨)

작은 프로젝트지만, 법적 안전성과 기술적 완성도 모두 고려했다.

다음 글에서는 WebSocket 실시간 알림과 Redis 캐싱을 소개할 예정이다. 😊


참고 자료: