Ver3.0 ๐ ๋ผ๋ผ๋ฒจ๋ก ๋ง๋๋ DAO ์์ฌ๊ฒฐ์ ๊ธฐ๋ก ํ๋ซํผ ์๋ฒฝ ๊ฐ์ด๋

๐ ๋ผ๋ผ๋ฒจ๋ก ๋ง๋๋ DAO ์์ฌ๊ฒฐ์ ๊ธฐ๋ก ํ๋ซํผ ์๋ฒฝ ๊ฐ์ด๋
๋ธ๋ก์ฒด์ธ ๊ธฐ์ ๊ณผ PHP ํ๋ ์์ํฌ์ ๋ง๋จ, ํ์ค์ํ ์กฐ์ง์ ํฌ๋ช ํ ๊ฑฐ๋ฒ๋์ค ์์คํ ๊ตฌ์ถํ๊ธฐ
๐ DAO์ ์์ฌ๊ฒฐ์ ํ๋ซํผ, ์ ํ์ํ ๊น?
์์ฆ ๋ธ๋ก์ฒด์ธ ์
๊ณ์์ ๊ฐ์ฅ ํซํ ํค์๋ ์ค ํ๋๊ฐ ๋ฐ๋ก DAO(Decentralized Autonomous Organization)์ผ. ์ ํต์ ์ธ ํ์ฌ์ฒ๋ผ CEO๋ ์ด์ฌํ๊ฐ ๋ชจ๋ ๊ฑธ ๊ฒฐ์ ํ๋ ๊ฒ ์๋๋ผ, ์ปค๋ฎค๋ํฐ ๊ตฌ์ฑ์๋ค์ด ํจ๊ป ํฌํํ๊ณ ์์ฌ๊ฒฐ์ ์ ๋ด๋ฆฌ๋ ์กฐ์ง ํํ์ง.
๊ทธ๋ฐ๋ฐ ๋ฌธ์ ๊ฐ ์์ด. DAO์ ํต์ฌ ๊ฐ์น๋ 'ํฌ๋ช
์ฑ'๊ณผ 'ํ์ค์ํ'์ธ๋ฐ, ์ ์ ์์ฌ๊ฒฐ์ ๊ณผ์ ์ ์ ๋๋ก ๊ธฐ๋กํ๊ณ ๊ด๋ฆฌํ๋ ์์คํ
์ด ๋ถ์กฑํ ๊ฒฝ์ฐ๊ฐ ๋ง๋ค๋ ๊ฑฐ์ผ. ๐ญ
๋ธ๋ก์ฒด์ธ์ ๋ชจ๋ ๊ฑธ ๊ธฐ๋กํ๋ฉด ๋์ง ์๋๊ณ ? ๋ฌผ๋ก ์ต์ข
ํฌํ ๊ฒฐ๊ณผ๋ ์ค๋งํธ ์ปจํธ๋ํธ๋ก ์จ์ฒด์ธ์ ๊ธฐ๋กํ๋ ๊ฒ ๋ง์. ํ์ง๋ง ์ ์์ ์์ฑ, ํ ๋ก ๊ณผ์ , ์์ ์ด๋ ฅ, ์ฐธ๊ณ ์๋ฃ ๊ฐ์ ๊ฑด ๊ฐ์ค๋น ๋๋ฌธ์ ์จ์ฒด์ธ์ ๋ค ์ฌ๋ฆฌ๊ธฐ์ ๋นํจ์จ์ ์ด์ง. ๐
2. ์ปค๋ฎค๋ํฐ ๊ตฌ์ฑ์๋ค์ด ์์ ๋กญ๊ฒ ํ ๋ก ํ ์ ์์ด์ผ ํด
3. ํฌํ ๊ณผ์ ์ด ํฌ๋ช ํ๊ณ ๊ฒ์ฆ ๊ฐ๋ฅํด์ผ ํด
4. ๋ชจ๋ ์์ฌ๊ฒฐ์ ์ด๋ ฅ์ด ์๊ตฌ์ ์ผ๋ก ๋ณด์กด๋์ด์ผ ํด
5. ์ง๊ฐ ์ฐ๊ฒฐ๋ง์ผ๋ก ๊ฐํธํ๊ฒ ์ฐธ์ฌํ ์ ์์ด์ผ ํด
6. ์จ์ฒด์ธ๊ณผ ์คํ์ฒด์ธ ๋ฐ์ดํฐ๊ฐ ์กฐํ๋กญ๊ฒ ์ฐ๋๋์ด์ผ ํด
์ด๋ฐ ์๊ตฌ์ฌํญ์ ์ถฉ์กฑํ๋ ค๋ฉด ํ์ด๋ธ๋ฆฌ๋ ์ํคํ ์ฒ๊ฐ ํ์ํด. ์ค์ํ ํฌํ ๊ฒฐ๊ณผ์ ์ต์ข ์์ฌ๊ฒฐ์ ์ ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋กํ๊ณ , ๊ทธ ์ธ์ ๋ฐ์ดํฐ๋ ์ ํต์ ์ธ ์น ์๋ฒ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ก ๊ด๋ฆฌํ๋ ๊ฑฐ์ง. ๊ทธ๋ฆฌ๊ณ ์ด ๋ ์ธ๊ณ๋ฅผ ์ฐ๊ฒฐํ๋ ๋ค๋ฆฌ ์ญํ ์ ๋ผ๋ผ๋ฒจ์ด ์๋ฒฝํ๊ฒ ํด์ค ์ ์์ด! ๐
๐ ๏ธ ๋ผ๋ผ๋ฒจ์ ์ ํํ๋ ์ด์
๋ธ๋ก์ฒด์ธ ํ๋ก์ ํธ๋ผ๊ณ ํ๋ฉด ๋ณดํต Node.js๋ Python์ ๋ ์ฌ๋ฆฌ๋ ์ฌ๋๋ค์ด ๋ง์. ํ์ง๋ง ๋ผ๋ผ๋ฒจ์ DAO ํ๋ซํผ์ ๋ง๋ค๊ธฐ์ ์ ๋ง ์ต์ ์ ์ ํ์ด์ผ. ๊ทธ ์ด์ ๋ฅผ ํ๋์ฉ ์ดํด๋ณผ๊น? ๐ค
๋ผ๋ผ๋ฒจ์ Eloquent ORM์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ ์ ์ ๋ง ์ง๊ด์ ์ผ๋ก ๋ง๋ค์ด์ค. ์ ์์(Proposal) ๋ชจ๋ธ์ ๋ง๋ค๊ณ ๊ด๊ณ๋ฅผ ์ค์ ํ๋ ๊ฒ ๋ช ์ค์ ์ฝ๋๋ก ๋๋๊ฑฐ๋ . ๋ณต์กํ ์ฟผ๋ฆฌ๋ ๋ฉ์๋ ์ฒด์ด๋์ผ๋ก ๊น๋ํ๊ฒ ์์ฑํ ์ ์์ด.
// ์ ์์์ ํฌํ ๊ด๊ณ ์ค์ ์ด ์ด๋ ๊ฒ ๊ฐ๋จํด!
class Proposal extends Model {
public function votes() {
return $this->hasMany(Vote::class);
}
public function author() {
return $this->belongsTo(User::class);
}
}
// ํน์ ์ ์์์ ์ฐฌ์ฑ ํฌํ ์ ์กฐํ
$yesVotes = Proposal::find($id)->votes()->where('choice', 'yes')->count();
DAO ํ๋ซํผ์์๋ ์ง๊ฐ ์ฃผ์ ๊ธฐ๋ฐ ์ธ์ฆ์ด ํ์์ผ. ๋ผ๋ผ๋ฒจ์ ์ ์ฐํ ์ธ์ฆ ์์คํ ์ ํ์ฉํ๋ฉด MetaMask ๊ฐ์ ์ง๊ฐ ์ฐ๊ฒฐ์ ์ฝ๊ฒ ๊ตฌํํ ์ ์์ด. Laravel Sanctum์ด๋ Passport๋ฅผ ์ฌ์ฉํ๋ฉด API ํ ํฐ ๊ด๋ฆฌ๋ ๊ฐํธํ์ง.
์ผ๋ฐ์ ์ธ ์ด๋ฉ์ผ ๋ก๊ทธ์ธ๊ณผ ์ง๊ฐ ์ฐ๊ฒฐ ๋ก๊ทธ์ธ์ ๋์์ ์ง์ํ๋ ๊ฒ๋ ๊ฐ๋ฅํด. ์ฌ์ฉ์๊ฐ ์ ํํ ์ ์๊ฒ ํ๋ ๊ฑฐ์ผ! ๐
๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ์ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆด ์ ์์ด. ์ฌ์ฉ์๊ฐ ํฌํ ๋ฒํผ์ ๋๋ ์ ๋ ํธ๋์ญ์ ์ด ์๋ฃ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฌ๊ฒ ํ๋ฉด UX๊ฐ ์๋ง์ด ๋์ง. ๐ฐ
๋ผ๋ผ๋ฒจ์ ํ ์์คํ ์ ์ฌ์ฉํ๋ฉด ์ด๋ฐ ์์ ์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ฒ๋ฆฌํ ์ ์์ด. ์ฌ์ฉ์๋ ์ฆ์ ์๋ต์ ๋ฐ๊ณ , ์ค์ ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ก์ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌ๋๋ ๊ฑฐ์ผ. Redis๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ๋ฅผ ํ์ฉํ๋ฉด ์๋ฒฝํด!
ํ๋ก ํธ์๋๋ฅผ React๋ Vue๋ก ์์ ํ ๋ถ๋ฆฌํ ์๋ ์์ง๋ง, ๊ด๋ฆฌ์ ํ์ด์ง๋ ์ผ๋ถ ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง์ด ํ์ํ ํ์ด์ง๋ Blade๋ก ๋น ๋ฅด๊ฒ ๋ง๋ค ์ ์์ด. ์ปดํฌ๋ํธ ์ฌ์ฌ์ฉ๋ ์ฝ๊ณ , ์กฐ๊ฑด๋ฌธ์ด๋ ๋ฐ๋ณต๋ฌธ๋ ๊น๋ํ๊ฒ ์์ฑํ ์ ์์ง.
ํนํ SEO๊ฐ ์ค์ํ ์ ์์ ์์ธ ํ์ด์ง ๊ฐ์ ๊ฑด ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง์ด ์ ๋ฆฌํ๊ฑฐ๋ ! ๐
์๋ก์ด ์ ์์๊ฐ ๋ฑ๋ก๋๋ฉด ์๋ฆผ์ ๋ณด๋ด๊ณ , ํฌํ๊ฐ ์๋ฃ๋๋ฉด ๊ฒฐ๊ณผ๋ฅผ ์ง๊ณํ๊ณ , ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋กํ๋ ๋ฑ... ์ด๋ฐ ๋ณต์กํ ์ํฌํ๋ก์ฐ๋ฅผ ์ด๋ฒคํธ ๊ธฐ๋ฐ์ผ๋ก ๊น๋ํ๊ฒ ๊ตฌํํ ์ ์์ด.
// ์ ์์ ์์ฑ ์ด๋ฒคํธ
event(new ProposalCreated($proposal));
// ๋ฆฌ์ค๋์์ ์ฒ๋ฆฌ
class NotifyProposalCreated {
public function handle(ProposalCreated $event) {
// ์ปค๋ฎค๋ํฐ ๋ฉค๋ฒ๋ค์๊ฒ ์๋ฆผ ๋ฐ์ก
// ๋ธ๋ก์ฒด์ธ์ ํด์ ๊ธฐ๋ก
// ํต๊ณ ์
๋ฐ์ดํธ
}
}
๐๏ธ ์์คํ ์ํคํ ์ฒ ์ค๊ณํ๊ธฐ
๋ณธ๊ฒฉ์ ์ผ๋ก ์ฝ๋ฉ์ ๋ค์ด๊ฐ๊ธฐ ์ ์ ์ ์ฒด ์์คํ ๊ตฌ์กฐ๋ฅผ ์ค๊ณํด์ผ ํด. DAO ์์ฌ๊ฒฐ์ ํ๋ซํผ์ ํฌ๊ฒ 4๊ฐ์ ํต์ฌ ๋ชจ๋๋ก ๊ตฌ์ฑ๋ผ.
| ๋ชจ๋ | ์ฃผ์ ๊ธฐ๋ฅ | ์ ์ฅ ์์น |
|---|---|---|
| ์ฌ์ฉ์ ๊ด๋ฆฌ ๐ฅ | ์ง๊ฐ ์ฐ๊ฒฐ, ํ๋กํ ๊ด๋ฆฌ, ๊ถํ ์ค์ | MySQL (์คํ์ฒด์ธ) |
| ์ ์์ ์์คํ ๐ | ์ ์์ ์์ฑ/์์ , ์นดํ ๊ณ ๋ฆฌ ๋ถ๋ฅ, ๊ฒ์ | MySQL (์คํ์ฒด์ธ) |
| ํฌํ ์์คํ ๐ณ๏ธ | ํฌํ ์งํ, ๊ฒฐ๊ณผ ์ง๊ณ, ๊ถํ ๊ฒ์ฆ | ํ์ด๋ธ๋ฆฌ๋ (์จ/์คํ์ฒด์ธ) |
| ๊ฑฐ๋ฒ๋์ค ๊ธฐ๋ก ๐ | ์์ฌ๊ฒฐ์ ์ด๋ ฅ, ๊ฐ์ฌ ๋ก๊ทธ, ํต๊ณ | MySQL + IPFS + ๋ธ๋ก์ฒด์ธ |
IPFS: ์ ์์ ์๋ณธ ํ์ผ, ์ฒจ๋ถ ์๋ฃ, ๋์ฉ๋ ๋ฌธ์
๋ธ๋ก์ฒด์ธ: ํฌํ ๊ฒฐ๊ณผ, ์ต์ข ์์ฌ๊ฒฐ์ , ๊ฑฐ๋ฒ๋์ค ํ ํฐ ์ด๋ ๋ด์ญ
์ด๋ ๊ฒ 3๋จ๊ณ๋ก ๋๋๋ฉด ๋น์ฉ ํจ์จ์ ์ด๋ฉด์๋ ํฌ๋ช ์ฑ์ ๋ณด์ฅํ ์ ์์ด! ๐ฐ
๐ป ๋ผ๋ผ๋ฒจ ํ๋ก์ ํธ ์ด๊ธฐ ์ค์
์, ์ด์ ์ค์ ๋ก ํ๋ก์ ํธ๋ฅผ ์์ํด๋ณด์! ๋จผ์ ๋ผ๋ผ๋ฒจ์ ์ค์นํ๊ณ ๊ธฐ๋ณธ ์ค์ ์ ํด์ผ ํด. ๋๋ ๋ผ๋ผ๋ฒจ 10 ๋ฒ์ ์ ๊ธฐ์ค์ผ๋ก ์ค๋ช ํ ๊ฒ. ๐
composer create-project laravel/laravel dao-platform
cd dao-platform
php artisan serve
์ด์ http://localhost:8000์ ์ ์ํ๋ฉด ๋ผ๋ผ๋ฒจ ์ฐ์ปด ํ์ด์ง๊ฐ ๋ณด์ผ ๊ฑฐ์ผ!
DAO ํ๋ซํผ์ ํ์ํ ํจํค์ง๋ค์ ์ค์นํ์:
// Web3 PHP ๋ผ์ด๋ธ๋ฌ๋ฆฌ
composer require web3p/web3.php
// API ์ธ์ฆ (Sanctum)
composer require laravel/sanctum
// ๋งํฌ๋ค์ด ํ์
composer require league/commonmark
// ํ ์์
์ ์ํ Redis
composer require predis/predis
// ํ์ผ ์
๋ก๋ ์ฒ๋ฆฌ
composer require intervention/image
// IPFS ํด๋ผ์ด์ธํธ
composer require rize/uri-template
.env ํ์ผ์ ์ด์ด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ธ๋ก์ฒด์ธ ๊ด๋ จ ์ค์ ์ ์ถ๊ฐํด:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=dao_platform
DB_USERNAME=root
DB_PASSWORD=
# ๋ธ๋ก์ฒด์ธ ๋คํธ์ํฌ ์ค์
BLOCKCHAIN_NETWORK=polygon
BLOCKCHAIN_RPC_URL=https://polygon-rpc.com
CONTRACT_ADDRESS=0x...
# IPFS ์ค์
IPFS_HOST=ipfs.infura.io
IPFS_PORT=5001
IPFS_PROTOCOL=https
# ํ ์ค์
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
๐๏ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ์ค๊ณ
์ด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ตฌ์กฐ๋ฅผ ์ค๊ณํ ์ฐจ๋ก์ผ. DAO ํ๋ซํผ์ ํต์ฌ ํ ์ด๋ธ๋ค์ ๋ง๋ค์ด๋ณด์. ๋ง์ด๊ทธ๋ ์ด์ ํ์ผ์ ์์ฑํ๊ณ ์คํค๋ง๋ฅผ ์ ์ํ ๊ฑฐ์ผ! ๐
php artisan make:migration create_users_table
// database/migrations/xxxx_create_users_table.php
public function up() {
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('wallet_address')->unique();
$table->string('username')->nullable();
$table->string('email')->nullable();
$table->text('bio')->nullable();
$table->string('avatar_url')->nullable();
$table->decimal('token_balance', 20, 8)->default(0);
$table->boolean('is_verified')->default(false);
$table->timestamp('last_login_at')->nullable();
$table->timestamps();
$table->index('wallet_address');
});
}
์ง๊ฐ ์ฃผ์๋ฅผ ๊ธฐ๋ณธ ์๋ณ์๋ก ์ฌ์ฉํ๊ณ , ์ด๋ฉ์ผ์ ์ ํ์ฌํญ์ผ๋ก ํด. ํ ํฐ ์์ก๋ ์ ์ฅํด์ ํฌํ ๊ถํ์ ๋น ๋ฅด๊ฒ ํ์ธํ ์ ์๊ฒ ํ์! ๐ฐ
php artisan make:migration create_proposals_table
public function up() {
Schema::create('proposals', function (Blueprint $table) {
$table->id();
$table->foreignId('author_id')->constrained('users');
$table->string('title');
$table->text('description');
$table->longText('content'); // ๋งํฌ๋ค์ด ํ์
$table->string('category');
$table->enum('status', ['draft', 'active', 'passed', 'rejected', 'executed']);
$table->string('ipfs_hash')->nullable(); // IPFS์ ์ ์ฅ๋ ์๋ณธ ํด์
$table->string('blockchain_tx_hash')->nullable(); // ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์
ํด์
$table->timestamp('voting_starts_at')->nullable();
$table->timestamp('voting_ends_at')->nullable();
$table->integer('quorum_required')->default(0); // ์ต์ ํฌํ ์ฐธ์ฌ ์
$table->decimal('approval_threshold', 5, 2)->default(50.00); // ํต๊ณผ ๊ธฐ์ค (%)
$table->timestamps();
$table->softDeletes();
$table->index(['status', 'voting_ends_at']);
$table->fulltext(['title', 'description']);
});
}
์ ์์์ ์์ ์ฃผ๊ธฐ๋ฅผ status๋ก ๊ด๋ฆฌํ๊ณ , ํฌํ ๊ธฐ๊ฐ๊ณผ ํต๊ณผ ๊ธฐ์ค์ ์ค์ ํ ์ ์๊ฒ ํ์ด. IPFS ํด์์ ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ํด์๋ ์ ์ฅํ์ง! ๐
php artisan make:migration create_votes_table
public function up() {
Schema::create('votes', function (Blueprint $table) {
$table->id();
$table->foreignId('proposal_id')->constrained();
$table->foreignId('user_id')->constrained();
$table->enum('choice', ['yes', 'no', 'abstain']);
$table->decimal('voting_power', 20, 8); // ํฌํ ์์ ์ ํ ํฐ ๋ณด์ ๋
$table->text('reason')->nullable(); // ํฌํ ์ด์ (์ ํ)
$table->string('blockchain_tx_hash')->nullable();
$table->timestamp('voted_at');
$table->timestamps();
// ํ ์ฌ์ฉ์๊ฐ ๊ฐ์ ์ ์์์ ์ค๋ณต ํฌํ ๋ฐฉ์ง
$table->unique(['proposal_id', 'user_id']);
$table->index('blockchain_tx_hash');
});
}
ํฌํ ์์ ์ ํ ํฐ ๋ณด์ ๋์ ๊ธฐ๋กํ๋ ๊ฒ ์ค์ํด. ๋์ค์ ํ ํฐ์ ๋ ์ฌ๊ฑฐ๋ ํ์๋ ํฌํ ๊ฒฐ๊ณผ๋ ๋ณํ์ง ์์์ผ ํ๋๊น! ๐ฏ
php artisan make:migration create_comments_table
public function up() {
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('proposal_id')->constrained();
$table->foreignId('user_id')->constrained();
$table->foreignId('parent_id')->nullable()->constrained('comments'); // ๋๋๊ธ
$table->text('content');
$table->integer('likes_count')->default(0);
$table->timestamps();
$table->softDeletes();
$table->index(['proposal_id', 'created_at']);
});
}
๋๋๊ธ ๊ธฐ๋ฅ์ ์ํด parent_id๋ฅผ ์ถ๊ฐํ์ด. ์ ์์์ ๋ํ ํ๋ฐํ ํ ๋ก ์ด ๊ฐ๋ฅํ๋๋ก! ๐ญ
php artisan make:migration create_proposal_statistics_table
public function up() {
Schema::create('proposal_statistics', function (Blueprint $table) {
$table->id();
$table->foreignId('proposal_id')->constrained();
$table->integer('total_votes')->default(0);
$table->decimal('yes_votes', 20, 8)->default(0);
$table->decimal('no_votes', 20, 8)->default(0);
$table->decimal('abstain_votes', 20, 8)->default(0);
$table->decimal('participation_rate', 5, 2)->default(0);
$table->integer('comments_count')->default(0);
$table->integer('views_count')->default(0);
$table->timestamps();
$table->unique('proposal_id');
});
}
ํต๊ณ๋ฅผ ๋ณ๋ ํ ์ด๋ธ๋ก ๋ถ๋ฆฌํ๋ฉด ์ง๊ณ ์ฟผ๋ฆฌ ์ฑ๋ฅ์ด ์ข์์ ธ. ์ค์๊ฐ์ผ๋ก ํฌํ ํํฉ์ ๋ณด์ฌ์ค ๋ ์ ์ฉํ์ง! ๐
๋ง์ด๊ทธ๋ ์ด์ ์ ์คํํด์ ํ ์ด๋ธ์ ์์ฑํ์:
php artisan migrate
๐จ Eloquent ๋ชจ๋ธ ๊ตฌ์ถํ๊ธฐ
ํ ์ด๋ธ์ ๋ง๋ค์์ผ๋ ์ด์ Eloquent ๋ชจ๋ธ์ ์ ์ํ ์ฐจ๋ก์ผ. ๋ชจ๋ธ์์ ๊ด๊ณ(Relationship)๋ฅผ ์ค์ ํ๊ณ ๋น์ฆ๋์ค ๋ก์ง์ ์ถ๊ฐํ๋ฉด ์ฝ๋๊ฐ ํจ์ฌ ๊น๋ํด์ ธ! ๐ฏ
php artisan make:model Proposal
// app/Models/Proposal.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Proposal extends Model
{
use SoftDeletes;
protected $fillable = [
'author_id', 'title', 'description', 'content',
'category', 'status', 'ipfs_hash', 'blockchain_tx_hash',
'voting_starts_at', 'voting_ends_at',
'quorum_required', 'approval_threshold'
];
protected $casts = [
'voting_starts_at' => 'datetime',
'voting_ends_at' => 'datetime',
'approval_threshold' => 'decimal:2'
];
// ๊ด๊ณ ์ ์
public function author() {
return $this->belongsTo(User::class, 'author_id');
}
public function votes() {
return $this->hasMany(Vote::class);
}
public function comments() {
return $this->hasMany(Comment::class);
}
public function statistics() {
return $this->hasOne(ProposalStatistic::class);
}
// ๋น์ฆ๋์ค ๋ก์ง
public function isActive() {
return $this->status === 'active'
&& now()->between($this->voting_starts_at, $this->voting_ends_at);
}
public function canVote(User $user) {
// ์ด๋ฏธ ํฌํํ๋์ง ํ์ธ
if ($this->votes()->where('user_id', $user->id)->exists()) {
return false;
}
// ํฌํ ๊ธฐ๊ฐ์ธ์ง ํ์ธ
if (!$this->isActive()) {
return false;
}
// ํ ํฐ ๋ณด์ ๋ ํ์ธ
return $user->token_balance > 0;
}
public function calculateResult() {
$stats = $this->statistics;
$totalVotingPower = $stats->yes_votes + $stats->no_votes + $stats->abstain_votes;
if ($totalVotingPower == 0) {
return 'no_votes';
}
$yesPercentage = ($stats->yes_votes / $totalVotingPower) * 100;
// ์ ์กฑ์ ์ถฉ์กฑ ์ฌ๋ถ
if ($stats->total_votes < $this->quorum_required) {
return 'quorum_not_met';
}
// ํต๊ณผ ๊ธฐ์ค ์ถฉ์กฑ ์ฌ๋ถ
return $yesPercentage >= $this->approval_threshold ? 'passed' : 'rejected';
}
}
๋ชจ๋ธ์ ๋น์ฆ๋์ค ๋ก์ง์ ๋ฃ์ผ๋ฉด ์ปจํธ๋กค๋ฌ๊ฐ ๊น๋ํด์ ธ. canVote() ๋ฉ์๋๋ก ํฌํ ๊ฐ๋ฅ ์ฌ๋ถ๋ฅผ ์ฝ๊ฒ ํ์ธํ ์ ์๊ณ , calculateResult()๋ก ํฌํ ๊ฒฐ๊ณผ๋ฅผ ๊ณ์ฐํ ์ ์์ด! ๐ฒ
php artisan make:model Vote
// app/Models/Vote.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Vote extends Model
{
protected $fillable = [
'proposal_id', 'user_id', 'choice',
'voting_power', 'reason', 'blockchain_tx_hash', 'voted_at'
];
protected $casts = [
'voting_power' => 'decimal:8',
'voted_at' => 'datetime'
];
public function proposal() {
return $this->belongsTo(Proposal::class);
}
public function user() {
return $this->belongsTo(User::class);
}
// ํฌํ ํ ํต๊ณ ์
๋ฐ์ดํธ
protected static function booted() {
static::created(function ($vote) {
$vote->updateProposalStatistics();
});
}
public function updateProposalStatistics() {
$stats = $this->proposal->statistics;
$stats->total_votes += 1;
switch ($this->choice) {
case 'yes':
$stats->yes_votes += $this->voting_power;
break;
case 'no':
$stats->no_votes += $this->voting_power;
break;
case 'abstain':
$stats->abstain_votes += $this->voting_power;
break;
}
// ์ฐธ์ฌ์จ ๊ณ์ฐ (์ ์ฒด ํ ํฐ ๊ณต๊ธ๋ ๋๋น)
$totalSupply = User::sum('token_balance');
$stats->participation_rate = ($stats->yes_votes + $stats->no_votes + $stats->abstain_votes) / $totalSupply * 100;
$stats->save();
}
}
Vote ๋ชจ๋ธ์ด ์์ฑ๋ ๋ ์๋์ผ๋ก ํต๊ณ๋ฅผ ์ ๋ฐ์ดํธํ๋๋ก ์ด๋ฒคํธ๋ฅผ ์ฐ๊ฒฐํ์ด. ์ด๋ ๊ฒ ํ๋ฉด ์ค์๊ฐ ์ง๊ณ๊ฐ ๊ฐ๋ฅํ์ง! โก
๐ ์ง๊ฐ ์ฐ๊ฒฐ ์ธ์ฆ ์์คํ ๊ตฌ์ถ
DAO ํ๋ซํผ์ ํต์ฌ์ ๋ฐ๋ก ์ง๊ฐ ๊ธฐ๋ฐ ์ธ์ฆ์ด์ผ. MetaMask ๊ฐ์ ์ง๊ฐ์ผ๋ก ๋ก๊ทธ์ธํ๊ณ , ์๋ช ์ ํตํด ์ ์์ ์ฆ๋ช ํ๋ ์์คํ ์ ๋ง๋ค์ด๋ณด์! ๐
1. ์ฌ์ฉ์๊ฐ "์ง๊ฐ ์ฐ๊ฒฐ" ๋ฒํผ ํด๋ฆญ
2. MetaMask ํ์ ์ด ๋จ๊ณ ์ง๊ฐ ์ฃผ์ ์ ๊ทผ ๊ถํ ์์ฒญ
3. ์๋ฒ์์ ๋๋ค nonce(์ผํ์ฉ ์ซ์) ์์ฑ
4. ์ฌ์ฉ์๊ฐ nonce๋ฅผ ํฌํจํ ๋ฉ์์ง์ ์๋ช
5. ์๋ฒ์์ ์๋ช ๊ฒ์ฆ ํ JWT ํ ํฐ ๋ฐ๊ธ
6. ์ดํ ์์ฒญ์ JWT ํ ํฐ ํฌํจํ์ฌ ์ธ์ฆ
์ด ๋ฐฉ์์ ๋น๋ฐ๋ฒํธ ์์ด๋ ์์ ํ๊ฒ ์ธ์ฆํ ์ ์์ด! ๐ก๏ธ
// resources/js/wallet-auth.js
async function connectWallet() {
if (typeof window.ethereum === 'undefined') {
alert('MetaMask๋ฅผ ์ค์นํด์ฃผ์ธ์!');
return;
}
try {
// ์ง๊ฐ ์ฐ๊ฒฐ ์์ฒญ
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
const walletAddress = accounts[0];
// ์๋ฒ์์ nonce ๋ฐ์์ค๊ธฐ
const nonceResponse = await fetch('/api/auth/nonce', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ wallet_address: walletAddress })
});
const { nonce } = await nonceResponse.json();
// ๋ฉ์์ง ์๋ช
const message = `DAO ํ๋ซํผ ๋ก๊ทธ์ธ\n\nNonce: ${nonce}\n์ฃผ์: ${walletAddress}`;
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [message, walletAddress]
});
// ์๋ช
๊ฒ์ฆ ๋ฐ ํ ํฐ ๋ฐ๊ธฐ
const authResponse = await fetch('/api/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
wallet_address: walletAddress,
signature: signature,
nonce: nonce
})
});
const { token, user } = await authResponse.json();
// ํ ํฐ ์ ์ฅ
localStorage.setItem('auth_token', token);
localStorage.setItem('wallet_address', walletAddress);
console.log('๋ก๊ทธ์ธ ์ฑ๊ณต!', user);
window.location.href = '/dashboard';
} catch (error) {
console.error('์ง๊ฐ ์ฐ๊ฒฐ ์คํจ:', error);
alert('์ง๊ฐ ์ฐ๊ฒฐ์ ์คํจํ์ต๋๋ค.');
}
}
php artisan make:controller Api/AuthController
// app/Http/Controllers/Api/AuthController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Cache;
use Web3\Web3;
use Web3\Utils;
class AuthController extends Controller
{
// Nonce ์์ฑ
public function getNonce(Request $request) {
$request->validate([
'wallet_address' => 'required|string'
]);
$walletAddress = strtolower($request->wallet_address);
$nonce = Str::random(32);
// Nonce๋ฅผ 5๋ถ๊ฐ ์บ์์ ์ ์ฅ
Cache::put("nonce:{$walletAddress}", $nonce, 300);
return response()->json(['nonce' => $nonce]);
}
// ์๋ช
๊ฒ์ฆ ๋ฐ ๋ก๊ทธ์ธ
public function verifySignature(Request $request) {
$request->validate([
'wallet_address' => 'required|string',
'signature' => 'required|string',
'nonce' => 'required|string'
]);
$walletAddress = strtolower($request->wallet_address);
$signature = $request->signature;
$nonce = $request->nonce;
// ์บ์๋ nonce ํ์ธ
$cachedNonce = Cache::get("nonce:{$walletAddress}");
if ($cachedNonce !== $nonce) {
return response()->json(['error' => '์ ํจํ์ง ์์ nonce์
๋๋ค.'], 401);
}
// ์๋ช
๊ฒ์ฆ
$message = "DAO ํ๋ซํผ ๋ก๊ทธ์ธ\n\nNonce: {$nonce}\n์ฃผ์: {$walletAddress}";
$recoveredAddress = $this->recoverAddress($message, $signature);
if (strtolower($recoveredAddress) !== $walletAddress) {
return response()->json(['error' => '์๋ช
๊ฒ์ฆ ์คํจ'], 401);
}
// Nonce ์ญ์ (์ฌ์ฌ์ฉ ๋ฐฉ์ง)
Cache::forget("nonce:{$walletAddress}");
// ์ฌ์ฉ์ ์กฐํ ๋๋ ์์ฑ
$user = User::firstOrCreate(
['wallet_address' => $walletAddress],
['username' => 'User_' . substr($walletAddress, 0, 8)]
);
// ๋ง์ง๋ง ๋ก๊ทธ์ธ ์๊ฐ ์
๋ฐ์ดํธ
$user->update(['last_login_at' => now()]);
// JWT ํ ํฐ ์์ฑ (Laravel Sanctum ์ฌ์ฉ)
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'token' => $token,
'user' => $user
]);
}
// ์๋ช
์์ ์ฃผ์ ๋ณต๊ตฌ
private function recoverAddress($message, $signature) {
$web3 = new Web3('http://localhost:8545');
$utils = new Utils();
// ์ด๋๋ฆฌ์ ์๋ช
๋ฉ์์ง ํ์
$hash = $utils->sha3("\x19Ethereum Signed Message:\n" . strlen($message) . $message);
// ์๋ช
์์ r, s, v ์ถ์ถ
$r = substr($signature, 0, 66);
$s = '0x' . substr($signature, 66, 64);
$v = hexdec(substr($signature, 130, 2));
// ์ฃผ์ ๋ณต๊ตฌ (์ค์ ๋ก๋ web3.php ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ)
// ์ฌ๊ธฐ์๋ ๊ฐ๋จํ ํํ
return $this->ecRecover($hash, $v, $r, $s);
}
// ๋ก๊ทธ์์
public function logout(Request $request) {
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => '๋ก๊ทธ์์ ๋์์ต๋๋ค.']);
}
}
์๋ช ๊ฒ์ฆ ๋ก์ง์ด ํต์ฌ์ด์ผ. ์ฌ์ฉ์๊ฐ ๋ณด๋ธ ์๋ช ์์ ์ฃผ์๋ฅผ ๋ณต๊ตฌํด์, ์๋ ์ฃผ์์ ์ผ์นํ๋์ง ํ์ธํ๋ ๊ฑฐ์ง. ์ด๋ ๊ฒ ํ๋ฉด ๋น๋ฐ๋ฒํธ ์์ด๋ ์์ ํ๊ฒ ์ธ์ฆํ ์ ์์ด! ๐
๐ ์ ์์ ์์ฑ ๋ฐ ๊ด๋ฆฌ ์์คํ
์ด์ DAO์ ํต์ฌ ๊ธฐ๋ฅ์ธ ์ ์์ ์์คํ ์ ๋ง๋ค์ด๋ณด์. ์ฌ์ฉ์๊ฐ ์ ์์๋ฅผ ์์ฑํ๊ณ , ์์ ํ๊ณ , ์ ์ถํ๋ ์ ์ฒด ํ๋ก์ธ์ค๋ฅผ ๊ตฌํํ ๊ฑฐ์ผ! โ๏ธ
php artisan make:controller ProposalController
// app/Http/Controllers/ProposalController.php
namespace App\Http\Controllers;
use App\Models\Proposal;
use App\Models\ProposalStatistic;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use League\CommonMark\CommonMarkConverter;
class ProposalController extends Controller
{
// ์ ์์ ๋ชฉ๋ก
public function index(Request $request) {
$query = Proposal::with(['author', 'statistics'])
->where('status', '!=', 'draft');
// ์นดํ
๊ณ ๋ฆฌ ํํฐ
if ($request->has('category')) {
$query->where('category', $request->category);
}
// ์ํ ํํฐ
if ($request->has('status')) {
$query->where('status', $request->status);
}
// ๊ฒ์
if ($request->has('search')) {
$query->where(function($q) use ($request) {
$q->where('title', 'like', "%{$request->search}%")
->orWhere('description', 'like', "%{$request->search}%");
});
}
// ์ ๋ ฌ
$sortBy = $request->get('sort', 'created_at');
$sortOrder = $request->get('order', 'desc');
$query->orderBy($sortBy, $sortOrder);
$proposals = $query->paginate(20);
return view('proposals.index', compact('proposals'));
}
// ์ ์์ ์์ธ
public function show($id) {
$proposal = Proposal::with(['author', 'votes.user', 'comments.user', 'statistics'])
->findOrFail($id);
// ์กฐํ์ ์ฆ๊ฐ
$proposal->statistics->increment('views_count');
// ๋งํฌ๋ค์ด์ HTML๋ก ๋ณํ
$converter = new CommonMarkConverter();
$proposal->html_content = $converter->convert($proposal->content);
// ํ์ฌ ์ฌ์ฉ์๊ฐ ํฌํํ๋์ง ํ์ธ
$userVote = null;
if (auth()->check()) {
$userVote = $proposal->votes()
->where('user_id', auth()->id())
->first();
}
return view('proposals.show', compact('proposal', 'userVote'));
}
// ์ ์์ ์์ฑ ํผ
public function create() {
$categories = [
'governance' => '๊ฑฐ๋ฒ๋์ค',
'treasury' => '์ฌ๋ฌด',
'technical' => '๊ธฐ์ ',
'community' => '์ปค๋ฎค๋ํฐ',
'partnership' => 'ํํธ๋์ญ'
];
return view('proposals.create', compact('categories'));
}
// ์ ์์ ์ ์ฅ
public function store(Request $request) {
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'required|string|max:500',
'content' => 'required|string',
'category' => 'required|string',
'voting_duration' => 'required|integer|min:1|max:30', // ์ผ ๋จ์
'quorum_required' => 'required|integer|min:1',
'approval_threshold' => 'required|numeric|min:50|max:100'
]);
// ์ ์์ ์์ฑ
$proposal = Proposal::create([
'author_id' => auth()->id(),
'title' => $validated['title'],
'description' => $validated['description'],
'content' => $validated['content'],
'category' => $validated['category'],
'status' => 'draft',
'quorum_required' => $validated['quorum_required'],
'approval_threshold' => $validated['approval_threshold']
]);
// ํต๊ณ ๋ ์ฝ๋ ์์ฑ
ProposalStatistic::create([
'proposal_id' => $proposal->id
]);
// IPFS์ ์
๋ก๋ (๋น๋๊ธฐ ์ฒ๋ฆฌ)
dispatch(new \App\Jobs\UploadToIpfs($proposal));
return redirect()
->route('proposals.show', $proposal->id)
->with('success', '์ ์์ ์ด์์ด ์ ์ฅ๋์์ต๋๋ค.');
}
// ์ ์์ ์ ์ถ (ํฌํ ์์)
public function submit($id) {
$proposal = Proposal::findOrFail($id);
// ์์ฑ์๋ง ์ ์ถ ๊ฐ๋ฅ
if ($proposal->author_id !== auth()->id()) {
abort(403, '๊ถํ์ด ์์ต๋๋ค.');
}
// ์ด๋ฏธ ์ ์ถ๋ ์ ์์๋ ์์ ๋ถ๊ฐ
if ($proposal->status !== 'draft') {
return back()->with('error', '์ด๋ฏธ ์ ์ถ๋ ์ ์์์
๋๋ค.');
}
// ํฌํ ๊ธฐ๊ฐ ์ค์
$votingDuration = request('voting_duration', 7); // ๊ธฐ๋ณธ 7์ผ
$proposal->update([
'status' => 'active',
'voting_starts_at' => now(),
'voting_ends_at' => now()->addDays($votingDuration)
]);
// ๋ธ๋ก์ฒด์ธ์ ์ ์์ ํด์ ๊ธฐ๋ก (๋น๋๊ธฐ)
dispatch(new \App\Jobs\RecordProposalOnChain($proposal));
// ์ปค๋ฎค๋ํฐ ๋ฉค๋ฒ๋ค์๊ฒ ์๋ฆผ
dispatch(new \App\Jobs\NotifyNewProposal($proposal));
return redirect()
->route('proposals.show', $proposal->id)
->with('success', '์ ์์๊ฐ ์ ์ถ๋์์ต๋๋ค. ํฌํ๊ฐ ์์๋ฉ๋๋ค!');
}
}
์ ์์ ์๋ณธ์ IPFS์ ์ ์ฅํ๋ ๋น๋๊ธฐ ์์ ์ ๋ง๋ค์:
php artisan make:job UploadToIpfs
// app/Jobs/UploadToIpfs.php
namespace App\Jobs;
use App\Models\Proposal;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use GuzzleHttp\Client;
class UploadToIpfs implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $proposal;
public function __construct(Proposal $proposal) {
$this->proposal = $proposal;
}
public function handle() {
// ์ ์์ ๋ฐ์ดํฐ๋ฅผ JSON์ผ๋ก ๋ณํ
$data = [
'title' => $this->proposal->title,
'description' => $this->proposal->description,
'content' => $this->proposal->content,
'author' => $this->proposal->author->wallet_address,
'category' => $this->proposal->category,
'created_at' => $this->proposal->created_at->toIso8601String()
];
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
// IPFS์ ์
๋ก๋
$client = new Client();
$response = $client->post(config('services.ipfs.url') . '/api/v0/add', [
'multipart' => [
[
'name' => 'file',
'contents' => $json,
'filename' => "proposal_{$this->proposal->id}.json"
]
]
]);
$result = json_decode($response->getBody(), true);
$ipfsHash = $result['Hash'];
// IPFS ํด์ ์ ์ฅ
$this->proposal->update(['ipfs_hash' => $ipfsHash]);
\Log::info("Proposal {$this->proposal->id} uploaded to IPFS: {$ipfsHash}");
}
}
IPFS์ ์ ์ฅํ๋ฉด ์ ์์๊ฐ ์๊ตฌ์ ์ผ๋ก ๋ณด์กด๋๊ณ , ๋๊ตฌ๋ ๊ฒ์ฆํ ์ ์์ด. ํ์ค์ํ์ ํต์ฌ์ด์ง! ๐
๐ณ๏ธ ํฌํ ์์คํ ๊ตฌํํ๊ธฐ
์ด์ ๊ฐ์ฅ ์ค์ํ ํฌํ ์์คํ ์ ๋ง๋ค ์ฐจ๋ก์ผ! ์ฌ์ฉ์๊ฐ ์ ์์์ ํฌํํ๊ณ , ๊ทธ ๊ฒฐ๊ณผ๊ฐ ์ค์๊ฐ์ผ๋ก ์ง๊ณ๋๋ฉฐ, ์ต์ข ์ ์ผ๋ก ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋ก๋๋ ์ ์ฒด ํ๋ก์ธ์ค๋ฅผ ๊ตฌํํด๋ณด์. ๐ฏ
php artisan make:controller VoteController
// app/Http/Controllers/VoteController.php
namespace App\Http\Controllers;
use App\Models\Proposal;
use App\Models\Vote;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class VoteController extends Controller
{
// ํฌํํ๊ธฐ
public function store(Request $request, $proposalId) {
$validated = $request->validate([
'choice' => 'required|in:yes,no,abstain',
'reason' => 'nullable|string|max:1000'
]);
$proposal = Proposal::findOrFail($proposalId);
$user = auth()->user();
// ํฌํ ๊ฐ๋ฅ ์ฌ๋ถ ํ์ธ
if (!$proposal->canVote($user)) {
return back()->with('error', 'ํฌํํ ์ ์์ต๋๋ค.');
}
// ํธ๋์ญ์
์ผ๋ก ํฌํ ์ฒ๋ฆฌ
DB::beginTransaction();
try {
// ํฌํ ๊ธฐ๋ก ์์ฑ
$vote = Vote::create([
'proposal_id' => $proposal->id,
'user_id' => $user->id,
'choice' => $validated['choice'],
'voting_power' => $user->token_balance,
'reason' => $validated['reason'] ?? null,
'voted_at' => now()
]);
// ๋ธ๋ก์ฒด์ธ์ ํฌํ ๊ธฐ๋ก (๋น๋๊ธฐ)
dispatch(new \App\Jobs\RecordVoteOnChain($vote));
DB::commit();
return back()->with('success', 'ํฌํ๊ฐ ์๋ฃ๋์์ต๋๋ค!');
} catch (\Exception $e) {
DB::rollBack();
\Log::error('Vote failed: ' . $e->getMessage());
return back()->with('error', 'ํฌํ ์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
}
}
// ํฌํ ์ทจ์ (ํฌํ ๊ธฐ๊ฐ ์ค์๋ง ๊ฐ๋ฅ)
public function destroy($proposalId) {
$proposal = Proposal::findOrFail($proposalId);
$user = auth()->user();
// ํฌํ ๊ธฐ๊ฐ์ด ์๋๋ฉด ์ทจ์ ๋ถ๊ฐ
if (!$proposal->isActive()) {
return back()->with('error', 'ํฌํ ๊ธฐ๊ฐ์ด ์๋๋๋ค.');
}
$vote = Vote::where('proposal_id', $proposalId)
->where('user_id', $user->id)
->first();
if (!$vote) {
return back()->with('error', 'ํฌํ ๊ธฐ๋ก์ด ์์ต๋๋ค.');
}
DB::beginTransaction();
try {
// ํต๊ณ ์
๋ฐ์ดํธ (ํฌํ ์ทจ์ ๋ฐ์)
$stats = $proposal->statistics;
$stats->total_votes -= 1;
switch ($vote->choice) {
case 'yes':
$stats->yes_votes -= $vote->voting_power;
break;
case 'no':
$stats->no_votes -= $vote->voting_power;
break;
case 'abstain':
$stats->abstain_votes -= $vote->voting_power;
break;
}
$stats->save();
// ํฌํ ์ญ์
$vote->delete();
DB::commit();
return back()->with('success', 'ํฌํ๊ฐ ์ทจ์๋์์ต๋๋ค.');
} catch (\Exception $e) {
DB::rollBack();
return back()->with('error', 'ํฌํ ์ทจ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
}
}
// ํฌํ ๊ฒฐ๊ณผ ์กฐํ
public function results($proposalId) {
$proposal = Proposal::with(['statistics', 'votes.user'])
->findOrFail($proposalId);
// ํฌํ๊ฐ ์ข
๋ฃ๋์ง ์์์ผ๋ฉด ์ ๊ทผ ๋ถ๊ฐ
if ($proposal->isActive()) {
return back()->with('error', 'ํฌํ๊ฐ ์์ง ์งํ ์ค์
๋๋ค.');
}
$stats = $proposal->statistics;
// ํฌํ ๊ฒฐ๊ณผ ๊ณ์ฐ
$totalVotingPower = $stats->yes_votes + $stats->no_votes + $stats->abstain_votes;
$results = [
'yes' => [
'count' => $proposal->votes()->where('choice', 'yes')->count(),
'power' => $stats->yes_votes,
'percentage' => $totalVotingPower > 0 ? ($stats->yes_votes / $totalVotingPower * 100) : 0
],
'no' => [
'count' => $proposal->votes()->where('choice', 'no')->count(),
'power' => $stats->no_votes,
'percentage' => $totalVotingPower > 0 ? ($stats->no_votes / $totalVotingPower * 100) : 0
],
'abstain' => [
'count' => $proposal->votes()->where('choice', 'abstain')->count(),
'power' => $stats->abstain_votes,
'percentage' => $totalVotingPower > 0 ? ($stats->abstain_votes / $totalVotingPower * 100) : 0
]
];
// ์ต์ข
๊ฒฐ๊ณผ
$finalResult = $proposal->calculateResult();
return view('proposals.results', compact('proposal', 'results', 'finalResult'));
}
}
php artisan make:job RecordVoteOnChain
// app/Jobs/RecordVoteOnChain.php
namespace App\Jobs;
use App\Models\Vote;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Web3\Web3;
use Web3\Contract;
class RecordVoteOnChain implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $vote;
public function __construct(Vote $vote) {
$this->vote = $vote;
}
public function handle() {
try {
$web3 = new Web3(config('blockchain.rpc_url'));
// ์ค๋งํธ ์ปจํธ๋ํธ ABI ๋ก๋
$abi = json_decode(file_get_contents(storage_path('contracts/DAOGovernance.json')), true);
$contract = new Contract($web3->provider, $abi);
$contractAddress = config('blockchain.contract_address');
// ํฌํ ๋ฐ์ดํฐ ์ค๋น
$proposalId = $this->vote->proposal_id;
$voter = $this->vote->user->wallet_address;
$choice = $this->vote->choice === 'yes' ? 1 : ($this->vote->choice === 'no' ? 2 : 0);
$votingPower = $this->vote->voting_power * 1e18; // Wei ๋จ์๋ก ๋ณํ
// ํธ๋์ญ์
์ ์ก
$contract->at($contractAddress)->send('recordVote',
$proposalId,
$voter,
$choice,
$votingPower,
[
'from' => config('blockchain.admin_address'),
'gas' => '200000'
],
function ($err, $txHash) {
if ($err !== null) {
\Log::error('Blockchain vote recording failed: ' . $err->getMessage());
return;
}
// ํธ๋์ญ์
ํด์ ์ ์ฅ
$this->vote->update(['blockchain_tx_hash' => $txHash]);
\Log::info("Vote recorded on blockchain: {$txHash}");
}
);
} catch (\Exception $e) {
\Log::error('RecordVoteOnChain failed: ' . $e->getMessage());
// ์ฌ์๋ (์ต๋ 3๋ฒ)
if ($this->attempts() < 3) {
$this->release(60); // 1๋ถ ํ ์ฌ์๋
}
}
}
}
๋ธ๋ก์ฒด์ธ ๊ธฐ๋ก์ ์๊ฐ์ด ๊ฑธ๋ฆฌ๊ณ ์คํจํ ์๋ ์์ผ๋๊น ํ๋ก ๋น๋๊ธฐ ์ฒ๋ฆฌํ๋ ๊ฒ ์ค์ํด. ์ฌ์ฉ์๋ ์ฆ์ ์๋ต์ ๋ฐ๊ณ , ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ก์ด ์งํ๋๋ ๊ฑฐ์ง! โก
๐ ์ค์๊ฐ ํฌํ ํํฉ ๋์๋ณด๋
ํฌํ๊ฐ ์งํ๋๋ ๋์ ์ค์๊ฐ์ผ๋ก ํํฉ์ ๋ณด์ฌ์ฃผ๋ ๋์๋ณด๋๋ฅผ ๋ง๋ค์ด๋ณด์. Laravel Echo์ Pusher๋ฅผ ์ฌ์ฉํ๋ฉด WebSocket์ผ๋ก ์ค์๊ฐ ์ ๋ฐ์ดํธ๊ฐ ๊ฐ๋ฅํด! ๐ด
// Pusher ํจํค์ง ์ค์น
composer require pusher/pusher-php-server
// .env ์ค์
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=your_app_id
PUSHER_APP_KEY=your_app_key
PUSHER_APP_SECRET=your_app_secret
PUSHER_APP_CLUSTER=ap3
// config/broadcasting.php ํ์ธ
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'encrypted' => true,
],
],
php artisan make:event VoteCasted
// app/Events/VoteCasted.php
namespace App\Events;
use App\Models\Vote;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class VoteCasted implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $vote;
public $proposalId;
public function __construct(Vote $vote) {
$this->vote = $vote;
$this->proposalId = $vote->proposal_id;
}
public function broadcastOn() {
return new Channel('proposal.' . $this->proposalId);
}
public function broadcastWith() {
$proposal = $this->vote->proposal;
$stats = $proposal->statistics;
return [
'vote_id' => $this->vote->id,
'choice' => $this->vote->choice,
'voter' => $this->vote->user->username,
'voting_power' => $this->vote->voting_power,
'statistics' => [
'total_votes' => $stats->total_votes,
'yes_votes' => $stats->yes_votes,
'no_votes' => $stats->no_votes,
'abstain_votes' => $stats->abstain_votes,
'participation_rate' => $stats->participation_rate
]
];
}
}
ํฌํ๊ฐ ๋ฐ์ํ ๋๋ง๋ค ์ด ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ํค๋ฉด, ํด๋น ์ ์์๋ฅผ ๋ณด๊ณ ์๋ ๋ชจ๋ ์ฌ์ฉ์์๊ฒ ์ค์๊ฐ์ผ๋ก ์ ๋ฐ์ดํธ๊ฐ ์ ๋ฌ๋ผ! ๐
// resources/js/proposal-realtime.js
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'pusher',
key: process.env.MIX_PUSHER_APP_KEY,
cluster: process.env.MIX_PUSHER_APP_CLUSTER,
encrypted: true
});
// ์ ์์ ์์ธ ํ์ด์ง์์
const proposalId = document.querySelector('[data-proposal-id]').dataset.proposalId;
Echo.channel(`proposal.${proposalId}`)
.listen('VoteCasted', (e) => {
console.log('์๋ก์ด ํฌํ:', e);
// ํต๊ณ ์
๋ฐ์ดํธ
updateStatistics(e.statistics);
// ์ฐจํธ ์
๋ฐ์ดํธ
updateChart(e.statistics);
// ์ต๊ทผ ํฌํ ๋ชฉ๋ก์ ์ถ๊ฐ
addRecentVote(e);
// ์๋ฆผ ํ์
showNotification(`${e.voter}๋์ด ํฌํํ์ต๋๋ค!`);
});
function updateStatistics(stats) {
document.getElementById('total-votes').textContent = stats.total_votes;
document.getElementById('yes-votes').textContent = stats.yes_votes.toFixed(2);
document.getElementById('no-votes').textContent = stats.no_votes.toFixed(2);
document.getElementById('participation-rate').textContent = stats.participation_rate.toFixed(2) + '%';
// ํ๋ก๊ทธ๋ ์ค ๋ฐ ์
๋ฐ์ดํธ
const totalPower = stats.yes_votes + stats.no_votes + stats.abstain_votes;
const yesPercentage = (stats.yes_votes / totalPower * 100).toFixed(1);
const noPercentage = (stats.no_votes / totalPower * 100).toFixed(1);
document.getElementById('yes-bar').style.width = yesPercentage + '%';
document.getElementById('no-bar').style.width = noPercentage + '%';
document.getElementById('yes-percentage').textContent = yesPercentage + '%';
document.getElementById('no-percentage').textContent = noPercentage + '%';
}
function updateChart(stats) {
// Chart.js ์
๋ฐ์ดํธ
if (window.voteChart) {
window.voteChart.data.datasets[0].data = [
stats.yes_votes,
stats.no_votes,
stats.abstain_votes
];
window.voteChart.update();
}
}
function addRecentVote(vote) {
const votesList = document.getElementById('recent-votes');
const voteElement = document.createElement('div');
voteElement.className = 'vote-item';
voteElement.innerHTML = `
<span class="voter">${vote.voter}</span>
<span class="choice ${vote.choice}">${vote.choice.toUpperCase()}</span>
<span class="power">${vote.voting_power} ํ ํฐ</span>
<span class="time">๋ฐฉ๊ธ ์ </span>
`;
votesList.prepend(voteElement);
}
function showNotification(message) {
// ํ ์คํธ ์๋ฆผ ํ์
const toast = document.createElement('div');
toast.className = 'toast-notification';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('show');
}, 100);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
๐ ์ค๋งํธ ์ปจํธ๋ํธ ์ฐ๋ํ๊ธฐ
์ด์ ์ค์ ๋ธ๋ก์ฒด์ธ ์ค๋งํธ ์ปจํธ๋ํธ์ ์ฐ๋ํ๋ ๋ถ๋ถ์ ๊ตฌํํด๋ณด์. Solidity๋ก ์์ฑ๋ DAO ๊ฑฐ๋ฒ๋์ค ์ปจํธ๋ํธ์ ๋ผ๋ผ๋ฒจ ๋ฐฑ์๋๋ฅผ ์ฐ๊ฒฐํ ๊ฑฐ์ผ! โ๏ธ
// contracts/DAOGovernance.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract DAOGovernance {
struct Proposal {
uint256 id;
bytes32 ipfsHash;
uint256 votingStartTime;
uint256 votingEndTime;
uint256 yesVotes;
uint256 noVotes;
uint256 abstainVotes;
bool executed;
mapping(address => bool) hasVoted;
}
mapping(uint256 => Proposal) public proposals;
uint256 public proposalCount;
event ProposalCreated(uint256 indexed proposalId, bytes32 ipfsHash);
event VoteCasted(uint256 indexed proposalId, address indexed voter, uint8 choice, uint256 votingPower);
event ProposalExecuted(uint256 indexed proposalId);
function createProposal(bytes32 _ipfsHash, uint256 _votingDuration) external returns (uint256) {
proposalCount++;
Proposal storage newProposal = proposals[proposalCount];
newProposal.id = proposalCount;
newProposal.ipfsHash = _ipfsHash;
newProposal.votingStartTime = block.timestamp;
newProposal.votingEndTime = block.timestamp + _votingDuration;
emit ProposalCreated(proposalCount, _ipfsHash);
return proposalCount;
}
function vote(uint256 _proposalId, uint8 _choice, uint256 _votingPower) external {
Proposal storage proposal = proposals[_proposalId];
require(block.timestamp >= proposal.votingStartTime, "Voting not started");
require(block.timestamp <= proposal.votingEndTime, "Voting ended");
require(!proposal.hasVoted[msg.sender], "Already voted");
require(_choice <= 2, "Invalid choice");
proposal.hasVoted[msg.sender] = true;
if (_choice == 0) {
proposal.yesVotes += _votingPower;
} else if (_choice == 1) {
proposal.noVotes += _votingPower;
} else {
proposal.abstainVotes += _votingPower;
}
emit VoteCasted(_proposalId, msg.sender, _choice, _votingPower);
}
function executeProposal(uint256 _proposalId) external {
Proposal storage proposal = proposals[_proposalId];
require(block.timestamp > proposal.votingEndTime, "Voting not ended");
require(!proposal.executed, "Already executed");
require(proposal.yesVotes > proposal.noVotes, "Proposal rejected");
proposal.executed = true;
emit ProposalExecuted(_proposalId);
}
}
php artisan make:service Web3Service
// app/Services/Web3Service.php
namespace App\Services;
use Web3\Web3;
use Web3\Contract;
use Web3\Utils;
class Web3Service
{
protected $web3;
protected $contract;
protected $contractAddress;
public function __construct() {
$this->web3 = new Web3(config('blockchain.rpc_url'));
$this->contractAddress = config('blockchain.contract_address');
$abi = json_decode(
file_get_contents(storage_path('contracts/DAOGovernance.json')),
true
);
$this->contract = new Contract($this->web3->provider, $abi);
}
// ์ ์์๋ฅผ ๋ธ๋ก์ฒด์ธ์ ์์ฑ
public function createProposal($ipfsHash, $votingDuration) {
$ipfsHashBytes32 = $this->stringToBytes32($ipfsHash);
return new Promise(function ($resolve, $reject) use ($ipfsHashBytes32, $votingDuration) {
$this->contract->at($this->contractAddress)->send(
'createProposal',
$ipfsHashBytes32,
$votingDuration,
[
'from' => config('blockchain.admin_address'),
'gas' => '300000'
],
function ($err, $txHash) use ($resolve, $reject) {
if ($err !== null) {
$reject($err);
return;
}
$resolve($txHash);
}
);
});
}
// ํฌํ๋ฅผ ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋ก
public function castVote($proposalId, $voterAddress, $choice, $votingPower) {
$choiceInt = $choice === 'yes' ? 0 : ($choice === 'no' ? 1 : 2);
$votingPowerWei = bcmul($votingPower, '1000000000000000000'); // Wei ๋ณํ
return new Promise(function ($resolve, $reject) use ($proposalId, $voterAddress, $choiceInt, $votingPowerWei) {
$this->contract->at($this->contractAddress)->send(
'vote',
$proposalId,
$choiceInt,
$votingPowerWei,
[
'from' => $voterAddress,
'gas' => '200000'
],
function ($err, $txHash) use ($resolve, $reject) {
if ($err !== null) {
$reject($err);
return;
}
$resolve($txHash);
}
);
});
}
// ๋ธ๋ก์ฒด์ธ์์ ์ ์์ ์ ๋ณด ์กฐํ
public function getProposal($proposalId) {
return new Promise(function ($resolve, $reject) use ($proposalId) {
$this->contract->at($this->contractAddress)->call(
'proposals',
$proposalId,
function ($err, $result) use ($resolve, $reject) {
if ($err !== null) {
$reject($err);
return;
}
$resolve($result);
}
);
});
}
// ๋ฌธ์์ด์ bytes32๋ก ๋ณํ
private function stringToBytes32($string) {
$hex = bin2hex($string);
return '0x' . str_pad($hex, 64, '0', STR_PAD_RIGHT);
}
}
๐ฑ API ์๋ํฌ์ธํธ ์ค๊ณ
ํ๋ก ํธ์๋(React, Vue ๋ฑ)์ ์ฐ๋ํ๊ธฐ ์ํ RESTful API๋ฅผ ์ค๊ณํด๋ณด์. ๋ชจ๋ฐ์ผ ์ฑ์ด๋ ์ธ๋ถ ์๋น์ค์์๋ ์ฌ์ฉํ ์ ์๋๋ก ์ ์ ๋ฆฌ๋ API๊ฐ ํ์ํด! ๐
// routes/api.php
use App\Http\Controllers\Api\{
AuthController,
ProposalController,
VoteController,
UserController
};
// ์ธ์ฆ ๊ด๋ จ
Route::post('/auth/nonce', [AuthController::class, 'getNonce']);
Route::post('/auth/verify', [AuthController::class, 'verifySignature']);
// ์ธ์ฆ ํ์ํ ๋ผ์ฐํธ
Route::middleware('auth:sanctum')->group(function () {
Route::post('/auth/logout', [AuthController::class, 'logout']);
// ์ฌ์ฉ์
Route::get('/user/profile', [UserController::class, 'profile']);
Route::put('/user/profile', [UserController::class, 'updateProfile']);
Route::get('/user/votes', [UserController::class, 'myVotes']);
Route::get('/user/proposals', [UserController::class, 'myProposals']);
// ์ ์์
Route::apiResource('proposals', ProposalController::class);
Route::post('/proposals/{id}/submit', [ProposalController::class, 'submit']);
Route::get('/proposals/{id}/statistics', [ProposalController::class, 'statistics']);
// ํฌํ
Route::post('/proposals/{id}/vote', [VoteController::class, 'store']);
Route::delete('/proposals/{id}/vote', [VoteController::class, 'destroy']);
Route::get('/proposals/{id}/votes', [VoteController::class, 'index']);
// ๋๊ธ
Route::post('/proposals/{id}/comments', [CommentController::class, 'store']);
Route::put('/comments/{id}', [CommentController::class, 'update']);
Route::delete('/comments/{id}', [CommentController::class, 'destroy']);
});
// ๊ณต๊ฐ API
Route::get('/proposals', [ProposalController::class, 'index']);
Route::get('/proposals/{id}', [ProposalController::class, 'show']);
Route::get('/statistics/overview', [StatisticsController::class, 'overview']);
php artisan make:resource ProposalResource
// app/Http/Resources/ProposalResource.php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ProposalResource extends JsonResource
{
public function toArray($request) {
return [
'id' => $this->id,
'title' => $this->title,
'description' => $this->description,
'content' => $this->content,
'category' => $this->category,
'status' => $this->status,
'author' => [
'id' => $this->author->id,
'username' => $this->author->username,
'wallet_address' => $this->author->wallet_address,
'avatar_url' => $this->author->avatar_url
],
'voting' => [
'starts_at' => $this->voting_starts_at?->toIso8601String(),
'ends_at' => $this->voting_ends_at?->toIso8601String(),
'is_active' => $this->isActive(),
'time_remaining' => $this->voting_ends_at ?
now()->diffInSeconds($this->voting_ends_at) : null
],
'statistics' => [
'total_votes' => $this->statistics->total_votes,
'yes_votes' => (float) $this->statistics->yes_votes,
'no_votes' => (float) $this->statistics->no_votes,
'abstain_votes' => (float) $this->statistics->abstain_votes,
'participation_rate' => (float) $this->statistics->participation_rate,
'comments_count' => $this->statistics->comments_count,
'views_count' => $this->statistics->views_count
],
'blockchain' => [
'ipfs_hash' => $this->ipfs_hash,
'tx_hash' => $this->blockchain_tx_hash,
'ipfs_url' => $this->ipfs_hash ?
"https://ipfs.io/ipfs/{$this->ipfs_hash}" : null
],
'created_at' => $this->created_at->toIso8601String(),
'updated_at' => $this->updated_at->toIso8601String()
];
}
}
API ๋ฆฌ์์ค๋ฅผ ์ฌ์ฉํ๋ฉด ์๋ต ํ์์ ์ผ๊ด๋๊ฒ ์ ์งํ ์ ์์ด. ํ๋ก ํธ์๋ ๊ฐ๋ฐ์๊ฐ ํจ์ฌ ํธํ๊ฒ ์์ ํ ์ ์์ง! ๐จ
๐จ ํ๋ก ํธ์๋ UI ๊ตฌํ ํ
๋ฐฑ์๋๊ฐ ์์ฑ๋์์ผ๋ ์ด์ ์ฌ์ฉ์๊ฐ ์ค์ ๋ก ์ฌ์ฉํ UI๋ฅผ ๋ง๋ค์ด์ผ ํด. Blade ํ ํ๋ฆฟ์ด๋ Vue.js/React๋ฅผ ํ์ฉํ ๋ช ๊ฐ์ง ํ์ ๊ณต์ ํ ๊ฒ! ๐ผ๏ธ
2. ํฌํ ์์ ฏ: Yes/No/Abstain ๋ฒํผ๊ณผ ์ค์๊ฐ ์ง๊ณ
3. ํ์๋ผ์ธ: ์ ์์ ์๋ช ์ฃผ๊ธฐ๋ฅผ ์๊ฐ์ ์ผ๋ก ํํ
4. ๋๊ธ ์์คํ : ์ค๋ ๋ ํํ์ ํ ๋ก ๊ณต๊ฐ
5. ์ง๊ฐ ์ฐ๊ฒฐ ๋ฒํผ: MetaMask ์ฐ๋ UI
6. ํต๊ณ ๋์๋ณด๋: ์ฐจํธ์ ๊ทธ๋ํ๋ก ๋ฐ์ดํฐ ์๊ฐํ
// resources/js/components/VoteWidget.vue
<template>
<div class="vote-widget">
<div v-if="!hasVoted" class="vote-buttons">
<button
@click="vote('yes')"
class="vote-btn yes"
:disabled="!canVote">
๐ ์ฐฌ์ฑ
</button>
<button
@click="vote('no')"
class="vote-btn no"
:disabled="!canVote">
๐ ๋ฐ๋
</button>
<button
@click="vote('abstain')"
class="vote-btn abstain"
:disabled="!canVote">
๐คท ๊ธฐ๊ถ
</button>
</div>
<div v-else class="voted-status">
<p>โ
ํฌํ ์๋ฃ: {{ userVote.choice.toUpperCase() }}</p>
<button @click="cancelVote" class="cancel-btn">
ํฌํ ์ทจ์
</button>
</div>
<div class="vote-stats">
<div class="stat-bar">
<div class="bar yes" :style="{ width: yesPercentage + '%' }">
<span>{{ yesPercentage }}%</span>
</div>
<div class="bar no" :style="{ width: noPercentage + '%' }">
<span>{{ noPercentage }}%</span>
</div>
</div>
<div class="stat-details">
<p>์ด ํฌํ: {{ statistics.total_votes }}๋ช
</p>
<p>์ฐธ์ฌ์จ: {{ statistics.participation_rate }}%</p>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['proposalId', 'initialStatistics', 'userVote'],
data() {
return {
statistics: this.initialStatistics,
hasVoted: !!this.userVote,
canVote: true
}
},
computed: {
totalPower() {
return this.statistics.yes_votes +
this.statistics.no_votes +
this.statistics.abstain_votes;
},
yesPercentage() {
return this.totalPower > 0 ?
(this.statistics.yes_votes / this.totalPower * 100).toFixed(1) : 0;
},
noPercentage() {
return this.totalPower > 0 ?
(this.statistics.no_votes / this.totalPower * 100).toFixed(1) : 0;
}
},
methods: {
async vote(choice) {
try {
const response = await axios.post(
`/api/proposals/${this.proposalId}/vote`,
{ choice }
);
this.hasVoted = true;
this.$emit('voted', response.data);
} catch (error) {
alert('ํฌํ ์คํจ: ' + error.response.data.message);
}
},
async cancelVote() {
if (!confirm('ํฌํ๋ฅผ ์ทจ์ํ์๊ฒ ์ต๋๊น?')) return;
try {
await axios.delete(`/api/proposals/${this.proposalId}/vote`);
this.hasVoted = false;
this.$emit('vote-cancelled');
} catch (error) {
alert('ํฌํ ์ทจ์ ์คํจ');
}
}
},
mounted() {
// ์ค์๊ฐ ์
๋ฐ์ดํธ ๋ฆฌ์ค๋
Echo.channel(`proposal.${this.proposalId}`)
.listen('VoteCasted', (e) => {
this.statistics = e.statistics;
});
}
}
</script>
๐ ์๋ฆผ ์์คํ ๊ตฌ์ถํ๊ธฐ
DAO ํ๋ซํผ์์๋ ๋ค์ํ ์ด๋ฒคํธ์ ๋ํ ์๋ฆผ์ด ์ค์ํด. ์๋ก์ด ์ ์์๊ฐ ์ฌ๋ผ์ค๊ฑฐ๋, ํฌํ๊ฐ ์์๋๊ฑฐ๋, ๊ฒฐ๊ณผ๊ฐ ๋์์ ๋ ์ฌ์ฉ์์๊ฒ ์๋ ค์ค์ผ ํ๋๊น! ๐ฌ
php artisan make:notification NewProposalNotification
// app/Notifications/NewProposalNotification.php
namespace App\Notifications;
use App\Models\Proposal;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\BroadcastMessage;
class NewProposalNotification extends Notification
{
use Queueable;
protected $proposal;
public function __construct(Proposal $proposal) {
$this->proposal = $proposal;
}
public function via($notifiable) {
return ['mail', 'database', 'broadcast'];
}
public function toMail($notifiable) {
return (new MailMessage)
->subject('์๋ก์ด ์ ์์: ' . $this->proposal->title)
->greeting('์๋
ํ์ธ์!')
->line('์๋ก์ด ์ ์์๊ฐ ๋ฑ๋ก๋์์ต๋๋ค.')
->line('์ ๋ชฉ: ' . $this->proposal->title)
->line('์์ฑ์: ' . $this->proposal->author->username)
->action('์ ์์ ๋ณด๊ธฐ', url('/proposals/' . $this->proposal->id))
->line('ํฌํ ๊ธฐ๊ฐ: ' . $this->proposal->voting_ends_at->diffForHumans());
}
public function toDatabase($notifiable) {
return [
'proposal_id' => $this->proposal->id,
'title' => $this->proposal->title,
'author' => $this->proposal->author->username,
'type' => 'new_proposal'
];
}
public function toBroadcast($notifiable) {
return new BroadcastMessage([
'proposal_id' => $this->proposal->id,
'title' => $this->proposal->title,
'message' => '์๋ก์ด ์ ์์๊ฐ ๋ฑ๋ก๋์์ต๋๋ค.'
]);
}
}
// ์ ์์๊ฐ ์ ์ถ๋ ๋ ๋ชจ๋ ํ์ฑ ์ฌ์ฉ์์๊ฒ ์๋ฆผ
$activeUsers = User::where('is_verified', true)
->where('last_login_at', '>', now()->subDays(30))
->get();
foreach ($activeUsers as $user) {
$user->notify(new NewProposalNotification($proposal));
}
// ๋๋ ํ๋ฅผ ์ฌ์ฉํ ๋๋ ์๋ฆผ
Notification::send($activeUsers, new NewProposalNotification($proposal));
๐ ํต๊ณ ๋ฐ ๋ถ์ ๊ธฐ๋ฅ
DAO์ ๊ฑด๊ฐ๋๋ฅผ ์ธก์ ํ๊ณ ๊ฐ์ ํ๋ ค๋ฉด ๋ค์ํ ํต๊ณ ๋ฐ์ดํฐ๊ฐ ํ์ํด. ์ฐธ์ฌ์จ, ํฌํ ํจํด, ์ ์์ ํต๊ณผ์จ ๋ฑ์ ๋ถ์ํ๋ ์์คํ ์ ๋ง๋ค์ด๋ณด์! ๐
php artisan make:controller StatisticsController
// app/Http/Controllers/StatisticsController.php
namespace App\Http\Controllers;
use App\Models\{Proposal, Vote, User};
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
class StatisticsController extends Controller
{
// ์ ์ฒด ํต๊ณ ๋์๋ณด๋
public function overview() {
$stats = Cache::remember('dao_statistics', 3600, function () {
return [
'total_proposals' => Proposal::count(),
'active_proposals' => Proposal::where('status', 'active')->count(),
'passed_proposals' => Proposal::where('status', 'passed')->count(),
'total_votes' => Vote::count(),
'active_voters' => User::whereHas('votes')->count(),
'total_members' => User::count(),
'avg_participation_rate' => ProposalStatistic::avg('participation_rate'),
'avg_approval_rate' => $this->calculateAverageApprovalRate()
];
});
return view('statistics.overview', compact('stats'));
}
// ์นดํ
๊ณ ๋ฆฌ๋ณ ํต๊ณ
public function byCategory() {
$categoryStats = Proposal::select('category')
->selectRaw('COUNT(*) as total')
->selectRaw('SUM(CASE WHEN status = "passed" THEN 1 ELSE 0 END) as passed')
->selectRaw('SUM(CASE WHEN status = "rejected" THEN 1 ELSE 0 END) as rejected')
->groupBy('category')
->get();
return response()->json($categoryStats);
}
// ์๊ฐ๋๋ณ ํฌํ ํ๋
public function votingActivity(Request $request) {
$period = $request->get('period', 30); // ๊ธฐ๋ณธ 30์ผ
$activity = Vote::select(DB::raw('DATE(voted_at) as date'))
->selectRaw('COUNT(*) as votes_count')
->selectRaw('COUNT(DISTINCT user_id) as unique_voters')
->where('voted_at', '>', now()->subDays($period))
->groupBy('date')
->orderBy('date')
->get();
return response()->json($activity);
}
// ์์ ๊ธฐ์ฌ์
public function topContributors() {
$topVoters = User::withCount('votes')
->orderBy('votes_count', 'desc')
->limit(10)
->get();
$topProposers = User::withCount('proposals')
->orderBy('proposals_count', 'desc')
->limit(10)
->get();
return response()->json([
'top_voters' => $topVoters,
'top_proposers' => $topProposers
]);
}
// ํ๊ท ์น์ธ์จ ๊ณ์ฐ
private function calculateAverageApprovalRate() {
$completedProposals = Proposal::whereIn('status', ['passed', 'rejected'])
->with('statistics')
->get();
if ($completedProposals->isEmpty()) {
return 0;
}
$totalApprovalRate = $completedProposals->sum(function ($proposal) {
$stats = $proposal->statistics;
$totalPower = $stats->yes_votes + $stats->no_votes + $stats->abstain_votes;
return $totalPower > 0 ? ($stats->yes_votes / $totalPower * 100) : 0;
});
return $totalApprovalRate / $completedProposals->count();
}
}
๐ ๋ณด์ ๋ฐ ๊ถํ ๊ด๋ฆฌ
DAO ํ๋ซํผ์ ๋ง์ ์ฌ๋๋ค์ ์์ฐ๊ณผ ์์ฌ๊ฒฐ์ ์ด ๊ฑธ๋ ค์๊ธฐ ๋๋ฌธ์ ๋ณด์์ด ์ ๋ง ์ค์ํด. ๋ค์ํ ๋ณด์ ์ํ์ ๋๋นํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์! ๐ก๏ธ
2. Front-running: ํธ๋์ญ์ ์ ๋จผ์ ๋ณด๊ณ ์์์ ์คํ
3. Flash Loan Attack: ์๊ฐ์ ์ผ๋ก ํ ํฐ์ ๋น๋ ค ํฌํ๊ถ ํ๋
4. Governance Attack: ์ ์์ ์ธ ์ ์์๋ก ์์คํ ์ฅ์
5. SQL Injection: ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ณต๊ฒฉ
6. XSS/CSRF: ์น ์ ํ๋ฆฌ์ผ์ด์ ์ทจ์ฝ์ ๊ณต๊ฒฉ
php artisan make:middleware CheckVotingEligibility
// app/Http/Middleware/CheckVotingEligibility.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CheckVotingEligibility
{
public function handle(Request $request, Closure $next) {
$user = $request->user();
// 1. ๊ณ์ ๊ฒ์ฆ ํ์ธ
if (!$user->is_verified) {
return response()->json([
'error' => '๊ณ์ ๊ฒ์ฆ์ด ํ์ํฉ๋๋ค.'
], 403);
}
// 2. ์ต์ ํ ํฐ ๋ณด์ ๋ ํ์ธ
$minTokenRequired = config('dao.min_token_for_voting', 1);
if ($user->token_balance < $minTokenRequired) {
return response()->json([
'error' => 'ํฌํํ๋ ค๋ฉด ์ต์ ' . $minTokenRequired . ' ํ ํฐ์ด ํ์ํฉ๋๋ค.'
], 403);
}
// 3. ๊ณ์ ์์ฑ ํ ์ต์ ๊ธฐ๊ฐ ํ์ธ (Sybil ๊ณต๊ฒฉ ๋ฐฉ์ง)
$minAccountAge = config('dao.min_account_age_days', 7);
if ($user->created_at->diffInDays(now()) < $minAccountAge) {
return response()->json([
'error' => '๊ณ์ ์์ฑ ํ ' . $minAccountAge . '์ผ์ด ์ง๋์ผ ํฌํํ ์ ์์ต๋๋ค.'
], 403);
}
// 4. Rate Limiting (๊ณผ๋ํ ์์ฒญ ๋ฐฉ์ง)
$key = 'vote_attempt:' . $user->id;
$attempts = Cache::get($key, 0);
if ($attempts > 10) {
return response()->json([
'error' => '๋๋ฌด ๋ง์ ํฌํ ์๋์
๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.'
], 429);
}
Cache::put($key, $attempts + 1, 60); // 1๋ถ๊ฐ ์ ์ง
return $next($request);
}
}
Flash Loan ๊ณต๊ฒฉ์ ๋ฐฉ์งํ๋ ค๋ฉด ํฌํ ์์ ์์ ์ ํ ํฐ ๋ณด์ ๋์ ๊ธฐ์ค์ผ๋ก ํด์ผ ํด:
// app/Models/Proposal.php
public function createSnapshot() {
// ํฌํ ์์ ์์ ์ ๋ชจ๋ ์ฌ์ฉ์ ํ ํฐ ์์ก ์ค๋
์ท
$users = User::where('token_balance', '>', 0)->get();
foreach ($users as $user) {
VotingSnapshot::create([
'proposal_id' => $this->id,
'user_id' => $user->id,
'token_balance' => $user->token_balance,
'snapshot_at' => now()
]);
}
}
// ํฌํ ์ ์ค๋
์ท ์์ก ์ฌ์ฉ
public function getVotingPower(User $user) {
$snapshot = VotingSnapshot::where('proposal_id', $this->id)
->where('user_id', $user->id)
->first();
return $snapshot ? $snapshot->token_balance : 0;
}
php artisan make:model AuditLog -m
// ๋ง์ด๊ทธ๋ ์ด์
Schema::create('audit_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained();
$table->string('action'); // 'vote', 'proposal_create', 'proposal_edit' ๋ฑ
$table->string('entity_type'); // 'Proposal', 'Vote' ๋ฑ
$table->unsignedBigInteger('entity_id');
$table->json('old_values')->nullable();
$table->json('new_values')->nullable();
$table->string('ip_address');
$table->string('user_agent');
$table->timestamps();
$table->index(['user_id', 'action', 'created_at']);
});
// ์ฌ์ฉ ์์
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'vote_cast',
'entity_type' => 'Vote',
'entity_id' => $vote->id,
'new_values' => [
'proposal_id' => $vote->proposal_id,
'choice' => $vote->choice,
'voting_power' => $vote->voting_power
],
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent()
]);
๐ ์ฑ๋ฅ ์ต์ ํ ์ ๋ต
์ฌ์ฉ์๊ฐ ๋ง์์ง๋ฉด ์ฑ๋ฅ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ด. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ ์ต์ ํ, ์บ์ฑ, ํ ํ์ฉ ๋ฑ ๋ค์ํ ์ต์ ํ ๊ธฐ๋ฒ์ ์ ์ฉํด๋ณด์! โก
- โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธ๋ฑ์ค ์ต์ ํ
- โ Eager Loading์ผ๋ก N+1 ์ฟผ๋ฆฌ ๋ฌธ์ ํด๊ฒฐ
- โ Redis ์บ์ฑ์ผ๋ก ๋ฐ๋ณต ์ฟผ๋ฆฌ ์ ๊ฑฐ
- โ ํ๋ฅผ ํ์ฉํ ๋ฌด๊ฑฐ์ด ์์ ๋น๋๊ธฐ ์ฒ๋ฆฌ
- โ CDN์ผ๋ก ์ ์ ํ์ผ ์ ๊ณต
- โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ํ์ด์ง๋ค์ด์
- โ API ์๋ต ์์ถ (Gzip)
- โ ์ด๋ฏธ์ง ์ต์ ํ ๋ฐ Lazy Loading
// ์ ์์ ๋ชฉ๋ก ์บ์ฑ
public function index() {
$cacheKey = 'proposals:list:' . request('page', 1);
$proposals = Cache::remember($cacheKey, 300, function () {
return Proposal::with(['author', 'statistics'])
->where('status', '!=', 'draft')
->orderBy('created_at', 'desc')
->paginate(20);
});
return view('proposals.index', compact('proposals'));
}
// ํฌํ ํต๊ณ ์บ์ฑ
public function statistics($id) {
$cacheKey = "proposal:{$id}:statistics";
$stats = Cache::remember($cacheKey, 60, function () use ($id) {
$proposal = Proposal::with('statistics')->findOrFail($id);
return $proposal->statistics;
});
return response()->json($stats);
}
// ํฌํ ๋ฐ์ ์ ์บ์ ๋ฌดํจํ
public function store(Request $request, $proposalId) {
// ... ํฌํ ์ฒ๋ฆฌ ...
// ๊ด๋ จ ์บ์ ์ญ์
Cache::forget("proposal:{$proposalId}:statistics");
Cache::forget("proposals:list:1"); // ์ฒซ ํ์ด์ง ์บ์ ์ญ์
return response()->json(['success' => true]);
}
// โ ๋์ ์: N+1 ์ฟผ๋ฆฌ ๋ฌธ์
$proposals = Proposal::all();
foreach ($proposals as $proposal) {
echo $proposal->author->username; // ๋งค๋ฒ ์ฟผ๋ฆฌ ์คํ!
echo $proposal->statistics->total_votes; // ๋ ์ฟผ๋ฆฌ!
}
// โ
์ข์ ์: Eager Loading
$proposals = Proposal::with(['author', 'statistics'])
->get();
foreach ($proposals as $proposal) {
echo $proposal->author->username; // ์ด๋ฏธ ๋ก๋๋จ
echo $proposal->statistics->total_votes; // ์ด๋ฏธ ๋ก๋๋จ
}
// โ
๋ ์ข์ ์: ํ์ํ ์ปฌ๋ผ๋ง ์ ํ
$proposals = Proposal::select(['id', 'title', 'author_id', 'status'])
->with([
'author:id,username,avatar_url',
'statistics:proposal_id,total_votes,yes_votes,no_votes'
])
->get();
๐งช ํ ์คํธ ์ฝ๋ ์์ฑํ๊ธฐ
์์ ์ ์ธ DAO ํ๋ซํผ์ ๋ง๋ค๋ ค๋ฉด ํ ์คํธ๊ฐ ํ์์ผ. PHPUnit์ ์ฌ์ฉํด์ ํต์ฌ ๊ธฐ๋ฅ๋ค์ ํ ์คํธํด๋ณด์! ๐งช
php artisan make:test VoteTest
// tests/Feature/VoteTest.php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\{User, Proposal, Vote};
use Illuminate\Foundation\Testing\RefreshDatabase;
class VoteTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_vote_on_active_proposal() {
$user = User::factory()->create(['token_balance' => 100]);
$proposal = Proposal::factory()->create([
'status' => 'active',
'voting_starts_at' => now()->subDay(),
'voting_ends_at' => now()->addDay()
]);
$response = $this->actingAs($user)
->postJson("/api/proposals/{$proposal->id}/vote", [
'choice' => 'yes'
]);
$response->assertStatus(200);
$this->assertDatabaseHas('votes', [
'proposal_id' => $proposal->id,
'user_id' => $user->id,
'choice' => 'yes'
]);
}
public function test_user_cannot_vote_twice() {
$user = User::factory()->create(['token_balance' => 100]);
$proposal = Proposal::factory()->create(['status' => 'active']);
Vote::factory()->create([
'proposal_id' => $proposal->id,
'user_id' => $user->id
]);
$response = $this->actingAs($user)
->postJson("/api/proposals/{$proposal->id}/vote", [
'choice' => 'yes'
]);
$response->assertStatus(403);
}
public function test_user_without_tokens_cannot_vote() {
$user = User::factory()->create(['token_balance' => 0]);
$proposal = Proposal::factory()->create(['status' => 'active']);
$response = $this->actingAs($user)
->postJson("/api/proposals/{$proposal->id}/vote", [
'choice' => 'yes'
]);
$response->assertStatus(403);
}
}
๐ ๋ฐฐํฌ ๋ฐ ์ด์ ๊ฐ์ด๋
๊ฐ๋ฐ์ด ์๋ฃ๋์์ผ๋ฉด ์ด์ ์ค์ ์๋ฒ์ ๋ฐฐํฌํ ์ฐจ๋ก์ผ. ์์ ์ ์ธ ์ด์์ ์ํ ๋ฐฐํฌ ์ ๋ต๊ณผ ๋ชจ๋ํฐ๋ง ๋ฐฉ๋ฒ์ ์์๋ณด์! ๐
# Dockerfile
FROM php:8.2-fpm
RUN apt-get update && apt-get install -y \
git \
curl \
libpng-dev \
libonig-dev \
libxml2-dev \
zip \
unzip
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www
COPY . .
RUN composer install --no-dev --optimize-autoloader
RUN php artisan config:cache
RUN php artisan route:cache
RUN php artisan view:cache
CMD php artisan serve --host=0.0.0.0 --port=8000
# .env.production
APP_ENV=production
APP_DEBUG=false
APP_URL=https://dao.example.com
DB_CONNECTION=mysql
DB_HOST=production-db.example.com
DB_DATABASE=dao_production
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
BLOCKCHAIN_NETWORK=mainnet
BLOCKCHAIN_RPC_URL=https://mainnet.infura.io/v3/YOUR_KEY
# ๋ชจ๋ํฐ๋ง
SENTRY_LARAVEL_DSN=your_sentry_dsn
- โ ํ๊ฒฝ ๋ณ์ ์ค์ ํ์ธ
- โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์ ์คํ
- โ ์บ์ ๋ฐ ์ค์ ํ์ผ ์ต์ ํ
- โ SSL ์ธ์ฆ์ ์ค์
- โ ๋ฐฉํ๋ฒฝ ๋ฐ ๋ณด์ ๊ทธ๋ฃน ์ค์
- โ ๋ฐฑ์ ์์คํ ๊ตฌ์ถ
- โ ๋ชจ๋ํฐ๋ง ๋๊ตฌ ์ค์
- โ ๋ก๊ทธ ์์ง ์์คํ ๊ตฌ์ถ
๐ ๋ง์น๋ฉฐ
์ฌ๊ธฐ๊น์ง ๋ผ๋ผ๋ฒจ ๊ธฐ๋ฐ DAO ์์ฌ๊ฒฐ์ ๊ธฐ๋ก ํ๋ซํผ์ ๋ง๋๋ ์ ์ฒด ๊ณผ์ ์ ์ดํด๋ดค์ด! ๐
์ฐ๋ฆฌ๊ฐ ๋ค๋ฃฌ ๋ด์ฉ์ ์ ๋ฆฌํ๋ฉด:
โ
์์คํ
์ํคํ
์ฒ ์ค๊ณ - ์จ์ฒด์ธ๊ณผ ์คํ์ฒด์ธ์ ํ์ด๋ธ๋ฆฌ๋ ๊ตฌ์กฐ
โ
๋ผ๋ผ๋ฒจ ํ๋ก์ ํธ ์ด๊ธฐ ์ค์ - ํ์ ํจํค์ง ์ค์น ๋ฐ ํ๊ฒฝ ๊ตฌ์ฑ
โ
๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ์ค๊ณ - ์ ์์, ํฌํ, ์ฌ์ฉ์ ํ
์ด๋ธ
โ
์ง๊ฐ ์ฐ๊ฒฐ ์ธ์ฆ ์์คํ
- MetaMask ์ฐ๋ ๋ฐ ์๋ช
๊ฒ์ฆ
โ
์ ์์ ์์ฑ ๋ฐ ๊ด๋ฆฌ - IPFS ์
๋ก๋ ๋ฐ ์๋ช
์ฃผ๊ธฐ ๊ด๋ฆฌ
โ
ํฌํ ์์คํ
๊ตฌํ - ์ค์๊ฐ ์ง๊ณ ๋ฐ ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ก
โ
์ค๋งํธ ์ปจํธ๋ํธ ์ฐ๋ - Web3.js๋ฅผ ํตํ ๋ธ๋ก์ฒด์ธ ํต์
โ
๋ณด์ ๋ฐ ๊ถํ ๊ด๋ฆฌ - ๋ค์ํ ๊ณต๊ฒฉ ๋ฒกํฐ ๋ฐฉ์ด
โ
์ฑ๋ฅ ์ต์ ํ - ์บ์ฑ, ํ, ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ต์ ํ
โ
ํ
์คํธ ๋ฐ ๋ฐฐํฌ - ์์ ์ ์ธ ์ด์์ ์ํ ์ค๋น
์ด ํ๋ซํผ์ ๋จ์ํ ํฌํ ์์คํ
์ด ์๋๋ผ, ํ์ค์ํ ์กฐ์ง์ ํฌ๋ช
ํ ๊ฑฐ๋ฒ๋์ค๋ฅผ ์คํํ๋ ๋๊ตฌ์ผ. ์ปค๋ฎค๋ํฐ ๊ตฌ์ฑ์๋ค์ด ํจ๊ป ์์ฌ๊ฒฐ์ ์ ๋ด๋ฆฌ๊ณ , ๊ทธ ๊ณผ์ ์ด ๋ชจ๋ ๊ธฐ๋ก๋๋ฉฐ, ๋๊ตฌ๋ ๊ฒ์ฆํ ์ ์๋ ์์คํ
์ด์ง. ๐
๋ผ๋ผ๋ฒจ์ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ๋ค(Eloquent ORM, ํ ์์คํ
, ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋ฑ)๊ณผ ๋ธ๋ก์ฒด์ธ ๊ธฐ์ ์ ๊ฒฐํฉํ๋ฉด, ์ ํต์ ์ธ ์น ๊ฐ๋ฐ์ ํธ๋ฆฌํจ๊ณผ Web3์ ํ์ ์ฑ์ ๋์์ ๋๋ฆด ์ ์์ด. ๐
๋ฌผ๋ก ์ฌ๊ธฐ์ ๋ค๋ฃฌ ๋ด์ฉ์ ๊ธฐ๋ณธ์ ์ธ ๊ตฌ์กฐ์ด๊ณ , ์ค์ ํ๋ก๋์
ํ๊ฒฝ์์๋ ๋ ๋ง์ ๊ณ ๋ ค์ฌํญ์ด ์์ด. ์๋ฅผ ๋ค๋ฉด:
๐น ๊ณ ๊ธ ํฌํ ๋ฉ์ปค๋์ฆ: Quadratic Voting, Conviction Voting ๋ฑ
๐น ๋ฉํฐ์๊ทธ ์คํ: ์ค์ํ ๊ฒฐ์ ์ ์ฌ๋ฌ ์๋ช
ํ์
๐น ํ ํฐ ์์: ๋ค๋ฅธ ์ฌ์ฉ์์๊ฒ ํฌํ๊ถ ์์
๐น ์๊ฐ ๊ฐ์ค ํฌํ: ํ ํฐ ๋ณด์ ๊ธฐ๊ฐ์ ๋ฐ๋ฅธ ๊ฐ์ค์น
๐น ์คํ์ฒด์ธ ํฌํ: Snapshot ๊ฐ์ ๊ฐ์ค๋น ์๋ ํฌํ
์ด๋ฐ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค๋ ๋ผ๋ผ๋ฒจ์ ์ ์ฐํ ๊ตฌ์กฐ ์์์ ์ถฉ๋ถํ ๊ตฌํํ ์ ์์ด! ๐ช
ํน์ ๊ตฌํ ์ค์ ๋งํ๋ ๋ถ๋ถ์ด ์๊ฑฐ๋, ๋ ์ ๋ฌธ์ ์ธ ๋์์ด ํ์ํ๋ค๋ฉด ์ฌ๋ฅ๋ท์์ ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ ์ ๋ฌธ๊ฐ๋ ๋ผ๋ผ๋ฒจ ์ ๋ฌธ๊ฐ๋ฅผ ์ฐพ์๋ณผ ์ ์์ด. ์ค๋ฌด ๊ฒฝํ์ด ํ๋ถํ ๊ฐ๋ฐ์๋ค์ด ํ๋ก์ ํธ๋ฅผ ์ฑ๊ณต์ ์ผ๋ก ์์ฑํ ์ ์๋๋ก ๋์์ค ๊ฑฐ์ผ! ๐
DAO๋ ๋ฏธ๋์ ์กฐ์ง ํํ์ผ. ์ ํต์ ์ธ ์๊ณ ๊ตฌ์กฐ๊ฐ ์๋, ์ปค๋ฎค๋ํฐ ์ค์ฌ์ ์ํ์ ์์ฌ๊ฒฐ์ ๊ตฌ์กฐ. ๊ทธ๋ฆฌ๊ณ ์ฐ๋ฆฌ๊ฐ ๋ง๋ ์ด ํ๋ซํผ์ด ๊ทธ ๋ฏธ๋๋ฅผ ์คํํ๋ ๋๊ตฌ๊ฐ ๋ ์ ์์ด. ๐
์, ์ด์ ๋น์ ์ DAO ํ๋ซํผ์ ๋ง๋ค์ด๋ณผ ์๊ฐ์ด์ผ! ์ฝ๋ฉ์ ์์ํ๊ณ , ์ปค๋ฎค๋ํฐ๋ฅผ ๊ตฌ์ถํ๊ณ , ํจ๊ป ๋ ๋์ ๋ฏธ๋๋ฅผ ๋ง๋ค์ด๊ฐ์! ํ์ดํ
! ๐
๋ผ๋ผ๋ฒจ + ๋ธ๋ก์ฒด์ธ = ๊ฐ๋ ฅํ DAO ํ๋ซํผ
์จ์ฒด์ธ(ํฌํ ๊ฒฐ๊ณผ) + ์คํ์ฒด์ธ(์ ์์ ๋ด์ฉ) ํ์ด๋ธ๋ฆฌ๋ ๊ตฌ์กฐ
์ง๊ฐ ๊ธฐ๋ฐ ์ธ์ฆ์ผ๋ก ํ์ค์ํ ์คํ
์ค์๊ฐ ํฌํ ํํฉ๊ณผ ํฌ๋ช ํ ์์ฌ๊ฒฐ์ ๊ณผ์
๋ณด์, ์ฑ๋ฅ, ํ์ฅ์ฑ์ ๋ชจ๋ ๊ณ ๋ คํ ์ค๊ณ
๋น์ ๋ ํ ์ ์์ด! ์ง๊ธ ์์ํ์! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.