CHT 토큰을 결제 수단으로 쓰는 기술 지원 플랫폼 작업.
프로젝트.
0030-tech-support
구성.
FastAPI
React
PostgreSQL
CHT / Sepolia
Docker Compose
Traefik
Caddy WAF
흐름.
회원가입
이메일 인증
지갑 생성
KRW 200,000 지급
KRW -> CHT 스왑
티켓 생성
CHT 결제
기술 지원 처리
구조.
0030-tech-support/
├── backend/
│ ├── app/
│ │ ├── main.py
│ │ ├── config.py
│ │ ├── database.py
│ │ ├── models.py
│ │ ├── schemas.py
│ │ ├── dependencies.py
│ │ ├── routers/
│ │ │ ├── auth.py
│ │ │ ├── tickets.py
│ │ │ ├── wallet.py
│ │ │ └── admin.py
│ │ └── utils/
│ │ └── jwt.py
│ ├── scripts/
│ │ ├── seed_categories.py
│ │ ├── seed_categories_hierarchy.py
│ │ └── seed_categories_v2.py
│ ├── tests/
│ ├── requirements.txt
│ └── Dockerfile
├── frontend/
│ ├── src/
│ │ ├── App.jsx
│ │ ├── pages/
│ │ ├── components/
│ │ ├── context/
│ │ └── api/
│ ├── nginx.conf
│ ├── package.json
│ └── Dockerfile
├── docker-compose.yml
└── scripts/send_eth.py
주요 API.
POST /api/v1/auth/register
POST /api/v1/auth/login
GET /api/v1/auth/me
GET /api/v1/categories
POST /api/v1/tickets
GET /api/v1/tickets
GET /api/v1/tickets/{id}
GET /api/v1/wallet/balance
POST /api/v1/wallet/charge-krw
POST /api/v1/wallet/swap
POST /api/v1/tickets/{id}/payment
GET /api/v1/admin/dashboard/stats
GET /api/v1/admin/tickets
PATCH /api/v1/admin/tickets/{id}/status
GET /api/v1/admin/transactions
이메일 인증 시 지갑 생성.
@router.get("/verify-email")
async def verify_email(token: str, db: Session = Depends(get_db)):
payload = decode_email_token(token)
user_id = payload.get("user_id")
user = db.query(User).filter(User.id == user_id).first()
if user.email_verified:
raise HTTPException(400, "이미 인증됨")
from eth_account import Account
account = Account.create()
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)
user.wallet_address = account.address
user.email_verified = True
wallet = Wallet(
user_id=user.id,
krw_balance=200000.0,
cht_balance=0.0
)
db.add(wallet)
db.commit()
return {"message": "이메일 인증 완료! 지갑이 생성되었습니다."}
환율 테이블.
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))
updated_at = Column(DateTime, default=datetime.utcnow)
외부 환율 API.
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"]
스왑 API.
@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_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_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}
티켓 카테고리.
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}
]
티켓 생성.
@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"
)
db.add(ticket)
db.commit()
return TicketResponse.from_orm(ticket)
상태.
requested
approved
processing
completed
paid
Docker 실행.
docker compose up --build -d
상태 확인.
docker compose ps
로그.
docker compose logs -f backend
테스트.
pytest backend/tests/ -v --cov=backend/app --cov-report=term-missing
결과.
112 tests passed
Coverage 81%
커밋 기록.
2026-03-05
CHT 기반 가격 체계 전환
2026-03-05
API 응답에 fixed_price_cht 필드 추가
2026-03-05
지갑에 ETH 잔액 추가
2026-03-05
에스크로 방식 복구: 티켓 생성 시 CHT 즉시 차감
2026-03-05
티켓 비용 계산 수정
2026-03-06
Header 컴포넌트 추가
2026-03-06
파스텔톤 배경 제거 및 이모지 삭제
2026-03-06
모든 파일에서 이모지 제거
처음엔 회원가입 시 지갑을 만들었는데 이메일 인증 완료 시점으로 변경.
자원 할당은 인증 이후.
Traefik 라우팅 문제는 restart로 안 잡혀서 force recreate 사용.
docker compose up -d --force-recreate