์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ ํŒŒ์ด์ฌ๊ณผ ์žฅ๊ณ (Django)๋กœ ํ† ํฐ ๊ฑฐ๋ž˜ ๋‚ด์—ญ ์กฐํšŒ ์‹œ์Šคํ…œ ๊ตฌ์ถ•ํ•˜๊ธฐ: ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ๋ฅผ ๋‚ด ์†์•ˆ์—!

๐Ÿš€ ํŒŒ์ด์ฌ๊ณผ ์žฅ๊ณ (Django)๋กœ ํ† ํฐ ๊ฑฐ๋ž˜ ๋‚ด์—ญ ์กฐํšŒ ์‹œ์Šคํ…œ ๊ตฌ์ถ•ํ•˜๊ธฐ: ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ๋ฅผ ๋‚ด ์†์•ˆ์—!

Web3 ์‹œ๋Œ€, ํ† ํฐ ๊ฑฐ๋ž˜ ๋‚ด์—ญ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ถ”์ ํ•˜๋Š” ๋‚˜๋งŒ์˜ ์‹œ์Šคํ…œ ๋งŒ๋“ค๊ธฐ

๐ŸŽฏ ์™œ ํ† ํฐ ๊ฑฐ๋ž˜ ๋‚ด์—ญ ์กฐํšŒ ์‹œ์Šคํ…œ์ด ํ•„์š”ํ• ๊นŒ?

์•ˆ๋…•, ์นœ๊ตฌ! ์š”์ฆ˜ ๋ธ”๋ก์ฒด์ธ๊ณผ ์•”ํ˜ธํ™”ํ ์‹œ์žฅ์ด ์ •๋ง ๋œจ๊ฒ์ง€? ๐Ÿ”ฅ ๋น„ํŠธ์ฝ”์ธ, ์ด๋”๋ฆฌ์›€์€ ๋ฌผ๋ก ์ด๊ณ  ์ˆ˜๋งŽ์€ ํ† ํฐ๋“ค์ด ๋งค์ผ ๊ฑฐ๋ž˜๋˜๊ณ  ์žˆ์–ด. ๊ทธ๋Ÿฐ๋ฐ ๋ง์ด์•ผ, ์ด๋Ÿฐ ํ† ํฐ๋“ค์˜ ๊ฑฐ๋ž˜ ๋‚ด์—ญ์„ ์ œ๋Œ€๋กœ ์ถ”์ ํ•˜๊ณ  ๋ถ„์„ํ•˜๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ๊นŒ?

๋Œ€๋ถ€๋ถ„์˜ ์‚ฌ๋žŒ๋“ค์€ ๊ฑฐ๋ž˜์†Œ ์•ฑ์ด๋‚˜ ๋ธ”๋ก ์ต์Šคํ”Œ๋กœ๋Ÿฌ(Block Explorer)๋ฅผ ์‚ฌ์šฉํ•˜์ง€. ํ•˜์ง€๋งŒ ๊ฐœ๋ฐœ์ž๋ผ๋ฉด, ํŠนํžˆ ๋ธ”๋ก์ฒด์ธ ๊ธฐ์ˆ ์— ๊ด€์‹ฌ์ด ์žˆ๋‹ค๋ฉด, ์ง์ ‘ ํ† ํฐ ๊ฑฐ๋ž˜ ๋‚ด์—ญ ์กฐํšŒ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•ด๋ณด๋Š” ๊ฑด ์ •๋ง ๊ฐ’์ง„ ๊ฒฝํ—˜์ด ๋  ๊ฑฐ์•ผ!

์ด๋Ÿฐ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค๋ฉด ๋ญ๊ฐ€ ์ข‹๋ƒ๊ณ ? ์ผ๋‹จ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํŠน์ • ์ง€๊ฐ‘ ์ฃผ์†Œ์˜ ๊ฑฐ๋ž˜ ๋‚ด์—ญ์„ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜ ์žˆ๊ณ , ๋Œ€๋Ÿ‰์˜ ๊ฑฐ๋ž˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„ํ•ด์„œ ํŒจํ„ด์„ ์ฐพ์„ ์ˆ˜๋„ ์žˆ์–ด. ๊ฒŒ๋‹ค๊ฐ€ ์„ธ๊ธˆ ์‹ ๊ณ ๋ฅผ ์œ„ํ•œ ๊ฑฐ๋ž˜ ๋‚ด์—ญ ์ •๋ฆฌ๋„ ํ›จ์”ฌ ์‰ฌ์›Œ์ง€์ง€. ๐Ÿ’ฐ

๐ŸŽ“ ์ด ๊ธ€์—์„œ ๋ฐฐ์šธ ๋‚ด์šฉ

โ€ข ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์™€ ํ† ํฐ ๊ฑฐ๋ž˜์˜ ๊ธฐ๋ณธ ์›๋ฆฌ
โ€ข ํŒŒ์ด์ฌ๊ณผ ์žฅ๊ณ ๋ฅผ ํ™œ์šฉํ•œ ๋ฐฑ์—”๋“œ ์‹œ์Šคํ…œ ์„ค๊ณ„
โ€ข Web3.py๋ฅผ ์ด์šฉํ•œ ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ ์กฐํšŒ
โ€ข REST API ์„ค๊ณ„ ๋ฐ ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๋™
โ€ข ์‹ค์ „ ํ”„๋กœ์ ํŠธ ๊ตฌํ˜„ ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ
โ€ข ์„ฑ๋Šฅ ์ตœ์ ํ™”์™€ ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ
๋ธ”๋ก์ฒด์ธ Web3.py ๋ฐ์ดํ„ฐ ์กฐํšŒ Django ๋ฐฑ์—”๋“œ ์ฒ˜๋ฆฌ ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค ํ† ํฐ ๊ฑฐ๋ž˜ ๋‚ด์—ญ ์กฐํšŒ ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ๋ฐ์ดํ„ฐ ๊ฐ€๊ณต ๋ฐ ์ €์žฅ 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๋ฅผ ์˜ˆ๋กœ ๋“ค์–ด๋ณผ๊ฒŒ:

1
Infura ๊ณ„์ • ์ƒ์„ฑ

Infura ์›น์‚ฌ์ดํŠธ(infura.io)์— ์ ‘์†ํ•ด์„œ ๋ฌด๋ฃŒ ๊ณ„์ •์„ ๋งŒ๋“ค์–ด. ์ด๋ฉ”์ผ ์ธ์ฆ๋งŒ ํ•˜๋ฉด ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด!
2
ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ

๋Œ€์‹œ๋ณด๋“œ์—์„œ ์ƒˆ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด. ํ”„๋กœ์ ํŠธ ์ด๋ฆ„์€ "TokenTracker" ๊ฐ™์€ ๊ฑธ๋กœ ์ง€์–ด์ฃผ๋ฉด ๋ผ.
3
API ํ‚ค ํ™•์ธ

ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค๋ฉด 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
๐Ÿ’ก Pro Tip

๊ฐœ๋ฐœ ์ดˆ๊ธฐ์—๋Š” ํ…Œ์ŠคํŠธ๋„ท(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 ๋น„๋™๊ธฐ ์ž‘์—… ํ๋ฆ„ ์‚ฌ์šฉ์ž ์š”์ฒญ ๊ฑฐ๋ž˜ ๋‚ด์—ญ ์กฐํšŒ API ํ˜ธ์ถœ Django View ์ž‘์—… ํ์— ์ถ”๊ฐ€ Task ์ƒ์„ฑ Celery Worker ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ๋ฐ์ดํ„ฐ ์กฐํšŒ ๋ธ”๋ก์ฒด์ธ Web3 Provider ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ ์ €์žฅ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค PostgreSQL ๊ฒฐ๊ณผ ์•Œ๋ฆผ

๐Ÿ”„ 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
๐ŸŽฏ Celery Task ์‚ฌ์šฉ ์˜ˆ์‹œ

# 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๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด! ๋ช‡ ๊ฐ€์ง€ ์˜ˆ์‹œ๋ฅผ ๋ณด์ž:

๐Ÿ” 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๏ธโƒฃ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ์ตœ์ ํ™”

N+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 ์˜ˆ์‹œ)

1
EC2 ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ

AWS EC2์—์„œ Ubuntu ์„œ๋ฒ„ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•ด. t3.medium ์ •๋„๋ฉด ์ถฉ๋ถ„ํ•ด.
2
RDS ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •

PostgreSQL RDS ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ๋ณด์•ˆ ๊ทธ๋ฃน์—์„œ EC2์—์„œ์˜ ์ ‘๊ทผ์„ ํ—ˆ์šฉํ•ด.
3
ElastiCache Redis ์„ค์ •

Redis ํด๋Ÿฌ์Šคํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  EC2์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค์ •ํ•ด.
4
์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฐฐํฌ

# 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
5
Nginx ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ ์„ค์ •

# /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 ๊ธฐ๋Šฅ:

โ€ข 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
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.