Ver3.0 ๐ ํ์ด์ฌ๊ณผ ์ฅ๊ณ (Django)๋ก ํ ํฐ ๊ฑฐ๋ ๋ด์ญ ์กฐํ ์์คํ ๊ตฌ์ถํ๊ธฐ: ๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ๋ฅผ ๋ด ์์์!

๐ ํ์ด์ฌ๊ณผ ์ฅ๊ณ (Django)๋ก ํ ํฐ ๊ฑฐ๋ ๋ด์ญ ์กฐํ ์์คํ ๊ตฌ์ถํ๊ธฐ: ๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ๋ฅผ ๋ด ์์์!
Web3 ์๋, ํ ํฐ ๊ฑฐ๋ ๋ด์ญ์ ์ค์๊ฐ์ผ๋ก ์ถ์ ํ๋ ๋๋ง์ ์์คํ ๋ง๋ค๊ธฐ
๐ฏ ์ ํ ํฐ ๊ฑฐ๋ ๋ด์ญ ์กฐํ ์์คํ ์ด ํ์ํ ๊น?
์๋
, ์น๊ตฌ! ์์ฆ ๋ธ๋ก์ฒด์ธ๊ณผ ์ํธํํ ์์ฅ์ด ์ ๋ง ๋จ๊ฒ์ง? ๐ฅ ๋นํธ์ฝ์ธ, ์ด๋๋ฆฌ์์ ๋ฌผ๋ก ์ด๊ณ ์๋ง์ ํ ํฐ๋ค์ด ๋งค์ผ ๊ฑฐ๋๋๊ณ ์์ด. ๊ทธ๋ฐ๋ฐ ๋ง์ด์ผ, ์ด๋ฐ ํ ํฐ๋ค์ ๊ฑฐ๋ ๋ด์ญ์ ์ ๋๋ก ์ถ์ ํ๊ณ ๋ถ์ํ๋ ค๋ฉด ์ด๋ป๊ฒ ํด์ผ ํ ๊น?
๋๋ถ๋ถ์ ์ฌ๋๋ค์ ๊ฑฐ๋์ ์ฑ์ด๋ ๋ธ๋ก ์ต์คํ๋ก๋ฌ(Block Explorer)๋ฅผ ์ฌ์ฉํ์ง. ํ์ง๋ง ๊ฐ๋ฐ์๋ผ๋ฉด, ํนํ ๋ธ๋ก์ฒด์ธ ๊ธฐ์ ์ ๊ด์ฌ์ด ์๋ค๋ฉด, ์ง์ ํ ํฐ ๊ฑฐ๋ ๋ด์ญ ์กฐํ ์์คํ
์ ๊ตฌ์ถํด๋ณด๋ ๊ฑด ์ ๋ง ๊ฐ์ง ๊ฒฝํ์ด ๋ ๊ฑฐ์ผ!
์ด๋ฐ ์์คํ
์ ๋ง๋ค๋ฉด ๋ญ๊ฐ ์ข๋๊ณ ? ์ผ๋จ ์ค์๊ฐ์ผ๋ก ํน์ ์ง๊ฐ ์ฃผ์์ ๊ฑฐ๋ ๋ด์ญ์ ๋ชจ๋ํฐ๋งํ ์ ์๊ณ , ๋๋์ ๊ฑฐ๋ ๋ฐ์ดํฐ๋ฅผ ๋ถ์ํด์ ํจํด์ ์ฐพ์ ์๋ ์์ด. ๊ฒ๋ค๊ฐ ์ธ๊ธ ์ ๊ณ ๋ฅผ ์ํ ๊ฑฐ๋ ๋ด์ญ ์ ๋ฆฌ๋ ํจ์ฌ ์ฌ์์ง์ง. ๐ฐ
โข ๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ ๊ตฌ์กฐ์ ํ ํฐ ๊ฑฐ๋์ ๊ธฐ๋ณธ ์๋ฆฌ
โข ํ์ด์ฌ๊ณผ ์ฅ๊ณ ๋ฅผ ํ์ฉํ ๋ฐฑ์๋ ์์คํ ์ค๊ณ
โข Web3.py๋ฅผ ์ด์ฉํ ๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ ์กฐํ
โข REST API ์ค๊ณ ๋ฐ ํ๋ก ํธ์๋ ์ฐ๋
โข ์ค์ ํ๋ก์ ํธ ๊ตฌํ ๋จ๊ณ๋ณ ๊ฐ์ด๋
โข ์ฑ๋ฅ ์ต์ ํ์ ๋ณด์ ๊ณ ๋ ค์ฌํญ
๐งฑ ๋ธ๋ก์ฒด์ธ๊ณผ ํ ํฐ ๊ฑฐ๋์ ๊ธฐ๋ณธ ์ดํด
๋ณธ๊ฒฉ์ ์ผ๋ก ์์คํ
์ ๊ตฌ์ถํ๊ธฐ ์ ์, ๋ธ๋ก์ฒด์ธ์์ ํ ํฐ ๊ฑฐ๋๊ฐ ์ด๋ป๊ฒ ์ด๋ฃจ์ด์ง๋์ง ์ดํดํด์ผ ํด. ์ด๊ฒ ๊ธฐ์ด ์ฒด๋ ฅ์ด๊ฑฐ๋ ! ๐ช
๋ธ๋ก์ฒด์ธ์ ์ฝ๊ฒ ๋งํด์ ๋ถ์ฐ๋ ๊ฑฐ๋ ์ฅ๋ถ์ผ. ์ํ ์ฅ๋ถ์ฒ๋ผ ์ค์์์ ๊ด๋ฆฌํ๋ ๊ฒ ์๋๋ผ, ์์ฒ ๊ฐ์ ์ปดํจํฐ๊ฐ ๋์์ ๊ฐ์ ์ฅ๋ถ๋ฅผ ๊ฐ์ง๊ณ ์๋ ๊ฑฐ์ง. ๋๊ตฐ๊ฐ ๊ฑฐ๋๋ฅผ ํ๋ฉด, ๊ทธ ๊ฑฐ๋ ๋ด์ญ์ด ๋ธ๋ก์ด๋ผ๋ ๋จ์๋ก ๋ฌถ์ฌ์ ์ฒด์ธ์ฒ๋ผ ์ฐ๊ฒฐ๋ผ.
ํ ํฐ์ ์ด ๋ธ๋ก์ฒด์ธ ์์์ ์๋ํ๋ ๋์งํธ ์์ฐ์ด์ผ. ์ด๋๋ฆฌ์ ๋ธ๋ก์ฒด์ธ์ ์๋ก ๋ค๋ฉด, ERC-20์ด๋ผ๋ ํ์ค์ ๋ฐ๋ฅด๋ ํ ํฐ๋ค์ด ์์ด. ์ด ํ ํฐ๋ค์ ์ค๋งํธ ์ปจํธ๋ํธ(Smart Contract)๋ผ๋ ํ๋ก๊ทธ๋จ์ผ๋ก ๊ด๋ฆฌ๋๋๋ฐ, ์ด๊ฒ ํต์ฌ์ด์ผ!
์ค๋งํธ ์ปจํธ๋ํธ๋ ๋ธ๋ก์ฒด์ธ ์์์ ์คํ๋๋ ํ๋ก๊ทธ๋จ์ด์ผ. ํน์ ์กฐ๊ฑด์ด ์ถฉ์กฑ๋๋ฉด ์๋์ผ๋ก ์คํ๋๋ ๊ณ์ฝ์๋ผ๊ณ ์๊ฐํ๋ฉด ๋ผ. ์๋ฅผ ๋ค์ด, "A๊ฐ B์๊ฒ 100ํ ํฐ์ ๋ณด๋ธ๋ค"๋ ๊ฑฐ๋๊ฐ ๋ฐ์ํ๋ฉด, ์ค๋งํธ ์ปจํธ๋ํธ๊ฐ ์๋์ผ๋ก A์ ์์ก์ 100 ์ค์ด๊ณ B์ ์์ก์ 100 ๋๋ ค์ฃผ๋ ๊ฑฐ์ง.
๐ ํ ํฐ ๊ฑฐ๋์ ๊ตฌ์กฐ
ํ ํฐ ๊ฑฐ๋๋ฅผ ์กฐํํ๋ ค๋ฉด ๊ฑฐ๋ ๋ฐ์ดํฐ์ ๊ตฌ์กฐ๋ฅผ ์์์ผ ํด. ์ผ๋ฐ์ ์ธ ํ ํฐ ๊ฑฐ๋์๋ ๋ค์๊ณผ ๊ฐ์ ์ ๋ณด๊ฐ ํฌํจ๋ผ:
| ํ๋๋ช | ์ค๋ช | ์์ |
|---|---|---|
| Transaction Hash | ๊ฑฐ๋์ ๊ณ ์ ์๋ณ์ | 0x1a2b3c4d... |
| From Address | ํ ํฐ์ ๋ณด๋ด๋ ์ง๊ฐ ์ฃผ์ | 0xabcd1234... |
| To Address | ํ ํฐ์ ๋ฐ๋ ์ง๊ฐ ์ฃผ์ | 0xefgh5678... |
| Value | ์ ์ก๋ ํ ํฐ์ ์ | 100.5 USDT |
| Block Number | ๊ฑฐ๋๊ฐ ํฌํจ๋ ๋ธ๋ก ๋ฒํธ | 15234567 |
| Timestamp | ๊ฑฐ๋ ๋ฐ์ ์๊ฐ | 2024-01-15 14:30:22 |
| Gas Fee | ๊ฑฐ๋ ์์๋ฃ | 0.002 ETH |
์ด๋ฐ ์ ๋ณด๋ค์ด ๋ธ๋ก์ฒด์ธ์ ์๊ตฌ์ ์ผ๋ก ๊ธฐ๋ก๋๊ณ , ์ฐ๋ฆฌ๋ ์ด ๋ฐ์ดํฐ๋ฅผ ์ฝ์ด์์ ์ฌ์ฉ์์๊ฒ ๋ณด์ฌ์ฃผ๋ ๊ฑฐ์ผ. ๐
๐ ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ ์ค๋นํ๊ธฐ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ๊ฐ๋ฐ์ ์์ํด๋ณผ๊น? ๋จผ์ ํ์ํ ๋๊ตฌ๋ค์ ์ค๋นํด์ผ ํด. ์๋ฆฌ๋ฅผ ํ๋ ค๋ฉด ์ฌ๋ฃ์ ๋๊ตฌ๊ฐ ํ์ํ๋ฏ์ด ๋ง์ด์ผ! ๐จโ๐ณ
๐ฆ ํ์ ํจํค์ง ๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
โข Python 3.8+: ๊ธฐ๋ณธ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด
โข Django 4.x: ์น ํ๋ ์์ํฌ
โข Web3.py: ์ด๋๋ฆฌ์ ๋ธ๋ก์ฒด์ธ๊ณผ ์ํธ์์ฉ
โข Django REST Framework: API ๊ตฌ์ถ
โข Celery: ๋น๋๊ธฐ ์์ ์ฒ๋ฆฌ
โข Redis: ์บ์ฑ ๋ฐ ๋ฉ์์ง ๋ธ๋ก์ปค
โข PostgreSQL: ๋ฐ์ดํฐ๋ฒ ์ด์ค
โข python-dotenv: ํ๊ฒฝ ๋ณ์ ๊ด๋ฆฌ
๐ป ์ค์น ๊ณผ์
๋จผ์ ๊ฐ์ํ๊ฒฝ์ ๋ง๋ค์ด์ฃผ์. ๊ฐ์ํ๊ฒฝ์ ํ๋ก์ ํธ๋ง๋ค ๋ ๋ฆฝ์ ์ธ ํ์ด์ฌ ํ๊ฒฝ์ ๋ง๋ค์ด์ฃผ๋ ๊ฑฐ์ผ. ์ด๋ ๊ฒ ํ๋ฉด ํจํค์ง ๋ฒ์ ์ถฉ๋์ ๋ฐฉ์งํ ์ ์์ด!
# ๊ฐ์ํ๊ฒฝ ์์ฑ
python -m venv token_tracker_env
# ๊ฐ์ํ๊ฒฝ ํ์ฑํ (Windows)
token_tracker_env\Scripts\activate
# ๊ฐ์ํ๊ฒฝ ํ์ฑํ (Mac/Linux)
source token_tracker_env/bin/activate
# ํ์ ํจํค์ง ์ค์น
pip install django==4.2.0
pip install web3==6.0.0
pip install djangorestframework==3.14.0
pip install celery==5.3.0
pip install redis==4.5.0
pip install psycopg2-binary==2.9.6
pip install python-dotenv==1.0.0
pip install requests==2.31.0
Web3.py ๋ฒ์ ์ ๋ฐ๋ผ API๊ฐ ์กฐ๊ธ์ฉ ๋ค๋ฅผ ์ ์์ด. ์ด ๊ธ์์๋ Web3.py 6.0.0 ๋ฒ์ ์ ๊ธฐ์ค์ผ๋ก ์ค๋ช ํ ๊ฑฐ์ผ. ์ต์ ๋ฒ์ ์ ์ฌ์ฉํ๊ณ ์ถ๋ค๋ฉด ๊ณต์ ๋ฌธ์๋ฅผ ๊ผญ ํ์ธํด!
๐ ๋ธ๋ก์ฒด์ธ ๋ ธ๋ ์ ๊ทผ ์ค์
๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ด์ค๋ ค๋ฉด ๋ธ๋ก์ฒด์ธ ๋
ธ๋์ ์ ๊ทผํด์ผ ํด. ์ง์ ๋
ธ๋๋ฅผ ์ด์ํ ์๋ ์์ง๋ง, ์ด๋ณด์์๊ฒ๋ ๋ถ๋ด์ค๋ฌ์ธ ์ ์์ด. ๋คํํ Infura๋ Alchemy ๊ฐ์ ์๋น์ค๋ฅผ ์ฌ์ฉํ๋ฉด ์ฝ๊ฒ ์ ๊ทผํ ์ ์์ด! ๐
Infura๋ฅผ ์๋ก ๋ค์ด๋ณผ๊ฒ:
Infura ์น์ฌ์ดํธ(infura.io)์ ์ ์ํด์ ๋ฌด๋ฃ ๊ณ์ ์ ๋ง๋ค์ด. ์ด๋ฉ์ผ ์ธ์ฆ๋ง ํ๋ฉด ๋ฐ๋ก ์ฌ์ฉํ ์ ์์ด!
๋์๋ณด๋์์ ์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด. ํ๋ก์ ํธ ์ด๋ฆ์ "TokenTracker" ๊ฐ์ ๊ฑธ๋ก ์ง์ด์ฃผ๋ฉด ๋ผ.
ํ๋ก์ ํธ๋ฅผ ๋ง๋ค๋ฉด API ํค๊ฐ ์์ฑ๋ผ. ์ด ํค๋ฅผ ์ฌ์ฉํด์ ๋ธ๋ก์ฒด์ธ์ ์ ๊ทผํ ์ ์์ด. ํค๋ ์ ๋ ๊ณต๊ฐํ๋ฉด ์ ๋ผ! ๐
API ํค๋ฅผ ๋ฐ์์ผ๋ฉด, ํ๋ก์ ํธ ๋ฃจํธ ๋๋ ํ ๋ฆฌ์ .env ํ์ผ์ ๋ง๋ค์ด์ ์ ์ฅํด:
# .env ํ์ผ
INFURA_PROJECT_ID=your_project_id_here
INFURA_PROJECT_SECRET=your_project_secret_here
ETHEREUM_NETWORK=mainnet # ๋๋ goerli, sepolia ๋ฑ ํ
์คํธ๋ท
DATABASE_URL=postgresql://user:password@localhost:5432/token_tracker
REDIS_URL=redis://localhost:6379/0
SECRET_KEY=your_django_secret_key_here
DEBUG=True
๊ฐ๋ฐ ์ด๊ธฐ์๋ ํ ์คํธ๋ท(Goerli, Sepolia ๋ฑ)์ ์ฌ์ฉํ๋ ๊ฑธ ์ถ์ฒํด! ๋ฉ์ธ๋ท์ ์ค์ ๋์ด ์ค๊ฐ๋ ๋คํธ์ํฌ๋ผ์ ์ค์ํ๋ฉด ํฐ์ผ ๋ ์ ์๊ฑฐ๋ . ํ ์คํธ๋ท์์ ์ถฉ๋ถํ ํ ์คํธํ ํ์ ๋ฉ์ธ๋ท์ผ๋ก ๋์ด๊ฐ์.
๐๏ธ Django ํ๋ก์ ํธ ๊ตฌ์กฐ ์ค๊ณ
์ด์ Django ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณผ ๊ฑฐ์ผ. ํ๋ก์ ํธ ๊ตฌ์กฐ๋ฅผ ์ ์ค๊ณํ๋ ๊ฒ ์ ๋ง ์ค์ํด. ๋์ค์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๊ฑฐ๋ ์์ ํ ๋ ํจ์ฌ ํธํ๊ฑฐ๋ ! ๐๏ธ
๐ ํ๋ก์ ํธ ์์ฑ
# Django ํ๋ก์ ํธ ์์ฑ
django-admin startproject token_tracker
# ํ๋ก์ ํธ ๋๋ ํ ๋ฆฌ๋ก ์ด๋
cd token_tracker
# ์ฑ ์์ฑ
python manage.py startapp blockchain
python manage.py startapp api
python manage.py startapp dashboard
์ด๋ ๊ฒ ํ๋ฉด ๋ค์๊ณผ ๊ฐ์ ๊ตฌ์กฐ๊ฐ ๋ง๋ค์ด์ ธ:
token_tracker/
โโโ token_tracker/ # ํ๋ก์ ํธ ์ค์ ๋๋ ํ ๋ฆฌ
โ โโโ __init__.py
โ โโโ settings.py # ํ๋ก์ ํธ ์ค์
โ โโโ urls.py # URL ๋ผ์ฐํ
โ โโโ wsgi.py
โ โโโ asgi.py
โโโ blockchain/ # ๋ธ๋ก์ฒด์ธ ๊ด๋ จ ๋ก์ง
โ โโโ migrations/
โ โโโ __init__.py
โ โโโ models.py # ๋ฐ์ดํฐ ๋ชจ๋ธ
โ โโโ views.py
โ โโโ services.py # ๋น์ฆ๋์ค ๋ก์ง
โ โโโ utils.py # ์ ํธ๋ฆฌํฐ ํจ์
โโโ api/ # REST API
โ โโโ migrations/
โ โโโ __init__.py
โ โโโ serializers.py # ๋ฐ์ดํฐ ์ง๋ ฌํ
โ โโโ views.py # API ๋ทฐ
โ โโโ urls.py
โโโ dashboard/ # ํ๋ก ํธ์๋ ๋์๋ณด๋
โ โโโ templates/
โ โโโ static/
โ โโโ views.py
โโโ .env # ํ๊ฒฝ ๋ณ์
โโโ manage.py
โโโ requirements.txt # ํจํค์ง ๋ชฉ๋ก
โ๏ธ settings.py ์ค์
Django์ ํต์ฌ ์ค์ ํ์ผ์ธ settings.py๋ฅผ ์์ ํด์ผ ํด. ์ฌ๊ธฐ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค, ์ฑ, ๋ฏธ๋ค์จ์ด ๋ฑ์ ์ค์ ํด:
# token_tracker/settings.py
import os
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.getenv('SECRET_KEY')
DEBUG = os.getenv('DEBUG', 'False') == 'True'
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
# ์ค์น๋ ์ฑ
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third party apps
'rest_framework',
'corsheaders',
# Local apps
'blockchain',
'api',
'dashboard',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'token_tracker',
'USER': 'postgres',
'PASSWORD': 'your_password',
'HOST': 'localhost',
'PORT': '5432',
}
}
# REST Framework ์ค์
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 50,
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
}
# Celery ์ค์
CELERY_BROKER_URL = os.getenv('REDIS_URL', 'redis://localhost:6379/0')
CELERY_RESULT_BACKEND = os.getenv('REDIS_URL', 'redis://localhost:6379/0')
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
# ๋ธ๋ก์ฒด์ธ ์ค์
INFURA_PROJECT_ID = os.getenv('INFURA_PROJECT_ID')
ETHEREUM_NETWORK = os.getenv('ETHEREUM_NETWORK', 'mainnet')
๐๏ธ ๋ฐ์ดํฐ ๋ชจ๋ธ ์ค๊ณ
๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ชจ๋ธ์ ์ค๊ณํ ์๊ฐ์ด์ผ! ํ ํฐ ๊ฑฐ๋ ๋ด์ญ์ ์ ์ฅํ๊ธฐ ์ํ ํ
์ด๋ธ ๊ตฌ์กฐ๋ฅผ ๋ง๋ค์ด๋ณด์. ๐
์ฐ๋ฆฌ๊ฐ ์ถ์ ํด์ผ ํ ์ฃผ์ ๋ฐ์ดํฐ๋ ๋ค์๊ณผ ๊ฐ์:
โข Token: ํ ํฐ ์ ๋ณด (์ด๋ฆ, ์ฌ๋ณผ, ์ปจํธ๋ํธ ์ฃผ์ ๋ฑ)
โข Transaction: ๊ฑฐ๋ ๋ด์ญ
โข Wallet: ์ง๊ฐ ์ฃผ์ ์ ๋ณด
โข Block: ๋ธ๋ก ์ ๋ณด (์ ํ์ฌํญ)
๐ models.py ์์ฑ
# blockchain/models.py
from django.db import models
from django.core.validators import MinValueValidator
from decimal import Decimal
class Token(models.Model):
"""ํ ํฐ ์ ๋ณด ๋ชจ๋ธ"""
contract_address = models.CharField(
max_length=42,
unique=True,
db_index=True,
help_text="ํ ํฐ ์ค๋งํธ ์ปจํธ๋ํธ ์ฃผ์"
)
name = models.CharField(max_length=100, help_text="ํ ํฐ ์ด๋ฆ")
symbol = models.CharField(max_length=10, help_text="ํ ํฐ ์ฌ๋ณผ")
decimals = models.IntegerField(
default=18,
help_text="ํ ํฐ ์์์ ์๋ฆฟ์"
)
total_supply = models.DecimalField(
max_digits=78,
decimal_places=0,
null=True,
blank=True,
help_text="์ด ๋ฐํ๋"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'tokens'
verbose_name = 'Token'
verbose_name_plural = 'Tokens'
ordering = ['-created_at']
def __str__(self):
return f"{self.name} ({self.symbol})"
class Wallet(models.Model):
"""์ง๊ฐ ์ฃผ์ ๋ชจ๋ธ"""
address = models.CharField(
max_length=42,
unique=True,
db_index=True,
help_text="์ง๊ฐ ์ฃผ์"
)
label = models.CharField(
max_length=100,
blank=True,
help_text="์ง๊ฐ ๋ผ๋ฒจ (์ ํ์ฌํญ)"
)
is_contract = models.BooleanField(
default=False,
help_text="์ปจํธ๋ํธ ์ฃผ์ ์ฌ๋ถ"
)
first_seen = models.DateTimeField(auto_now_add=True)
last_activity = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = 'wallets'
verbose_name = 'Wallet'
verbose_name_plural = 'Wallets'
ordering = ['-last_activity']
def __str__(self):
return self.label if self.label else self.address[:10] + "..."
class Transaction(models.Model):
"""ํ ํฐ ๊ฑฐ๋ ๋ด์ญ ๋ชจ๋ธ"""
STATUS_CHOICES = [
('pending', 'Pending'),
('success', 'Success'),
('failed', 'Failed'),
]
transaction_hash = models.CharField(
max_length=66,
unique=True,
db_index=True,
help_text="๊ฑฐ๋ ํด์"
)
token = models.ForeignKey(
Token,
on_delete=models.CASCADE,
related_name='transactions',
help_text="๊ด๋ จ ํ ํฐ"
)
from_address = models.ForeignKey(
Wallet,
on_delete=models.CASCADE,
related_name='sent_transactions',
help_text="์ก์ ์ ์ฃผ์"
)
to_address = models.ForeignKey(
Wallet,
on_delete=models.CASCADE,
related_name='received_transactions',
help_text="์์ ์ ์ฃผ์"
)
value = models.DecimalField(
max_digits=78,
decimal_places=18,
validators=[MinValueValidator(Decimal('0'))],
help_text="์ ์ก ๊ธ์ก"
)
block_number = models.BigIntegerField(
db_index=True,
help_text="๋ธ๋ก ๋ฒํธ"
)
block_timestamp = models.DateTimeField(
db_index=True,
help_text="๋ธ๋ก ํ์์คํฌํ"
)
gas_price = models.DecimalField(
max_digits=78,
decimal_places=0,
help_text="๊ฐ์ค ๊ฐ๊ฒฉ (wei)"
)
gas_used = models.BigIntegerField(help_text="์ฌ์ฉ๋ ๊ฐ์ค")
transaction_fee = models.DecimalField(
max_digits=78,
decimal_places=18,
help_text="๊ฑฐ๋ ์์๋ฃ (ETH)"
)
status = models.CharField(
max_length=10,
choices=STATUS_CHOICES,
default='success',
help_text="๊ฑฐ๋ ์ํ"
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'transactions'
verbose_name = 'Transaction'
verbose_name_plural = 'Transactions'
ordering = ['-block_timestamp']
indexes = [
models.Index(fields=['-block_timestamp']),
models.Index(fields=['from_address', '-block_timestamp']),
models.Index(fields=['to_address', '-block_timestamp']),
models.Index(fields=['token', '-block_timestamp']),
]
def __str__(self):
return f"{self.transaction_hash[:10]}... ({self.value} {self.token.symbol})"
@property
def value_formatted(self):
"""ํ ํฐ decimals๋ฅผ ๊ณ ๋ คํ ์ค์ ๊ฐ"""
return self.value / (10 ** self.token.decimals)
class Block(models.Model):
"""๋ธ๋ก ์ ๋ณด ๋ชจ๋ธ (์ ํ์ฌํญ)"""
block_number = models.BigIntegerField(
unique=True,
db_index=True,
help_text="๋ธ๋ก ๋ฒํธ"
)
block_hash = models.CharField(
max_length=66,
unique=True,
help_text="๋ธ๋ก ํด์"
)
timestamp = models.DateTimeField(
db_index=True,
help_text="๋ธ๋ก ์์ฑ ์๊ฐ"
)
transaction_count = models.IntegerField(
default=0,
help_text="๋ธ๋ก ๋ด ๊ฑฐ๋ ์"
)
miner = models.CharField(
max_length=42,
help_text="์ฑ๊ตด์ ์ฃผ์"
)
class Meta:
db_table = 'blocks'
verbose_name = 'Block'
verbose_name_plural = 'Blocks'
ordering = ['-block_number']
def __str__(self):
return f"Block #{self.block_number}"
๋ชจ๋ธ์ ๋ง๋ค์์ผ๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์ํด์ผ ํด:
# ๋ง์ด๊ทธ๋ ์ด์
ํ์ผ ์์ฑ
python manage.py makemigrations
# ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฉ
python manage.py migrate
์ ๋ชจ๋ธ์์ db_index=True์ indexes๋ฅผ ์ฌ์ฉํ ๊ฑธ ๋ดค์ง? ์ด๊ฑด ๋ฐ์ดํฐ๋ฒ ์ด์ค ์กฐํ ์ฑ๋ฅ์ ๋์ด๊ธฐ ์ํ ๊ฑฐ์ผ. ํนํ ๊ฑฐ๋ ๋ด์ญ์ฒ๋ผ ๋ฐ์ดํฐ๊ฐ ๋ง์ด ์์ด๋ ํ ์ด๋ธ์์๋ ์ธ๋ฑ์ค๊ฐ ํ์์ผ! ์์ฃผ ๊ฒ์ํ๋ ํ๋(์ฃผ์, ์๊ฐ, ํ ํฐ ๋ฑ)์ ์ธ๋ฑ์ค๋ฅผ ๊ฑธ์ด์ฃผ๋ฉด ์กฐํ ์๋๊ฐ ์์ฒญ ๋นจ๋ผ์ ธ.
๐ Web3.py๋ก ๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ ์กฐํํ๊ธฐ
๋๋์ด ๋ธ๋ก์ฒด์ธ๊ณผ ์ง์ ์ํตํ ์๊ฐ์ด์ผ! Web3.py๋ฅผ ์ฌ์ฉํ๋ฉด ํ์ด์ฌ์์ ์ด๋๋ฆฌ์ ๋ธ๋ก์ฒด์ธ๊ณผ ์ํธ์์ฉํ ์ ์์ด. ๐
๋จผ์ Web3 ์ฐ๊ฒฐ์ ์ค์ ํ๋ ์ ํธ๋ฆฌํฐ ํจ์๋ฅผ ๋ง๋ค์ด๋ณด์:
๐ง Web3 ์ฐ๊ฒฐ ์ค์
# blockchain/utils.py
from web3 import Web3
from web3.middleware import geth_poa_middleware
from django.conf import settings
import logging
logger = logging.getLogger(__name__)
class Web3Connection:
"""Web3 ์ฐ๊ฒฐ ๊ด๋ฆฌ ํด๋์ค"""
_instance = None
_w3 = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if self._w3 is None:
self._initialize_connection()
def _initialize_connection(self):
"""Web3 ์ฐ๊ฒฐ ์ด๊ธฐํ"""
try:
# Infura ์๋ํฌ์ธํธ ์ค์
infura_url = f"https://{settings.ETHEREUM_NETWORK}.infura.io/v3/{settings.INFURA_PROJECT_ID}"
# Web3 ์ธ์คํด์ค ์์ฑ
self._w3 = Web3(Web3.HTTPProvider(infura_url))
# PoA ์ฒด์ธ์ ์ํ ๋ฏธ๋ค์จ์ด ์ถ๊ฐ (ํ
์คํธ๋ท์ฉ)
if settings.ETHEREUM_NETWORK != 'mainnet':
self._w3.middleware_onion.inject(geth_poa_middleware, layer=0)
# ์ฐ๊ฒฐ ํ์ธ
if self._w3.is_connected():
logger.info(f"Web3 ์ฐ๊ฒฐ ์ฑ๊ณต: {settings.ETHEREUM_NETWORK}")
logger.info(f"ํ์ฌ ๋ธ๋ก: {self._w3.eth.block_number}")
else:
logger.error("Web3 ์ฐ๊ฒฐ ์คํจ")
raise ConnectionError("๋ธ๋ก์ฒด์ธ ๋
ธ๋์ ์ฐ๊ฒฐํ ์ ์์ต๋๋ค")
except Exception as e:
logger.error(f"Web3 ์ด๊ธฐํ ์ค๋ฅ: {str(e)}")
raise
@property
def w3(self):
"""Web3 ์ธ์คํด์ค ๋ฐํ"""
if self._w3 is None or not self._w3.is_connected():
self._initialize_connection()
return self._w3
def get_latest_block_number(self):
"""์ต์ ๋ธ๋ก ๋ฒํธ ์กฐํ"""
return self.w3.eth.block_number
def get_block(self, block_number):
"""ํน์ ๋ธ๋ก ์ ๋ณด ์กฐํ"""
return self.w3.eth.get_block(block_number, full_transactions=True)
def get_transaction(self, tx_hash):
"""๊ฑฐ๋ ์ ๋ณด ์กฐํ"""
return self.w3.eth.get_transaction(tx_hash)
def get_transaction_receipt(self, tx_hash):
"""๊ฑฐ๋ ์์์ฆ ์กฐํ"""
return self.w3.eth.get_transaction_receipt(tx_hash)
# ์ฑ๊ธํค ์ธ์คํด์ค
web3_connection = Web3Connection()
๐ ERC-20 ํ ํฐ ์ปจํธ๋ํธ ์ํธ์์ฉ
ERC-20 ํ ํฐ์ ๊ฑฐ๋ ๋ด์ญ์ ์กฐํํ๋ ค๋ฉด ํ ํฐ ์ปจํธ๋ํธ์ ์ํธ์์ฉํด์ผ ํด. ERC-20 ํ์ค ABI(Application Binary Interface)๋ฅผ ์ฌ์ฉํ๋ฉด ๋ผ:
# blockchain/contracts.py
from web3 import Web3
from .utils import web3_connection
import logging
logger = logging.getLogger(__name__)
# ERC-20 ํ์ค ABI (์ฃผ์ ํจ์๋ง)
ERC20_ABI = [
{
"constant": True,
"inputs": [],
"name": "name",
"outputs": [{"name": "", "type": "string"}],
"type": "function"
},
{
"constant": True,
"inputs": [],
"name": "symbol",
"outputs": [{"name": "", "type": "string"}],
"type": "function"
},
{
"constant": True,
"inputs": [],
"name": "decimals",
"outputs": [{"name": "", "type": "uint8"}],
"type": "function"
},
{
"constant": True,
"inputs": [],
"name": "totalSupply",
"outputs": [{"name": "", "type": "uint256"}],
"type": "function"
},
{
"constant": True,
"inputs": [{"name": "_owner", "type": "address"}],
"name": "balanceOf",
"outputs": [{"name": "balance", "type": "uint256"}],
"type": "function"
},
{
"anonymous": False,
"inputs": [
{"indexed": True, "name": "from", "type": "address"},
{"indexed": True, "name": "to", "type": "address"},
{"indexed": False, "name": "value", "type": "uint256"}
],
"name": "Transfer",
"type": "event"
}
]
class ERC20Token:
"""ERC-20 ํ ํฐ ์ปจํธ๋ํธ ๋ํผ ํด๋์ค"""
def __init__(self, contract_address):
"""
Args:
contract_address: ํ ํฐ ์ปจํธ๋ํธ ์ฃผ์
"""
self.w3 = web3_connection.w3
self.contract_address = Web3.to_checksum_address(contract_address)
self.contract = self.w3.eth.contract(
address=self.contract_address,
abi=ERC20_ABI
)
def get_name(self):
"""ํ ํฐ ์ด๋ฆ ์กฐํ"""
try:
return self.contract.functions.name().call()
except Exception as e:
logger.error(f"ํ ํฐ ์ด๋ฆ ์กฐํ ์คํจ: {str(e)}")
return None
def get_symbol(self):
"""ํ ํฐ ์ฌ๋ณผ ์กฐํ"""
try:
return self.contract.functions.symbol().call()
except Exception as e:
logger.error(f"ํ ํฐ ์ฌ๋ณผ ์กฐํ ์คํจ: {str(e)}")
return None
def get_decimals(self):
"""ํ ํฐ ์์์ ์๋ฆฟ์ ์กฐํ"""
try:
return self.contract.functions.decimals().call()
except Exception as e:
logger.error(f"ํ ํฐ decimals ์กฐํ ์คํจ: {str(e)}")
return 18 # ๊ธฐ๋ณธ๊ฐ
def get_total_supply(self):
"""์ด ๋ฐํ๋ ์กฐํ"""
try:
return self.contract.functions.totalSupply().call()
except Exception as e:
logger.error(f"์ด ๋ฐํ๋ ์กฐํ ์คํจ: {str(e)}")
return None
def get_balance(self, address):
"""ํน์ ์ฃผ์์ ํ ํฐ ์์ก ์กฐํ"""
try:
checksum_address = Web3.to_checksum_address(address)
balance = self.contract.functions.balanceOf(checksum_address).call()
return balance
except Exception as e:
logger.error(f"์์ก ์กฐํ ์คํจ: {str(e)}")
return 0
def get_transfer_events(self, from_block, to_block, address=None):
"""Transfer ์ด๋ฒคํธ ์กฐํ
Args:
from_block: ์์ ๋ธ๋ก
to_block: ์ข
๋ฃ ๋ธ๋ก
address: ํน์ ์ฃผ์ ํํฐ (์ ํ์ฌํญ)
Returns:
Transfer ์ด๋ฒคํธ ๋ฆฌ์คํธ
"""
try:
# ์ด๋ฒคํธ ํํฐ ์์ฑ
event_filter = {
'fromBlock': from_block,
'toBlock': to_block,
}
# ํน์ ์ฃผ์ ํํฐ๋ง
if address:
checksum_address = Web3.to_checksum_address(address)
# from ๋๋ to๊ฐ ํด๋น ์ฃผ์์ธ ์ด๋ฒคํธ๋ง ์กฐํ
events_from = self.contract.events.Transfer.create_filter(
fromBlock=from_block,
toBlock=to_block,
argument_filters={'from': checksum_address}
).get_all_entries()
events_to = self.contract.events.Transfer.create_filter(
fromBlock=from_block,
toBlock=to_block,
argument_filters={'to': checksum_address}
).get_all_entries()
# ์ค๋ณต ์ ๊ฑฐ ๋ฐ ๋ณํฉ
all_events = list({event['transactionHash']: event
for event in events_from + events_to}.values())
return all_events
else:
# ๋ชจ๋ Transfer ์ด๋ฒคํธ ์กฐํ
return self.contract.events.Transfer.create_filter(
**event_filter
).get_all_entries()
except Exception as e:
logger.error(f"Transfer ์ด๋ฒคํธ ์กฐํ ์คํจ: {str(e)}")
return []
def get_token_info(self):
"""ํ ํฐ ์ ์ฒด ์ ๋ณด ์กฐํ"""
return {
'address': self.contract_address,
'name': self.get_name(),
'symbol': self.get_symbol(),
'decimals': self.get_decimals(),
'total_supply': self.get_total_supply(),
}
Infura ๊ฐ์ ์๋น์ค๋ ํ ๋ฒ์ ์กฐํํ ์ ์๋ ๋ธ๋ก ๋ฒ์์ ์ ํ์ด ์์ด. ๋ณดํต 10,000 ๋ธ๋ก ์ ๋๊ฐ ํ๊ณ์ผ. ๊ทธ๋์ ํฐ ๋ฒ์๋ฅผ ์กฐํํ ๋๋ ์ฌ๋ฌ ๋ฒ์ ๋๋ ์ ์กฐํํด์ผ ํด. ์ด๊ฑด ๋์ค์ ์๋น์ค ๋ ์ด์ด์์ ์ฒ๋ฆฌํ ๊ฑฐ์ผ!
โ๏ธ ๋น์ฆ๋์ค ๋ก์ง ๊ตฌํ
์ด์ ์ค์ ๋ก ํ ํฐ ๊ฑฐ๋ ๋ด์ญ์ ์กฐํํ๊ณ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ๋ ๋ก์ง์ ๋ง๋ค์ด๋ณด์. ์ด ๋ถ๋ถ์ด ์์คํ ์ ํต์ฌ์ด์ผ! ๐
๐ ํ ํฐ ๊ฑฐ๋ ์์ง ์๋น์ค
# blockchain/services.py
from django.utils import timezone
from django.db import transaction as db_transaction
from datetime import datetime
from decimal import Decimal
from .models import Token, Wallet, Transaction, Block
from .contracts import ERC20Token
from .utils import web3_connection
import logging
logger = logging.getLogger(__name__)
class TokenService:
"""ํ ํฐ ๊ด๋ จ ๋น์ฆ๋์ค ๋ก์ง"""
@staticmethod
def register_token(contract_address):
"""ํ ํฐ ๋ฑ๋ก
Args:
contract_address: ํ ํฐ ์ปจํธ๋ํธ ์ฃผ์
Returns:
Token ์ธ์คํด์ค
"""
try:
# ์ด๋ฏธ ๋ฑ๋ก๋ ํ ํฐ์ธ์ง ํ์ธ
token = Token.objects.filter(
contract_address=contract_address
).first()
if token:
logger.info(f"์ด๋ฏธ ๋ฑ๋ก๋ ํ ํฐ: {token.symbol}")
return token
# ํ ํฐ ์ ๋ณด ์กฐํ
erc20 = ERC20Token(contract_address)
token_info = erc20.get_token_info()
if not token_info['name']:
raise ValueError("์ ํจํ์ง ์์ ํ ํฐ ์ปจํธ๋ํธ")
# ํ ํฐ ์์ฑ
token = Token.objects.create(
contract_address=contract_address,
name=token_info['name'],
symbol=token_info['symbol'],
decimals=token_info['decimals'],
total_supply=token_info['total_supply']
)
logger.info(f"ํ ํฐ ๋ฑ๋ก ์๋ฃ: {token.symbol}")
return token
except Exception as e:
logger.error(f"ํ ํฐ ๋ฑ๋ก ์คํจ: {str(e)}")
raise
class TransactionService:
"""๊ฑฐ๋ ๋ด์ญ ๊ด๋ จ ๋น์ฆ๋์ค ๋ก์ง"""
@staticmethod
def get_or_create_wallet(address, is_contract=False):
"""์ง๊ฐ ์ฃผ์ ์กฐํ ๋๋ ์์ฑ"""
wallet, created = Wallet.objects.get_or_create(
address=address,
defaults={'is_contract': is_contract}
)
if created:
logger.debug(f"์ ์ง๊ฐ ์์ฑ: {address}")
return wallet
@staticmethod
def sync_token_transactions(token, from_block, to_block, target_address=None):
"""ํ ํฐ ๊ฑฐ๋ ๋ด์ญ ๋๊ธฐํ
Args:
token: Token ์ธ์คํด์ค
from_block: ์์ ๋ธ๋ก
to_block: ์ข
๋ฃ ๋ธ๋ก
target_address: ํน์ ์ฃผ์๋ง ์กฐํ (์ ํ์ฌํญ)
Returns:
์ ์ฅ๋ ๊ฑฐ๋ ์
"""
try:
w3 = web3_connection.w3
erc20 = ERC20Token(token.contract_address)
# Transfer ์ด๋ฒคํธ ์กฐํ
events = erc20.get_transfer_events(
from_block=from_block,
to_block=to_block,
address=target_address
)
logger.info(f"{len(events)}๊ฐ์ Transfer ์ด๋ฒคํธ ๋ฐ๊ฒฌ")
saved_count = 0
# ๊ฐ ์ด๋ฒคํธ ์ฒ๋ฆฌ
for event in events:
try:
# ์ด๋ฏธ ์ ์ฅ๋ ๊ฑฐ๋์ธ์ง ํ์ธ
tx_hash = event['transactionHash'].hex()
if Transaction.objects.filter(transaction_hash=tx_hash).exists():
continue
# ๊ฑฐ๋ ์์ธ ์ ๋ณด ์กฐํ
tx = w3.eth.get_transaction(event['transactionHash'])
receipt = w3.eth.get_transaction_receipt(event['transactionHash'])
block = w3.eth.get_block(event['blockNumber'])
# ์ง๊ฐ ์ฃผ์ ์ฒ๋ฆฌ
from_wallet = TransactionService.get_or_create_wallet(
event['args']['from']
)
to_wallet = TransactionService.get_or_create_wallet(
event['args']['to']
)
# ๊ฑฐ๋ ์์๋ฃ ๊ณ์ฐ
gas_price = tx['gasPrice']
gas_used = receipt['gasUsed']
tx_fee = Decimal(gas_price * gas_used) / Decimal(10**18)
# ๊ฑฐ๋ ์ํ ํ์ธ
status = 'success' if receipt['status'] == 1 else 'failed'
# ๊ฑฐ๋ ์ ์ฅ
with db_transaction.atomic():
Transaction.objects.create(
transaction_hash=tx_hash,
token=token,
from_address=from_wallet,
to_address=to_wallet,
value=event['args']['value'],
block_number=event['blockNumber'],
block_timestamp=datetime.fromtimestamp(
block['timestamp'],
tz=timezone.utc
),
gas_price=gas_price,
gas_used=gas_used,
transaction_fee=tx_fee,
status=status
)
# ์ง๊ฐ ๋ง์ง๋ง ํ๋ ์๊ฐ ์
๋ฐ์ดํธ
from_wallet.last_activity = datetime.fromtimestamp(
block['timestamp'],
tz=timezone.utc
)
from_wallet.save()
to_wallet.last_activity = datetime.fromtimestamp(
block['timestamp'],
tz=timezone.utc
)
to_wallet.save()
saved_count += 1
except Exception as e:
logger.error(f"๊ฑฐ๋ ์ฒ๋ฆฌ ์คํจ ({tx_hash}): {str(e)}")
continue
logger.info(f"{saved_count}๊ฐ์ ๊ฑฐ๋ ์ ์ฅ ์๋ฃ")
return saved_count
except Exception as e:
logger.error(f"๊ฑฐ๋ ๋๊ธฐํ ์คํจ: {str(e)}")
raise
@staticmethod
def sync_wallet_transactions(wallet_address, token_address, lookback_blocks=10000):
"""ํน์ ์ง๊ฐ์ ํ ํฐ ๊ฑฐ๋ ๋ด์ญ ๋๊ธฐํ
Args:
wallet_address: ์ง๊ฐ ์ฃผ์
token_address: ํ ํฐ ์ปจํธ๋ํธ ์ฃผ์
lookback_blocks: ์กฐํํ ๊ณผ๊ฑฐ ๋ธ๋ก ์
Returns:
์ ์ฅ๋ ๊ฑฐ๋ ์
"""
try:
# ํ ํฐ ๋ฑ๋ก (์์ผ๋ฉด ์์ฑ)
token = TokenService.register_token(token_address)
# ํ์ฌ ๋ธ๋ก ๋ฒํธ
w3 = web3_connection.w3
current_block = w3.eth.block_number
from_block = max(0, current_block - lookback_blocks)
logger.info(f"๋ธ๋ก {from_block} ~ {current_block} ๋ฒ์ ์กฐํ")
# ๊ฑฐ๋ ๋๊ธฐํ
return TransactionService.sync_token_transactions(
token=token,
from_block=from_block,
to_block=current_block,
target_address=wallet_address
)
except Exception as e:
logger.error(f"์ง๊ฐ ๊ฑฐ๋ ๋๊ธฐํ ์คํจ: {str(e)}")
raise
์ด์ ์ด ์๋น์ค๋ฅผ ์ฌ์ฉํด์ ์ค์ ๋ก ๊ฑฐ๋ ๋ด์ญ์ ์กฐํํ ์ ์์ด! ์๋ฅผ ๋ค์ด, USDT ํ ํฐ์ ๊ฑฐ๋ ๋ด์ญ์ ์กฐํํ๊ณ ์ถ๋ค๋ฉด:
# Django shell์์ ํ
์คํธ
python manage.py shell
from blockchain.services import TokenService, TransactionService
# USDT ํ ํฐ ์ฃผ์ (์ด๋๋ฆฌ์ ๋ฉ์ธ๋ท)
usdt_address = "0xdAC17F958D2ee523a2206206994597C13D831ec7"
# ํน์ ์ง๊ฐ์ USDT ๊ฑฐ๋ ๋ด์ญ ์กฐํ
wallet_address = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"
TransactionService.sync_wallet_transactions(
wallet_address=wallet_address,
token_address=usdt_address,
lookback_blocks=5000
)
์ฒ์๋ถํฐ ๋ชจ๋ ๊ฑฐ๋ ๋ด์ญ์ ๊ฐ์ ธ์ค๋ ค๊ณ ํ๋ฉด ์๊ฐ์ด ์์ฒญ ์ค๋ ๊ฑธ๋ ค. ๊ทธ๋์ ์ค์ ์์๋ ์ ์ง์ ๋๊ธฐํ ๋ฐฉ์์ ์ฌ์ฉํด. ๋จผ์ ์ต๊ทผ 1,000๋ธ๋ก๋ง ๊ฐ์ ธ์ค๊ณ , ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ฒ์ฒํ ๊ณผ๊ฑฐ ๋ฐ์ดํฐ๋ฅผ ์ฑ์๋๊ฐ๋ ๊ฑฐ์ง. ์ด๋ ๊ฒ ํ๋ฉด ์ฌ์ฉ์๋ ๋น ๋ฅด๊ฒ ์ต์ ๋ฐ์ดํฐ๋ฅผ ๋ณผ ์ ์์ด!
๐ Celery๋ก ๋น๋๊ธฐ ์์ ์ฒ๋ฆฌํ๊ธฐ
๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋ ๊ฑด ์๊ฐ์ด ๊ฝค ๊ฑธ๋ฆฌ๋ ์์
์ด์ผ. ์ฌ์ฉ์๊ฐ ์์ฒญํ ๋๋ง๋ค ๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ฉด ์๋ต์ด ๋๋ฌด ๋๋ ค์ ธ์ ์ฌ์ฉ์ ๊ฒฝํ์ด ๋๋น ์ง์ง. ๐ฐ
์ด๋ด ๋ Celery๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๋น๋๊ธฐ์ ์ผ๋ก ์์
์ ์ฒ๋ฆฌํ ์ ์์ด! ์ฌ์ฉ์๋ ์ฆ์ ์๋ต์ ๋ฐ๊ณ , ์ค์ ๋ฐ์ดํฐ ์์ง์ ๋ค์์ ์ฒ์ฒํ ์งํ๋๋ ๊ฑฐ์ง.
โ๏ธ Celery ์ค์
# token_tracker/celery.py
import os
from celery import Celery
from celery.schedules import crontab
# Django ์ค์ ๋ชจ๋ ์ง์
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'token_tracker.settings')
# Celery ์ฑ ์์ฑ
app = Celery('token_tracker')
# Django ์ค์ ์์ Celery ์ค์ ๋ก๋
app.config_from_object('django.conf:settings', namespace='CELERY')
# Django ์ฑ์์ task ์๋ ๋ฐ๊ฒฌ
app.autodiscover_tasks()
# ์ฃผ๊ธฐ์ ์์
์ค์ผ์ค ์ค์
app.conf.beat_schedule = {
'sync-recent-transactions': {
'task': 'blockchain.tasks.sync_recent_transactions',
'schedule': crontab(minute='*/10'), # 10๋ถ๋ง๋ค ์คํ
},
'update-token-prices': {
'task': 'blockchain.tasks.update_token_prices',
'schedule': crontab(minute='*/5'), # 5๋ถ๋ง๋ค ์คํ
},
}
@app.task(bind=True)
def debug_task(self):
print(f'Request: {self.request!r}')
# token_tracker/__init__.py
from .celery import app as celery_app
__all__ = ('celery_app',)
๐ Celery Task ์์ฑ
# blockchain/tasks.py
from celery import shared_task
from celery.utils.log import get_task_logger
from django.utils import timezone
from datetime import timedelta
from .models import Token, Wallet
from .services import TransactionService
from .utils import web3_connection
logger = get_task_logger(__name__)
@shared_task(bind=True, max_retries=3)
def sync_wallet_transactions_task(self, wallet_address, token_address, lookback_blocks=1000):
"""์ง๊ฐ ๊ฑฐ๋ ๋ด์ญ ๋๊ธฐํ ์์
Args:
wallet_address: ์ง๊ฐ ์ฃผ์
token_address: ํ ํฐ ์ปจํธ๋ํธ ์ฃผ์
lookback_blocks: ์กฐํํ ๊ณผ๊ฑฐ ๋ธ๋ก ์
Returns:
์ ์ฅ๋ ๊ฑฐ๋ ์
"""
try:
logger.info(f"๊ฑฐ๋ ๋๊ธฐํ ์์: {wallet_address}")
count = TransactionService.sync_wallet_transactions(
wallet_address=wallet_address,
token_address=token_address,
lookback_blocks=lookback_blocks
)
logger.info(f"๊ฑฐ๋ ๋๊ธฐํ ์๋ฃ: {count}๊ฐ ์ ์ฅ")
return {
'status': 'success',
'wallet_address': wallet_address,
'token_address': token_address,
'saved_count': count
}
except Exception as exc:
logger.error(f"๊ฑฐ๋ ๋๊ธฐํ ์คํจ: {str(exc)}")
# ์ฌ์๋
raise self.retry(exc=exc, countdown=60)
@shared_task
def sync_recent_transactions():
"""์ต๊ทผ ๊ฑฐ๋ ๋ด์ญ ์๋ ๋๊ธฐํ (์ฃผ๊ธฐ์ ์คํ)"""
try:
logger.info("์ต๊ทผ ๊ฑฐ๋ ์๋ ๋๊ธฐํ ์์")
# ์ต๊ทผ 24์๊ฐ ๋ด ํ๋์ด ์์๋ ์ง๊ฐ๋ค
recent_wallets = Wallet.objects.filter(
last_activity__gte=timezone.now() - timedelta(days=1)
).exclude(is_contract=True)
# ๋ฑ๋ก๋ ๋ชจ๋ ํ ํฐ
tokens = Token.objects.all()
total_synced = 0
for wallet in recent_wallets[:50]: # ํ ๋ฒ์ ์ต๋ 50๊ฐ ์ง๊ฐ
for token in tokens:
try:
count = TransactionService.sync_token_transactions(
token=token,
from_block=web3_connection.get_latest_block_number() - 100,
to_block='latest',
target_address=wallet.address
)
total_synced += count
except Exception as e:
logger.error(f"๋๊ธฐํ ์คํจ ({wallet.address}, {token.symbol}): {str(e)}")
continue
logger.info(f"์๋ ๋๊ธฐํ ์๋ฃ: {total_synced}๊ฐ ๊ฑฐ๋ ์ ์ฅ")
return total_synced
except Exception as e:
logger.error(f"์๋ ๋๊ธฐํ ์คํจ: {str(e)}")
return 0
@shared_task
def bulk_sync_token_transactions(token_address, from_block, to_block, chunk_size=1000):
"""๋๋ ๊ฑฐ๋ ๋ด์ญ ๋๊ธฐํ (๋ธ๋ก ๋ฒ์๋ฅผ ๋๋ ์ ์ฒ๋ฆฌ)
Args:
token_address: ํ ํฐ ์ปจํธ๋ํธ ์ฃผ์
from_block: ์์ ๋ธ๋ก
to_block: ์ข
๋ฃ ๋ธ๋ก
chunk_size: ํ ๋ฒ์ ์ฒ๋ฆฌํ ๋ธ๋ก ์
"""
try:
from .services import TokenService
logger.info(f"๋๋ ๋๊ธฐํ ์์: {from_block} ~ {to_block}")
token = TokenService.register_token(token_address)
total_saved = 0
# ๋ธ๋ก ๋ฒ์๋ฅผ chunk_size๋ก ๋๋ ์ ์ฒ๋ฆฌ
current_block = from_block
while current_block <= to_block:
end_block = min(current_block + chunk_size, to_block)
try:
count = TransactionService.sync_token_transactions(
token=token,
from_block=current_block,
to_block=end_block
)
total_saved += count
logger.info(f"๋ธ๋ก {current_block}~{end_block}: {count}๊ฐ ์ ์ฅ")
except Exception as e:
logger.error(f"๋ธ๋ก {current_block}~{end_block} ์ฒ๋ฆฌ ์คํจ: {str(e)}")
current_block = end_block + 1
logger.info(f"๋๋ ๋๊ธฐํ ์๋ฃ: ์ด {total_saved}๊ฐ ๊ฑฐ๋ ์ ์ฅ")
return total_saved
except Exception as e:
logger.error(f"๋๋ ๋๊ธฐํ ์คํจ: {str(e)}")
raise
@shared_task
def update_token_prices():
"""ํ ํฐ ๊ฐ๊ฒฉ ์
๋ฐ์ดํธ (์ฃผ๊ธฐ์ ์คํ)"""
# ์ค์ ๋ก๋ CoinGecko API ๋ฑ์ ์ฌ์ฉํด์ ๊ฐ๊ฒฉ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์์ผ ํจ
# ์ฌ๊ธฐ์๋ ๊ฐ๋จํ ์์๋ง ๋ณด์ฌ์ค
try:
logger.info("ํ ํฐ ๊ฐ๊ฒฉ ์
๋ฐ์ดํธ ์์")
tokens = Token.objects.all()
for token in tokens:
# TODO: ์ค์ ๊ฐ๊ฒฉ API ํธ์ถ
logger.debug(f"๊ฐ๊ฒฉ ์
๋ฐ์ดํธ: {token.symbol}")
logger.info("ํ ํฐ ๊ฐ๊ฒฉ ์
๋ฐ์ดํธ ์๋ฃ")
except Exception as e:
logger.error(f"๊ฐ๊ฒฉ ์
๋ฐ์ดํธ ์คํจ: {str(e)}")
๐ Celery ์คํ
Celery๋ฅผ ์คํํ๋ ค๋ฉด ๋ณ๋์ ํฐ๋ฏธ๋์์ ๋ค์ ๋ช ๋ น์ด๋ฅผ ์คํํด:
# Celery Worker ์คํ
celery -A token_tracker worker --loglevel=info
# Celery Beat ์คํ (์ฃผ๊ธฐ์ ์์
์ค์ผ์ค๋ฌ)
celery -A token_tracker beat --loglevel=info
# ๋๋ ๋ ๋ค ํ ๋ฒ์ ์คํ
celery -A token_tracker worker --beat --loglevel=info
# Django view๋ shell์์ task ํธ์ถ
from blockchain.tasks import sync_wallet_transactions_task
# ๋น๋๊ธฐ ์คํ
result = sync_wallet_transactions_task.delay(
wallet_address="0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
token_address="0xdAC17F958D2ee523a2206206994597C13D831ec7",
lookback_blocks=5000
)
# Task ID ํ์ธ
print(f"Task ID: {result.id}")
# Task ์ํ ํ์ธ
print(f"Status: {result.status}")
# ๊ฒฐ๊ณผ ๋๊ธฐ (blocking)
result.get(timeout=300) # ์ต๋ 5๋ถ ๋๊ธฐ
๐ REST API ๊ตฌ์ถํ๊ธฐ
์ด์ ํ๋ก ํธ์๋๋ ์ธ๋ถ ์๋น์ค์์ ์ฐ๋ฆฌ ์์คํ ์ ์ฌ์ฉํ ์ ์๋๋ก REST API๋ฅผ ๋ง๋ค์ด๋ณด์! Django REST Framework๋ฅผ ์ฌ์ฉํ๋ฉด ์ ๋ง ์ฝ๊ฒ ๋ง๋ค ์ ์์ด. ๐จ
๐ฆ Serializer ์์ฑ
Serializer๋ Django ๋ชจ๋ธ์ JSON์ผ๋ก ๋ณํํด์ฃผ๋ ์ญํ ์ ํด:
# api/serializers.py
from rest_framework import serializers
from blockchain.models import Token, Wallet, Transaction
from decimal import Decimal
class TokenSerializer(serializers.ModelSerializer):
"""ํ ํฐ ์๋ฆฌ์ผ๋ผ์ด์ """
transaction_count = serializers.SerializerMethodField()
class Meta:
model = Token
fields = [
'id', 'contract_address', 'name', 'symbol',
'decimals', 'total_supply', 'transaction_count',
'created_at', 'updated_at'
]
def get_transaction_count(self, obj):
return obj.transactions.count()
class WalletSerializer(serializers.ModelSerializer):
"""์ง๊ฐ ์๋ฆฌ์ผ๋ผ์ด์ """
sent_count = serializers.SerializerMethodField()
received_count = serializers.SerializerMethodField()
class Meta:
model = Wallet
fields = [
'id', 'address', 'label', 'is_contract',
'sent_count', 'received_count',
'first_seen', 'last_activity'
]
def get_sent_count(self, obj):
return obj.sent_transactions.count()
def get_received_count(self, obj):
return obj.received_transactions.count()
class TransactionSerializer(serializers.ModelSerializer):
"""๊ฑฐ๋ ๋ด์ญ ์๋ฆฌ์ผ๋ผ์ด์ """
token_symbol = serializers.CharField(source='token.symbol', read_only=True)
token_name = serializers.CharField(source='token.name', read_only=True)
from_address_str = serializers.CharField(source='from_address.address', read_only=True)
to_address_str = serializers.CharField(source='to_address.address', read_only=True)
value_formatted = serializers.SerializerMethodField()
class Meta:
model = Transaction
fields = [
'id', 'transaction_hash', 'token', 'token_symbol', 'token_name',
'from_address', 'from_address_str', 'to_address', 'to_address_str',
'value', 'value_formatted', 'block_number', 'block_timestamp',
'gas_price', 'gas_used', 'transaction_fee', 'status', 'created_at'
]
def get_value_formatted(self, obj):
"""ํ ํฐ decimals๋ฅผ ๊ณ ๋ คํ ์ค์ ๊ฐ"""
decimals = obj.token.decimals
value = Decimal(obj.value) / (Decimal(10) ** decimals)
return str(value)
class TransactionDetailSerializer(TransactionSerializer):
"""๊ฑฐ๋ ์์ธ ์ ๋ณด ์๋ฆฌ์ผ๋ผ์ด์ """
token = TokenSerializer(read_only=True)
from_address = WalletSerializer(read_only=True)
to_address = WalletSerializer(read_only=True)
class Meta(TransactionSerializer.Meta):
fields = TransactionSerializer.Meta.fields
class WalletBalanceSerializer(serializers.Serializer):
"""์ง๊ฐ ์์ก ์๋ฆฌ์ผ๋ผ์ด์ """
token = TokenSerializer(read_only=True)
balance = serializers.DecimalField(max_digits=78, decimal_places=18)
balance_formatted = serializers.CharField()
class SyncRequestSerializer(serializers.Serializer):
"""๋๊ธฐํ ์์ฒญ ์๋ฆฌ์ผ๋ผ์ด์ """
wallet_address = serializers.CharField(max_length=42, required=True)
token_address = serializers.CharField(max_length=42, required=True)
lookback_blocks = serializers.IntegerField(default=1000, min_value=1, max_value=10000)
๐ฏ API View ์์ฑ
# api/views.py
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.pagination import PageNumberPagination
from django.db.models import Q, Count, Sum
from django.shortcuts import get_object_or_404
from blockchain.models import Token, Wallet, Transaction
from blockchain.tasks import sync_wallet_transactions_task
from blockchain.contracts import ERC20Token
from .serializers import (
TokenSerializer, WalletSerializer, TransactionSerializer,
TransactionDetailSerializer, WalletBalanceSerializer,
SyncRequestSerializer
)
import logging
logger = logging.getLogger(__name__)
class StandardResultsSetPagination(PageNumberPagination):
"""ํ์ค ํ์ด์ง๋ค์ด์
"""
page_size = 50
page_size_query_param = 'page_size'
max_page_size = 200
class TokenViewSet(viewsets.ReadOnlyModelViewSet):
"""ํ ํฐ API ViewSet"""
queryset = Token.objects.all()
serializer_class = TokenSerializer
pagination_class = StandardResultsSetPagination
def get_queryset(self):
queryset = super().get_queryset()
# ๊ฒ์ ํํฐ
search = self.request.query_params.get('search', None)
if search:
queryset = queryset.filter(
Q(name__icontains=search) |
Q(symbol__icontains=search) |
Q(contract_address__icontains=search)
)
return queryset.annotate(
transaction_count=Count('transactions')
).order_by('-transaction_count')
@action(detail=True, methods=['get'])
def transactions(self, request, pk=None):
"""ํน์ ํ ํฐ์ ๊ฑฐ๋ ๋ด์ญ ์กฐํ"""
token = self.get_object()
transactions = Transaction.objects.filter(token=token).select_related(
'from_address', 'to_address'
)
page = self.paginate_queryset(transactions)
if page is not None:
serializer = TransactionSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = TransactionSerializer(transactions, many=True)
return Response(serializer.data)
@action(detail=True, methods=['get'])
def stats(self, request, pk=None):
"""ํ ํฐ ํต๊ณ ์ ๋ณด"""
token = self.get_object()
transactions = Transaction.objects.filter(token=token)
stats = {
'total_transactions': transactions.count(),
'total_volume': transactions.aggregate(Sum('value'))['value__sum'] or 0,
'unique_senders': transactions.values('from_address').distinct().count(),
'unique_receivers': transactions.values('to_address').distinct().count(),
'avg_transaction_value': transactions.aggregate(
avg=Sum('value')
)['avg'] or 0,
}
return Response(stats)
class WalletViewSet(viewsets.ReadOnlyModelViewSet):
"""์ง๊ฐ API ViewSet"""
queryset = Wallet.objects.all()
serializer_class = WalletSerializer
pagination_class = StandardResultsSetPagination
lookup_field = 'address'
def get_queryset(self):
queryset = super().get_queryset()
# ์ปจํธ๋ํธ ์ ์ธ ํํฐ
exclude_contracts = self.request.query_params.get('exclude_contracts', 'false')
if exclude_contracts.lower() == 'true':
queryset = queryset.filter(is_contract=False)
return queryset.order_by('-last_activity')
@action(detail=True, methods=['get'])
def transactions(self, request, address=None):
"""ํน์ ์ง๊ฐ์ ๊ฑฐ๋ ๋ด์ญ ์กฐํ"""
wallet = self.get_object()
# ์ก์ /์์ ๊ฑฐ๋ ๋ชจ๋ ์กฐํ
transactions = Transaction.objects.filter(
Q(from_address=wallet) | Q(to_address=wallet)
).select_related('token', 'from_address', 'to_address')
# ํ ํฐ ํํฐ
token_address = request.query_params.get('token', None)
if token_address:
transactions = transactions.filter(token__contract_address=token_address)
# ๋ ์ง ํํฐ
from_date = request.query_params.get('from_date', None)
to_date = request.query_params.get('to_date', None)
if from_date:
transactions = transactions.filter(block_timestamp__gte=from_date)
if to_date:
transactions = transactions.filter(block_timestamp__lte=to_date)
page = self.paginate_queryset(transactions)
if page is not None:
serializer = TransactionSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = TransactionSerializer(transactions, many=True)
return Response(serializer.data)
@action(detail=True, methods=['get'])
def balance(self, request, address=None):
"""์ง๊ฐ์ ํ ํฐ ์์ก ์กฐํ"""
wallet = self.get_object()
token_address = request.query_params.get('token', None)
if not token_address:
return Response(
{'error': 'token ํ๋ผ๋ฏธํฐ๊ฐ ํ์ํฉ๋๋ค'},
status=status.HTTP_400_BAD_REQUEST
)
try:
token = Token.objects.get(contract_address=token_address)
erc20 = ERC20Token(token_address)
balance = erc20.get_balance(wallet.address)
balance_formatted = balance / (10 ** token.decimals)
data = {
'token': TokenSerializer(token).data,
'balance': balance,
'balance_formatted': str(balance_formatted)
}
return Response(data)
except Token.DoesNotExist:
return Response(
{'error': '๋ฑ๋ก๋์ง ์์ ํ ํฐ์
๋๋ค'},
status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
logger.error(f"์์ก ์กฐํ ์คํจ: {str(e)}")
return Response(
{'error': '์์ก ์กฐํ์ ์คํจํ์ต๋๋ค'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@action(detail=True, methods=['post'])
def sync(self, request, address=None):
"""์ง๊ฐ ๊ฑฐ๋ ๋ด์ญ ๋๊ธฐํ ์์ฒญ"""
wallet = self.get_object()
serializer = SyncRequestSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
token_address = serializer.validated_data['token_address']
lookback_blocks = serializer.validated_data['lookback_blocks']
# Celery task ์คํ
task = sync_wallet_transactions_task.delay(
wallet_address=wallet.address,
token_address=token_address,
lookback_blocks=lookback_blocks
)
return Response({
'status': 'processing',
'task_id': task.id,
'message': '๊ฑฐ๋ ๋ด์ญ ๋๊ธฐํ๊ฐ ์์๋์์ต๋๋ค'
}, status=status.HTTP_202_ACCEPTED)
class TransactionViewSet(viewsets.ReadOnlyModelViewSet):
"""๊ฑฐ๋ ๋ด์ญ API ViewSet"""
queryset = Transaction.objects.all().select_related(
'token', 'from_address', 'to_address'
)
pagination_class = StandardResultsSetPagination
lookup_field = 'transaction_hash'
def get_serializer_class(self):
if self.action == 'retrieve':
return TransactionDetailSerializer
return TransactionSerializer
def get_queryset(self):
queryset = super().get_queryset()
# ํ ํฐ ํํฐ
token = self.request.query_params.get('token', None)
if token:
queryset = queryset.filter(token__contract_address=token)
# ์ง๊ฐ ํํฐ
wallet = self.request.query_params.get('wallet', None)
if wallet:
queryset = queryset.filter(
Q(from_address__address=wallet) | Q(to_address__address=wallet)
)
# ์ํ ํํฐ
status_filter = self.request.query_params.get('status', None)
if status_filter:
queryset = queryset.filter(status=status_filter)
# ๋ ์ง ๋ฒ์ ํํฐ
from_date = self.request.query_params.get('from_date', None)
to_date = self.request.query_params.get('to_date', None)
if from_date:
queryset = queryset.filter(block_timestamp__gte=from_date)
if to_date:
queryset = queryset.filter(block_timestamp__lte=to_date)
return queryset.order_by('-block_timestamp')
๐ฃ๏ธ URL ๋ผ์ฐํ
# api/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TokenViewSet, WalletViewSet, TransactionViewSet
router = DefaultRouter()
router.register(r'tokens', TokenViewSet, basename='token')
router.register(r'wallets', WalletViewSet, basename='wallet')
router.register(r'transactions', TransactionViewSet, basename='transaction')
urlpatterns = [
path('', include(router.urls)),
]
# token_tracker/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('api.urls')),
]
์ด์ API๋ฅผ ์ฌ์ฉํ ์ ์์ด! ๋ช ๊ฐ์ง ์์๋ฅผ ๋ณด์:
1. ๋ชจ๋ ํ ํฐ ์กฐํ
GET /api/v1/tokens/
2. ํน์ ํ ํฐ ์ ๋ณด ์กฐํ
GET /api/v1/tokens/{id}/
3. ํ ํฐ์ ๊ฑฐ๋ ๋ด์ญ ์กฐํ
GET /api/v1/tokens/{id}/transactions/
4. ์ง๊ฐ ์ ๋ณด ์กฐํ
GET /api/v1/wallets/{address}/
5. ์ง๊ฐ์ ๊ฑฐ๋ ๋ด์ญ ์กฐํ
GET /api/v1/wallets/{address}/transactions/?token=0x...
6. ์ง๊ฐ ์์ก ์กฐํ
GET /api/v1/wallets/{address}/balance/?token=0x...
7. ๊ฑฐ๋ ๋ด์ญ ๋๊ธฐํ ์์ฒญ
POST /api/v1/wallets/{address}/sync/
Body: {"token_address": "0x...", "lookback_blocks": 5000}
๐จ ํ๋ก ํธ์๋ ๋์๋ณด๋ ๋ง๋ค๊ธฐ
API๋ง ์์ผ๋ฉด ๋ญํด, ์ฌ์ฉ์๊ฐ ๋ณผ ์ ์๋ ํ๋ฉด์ด ์์ด์ผ์ง! ๊ฐ๋จํ ๋์๋ณด๋๋ฅผ ๋ง๋ค์ด๋ณด์. ๐ฅ๏ธ
์ฌ๊ธฐ์๋ Django ํ
ํ๋ฆฟ๊ณผ ๋ฐ๋๋ผ JavaScript๋ฅผ ์ฌ์ฉํ ๊ฑฐ์ผ. ๋ฌผ๋ก React๋ Vue.js๋ฅผ ์ฌ์ฉํด๋ ๋์ง๋ง, ๊ฐ๋จํ๊ฒ ์์ํ๋ ๊ฒ ์ข์์?
๐ ํ ํ๋ฆฟ ์์ฑ
<!-- dashboard/templates/dashboard/index.html -->
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ํ ํฐ ๊ฑฐ๋ ๋ด์ญ ์กฐํ ์์คํ
</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f5f7fa; }
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 12px; margin-bottom: 30px; }
.header h1 { font-size: 2em; margin-bottom: 10px; }
.search-box { background: white; padding: 25px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin-bottom: 30px; }
.search-box input { width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 1em; }
.search-box button { margin-top: 15px; padding: 12px 30px; background: #667eea; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 1em; }
.search-box button:hover { background: #5568d3; }
.results { background: white; padding: 25px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.transaction-item { border-bottom: 1px solid #e0e0e0; padding: 15px 0; }
.transaction-item:last-child { border-bottom: none; }
.tx-hash { font-family: monospace; color: #667eea; font-weight: bold; }
.amount { font-size: 1.2em; font-weight: bold; color: #4caf50; }
.loading { text-align: center; padding: 40px; color: #999; }
.error { background: #ffebee; color: #c62828; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>๐ ํ ํฐ ๊ฑฐ๋ ๋ด์ญ ์กฐํ ์์คํ
</h1>
<p>๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ๋ฅผ ์ค์๊ฐ์ผ๋ก ์กฐํํ์ธ์</p>
</div>
<div class="search-box">
<h2>์ง๊ฐ ์ฃผ์ ๊ฒ์</h2>
<input type="text" id="walletAddress" placeholder="0x๋ก ์์ํ๋ ์ง๊ฐ ์ฃผ์๋ฅผ ์
๋ ฅํ์ธ์">
<input type="text" id="tokenAddress" placeholder="ํ ํฐ ์ปจํธ๋ํธ ์ฃผ์ (์ ํ์ฌํญ)">
<button onclick="searchTransactions()">๊ฒ์</button>
</div>
<div id="errorMessage"></div>
<div class="results" id="results">
<div class="loading">์ง๊ฐ ์ฃผ์๋ฅผ ์
๋ ฅํ๊ณ ๊ฒ์ ๋ฒํผ์ ํด๋ฆญํ์ธ์</div>
</div>
</div>
<script>
async function searchTransactions() {
const walletAddress = document.getElementById('walletAddress').value.trim();
const tokenAddress = document.getElementById('tokenAddress').value.trim();
const resultsDiv = document.getElementById('results');
const errorDiv = document.getElementById('errorMessage');
errorDiv.innerHTML = '';
if (!walletAddress) {
errorDiv.innerHTML = '<div class="error">์ง๊ฐ ์ฃผ์๋ฅผ ์
๋ ฅํด์ฃผ์ธ์</div>';
return;
}
resultsDiv.innerHTML = '<div class="loading">๊ฑฐ๋ ๋ด์ญ์ ์กฐํ ์ค์
๋๋ค...</div>';
try {
let url = `/api/v1/wallets/${walletAddress}/transactions/`;
if (tokenAddress) {
url += `?token=${tokenAddress}`;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error('๊ฑฐ๋ ๋ด์ญ ์กฐํ์ ์คํจํ์ต๋๋ค');
}
const data = await response.json();
displayTransactions(data.results || data);
} catch (error) {
errorDiv.innerHTML = `<div class="error">${error.message}</div>`;
resultsDiv.innerHTML = '';
}
}
function displayTransactions(transactions) {
const resultsDiv = document.getElementById('results');
if (transactions.length === 0) {
resultsDiv.innerHTML = '<div class="loading">๊ฑฐ๋ ๋ด์ญ์ด ์์ต๋๋ค</div>';
return;
}
let html = '<h2>๊ฑฐ๋ ๋ด์ญ (' + transactions.length + '๊ฑด)</h2>';
transactions.forEach(tx => {
const date = new Date(tx.block_timestamp).toLocaleString('ko-KR');
html += `
<div class="transaction-item">
<div class="tx-hash">${tx.transaction_hash}</div>
<div>
<strong>ํ ํฐ:</strong> ${tx.token_symbol} |
<strong>๊ธ์ก:</strong> <span class="amount">${tx.value_formatted}</span> |
<strong>์๊ฐ:</strong> ${date}
</div>
<div>
<strong>From:</strong> ${tx.from_address_str} โ
<strong>To:</strong> ${tx.to_address_str}
</div>
</div>
`;
});
resultsDiv.innerHTML = html;
}
</script>
</body>
</html>
# dashboard/views.py
from django.shortcuts import render
def index(request):
"""๋์๋ณด๋ ๋ฉ์ธ ํ์ด์ง"""
return render(request, 'dashboard/index.html')
# dashboard/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='dashboard'),
]
# token_tracker/urls.py์ ์ถ๊ฐ
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('api.urls')),
path('', include('dashboard.urls')), # ์ถ๊ฐ
]
์ด์ ๋ธ๋ผ์ฐ์ ์์ http://localhost:8000/์ ์ ์ํ๋ฉด ๋์๋ณด๋๋ฅผ ๋ณผ ์ ์์ด! ๐
ํน์ ํ๋ก ํธ์๋ ๋์์ธ์ด ์ด๋ ต๊ฑฐ๋, ๋ ๋ณต์กํ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๊ณ ์ถ๋ค๋ฉด ์ฌ๋ฅ๋ท์์ ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ ์ ๋ฌธ๊ฐ๋ฅผ ์ฐพ์๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ! ์ฌ๋ฅ๋ท์๋ ๋ค์ํ ๋ถ์ผ์ ์ ๋ฌธ๊ฐ๋ค์ด ์์ด์, ํ๋ก์ ํธ์ ํ์ํ ๋์์ ์ฝ๊ฒ ๋ฐ์ ์ ์์ด.
โก ์ฑ๋ฅ ์ต์ ํ ์ ๋ต
์์คํ ์ด ์ ์๋ํ๋ค๊ณ ๋์ด ์๋์ผ! ์ฌ์ฉ์๊ฐ ๋ง์์ง๋ฉด ์ฑ๋ฅ ๋ฌธ์ ๊ฐ ์๊ธธ ์ ์๊ฑฐ๋ . ๋ฏธ๋ฆฌ ์ต์ ํ๋ฅผ ํด๋๋ฉด ๋์ค์ ๊ณ ์์ ๋ ์ ์์ด. ๐
1๏ธโฃ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ ์ต์ ํ
Django ORM์ ์ฌ์ฉํ ๋ ๊ฐ์ฅ ํํ ์ฑ๋ฅ ๋ฌธ์ ๊ฐ N+1 ์ฟผ๋ฆฌ์ผ. ์๋ฅผ ๋ค์ด, 100๊ฐ์ ๊ฑฐ๋ ๋ด์ญ์ ์กฐํํ ๋ ๊ฐ ๊ฑฐ๋๋ง๋ค ํ ํฐ ์ ๋ณด๋ฅผ ๋ฐ๋ก ์กฐํํ๋ฉด ์ด 101๋ฒ์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํด!
โ ๋์ ์:
transactions = Transaction.objects.all()
for tx in transactions:
print(tx.token.name) # ๋งค๋ฒ DB ์กฐํ!
โ
์ข์ ์:transactions = Transaction.objects.select_related('token', 'from_address', 'to_address')
for tx in transactions:
print(tx.token.name) # ์ด๋ฏธ ๋ก๋๋จ!
2๏ธโฃ Redis ์บ์ฑ ํ์ฉ
์์ฃผ ์กฐํ๋๋ ๋ฐ์ดํฐ๋ Redis์ ์บ์ฑํ๋ฉด ์๋ต ์๋๊ฐ ์์ฒญ ๋นจ๋ผ์ ธ:
# blockchain/cache.py
from django.core.cache import cache
from django.conf import settings
import json
class TransactionCache:
"""๊ฑฐ๋ ๋ด์ญ ์บ์ ๊ด๋ฆฌ"""
CACHE_TTL = 300 # 5๋ถ
@staticmethod
def get_wallet_transactions_key(wallet_address, token_address=None):
"""์บ์ ํค ์์ฑ"""
if token_address:
return f"wallet_tx:{wallet_address}:{token_address}"
return f"wallet_tx:{wallet_address}:all"
@classmethod
def get_transactions(cls, wallet_address, token_address=None):
"""์บ์์์ ๊ฑฐ๋ ๋ด์ญ ์กฐํ"""
key = cls.get_wallet_transactions_key(wallet_address, token_address)
cached_data = cache.get(key)
if cached_data:
return json.loads(cached_data)
return None
@classmethod
def set_transactions(cls, wallet_address, transactions, token_address=None):
"""๊ฑฐ๋ ๋ด์ญ ์บ์์ ์ ์ฅ"""
key = cls.get_wallet_transactions_key(wallet_address, token_address)
cache.set(key, json.dumps(transactions), cls.CACHE_TTL)
@classmethod
def invalidate(cls, wallet_address, token_address=None):
"""์บ์ ๋ฌดํจํ"""
key = cls.get_wallet_transactions_key(wallet_address, token_address)
cache.delete(key)
API View์์ ์บ์ ์ฌ์ฉ:
# api/views.py์ ์ถ๊ฐ
from blockchain.cache import TransactionCache
class WalletViewSet(viewsets.ReadOnlyModelViewSet):
# ... ๊ธฐ์กด ์ฝ๋ ...
@action(detail=True, methods=['get'])
def transactions(self, request, address=None):
"""ํน์ ์ง๊ฐ์ ๊ฑฐ๋ ๋ด์ญ ์กฐํ (์บ์ฑ ์ ์ฉ)"""
wallet = self.get_object()
token_address = request.query_params.get('token', None)
# ์บ์ ํ์ธ
cached_data = TransactionCache.get_transactions(
wallet.address,
token_address
)
if cached_data:
return Response(cached_data)
# ์บ์ ๋ฏธ์ค - DB์์ ์กฐํ
transactions = Transaction.objects.filter(
Q(from_address=wallet) | Q(to_address=wallet)
).select_related('token', 'from_address', 'to_address')
if token_address:
transactions = transactions.filter(token__contract_address=token_address)
serializer = TransactionSerializer(transactions[:100], many=True)
# ์บ์์ ์ ์ฅ
TransactionCache.set_transactions(
wallet.address,
serializer.data,
token_address
)
return Response(serializer.data)
3๏ธโฃ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธ๋ฑ์ค ์ถ๊ฐ
์์ฃผ ๊ฒ์ํ๋ ํ๋์ ์ธ๋ฑ์ค๋ฅผ ์ถ๊ฐํ๋ฉด ์กฐํ ์๋๊ฐ ํฌ๊ฒ ํฅ์๋ผ:
# ๋ง์ด๊ทธ๋ ์ด์
ํ์ผ ์์ฑ
python manage.py makemigrations --empty blockchain
# ์์ฑ๋ ๋ง์ด๊ทธ๋ ์ด์
ํ์ผ ์์
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blockchain', '0001_initial'),
]
operations = [
# ๋ณตํฉ ์ธ๋ฑ์ค ์ถ๊ฐ
migrations.AddIndex(
model_name='transaction',
index=models.Index(
fields=['from_address', 'token', '-block_timestamp'],
name='tx_from_token_time_idx'
),
),
migrations.AddIndex(
model_name='transaction',
index=models.Index(
fields=['to_address', 'token', '-block_timestamp'],
name='tx_to_token_time_idx'
),
),
]
4๏ธโฃ ํ์ด์ง๋ค์ด์ ์ต์ ํ
๋๋์ ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ์กฐํํ๋ฉด ๋ฉ๋ชจ๋ฆฌ์ ๋คํธ์ํฌ ๋์ญํญ์ ๋ง์ด ์ฌ์ฉํด. ์ปค์ ๊ธฐ๋ฐ ํ์ด์ง๋ค์ด์ ์ ์ฌ์ฉํ๋ฉด ๋ ํจ์จ์ ์ด์ผ:
# api/pagination.py
from rest_framework.pagination import CursorPagination
class TransactionCursorPagination(CursorPagination):
"""์ปค์ ๊ธฐ๋ฐ ํ์ด์ง๋ค์ด์
"""
page_size = 50
ordering = '-block_timestamp'
cursor_query_param = 'cursor'
5๏ธโฃ ๋ฐฐ์น ์ฒ๋ฆฌ ์ต์ ํ
๋๋์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ๋๋ bulk_create๋ฅผ ์ฌ์ฉํ๋ฉด ํจ์ฌ ๋นจ๋ผ:
# blockchain/services.py ์์
@staticmethod
def sync_token_transactions_bulk(token, from_block, to_block):
"""๋๋ ๊ฑฐ๋ ๋ด์ญ ๋๊ธฐํ (์ต์ ํ ๋ฒ์ )"""
try:
w3 = web3_connection.w3
erc20 = ERC20Token(token.contract_address)
events = erc20.get_transfer_events(from_block, to_block)
# ๊ธฐ์กด ๊ฑฐ๋ ํด์ ์กฐํ (ํ ๋ฒ๋ง)
existing_hashes = set(
Transaction.objects.filter(
transaction_hash__in=[e['transactionHash'].hex() for e in events]
).values_list('transaction_hash', flat=True)
)
# ์๋ก์ด ๊ฑฐ๋๋ง ํํฐ๋ง
new_events = [e for e in events if e['transactionHash'].hex() not in existing_hashes]
if not new_events:
return 0
# ์ง๊ฐ ์ฃผ์ ์ผ๊ด ์์ฑ
addresses = set()
for event in new_events:
addresses.add(event['args']['from'])
addresses.add(event['args']['to'])
existing_wallets = {
w.address: w for w in Wallet.objects.filter(address__in=addresses)
}
new_wallets = []
for addr in addresses:
if addr not in existing_wallets:
new_wallets.append(Wallet(address=addr))
if new_wallets:
Wallet.objects.bulk_create(new_wallets, ignore_conflicts=True)
# ๋ค์ ์กฐํ
existing_wallets = {
w.address: w for w in Wallet.objects.filter(address__in=addresses)
}
# ๊ฑฐ๋ ๊ฐ์ฒด ์์ฑ
transactions = []
for event in new_events:
tx = w3.eth.get_transaction(event['transactionHash'])
receipt = w3.eth.get_transaction_receipt(event['transactionHash'])
block = w3.eth.get_block(event['blockNumber'])
transactions.append(Transaction(
transaction_hash=event['transactionHash'].hex(),
token=token,
from_address=existing_wallets[event['args']['from']],
to_address=existing_wallets[event['args']['to']],
value=event['args']['value'],
block_number=event['blockNumber'],
block_timestamp=datetime.fromtimestamp(block['timestamp'], tz=timezone.utc),
gas_price=tx['gasPrice'],
gas_used=receipt['gasUsed'],
transaction_fee=Decimal(tx['gasPrice'] * receipt['gasUsed']) / Decimal(10**18),
status='success' if receipt['status'] == 1 else 'failed'
))
# ์ผ๊ด ์ ์ฅ
Transaction.objects.bulk_create(transactions, batch_size=1000)
return len(transactions)
except Exception as e:
logger.error(f"๋๋ ๋๊ธฐํ ์คํจ: {str(e)}")
raise
๊ณผ๋ํ ์ต์ ํ๋ ์คํ๋ ค ์ฝ๋๋ฅผ ๋ณต์กํ๊ฒ ๋ง๋ค ์ ์์ด. ๋จผ์ ํ๋กํ์ผ๋ง์ ํตํด ์ค์ ๋ณ๋ชฉ ์ง์ ์ ์ฐพ๊ณ , ๊ทธ ๋ถ๋ถ๋ง ์ต์ ํํ๋ ๊ฒ ์ข์. "์กฐ๊ธฐ ์ต์ ํ๋ ๋ชจ๋ ์ ์ ๊ทผ์"์ด๋ผ๋ ๋ง๋ ์์์!
๐ ๋ณด์ ๊ณ ๋ ค์ฌํญ
๋ธ๋ก์ฒด์ธ ์์คํ ์ ๋ค๋ฃจ๋ค ๋ณด๋ฉด ๋ณด์์ด ์ ๋ง ์ค์ํด. ํนํ API ํค๋ ๊ฐ์ธ ์ ๋ณด๋ฅผ ๋ค๋ฃฐ ๋๋ ๋์ฑ ์กฐ์ฌํด์ผ ํด! ๐ก๏ธ
1๏ธโฃ ํ๊ฒฝ ๋ณ์ ๋ณดํธ
โ API ํค๋ฅผ ์ฝ๋์ ํ๋์ฝ๋ฉ
โ .env ํ์ผ์ Git์ ์ปค๋ฐ
โ ํ๋ก๋์ ํ๊ฒฝ์์ DEBUG=True
โ SECRET_KEY๋ฅผ ๊ณต๊ฐ ์ ์ฅ์์ ์ ๋ก๋
โ ์ฌ๋ฐ๋ฅธ ๋ฐฉ๋ฒ:
โข .env ํ์ผ์ .gitignore์ ์ถ๊ฐ
โข ํ๊ฒฝ๋ณ๋ก ๋ค๋ฅธ ์ค์ ํ์ผ ์ฌ์ฉ
โข AWS Secrets Manager๋ ํ๊ฒฝ ๋ณ์ ์ฌ์ฉ
โข ์ ๊ธฐ์ ์ผ๋ก API ํค ๋กํ ์ด์
2๏ธโฃ API ์์ฒญ ์ ํ (Rate Limiting)
๋ฌด๋ถ๋ณํ API ํธ์ถ์ ๋ฐฉ์งํ๊ธฐ ์ํด ์์ฒญ ์ ํ์ ๊ฑธ์ด์ผ ํด:
# settings.py์ ์ถ๊ฐ
REST_FRAMEWORK = {
# ... ๊ธฐ์กด ์ค์ ...
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/hour', # ์ต๋ช
์ฌ์ฉ์: ์๊ฐ๋น 100ํ
'user': '1000/hour', # ์ธ์ฆ ์ฌ์ฉ์: ์๊ฐ๋น 1000ํ
}
}
3๏ธโฃ ์ ๋ ฅ ๊ฐ ๊ฒ์ฆ
์ฌ์ฉ์ ์ ๋ ฅ์ ํญ์ ๊ฒ์ฆํด์ผ ํด. ํนํ ๋ธ๋ก์ฒด์ธ ์ฃผ์๋ ํ์์ด ์ ํด์ ธ ์์ผ๋๊น ํ์คํ ์ฒดํฌํ์:
# api/validators.py
from rest_framework import serializers
from web3 import Web3
def validate_ethereum_address(value):
"""์ด๋๋ฆฌ์ ์ฃผ์ ๊ฒ์ฆ"""
if not value:
raise serializers.ValidationError("์ฃผ์๋ฅผ ์
๋ ฅํด์ฃผ์ธ์")
if not value.startswith('0x'):
raise serializers.ValidationError("์ฃผ์๋ 0x๋ก ์์ํด์ผ ํฉ๋๋ค")
if len(value) != 42:
raise serializers.ValidationError("์ฃผ์ ๊ธธ์ด๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค")
if not Web3.is_address(value):
raise serializers.ValidationError("์ ํจํ์ง ์์ ์ด๋๋ฆฌ์ ์ฃผ์์
๋๋ค")
return Web3.to_checksum_address(value)
# serializers.py์์ ์ฌ์ฉ
class WalletAddressSerializer(serializers.Serializer):
address = serializers.CharField(validators=[validate_ethereum_address])
4๏ธโฃ CORS ์ค์
ํ๋ก ํธ์๋๊ฐ ๋ค๋ฅธ ๋๋ฉ์ธ์ ์๋ค๋ฉด CORS ์ค์ ์ด ํ์ํด:
# settings.py
INSTALLED_APPS = [
# ...
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
# ...
]
# ๊ฐ๋ฐ ํ๊ฒฝ
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000",
]
# ํ๋ก๋์
ํ๊ฒฝ์์๋ ์ค์ ๋๋ฉ์ธ๋ง ํ์ฉ
# CORS_ALLOWED_ORIGINS = [
# "https://yourdomain.com",
# ]
5๏ธโฃ SQL Injection ๋ฐฉ์ง
Django ORM์ ์ฌ์ฉํ๋ฉด ๋๋ถ๋ถ ์๋์ผ๋ก ๋ฐฉ์ง๋์ง๋ง, raw SQL์ ์ฌ์ฉํ ๋๋ ์กฐ์ฌํด์ผ ํด:
# โ ์ํํ ์ฝ๋
def get_transactions_unsafe(wallet_address):
query = f"SELECT * FROM transactions WHERE from_address = '{wallet_address}'"
return Transaction.objects.raw(query)
# โ
์์ ํ ์ฝ๋
def get_transactions_safe(wallet_address):
query = "SELECT * FROM transactions WHERE from_address = %s"
return Transaction.objects.raw(query, [wallet_address])
- โ ๋ชจ๋ API ์๋ํฌ์ธํธ์ ์ธ์ฆ/์ธ๊ฐ ์ ์ฉ
- โ HTTPS ์ฌ์ฉ (ํ๋ก๋์ ํ์)
- โ ์ ๊ธฐ์ ์ธ ์์กด์ฑ ํจํค์ง ์ ๋ฐ์ดํธ
- โ ๋ก๊น ๋ฐ ๋ชจ๋ํฐ๋ง ์์คํ ๊ตฌ์ถ
- โ ์๋ฌ ๋ฉ์์ง์ ๋ฏผ๊ฐํ ์ ๋ณด ๋ ธ์ถ ๊ธ์ง
- โ ์ ๊ธฐ์ ์ธ ๋ณด์ ๊ฐ์ฌ
๐งช ํ ์คํธ ์ฝ๋ ์์ฑ
ํ ์คํธ ์ฝ๋๋ ๊ท์ฐฎ์ ๋ณด์ด์ง๋ง, ๋์ค์ ๋ฒ๊ทธ๋ฅผ ์ฐพ๊ณ ์์ ํ๋ ์๊ฐ์ ์์ฒญ ์ค์ฌ์ค. ํนํ ๋ธ๋ก์ฒด์ธ ๊ฐ์ ๋ณต์กํ ์์คํ ์์๋ ํ์์ผ! ๐ฌ
๐ ๋จ์ ํ ์คํธ (Unit Test)
# blockchain/tests/test_models.py
from django.test import TestCase
from django.utils import timezone
from decimal import Decimal
from blockchain.models import Token, Wallet, Transaction
class TokenModelTest(TestCase):
"""Token ๋ชจ๋ธ ํ
์คํธ"""
def setUp(self):
self.token = Token.objects.create(
contract_address="0x1234567890123456789012345678901234567890",
name="Test Token",
symbol="TEST",
decimals=18,
total_supply=1000000
)
def test_token_creation(self):
"""ํ ํฐ ์์ฑ ํ
์คํธ"""
self.assertEqual(self.token.name, "Test Token")
self.assertEqual(self.token.symbol, "TEST")
self.assertEqual(self.token.decimals, 18)
def test_token_str(self):
"""ํ ํฐ ๋ฌธ์์ด ํํ ํ
์คํธ"""
self.assertEqual(str(self.token), "Test Token (TEST)")
class TransactionModelTest(TestCase):
"""Transaction ๋ชจ๋ธ ํ
์คํธ"""
def setUp(self):
self.token = Token.objects.create(
contract_address="0x1234567890123456789012345678901234567890",
name="Test Token",
symbol="TEST",
decimals=18
)
self.from_wallet = Wallet.objects.create(
address="0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
)
self.to_wallet = Wallet.objects.create(
address="0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
)
self.transaction = Transaction.objects.create(
transaction_hash="0xabcdef1234567890",
token=self.token,
from_address=self.from_wallet,
to_address=self.to_wallet,
value=Decimal("1000000000000000000"), # 1 token
block_number=12345678,
block_timestamp=timezone.now(),
gas_price=20000000000,
gas_used=21000,
transaction_fee=Decimal("0.00042"),
status='success'
)
def test_transaction_creation(self):
"""๊ฑฐ๋ ์์ฑ ํ
์คํธ"""
self.assertEqual(self.transaction.token, self.token)
self.assertEqual(self.transaction.from_address, self.from_wallet)
self.assertEqual(self.transaction.to_address, self.to_wallet)
self.assertEqual(self.transaction.status, 'success')
def test_value_formatted(self):
"""ํฌ๋งท๋ ๊ฐ ํ
์คํธ"""
# 1000000000000000000 / 10^18 = 1.0
self.assertEqual(self.transaction.value_formatted, Decimal('1.0'))
๐ ํตํฉ ํ ์คํธ (Integration Test)
# blockchain/tests/test_services.py
from django.test import TestCase
from unittest.mock import patch, MagicMock
from blockchain.services import TokenService, TransactionService
from blockchain.models import Token
class TokenServiceTest(TestCase):
"""TokenService ํ
์คํธ"""
@patch('blockchain.services.ERC20Token')
def test_register_token(self, mock_erc20):
"""ํ ํฐ ๋ฑ๋ก ํ
์คํธ"""
# Mock ์ค์
mock_instance = MagicMock()
mock_instance.get_token_info.return_value = {
'address': '0x1234567890123456789012345678901234567890',
'name': 'Mock Token',
'symbol': 'MOCK',
'decimals': 18,
'total_supply': 1000000
}
mock_erc20.return_value = mock_instance
# ํ ํฐ ๋ฑ๋ก
token = TokenService.register_token(
'0x1234567890123456789012345678901234567890'
)
# ๊ฒ์ฆ
self.assertIsNotNone(token)
self.assertEqual(token.name, 'Mock Token')
self.assertEqual(token.symbol, 'MOCK')
# ์ค๋ณต ๋ฑ๋ก ํ
์คํธ
token2 = TokenService.register_token(
'0x1234567890123456789012345678901234567890'
)
self.assertEqual(token.id, token2.id)
๐ API ํ ์คํธ
# api/tests/test_views.py
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APIClient
from rest_framework import status
from blockchain.models import Token, Wallet, Transaction
from django.utils import timezone
from decimal import Decimal
class TokenAPITest(TestCase):
"""Token API ํ
์คํธ"""
def setUp(self):
self.client = APIClient()
self.token = Token.objects.create(
contract_address="0x1234567890123456789012345678901234567890",
name="Test Token",
symbol="TEST",
decimals=18
)
def test_list_tokens(self):
"""ํ ํฐ ๋ชฉ๋ก ์กฐํ ํ
์คํธ"""
url = reverse('token-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)
self.assertEqual(response.data['results'][0]['symbol'], 'TEST')
def test_retrieve_token(self):
"""ํ ํฐ ์์ธ ์กฐํ ํ
์คํธ"""
url = reverse('token-detail', args=[self.token.id])
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['name'], 'Test Token')
class WalletAPITest(TestCase):
"""Wallet API ํ
์คํธ"""
def setUp(self):
self.client = APIClient()
self.wallet = Wallet.objects.create(
address="0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
label="Test Wallet"
)
def test_retrieve_wallet(self):
"""์ง๊ฐ ์กฐํ ํ
์คํธ"""
url = reverse('wallet-detail', args=[self.wallet.address])
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['address'], self.wallet.address)
self.assertEqual(response.data['label'], 'Test Wallet')
ํ ์คํธ ์คํ:
# ๋ชจ๋ ํ
์คํธ ์คํ
python manage.py test
# ํน์ ์ฑ๋ง ํ
์คํธ
python manage.py test blockchain
# ์ปค๋ฒ๋ฆฌ์ง ์ธก์
pip install coverage
coverage run --source='.' manage.py test
coverage report
coverage html # HTML ๋ฆฌํฌํธ ์์ฑ
โข ๊ฐ ํจ์/๋ฉ์๋๋ง๋ค ์ต์ 1๊ฐ์ ํ ์คํธ ์์ฑ
โข ์ ์ ์ผ์ด์ค์ ์์ธ ์ผ์ด์ค ๋ชจ๋ ํ ์คํธ
โข Mock์ ํ์ฉํด์ ์ธ๋ถ ์์กด์ฑ ์ ๊ฑฐ
โข ํ ์คํธ๋ ๋ ๋ฆฝ์ ์ด๊ณ ๋ฐ๋ณต ๊ฐ๋ฅํด์ผ ํจ
โข CI/CD ํ์ดํ๋ผ์ธ์ ํ ์คํธ ์๋ํ ํฌํจ
๐ ๋ฐฐํฌ ๋ฐ ์ด์
๊ฐ๋ฐ์ด ๋๋ฌ๋ค๋ฉด ์ด์ ์ค์ ์๋ฒ์ ๋ฐฐํฌํ ์ฐจ๋ก์ผ! ์ฌ๊ธฐ์๋ ๊ฐ๋จํ ๋ฐฐํฌ ๋ฐฉ๋ฒ์ ์๊ฐํ ๊ฒ. โ๏ธ
๐ณ Docker๋ฅผ ์ด์ฉํ ๋ฐฐํฌ
Docker๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋ค ํ๊ฒฝ์์๋ ๋์ผํ๊ฒ ์คํํ ์ ์์ด:
# Dockerfile
FROM python:3.10-slim
# ์์
๋๋ ํ ๋ฆฌ ์ค์
WORKDIR /app
# ์์คํ
ํจํค์ง ์ค์น
RUN apt-get update && apt-get install -y \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Python ํจํค์ง ์ค์น
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# ์ ํ๋ฆฌ์ผ์ด์
์ฝ๋ ๋ณต์ฌ
COPY . .
# ์ ์ ํ์ผ ์์ง
RUN python manage.py collectstatic --noinput
# ํฌํธ ๋
ธ์ถ
EXPOSE 8000
# ์คํ ๋ช
๋ น
CMD ["gunicorn", "token_tracker.wsgi:application", "--bind", "0.0.0.0:8000"]
# docker-compose.yml
version: '3.8'
services:
db:
image: postgres:14
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=token_tracker
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=your_password
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
web:
build: .
command: gunicorn token_tracker.wsgi:application --bind 0.0.0.0:8000
volumes:
- .:/app
- static_volume:/app/staticfiles
ports:
- "8000:8000"
env_file:
- .env
depends_on:
- db
- redis
celery:
build: .
command: celery -A token_tracker worker --loglevel=info
volumes:
- .:/app
env_file:
- .env
depends_on:
- db
- redis
celery-beat:
build: .
command: celery -A token_tracker beat --loglevel=info
volumes:
- .:/app
env_file:
- .env
depends_on:
- db
- redis
volumes:
postgres_data:
static_volume:
Docker Compose๋ก ์คํ:
# ๋น๋ ๋ฐ ์คํ
docker-compose up -d
# ๋ง์ด๊ทธ๋ ์ด์
docker-compose exec web python manage.py migrate
# ์ํผ์ ์ ์์ฑ
docker-compose exec web python manage.py createsuperuser
# ๋ก๊ทธ ํ์ธ
docker-compose logs -f web
# ์ค์ง
docker-compose down
โ๏ธ ํด๋ผ์ฐ๋ ๋ฐฐํฌ (AWS ์์)
AWS EC2์์ Ubuntu ์๋ฒ ์ธ์คํด์ค๋ฅผ ์์ฑํด. t3.medium ์ ๋๋ฉด ์ถฉ๋ถํด.
PostgreSQL RDS ์ธ์คํด์ค๋ฅผ ์์ฑํ๊ณ , ๋ณด์ ๊ทธ๋ฃน์์ EC2์์์ ์ ๊ทผ์ ํ์ฉํด.
Redis ํด๋ฌ์คํฐ๋ฅผ ์์ฑํ๊ณ EC2์์ ์ ๊ทผ ๊ฐ๋ฅํ๋๋ก ์ค์ ํด.
# EC2์ ์ ์
ssh -i your-key.pem ubuntu@your-ec2-ip
# Docker ์ค์น
sudo apt-get update
sudo apt-get install docker.io docker-compose
# ์ฝ๋ ํด๋ก
git clone your-repository-url
cd token_tracker
# ํ๊ฒฝ ๋ณ์ ์ค์
nano .env
# ๋ฐฐํฌ
docker-compose up -d
# /etc/nginx/sites-available/token_tracker
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /static/ {
alias /path/to/staticfiles/;
}
}
๐ ๋ชจ๋ํฐ๋ง ์ค์
์์คํ ์ด ์ ์๋ํ๋์ง ๋ชจ๋ํฐ๋งํ๋ ๊ฒ๋ ์ค์ํด:
# requirements.txt์ ์ถ๊ฐ
sentry-sdk==1.40.0
django-prometheus==2.3.1
# settings.py์ ์ถ๊ฐ
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
sentry_sdk.init(
dsn="your-sentry-dsn",
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
send_default_pii=True
)
INSTALLED_APPS = [
# ...
'django_prometheus',
]
MIDDLEWARE = [
'django_prometheus.middleware.PrometheusBeforeMiddleware',
# ... ๊ธฐ์กด ๋ฏธ๋ค์จ์ด ...
'django_prometheus.middleware.PrometheusAfterMiddleware',
]
- โ DEBUG = False ์ค์
- โ ALLOWED_HOSTS ์ค์
- โ SECRET_KEY ๋ณ๊ฒฝ
- โ HTTPS ์ ์ฉ (Let's Encrypt)
- โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฐฑ์ ์๋ํ
- โ ๋ก๊ทธ ๋กํ ์ด์ ์ค์
- โ ๋ฐฉํ๋ฒฝ ์ค์
- โ ์ ๊ธฐ์ ์ธ ๋ณด์ ์ ๋ฐ์ดํธ
๐ ์ค์ ํ๋ก์ ํธ ํ์ฅ ์์ด๋์ด
๊ธฐ๋ณธ ์์คํ ์ ๋ง๋ค์๋ค๋ฉด, ์ด์ ๋ ์ฌ๋ฏธ์๋ ๊ธฐ๋ฅ๋ค์ ์ถ๊ฐํด๋ณผ ์ ์์ด! ๐ก
1๏ธโฃ ์ค์๊ฐ ์๋ฆผ ์์คํ
ํน์ ์กฐ๊ฑด์ด ๋ง์กฑ๋๋ฉด ์ฌ์ฉ์์๊ฒ ์๋ฆผ์ ๋ณด๋ด๋ ๊ธฐ๋ฅ:
โข ํน์ ์ง๊ฐ์์ ๋๋ ๊ฑฐ๋ ๋ฐ์ ์ ์๋ฆผ
โข ํ ํฐ ๊ฐ๊ฒฉ์ด ํน์ ์์ค์ ๋๋ฌํ๋ฉด ์๋ฆผ
โข ์๋ก์ด ํ ํฐ ์ปจํธ๋ํธ ๋ฐฐํฌ ๊ฐ์ง
โข WebSocket์ ์ด์ฉํ ์ค์๊ฐ ์ ๋ฐ์ดํธ
โข ์ด๋ฉ์ผ, SMS, ํ ๋ ๊ทธ๋จ ๋ด ์ฐ๋
2๏ธโฃ ๋ฐ์ดํฐ ๋ถ์ ๋ฐ ์๊ฐํ
๊ฑฐ๋ ๋ฐ์ดํฐ๋ฅผ ๋ถ์ํด์ ์ ์ฉํ ์ธ์ฌ์ดํธ๋ฅผ ์ ๊ณต:
โข ๊ฑฐ๋๋ ์ถ์ด ๊ทธ๋ํ
โข ์์ ๊ฑฐ๋์ ๋ญํน
โข ํ ํฐ ๋ณด์ ์ ๋ถํฌ ๋ถ์
โข ๊ฑฐ๋ ํจํด ๋ถ์ (์๊ฐ๋๋ณ, ์์ผ๋ณ)
โข ๋คํธ์ํฌ ๊ทธ๋ํ (์ง๊ฐ ๊ฐ ์ฐ๊ฒฐ ๊ด๊ณ)
โข Chart.js, D3.js๋ฅผ ์ด์ฉํ ์๊ฐํ
3๏ธโฃ DeFi ํ๋กํ ์ฝ ํตํฉ
Uniswap, Aave ๊ฐ์ DeFi ํ๋กํ ์ฝ๊ณผ ์ฐ๋:
โข DEX ๊ฑฐ๋ ๋ด์ญ ์ถ์
โข ์ ๋์ฑ ํ ๋ชจ๋ํฐ๋ง
โข ๋์ถ/์์น ๋ด์ญ ์กฐํ
โข ์์ต๋ฅ ๊ณ์ฐ
โข ๊ฐ์ค๋น ์ต์ ํ ์ถ์ฒ
4๏ธโฃ NFT ๊ฑฐ๋ ์ถ์
ERC-721, ERC-1155 NFT ๊ฑฐ๋๋ ์ถ์ :
โข NFT ๊ฑฐ๋ ๋ด์ญ ์กฐํ
โข ์ปฌ๋ ์ ๋ณ ํต๊ณ
โข ํฌ๊ท๋ ๋ถ์
โข ๊ฐ๊ฒฉ ์ถ์ด ๊ทธ๋ํ
โข ๋ฉํ๋ฐ์ดํฐ ์กฐํ ๋ฐ ํ์
5๏ธโฃ ์ธ๊ธ ์ ๊ณ ๋์ฐ๋ฏธ
์ํธํํ ๊ฑฐ๋ ์ธ๊ธ ์ ๊ณ ๋ฅผ ๋์์ฃผ๋ ๊ธฐ๋ฅ:
โข ๊ฑฐ๋ ์์ต ๊ณ์ฐ (FIFO, LIFO)
โข ์ฐ๊ฐ ๊ฑฐ๋ ๋ด์ญ ๋ฆฌํฌํธ
โข CSV/Excel ๋ด๋ณด๋ด๊ธฐ
โข ๊ตญ๊ฐ๋ณ ์ธ๋ฒ ์ ์ฉ
โข ์ธ๋ฌด์ฌ ์ฐ๋ (์ฌ๋ฅ๋ท์์ ์ฐพ์ ์ ์์ด!)
6๏ธโฃ ๋ฉํฐ์ฒด์ธ ์ง์
์ด๋๋ฆฌ์๋ฟ๋ง ์๋๋ผ ๋ค๋ฅธ ๋ธ๋ก์ฒด์ธ๋ ์ง์:
โข Binance Smart Chain (BSC)
โข Polygon
โข Avalanche
โข Arbitrum
โข Optimism
โข Solana (๋ค๋ฅธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํ์)
๋ฉํฐ์ฒด์ธ ์ง์์ ์ํ ๊ตฌ์กฐ ์์ :
# blockchain/models.py์ ์ถ๊ฐ
class Blockchain(models.Model):
"""๋ธ๋ก์ฒด์ธ ๋คํธ์ํฌ ๋ชจ๋ธ"""
CHAIN_CHOICES = [
('ethereum', 'Ethereum'),
('bsc', 'Binance Smart Chain'),
('polygon', 'Polygon'),
('avalanche', 'Avalanche'),
]
name = models.CharField(max_length=50, choices=CHAIN_CHOICES, unique=True)
chain_id = models.IntegerField(unique=True)
rpc_url = models.URLField()
explorer_url = models.URLField()
is_active = models.BooleanField(default=True)
def __str__(self):
return self.get_name_display()
# Token ๋ชจ๋ธ์ blockchain ํ๋ ์ถ๊ฐ
class Token(models.Model):
blockchain = models.ForeignKey(
Blockchain,
on_delete=models.CASCADE,
related_name='tokens'
)
# ... ๊ธฐ์กด ํ๋๋ค ...
์ด๋ฐ ํ์ฅ ๊ธฐ๋ฅ๋ค์ ํผ์ ๋ค ๊ตฌํํ๊ธฐ๋ ๋ถ๋ด์ค๋ฌ์ธ ์ ์์ด. ์ฌ๋ฅ๋ท์์ ๊ฐ ๋ถ์ผ์ ์ ๋ฌธ๊ฐ๋ฅผ ์ฐพ์์ ํ์ ํ๋ฉด ํจ์ฌ ๋น ๋ฅด๊ณ ํ๋ฆฌํฐ ๋์ ๊ฒฐ๊ณผ๋ฌผ์ ๋ง๋ค ์ ์์ด!
์๋ฅผ ๋ค์ด:
โข ํ๋ก ํธ์๋ ๋์์ด๋: ๋ฉ์ง UI/UX ๋์์ธ
โข ๋ฐ์ดํฐ ๋ถ์๊ฐ: ๊ณ ๊ธ ๋ถ์ ์๊ณ ๋ฆฌ์ฆ
โข ๋ธ๋ก์ฒด์ธ ์ ๋ฌธ๊ฐ: ์ค๋งํธ ์ปจํธ๋ํธ ๊ฐ์ฌ
โข ๋ง์ผํฐ: ์๋น์ค ํ๋ณด ์ ๋ต
๐ฏ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ค๋๋ผ ์๊ณ ํ์ด! ๐
์ฐ๋ฆฌ๋ ํ์ด์ฌ๊ณผ ์ฅ๊ณ ๋ฅผ ์ฌ์ฉํด์ ํ ํฐ ๊ฑฐ๋ ๋ด์ญ ์กฐํ ์์คํ
์ ์ฒ์๋ถํฐ ๋๊น์ง ๋ง๋ค์ด๋ดค์ด. ๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ ๊ตฌ์กฐ ์ดํด๋ถํฐ ์์ํด์, Web3.py๋ก ๋ฐ์ดํฐ ์กฐํํ๊ณ , Django๋ก ๋ฐฑ์๋ ๊ตฌ์ถํ๊ณ , REST API ๋ง๋ค๊ณ , ์ฌ์ง์ด ๋ฐฐํฌ๊น์ง!
์ด ์์คํ
์ ๋จ์ํ ๊ฑฐ๋ ๋ด์ญ์ ๋ณด์ฌ์ฃผ๋ ๊ฒ ์ด์์ ๊ฐ์น๊ฐ ์์ด. ๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃจ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ ๊ณ , ๋์ฉ๋ ๋ฐ์ดํฐ๋ฅผ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ๋ ์ตํ์ง. ๊ทธ๋ฆฌ๊ณ ๋ฌด์๋ณด๋ค ์ค์ ๋ก ์๋ํ๋ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์๋ค๋ ๊ฒ ์ค์ํด!
๐ ํต์ฌ ์์ฝ
1. ๋ธ๋ก์ฒด์ธ ๊ธฐ์ด
โข ํ ํฐ๊ณผ ์ค๋งํธ ์ปจํธ๋ํธ์ ์๋ ์๋ฆฌ
โข ERC-20 ํ์ค๊ณผ Transfer ์ด๋ฒคํธ
โข ๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ ๊ตฌ์กฐ
2. ๋ฐฑ์๋ ๊ฐ๋ฐ
โข Django ํ๋ก์ ํธ ๊ตฌ์กฐ ์ค๊ณ
โข ๋ฐ์ดํฐ ๋ชจ๋ธ๋ง๊ณผ ๊ด๊ณ ์ค์
โข Web3.py๋ฅผ ์ด์ฉํ ๋ธ๋ก์ฒด์ธ ์ํธ์์ฉ
โข Celery๋ฅผ ์ด์ฉํ ๋น๋๊ธฐ ์์ ์ฒ๋ฆฌ
3. API ๊ฐ๋ฐ
โข Django REST Framework ํ์ฉ
โข RESTful API ์ค๊ณ ์์น
โข ํ์ด์ง๋ค์ด์ ๊ณผ ํํฐ๋ง
โข API ๋ฌธ์ํ
4. ์ฑ๋ฅ ์ต์ ํ
โข ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ ์ต์ ํ
โข Redis ์บ์ฑ
โข ์ธ๋ฑ์ค ์ค๊ณ
โข ๋ฐฐ์น ์ฒ๋ฆฌ
5. ๋ณด์
โข ํ๊ฒฝ ๋ณ์ ๊ด๋ฆฌ
โข API ์์ฒญ ์ ํ
โข ์ ๋ ฅ ๊ฐ ๊ฒ์ฆ
โข CORS ์ค์
6. ๋ฐฐํฌ ๋ฐ ์ด์
โข Docker ์ปจํ ์ด๋ํ
โข ํด๋ผ์ฐ๋ ๋ฐฐํฌ
โข ๋ชจ๋ํฐ๋ง ์ค์
โข CI/CD ํ์ดํ๋ผ์ธ
๐ ๋ค์ ๋จ๊ณ
์ด์ ์ด ์์คํ ์ ๊ธฐ๋ฐ์ผ๋ก ๋ ๋ฐ์ ์ํฌ ์ ์์ด:
์ด๊ธ โ ์ค๊ธ
โข ๋ ๋ง์ ํ ํฐ ํ์ค ์ง์ (ERC-721, ERC-1155)
โข ํ๋ก ํธ์๋ ํ๋ ์์ํฌ ํ์ต (React, Vue.js)
โข GraphQL API ๊ตฌ์ถ
โข ์ค์๊ฐ ๋ฐ์ดํฐ ์คํธ๋ฆฌ๋ฐ (WebSocket)
์ค๊ธ โ ๊ณ ๊ธ
โข ๋จธ์ ๋ฌ๋์ ์ด์ฉํ ๊ฑฐ๋ ํจํด ๋ถ์
โข ์ค๋งํธ ์ปจํธ๋ํธ ๊ฐ๋ฐ (Solidity)
โข ํ์ค์ํ ์ ํ๋ฆฌ์ผ์ด์ (DApp) ๊ตฌ์ถ
โข ๋ฉํฐ์ฒด์ธ ์ง์ ๋ฐ ํฌ๋ก์ค์ฒด์ธ ๋ธ๋ฆฌ์ง
๊ณ ๊ธ โ ์ ๋ฌธ๊ฐ
โข MEV (Maximal Extractable Value) ๋ถ์
โข ์จ์ฒด์ธ ๋ฐ์ดํฐ ์ธ๋ฑ์ฑ ์์ง ๊ตฌ์ถ
โข ๊ณ ๋น๋ ๊ฑฐ๋ ์์คํ
โข ๋ธ๋ก์ฒด์ธ ๋ณด์ ๊ฐ์ฌ
๐ช ์ค์ ํ๋ก์ ํธ ์์ด๋์ด
๋ฐฐ์ด ๋ด์ฉ์ ํ์ฉํด์ ์ด๋ฐ ํ๋ก์ ํธ๋ค์ ๋ง๋ค์ด๋ณผ ์ ์์ด:
โข ๊ณ ๋ ์ถ์ ๊ธฐ: ๋๋ ๊ฑฐ๋๋ฅผ ์ค์๊ฐ์ผ๋ก ๊ฐ์งํ๊ณ ์๋ฆผ
โข ๊ฐ์ค๋น ์์ธก๊ธฐ: ์ต์ ์ ๊ฑฐ๋ ์์ ์ถ์ฒ
โข ํ ํฐ ์ค๋ํผ: ์๋ก ์ถ์๋ ํ ํฐ ์๋ ๋ฐ๊ฒฌ
โข DeFi ๋์๋ณด๋: ์์ต๋ฅ ๋น๊ต ๋ฐ ์ต์ ํ
โข NFT ๋ถ์ ํ๋ซํผ: ํฌ๊ท๋ ๋ฐ ๊ฐ๊ฒฉ ์ถ์ด ๋ถ์
โข ์ธ๊ธ ๊ณ์ฐ๊ธฐ: ์ํธํํ ๊ฑฐ๋ ์ธ๊ธ ์๋ ๊ณ์ฐ
๐ ๋ง์ง๋ง ์กฐ์ธ
๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ์ ๊ณ์ ์งํํ๋ ๋ถ์ผ์ผ. ์๋ก์ด ๊ธฐ์ ๊ณผ ํ๋กํ ์ฝ์ด ๋์์์ด ๋์ค๊ณ ์์ด. ์ค์ํ ๊ฑด ๊ธฐ์ด๋ฅผ ํํํ ํ๊ณ , ๊พธ์คํ ํ์ตํ๋ ๊ฒ์ด์ผ. ๐
๊ทธ๋ฆฌ๊ณ ํผ์ ํ๊ธฐ ์ด๋ ค์ธ ๋๋ ์ปค๋ฎค๋ํฐ์ ๋์์ ๋ฐ๋ ๊ฒ๋ ์ข์. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค๊ณผ ํ์
ํ๊ฑฐ๋, ๋ฉํ ๋ฅผ ์ฐพ์์ ๋ฐฐ์ฐ๋ ๊ฒ๋ ํ๋ฅญํ ๋ฐฉ๋ฒ์ด์ง!
์ด ๊ธ์ด ๋์์ด ๋์๊ธฐ๋ฅผ ๋ฐ๋ผ! ๊ถ๊ธํ ์ ์ด ์๊ฑฐ๋ ๋งํ๋ ๋ถ๋ถ์ด ์์ผ๋ฉด ์ธ์ ๋ ์ง ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํด๋ด. ์ฐ๋ฆฌ ๋ชจ๋ ์ฒ์์๋ ์ด๋ณด์์ผ๋๊น! ๐
์, ์ด์ ๋น์ ๋ง์ ๋ฉ์ง ๋ธ๋ก์ฒด์ธ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณผ ์๊ฐ์ด์ผ! ํ์ดํ
! ๐๐ช
๐ Happy Coding!
๋ธ๋ก์ฒด์ธ์ ์ธ๊ณ๋ ๋ฌดํํ ๊ฐ๋ฅ์ฑ์ผ๋ก ๊ฐ๋ํด.
๋น์ ์ ์์ด๋์ด๋ก ์ธ์์ ๋ฐ๊ฟ๋ณด์! ๐โจ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.