์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ—๏ธ ๋ผ๋ผ๋ฒจ(Laravel)๊ณผ ํŒŒ์ด์ฌ(Python)์„ ํ†ตํ•ฉํ•œDAO ๋ฐฑ์—”๋“œ ๊ตฌ์กฐ ์„ค๊ณ„ํ•˜๊ธฐ โ€” Web3 ์‹œ๋Œ€์˜ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•„ํ‚คํ…์ฒ˜ ์™„์ „ ์ •๋ณต

๐Ÿ—๏ธ ๋ผ๋ผ๋ฒจ(Laravel)๊ณผ ํŒŒ์ด์ฌ(Python)์„ ํ†ตํ•ฉํ•œ
DAO ๋ฐฑ์—”๋“œ ๊ตฌ์กฐ ์„ค๊ณ„ํ•˜๊ธฐ โ€” Web3 ์‹œ๋Œ€์˜ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•„ํ‚คํ…์ฒ˜ ์™„์ „ ์ •๋ณต

Laravel ร— Python ร— DAO ร— Blockchain Backend Architecture

๐Ÿ”— ๋ธ”๋ก์ฒด์ธ/Web3 โš™๏ธ ํ”„๋กœ๊ทธ๋žจ๊ฐœ๋ฐœ ๐Ÿ Python ๐ŸŽฏ Laravel ๐Ÿ›๏ธ DAO
DAO ๋ฐฑ์—”๋“œ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•„ํ‚คํ…์ฒ˜ ๐Ÿ‘ค ์‚ฌ์šฉ์ž / ํ”„๋ก ํŠธ์—”๋“œ React / Vue / Next.js ๐ŸŽฏ Laravel API REST API ๊ฒŒ์ดํŠธ์›จ์ด ์ธ์ฆ / ๊ถŒํ•œ ๊ด€๋ฆฌ DB ORM / ์บ์‹ฑ ํ / ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๐Ÿ Python ์„œ๋น„์Šค ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์—ฐ๋™ Web3.py / ์˜จ์ฒด์ธ ๋ถ„์„ ํˆฌํ‘œ ์ง‘๊ณ„ / ๊ฑฐ๋ฒ„๋„Œ์Šค AI/ML ์˜์‚ฌ๊ฒฐ์ • ๋ณด์กฐ ๐Ÿ”„ ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค Redis / RabbitMQ ์ด๋ฒคํŠธ ํ / ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ Laravel โ†” Python ํ†ต์‹  ์‹ค์‹œ๊ฐ„ ์ƒํƒœ ๋™๊ธฐํ™” โ›“๏ธ ๋ธ”๋ก์ฒด์ธ ๋ ˆ์ด์–ด (Ethereum / Polygon / Arbitrum) ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ (Solidity) ยท DAO ๊ฑฐ๋ฒ„๋„Œ์Šค ์ปจํŠธ๋ž™ํŠธ ยท ํ† ํฐ ์ปจํŠธ๋ž™ํŠธ ยท ํŠธ๋ ˆ์ €๋ฆฌ Governor.sol ยท TimelockController ยท ERC-20 Governance Token ยท Multisig Laravel = ์˜คํ”„์ฒด์ธ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ยท Python = ์˜จ์ฒด์ธ ์ธํ„ฐ๋ž™์…˜ ยท ๋ธŒ๋กœ์ปค = ๋‘ ์„ธ๊ณ„์˜ ๋‹ค๋ฆฌ ๐Ÿ”ด Laravel ๐ŸŸฃ Message Broker ๐Ÿ”ต Python โ›“๏ธ Blockchain

๐Ÿš€ ๋“ค์–ด๊ฐ€๋ฉฐ โ€” ์™œ Laravel + Python + DAO ์กฐํ•ฉ์ธ๊ฐ€?

์•ผ, ์†”์งํžˆ ๋งํ•˜์ž. ์ฒ˜์Œ์— ์ด ์กฐํ•ฉ ๋“ค์—ˆ์„ ๋•Œ "์ด๊ฒŒ ๋ญ” ์กฐํ•ฉ์ด์•ผ?" ์‹ถ์—ˆ์ง€? ๐Ÿ˜…
Laravel์€ PHP ๊ธฐ๋ฐ˜ ์›น ํ”„๋ ˆ์ž„์›Œํฌ๊ณ , Python์€ ๋ฐ์ดํ„ฐ/AI/๋ธ”๋ก์ฒด์ธ ์ชฝ์—์„œ ๊ฐ•์„ธ์ธ ์–ธ์–ด๊ณ , DAO๋Š” ํƒˆ์ค‘์•™ํ™” ์ž์œจ ์กฐ์ง(Decentralized Autonomous Organization)์ด์ž–์•„.
๊ทผ๋ฐ ์ด ์„ธ ๊ฐ€์ง€๋ฅผ ํ•ฉ์น˜๋ฉด ์ง„์งœ ๊ฐ•๋ ฅํ•œ Web3 ๋ฐฑ์—”๋“œ ์•„ํ‚คํ…์ฒ˜๊ฐ€ ํƒ„์ƒํ•œ๋‹ค๋Š” ๊ฑฐ, ์ง€๊ธˆ๋ถ€ํ„ฐ ๊ฐ™์ด ํŒŒํ—ค์ณ๋ณด์ž!

๐ŸŽฏ ํ•ต์‹ฌ ์•„์ด๋””์–ด: Laravel์€ ์˜คํ”„์ฒด์ธ(Off-chain) ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋‹ด๋‹นํ•˜๊ณ , Python์€ ์˜จ์ฒด์ธ(On-chain) ์ธํ„ฐ๋ž™์…˜๊ณผ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์—ฐ๋™์„ ๋‹ด๋‹นํ•œ๋‹ค. ๋‘ ์„œ๋น„์Šค๋Š” ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค(Redis/RabbitMQ)๋ฅผ ํ†ตํ•ด ๋น„๋™๊ธฐ์ ์œผ๋กœ ํ†ต์‹ ํ•˜๋ฉด์„œ DAO์˜ ๊ฑฐ๋ฒ„๋„Œ์Šค ์‹œ์Šคํ…œ์„ ๊ตฌ๋™ํ•œ๋‹ค.

DAO ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค ๋•Œ ๊ฐ€์žฅ ํ”ํ•œ ์‹ค์ˆ˜๊ฐ€ ๋ญ”์ง€ ์•Œ์•„? ๋ชจ๋“  ๊ฑธ ํ•˜๋‚˜์˜ ์–ธ์–ด, ํ•˜๋‚˜์˜ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ ํ•ด๊ฒฐํ•˜๋ ค๋Š” ๊ฑฐ์•ผ. ๊ทผ๋ฐ ํ˜„์‹ค์€ ๋‹ฌ๋ผ.
์‚ฌ์šฉ์ž ์ธ์ฆ, ๊ถŒํ•œ ๊ด€๋ฆฌ, ์˜คํ”„์ฒด์ธ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ๋Š” Laravel์ด ์••๋„์ ์œผ๋กœ ์ž˜ํ•˜๊ณ ,
Web3.py๋ฅผ ํ†ตํ•œ ์ด๋”๋ฆฌ์›€ ์—ฐ๋™, ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹, ์˜จ์ฒด์ธ ๋ฐ์ดํ„ฐ ๋ถ„์„์€ Python์ด ํ›จ์”ฌ ์ƒํƒœ๊ณ„๊ฐ€ ํ’๋ถ€ํ•˜๊ฑฐ๋“ .

๊ทธ๋ž˜์„œ ์ด ๊ธ€์—์„œ๋Š” ๋‘ ๊ธฐ์ˆ ์„ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ ๋ฐฉ์‹์œผ๋กœ ํ†ตํ•ฉํ•ด์„œ ์™„์„ฑ๋„ ๋†’์€ DAO ๋ฐฑ์—”๋“œ๋ฅผ ์„ค๊ณ„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ๋‹ค๋ฃฐ ๊ฑฐ์•ผ. ์ฝ”๋“œ๋„ ์‹ค์ œ๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” ์ˆ˜์ค€์œผ๋กœ ๋ณด์—ฌ์ค„ ํ…Œ๋‹ˆ๊นŒ ์ง‘์ค‘ํ•ด! ๐Ÿ’ช

๐Ÿ›๏ธ DAO๋ž€ ๋ฌด์—‡์ธ๊ฐ€? โ€” ๋ฐฑ์—”๋“œ ์„ค๊ณ„ ์ „์— ๊ฐœ๋…๋ถ€ํ„ฐ ์žก์ž

DAO๋ฅผ ๋ชจ๋ฅด๋ฉด ๋ฐฑ์—”๋“œ๋ฅผ ์„ค๊ณ„ํ•  ์ˆ˜๊ฐ€ ์—†์–ด. ๊ทธ๋Ÿฌ๋‹ˆ๊นŒ ์ž ๊น ๊ฐœ๋… ์ •๋ฆฌํ•˜๊ณ  ๊ฐ€์ž.

DAO์˜ ํ•ต์‹ฌ ๊ตฌ์„ฑ ์š”์†Œ

1
๊ฑฐ๋ฒ„๋„Œ์Šค ํ† ํฐ (Governance Token)
ERC-20 ๊ธฐ๋ฐ˜์˜ ํˆฌํ‘œ๊ถŒ ํ† ํฐ. ํ† ํฐ์„ ๋งŽ์ด ๊ฐ€์งˆ์ˆ˜๋ก ํˆฌํ‘œ ์˜ํ–ฅ๋ ฅ์ด ์ปค์ง„๋‹ค. ์˜ˆ: Compound์˜ COMP, Uniswap์˜ UNI.
2
ํ”„๋กœํฌ์ ˆ ์‹œ์Šคํ…œ (Proposal System)
๋ฉค๋ฒ„๋“ค์ด ์ œ์•ˆ์„ ์˜ฌ๋ฆฌ๊ณ , ์ผ์ • ๊ธฐ๊ฐ„ ๋™์•ˆ ํˆฌํ‘œ๊ฐ€ ์ง„ํ–‰๋œ๋‹ค. ์˜จ์ฒด์ธ์— ๊ธฐ๋ก๋˜๋ฉฐ ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.
3
ํˆฌํ‘œ ๋ฉ”์ปค๋‹ˆ์ฆ˜ (Voting Mechanism)
For/Against/Abstain ํˆฌํ‘œ. ์ฟผ๋Ÿผ(์ตœ์†Œ ์ฐธ์—ฌ์œจ)์„ ์ถฉ์กฑํ•ด์•ผ ํ†ต๊ณผ๋œ๋‹ค. ํƒ€์ž„๋ฝ(Timelock)์œผ๋กœ ์‹คํ–‰ ์ง€์—ฐ.
4
ํŠธ๋ ˆ์ €๋ฆฌ (Treasury)
DAO๊ฐ€ ๊ด€๋ฆฌํ•˜๋Š” ์ž๊ธˆ. ํ”„๋กœํฌ์ ˆ์ด ํ†ต๊ณผ๋˜๋ฉด ์ž๋™์œผ๋กœ ์‹คํ–‰๋˜๋Š” ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๊ฐ€ ์ž๊ธˆ์„ ์ง‘ํ–‰ํ•œ๋‹ค.
5
ํƒ€์ž„๋ฝ ์ปจํŠธ๋กค๋Ÿฌ (TimelockController)
ํ”„๋กœํฌ์ ˆ ํ†ต๊ณผ ํ›„ ์‹ค์ œ ์‹คํ–‰๊นŒ์ง€ ์ผ์ • ์‹œ๊ฐ„ ์ง€์—ฐ. ์•…์˜์ ์ธ ๊ฑฐ๋ฒ„๋„Œ์Šค ๊ณต๊ฒฉ ๋ฐฉ์ง€์šฉ.
๐Ÿ’ก ๋ฐฑ์—”๋“œ ๊ด€์ ์—์„œ DAO๋ฅผ ๋ณด๋ฉด: DAO์˜ ํ•ต์‹ฌ ๋กœ์ง์€ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ(์˜จ์ฒด์ธ)์— ์žˆ์ง€๋งŒ, ์‚ฌ์šฉ์ž ๊ฒฝํ—˜(UX)์„ ์œ„ํ•œ ์˜คํ”„์ฒด์ธ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ, ์•Œ๋ฆผ ์‹œ์Šคํ…œ, ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅ, ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ ๋“ฑ์€ ์ „ํ†ต์ ์ธ ๋ฐฑ์—”๋“œ๊ฐ€ ํ•„์š”ํ•ด. ์ด ์˜คํ”„์ฒด์ธ ๋ ˆ์ด์–ด๋ฅผ ์ž˜ ์„ค๊ณ„ํ•˜๋Š” ๊ฒŒ ์ด ๊ธ€์˜ ํ•ต์‹ฌ์ด์•ผ.

์˜คํ”„์ฒด์ธ vs ์˜จ์ฒด์ธ โ€” ๋ฌด์—‡์„ ์–ด๋””์— ์ €์žฅํ• ๊นŒ?

ํ•ญ๋ชฉ ์˜จ์ฒด์ธ (์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ) ์˜คํ”„์ฒด์ธ (Laravel/Python)
ํˆฌํ‘œ ๊ฒฐ๊ณผ โœ… ์ตœ์ข… ๊ฒฐ๊ณผ ๊ธฐ๋ก โœ… ์‹ค์‹œ๊ฐ„ ์ง‘๊ณ„ ์บ์‹ฑ
ํ”„๋กœํฌ์ ˆ ๋‚ด์šฉ โœ… ํ•ด์‹œ๊ฐ’๋งŒ ์ €์žฅ โœ… ์ „์ฒด ํ…์ŠคํŠธ/๋ฉ”ํƒ€๋ฐ์ดํ„ฐ
์‚ฌ์šฉ์ž ํ”„๋กœํ•„ โŒ ๋น„์šฉ ๊ณผ๋‹ค โœ… DB์— ์ €์žฅ
ํ† ํฐ ์ž”์•ก โœ… ์›๋ณธ ๋ฐ์ดํ„ฐ โœ… ์ธ๋ฑ์‹ฑ/์บ์‹ฑ
์•Œ๋ฆผ/์ด๋ฉ”์ผ โŒ ๋ถˆ๊ฐ€ โœ… ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹ ํ›„ ์ฒ˜๋ฆฌ
๋ถ„์„ ๋ฐ์ดํ„ฐ โŒ ์ฟผ๋ฆฌ ๋ถˆํŽธ โœ… ์ธ๋ฑ์„œ๋กœ ์ˆ˜์ง‘ ํ›„ ๋ถ„์„

๐Ÿ—บ๏ธ ์ „์ฒด ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์„ค๊ณ„ ๋“ค์–ด๊ฐ€๋ณด์ž. ํฐ ๊ทธ๋ฆผ๋ถ€ํ„ฐ ๊ทธ๋ ค์•ผ ์„ธ๋ถ€ ๊ตฌํ˜„์ด ํ”๋“ค๋ฆฌ์ง€ ์•Š์•„.

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ตฌ์„ฑ๋„

๐ŸŒ ํ”„๋ก ํŠธ์—”๋“œ
โ†’
๐ŸŽฏ Laravel API
โ‡„
๐Ÿ”„ Redis/Queue
โ‡„
๐Ÿ Python Worker
โ†’
โ›“๏ธ Blockchain

๊ฐ ์„œ๋น„์Šค์˜ ์—ญํ• ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒŒ ํ•ต์‹ฌ์ด์•ผ. ์„œ๋กœ ์ง์ ‘ ํ˜ธ์ถœํ•˜์ง€ ์•Š๊ณ  ๋ฉ”์‹œ์ง€ ํ๋ฅผ ํ†ตํ•ด ๋น„๋™๊ธฐ ํ†ต์‹ ํ•˜๋Š” ๊ตฌ์กฐ์•ผ.

๐ŸŽฏ Laravel ์„œ๋น„์Šค ๋‹ด๋‹น โ€ข REST API ์—”๋“œํฌ์ธํŠธ ์ œ๊ณต
โ€ข JWT/Sanctum ์ธ์ฆ ์ฒ˜๋ฆฌ
โ€ข ์‚ฌ์šฉ์ž/๋ฉค๋ฒ„ ๊ด€๋ฆฌ (MySQL)
โ€ข ํ”„๋กœํฌ์ ˆ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ CRUD
โ€ข ์˜คํ”„์ฒด์ธ ํˆฌํ‘œ ์ง‘๊ณ„ ์บ์‹ฑ
โ€ข ์ด๋ฉ”์ผ/์•Œ๋ฆผ ๋ฐœ์†ก
โ€ข ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ API
โ€ข ํŒŒ์ผ/IPFS ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ
๐Ÿ Python ์„œ๋น„์Šค ๋‹ด๋‹น โ€ข Web3.py ๋ธ”๋ก์ฒด์ธ ์—ฐ๋™
โ€ข ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹
โ€ข ์˜จ์ฒด์ธ ํŠธ๋žœ์žญ์…˜ ์„œ๋ช…/์ „์†ก
โ€ข ํ† ํฐ ์ž”์•ก ์ธ๋ฑ์‹ฑ
โ€ข ํˆฌํ‘œ ์ง‘๊ณ„ ์˜จ์ฒด์ธ ๊ฒ€์ฆ
โ€ข ๊ฑฐ๋ฒ„๋„Œ์Šค ๋ถ„์„ (AI/ML)
โ€ข The Graph ์ฟผ๋ฆฌ ์ฒ˜๋ฆฌ
โ€ข ๊ฐ€์Šค๋น„ ์ตœ์ ํ™” ๋กœ์ง

Docker Compose ๊ธฐ๋ฐ˜ ์„œ๋น„์Šค ๊ตฌ์„ฑ

์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” Docker Compose๋กœ ๋ชจ๋“  ์„œ๋น„์Šค๋ฅผ ํ•œ ๋ฒˆ์— ๊ด€๋ฆฌํ•ด. ์•„๋ž˜๊ฐ€ ๊ธฐ๋ณธ ๊ตฌ์กฐ์•ผ:


# docker-compose.yml

version: '3.8'

services:

# Laravel API ์„œ๋ฒ„

laravel-api:

build: ./laravel

ports: ["8000:8000"]

environment:

- DB_HOST=mysql

- REDIS_HOST=redis

- PYTHON_SERVICE_URL=http://python-worker:5000

depends_on: [mysql, redis]



# Python Web3 Worker

python-worker:

build: ./python

ports: ["5000:5000"]

environment:

- WEB3_PROVIDER_URL=https://mainnet.infura.io/v3/YOUR_KEY

- REDIS_URL=redis://redis:6379

- CONTRACT_ADDRESS=0x...

depends_on: [redis]



# MySQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค

mysql:

image: mysql:8.0

volumes: ["mysql_data:/var/lib/mysql"]

environment:

MYSQL_DATABASE: dao_backend



# Redis (์บ์‹œ + ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค)

redis:

image: redis:7-alpine

ports: ["6379:6379"]



# Nginx ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ

nginx:

image: nginx:alpine

ports: ["80:80", "443:443"]

depends_on: [laravel-api, python-worker]
๐ŸŽฏ Laravel DAO ๋ฐฑ์—”๋“œ ๋‚ด๋ถ€ ๊ตฌ์กฐ ๐Ÿ“ก Routes Layer /api/proposals /api/votes /api/members /api/treasury /api/governance ๐Ÿ›ก๏ธ Middleware: JWT Auth ยท Rate Limit ยท CORS ยท Web3 Signature Verify Sanctum ํ† ํฐ ๊ฒ€์ฆ ยท ์ง€๊ฐ‘ ์„œ๋ช… ๊ฒ€์ฆ ยท API ์š”์ฒญ ์ œํ•œ ๐ŸŽฎ Controllers ProposalController index / store show / update execute / cancel VoteController castVote getVoteStatus delegateVote MemberController register / profile walletConnect tokenBalance TreasuryController getBalance transactions allocate GovernanceController getStats quorumCheck timeline โš™๏ธ Services / Jobs ProposalService IPFS ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅ/์กฐํšŒ ํ”„๋กœํฌ์ ˆ ์ƒํƒœ ๊ด€๋ฆฌ BlockchainQueueJob Python ์„œ๋น„์Šค์— ์ž‘์—… ์œ„์ž„ Redis ํ ๋ฐœํ–‰ NotificationService ์ด๋ฉ”์ผ/ํ‘ธ์‹œ ์•Œ๋ฆผ ์˜จ์ฒด์ธ ์ด๋ฒคํŠธ ์•Œ๋ฆผ CacheService Redis ํˆฌํ‘œ ์ง‘๊ณ„ ์บ์‹ฑ ํ† ํฐ ์ž”์•ก ์บ์‹ฑ ๐Ÿ—„๏ธ Data Layer MySQL (์˜คํ”„์ฒด์ธ ๋ฐ์ดํ„ฐ) Redis (์บ์‹œ/ํ) IPFS (๋ฉ”ํƒ€๋ฐ์ดํ„ฐ) Elasticsearch (๊ฒ€์ƒ‰)

๐ŸŽฏ Laravel ๋ฐฑ์—”๋“œ ๊ตฌํ˜„ โ€” ์˜คํ”„์ฒด์ธ ๋ ˆ์ด์–ด ์„ค๊ณ„

Laravel ์ชฝ ๊ตฌํ˜„๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜์ž. ์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ๊ฑด ๋ธ”๋ก์ฒด์ธ ๋กœ์ง์„ ์ง์ ‘ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๊ณ , ์˜คํ”„์ฒด์ธ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ์™€ Python ์„œ๋น„์Šค๋กœ์˜ ์ž‘์—… ์œ„์ž„์— ์ง‘์ค‘ํ•˜๋Š” ๊ฑฐ์•ผ.

1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์„ค๊ณ„

DAO ๋ฐฑ์—”๋“œ์—์„œ ์˜คํ”„์ฒด์ธ์œผ๋กœ ๊ด€๋ฆฌํ•ด์•ผ ํ•  ํ…Œ์ด๋ธ”๋“ค์ด์•ผ:


// database/migrations/create_proposals_table.php

Schema::create('proposals', function (Blueprint $table) {

$table->id();

$table->string('onchain_id')->nullable()->index(); // ์˜จ์ฒด์ธ ํ”„๋กœํฌ์ ˆ ID

$table->string('title');

$table->text('description'); // IPFS์— ์˜ฌ๋ฆฌ๊ธฐ ์ „ ์˜คํ”„์ฒด์ธ ์ €์žฅ

$table->string('ipfs_hash')->nullable(); // IPFS CID

$table->string('proposer_address', 42)->index(); // ์ด๋”๋ฆฌ์›€ ์ฃผ์†Œ

$table->foreignId('proposer_id')->constrained('members');

$table->enum('status', [

'draft', 'pending', 'active', 'succeeded',

'defeated', 'queued', 'executed', 'cancelled'

])->default('draft');

$table->string('tx_hash')->nullable(); // ์˜จ์ฒด์ธ ์ œ์ถœ ํŠธ๋žœ์žญ์…˜

$table->bigInteger('for_votes')->default(0);

$table->bigInteger('against_votes')->default(0);

$table->bigInteger('abstain_votes')->default(0);

$table->integer('quorum_required');

$table->timestamp('voting_start_at')->nullable();

$table->timestamp('voting_end_at')->nullable();

$table->timestamp('execution_eta')->nullable();

$table->json('actions')->nullable(); // ์‹คํ–‰ํ•  ์ปจํŠธ๋ž™ํŠธ ์•ก์…˜๋“ค

$table->timestamps();

});



// create_votes_table.php

Schema::create('votes', function (Blueprint $table) {

$table->id();

$table->foreignId('proposal_id')->constrained();

$table->string('voter_address', 42)->index();

$table->enum('support', ['for', 'against', 'abstain']);

$table->decimal('voting_power', 36, 18); // ํ† ํฐ ๊ธฐ๋ฐ˜ ํˆฌํ‘œ๋ ฅ

$table->string('tx_hash')->nullable();

$table->string('reason')->nullable();

$table->boolean('is_onchain_verified')->default(false);

$table->timestamps();

$table->unique(['proposal_id', 'voter_address']);

});



// create_members_table.php

Schema::create('members', function (Blueprint $table) {

$table->id();

$table->string('wallet_address', 42)->unique();

$table->string('ens_name')->nullable(); // ENS ๋„๋ฉ”์ธ

$table->string('display_name')->nullable();

$table->string('email')->nullable();

$table->string('avatar_url')->nullable();

$table->decimal('token_balance', 36, 18)->default(0);

$table->decimal('delegated_votes', 36, 18)->default(0);

$table->string('delegate_address', 42)->nullable();

$table->integer('proposals_created')->default(0);

$table->integer('votes_cast')->default(0);

$table->timestamp('last_activity_at')->nullable();

$table->timestamps();

});

2. ์ง€๊ฐ‘ ์„œ๋ช… ๊ธฐ๋ฐ˜ ์ธ์ฆ ์‹œ์Šคํ…œ

DAO์—์„œ๋Š” ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ๋Œ€์‹  ์ด๋”๋ฆฌ์›€ ์ง€๊ฐ‘ ์„œ๋ช…(SIWE - Sign-In with Ethereum)์œผ๋กœ ์ธ์ฆํ•ด. Laravel์—์„œ ์ด๊ฑธ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์•ผ:


// app/Http/Controllers/AuthController.php

class AuthController extends Controller

{

public function getNonce(Request $request): JsonResponse

{

$address = strtolower($request->wallet_address);

$nonce = Str::random(32);



// Redis์— 5๋ถ„๊ฐ„ nonce ์ €์žฅ

Cache::put("siwe_nonce:{$address}", $nonce, 300);



return response()->json([

'nonce' => $nonce,

'message' => "DAO์— ๋กœ๊ทธ์ธํ•ฉ๋‹ˆ๋‹ค.\n\n์ฃผ์†Œ: {$address}\nNonce: {$nonce}"

]);

}



public function verifySignature(Request $request): JsonResponse

{

$address = strtolower($request->wallet_address);

$signature = $request->signature;

$message = $request->message;



// Redis์—์„œ nonce ํ™•์ธ

$storedNonce = Cache::get("siwe_nonce:{$address}");

if (!$storedNonce || !str_contains($message, $storedNonce)) {

return response()->json(['error' => '์œ ํšจํ•˜์ง€ ์•Š์€ nonce'], 401);

}



// Python ์„œ๋น„์Šค์— ์„œ๋ช… ๊ฒ€์ฆ ์œ„์ž„

$verified = $this->pythonService->verifyEthSignature(

$address, $message, $signature

);



if (!$verified) {

return response()->json(['error' => '์„œ๋ช… ๊ฒ€์ฆ ์‹คํŒจ'], 401);

}



// ๋ฉค๋ฒ„ ์กฐํšŒ ๋˜๋Š” ์ƒ์„ฑ

$member = Member::firstOrCreate(

['wallet_address' => $address],

['display_name' => substr($address, 0, 6) . '...' . substr($address, -4)]

);



// Sanctum ํ† ํฐ ๋ฐœ๊ธ‰

$token = $member->createToken('dao-session')->plainTextToken;

Cache::forget("siwe_nonce:{$address}");



return response()->json(['token' => $token, 'member' => $member]);

}

}

3. ํ”„๋กœํฌ์ ˆ ์„œ๋น„์Šค โ€” IPFS ์—ฐ๋™

ํ”„๋กœํฌ์ ˆ์˜ ์ƒ์„ธ ๋‚ด์šฉ์€ IPFS์— ์ €์žฅํ•˜๊ณ , ์˜จ์ฒด์ธ์—๋Š” IPFS ํ•ด์‹œ๋งŒ ์˜ฌ๋ ค. Laravel์—์„œ IPFS ์—…๋กœ๋“œ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์•ผ:


// app/Services/ProposalService.php

class ProposalService

{

public function __construct(

private PythonServiceClient $pythonClient,

private IpfsService $ipfsService

) {}



public function createProposal(array $data, Member $proposer): Proposal

{

// 1. IPFS์— ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์—…๋กœ๋“œ

$metadata = [

'title' => $data['title'],

'description' => $data['description'],

'actions' => $data['actions'],

'proposer' => $proposer->wallet_address,

'created_at' => now()->toIso8601String(),

];



$ipfsHash = $this->ipfsService->uploadJson($metadata);



// 2. DB์— ์˜คํ”„์ฒด์ธ ๋ฐ์ดํ„ฐ ์ €์žฅ

$proposal = Proposal::create([

'title' => $data['title'],

'description' => $data['description'],

'ipfs_hash' => $ipfsHash,

'proposer_address' => $proposer->wallet_address,

'proposer_id' => $proposer->id,

'status' => 'pending',

'actions' => $data['actions'],

'quorum_required' => config('dao.quorum_percentage'),

]);



// 3. Python ์„œ๋น„์Šค์— ์˜จ์ฒด์ธ ์ œ์ถœ ์ž‘์—… ํ์ž‰

SubmitProposalJob::dispatch($proposal->id, $ipfsHash, $data['actions']);



return $proposal;

}



public function syncOnchainStatus(int $proposalId): void

{

// Python ์„œ๋น„์Šค์—์„œ ์˜จ์ฒด์ธ ์ƒํƒœ ์กฐํšŒ

$onchainData = $this->pythonClient->getProposalState($proposalId);



Proposal::where('id', $proposalId)->update([

'status' => $this->mapOnchainStatus($onchainData['state']),

'for_votes' => $onchainData['for_votes'],

'against_votes' => $onchainData['against_votes'],

'abstain_votes' => $onchainData['abstain_votes'],

]);



// Redis ์บ์‹œ ๊ฐฑ์‹ 

Cache::put("proposal_status:{$proposalId}", $onchainData, 60);

}

}

4. Python ์„œ๋น„์Šค ํด๋ผ์ด์–ธํŠธ โ€” HTTP + Redis ์ด์ค‘ ํ†ต์‹ 

Laravel์—์„œ Python ์„œ๋น„์Šค์™€ ํ†ต์‹ ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋‘ ๊ฐ€์ง€์•ผ. ๋™๊ธฐ HTTP ์š”์ฒญ๊ณผ ๋น„๋™๊ธฐ Redis ํ. ์ƒํ™ฉ์— ๋”ฐ๋ผ ๊ณจ๋ผ ์จ์•ผ ํ•ด:


// app/Services/PythonServiceClient.php

class PythonServiceClient

{

private string $baseUrl;



public function __construct()

{

$this->baseUrl = config('services.python.url', 'http://python-worker:5000');

}



// ๋™๊ธฐ ๋ฐฉ์‹: ์ฆ‰์‹œ ์‘๋‹ต์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ

public function verifyEthSignature(string $address, string $message, string $sig): bool

{

$response = Http::timeout(10)->post("{$this->baseUrl}/verify-signature", [

'address' => $address,

'message' => $message,

'signature' => $sig,

]);



return $response->successful() && $response->json('verified') === true;

}



// ๋™๊ธฐ ๋ฐฉ์‹: ํ† ํฐ ์ž”์•ก ์กฐํšŒ

public function getTokenBalance(string $address): string

{

$cacheKey = "token_balance:{$address}";



return Cache::remember($cacheKey, 30, function () use ($address) {

$response = Http::timeout(15)->get("{$this->baseUrl}/token-balance/{$address}");

return $response->json('balance', '0');

});

}



// ๋น„๋™๊ธฐ ๋ฐฉ์‹: Redis ํ๋ฅผ ํ†ตํ•œ ์ž‘์—… ์œ„์ž„

public function queueBlockchainJob(string $jobType, array $payload): string

{

$jobId = Str::uuid()->toString();



Redis::lpush('blockchain_jobs', json_encode([

'job_id' => $jobId,

'type' => $jobType,

'payload' => $payload,

'created_at' => now()->timestamp,

]));



return $jobId; // ํด๋ผ์ด์–ธํŠธ์— job_id ๋ฐ˜ํ™˜, ํด๋ง์œผ๋กœ ์ƒํƒœ ํ™•์ธ

}



// ์ž‘์—… ์ƒํƒœ ํ™•์ธ

public function getJobStatus(string $jobId): array

{

$result = Redis::get("job_result:{$jobId}");

return $result ? json_decode($result, true) : ['status' => 'pending'];

}

}
โš ๏ธ ์ค‘์š”ํ•œ ์„ค๊ณ„ ์›์น™: Laravel์—์„œ ์ง์ ‘ Web3 ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ฐ๋Š” ๊ฑด ์ ˆ๋Œ€ ๊ถŒ์žฅํ•˜์ง€ ์•Š์•„. PHP์˜ Web3 ์ƒํƒœ๊ณ„๋Š” Python์— ๋น„ํ•ด ํ›จ์”ฌ ๋นˆ์•ฝํ•˜๊ณ , ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์–ด๋ ค์›Œ. ๋ธ”๋ก์ฒด์ธ ๊ด€๋ จ ๋ชจ๋“  ์ž‘์—…์€ Python ์„œ๋น„์Šค์— ์œ„์ž„ํ•˜๋Š” ๊ฒŒ ๋งž์•„.

๐Ÿ Python Web3 ์„œ๋น„์Šค ๊ตฌํ˜„ โ€” ์˜จ์ฒด์ธ ๋ ˆ์ด์–ด ์„ค๊ณ„

์ด์ œ ์ง„์งœ ์žฌ๋ฐŒ๋Š” ํŒŒํŠธ์•ผ! Python์œผ๋กœ ๋ธ”๋ก์ฒด์ธ์ด๋ž‘ ์ง์ ‘ ๋Œ€ํ™”ํ•˜๋Š” ๋ถ€๋ถ„์ด๊ฑฐ๋“ . ๐Ÿ”ฅ

Python ์„œ๋น„์Šค ๊ธฐ์ˆ  ์Šคํƒ

๐Ÿ”ง ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ web3.py ์ด๋”๋ฆฌ์›€ ์—ฐ๋™
FastAPI HTTP API ์„œ๋ฒ„
celery ๋น„๋™๊ธฐ ์ž‘์—… ์ฒ˜๋ฆฌ
redis-py Redis ์—ฐ๋™
eth-account ์„œ๋ช… ๊ฒ€์ฆ
eth-abi ABI ์ธ์ฝ”๋”ฉ/๋””์ฝ”๋”ฉ
๐Ÿ“ฆ ๋ณด์กฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ httpx ๋น„๋™๊ธฐ HTTP
pydantic ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ
sqlalchemy DB ์—ฐ๋™
python-dotenv ํ™˜๊ฒฝ๋ณ€์ˆ˜
gql The Graph ์ฟผ๋ฆฌ
pandas ๊ฑฐ๋ฒ„๋„Œ์Šค ๋ถ„์„

1. FastAPI ๋ฉ”์ธ ์„œ๋ฒ„ ๊ตฌ์กฐ


# python/main.py

from fastapi import FastAPI, HTTPException

from web3 import Web3

from eth_account.messages import encode_defunct

from eth_account import Account

import redis

import json

import os

from contextlib import asynccontextmanager



# Web3 ์—ฐ๊ฒฐ ์ดˆ๊ธฐํ™”

w3 = Web3(Web3.HTTPProvider(os.getenv('WEB3_PROVIDER_URL')))

redis_client = redis.from_url(os.getenv('REDIS_URL', 'redis://localhost:6379'))



@asynccontextmanager

async def lifespan(app: FastAPI):

# ์‹œ์ž‘ ์‹œ ๋ธ”๋ก์ฒด์ธ ์—ฐ๊ฒฐ ํ™•์ธ

if not w3.is_connected():

raise Exception("๋ธ”๋ก์ฒด์ธ ์—ฐ๊ฒฐ ์‹คํŒจ!")

print(f"โœ… ๋ธ”๋ก์ฒด์ธ ์—ฐ๊ฒฐ ์„ฑ๊ณต - ํ˜„์žฌ ๋ธ”๋ก: {w3.eth.block_number}")

yield

print("๐Ÿ”ด Python ์„œ๋น„์Šค ์ข…๋ฃŒ")



app = FastAPI(title="DAO Python Service", lifespan=lifespan)



# ๋ผ์šฐํ„ฐ ๋“ฑ๋ก

from routers import signature, token, proposal, governance

app.include_router(signature.router, prefix="/api")

app.include_router(token.router, prefix="/api")

app.include_router(proposal.router, prefix="/api")

app.include_router(governance.router, prefix="/api")

2. ์„œ๋ช… ๊ฒ€์ฆ ๋ผ์šฐํ„ฐ


# python/routers/signature.py

from fastapi import APIRouter, HTTPException

from pydantic import BaseModel

from eth_account.messages import encode_defunct

from eth_account import Account

from web3 import Web3



router = APIRouter()



class SignatureVerifyRequest(BaseModel):

address: str

message: str

signature: str



@router.post("/verify-signature")

async def verify_signature(req: SignatureVerifyRequest):

try:

# EIP-191 ๋ฉ”์‹œ์ง€ ํ•ด์‹œ ์ƒ์„ฑ

message_hash = encode_defunct(text=req.message)



# ์„œ๋ช…์—์„œ ์ฃผ์†Œ ๋ณต๊ตฌ

recovered_address = Account.recover_message(

message_hash,

signature=req.signature

)



# ์ฃผ์†Œ ๋น„๊ต (๋Œ€์†Œ๋ฌธ์ž ๋ฌด์‹œ)

verified = recovered_address.lower() == req.address.lower()



return {"verified": verified, "recovered_address": recovered_address}

except Exception as e:

raise HTTPException(status_code=400, detail=f"์„œ๋ช… ๊ฒ€์ฆ ์˜ค๋ฅ˜: {str(e)}")



@router.post("/verify-typed-data")

async def verify_typed_data(req: dict):

"""EIP-712 ํƒ€์ž…๋“œ ๋ฐ์ดํ„ฐ ์„œ๋ช… ๊ฒ€์ฆ (๋ฉ”ํƒ€ํŠธ๋žœ์žญ์…˜์šฉ)"""

try:

from eth_account._utils.structured_data.hashing import hash_domain

# EIP-712 ๊ฒ€์ฆ ๋กœ์ง

structured_data = req.get('structured_data')

signature = req.get('signature')



recovered = Account.recover_message(

encode_defunct(hexstr=Web3.keccak(text=str(structured_data)).hex()),

signature=signature

)

return {"verified": True, "signer": recovered}

except Exception as e:

raise HTTPException(status_code=400, detail=str(e))

3. ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์—ฐ๋™ โ€” Governor ์ปจํŠธ๋ž™ํŠธ

OpenZeppelin์˜ Governor ์ปจํŠธ๋ž™ํŠธ์™€ ์—ฐ๋™ํ•˜๋Š” ํ•ต์‹ฌ ์ฝ”๋“œ์•ผ. ์ด๊ฒŒ DAO์˜ ์‹ฌ์žฅ์ด๋ผ๊ณ  ๋ณผ ์ˆ˜ ์žˆ์–ด:


# python/services/governor_service.py

from web3 import Web3

import json

import os



# Governor ABI (ํ•ต์‹ฌ ํ•จ์ˆ˜๋“ค๋งŒ)

GOVERNOR_ABI = json.loads('''[

{"name":"propose","type":"function","inputs":[

{"name":"targets","type":"address[]"},

{"name":"values","type":"uint256[]"},

{"name":"calldatas","type":"bytes[]"},

{"name":"description","type":"string"}

],"outputs":[{"name":"","type":"uint256"}]},

{"name":"castVote","type":"function","inputs":[

{"name":"proposalId","type":"uint256"},

{"name":"support","type":"uint8"}

],"outputs":[{"name":"","type":"uint256"}]},

{"name":"state","type":"function","inputs":[

{"name":"proposalId","type":"uint256"}

],"outputs":[{"name":"","type":"uint8"}]},

{"name":"proposalVotes","type":"function","inputs":[

{"name":"proposalId","type":"uint256"}

],"outputs":[

{"name":"againstVotes","type":"uint256"},

{"name":"forVotes","type":"uint256"},

{"name":"abstainVotes","type":"uint256"}

]}

]''')



class GovernorService:

# ํ”„๋กœํฌ์ ˆ ์ƒํƒœ ๋งคํ•‘

PROPOSAL_STATES = {

0: "pending", 1: "active", 2: "cancelled",

3: "defeated", 4: "succeeded", 5: "queued",

6: "expired", 7: "executed"

}



def __init__(self, w3: Web3):

self.w3 = w3

self.contract = w3.eth.contract(

address=Web3.to_checksum_address(os.getenv('GOVERNOR_ADDRESS')),

abi=GOVERNOR_ABI

)



def get_proposal_state(self, proposal_id: int) -> dict:

"""์˜จ์ฒด์ธ ํ”„๋กœํฌ์ ˆ ์ƒํƒœ ์กฐํšŒ"""

state_code = self.contract.functions.state(proposal_id).call()

votes = self.contract.functions.proposalVotes(proposal_id).call()



return {

"state": self.PROPOSAL_STATES.get(state_code, "unknown"),

"state_code": state_code,

"against_votes": str(votes[0]),

"for_votes": str(votes[1]),

"abstain_votes": str(votes[2]),

}



def submit_proposal(

self,

targets: list,

values: list,

calldatas: list,

description: str,

private_key: str  # ์‹ค์ œ๋กœ๋Š” HSM์ด๋‚˜ KMS ์‚ฌ์šฉ ๊ถŒ์žฅ!

) -> str:

"""์˜จ์ฒด์ธ ํ”„๋กœํฌ์ ˆ ์ œ์ถœ"""

account = self.w3.eth.account.from_key(private_key)



# ๊ฐ€์Šค ์ถ”์ •

gas_estimate = self.contract.functions.propose(

targets, values, calldatas, description

).estimate_gas({'from': account.address})



# ํŠธ๋žœ์žญ์…˜ ๋นŒ๋“œ

tx = self.contract.functions.propose(

targets, values, calldatas, description

).build_transaction({

'from': account.address,

'gas': int(gas_estimate * 1.2),  # 20% ๋ฒ„ํผ

'gasPrice': self.w3.eth.gas_price,

'nonce': self.w3.eth.get_transaction_count(account.address),

})



# ์„œ๋ช… ๋ฐ ์ „์†ก

signed_tx = self.w3.eth.account.sign_transaction(tx, private_key)

tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)



# ์˜์ˆ˜์ฆ ๋Œ€๊ธฐ

receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)



return tx_hash.hex()

4. ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ โ€” ์˜จ์ฒด์ธ ์ด๋ฒคํŠธ ์‹ค์‹œ๊ฐ„ ๊ฐ์ง€

์ด๊ฒŒ ์ง„์งœ ํ•ต์‹ฌ์ด์•ผ! ๋ธ”๋ก์ฒด์ธ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ฐ์ง€ํ•ด์„œ ์˜คํ”„์ฒด์ธ DB๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋กœ์ง์ด๊ฑฐ๋“ :


# python/workers/event_listener.py

import asyncio

from web3 import Web3, AsyncWeb3

from web3.middleware import geth_poa_middleware

import httpx

import redis

import json

import logging



logger = logging.getLogger(__name__)



class DAOEventListener:

def __init__(self, w3: Web3, redis_client, laravel_url: str):

self.w3 = w3

self.redis = redis_client

self.laravel_url = laravel_url

self.governor_contract = self._load_governor_contract()

self.token_contract = self._load_token_contract()



async def listen_proposal_created(self):

"""ProposalCreated ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹"""

event_filter = self.governor_contract.events.ProposalCreated.create_filter(

fromBlock='latest'

)



while True:

try:

events = event_filter.get_new_entries()

for event in events:

await self._handle_proposal_created(event)

await asyncio.sleep(12)  # ์ด๋”๋ฆฌ์›€ ๋ธ”๋ก ์‹œ๊ฐ„ ~12์ดˆ

except Exception as e:

logger.error(f"์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹ ์˜ค๋ฅ˜: {e}")

await asyncio.sleep(30)



async def _handle_proposal_created(self, event):

"""ProposalCreated ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ"""

args = event['args']

proposal_id = args['proposalId']

proposer = args['proposer']

description = args['description']

tx_hash = event['transactionHash'].hex()



logger.info(f"์ƒˆ ํ”„๋กœํฌ์ ˆ ๊ฐ์ง€: {proposal_id}")



# Laravel API์— ์ƒํƒœ ์—…๋ฐ์ดํŠธ ์š”์ฒญ

async with httpx.AsyncClient() as client:

await client.post(

f"{self.laravel_url}/internal/proposals/onchain-created",

json={

"onchain_id": str(proposal_id),

"proposer_address": proposer,

"tx_hash": tx_hash,

"start_block": args['startBlock'],

"end_block": args['endBlock'],

},

headers={"X-Internal-Secret": "your-secret-key"}

)



# Redis์— ์ด๋ฒคํŠธ ๋ฐœํ–‰ (์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ์šฉ)

self.redis.publish('dao_events', json.dumps({

'type': 'PROPOSAL_CREATED',

'proposal_id': str(proposal_id),

'proposer': proposer,

}))



async def listen_vote_cast(self):

"""VoteCast ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹"""

event_filter = self.governor_contract.events.VoteCast.create_filter(

fromBlock='latest'

)



while True:

try:

events = event_filter.get_new_entries()

for event in events:

args = event['args']

# Laravel์— ํˆฌํ‘œ ๊ฒฐ๊ณผ ๋™๊ธฐํ™”

async with httpx.AsyncClient() as client:

await client.post(

f"{self.laravel_url}/internal/votes/sync",

json={

"proposal_id": str(args['proposalId']),

"voter": args['voter'],

"support": args['support'],

"weight": str(args['weight']),

"reason": args.get('reason', ''),

},

headers={"X-Internal-Secret": "your-secret-key"}

)

await asyncio.sleep(12)

except Exception as e:

logger.error(f"VoteCast ๋ฆฌ์Šค๋‹ ์˜ค๋ฅ˜: {e}")

await asyncio.sleep(30)



async def run_all_listeners(self):

"""๋ชจ๋“  ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋™์‹œ ์‹คํ–‰"""

await asyncio.gather(

self.listen_proposal_created(),

self.listen_vote_cast(),

self.listen_transfer_events(),  # ํ† ํฐ ์ด์ „ ๊ฐ์ง€

self.listen_delegate_changed(), # ์œ„์ž„ ๋ณ€๊ฒฝ ๊ฐ์ง€

)
๐Ÿ Python Web3 ์„œ๋น„์Šค โ€” ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ํ๋ฆ„ โ›“๏ธ ๋ธ”๋ก์ฒด์ธ ProposalCreated ์ด๋ฒคํŠธ ๋ฐœ์ƒ VoteCast ํˆฌํ‘œ ์ด๋ฒคํŠธ Transfer ํ† ํฐ ์ด์ „ ProposalExecuted ์‹คํ–‰ ์™„๋ฃŒ DelegateChanged ์œ„์ž„ ๋ณ€๊ฒฝ ๐Ÿ Event Listener web3.py ํ•„ํ„ฐ 12์ดˆ ํด๋ง ์ด๋ฒคํŠธ ํŒŒ์‹ฑ ABI ๋””์ฝ”๋”ฉ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ์˜จ์ฒด์ธ ํ™•์ธ Redis ๋ฐœํ–‰ ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ Laravel ์ฝœ๋ฐฑ HTTP POST ๐Ÿ”ด Redis Pub/Sub ์ฑ„๋„ dao_events blockchain_jobs job_results ๐ŸŽฏ Laravel ๋‚ด๋ถ€ API ์ˆ˜์‹  DB ์—…๋ฐ์ดํŠธ ์•Œ๋ฆผ ๋ฐœ์†ก ์บ์‹œ ๊ฐฑ์‹  ๐ŸŒ ํ”„๋ก ํŠธ์—”๋“œ WebSocket ๊ตฌ๋… ์‹ค์‹œ๊ฐ„ ํˆฌํ‘œ ํ˜„ํ™ฉ ํ”„๋กœํฌ์ ˆ ๋ชฉ๋ก Laravel API ์กฐํšŒ ํˆฌํ‘œ ์ œ์ถœ ์ง€๊ฐ‘ ์„œ๋ช… โ†’ API ์•Œ๋ฆผ ์ˆ˜์‹  ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์—…๋ฐ์ดํŠธ ๋Œ€์‹œ๋ณด๋“œ ๊ฑฐ๋ฒ„๋„Œ์Šค ํ†ต๊ณ„ ํˆฌํ‘œ ์ฐธ์—ฌ์œจ ๋ถ„์„ ๋ธ”๋ก์ฒด์ธ ์ด๋ฒคํŠธ โ†’ Python ๊ฐ์ง€ โ†’ Redis/HTTP โ†’ Laravel ์ฒ˜๋ฆฌ โ†’ ํ”„๋ก ํŠธ์—”๋“œ ๋ฐ˜์˜ ํ‰๊ท  ์ง€์—ฐ์‹œ๊ฐ„: 12~24์ดˆ (์ด๋”๋ฆฌ์›€ ๋ธ”๋ก ํ™•์ธ ์‹œ๊ฐ„ ๊ธฐ์ค€)

๐Ÿ”„ Laravel โ†” Python ํ†ต์‹  ํŒจํ„ด ์‹ฌํ™”

๋‘ ์„œ๋น„์Šค๊ฐ€ ์–ด๋–ป๊ฒŒ ๋Œ€ํ™”ํ•˜๋Š”์ง€ ๋” ๊นŠ์ด ํŒŒ๋ณด์ž. ์ด ๋ถ€๋ถ„์ด ์•„ํ‚คํ…์ฒ˜์˜ ํ•ต์‹ฌ์ด๊ฑฐ๋“ !

ํŒจํ„ด 1: ๋™๊ธฐ HTTP ํ†ต์‹  (์ฆ‰์‹œ ์‘๋‹ต ํ•„์š”)

์„œ๋ช… ๊ฒ€์ฆ, ํ† ํฐ ์ž”์•ก ์กฐํšŒ์ฒ˜๋Ÿผ ์ฆ‰์‹œ ์‘๋‹ต์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ์— ์‚ฌ์šฉํ•ด:


// Laravel โ†’ Python ๋™๊ธฐ ํ˜ธ์ถœ ์˜ˆ์‹œ

// app/Http/Controllers/MemberController.php



public function getTokenBalance(Request $request): JsonResponse

{

$address = $request->user()->wallet_address;



try {

// Python ์„œ๋น„์Šค์—์„œ ์‹ค์‹œ๊ฐ„ ์ž”์•ก ์กฐํšŒ

$balance = $this->pythonClient->getTokenBalance($address);



// DB ์—…๋ฐ์ดํŠธ (๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌ)

UpdateMemberBalanceJob::dispatch($address, $balance);



return response()->json([

'address' => $address,

'balance' => $balance,

'formatted' => number_format($balance / 1e18, 4) . ' GOV',

]);

} catch (ConnectionException $e) {

// Python ์„œ๋น„์Šค ๋‹ค์šด ์‹œ ์บ์‹œ ํด๋ฐฑ

$cachedBalance = Cache::get("token_balance:{$address}", '0');

return response()->json([

'balance' => $cachedBalance,

'source' => 'cache', // ์บ์‹œ์—์„œ ๊ฐ€์ ธ์™”์Œ์„ ๋ช…์‹œ

]);

}

}

ํŒจํ„ด 2: ๋น„๋™๊ธฐ Redis ํ ํ†ต์‹  (์‹œ๊ฐ„์ด ๊ฑธ๋ฆฌ๋Š” ์ž‘์—…)

ํŠธ๋žœ์žญ์…˜ ์ „์†ก์ฒ˜๋Ÿผ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฌ๋Š” ์ž‘์—…์€ ํ๋ฅผ ํ†ตํ•ด ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•ด:


# Python ์ชฝ Redis ํ ์›Œ์ปค

# python/workers/job_processor.py



import redis

import json

import asyncio

from services.governor_service import GovernorService



class BlockchainJobProcessor:

def __init__(self, redis_client, governor_service: GovernorService):

self.redis = redis_client

self.governor = governor_service



async def process_jobs(self):

"""Redis ํ์—์„œ ์ž‘์—… ๊บผ๋‚ด์„œ ์ฒ˜๋ฆฌ"""

while True:

try:

# ๋ธ”๋กœํ‚น ํŒ (์ž‘์—…์ด ์˜ฌ ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ)

job_data = self.redis.brpop('blockchain_jobs', timeout=30)



if job_data:

_, job_json = job_data

job = json.loads(job_json)



await self._process_single_job(job)



except Exception as e:

print(f"์ž‘์—… ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜: {e}")
await asyncio.sleep(5)



async def _process_single_job(self, job: dict):

job_id = job['job_id']

job_type = job['type']

payload = job['payload']



try:

result = None



if job_type == 'SUBMIT_PROPOSAL':

result = self.governor.submit_proposal(

targets=payload['targets'],

values=payload['values'],

calldatas=payload['calldatas'],

description=payload['description'],

private_key=payload['private_key']

)

elif job_type == 'CAST_VOTE':

result = await self._cast_vote(payload)

elif job_type == 'QUEUE_PROPOSAL':

result = await self._queue_proposal(payload)

elif job_type == 'EXECUTE_PROPOSAL':

result = await self._execute_proposal(payload)



# ๊ฒฐ๊ณผ๋ฅผ Redis์— ์ €์žฅ (Laravel์ด ํด๋ง์œผ๋กœ ํ™•์ธ)

self.redis.setex(

f"job_result:{job_id}",

3600,  # 1์‹œ๊ฐ„ ๋ณด๊ด€

json.dumps({

'status': 'completed',

'result': result,

'completed_at': asyncio.get_event_loop().time()

})

)



except Exception as e:

self.redis.setex(

f"job_result:{job_id}",

3600,

json.dumps({'status': 'failed', 'error': str(e)})

)

ํŒจํ„ด 3: WebSocket์„ ํ†ตํ•œ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ

Laravel์˜ Broadcasting ๊ธฐ๋Šฅ๊ณผ Redis Pub/Sub์„ ๊ฒฐํ•ฉํ•ด์„œ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ๋ฅผ ๊ตฌํ˜„ํ•ด:


// Laravel Broadcasting ์„ค์ •

// app/Events/ProposalStatusUpdated.php



class ProposalStatusUpdated implements ShouldBroadcast

{

use Dispatchable, InteractsWithSockets, SerializesModels;



public function __construct(

public Proposal $proposal

) {}



public function broadcastOn(): array

{

return [

new Channel('proposals'),

new PrivateChannel("proposal.{$this->proposal->id}"),

];

}



public function broadcastWith(): array

{

return [

'id' => $this->proposal->id,

'status' => $this->proposal->status,

'for_votes' => $this->proposal->for_votes,

'against_votes' => $this->proposal->against_votes,

'updated_at' => $this->proposal->updated_at,

];

}

}



// Python์—์„œ Redis Pub/Sub์œผ๋กœ Laravel์— ์ด๋ฒคํŠธ ์ „๋‹ฌ

# python/workers/event_broadcaster.py



class EventBroadcaster:

def __init__(self, redis_client):

self.redis = redis_client

self.pubsub = redis_client.pubsub()



def broadcast_proposal_update(self, proposal_id: int, status: str):

"""Laravel Broadcasting ์ฑ„๋„์— ์ด๋ฒคํŠธ ๋ฐœํ–‰"""

# Laravel Echo ์„œ๋ฒ„๊ฐ€ ๊ตฌ๋…ํ•˜๋Š” ์ฑ„๋„์— ๋ฐœํ–‰

channel_data = {

'event': 'ProposalStatusUpdated',

'data': {

'proposal_id': proposal_id,

'status': status,

}

}

self.redis.publish(

f'laravel_database_proposals',

json.dumps(channel_data)

)

๐Ÿ” ๋ณด์•ˆ ์„ค๊ณ„ โ€” DAO ๋ฐฑ์—”๋“œ์˜ ๋ณด์•ˆ์€ ์ƒ๋ช…์ด๋‹ค

DAO๋Š” ์‹ค์ œ ์ž๊ธˆ์„ ๋‹ค๋ฃจ๊ธฐ ๋•Œ๋ฌธ์— ๋ณด์•ˆ์ด ์ •๋ง ์ค‘์š”ํ•ด. ์ ˆ๋Œ€ ๋Œ€์ถฉ ๋„˜์–ด๊ฐ€๋ฉด ์•ˆ ๋˜๋Š” ํŒŒํŠธ์•ผ! ๐Ÿ˜ค

ํ•ต์‹ฌ ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ

๐Ÿ”‘
ํ”„๋ผ์ด๋น— ํ‚ค ๊ด€๋ฆฌ โ€” HSM/KMS ํ•„์ˆ˜
Python ์„œ๋น„์Šค์—์„œ ํŠธ๋žœ์žญ์…˜์„ ์„œ๋ช…ํ•  ๋•Œ ํ”„๋ผ์ด๋น— ํ‚ค๋ฅผ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜๋ฉด ์ ˆ๋Œ€ ์•ˆ ๋ผ. AWS KMS, HashiCorp Vault, ๋˜๋Š” ํ•˜๋“œ์›จ์–ด ๋ณด์•ˆ ๋ชจ๋“ˆ(HSM)์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ด.

# python/services/key_management.py

import boto3

from web3 import Web3



class AWSKMSSigner:

"""AWS KMS๋ฅผ ์‚ฌ์šฉํ•œ ์•ˆ์ „ํ•œ ํŠธ๋žœ์žญ์…˜ ์„œ๋ช…"""



def __init__(self, key_id: str, region: str = 'ap-northeast-2'):

self.kms_client = boto3.client('kms', region_name=region)

self.key_id = key_id



def sign_transaction(self, tx_hash: bytes) -> bytes:

"""KMS๋กœ ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ ์„œ๋ช…"""

response = self.kms_client.sign(

KeyId=self.key_id,

Message=tx_hash,

MessageType='DIGEST',

SigningAlgorithm='ECDSA_SHA_256'

)

return response['Signature']



def get_public_address(self) -> str:

"""KMS ํ‚ค์˜ ์ด๋”๋ฆฌ์›€ ์ฃผ์†Œ ๋ฐ˜ํ™˜"""

response = self.kms_client.get_public_key(KeyId=self.key_id)

# DER ์ธ์ฝ”๋”ฉ๋œ ๊ณต๊ฐœํ‚ค์—์„œ ์ด๋”๋ฆฌ์›€ ์ฃผ์†Œ ์ถ”์ถœ

public_key_der = response['PublicKey']

# ... ์ฃผ์†Œ ์ถ”์ถœ ๋กœ์ง

return address
๐Ÿ›ก๏ธ
๋‚ด๋ถ€ ์„œ๋น„์Šค ํ†ต์‹  ๋ณด์•ˆ
Laravel๊ณผ Python ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹ ์—๋Š” ๋ฐ˜๋“œ์‹œ ๋‚ด๋ถ€ ์‹œํฌ๋ฆฟ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•ด. ์™ธ๋ถ€์—์„œ ๋‚ด๋ถ€ API๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜์ง€ ๋ชปํ•˜๋„๋ก ๋ง‰์•„์•ผ ํ•ด.

// Laravel ๋‚ด๋ถ€ API ๋ณดํ˜ธ ๋ฏธ๋“ค์›จ์–ด

// app/Http/Middleware/InternalServiceAuth.php



class InternalServiceAuth

{

public function handle(Request $request, Closure $next): Response

{

$secret = $request->header('X-Internal-Secret');



if (!$secret || !hash_equals(config('services.internal_secret'), $secret)) {

return response()->json(['error' => '๋‚ด๋ถ€ ์„œ๋น„์Šค ์ธ์ฆ ์‹คํŒจ'], 403);

}



// IP ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ํ™•์ธ (Docker ๋‚ด๋ถ€ ๋„คํŠธ์›Œํฌ)

$allowedIps = ['172.16.0.0/12', '10.0.0.0/8'];

$clientIp = $request->ip();



$isAllowed = collect($allowedIps)->some(fn($range) =>

$this->ipInRange($clientIp, $range)

);



if (!$isAllowed) {

return response()->json(['error' => 'ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ IP'], 403);

}



return $next($request);

}

}
โšก
Rate Limiting โ€” API ๋‚จ์šฉ ๋ฐฉ์ง€
ํŠนํžˆ ํˆฌํ‘œ API๋Š” ์ค‘๋ณต ํˆฌํ‘œ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ์—„๊ฒฉํ•œ Rate Limiting์ด ํ•„์š”ํ•ด.

// routes/api.php

Route::middleware(['auth:sanctum'])->group(function () {

// ํˆฌํ‘œ๋Š” 1๋ถ„์— 5๋ฒˆ์œผ๋กœ ์ œํ•œ

Route::post('/proposals/{proposal}/vote', [VoteController::class, 'castVote'])

->middleware('throttle:5,1');



// ํ”„๋กœํฌ์ ˆ ์ƒ์„ฑ์€ 1์‹œ๊ฐ„์— 3๋ฒˆ์œผ๋กœ ์ œํ•œ

Route::post('/proposals', [ProposalController::class, 'store'])

->middleware('throttle:3,60');



// ์ผ๋ฐ˜ ์กฐํšŒ๋Š” 1๋ถ„์— 60๋ฒˆ

Route::get('/proposals', [ProposalController::class, 'index'])

->middleware('throttle:60,1');

});
๐Ÿ’ก ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ Web3 ๊ฐœ๋ฐœ์„ ๋ฐฐ์šฐ๊ณ  ์‹ถ๋‹ค๋ฉด: ์žฌ๋Šฅ๋„ท์—์„œ๋Š” Laravel, Python, ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ ์ „๋ฌธ๊ฐ€๋“ค์˜ 1:1 ๋ฉ˜ํ† ๋ง์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด. DAO ๋ฐฑ์—”๋“œ ์„ค๊ณ„์ฒ˜๋Ÿผ ๋ณต์žกํ•œ ์•„ํ‚คํ…์ฒ˜๋Š” ํ˜ผ์ž ๊ณต๋ถ€ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ์ „๋ฌธ๊ฐ€์—๊ฒŒ ์ง์ ‘ ๋ฐฐ์šฐ๋Š” ๊ฒŒ ํ›จ์”ฌ ํšจ์œจ์ ์ด๊ฑฐ๋“ !

๐Ÿ“Š ๊ฑฐ๋ฒ„๋„Œ์Šค ๋ถ„์„ โ€” Python ML๋กœ DAO ๊ฑด๊ฐ•๋„ ์ธก์ •ํ•˜๊ธฐ

Python์˜ ์ง„์งœ ๊ฐ•์ ์ด ์—ฌ๊ธฐ์„œ ๋‚˜์™€! ๋ฐ์ดํ„ฐ ๋ถ„์„๊ณผ ML์„ ํ™œ์šฉํ•ด์„œ DAO์˜ ๊ฑฐ๋ฒ„๋„Œ์Šค ๊ฑด๊ฐ•๋„๋ฅผ ์ธก์ •ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์–ด๋ณด์ž.

๊ฑฐ๋ฒ„๋„Œ์Šค ๋ถ„์„ ์„œ๋น„์Šค


# python/services/governance_analytics.py

import pandas as pd

import numpy as np

from web3 import Web3

from gql import gql, Client

from gql.transport.requests import RequestsHTTPTransport



class GovernanceAnalytics:

"""DAO ๊ฑฐ๋ฒ„๋„Œ์Šค ๊ฑด๊ฐ•๋„ ๋ถ„์„ ์„œ๋น„์Šค"""



def __init__(self, subgraph_url: str):

# The Graph ์„œ๋ธŒ๊ทธ๋ž˜ํ”„ ํด๋ผ์ด์–ธํŠธ

transport = RequestsHTTPTransport(url=subgraph_url)

self.graph_client = Client(transport=transport)



def get_voter_participation_stats(self, days: int = 30) -> dict:

"""ํˆฌํ‘œ ์ฐธ์—ฌ์œจ ํ†ต๊ณ„"""

query = gql("""

query GetVotes($since: Int!) {

votes(where: {blockTimestamp_gte: $since}) {

voter { id tokenBalance }

proposal { id }

support

weight

}

}

""")



since = int((pd.Timestamp.now() - pd.Timedelta(days=days)).timestamp())

result = self.graph_client.execute(query, {'since': since})



df = pd.DataFrame(result['votes'])



if df.empty:

return {'participation_rate': 0, 'unique_voters': 0}



# ๊ณ ๋ž˜ vs ์†Œ์•ก ํˆฌํ‘œ์ž ๋ถ„์„

df['weight_float'] = df['weight'].astype(float) / 1e18

whale_threshold = df['weight_float'].quantile(0.9)



return {

'unique_voters': df['voter'].apply(lambda x: x['id']).nunique(),

'total_votes': len(df),

'whale_voters': len(df[df['weight_float'] >= whale_threshold]),

'avg_voting_power': df['weight_float'].mean(),

'median_voting_power': df['weight_float'].median(),

'gini_coefficient': self._calculate_gini(df['weight_float'].values),

'participation_trend': self._calculate_trend(df),

}



def _calculate_gini(self, values: np.ndarray) -> float:

"""์ง€๋‹ˆ ๊ณ„์ˆ˜ ๊ณ„์‚ฐ (ํˆฌํ‘œ๊ถŒ ๋ถ„๋ฐฐ ๋ถˆํ‰๋“ฑ๋„)"""

sorted_vals = np.sort(values)

n = len(sorted_vals)

cumsum = np.cumsum(sorted_vals)

return (2 * np.sum((np.arange(1, n+1) * sorted_vals))) / (n * cumsum[-1]) - (n+1)/n



def calculate_dao_health_score(self) -> dict:

"""DAO ๊ฑด๊ฐ•๋„ ์ข…ํ•ฉ ์ ์ˆ˜ (0-100)"""

participation = self.get_voter_participation_stats()



# ๊ฐ ์ง€ํ‘œ๋ณ„ ์ ์ˆ˜ ๊ณ„์‚ฐ

participation_score = min(participation['unique_voters'] / 100 * 30, 30)

decentralization_score = max(0, 30 - participation['gini_coefficient'] * 30)

activity_score = min(participation['total_votes'] / 50 * 20, 20)

diversity_score = self._calculate_voter_diversity_score() * 20



total_score = participation_score + decentralization_score + activity_score + diversity_score



return {

'total_score': round(total_score, 1),

'grade': self._score_to_grade(total_score),

'breakdown': {

'participation': round(participation_score, 1),

'decentralization': round(decentralization_score, 1),

'activity': round(activity_score, 1),

'diversity': round(diversity_score, 1),

}

}

๐Ÿš€ ๋ฐฐํฌ ์ „๋žต โ€” ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ ๊ตฌ์„ฑ

๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ ์ž˜ ๋Œ์•„๊ฐ„๋‹ค๊ณ  ๋์ด ์•„๋‹ˆ์•ผ. ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ๊นŒ์ง€ ์ƒ๊ฐํ•ด์•ผ ์ง„์งœ ์™„์„ฑ์ด๊ฑฐ๋“ !

Kubernetes ๊ธฐ๋ฐ˜ ๋ฐฐํฌ ๊ตฌ์„ฑ


# k8s/laravel-deployment.yaml

apiVersion: apps/v1

kind: Deployment

metadata:

name: laravel-api

labels:

app: dao-laravel

spec:

replicas: 3  # ์ˆ˜ํ‰ ํ™•์žฅ

selector:

matchLabels:

app: dao-laravel

template:

metadata:

labels:

app: dao-laravel

spec:

containers:

- name: laravel

image: your-registry/dao-laravel:latest

ports:

- containerPort: 8000

env:

- name: DB_HOST

valueFrom:

secretKeyRef:

name: dao-secrets

key: db-host

- name: REDIS_HOST

value: "redis-service"

resources:

requests:

memory: "256Mi"

cpu: "250m"

limits:

memory: "512Mi"

cpu: "500m"

livenessProbe:

httpGet:

path: /api/health

port: 8000

initialDelaySeconds: 30

periodSeconds: 10

---

# k8s/python-deployment.yaml

apiVersion: apps/v1

kind: Deployment

metadata:

name: python-worker

spec:

replicas: 2

selector:

matchLabels:

app: dao-python

template:

spec:

containers:

- name: python

image: your-registry/dao-python:latest

env:

- name: WEB3_PROVIDER_URL

valueFrom:

secretKeyRef:

name: dao-secrets

key: web3-provider

resources:

requests:

memory: "512Mi"

cpu: "500m"

limits:

memory: "1Gi"

cpu: "1000m"

๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์•Œ๋ฆผ ์„ค์ •

๐Ÿ“Š ๋ชจ๋‹ˆํ„ฐ๋ง ์Šคํƒ โ€ข Prometheus: ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘
โ€ข Grafana: ๋Œ€์‹œ๋ณด๋“œ ์‹œ๊ฐํ™”
โ€ข Loki: ๋กœ๊ทธ ์ง‘๊ณ„
โ€ข Alertmanager: ์•Œ๋ฆผ ๋ฐœ์†ก

์ฃผ์š” ๋ชจ๋‹ˆํ„ฐ๋ง ์ง€ํ‘œ:
- API ์‘๋‹ต ์‹œ๊ฐ„
- ๋ธ”๋ก์ฒด์ธ ์—ฐ๊ฒฐ ์ƒํƒœ
- ํ ์ ์ฒด ์ˆ˜
- ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์ง€์—ฐ
๐Ÿšจ ํ•ต์‹ฌ ์•Œ๋ฆผ ์กฐ๊ฑด โ€ข Python ์„œ๋น„์Šค ๋‹ค์šด โ†’ ์ฆ‰์‹œ ์•Œ๋ฆผ
โ€ข ๋ธ”๋ก์ฒด์ธ ์—ฐ๊ฒฐ ๋Š๊น€ โ†’ ์ฆ‰์‹œ ์•Œ๋ฆผ
โ€ข ํ ์ ์ฒด 100๊ฐœ ์ดˆ๊ณผ โ†’ ๊ฒฝ๊ณ 
โ€ข ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์ง€์—ฐ 5๋ถ„ ์ดˆ๊ณผ โ†’ ๊ฒฝ๊ณ 
โ€ข ๊ฐ€์Šค๋น„ ๊ธ‰๋“ฑ โ†’ ์ •๋ณด ์•Œ๋ฆผ
โ€ข ํ”„๋กœํฌ์ ˆ ํˆฌํ‘œ ์‹œ์ž‘ โ†’ ๋ฉค๋ฒ„ ์•Œ๋ฆผ
โ€ข ํ”„๋กœํฌ์ ˆ ์‹คํ–‰ ์™„๋ฃŒ โ†’ ์ „์ฒด ์•Œ๋ฆผ
  • โš ๏ธ ํ”„๋กœ๋•์…˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ:
  • โœ… ํ”„๋ผ์ด๋น— ํ‚ค๋Š” ์ ˆ๋Œ€ ํ™˜๊ฒฝ๋ณ€์ˆ˜์— ์ง์ ‘ ์ €์žฅ ๊ธˆ์ง€ โ†’ KMS/Vault ์‚ฌ์šฉ
  • โœ… ๋‚ด๋ถ€ ์„œ๋น„์Šค ํ†ต์‹ ์€ TLS ์ ์šฉ
  • โœ… ๋ธ”๋ก์ฒด์ธ ๋…ธ๋“œ๋Š” Infura/Alchemy ์™ธ์— ์ž์ฒด ๋…ธ๋“œ ๋ฐฑ์—… ๊ตฌ์„ฑ
  • โœ… ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์žฌ์‹œ์ž‘ ์‹œ ๋†“์นœ ์ด๋ฒคํŠธ ๋ณต๊ตฌ ๋กœ์ง ํ•„์ˆ˜
  • โœ… ํŠธ๋žœ์žญ์…˜ ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ ๋กœ์ง ๊ตฌํ˜„
  • โœ… ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ABI ๋ฒ„์ „ ๊ด€๋ฆฌ ์ฒ ์ €ํžˆ
๐Ÿš€ ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ์•„ํ‚คํ…์ฒ˜ (Kubernetes) ๐ŸŒ CDN + Load Balancer CloudFront / Nginx Ingress ๐ŸŽฏ Laravel API Pods (ร—3) Pod 1 8000 Pod 2 8000 Pod 3 8000 HPA: CPU 70% ๊ธฐ์ค€ ์ž๋™ ํ™•์žฅ ๐Ÿ Python Worker Pods (ร—2) FastAPI Pod 1 5000 FastAPI Pod 2 5000 ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ + Job Processor ๐Ÿ”ง ๊ณต์œ  ์ธํ”„๋ผ Redis Cluster 6379 MySQL RDS 3306 Elasticsearch 9200 Multi-AZ ๊ณ ๊ฐ€์šฉ์„ฑ ๊ตฌ์„ฑ ๐Ÿ“Š ๋ชจ๋‹ˆํ„ฐ๋ง Prometheus + Grafana Loki ๋กœ๊ทธ ์ˆ˜์ง‘ Alertmanager ์•Œ๋ฆผ โ›“๏ธ ๋ธ”๋ก์ฒด์ธ ๋…ธ๋“œ Primary: Infura/Alchemy Backup: ์ž์ฒด Geth ๋…ธ๋“œ Failover ์ž๋™ ์ „ํ™˜ ๐Ÿ”„ CI/CD ํŒŒ์ดํ”„๋ผ์ธ GitHub Actions Docker Build โ†’ ECR Push ArgoCD ์ž๋™ ๋ฐฐํฌ ๋ชจ๋“  ์„œ๋น„์Šค๋Š” Kubernetes ํด๋Ÿฌ์Šคํ„ฐ ๋‚ด์—์„œ ์šด์˜ ยท ์ž๋™ ์Šค์ผ€์ผ๋ง ยท ๋ฌด์ค‘๋‹จ ๋ฐฐํฌ

๐Ÿงช ํ…Œ์ŠคํŠธ ์ „๋žต โ€” ๋ฏฟ์„ ์ˆ˜ ์žˆ๋Š” DAO ๋ฐฑ์—”๋“œ ๋งŒ๋“ค๊ธฐ

DAO๋Š” ์‹ค์ œ ์ž๊ธˆ์ด ์˜ค๊ฐ€๋Š” ์‹œ์Šคํ…œ์ด์•ผ. ํ…Œ์ŠคํŠธ ์—†์ด ๋ฐฐํฌํ•˜๋Š” ๊ฑด ์ž์‚ดํ–‰์œ„์•ผ! ๐Ÿ˜ฑ

Laravel ํ…Œ์ŠคํŠธ


// tests/Feature/ProposalTest.php

class ProposalTest extends TestCase

{

use RefreshDatabase;



public function test_member_can_create_proposal(): void

{

// Python ์„œ๋น„์Šค Mock

Http::fake([

'http://python-worker:5000/*' => Http::response([

'ipfs_hash' => 'QmTest123',

'tx_hash' => '0xabc123',

], 200),

]);



$member = Member::factory()->create([

'wallet_address' => '0x742d35Cc6634C0532925a3b8D4C9C5e3',

'token_balance' => '1000000000000000000000', // 1000 GOV

]);



$response = $this->actingAs($member, 'sanctum')

->postJson('/api/proposals', [

'title' => 'ํ…Œ์ŠคํŠธ ํ”„๋กœํฌ์ ˆ',

'description' => '์ด๊ฒƒ์€ ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค.',

'actions' => [],

]);



$response->assertStatus(201)

->assertJsonStructure(['id', 'title', 'status', 'ipfs_hash']);



$this->assertDatabaseHas('proposals', [

'title' => 'ํ…Œ์ŠคํŠธ ํ”„๋กœํฌ์ ˆ',

'status' => 'pending',

]);

}



public function test_member_cannot_vote_twice(): void

{

$proposal = Proposal::factory()->create(['status' => 'active']);

$member = Member::factory()->create();



// ์ฒซ ๋ฒˆ์งธ ํˆฌํ‘œ

$this->actingAs($member, 'sanctum')

->postJson("/api/proposals/{$proposal->id}/vote", ['support' => 'for'])

->assertStatus(200);



// ๋‘ ๋ฒˆ์งธ ํˆฌํ‘œ ์‹œ๋„ โ†’ ์‹คํŒจํ•ด์•ผ ํ•จ

$this->actingAs($member, 'sanctum')

->postJson("/api/proposals/{$proposal->id}/vote", ['support' => 'against'])

->assertStatus(422)

->assertJsonFragment(['error' => '์ด๋ฏธ ํˆฌํ‘œํ•˜์…จ์Šต๋‹ˆ๋‹ค.']);

}

}

Python ํ…Œ์ŠคํŠธ โ€” ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ Mock


# python/tests/test_governor_service.py

import pytest

from unittest.mock import MagicMock, patch

from web3 import Web3

from services.governor_service import GovernorService



@pytest.fixture

def mock_w3():

w3 = MagicMock(spec=Web3)

w3.is_connected.return_value = True

return w3



@pytest.fixture

def governor_service(mock_w3):

with patch.dict('os.environ', {'GOVERNOR_ADDRESS': '0x' + '1' * 40}):

return GovernorService(mock_w3)



def test_get_proposal_state_active(governor_service):

"""ํ™œ์„ฑ ํ”„๋กœํฌ์ ˆ ์ƒํƒœ ์กฐํšŒ ํ…Œ์ŠคํŠธ"""

# Mock ์ปจํŠธ๋ž™ํŠธ ์‘๋‹ต

governor_service.contract.functions.state.return_value.call.return_value = 1

governor_service.contract.functions.proposalVotes.return_value.call.return_value = (

100 * 10**18,  # against

500 * 10**18,  # for

50 * 10**18,   # abstain

)



result = governor_service.get_proposal_state(12345)



assert result['state'] == 'active'

assert result['for_votes'] == str(500 * 10**18)

assert result['against_votes'] == str(100 * 10**18)



def test_verify_signature_valid():

"""์œ ํšจํ•œ ์„œ๋ช… ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ"""

from eth_account import Account

from eth_account.messages import encode_defunct



# ํ…Œ์ŠคํŠธ ๊ณ„์ • ์ƒ์„ฑ

account = Account.create()

message = "ํ…Œ์ŠคํŠธ ๋ฉ”์‹œ์ง€"

msg_hash = encode_defunct(text=message)

signed = account.sign_message(msg_hash)



# ์„œ๋ช… ๊ฒ€์ฆ

recovered = Account.recover_message(msg_hash, signature=signed.signature)

assert recovered.lower() == account.address.lower()

๐Ÿ’ก ์‹ค์ „ ํŒ โ€” ๊ฐœ๋ฐœํ•˜๋ฉด์„œ ์ž์ฃผ ๋งŒ๋‚˜๋Š” ๋ฌธ์ œ๋“ค

์ด๋ก ์€ ๋‹ค ์•Œ๊ฒ ๋Š”๋ฐ ์‹ค์ œ๋กœ ๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฌธ์ œ๋“ค์ด ํ„ฐ์ ธ. ๋ฏธ๋ฆฌ ์•Œ๊ณ  ๊ฐ€์ž!

๋ฌธ์ œ 1: ์ด๋ฒคํŠธ ์ค‘๋ณต ์ฒ˜๋ฆฌ

๐Ÿ’ก ํ•ด๊ฒฐ์ฑ…:
์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๊ฐ€ ์žฌ์‹œ์ž‘๋˜๊ฑฐ๋‚˜ ์—ฌ๋Ÿฌ ์ธ์Šคํ„ด์Šค๊ฐ€ ๋™์‹œ์— ์‹คํ–‰๋  ๋•Œ ๊ฐ™์€ ์ด๋ฒคํŠธ๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด. Redis์— ์ฒ˜๋ฆฌ๋œ ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ๋ฅผ ์ €์žฅํ•ด์„œ ์ค‘๋ณต ์ฒ˜๋ฆฌ๋ฅผ ๋ฐฉ์ง€ํ•ด์•ผ ํ•ด.

processed_key = f"processed_event:{tx_hash}:{log_index}"

if redis.exists(processed_key): return # ์ด๋ฏธ ์ฒ˜๋ฆฌ๋จ
redis.setex(processed_key, 86400, "1") # 24์‹œ๊ฐ„ ๋ณด๊ด€

๋ฌธ์ œ 2: ๋ธ”๋ก ์žฌ์กฐ์ง(Reorg) ์ฒ˜๋ฆฌ

โš ๏ธ ์ฃผ์˜: ์ด๋”๋ฆฌ์›€์—์„œ๋Š” ๋ธ”๋ก ์žฌ์กฐ์ง์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ ์ตœ์‹  ๋ธ”๋ก์—์„œ ๊ฐ€์ ธ์˜จ ์ด๋ฒคํŠธ๋Š” ๋‚˜์ค‘์— ์ทจ์†Œ๋  ์ˆ˜ ์žˆ๊ฑฐ๋“ . ์ตœ์†Œ 12๋ธ”๋ก ํ™•์ธ ํ›„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒŒ ์•ˆ์ „ํ•ด. Polygon ๊ฐ™์€ L2๋Š” ๋” ๋น ๋ฅด์ง€๋งŒ ์—ฌ์ „ํžˆ ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•ด.

# ์•ˆ์ „ํ•œ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ - ์ถฉ๋ถ„ํ•œ ๋ธ”๋ก ํ™•์ธ ํ›„ ์ฒ˜๋ฆฌ

SAFE_BLOCK_CONFIRMATIONS = 12  # ์ด๋”๋ฆฌ์›€ ๋ฉ”์ธ๋„ท

# SAFE_BLOCK_CONFIRMATIONS = 64  # Polygon



async def process_event_safely(event, current_block: int):

event_block = event['blockNumber']

confirmations = current_block - event_block



if confirmations < SAFE_BLOCK_CONFIRMATIONS:

# ์•„์ง ํ™•์ธ์ด ๋ถ€์กฑํ•จ - ๋‚˜์ค‘์— ๋‹ค์‹œ ์ฒ˜๋ฆฌ

await asyncio.sleep(12 * (SAFE_BLOCK_CONFIRMATIONS - confirmations))

return False



return True  # ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ

๋ฌธ์ œ 3: ๊ฐ€์Šค๋น„ ๊ธ‰๋“ฑ ์‹œ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ

๐Ÿ’ก ํ•ด๊ฒฐ์ฑ…: ๊ฐ€์Šค๋น„๊ฐ€ ๊ธ‰๋“ฑํ•  ๋•Œ ํŠธ๋žœ์žญ์…˜์ด ์˜ค๋ž˜ pending ์ƒํƒœ์— ๋จธ๋ฌผ ์ˆ˜ ์žˆ์–ด. EIP-1559 ๋™์  ๊ฐ€์Šค๋น„์™€ ํŠธ๋žœ์žญ์…˜ ๊ต์ฒด(Replace by Fee) ๋กœ์ง์„ ๊ตฌํ˜„ํ•ด์•ผ ํ•ด.

# python/services/gas_optimizer.py

class GasOptimizer:

def get_optimal_gas_params(self, urgency: str = 'normal') -> dict:

"""EIP-1559 ์ตœ์  ๊ฐ€์Šค ํŒŒ๋ผ๋ฏธํ„ฐ ๊ณ„์‚ฐ"""

latest_block = w3.eth.get_block('latest')

base_fee = latest_block['baseFeePerGas']



# ์šฐ์„ ์ˆœ์œ„์— ๋”ฐ๋ฅธ max_priority_fee ์„ค์ •

priority_fees = {

'slow': Web3.to_wei(1, 'gwei'),

'normal': Web3.to_wei(2, 'gwei'),

'fast': Web3.to_wei(5, 'gwei'),

'urgent': Web3.to_wei(10, 'gwei'),

}



max_priority_fee = priority_fees.get(urgency, priority_fees['normal'])

max_fee = base_fee * 2 + max_priority_fee  # ๋‹ค์Œ ๋ธ”๋ก ๋Œ€๋น„ ์—ฌ์œ 



return {

'maxFeePerGas': max_fee,

'maxPriorityFeePerGas': max_priority_fee,

}

๋ฌธ์ œ 4: IPFS ํ•€๋‹ ์„œ๋น„์Šค ์„ ํƒ

IPFS์— ๋ฐ์ดํ„ฐ๋ฅผ ์˜ฌ๋ ธ๋‹ค๊ณ  ์˜๊ตฌ ๋ณด์กด๋˜๋Š” ๊ฒŒ ์•„๋‹ˆ์•ผ! ํ•€๋‹ ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด:

์„œ๋น„์Šค ๋ฌด๋ฃŒ ํ”Œ๋žœ ํŠน์ง• ์ถ”์ฒœ ์šฉ๋„
Pinata 1GB API ์‚ฌ์šฉ ํŽธ๋ฆฌ, ์•ˆ์ •์  ํ”„๋กœ๋•์…˜ ์ถ”์ฒœ
Web3.Storage 5GB Filecoin ๋ฐฑ์—… ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ
Infura IPFS 5GB Infura ํ†ตํ•ฉ ๊ธฐ์กด Infura ์‚ฌ์šฉ์ž
์ž์ฒด IPFS ๋…ธ๋“œ ๋ฌด์ œํ•œ ์™„์ „ํ•œ ํ†ต์ œ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ

๐ŸŽ“ ๋งˆ๋ฌด๋ฆฌ โ€” ์ด ์•„ํ‚คํ…์ฒ˜์˜ ์ง„์งœ ๊ฐ•์ 

์ž, ์ด์ œ ์ „์ฒด ๊ทธ๋ฆผ์ด ๊ทธ๋ ค์กŒ์ง€? ๋งˆ์ง€๋ง‰์œผ๋กœ ์ด ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•„ํ‚คํ…์ฒ˜๊ฐ€ ์™œ ์ข‹์€์ง€ ์ •๋ฆฌํ•ด๋ณด์ž.

๐Ÿ† Laravel + Python DAO ์•„ํ‚คํ…์ฒ˜์˜ ํ•ต์‹ฌ ๊ฐ•์ 

1. ๊ด€์‹ฌ์‚ฌ์˜ ๋ถ„๋ฆฌ (Separation of Concerns)
๊ฐ ์„œ๋น„์Šค๊ฐ€ ์ž์‹ ์ด ๊ฐ€์žฅ ์ž˜ํ•˜๋Š” ์ผ๋งŒ ํ•ด. Laravel์€ ์˜คํ”„์ฒด์ธ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง, Python์€ ์˜จ์ฒด์ธ ์ธํ„ฐ๋ž™์…˜. ์„œ๋กœ ๊ฐ„์„ญ ์—†์ด ๋…๋ฆฝ์ ์œผ๋กœ ๋ฐœ์ „ํ•  ์ˆ˜ ์žˆ์–ด.

2. ๊ธฐ์ˆ  ์ƒํƒœ๊ณ„ ์ตœ์  ํ™œ์šฉ
PHP/Laravel์˜ ํ’๋ถ€ํ•œ ์›น ๊ฐœ๋ฐœ ์ƒํƒœ๊ณ„ + Python์˜ Web3/ML ์ƒํƒœ๊ณ„๋ฅผ ๋™์‹œ์— ํ™œ์šฉ. ์–ด๋А ํ•˜๋‚˜๋„ ํฌ๊ธฐํ•˜์ง€ ์•Š์•„๋„ ๋ผ.

3. ์žฅ์•  ๊ฒฉ๋ฆฌ (Fault Isolation)
Python ์„œ๋น„์Šค๊ฐ€ ๋‹ค์šด๋˜์–ด๋„ Laravel์€ ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๋กœ ๊ณ„์† ์„œ๋น„์Šค ๊ฐ€๋Šฅ. ๋ธ”๋ก์ฒด์ธ ์—ฐ๊ฒฐ์ด ๋Š๊ฒจ๋„ ์˜คํ”„์ฒด์ธ ๊ธฐ๋Šฅ์€ ์ •์ƒ ๋™์ž‘.

4. ๋…๋ฆฝ์  ํ™•์žฅ (Independent Scaling)
ํŠธ๋ž˜ํ”ฝ์ด ๋ชฐ๋ฆด ๋•Œ Laravel Pod๋งŒ ๋Š˜๋ฆฌ๊ฑฐ๋‚˜, ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๊ฐ€ ๋งŽ์„ ๋•Œ Python Worker๋งŒ ๋Š˜๋ฆด ์ˆ˜ ์žˆ์–ด. ํšจ์œจ์ ์ธ ๋ฆฌ์†Œ์Šค ๊ด€๋ฆฌ ๊ฐ€๋Šฅ.
๐Ÿš€
๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋‚˜์•„๊ฐ€๋ ค๋ฉด?
์ด ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋” ๋ฐœ์ „์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” ๋ฐฉํ–ฅ๋“ค์ด์•ผ:
โ€ข The Graph ์„œ๋ธŒ๊ทธ๋ž˜ํ”„ ๊ตฌ์ถ•์œผ๋กœ ์˜จ์ฒด์ธ ๋ฐ์ดํ„ฐ ์ธ๋ฑ์‹ฑ ๊ณ ๋„ํ™”
โ€ข Snapshot.org ํ†ตํ•ฉ์œผ๋กœ ๊ฐ€์Šค๋น„ ์—†๋Š” ์˜คํ”„์ฒด์ธ ํˆฌํ‘œ ์ง€์›
โ€ข Safe(Gnosis Safe) ๋ฉ€ํ‹ฐ์‹œ๊ทธ ํ†ตํ•ฉ์œผ๋กœ ํŠธ๋ ˆ์ €๋ฆฌ ๋ณด์•ˆ ๊ฐ•ํ™”
โ€ข zkProof ๊ธฐ๋ฐ˜ ํ”„๋ผ์ด๋ฒ„์‹œ ํˆฌํ‘œ ๊ตฌํ˜„
โ€ข ํฌ๋กœ์Šค์ฒด์ธ ๊ฑฐ๋ฒ„๋„Œ์Šค (์—ฌ๋Ÿฌ ์ฒด์ธ์˜ ํ† ํฐ ํ™€๋” ํ†ตํ•ฉ ํˆฌํ‘œ)
๐Ÿ“š ๋” ๊นŠ์ด ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด: ์ด๋Ÿฐ ๋ณต์žกํ•œ Web3 ๋ฐฑ์—”๋“œ ์•„ํ‚คํ…์ฒ˜๋ฅผ ํ˜ผ์ž ๊ณต๋ถ€ํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท์—์„œ ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ ์ „๋ฌธ๊ฐ€๋ฅผ ์ฐพ์•„ 1:1 ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋‚˜ ์•„ํ‚คํ…์ฒ˜ ์ปจ์„คํŒ…์„ ๋ฐ›์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ์‹ค์ œ ํ”„๋กœ์ ํŠธ ๊ฒฝํ—˜์ด ์žˆ๋Š” ๊ฐœ๋ฐœ์ž์—๊ฒŒ ๋ฐฐ์šฐ๋ฉด ์ด๋ก ๊ณผ ์‹ค์ „์˜ ๊ฐญ์„ ๋น ๋ฅด๊ฒŒ ์ค„์ผ ์ˆ˜ ์žˆ๊ฑฐ๋“ !

์ด ์•„ํ‚คํ…์ฒ˜๋Š” ์‹ค์ œ ํ”„๋กœ๋•์…˜ DAO ํ”„๋กœ์ ํŠธ์—์„œ ๊ฒ€์ฆ๋œ ํŒจํ„ด๋“ค์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž‘์„ฑ๋˜์—ˆ์–ด.
๋ฌผ๋ก  ํ”„๋กœ์ ํŠธ๋งˆ๋‹ค ์š”๊ตฌ์‚ฌํ•ญ์ด ๋‹ค๋ฅด๋‹ˆ๊นŒ, ์ด๊ฑธ ๋ฒ ์ด์Šค๋กœ ์‚ผ์•„์„œ ์ž์‹ ์˜ DAO์— ๋งž๊ฒŒ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด! ๐Ÿ’ช

Happy Building! ๐Ÿ—๏ธโ›“๏ธ๐Ÿš€

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

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