Ver3.0
์ ์ฒด ์ด๋
Ver3.0 ๐ ๋ผ๋ผ๋ฒจ๋ก ๋ง๋๋ DAO ๊ธฐ์ฌ๋ ๋ฆฌ๋๋ณด๋ ์์คํ

๐ ๋ผ๋ผ๋ฒจ๋ก ๋ง๋๋ DAO ๊ธฐ์ฌ๋ ๋ฆฌ๋๋ณด๋ ์์คํ
๋ธ๋ก์ฒด์ธ๊ณผ ์น ๊ฐ๋ฐ์ ์๋ฒฝํ ๋ง๋จ! ์ค์ ํ๋ก์ ํธ๋ก ๋ฐฐ์ฐ๋ Web3 ๊ฐ๋ฐ ๊ฐ์ด๋ ๐
๐ฏ ์ DAO ๋ฆฌ๋๋ณด๋ ์์คํ ์ด ํ์ํ ๊น?
์๋ ! ์ค๋์ ์ ๋ง ์ฌ๋ฏธ์๋ ์ฃผ์ ๋ฅผ ๊ฐ์ง๊ณ ์์ด. ๋ฐ๋ก ๋ผ๋ผ๋ฒจ(Laravel)๋ก DAO ๊ธฐ์ฌ๋ ๋ฆฌ๋๋ณด๋ ์์คํ ์ ๋ง๋๋ ๋ฐฉ๋ฒ์ด์ผ! ๐์์ฆ Web3์ ๋ธ๋ก์ฒด์ธ ์ธ๊ณ์์ DAO(Decentralized Autonomous Organization, ํ์ค์ํ ์์จ ์กฐ์ง)๊ฐ ์์ฒญ ํซํ์์? ๊ทผ๋ฐ DAO๋ฅผ ์ด์ํ๋ค ๋ณด๋ฉด ๊ฐ์ฅ ํฐ ๊ณ ๋ฏผ์ด ๋ญ์ง ์์? ๋ฐ๋ก "๋๊ฐ ์ฐ๋ฆฌ ์ปค๋ฎค๋ํฐ์ ์ผ๋ง๋ ๊ธฐ์ฌํ๋์ง ์ด๋ป๊ฒ ์ธก์ ํ์ง?"๋ผ๋ ๊ฑฐ์ผ. ๐ค
์๊ฐํด๋ด. ์ด๋ค ์ฌ๋์ ์ฝ๋๋ฅผ ์ด์ฌํ ์ง๊ณ , ์ด๋ค ์ฌ๋์ ๋ง์ผํ ์ ํ๊ณ , ๋ ์ด๋ค ์ฌ๋์ ์ปค๋ฎค๋ํฐ ๊ด๋ฆฌ๋ฅผ ํด. ์ด ๋ชจ๋ ๊ธฐ์ฌ๋ฅผ ๊ณต์ ํ๊ฒ ํ๊ฐํ๊ณ ์๊ฐํํด์ ๋ณด์ฌ์ฃผ๋ ์์คํ ์ด ํ์ํ ๊ฑฐ์ง!
๐ก ์ค์ ์ฌ๋ก: Uniswap, Compound, MakerDAO ๊ฐ์ ์ ๋ช
DAO๋ค์ ๋ชจ๋ ๊ธฐ์ฌ๋ ์ถ์ ์์คํ
์ ๊ฐ์ง๊ณ ์์ด. ์ด๊ฒ ์์ด์ผ ๊ฑฐ๋ฒ๋์ค ํ ํฐ ๋ถ๋ฐฐ๋ ๊ณต์ ํ๊ฒ ํ ์ ์๊ฑฐ๋ !
๊ทธ๋์ ์ค๋์ ๋ผ๋ผ๋ฒจ์ด๋ผ๋ ๊ฐ๋ ฅํ PHP ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํด์ ์ด๋ฐ ์์คํ
์ ์ง์ ๋ง๋ค์ด๋ณผ ๊ฑฐ์ผ. ๋ผ๋ผ๋ฒจ์ ์ ํํ ์ด์ ? ๊ฐ๋จํด! ๋น ๋ฅด๊ณ , ์์ ์ ์ด๊ณ , ๊ฐ๋ฐํ๊ธฐ ํธํ๊ฑฐ๋ . ๐จ
๐ ๏ธ ํ๋ก์ ํธ ์ค๋นํ๊ธฐ: ๊ฐ๋ฐ ํ๊ฒฝ ์ธํ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์์ํด๋ณผ๊น? ๋จผ์ ๊ฐ๋ฐ ํ๊ฒฝ๋ถํฐ ์ธํ ํด์ผ ํด. ๊ฑฑ์ ๋ง! ํ๋ํ๋ ์ฐจ๊ทผ์ฐจ๊ทผ ์๋ ค์ค๊ฒ. ๐๐ฆ ํ์ํ ๋๊ตฌ๋ค
1. PHP 8.1 ์ด์
๋ผ๋ผ๋ฒจ 10์ ์ฌ์ฉํ ๊ฑฐ๋๊น PHP 8.1 ์ด์์ด ํ์ํด. ์ต์ ๋ฒ์ ์ผ์๋ก ์ฑ๋ฅ๋ ์ข๊ณ ๋ณด์๋ ๊ฐํ๋์ด ์์ด!
2. Composer
PHP์ ์์กด์ฑ ๊ด๋ฆฌ ๋๊ตฌ์ผ. npm์ด ์๋ฐ์คํฌ๋ฆฝํธ์ ํจํค์ง ๋งค๋์ ๋ผ๋ฉด, Composer๋ PHP์ ํจํค์ง ๋งค๋์ ์ง!
3. MySQL ๋๋ PostgreSQL
๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ํ์! ๋๋ MySQL์ ์ถ์ฒํ๋๋ฐ, ์ต์ํ ๊ฑธ ์ฐ๋ฉด ๋ผ.
4. Node.js & NPM
ํ๋ก ํธ์๋ ์์ฐ์ ์ปดํ์ผํ๋ ๋ฐ ํ์ํด. Vue.js๋ React๋ฅผ ์ธ ๊ฑฐ๋ผ๋ฉด ๋๋์ฑ ํ์ํ์ง!
5. Web3 ๋ผ์ด๋ธ๋ฌ๋ฆฌ
๋ธ๋ก์ฒด์ธ๊ณผ ํต์ ํ๋ ค๋ฉด Web3.php๋ ethers.js ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ํ์ํด. ๐
๋ผ๋ผ๋ฒจ 10์ ์ฌ์ฉํ ๊ฑฐ๋๊น PHP 8.1 ์ด์์ด ํ์ํด. ์ต์ ๋ฒ์ ์ผ์๋ก ์ฑ๋ฅ๋ ์ข๊ณ ๋ณด์๋ ๊ฐํ๋์ด ์์ด!
2. Composer
PHP์ ์์กด์ฑ ๊ด๋ฆฌ ๋๊ตฌ์ผ. npm์ด ์๋ฐ์คํฌ๋ฆฝํธ์ ํจํค์ง ๋งค๋์ ๋ผ๋ฉด, Composer๋ PHP์ ํจํค์ง ๋งค๋์ ์ง!
3. MySQL ๋๋ PostgreSQL
๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ํ์! ๋๋ MySQL์ ์ถ์ฒํ๋๋ฐ, ์ต์ํ ๊ฑธ ์ฐ๋ฉด ๋ผ.
4. Node.js & NPM
ํ๋ก ํธ์๋ ์์ฐ์ ์ปดํ์ผํ๋ ๋ฐ ํ์ํด. Vue.js๋ React๋ฅผ ์ธ ๊ฑฐ๋ผ๋ฉด ๋๋์ฑ ํ์ํ์ง!
5. Web3 ๋ผ์ด๋ธ๋ฌ๋ฆฌ
๋ธ๋ก์ฒด์ธ๊ณผ ํต์ ํ๋ ค๋ฉด Web3.php๋ ethers.js ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ํ์ํด. ๐
๐ ๋ผ๋ผ๋ฒจ ํ๋ก์ ํธ ์์ฑ
์, ์ด์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด์! ํฐ๋ฏธ๋์ ์ด๊ณ ๋ค์ ๋ช ๋ น์ด๋ฅผ ์ ๋ ฅํด:composer create-project laravel/laravel dao-leaderboard
cd dao-leaderboard
php artisan serve
์ง์! ๐ ์ด์ http://localhost:8000 ์ผ๋ก ์ ์ํ๋ฉด ๋ผ๋ผ๋ฒจ ํ์ ํ์ด์ง๊ฐ ๋ณด์ผ ๊ฑฐ์ผ.
๐ก Pro Tip: Laravel Sail์ ์ฌ์ฉํ๋ฉด Docker ๊ธฐ๋ฐ์ผ๋ก ๋ ์ฝ๊ฒ ๊ฐ๋ฐ ํ๊ฒฝ์ ๊ตฌ์ถํ ์ ์์ด. ํนํ ํ ํ๋ก์ ํธํ ๋ ํ๊ฒฝ ํต์ผ์ด ์ค์ํ๊ฑฐ๋ !
๐ Web3 ํจํค์ง ์ค์น
๋ธ๋ก์ฒด์ธ๊ณผ ํต์ ํ๋ ค๋ฉด Web3 ๊ด๋ จ ํจํค์ง๋ฅผ ์ค์นํด์ผ ํด:composer require web3p/web3.php
composer require kornrunner/keccak
npm install web3 ethers
์ด์ ์ค๋น ์๋ฃ! ๋ณธ๊ฒฉ์ ์ผ๋ก ์ฝ๋ฉ์ ์์ํ ์ ์์ด. ๐ช
๐๏ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ: ๊ธฐ์ฌ๋๋ฅผ ์ด๋ป๊ฒ ์ ์ฅํ ๊น?
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ๋ ์ ๋ง ์ค์ํด! ๋์ค์ ํ์ฅํ๊ธฐ ์ฝ๊ฒ, ๊ทธ๋ฆฌ๊ณ ์ฟผ๋ฆฌ ์ฑ๋ฅ๋ ์ข๊ฒ ๋ง๋ค์ด์ผ ํ๊ฑฐ๋ . ๐ฏ๐ ํต์ฌ ํ ์ด๋ธ ๊ตฌ์กฐ
์ฐ๋ฆฌ ์์คํ ์๋ ํฌ๊ฒ 4๊ฐ์ ํต์ฌ ํ ์ด๋ธ์ด ํ์ํด:
1๏ธโฃ users ํ
์ด๋ธ
DAO ๋ฉค๋ฒ๋ค์ ๊ธฐ๋ณธ ์ ๋ณด๋ฅผ ์ ์ฅํด. ์ง๊ฐ ์ฃผ์๊ฐ ๊ฐ์ฅ ์ค์ํ ์๋ณ์์ผ!
DAO ๋ฉค๋ฒ๋ค์ ๊ธฐ๋ณธ ์ ๋ณด๋ฅผ ์ ์ฅํด. ์ง๊ฐ ์ฃผ์๊ฐ ๊ฐ์ฅ ์ค์ํ ์๋ณ์์ผ!
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('wallet_address')->unique();
$table->string('username')->nullable();
$table->string('avatar_url')->nullable();
$table->integer('total_score')->default(0);
$table->integer('rank')->nullable();
$table->timestamps();
$table->index('wallet_address');
$table->index('total_score');
});
ํฌ์ธํธ: wallet_address์ total_score์ ์ธ๋ฑ์ค๋ฅผ ๊ฑธ์ด์คฌ์ด. ๋ฆฌ๋๋ณด๋ ์กฐํ๊ฐ ์์ฒญ ๋นจ๋ผ์ง ๊ฑฐ์ผ! โก
2๏ธโฃ contributions ํ
์ด๋ธ
์ค์ ๊ธฐ์ฌ ํ๋์ ๊ธฐ๋กํ๋ ํ ์ด๋ธ์ด์ผ. ๋ชจ๋ ํ๋์ด ์ฌ๊ธฐ ์์ฌ!
์ค์ ๊ธฐ์ฌ ํ๋์ ๊ธฐ๋กํ๋ ํ ์ด๋ธ์ด์ผ. ๋ชจ๋ ํ๋์ด ์ฌ๊ธฐ ์์ฌ!
Schema::create('contributions', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('contribution_type'); // 'code', 'governance', 'community' ๋ฑ
$table->text('description');
$table->integer('score');
$table->string('transaction_hash')->nullable();
$table->string('proof_url')->nullable(); // GitHub PR, ํฌ๋ผ ๋งํฌ ๋ฑ
$table->enum('status', ['pending', 'approved', 'rejected'])->default('pending');
$table->timestamp('verified_at')->nullable();
$table->timestamps();
$table->index(['user_id', 'created_at']);
$table->index('contribution_type');
});
์ฌ๊ธฐ์ ์ค์ํ ๊ฑด transaction_hash ํ๋์ผ. ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋ก๋ ํธ๋์ญ์
๊ณผ ์ฐ๊ฒฐํ ์ ์๊ฑฐ๋ ! ๐
3๏ธโฃ contribution_types ํ
์ด๋ธ
๊ธฐ์ฌ ์ ํ๋ณ๋ก ์ ์ ๊ฐ์ค์น๋ฅผ ๊ด๋ฆฌํด:
๊ธฐ์ฌ ์ ํ๋ณ๋ก ์ ์ ๊ฐ์ค์น๋ฅผ ๊ด๋ฆฌํด:
Schema::create('contribution_types', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('display_name');
$table->text('description');
$table->integer('base_score');
$table->decimal('multiplier', 3, 2)->default(1.00);
$table->boolean('requires_verification')->default(false);
$table->timestamps();
});
์๋ฅผ ๋ค์ด, ์ฝ๋ ๊ธฐ์ฌ๋ base_score๊ฐ 100์ด๊ณ , ์ปค๋ฎค๋ํฐ ํ๋์ 50์ผ ์ ์์ง. multiplier๋ก ์์ฆ๋ณ ๊ฐ์ค์น๋ ์กฐ์ ํ ์ ์์ด! ๐ฎ
4๏ธโฃ leaderboard_snapshots ํ
์ด๋ธ
์ฃผ๊ธฐ์ ์ผ๋ก ๋ฆฌ๋๋ณด๋ ์ค๋ ์ท์ ์ ์ฅํด. ํ์คํ ๋ฆฌ ์ถ์ ์ ์ ์ฉํด!
์ฃผ๊ธฐ์ ์ผ๋ก ๋ฆฌ๋๋ณด๋ ์ค๋ ์ท์ ์ ์ฅํด. ํ์คํ ๋ฆฌ ์ถ์ ์ ์ ์ฉํด!
Schema::create('leaderboard_snapshots', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->integer('rank');
$table->integer('score');
$table->string('period'); // 'daily', 'weekly', 'monthly', 'all-time'
$table->date('snapshot_date');
$table->timestamps();
$table->index(['period', 'snapshot_date']);
$table->unique(['user_id', 'period', 'snapshot_date']);
});
์ด๋ ๊ฒ ํ๋ฉด "์ง๋์ฃผ 1์๊ฐ ๋๊ตฌ์์ง?" ๊ฐ์ ์ง๋ฌธ์๋ ๋ฐ๋ก ๋ตํ ์ ์์ด! ๐
๐ ๋ง์ด๊ทธ๋ ์ด์ ์คํ
ํ ์ด๋ธ ์ค๊ณ๊ฐ ๋๋ฌ์ผ๋ฉด ๋ง์ด๊ทธ๋ ์ด์ ์ ์คํํด:php artisan migrate
๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํ
์ด๋ธ๋ค์ด ์ซ ์์ฑ๋ ๊ฑฐ์ผ! ๐
โ๏ธ ํต์ฌ ๋ก์ง ๊ตฌํ: ๊ธฐ์ฌ๋ ๊ณ์ฐ ์์ง
์, ์ด์ ๊ฐ์ฅ ์ค์ํ ๋ถ๋ถ์ด์ผ! ๊ธฐ์ฌ๋๋ฅผ ์ด๋ป๊ฒ ๊ณ์ฐํ๊ณ ์ ์๋ฅผ ๋งค๊ธธ ๊ฒ์ธ๊ฐ? ์ด๊ฒ ์์คํ ์ ํต์ฌ์ด๊ฑฐ๋ . ๐ง๐ฏ ๊ธฐ์ฌ๋ ๊ณ์ฐ ์ ๋ต
๊ธฐ์ฌ๋ ๊ณ์ฐ์ ๋จ์ํ "1๊ฐ ํ์ผ๋ 10์ " ์ด๋ฐ ์์ผ๋ก ํ๋ฉด ์ ๋ผ. ์ฌ๋ฌ ์์๋ฅผ ๊ณ ๋ คํด์ผ ํด:
๊ณ ๋ คํด์ผ ํ ์์๋ค:
1๏ธโฃ ๊ธฐ์ฌ ์ ํ: ์ฝ๋, ๊ฑฐ๋ฒ๋์ค ์ฐธ์ฌ, ์ปค๋ฎค๋ํฐ ํ๋ ๋ฑ
2๏ธโฃ ๋์ด๋: ์ฌ์ด ์์ vs ์ด๋ ค์ด ์์
3๏ธโฃ ์ํฅ๋ ฅ: ์ผ๋ง๋ ๋ง์ ์ฌ๋์๊ฒ ์ํฅ์ ๋ฏธ์ณค๋๊ฐ
4๏ธโฃ ์๊ฐ ๊ฐ์ค์น: ์ต๊ทผ ๊ธฐ์ฌ์ ๋ ๋์ ๊ฐ์ค์น
5๏ธโฃ ๊ฒ์ฆ ์ํ: ์ปค๋ฎค๋ํฐ ๊ฒ์ฆ์ ๊ฑฐ์ณค๋๊ฐ
6๏ธโฃ ์ฐ์์ฑ: ๊พธ์คํ ๊ธฐ์ฌํ๋ ์ฌ๋์๊ฒ ๋ณด๋์ค
2๏ธโฃ ๋์ด๋: ์ฌ์ด ์์ vs ์ด๋ ค์ด ์์
3๏ธโฃ ์ํฅ๋ ฅ: ์ผ๋ง๋ ๋ง์ ์ฌ๋์๊ฒ ์ํฅ์ ๋ฏธ์ณค๋๊ฐ
4๏ธโฃ ์๊ฐ ๊ฐ์ค์น: ์ต๊ทผ ๊ธฐ์ฌ์ ๋ ๋์ ๊ฐ์ค์น
5๏ธโฃ ๊ฒ์ฆ ์ํ: ์ปค๋ฎค๋ํฐ ๊ฒ์ฆ์ ๊ฑฐ์ณค๋๊ฐ
6๏ธโฃ ์ฐ์์ฑ: ๊พธ์คํ ๊ธฐ์ฌํ๋ ์ฌ๋์๊ฒ ๋ณด๋์ค
๐ป ContributionService ํด๋์ค ๋ง๋ค๊ธฐ
๋จผ์ ๊ธฐ์ฌ๋ ๊ณ์ฐ์ ๋ด๋นํ ์๋น์ค ํด๋์ค๋ฅผ ๋ง๋ค์ด๋ณด์:php artisan make:service ContributionService
app/Services/ContributionService.php ํ์ผ์ ์ด๊ณ ๋ค์ ์ฝ๋๋ฅผ ์์ฑํด:<?php
namespace App\Services;
use App\Models\Contribution;
use App\Models\User;
use App\Models\ContributionType;
use Carbon\Carbon;
class ContributionService
{
/**
* ๊ธฐ์ฌ๋ ์ ์ ๊ณ์ฐ
*/
public function calculateScore(Contribution $contribution): int
{
$type = ContributionType::where('name', $contribution->contribution_type)->first();
if (!$type) {
return 0;
}
$baseScore = $type->base_score;
$multiplier = $type->multiplier;
// ์๊ฐ ๊ฐ์ค์น ์ ์ฉ (์ต๊ทผ 30์ผ ๋ด ๊ธฐ์ฌ๋ 1.5๋ฐฐ)
$timeWeight = $this->calculateTimeWeight($contribution->created_at);
// ๊ฒ์ฆ ๋ณด๋์ค (๊ฒ์ฆ๋ ๊ธฐ์ฌ๋ 1.2๋ฐฐ)
$verificationBonus = $contribution->status === 'approved' ? 1.2 : 1.0;
// ์ต์ข
์ ์ ๊ณ์ฐ
$finalScore = $baseScore * $multiplier * $timeWeight * $verificationBonus;
return (int) round($finalScore);
}
/**
* ์๊ฐ ๊ฐ์ค์น ๊ณ์ฐ
*/
private function calculateTimeWeight(Carbon $contributionDate): float
{
$daysAgo = now()->diffInDays($contributionDate);
if ($daysAgo <= 30) {
return 1.5; // ์ต๊ทผ 30์ผ
} elseif ($daysAgo <= 90) {
return 1.2; // ์ต๊ทผ 90์ผ
} elseif ($daysAgo <= 180) {
return 1.0; // ์ต๊ทผ 180์ผ
} else {
return 0.8; // ๊ทธ ์ด์
}
}
/**
* ์ฌ์ฉ์ ์ด ์ ์ ์
๋ฐ์ดํธ
*/
public function updateUserTotalScore(User $user): void
{
$totalScore = $user->contributions()
->where('status', 'approved')
->sum('score');
$user->update(['total_score' => $totalScore]);
// ๋ญํน๋ ์
๋ฐ์ดํธ
$this->updateUserRank($user);
}
/**
* ์ฌ์ฉ์ ๋ญํน ์
๋ฐ์ดํธ
*/
private function updateUserRank(User $user): void
{
$rank = User::where('total_score', '>', $user->total_score)->count() + 1;
$user->update(['rank' => $rank]);
}
/**
* ์ฐ์ ๊ธฐ์ฌ ๋ณด๋์ค ๊ณ์ฐ
*/
public function calculateStreakBonus(User $user): int
{
$contributions = $user->contributions()
->where('status', 'approved')
->orderBy('created_at', 'desc')
->get();
$streak = 0;
$lastDate = null;
foreach ($contributions as $contribution) {
$currentDate = $contribution->created_at->startOfDay();
if ($lastDate === null) {
$streak = 1;
$lastDate = $currentDate;
continue;
}
$daysDiff = $lastDate->diffInDays($currentDate);
if ($daysDiff <= 1) {
$streak++;
$lastDate = $currentDate;
} else {
break;
}
}
// 7์ผ ์ฐ์: 50์ , 30์ผ ์ฐ์: 200์ , 90์ผ ์ฐ์: 500์
if ($streak >= 90) {
return 500;
} elseif ($streak >= 30) {
return 200;
} elseif ($streak >= 7) {
return 50;
}
return 0;
}
}
์! ์ฝ๋๊ฐ ์ข ๊ธธ์ง? ํ์ง๋ง ํ๋ํ๋ ๋ฏ์ด๋ณด๋ฉด ๋ณ๊ฑฐ ์์ด. ๐
๐ก ํต์ฌ ํฌ์ธํธ:
- calculateScore(): ์ฌ๋ฌ ๊ฐ์ค์น๋ฅผ ๊ณฑํด์ ์ต์ข
์ ์ ๊ณ์ฐ
- calculateTimeWeight(): ์ต๊ทผ ๊ธฐ์ฌ์ผ์๋ก ๋์ ๊ฐ์ค์น
- calculateStreakBonus(): ์ฐ์ ๊ธฐ์ฌ์ ๋ํ ๋ณด๋์ค ์ ์
์ด๋ฐ ์์ผ๋ก ๋ค์ํ ์์๋ฅผ ๊ณ ๋ คํ๋ฉด ๋ ๊ณต์ ํ ํ๊ฐ๊ฐ ๊ฐ๋ฅํด!
- calculateTimeWeight(): ์ต๊ทผ ๊ธฐ์ฌ์ผ์๋ก ๋์ ๊ฐ์ค์น
- calculateStreakBonus(): ์ฐ์ ๊ธฐ์ฌ์ ๋ํ ๋ณด๋์ค ์ ์
์ด๋ฐ ์์ผ๋ก ๋ค์ํ ์์๋ฅผ ๊ณ ๋ คํ๋ฉด ๋ ๊ณต์ ํ ํ๊ฐ๊ฐ ๊ฐ๋ฅํด!
๐ ๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ ๋๊ธฐํ
์ด์ ๋ธ๋ก์ฒด์ธ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ ๊ธฐ์ฌ๋๋ก ๋ณํํ๋ ๋ก์ง์ ๋ง๋ค์ด๋ณด์. Web3Service๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ:<?php
namespace App\Services;
use Web3\Web3;
use Web3\Contract;
use Web3\Utils;
class Web3Service
{
private $web3;
private $contract;
public function __construct()
{
// Infura๋ Alchemy ๊ฐ์ ๋
ธ๋ ํ๋ก๋ฐ์ด๋ ์ฌ์ฉ
$this->web3 = new Web3(env('WEB3_PROVIDER_URL'));
// DAO ์ค๋งํธ ์ปจํธ๋ํธ ์ฐ๊ฒฐ
$this->contract = new Contract(
$this->web3->provider,
file_get_contents(storage_path('contracts/DAO.abi.json'))
);
$this->contract->at(env('DAO_CONTRACT_ADDRESS'));
}
/**
* ํน์ ์ฃผ์์ ๊ฑฐ๋ฒ๋์ค ํฌํ ์ฐธ์ฌ ํ์ ์กฐํ
*/
public function getVoteCount(string $address): int
{
$voteCount = 0;
$this->contract->call('getVoteCount', $address, function ($err, $result) use (&$voteCount) {
if ($err !== null) {
throw new \Exception('Failed to get vote count: ' . $err->getMessage());
}
$voteCount = hexdec($result[0]->toString());
});
return $voteCount;
}
/**
* ํน์ ์ฃผ์์ ํ๋กํฌ์ ์ ์ ํ์ ์กฐํ
*/
public function getProposalCount(string $address): int
{
$proposalCount = 0;
$this->contract->call('getProposalCount', $address, function ($err, $result) use (&$proposalCount) {
if ($err !== null) {
throw new \Exception('Failed to get proposal count: ' . $err->getMessage());
}
$proposalCount = hexdec($result[0]->toString());
});
return $proposalCount;
}
/**
* ๋ธ๋ก์ฒด์ธ ์ด๋ฒคํธ ๋ก๊ทธ ํ์ฑ
*/
public function parseContributionEvents(string $fromBlock = 'latest'): array
{
$events = [];
$this->contract->eth->getLogs([
'fromBlock' => $fromBlock,
'toBlock' => 'latest',
'address' => env('DAO_CONTRACT_ADDRESS'),
'topics' => [
Utils::sha3('ContributionRecorded(address,string,uint256)')
]
], function ($err, $logs) use (&$events) {
if ($err !== null) {
throw new \Exception('Failed to get logs: ' . $err->getMessage());
}
foreach ($logs as $log) {
$events[] = $this->decodeContributionEvent($log);
}
});
return $events;
}
/**
* ์ด๋ฒคํธ ๋ก๊ทธ ๋์ฝ๋ฉ
*/
private function decodeContributionEvent($log): array
{
// ์ค์ ๋ก๋ ABI ๋์ฝ๋๋ฅผ ์ฌ์ฉํด์ผ ํจ
return [
'address' => '0x' . substr($log->topics[1], 26),
'contribution_type' => $this->hexToString($log->data),
'transaction_hash' => $log->transactionHash,
'block_number' => hexdec($log->blockNumber),
];
}
private function hexToString($hex): string
{
$string = '';
for ($i = 0; $i < strlen($hex) - 1; $i += 2) {
$string .= chr(hexdec($hex[$i] . $hex[$i + 1]));
}
return $string;
}
}
์ด ์๋น์ค๋ฅผ ์ฌ์ฉํ๋ฉด ๋ธ๋ก์ฒด์ธ์์ ์ค์๊ฐ์ผ๋ก ๊ธฐ์ฌ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ด! ๐
โ ๏ธ ์ฃผ์์ฌํญ: Web3 ํธ์ถ์ ๋น์ฉ์ด ๋ค ์ ์์ด. ํนํ ๋ฉ์ธ๋ท์์๋ ๊ฐ์ค๋น๊ฐ ๋ฐ์ํ๋๊น, ์ฝ๊ธฐ ์ ์ฉ ํธ์ถ๋ง ํ๊ฑฐ๋ ์บ์ฑ์ ์ ๊ทน ํ์ฉํด์ผ ํด!
๐จ API ์๋ํฌ์ธํธ ๊ตฌ์ถํ๊ธฐ
์ด์ ํ๋ก ํธ์๋์์ ์ฌ์ฉํ API๋ฅผ ๋ง๋ค์ด๋ณด์! RESTful API ์ค๊ณ ์์น์ ๋ฐ๋ฅผ ๊ฑฐ์ผ. ๐ก๐ฃ๏ธ ๋ผ์ฐํธ ์ ์
routes/api.php ํ์ผ์ ์ด๊ณ ๋ค์ ๋ผ์ฐํธ๋ค์ ์ถ๊ฐํด:<?php
use App\Http\Controllers\Api\LeaderboardController;
use App\Http\Controllers\Api\ContributionController;
use App\Http\Controllers\Api\UserController;
// ๋ฆฌ๋๋ณด๋ ๊ด๋ จ
Route::prefix('leaderboard')->group(function () {
Route::get('/', [LeaderboardController::class, 'index']);
Route::get('/top/{limit}', [LeaderboardController::class, 'top']);
Route::get('/period/{period}', [LeaderboardController::class, 'byPeriod']);
Route::get('/user/{address}', [LeaderboardController::class, 'userRank']);
});
// ๊ธฐ์ฌ ๊ด๋ จ
Route::prefix('contributions')->group(function () {
Route::get('/', [ContributionController::class, 'index']);
Route::post('/', [ContributionController::class, 'store']);
Route::get('/{id}', [ContributionController::class, 'show']);
Route::put('/{id}/verify', [ContributionController::class, 'verify']);
Route::get('/user/{address}', [ContributionController::class, 'userContributions']);
});
// ์ฌ์ฉ์ ๊ด๋ จ
Route::prefix('users')->group(function () {
Route::get('/{address}', [UserController::class, 'show']);
Route::get('/{address}/stats', [UserController::class, 'stats']);
Route::post('/sync', [UserController::class, 'syncFromBlockchain']);
});
๐ LeaderboardController ๊ตฌํ
๊ฐ์ฅ ์ค์ํ ๋ฆฌ๋๋ณด๋ ์ปจํธ๋กค๋ฌ๋ฅผ ๋ง๋ค์ด๋ณด์:<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\LeaderboardSnapshot;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class LeaderboardController extends Controller
{
/**
* ์ ์ฒด ๋ฆฌ๋๋ณด๋ ์กฐํ
*/
public function index(Request $request)
{
$perPage = $request->input('per_page', 50);
// ์บ์ฑ์ผ๋ก ์ฑ๋ฅ ์ต์ ํ (5๋ถ๊ฐ ์บ์)
$leaderboard = Cache::remember('leaderboard:all', 300, function () use ($perPage) {
return User::where('total_score', '>', 0)
->orderBy('total_score', 'desc')
->paginate($perPage);
});
return response()->json([
'success' => true,
'data' => $leaderboard->map(function ($user) {
return [
'rank' => $user->rank,
'wallet_address' => $user->wallet_address,
'username' => $user->username ?? 'Anonymous',
'avatar_url' => $user->avatar_url,
'total_score' => $user->total_score,
'contribution_count' => $user->contributions()->count(),
];
}),
'meta' => [
'current_page' => $leaderboard->currentPage(),
'last_page' => $leaderboard->lastPage(),
'per_page' => $leaderboard->perPage(),
'total' => $leaderboard->total(),
]
]);
}
/**
* ์์ N๋ช
์กฐํ
*/
public function top($limit = 10)
{
$limit = min($limit, 100); // ์ต๋ 100๋ช
๊น์ง
$topUsers = Cache::remember("leaderboard:top:{$limit}", 300, function () use ($limit) {
return User::where('total_score', '>', 0)
->orderBy('total_score', 'desc')
->limit($limit)
->get();
});
return response()->json([
'success' => true,
'data' => $topUsers->map(function ($user, $index) {
return [
'rank' => $index + 1,
'wallet_address' => $user->wallet_address,
'username' => $user->username ?? 'Anonymous',
'avatar_url' => $user->avatar_url,
'total_score' => $user->total_score,
'badge' => $this->getRankBadge($index + 1),
];
})
]);
}
/**
* ๊ธฐ๊ฐ๋ณ ๋ฆฌ๋๋ณด๋ ์กฐํ
*/
public function byPeriod($period)
{
$validPeriods = ['daily', 'weekly', 'monthly', 'all-time'];
if (!in_array($period, $validPeriods)) {
return response()->json([
'success' => false,
'message' => 'Invalid period. Use: daily, weekly, monthly, or all-time'
], 400);
}
$date = now()->toDateString();
$leaderboard = LeaderboardSnapshot::where('period', $period)
->where('snapshot_date', $date)
->orderBy('rank')
->get();
return response()->json([
'success' => true,
'period' => $period,
'date' => $date,
'data' => $leaderboard
]);
}
/**
* ํน์ ์ฌ์ฉ์ ์์ ์กฐํ
*/
public function userRank($address)
{
$user = User::where('wallet_address', $address)->first();
if (!$user) {
return response()->json([
'success' => false,
'message' => 'User not found'
], 404);
}
$totalUsers = User::where('total_score', '>', 0)->count();
$percentile = $totalUsers > 0 ? (($totalUsers - $user->rank + 1) / $totalUsers) * 100 : 0;
return response()->json([
'success' => true,
'data' => [
'rank' => $user->rank,
'total_score' => $user->total_score,
'percentile' => round($percentile, 2),
'total_users' => $totalUsers,
'badge' => $this->getRankBadge($user->rank),
]
]);
}
/**
* ์์์ ๋ฐ๋ฅธ ๋ฑ์ง ๋ถ์ฌ
*/
private function getRankBadge($rank): array
{
if ($rank === 1) {
return ['name' => 'Champion', 'emoji' => '๐', 'color' => '#FFD700'];
} elseif ($rank === 2) {
return ['name' => 'Elite', 'emoji' => '๐ฅ', 'color' => '#C0C0C0'];
} elseif ($rank === 3) {
return ['name' => 'Master', 'emoji' => '๐ฅ', 'color' => '#CD7F32'];
} elseif ($rank <= 10) {
return ['name' => 'Expert', 'emoji' => 'โญ', 'color' => '#4A90E2'];
} elseif ($rank <= 50) {
return ['name' => 'Advanced', 'emoji' => '๐', 'color' => '#50C878'];
} else {
return ['name' => 'Contributor', 'emoji' => '๐', 'color' => '#9B59B6'];
}
}
}
์์ฐ! ์ด์ ๋ฆฌ๋๋ณด๋ API๊ฐ ์์ฑ๋์ด! ๐
๐ก ์ฑ๋ฅ ์ต์ ํ ํ:
- Cache::remember()๋ฅผ ์ฌ์ฉํด์ 5๋ถ๊ฐ ๊ฒฐ๊ณผ๋ฅผ ์บ์ฑํ์ด. ๋ฆฌ๋๋ณด๋๋ ์ค์๊ฐ์ผ๋ก ๋ณํ ํ์๊ฐ ์์ผ๋๊น!
- ํ์ด์ง๋ค์ด์ ์ผ๋ก ํ ๋ฒ์ ๋๋ฌด ๋ง์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค์ง ์๋๋ก ํ์ด.
- ์ธ๋ฑ์ค๋ฅผ ์ ํ์ฉํ๋ฉด ์ฟผ๋ฆฌ ์๋๊ฐ ์์ฒญ ๋นจ๋ผ์ ธ!
- ํ์ด์ง๋ค์ด์ ์ผ๋ก ํ ๋ฒ์ ๋๋ฌด ๋ง์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค์ง ์๋๋ก ํ์ด.
- ์ธ๋ฑ์ค๋ฅผ ์ ํ์ฉํ๋ฉด ์ฟผ๋ฆฌ ์๋๊ฐ ์์ฒญ ๋นจ๋ผ์ ธ!
๐ ContributionController ๊ตฌํ
๊ธฐ์ฌ๋ฅผ ๋ฑ๋กํ๊ณ ๊ฒ์ฆํ๋ ์ปจํธ๋กค๋ฌ๋ ๋ง๋ค์ด๋ณด์:<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Contribution;
use App\Models\User;
use App\Services\ContributionService;
use App\Services\Web3Service;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class ContributionController extends Controller
{
private $contributionService;
private $web3Service;
public function __construct(ContributionService $contributionService, Web3Service $web3Service)
{
$this->contributionService = $contributionService;
$this->web3Service = $web3Service;
}
/**
* ๊ธฐ์ฌ ๋ฑ๋ก
*/
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'wallet_address' => 'required|string',
'contribution_type' => 'required|string',
'description' => 'required|string|max:1000',
'proof_url' => 'nullable|url',
'transaction_hash' => 'nullable|string',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors()
], 422);
}
// ์ฌ์ฉ์ ์ฐพ๊ธฐ ๋๋ ์์ฑ
$user = User::firstOrCreate(
['wallet_address' => $request->wallet_address],
['username' => null, 'total_score' => 0]
);
// ๊ธฐ์ฌ ์์ฑ
$contribution = Contribution::create([
'user_id' => $user->id,
'contribution_type' => $request->contribution_type,
'description' => $request->description,
'proof_url' => $request->proof_url,
'transaction_hash' => $request->transaction_hash,
'status' => 'pending',
'score' => 0, // ๊ฒ์ฆ ํ ๊ณ์ฐ
]);
return response()->json([
'success' => true,
'message' => 'Contribution submitted successfully',
'data' => $contribution
], 201);
}
/**
* ๊ธฐ์ฌ ๊ฒ์ฆ (๊ด๋ฆฌ์์ฉ)
*/
public function verify(Request $request, $id)
{
$contribution = Contribution::findOrFail($id);
$validator = Validator::make($request->all(), [
'status' => 'required|in:approved,rejected',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors()
], 422);
}
$contribution->status = $request->status;
if ($request->status === 'approved') {
// ์ ์ ๊ณ์ฐ
$score = $this->contributionService->calculateScore($contribution);
$contribution->score = $score;
$contribution->verified_at = now();
// ์ฌ์ฉ์ ์ด์ ์
๋ฐ์ดํธ
$this->contributionService->updateUserTotalScore($contribution->user);
}
$contribution->save();
return response()->json([
'success' => true,
'message' => 'Contribution verified successfully',
'data' => $contribution
]);
}
/**
* ์ฌ์ฉ์๋ณ ๊ธฐ์ฌ ๋ชฉ๋ก
*/
public function userContributions($address)
{
$user = User::where('wallet_address', $address)->first();
if (!$user) {
return response()->json([
'success' => false,
'message' => 'User not found'
], 404);
}
$contributions = $user->contributions()
->orderBy('created_at', 'desc')
->paginate(20);
return response()->json([
'success' => true,
'data' => $contributions
]);
}
}
์๋ฒฝํด! ์ด์ ๊ธฐ์ฌ๋ฅผ ๋ฑ๋กํ๊ณ , ๊ฒ์ฆํ๊ณ , ์กฐํํ ์ ์์ด! ๐ช
๐ ์ค์๊ฐ ์ ๋ฐ์ดํธ: WebSocket๊ณผ ์ด๋ฒคํธ ๋ธ๋ก๋์บ์คํ
๋ฆฌ๋๋ณด๋๊ฐ ์ค์๊ฐ์ผ๋ก ์ ๋ฐ์ดํธ๋๋ฉด ๋ ์ฌ๋ฏธ์๊ฒ ์ง? Laravel Echo์ Pusher๋ฅผ ์ฌ์ฉํด์ ์ค์๊ฐ ๊ธฐ๋ฅ์ ์ถ๊ฐํด๋ณด์! โก๐ก Laravel Broadcasting ์ค์
๋จผ์ ํ์ํ ํจํค์ง๋ฅผ ์ค์นํด:composer require pusher/pusher-php-server
npm install --save-dev laravel-echo pusher-js
config/broadcasting.php์์ Pusher ์ค์ ์ ํ์ธํ๊ณ , .env ํ์ผ์ Pusher ํค๋ฅผ ์ถ๊ฐํด:BROADCAST_DRIVER=pusher
PUSHER_APP_ID=your-app-id
PUSHER_APP_KEY=your-app-key
PUSHER_APP_SECRET=your-app-secret
PUSHER_APP_CLUSTER=your-cluster
๐ข ์ด๋ฒคํธ ์์ฑ
๋ฆฌ๋๋ณด๋๊ฐ ์ ๋ฐ์ดํธ๋ ๋ ๋ธ๋ก๋์บ์คํธํ ์ด๋ฒคํธ๋ฅผ ๋ง๋ค์ด:php artisan make:event LeaderboardUpdated
app/Events/LeaderboardUpdated.php:<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class LeaderboardUpdated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $topUsers;
public $updatedUser;
public function __construct($topUsers, $updatedUser = null)
{
$this->topUsers = $topUsers;
$this->updatedUser = $updatedUser;
}
public function broadcastOn()
{
return new Channel('leaderboard');
}
public function broadcastAs()
{
return 'updated';
}
public function broadcastWith()
{
return [
'top_users' => $this->topUsers,
'updated_user' => $this->updatedUser,
'timestamp' => now()->toIso8601String(),
];
}
}
๐ฏ ์ด๋ฒคํธ ๋ฐ์์ํค๊ธฐ
๊ธฐ์ฌ๊ฐ ๊ฒ์ฆ๋ ๋ ์ด๋ฒคํธ๋ฅผ ๋ฐ์์์ผ:// ContributionController์ verify ๋ฉ์๋์ ์ถ๊ฐ
if ($request->status === 'approved') {
// ... ์ ์ ๊ณ์ฐ ๋ก์ง ...
// ์์ 10๋ช
์กฐํ
$topUsers = User::orderBy('total_score', 'desc')->limit(10)->get();
// ์ด๋ฒคํธ ๋ธ๋ก๋์บ์คํธ
event(new LeaderboardUpdated($topUsers, $contribution->user));
}
๐จ ํ๋ก ํธ์๋์์ ์ค์๊ฐ ์์
Vue.js ์์ ๋ก ์ค์๊ฐ ์ ๋ฐ์ดํธ๋ฅผ ๋ฐ์๋ณด์:// resources/js/app.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,
forceTLS: true
});
// Leaderboard.vue
export default {
data() {
return {
leaderboard: [],
isLive: true
}
},
mounted() {
this.fetchLeaderboard();
this.listenForUpdates();
},
methods: {
fetchLeaderboard() {
axios.get('/api/leaderboard/top/10')
.then(response => {
this.leaderboard = response.data.data;
});
},
listenForUpdates() {
window.Echo.channel('leaderboard')
.listen('.updated', (e) => {
console.log('Leaderboard updated!', e);
// ์ ๋๋ฉ์ด์
๊ณผ ํจ๊ป ์
๋ฐ์ดํธ
this.animateUpdate(e.top_users);
// ์๋ฆผ ํ์
if (e.updated_user) {
this.showNotification(e.updated_user);
}
});
},
animateUpdate(newData) {
// ๋ถ๋๋ฌ์ด ์ ํ ํจ๊ณผ
this.leaderboard = newData;
},
showNotification(user) {
this.$notify({
title: '๐ New Contribution!',
message: `${user.username || 'Anonymous'} just earned points!`,
type: 'success'
});
}
}
}
์ด์ ๋๊ตฐ๊ฐ ๊ธฐ์ฌ๋ฅผ ํ๋ฉด ๋ชจ๋ ์ฌ์ฉ์์ ํ๋ฉด์ ์ค์๊ฐ์ผ๋ก ๋ฐ์๋ผ! ์ ๋ง ๋ฉ์ง์ง ์์? ๐
๐ฎ ๊ฒ์ด๋ฏธํผ์ผ์ด์ : ๋ ์ฌ๋ฏธ์๊ฒ ๋ง๋ค๊ธฐ
๋ฆฌ๋๋ณด๋๋ง ์์ผ๋ฉด ์ข ์ฌ์ฌํ์์? ๊ฒ์ด๋ฏธํผ์ผ์ด์ ์์๋ฅผ ์ถ๊ฐํด์ ์ฌ์ฉ์๋ค์ด ๋ ์ ๊ทน์ ์ผ๋ก ์ฐธ์ฌํ๊ฒ ๋ง๋ค์ด๋ณด์! ๐ฏ๐ ์ ์ (Achievement) ์์คํ
์ฌ์ฉ์๊ฐ ํน์ ๋ชฉํ๋ฅผ ๋ฌ์ฑํ๋ฉด ์ ์ ์ ๋ถ์ฌํ๋ ์์คํ ์ ๋ง๋ค์ด๋ณด์:php artisan make:model Achievement -m
๋ง์ด๊ทธ๋ ์ด์
ํ์ผ:Schema::create('achievements', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('display_name');
$table->text('description');
$table->string('icon'); // ์ด๋ชจ์ง๋ ์ด๋ฏธ์ง URL
$table->integer('points');
$table->json('criteria'); // ๋ฌ์ฑ ์กฐ๊ฑด
$table->enum('rarity', ['common', 'rare', 'epic', 'legendary']);
$table->timestamps();
});
Schema::create('user_achievements', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('achievement_id')->constrained()->onDelete('cascade');
$table->timestamp('unlocked_at');
$table->timestamps();
$table->unique(['user_id', 'achievement_id']);
});
๐๏ธ ์ ์ ์ ์ํ๊ธฐ
์๋(Seeder)๋ก ๊ธฐ๋ณธ ์ ์ ๋ค์ ๋ง๋ค์ด:<?php
namespace Database\Seeders;
use App\Models\Achievement;
use Illuminate\Database\Seeder;
class AchievementSeeder extends Seeder
{
public function run()
{
$achievements = [
[
'name' => 'first_contribution',
'display_name' => 'First Steps',
'description' => '์ฒซ ๋ฒ์งธ ๊ธฐ์ฌ๋ฅผ ์๋ฃํ์ต๋๋ค!',
'icon' => '๐ฑ',
'points' => 50,
'criteria' => json_encode(['contribution_count' => 1]),
'rarity' => 'common',
],
[
'name' => 'week_streak',
'display_name' => 'Dedicated',
'description' => '7์ผ ์ฐ์ ๊ธฐ์ฌํ์ต๋๋ค!',
'icon' => '๐ฅ',
'points' => 200,
'criteria' => json_encode(['streak_days' => 7]),
'rarity' => 'rare',
],
[
'name' => 'code_master',
'display_name' => 'Code Master',
'description' => '์ฝ๋ ๊ธฐ์ฌ 50๊ฐ๋ฅผ ๋ฌ์ฑํ์ต๋๋ค!',
'icon' => '๐ป',
'points' => 500,
'criteria' => json_encode(['contribution_type' => 'code', 'count' => 50]),
'rarity' => 'epic',
],
[
'name' => 'top_ten',
'display_name' => 'Elite Club',
'description' => '๋ฆฌ๋๋ณด๋ ์์ 10์์ ์ง์
ํ์ต๋๋ค!',
'icon' => 'โญ',
'points' => 1000,
'criteria' => json_encode(['rank' => 10]),
'rarity' => 'epic',
],
[
'name' => 'champion',
'display_name' => 'Champion',
'description' => '๋ฆฌ๋๋ณด๋ 1์๋ฅผ ๋ฌ์ฑํ์ต๋๋ค!',
'icon' => '๐',
'points' => 5000,
'criteria' => json_encode(['rank' => 1]),
'rarity' => 'legendary',
],
[
'name' => 'governance_guru',
'display_name' => 'Governance Guru',
'description' => '๊ฑฐ๋ฒ๋์ค ํฌํ์ 100๋ฒ ์ฐธ์ฌํ์ต๋๋ค!',
'icon' => '๐ณ๏ธ',
'points' => 800,
'criteria' => json_encode(['contribution_type' => 'governance', 'count' => 100]),
'rarity' => 'epic',
],
];
foreach ($achievements as $achievement) {
Achievement::create($achievement);
}
}
}
๐ฏ ์ ์ ์ฒดํฌ ์๋น์ค
์ฌ์ฉ์๊ฐ ์ ์ ์ ๋ฌ์ฑํ๋์ง ์๋์ผ๋ก ์ฒดํฌํ๋ ์๋น์ค:<?php
namespace App\Services;
use App\Models\User;
use App\Models\Achievement;
use App\Events\AchievementUnlocked;
class AchievementService
{
/**
* ์ฌ์ฉ์์ ์
์ ๋ฌ์ฑ ์ฌ๋ถ ์ฒดํฌ
*/
public function checkAchievements(User $user): array
{
$unlockedAchievements = [];
$achievements = Achievement::all();
foreach ($achievements as $achievement) {
// ์ด๋ฏธ ํ๋ํ ์
์ ์ ์คํต
if ($user->achievements()->where('achievement_id', $achievement->id)->exists()) {
continue;
}
// ๋ฌ์ฑ ์กฐ๊ฑด ์ฒดํฌ
if ($this->checkCriteria($user, $achievement)) {
$this->unlockAchievement($user, $achievement);
$unlockedAchievements[] = $achievement;
}
}
return $unlockedAchievements;
}
/**
* ๋ฌ์ฑ ์กฐ๊ฑด ์ฒดํฌ
*/
private function checkCriteria(User $user, Achievement $achievement): bool
{
$criteria = json_decode($achievement->criteria, true);
// ๊ธฐ์ฌ ํ์ ์ฒดํฌ
if (isset($criteria['contribution_count'])) {
$count = $user->contributions()->where('status', 'approved')->count();
if ($count < $criteria['contribution_count']) {
return false;
}
}
// ์ฐ์ ๊ธฐ์ฌ ์ผ์ ์ฒดํฌ
if (isset($criteria['streak_days'])) {
$streak = app(ContributionService::class)->calculateStreakBonus($user);
if ($streak < $criteria['streak_days']) {
return false;
}
}
// ํน์ ์ ํ ๊ธฐ์ฌ ํ์ ์ฒดํฌ
if (isset($criteria['contribution_type']) && isset($criteria['count'])) {
$count = $user->contributions()
->where('contribution_type', $criteria['contribution_type'])
->where('status', 'approved')
->count();
if ($count < $criteria['count']) {
return false;
}
}
// ์์ ์ฒดํฌ
if (isset($criteria['rank'])) {
if ($user->rank > $criteria['rank']) {
return false;
}
}
return true;
}
/**
* ์
์ ์ ๊ธ ํด์
*/
private function unlockAchievement(User $user, Achievement $achievement): void
{
$user->achievements()->attach($achievement->id, [
'unlocked_at' => now()
]);
// ์
์ ํฌ์ธํธ๋ฅผ ์ด์ ์ ์ถ๊ฐ
$user->increment('total_score', $achievement->points);
// ์ด๋ฒคํธ ๋ฐ์
event(new AchievementUnlocked($user, $achievement));
}
}
๐ก ์๋ํ ํ: ๊ธฐ์ฌ๊ฐ ์น์ธ๋ ๋๋ง๋ค ์๋์ผ๋ก ์
์ ์ ์ฒดํฌํ๋๋ก Observer ํจํด์ ์ฌ์ฉํ๋ฉด ์ข์!
// ContributionObserver.php
public function updated(Contribution $contribution)
{
if ($contribution->status === 'approved') {
app(AchievementService::class)->checkAchievements($contribution->user);
}
}
๐ ๋ ๋ฒจ ์์คํ
์ ์์ ๋ฐ๋ผ ๋ ๋ฒจ์ด ์ฌ๋ผ๊ฐ๋ ์์คํ ๋ ์ถ๊ฐํด๋ณด์:<?php
namespace App\Services;
class LevelService
{
private $levels = [
1 => ['min' => 0, 'max' => 100, 'title' => 'Newbie', 'color' => '#95a5a6'],
2 => ['min' => 101, 'max' => 500, 'title' => 'Beginner', 'color' => '#3498db'],
3 => ['min' => 501, 'max' => 1500, 'title' => 'Intermediate', 'color' => '#2ecc71'],
4 => ['min' => 1501, 'max' => 3000, 'title' => 'Advanced', 'color' => '#f39c12'],
5 => ['min' => 3001, 'max' => 5000, 'title' => 'Expert', 'color' => '#e74c3c'],
6 => ['min' => 5001, 'max' => 10000, 'title' => 'Master', 'color' => '#9b59b6'],
7 => ['min' => 10001, 'max' => 20000, 'title' => 'Grandmaster', 'color' => '#e91e63'],
8 => ['min' => 20001, 'max' => PHP_INT_MAX, 'title' => 'Legend', 'color' => '#ffd700'],
];
public function getUserLevel(int $score): array
{
foreach ($this->levels as $level => $data) {
if ($score >= $data['min'] && $score <= $data['max']) {
$progress = $this->calculateProgress($score, $data['min'], $data['max']);
return [
'level' => $level,
'title' => $data['title'],
'color' => $data['color'],
'current_score' => $score,
'min_score' => $data['min'],
'max_score' => $data['max'],
'progress' => $progress,
'next_level' => $level < 8 ? $this->levels[$level + 1]['title'] : null,
];
}
}
return $this->levels[1];
}
private function calculateProgress(int $current, int $min, int $max): float
{
if ($max === PHP_INT_MAX) {
return 100.0;
}
$range = $max - $min;
$progress = (($current - $min) / $range) * 100;
return round($progress, 2);
}
}
์ด์ ์ฌ์ฉ์ ํ๋กํ์ ๋ ๋ฒจ๊ณผ ์งํ๋๋ฅผ ํ์ํ ์ ์์ด! ๐ฎ
๐ ๋ณด์๊ณผ ๊ฒ์ฆ: ๋ถ์ ํ์ ๋ฐฉ์งํ๊ธฐ
๋ฆฌ๋๋ณด๋ ์์คํ ์์ ๊ฐ์ฅ ์ค์ํ ๊ฑด ๊ณต์ ์ฑ์ด์ผ. ๋ถ์ ํ์๋ฅผ ๋ฐฉ์งํ๋ ๋ฐฉ๋ฒ๋ค์ ์์๋ณด์! ๐ก๏ธ๐ ๋ธ๋ก์ฒด์ธ ๊ฒ์ฆ
๊ธฐ์ฌ๊ฐ ์ค์ ๋ก ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋ก๋์๋์ง ๊ฒ์ฆํ๋ ๋ก์ง:<?php
namespace App\Services;
class BlockchainVerificationService
{
private $web3Service;
public function __construct(Web3Service $web3Service)
{
$this->web3Service = $web3Service;
}
/**
* ํธ๋์ญ์
๊ฒ์ฆ
*/
public function verifyTransaction(string $txHash, string $expectedAddress): bool
{
try {
$transaction = $this->web3Service->getTransaction($txHash);
if (!$transaction) {
return false;
}
// ๋ฐ์ ์ ์ฃผ์ ํ์ธ
if (strtolower($transaction['from']) !== strtolower($expectedAddress)) {
return false;
}
// ํธ๋์ญ์
์ด ์ฑ๊ณตํ๋์ง ํ์ธ
$receipt = $this->web3Service->getTransactionReceipt($txHash);
if (!$receipt || $receipt['status'] !== '0x1') {
return false;
}
// ์ปจํธ๋ํธ ์ฃผ์ ํ์ธ
if (strtolower($receipt['to']) !== strtolower(env('DAO_CONTRACT_ADDRESS'))) {
return false;
}
return true;
} catch (\Exception $e) {
\Log::error('Transaction verification failed: ' . $e->getMessage());
return false;
}
}
/**
* ์๋ช
๊ฒ์ฆ
*/
public function verifySignature(string $message, string $signature, string $address): bool
{
try {
$recoveredAddress = $this->web3Service->recoverAddress($message, $signature);
return strtolower($recoveredAddress) === strtolower($address);
} catch (\Exception $e) {
\Log::error('Signature verification failed: ' . $e->getMessage());
return false;
}
}
}
โ ๏ธ Rate Limiting
์คํธ ๋ฐฉ์ง๋ฅผ ์ํ ์๋ ์ ํ:// routes/api.php
Route::middleware(['throttle:contributions'])->group(function () {
Route::post('/contributions', [ContributionController::class, 'store']);
});
// app/Providers/RouteServiceProvider.php
protected function configureRateLimiting()
{
RateLimiter::for('contributions', function (Request $request) {
return Limit::perHour(10)->by($request->ip());
});
}
๐ค ์ด์ ํจํด ๊ฐ์ง
๋น์ ์์ ์ธ ํ๋์ ๊ฐ์งํ๋ ์์คํ :<?php
namespace App\Services;
use App\Models\User;
use App\Models\Contribution;
class FraudDetectionService
{
/**
* ์์ฌ์ค๋ฌ์ด ํ๋ ๊ฐ์ง
*/
public function detectSuspiciousActivity(User $user): array
{
$flags = [];
// 1. ์งง์ ์๊ฐ์ ๋๋ฌด ๋ง์ ๊ธฐ์ฌ
$recentContributions = $user->contributions()
->where('created_at', '>=', now()->subHour())
->count();
if ($recentContributions > 5) {
$flags[] = [
'type' => 'high_frequency',
'severity' => 'medium',
'message' => '1์๊ฐ ๋ด 5๊ฐ ์ด์์ ๊ธฐ์ฌ'
];
}
// 2. ๋น์ ์์ ์ผ๋ก ๋์ ์ ์
$avgScore = Contribution::where('status', 'approved')->avg('score');
$userAvgScore = $user->contributions()
->where('status', 'approved')
->avg('score');
if ($userAvgScore > $avgScore * 2) {
$flags[] = [
'type' => 'abnormal_score',
'severity' => 'high',
'message' => 'ํ๊ท ๋ณด๋ค 2๋ฐฐ ์ด์ ๋์ ์ ์'
];
}
// 3. ๊ฒ์ฆ๋์ง ์์ ๊ธฐ์ฌ๊ฐ ๋๋ฌด ๋ง์
$pendingRatio = $user->contributions()
->where('status', 'pending')
->count() / max($user->contributions()->count(), 1);
if ($pendingRatio > 0.7) {
$flags[] = [
'type' => 'high_pending_ratio',
'severity' => 'low',
'message' => '๊ฒ์ฆ ๋๊ธฐ ์ค์ธ ๊ธฐ์ฌ๊ฐ 70% ์ด์'
];
}
// 4. ๋์ผํ proof_url ๋ฐ๋ณต ์ฌ์ฉ
$duplicateProofs = $user->contributions()
->select('proof_url')
->groupBy('proof_url')
->havingRaw('COUNT(*) > 1')
->count();
if ($duplicateProofs > 0) {
$flags[] = [
'type' => 'duplicate_proof',
'severity' => 'high',
'message' => '๋์ผํ ์ฆ๊ฑฐ URL ๋ฐ๋ณต ์ฌ์ฉ'
];
}
return $flags;
}
/**
* ์๋ ์กฐ์น
*/
public function takeAction(User $user, array $flags): void
{
$highSeverityCount = collect($flags)->where('severity', 'high')->count();
if ($highSeverityCount >= 2) {
// ๊ณ์ ์ผ์ ์ ์ง
$user->update(['is_suspended' => true]);
// ๊ด๋ฆฌ์์๊ฒ ์๋ฆผ
\Notification::route('mail', env('ADMIN_EMAIL'))
->notify(new SuspiciousActivityDetected($user, $flags));
}
}
}
โ ๏ธ ์ค์: ๋ถ์ ํ์ ๊ฐ์ง ์์คํ
์ false positive(์คํ)๊ฐ ๋ฐ์ํ ์ ์์ด. ์๋์ผ๋ก ๊ณ์ ์ ์ ์งํ๊ธฐ๋ณด๋ค๋ ๊ด๋ฆฌ์์๊ฒ ์๋ฆผ์ ๋ณด๋ด๊ณ ์๋์ผ๋ก ๊ฒํ ํ๋ ๊ฒ ์์ ํด!
๐ ๋ถ์๊ณผ ์ธ์ฌ์ดํธ: ๋ฐ์ดํฐ ์๊ฐํ
๋ฆฌ๋๋ณด๋ ๋ฐ์ดํฐ๋ฅผ ๋ถ์ํด์ ์ ์ฉํ ์ธ์ฌ์ดํธ๋ฅผ ์ ๊ณตํด๋ณด์! ๐๐ ํต๊ณ API ๋ง๋ค๊ธฐ
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Contribution;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class AnalyticsController extends Controller
{
/**
* ์ ์ฒด ํต๊ณ
*/
public function overview()
{
$stats = [
'total_users' => User::count(),
'active_users' => User::whereHas('contributions', function ($query) {
$query->where('created_at', '>=', now()->subDays(30));
})->count(),
'total_contributions' => Contribution::where('status', 'approved')->count(),
'total_score' => User::sum('total_score'),
'avg_score_per_user' => round(User::avg('total_score'), 2),
];
return response()->json([
'success' => true,
'data' => $stats
]);
}
/**
* ๊ธฐ์ฌ ํธ๋ ๋ (์ผ๋ณ)
*/
public function contributionTrend($days = 30)
{
$trend = Contribution::where('status', 'approved')
->where('created_at', '>=', now()->subDays($days))
->select(
DB::raw('DATE(created_at) as date'),
DB::raw('COUNT(*) as count'),
DB::raw('SUM(score) as total_score')
)
->groupBy('date')
->orderBy('date')
->get();
return response()->json([
'success' => true,
'period' => $days . ' days',
'data' => $trend
]);
}
/**
* ๊ธฐ์ฌ ์ ํ๋ณ ๋ถํฌ
*/
public function contributionTypeDistribution()
{
$distribution = Contribution::where('status', 'approved')
->select('contribution_type', DB::raw('COUNT(*) as count'), DB::raw('SUM(score) as total_score'))
->groupBy('contribution_type')
->get();
return response()->json([
'success' => true,
'data' => $distribution
]);
}
/**
* ์์ ๊ธฐ์ฌ์ ์ฑ์ฅ ์ถ์ด
*/
public function topContributorsGrowth()
{
$topUsers = User::orderBy('total_score', 'desc')->limit(10)->get();
$growth = $topUsers->map(function ($user) {
$monthlyScores = [];
for ($i = 5; $i >= 0; $i--) {
$month = now()->subMonths($i);
$score = $user->contributions()
->where('status', 'approved')
->whereYear('created_at', $month->year)
->whereMonth('created_at', $month->month)
->sum('score');
$monthlyScores[] = [
'month' => $month->format('Y-m'),
'score' => $score
];
}
return [
'user' => [
'wallet_address' => $user->wallet_address,
'username' => $user->username,
'total_score' => $user->total_score,
],
'monthly_scores' => $monthlyScores
];
});
return response()->json([
'success' => true,
'data' => $growth
]);
}
}
๐ ๋์๋ณด๋ ์ปดํฌ๋ํธ
Vue.js๋ก ๋ฉ์ง ์ฐจํธ๋ฅผ ๋ง๋ค์ด๋ณด์ (Chart.js ์ฌ์ฉ):<template>
<div class="analytics-dashboard">
<div class="stats-grid">
<div class="stat-card">
<h3>์ด ์ฌ์ฉ์</h3>
<p class="stat-value">{{ stats.total_users }}</p>
</div>
<div class="stat-card">
<h3>ํ์ฑ ์ฌ์ฉ์</h3>
<p class="stat-value">{{ stats.active_users }}</p>
</div>
<div class="stat-card">
<h3>์ด ๊ธฐ์ฌ</h3>
<p class="stat-value">{{ stats.total_contributions }}</p>
</div>
<div class="stat-card">
<h3>ํ๊ท ์ ์</h3>
<p class="stat-value">{{ stats.avg_score_per_user }}</p>
</div>
</div>
<div class="chart-container">
<h2>๊ธฐ์ฌ ํธ๋ ๋</h2>
<canvas ref="trendChart"></canvas>
</div>
<div class="chart-container">
<h2>๊ธฐ์ฌ ์ ํ๋ณ ๋ถํฌ</h2>
<canvas ref="distributionChart"></canvas>
</div>
</div>
</template>
<script>
import Chart from 'chart.js/auto';
export default {
data() {
return {
stats: {},
trendChart: null,
distributionChart: null
}
},
mounted() {
this.fetchStats();
this.fetchTrend();
this.fetchDistribution();
},
methods: {
async fetchStats() {
const response = await axios.get('/api/analytics/overview');
this.stats = response.data.data;
},
async fetchTrend() {
const response = await axios.get('/api/analytics/contribution-trend/30');
const data = response.data.data;
this.trendChart = new Chart(this.$refs.trendChart, {
type: 'line',
data: {
labels: data.map(d => d.date),
datasets: [{
label: '์ผ๋ณ ๊ธฐ์ฌ ์',
data: data.map(d => d.count),
borderColor: '#6c5ce7',
backgroundColor: 'rgba(108, 92, 231, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
plugins: {
legend: {
display: true
}
}
}
});
},
async fetchDistribution() {
const response = await axios.get('/api/analytics/contribution-type-distribution');
const data = response.data.data;
this.distributionChart = new Chart(this.$refs.distributionChart, {
type: 'doughnut',
data: {
labels: data.map(d => d.contribution_type),
datasets: [{
data: data.map(d => d.count),
backgroundColor: [
'#6c5ce7',
'#00b894',
'#fd79a8',
'#fdcb6e',
'#74b9ff'
]
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
}
}
</script>
์ด์ ๋ฉ์ง ๋์๋ณด๋๊ฐ ์์ฑ๋์ด! ๐โจ
๐ ์ฑ๋ฅ ์ต์ ํ: ๋น ๋ฅด๊ฒ ๋ง๋ค๊ธฐ
์ฌ์ฉ์๊ฐ ๋ง์์ง๋ฉด ์ฑ๋ฅ์ด ์ค์ํด์ ธ. ์ต์ ํ ๊ธฐ๋ฒ๋ค์ ์์๋ณด์! โก๐พ ์บ์ฑ ์ ๋ต
์บ์ฑ์ด ํ์ํ ๋ฐ์ดํฐ:
1๏ธโฃ ๋ฆฌ๋๋ณด๋ (5๋ถ๋ง๋ค ๊ฐฑ์ )
2๏ธโฃ ์ฌ์ฉ์ ํต๊ณ (10๋ถ๋ง๋ค ๊ฐฑ์ )
3๏ธโฃ ์ ์ ๋ชฉ๋ก (๋ณ๊ฒฝ์ด ๊ฑฐ์ ์์)
4๏ธโฃ ๊ธฐ์ฌ ์ ํ ์ค์ (๋ณ๊ฒฝ์ด ๊ฑฐ์ ์์)
2๏ธโฃ ์ฌ์ฉ์ ํต๊ณ (10๋ถ๋ง๋ค ๊ฐฑ์ )
3๏ธโฃ ์ ์ ๋ชฉ๋ก (๋ณ๊ฒฝ์ด ๊ฑฐ์ ์์)
4๏ธโฃ ๊ธฐ์ฌ ์ ํ ์ค์ (๋ณ๊ฒฝ์ด ๊ฑฐ์ ์์)
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
class CacheService
{
/**
* ๋ฆฌ๋๋ณด๋ ์บ์ฑ
*/
public function cacheLeaderboard($limit = 100)
{
$key = "leaderboard:top:{$limit}";
return Cache::remember($key, 300, function () use ($limit) {
return User::where('total_score', '>', 0)
->orderBy('total_score', 'desc')
->limit($limit)
->get();
});
}
/**
* ์ฌ์ฉ์ ๋ญํน ์บ์ฑ (Redis Sorted Set ์ฌ์ฉ)
*/
public function updateUserRankCache(User $user)
{
Redis::zadd('leaderboard:scores', $user->total_score, $user->wallet_address);
}
/**
* ์ฌ์ฉ์ ์์ ์กฐํ (O(log N) ์๊ฐ ๋ณต์ก๋)
*/
public function getUserRankFromCache($walletAddress)
{
$rank = Redis::zrevrank('leaderboard:scores', $walletAddress);
return $rank !== null ? $rank + 1 : null;
}
/**
* ์บ์ ๋ฌดํจํ
*/
public function invalidateLeaderboardCache()
{
$keys = Cache::get('leaderboard:keys', []);
foreach ($keys as $key) {
Cache::forget($key);
}
}
}
๐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ต์ ํ
1. ์ธ๋ฑ์ค ์ถ๊ฐ
// ๋ง์ด๊ทธ๋ ์ด์
์์
$table->index(['user_id', 'status', 'created_at']);
$table->index(['contribution_type', 'status']);
$table->index('total_score');
2. Eager Loading์ผ๋ก N+1 ๋ฌธ์ ํด๊ฒฐ// โ ๋์ ์
$users = User::all();
foreach ($users as $user) {
echo $user->contributions->count(); // N+1 ์ฟผ๋ฆฌ!
}
// โ
์ข์ ์
$users = User::withCount('contributions')->get();
foreach ($users as $user) {
echo $user->contributions_count; // ๋จ์ผ ์ฟผ๋ฆฌ!
}
3. ์ฟผ๋ฆฌ ์ต์ ํ// โ ๋๋ฆฐ ์ฟผ๋ฆฌ
$topUsers = User::all()->sortByDesc('total_score')->take(10);
// โ
๋น ๋ฅธ ์ฟผ๋ฆฌ
$topUsers = User::orderBy('total_score', 'desc')->limit(10)->get();
โ๏ธ ํ(Queue) ํ์ฉ
๋ฌด๊ฑฐ์ด ์์ ์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ฒ๋ฆฌํ์:<?php
namespace App\Jobs;
use App\Models\User;
use App\Services\ContributionService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class UpdateLeaderboardJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* ๋ฆฌ๋๋ณด๋ ์ ์ฒด ์
๋ฐ์ดํธ
*/
public function handle(ContributionService $contributionService)
{
$users = User::where('total_score', '>', 0)->get();
foreach ($users as $user) {
$contributionService->updateUserTotalScore($user);
}
\Log::info('Leaderboard updated successfully');
}
}
// ์ค์ผ์ค๋ฌ์ ๋ฑ๋ก
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
$schedule->job(new UpdateLeaderboardJob)->everyFiveMinutes();
}
๐ก Pro Tip: Redis๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฆฌ๋๋ณด๋ ์กฐํ ์ฑ๋ฅ์ด ์์ฒญ๋๊ฒ ๋นจ๋ผ์ ธ! Sorted Set ์๋ฃ๊ตฌ์กฐ๋ ๋ญํน ์์คํ
์ ์ต์ ํ๋์ด ์๊ฑฐ๋ . ๐
๐จ ํ๋ก ํธ์๋ ๊ตฌํ: ๋ฉ์ง UI ๋ง๋ค๊ธฐ
์ด์ ์ฌ์ฉ์๋ค์ด ์ค์ ๋ก ๋ณผ ํ๋ฉด์ ๋ง๋ค์ด๋ณด์! Vue.js์ Tailwind CSS๋ฅผ ์ฌ์ฉํ ๊ฑฐ์ผ. ๐จ๐ ๋ฆฌ๋๋ณด๋ ์ปดํฌ๋ํธ
<template>
<div class="leaderboard-container">
<div class="header">
<h1>๐ DAO ๊ธฐ์ฌ๋ ๋ฆฌ๋๋ณด๋</h1>
<div class="period-selector">
<button
v-for="period in periods"
:key="period.value"
@click="selectPeriod(period.value)"
:class="{ active: selectedPeriod === period.value }"
>
{{ period.label }}
</button>
</div>
</div>
<div class="top-three">
<div class="rank-card second" v-if="leaderboard[1]">
<div class="rank-badge">๐ฅ</div>
<img :src="leaderboard[1].avatar_url || defaultAvatar" alt="Avatar">
<h3>{{ leaderboard[1].username || 'Anonymous' }}</h3>
<p class="score">{{ formatScore(leaderboard[1].total_score) }}</p>
</div>
<div class="rank-card first" v-if="leaderboard[0]">
<div class="rank-badge">๐</div>
<img :src="leaderboard[0].avatar_url || defaultAvatar" alt="Avatar">
<h3>{{ leaderboard[0].username || 'Anonymous' }}</h3>
<p class="score">{{ formatScore(leaderboard[0].total_score) }}</p>
</div>
<div class="rank-card third" v-if="leaderboard[2]">
<div class="rank-badge">๐ฅ</div>
<img :src="leaderboard[2].avatar_url || defaultAvatar" alt="Avatar">
<h3>{{ leaderboard[2].username || 'Anonymous' }}</h3>
<p class="score">{{ formatScore(leaderboard[2].total_score) }}</p>
</div>
</div>
<div class="leaderboard-list">
<div
v-for="(user, index) in leaderboard.slice(3)"
:key="user.wallet_address"
class="leaderboard-item"
:class="{ 'is-current-user': isCurrentUser(user.wallet_address) }"
>
<div class="rank">#{{ index + 4 }}</div>
<img :src="user.avatar_url || defaultAvatar" alt="Avatar" class="avatar">
<div class="user-info">
<h4>{{ user.username || 'Anonymous' }}</h4>
<p class="wallet">{{ shortenAddress(user.wallet_address) }}</p>
</div>
<div class="stats">
<span class="score">{{ formatScore(user.total_score) }}</span>
<span class="contributions">{{ user.contribution_count }} ๊ธฐ์ฌ</span>
</div>
</div>
</div>
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p>๋ก๋ฉ ์ค...</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
leaderboard: [],
selectedPeriod: 'all-time',
periods: [
{ value: 'daily', label: '์ค๋' },
{ value: 'weekly', label: '์ด๋ฒ ์ฃผ' },
{ value: 'monthly', label: '์ด๋ฒ ๋ฌ' },
{ value: 'all-time', label: '์ ์ฒด' }
],
loading: false,
defaultAvatar: '/images/default-avatar.png',
currentUserAddress: null
}
},
mounted() {
this.fetchLeaderboard();
this.getCurrentUser();
this.listenForUpdates();
},
methods: {
async fetchLeaderboard() {
this.loading = true;
try {
const response = await axios.get(`/api/leaderboard/period/${this.selectedPeriod}`);
this.leaderboard = response.data.data;
} catch (error) {
console.error('Failed to fetch leaderboard:', error);
this.$notify({
type: 'error',
title: '์ค๋ฅ',
message: '๋ฆฌ๋๋ณด๋๋ฅผ ๋ถ๋ฌ์ค๋๋ฐ ์คํจํ์ต๋๋ค.'
});
} finally {
this.loading = false;
}
},
selectPeriod(period) {
this.selectedPeriod = period;
this.fetchLeaderboard();
},
formatScore(score) {
return new Intl.NumberFormat('ko-KR').format(score);
},
shortenAddress(address) {
return `${address.slice(0, 6)}...${address.slice(-4)}`;
},
isCurrentUser(address) {
return this.currentUserAddress &&
address.toLowerCase() === this.currentUserAddress.toLowerCase();
},
async getCurrentUser() {
if (window.ethereum) {
const accounts = await window.ethereum.request({ method: 'eth_accounts' });
if (accounts.length > 0) {
this.currentUserAddress = accounts[0];
}
}
},
listenForUpdates() {
window.Echo.channel('leaderboard')
.listen('.updated', (e) => {
this.fetchLeaderboard();
this.$notify({
type: 'success',
title: '๐ ์
๋ฐ์ดํธ!',
message: '๋ฆฌ๋๋ณด๋๊ฐ ์
๋ฐ์ดํธ๋์์ต๋๋ค.'
});
});
}
}
}
</script>
<style scoped>
.leaderboard-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 40px;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 20px;
}
.period-selector {
display: flex;
gap: 10px;
justify-content: center;
}
.period-selector button {
padding: 10px 20px;
border: 2px solid #e0e0e0;
border-radius: 8px;
background: white;
cursor: pointer;
transition: all 0.3s;
}
.period-selector button.active {
background: #6c5ce7;
color: white;
border-color: #6c5ce7;
}
.top-three {
display: flex;
justify-content: center;
align-items: flex-end;
gap: 20px;
margin-bottom: 40px;
}
.rank-card {
background: white;
border-radius: 16px;
padding: 30px;
text-align: center;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
transition: transform 0.3s;
}
.rank-card:hover {
transform: translateY(-5px);
}
.rank-card.first {
width: 280px;
background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
}
.rank-card.second {
width: 240px;
background: linear-gradient(135deg, #c0c0c0 0%, #e8e8e8 100%);
}
.rank-card.third {
width: 240px;
background: linear-gradient(135deg, #cd7f32 0%, #e8a87c 100%);
}
.rank-badge {
font-size: 3rem;
margin-bottom: 10px;
}
.rank-card img {
width: 100px;
height: 100px;
border-radius: 50%;
margin-bottom: 15px;
border: 4px solid white;
}
.rank-card h3 {
font-size: 1.5rem;
margin-bottom: 10px;
}
.rank-card .score {
font-size: 2rem;
font-weight: bold;
}
.leaderboard-list {
background: white;
border-radius: 16px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
.leaderboard-item {
display: flex;
align-items: center;
padding: 20px;
border-bottom: 1px solid #f0f0f0;
transition: background 0.3s;
}
.leaderboard-item:hover {
background: #f8f9fa;
}
.leaderboard-item.is-current-user {
background: #e3f2fd;
border-left: 4px solid #2196f3;
}
.leaderboard-item .rank {
font-size: 1.5rem;
font-weight: bold;
width: 60px;
color: #636e72;
}
.leaderboard-item .avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 15px;
}
.leaderboard-item .user-info {
flex: 1;
}
.leaderboard-item .user-info h4 {
font-size: 1.1rem;
margin-bottom: 5px;
}
.leaderboard-item .wallet {
font-size: 0.9rem;
color: #95a5a6;
}
.leaderboard-item .stats {
text-align: right;
}
.leaderboard-item .score {
display: block;
font-size: 1.3rem;
font-weight: bold;
color: #6c5ce7;
}
.leaderboard-item .contributions {
font-size: 0.9rem;
color: #95a5a6;
}
.loading {
text-align: center;
padding: 40px;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #6c5ce7;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
์! ์ด์ ์ ๋ง ๋ฉ์ง ๋ฆฌ๋๋ณด๋๊ฐ ์์ฑ๋์ด! ๐
๐ง ๋ฐฐํฌ์ ์ด์: ์ค์ ํฌ์ ํ๊ธฐ
๊ฐ๋ฐ์ด ๋๋ฌ์ผ๋ฉด ์ด์ ์ค์ ๋ก ๋ฐฐํฌํด์ผ์ง! ๋ฐฐํฌ ๊ณผ์ ๊ณผ ์ด์ ํ์ ์๋ ค์ค๊ฒ. ๐โ๏ธ ์๋ฒ ํ๊ฒฝ ๊ตฌ์ฑ
์ถ์ฒ ์คํ:
- ์๋ฒ: AWS EC2, DigitalOcean, ๋๋ Linode
- ์น์๋ฒ: Nginx
- ๋ฐ์ดํฐ๋ฒ ์ด์ค: AWS RDS (MySQL) ๋๋ ์์ฒด ํธ์คํ
- ์บ์: Redis (ElastiCache ๋๋ ์์ฒด ํธ์คํ )
- ํ: Redis Queue ๋๋ AWS SQS
- CDN: CloudFlare
- ๋ชจ๋ํฐ๋ง: New Relic, Datadog, ๋๋ Laravel Telescope
- ์๋ฒ: AWS EC2, DigitalOcean, ๋๋ Linode
- ์น์๋ฒ: Nginx
- ๋ฐ์ดํฐ๋ฒ ์ด์ค: AWS RDS (MySQL) ๋๋ ์์ฒด ํธ์คํ
- ์บ์: Redis (ElastiCache ๋๋ ์์ฒด ํธ์คํ )
- ํ: Redis Queue ๋๋ AWS SQS
- CDN: CloudFlare
- ๋ชจ๋ํฐ๋ง: New Relic, Datadog, ๋๋ Laravel Telescope
๐ฆ Docker๋ก ๋ฐฐํฌํ๊ธฐ
Docker๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฐฐํฌ๊ฐ ํจ์ฌ ์ฌ์์ ธ:# 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
# Composer ์ค์น
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# ์์
๋๋ ํ ๋ฆฌ ์ค์
WORKDIR /var/www
# ์ ํ๋ฆฌ์ผ์ด์
ํ์ผ ๋ณต์ฌ
COPY . .
# ์์กด์ฑ ์ค์น
RUN composer install --no-dev --optimize-autoloader
# ๊ถํ ์ค์
RUN chown -R www-data:www-data /var/www
EXPOSE 9000
CMD ["php-fpm"]
docker-compose.yml:version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: dao-leaderboard-app
restart: unless-stopped
working_dir: /var/www
volumes:
- ./:/var/www
networks:
- dao-network
nginx:
image: nginx:alpine
container_name: dao-leaderboard-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./:/var/www
- ./docker/nginx:/etc/nginx/conf.d
networks:
- dao-network
mysql:
image: mysql:8.0
container_name: dao-leaderboard-mysql
restart: unless-stopped
environment:
MYSQL_DATABASE: dao_leaderboard
MYSQL_ROOT_PASSWORD: secret
MYSQL_PASSWORD: secret
MYSQL_USER: dao_user
volumes:
- mysql-data:/var/lib/mysql
networks:
- dao-network
redis:
image: redis:alpine
container_name: dao-leaderboard-redis
restart: unless-stopped
networks:
- dao-network
networks:
dao-network:
driver: bridge
volumes:
mysql-data:
๋ฐฐํฌ ๋ช
๋ น์ด:docker-compose up -d
docker-compose exec app php artisan migrate --force
docker-compose exec app php artisan db:seed --force
docker-compose exec app php artisan optimize
๐ CI/CD ํ์ดํ๋ผ์ธ
GitHub Actions๋ก ์๋ ๋ฐฐํฌ ์ค์ :# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Install Dependencies
run: composer install --no-dev --optimize-autoloader
- name: Run Tests
run: php artisan test
- name: Deploy to Server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/dao-leaderboard
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan optimize
php artisan queue:restart
sudo systemctl reload nginx
๐ก ๋ฐฐํฌ ์ฒดํฌ๋ฆฌ์คํธ:
โ
ํ๊ฒฝ ๋ณ์ (.env) ์ค์ ํ์ธ
โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์ ์คํ
โ ์บ์ ๋ฐ ์ค์ ์ต์ ํ
โ ํ ์์ปค ์คํ ํ์ธ
โ SSL ์ธ์ฆ์ ์ค์
โ ๋ฐฉํ๋ฒฝ ๊ท์น ์ค์
โ ๋ฐฑ์ ์์คํ ๊ตฌ์ถ
โ ๋ชจ๋ํฐ๋ง ๋๊ตฌ ์ค์
โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์ ์คํ
โ ์บ์ ๋ฐ ์ค์ ์ต์ ํ
โ ํ ์์ปค ์คํ ํ์ธ
โ SSL ์ธ์ฆ์ ์ค์
โ ๋ฐฉํ๋ฒฝ ๊ท์น ์ค์
โ ๋ฐฑ์ ์์คํ ๊ตฌ์ถ
โ ๋ชจ๋ํฐ๋ง ๋๊ตฌ ์ค์
๐ ๋ชจ๋ํฐ๋ง๊ณผ ๋ก๊น
Laravel Telescope๋ก ์ ํ๋ฆฌ์ผ์ด์ ๋ชจ๋ํฐ๋ง:composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate
๋ก๊ทธ ๊ด๋ฆฌ:// config/logging.php
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['daily', 'slack'],
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'DAO Leaderboard Bot',
'emoji' => ':boom:',
'level' => 'error',
],
];
์ด์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด Slack์ผ๋ก ์๋ฆผ์ด ์! ํธํ์ง? ๐
๐ ์ค์ ํ์ฉ ์ฌ๋ก์ ํ์ฅ ์์ด๋์ด
์ด ์์คํ ์ ์ค์ ๋ก ์ด๋ป๊ฒ ํ์ฉํ ์ ์์๊น? ๋ช ๊ฐ์ง ์์ด๋์ด๋ฅผ ๊ณต์ ํ ๊ฒ! ๐ก๐ฏ ํ์ฉ ์ฌ๋ก
1. ํ ํฐ ์์ด๋๋ ๋ถ๋ฐฐ
๋ฆฌ๋๋ณด๋ ์ ์์ ๋น๋กํด์ ๊ฑฐ๋ฒ๋์ค ํ ํฐ์ ๋ถ๋ฐฐํ ์ ์์ด. ๊ณต์ ํ๊ณ ํฌ๋ช
ํ๊ฒ!
2. NFT ๋ณด์
์์ ๋ญ์ปค๋ค์๊ฒ ํน๋ณํ NFT๋ฅผ ๋ฐํํด์ค ์ ์์ด. ๋ช ์์ ์ ๋น ๊ฐ์ ๋๋!
3. ๊ฑฐ๋ฒ๋์ค ๊ฐ์ค์น
๊ธฐ์ฌ๋๊ฐ ๋์ ์ฌ๋์ ํฌํ๊ถ์ ๋ ๋์ ๊ฐ์ค์น๋ฅผ ๋ถ์ฌํ ์ ์์ด.
4. ๋ฉํ ๋ง ๋งค์นญ
์์ ๊ธฐ์ฌ์์ ์ ๊ท ๋ฉค๋ฒ๋ฅผ ๋งค์นญํด์ ๋ฉํ ๋ง ํ๋ก๊ทธ๋จ์ ์ด์ํ ์ ์์ด.
5. ์คํฐ์์ญ ๊ธฐํ
๊ธฐ์ ์คํฐ์๋ค์ด ์์ ๊ธฐ์ฌ์๋ค์๊ฒ ์ง์ ํ์ํ ์ ์๋ ์์คํ !
2. NFT ๋ณด์
์์ ๋ญ์ปค๋ค์๊ฒ ํน๋ณํ NFT๋ฅผ ๋ฐํํด์ค ์ ์์ด. ๋ช ์์ ์ ๋น ๊ฐ์ ๋๋!
3. ๊ฑฐ๋ฒ๋์ค ๊ฐ์ค์น
๊ธฐ์ฌ๋๊ฐ ๋์ ์ฌ๋์ ํฌํ๊ถ์ ๋ ๋์ ๊ฐ์ค์น๋ฅผ ๋ถ์ฌํ ์ ์์ด.
4. ๋ฉํ ๋ง ๋งค์นญ
์์ ๊ธฐ์ฌ์์ ์ ๊ท ๋ฉค๋ฒ๋ฅผ ๋งค์นญํด์ ๋ฉํ ๋ง ํ๋ก๊ทธ๋จ์ ์ด์ํ ์ ์์ด.
5. ์คํฐ์์ญ ๊ธฐํ
๊ธฐ์ ์คํฐ์๋ค์ด ์์ ๊ธฐ์ฌ์๋ค์๊ฒ ์ง์ ํ์ํ ์ ์๋ ์์คํ !
๐ ํ์ฅ ์์ด๋์ด
1. ํ ๋ฆฌ๋๋ณด๋
๊ฐ์ธ๋ฟ๋ง ์๋๋ผ ํ ๋จ์๋ก๋ ๊ฒฝ์ํ ์ ์๊ฒ!
๊ฐ์ธ๋ฟ๋ง ์๋๋ผ ํ ๋จ์๋ก๋ ๊ฒฝ์ํ ์ ์๊ฒ!
Schema::create('teams', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description');
$table->integer('total_score')->default(0);
$table->timestamps();
});
Schema::create('team_members', function (Blueprint $table) {
$table->foreignId('team_id')->constrained();
$table->foreignId('user_id')->constrained();
$table->enum('role', ['leader', 'member']);
$table->timestamps();
});
2. ์์ฆ ์์คํ
3๊ฐ์๋ง๋ค ์์ฆ์ ๋ฆฌ์ ํ๊ณ ์๋ก์ด ๊ฒฝ์์ ์์!
3๊ฐ์๋ง๋ค ์์ฆ์ ๋ฆฌ์ ํ๊ณ ์๋ก์ด ๊ฒฝ์์ ์์!
Schema::create('seasons', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->date('start_date');
$table->date('end_date');
$table->json('rewards');
$table->boolean('is_active')->default(false);
$table->timestamps();
});
3. ํ์คํธ ์์คํ
ํน์ ๋ชฉํ๋ฅผ ๋ฌ์ฑํ๋ฉด ๋ณด๋์ค ์ ์!
ํน์ ๋ชฉํ๋ฅผ ๋ฌ์ฑํ๋ฉด ๋ณด๋์ค ์ ์!
Schema::create('quests', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('description');
$table->json('requirements');
$table->integer('reward_points');
$table->date('expires_at')->nullable();
$table->timestamps();
});
4. ์์
๊ธฐ๋ฅ
ํ๋ก์ฐ, ์ข์์, ๋๊ธ ๋ฑ ์์ ๊ธฐ๋ฅ ์ถ๊ฐ!
ํ๋ก์ฐ, ์ข์์, ๋๊ธ ๋ฑ ์์ ๊ธฐ๋ฅ ์ถ๊ฐ!
Schema::create('follows', function (Blueprint $table) {
$table->foreignId('follower_id')->constrained('users');
$table->foreignId('following_id')->constrained('users');
$table->timestamps();
$table->unique(['follower_id', 'following_id']);
});
5. ๋ฉํฐ์ฒด์ธ ์ง์
์ด๋๋ฆฌ์๋ฟ๋ง ์๋๋ผ Polygon, BSC, Solana ๋ฑ ์ฌ๋ฌ ์ฒด์ธ ์ง์!
์ด๋๋ฆฌ์๋ฟ๋ง ์๋๋ผ Polygon, BSC, Solana ๋ฑ ์ฌ๋ฌ ์ฒด์ธ ์ง์!
Schema::create('blockchain_accounts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('chain'); // 'ethereum', 'polygon', 'bsc' ๋ฑ
$table->string('address');
$table->boolean('is_verified')->default(false);
$table->timestamps();
$table->unique(['chain', 'address']);
});
๐ผ ์ฌ๋ฅ๋ท์์์ ํ์ฉ
์ฌ๋ฅ๋ท(https://www.jaenung.net) ๊ฐ์ ์ฌ๋ฅ ๊ณต์ ํ๋ซํผ์์๋ ์ด๋ฐ ๋ฆฌ๋๋ณด๋ ์์คํ ์ ํ์ฉํ ์ ์์ด! ๐จ์๋ฅผ ๋ค์ด:
- ํ๋ก๊ทธ๋๋ฐ ๊ฐ์๋ฅผ ๋ง์ด ํ ์ฌ๋
- ๋์์ธ ์์ ์ ์ ์๋ฃํ ์ฌ๋
- ์ปค๋ฎค๋ํฐ์ ๋ง์ด ๊ธฐ์ฌํ ์ฌ๋
์ด๋ฐ ์์ผ๋ก ๊ฐ ๋ถ์ผ๋ณ ์ ๋ฌธ๊ฐ๋ค์ ๋ฆฌ๋๋ณด๋๋ก ๋ณด์ฌ์ฃผ๋ฉด, ์ฌ์ฉ์๋ค์ด ๋ ์ ๋ขฐํ ์ ์๋ ์ ๋ฌธ๊ฐ๋ฅผ ์ฝ๊ฒ ์ฐพ์ ์ ์๊ฒ ์ง? ์ฌ๋ฅ๋ท์ฒ๋ผ ๋ค์ํ ์ฌ๋ฅ์ด ๋ชจ์ด๋ ๊ณณ์์๋ ์ด๋ฐ ํฌ๋ช ํ ํ๊ฐ ์์คํ ์ด ์ ๋ง ์ค์ํด! ๐ช
๐ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ ๋ง ๊ธด ์ฌ์ ์ด์์ง? ๐ ์ฐ๋ฆฌ๊ฐ ํจ๊ป ๋ง๋ DAO ๊ธฐ์ฌ๋ ๋ฆฌ๋๋ณด๋ ์์คํ ์ ์ ๋ฆฌํด๋ณผ๊น?๐ฏ ์ฐ๋ฆฌ๊ฐ ๋ฐฐ์ด ๊ฒ๋ค:
- โ ๋ผ๋ผ๋ฒจ๋ก RESTful API ๊ตฌ์ถํ๊ธฐ
- โ ๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ์ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ฐ๋
- โ ์ค์๊ฐ ์ ๋ฐ์ดํธ๋ฅผ ์ํ WebSocket ํ์ฉ
- โ ๊ฒ์ด๋ฏธํผ์ผ์ด์ ์์ ์ค๊ณ
- โ ์ฑ๋ฅ ์ต์ ํ์ ์บ์ฑ ์ ๋ต
- โ ๋ณด์๊ณผ ๋ถ์ ํ์ ๋ฐฉ์ง
- โ Docker๋ฅผ ํ์ฉํ ๋ฐฐํฌ
- โ CI/CD ํ์ดํ๋ผ์ธ ๊ตฌ์ถ
- โ ๋ชจ๋ํฐ๋ง๊ณผ ๋ก๊น
- โ ํ์ฅ ๊ฐ๋ฅํ ์ํคํ ์ฒ ์ค๊ณ
๐ช ๋ค์ ๋จ๊ณ
์ด์ ์ฌ๋ฌ๋ถ์ ์ด ์์คํ ์ ๊ธฐ๋ฐ์ผ๋ก ๋ ๋ฉ์ง ๊ธฐ๋ฅ๋ค์ ์ถ๊ฐํ ์ ์์ด:1. **AI ๊ธฐ๋ฐ ๊ธฐ์ฌ๋ ํ๊ฐ**: ๋จธ์ ๋ฌ๋์ผ๋ก ๊ธฐ์ฌ์ ์ง์ ์๋ ํ๊ฐ
2. **ํฌ๋ก์ค์ฒด์ธ ๋ธ๋ฆฟ์ง**: ์ฌ๋ฌ ๋ธ๋ก์ฒด์ธ์ ํ๋์ ํตํฉ
3. **DAO ๊ฑฐ๋ฒ๋์ค ํตํฉ**: ํฌํ ์์คํ ๊ณผ ์ง์ ์ฐ๋
4. **๋ชจ๋ฐ์ผ ์ฑ**: React Native๋ Flutter๋ก ๋ชจ๋ฐ์ผ ๋ฒ์ ์ ์
5. **๋ฐ์ดํฐ ๋ถ์ ๋์๋ณด๋**: ๋ ๊น์ด ์๋ ์ธ์ฌ์ดํธ ์ ๊ณต
๐ก ๊ฐ๋ฐ ํ: ์ฒ์๋ถํฐ ์๋ฒฝํ๊ฒ ๋ง๋ค๋ ค๊ณ ํ์ง ๋ง! MVP(Minimum Viable Product)๋ถํฐ ์์ํด์ ์ฌ์ฉ์ ํผ๋๋ฐฑ์ ๋ฐ์ผ๋ฉฐ ์ ์ง์ ์ผ๋ก ๊ฐ์ ํด๋๊ฐ๋ ๊ฒ ์ต๊ณ ์ผ. ๐
๐ ์ปค๋ฎค๋ํฐ์ ํจ๊ป
Web3์ DAO๋ ์ปค๋ฎค๋ํฐ๊ฐ ํต์ฌ์ด์ผ. ์ฌ๋ฌ๋ถ์ด ๋ง๋ ๋ฆฌ๋๋ณด๋ ์์คํ ๋ ์ปค๋ฎค๋ํฐ์ ํผ๋๋ฐฑ์ ๋ฐ์๊ฐ๋ฉฐ ๋ฐ์ ์์ผ๋๊ฐ๋ฉด ์ข๊ฒ ์ด!GitHub์ ์คํ์์ค๋ก ๊ณต๊ฐํ๋ฉด ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค๋ ๊ธฐ์ฌํ ์ ์๊ณ , ๋ ๋ง์ DAO๋ค์ด ์ฌ์ฉํ ์ ์์ ๊ฑฐ์ผ. ๐
โ ๏ธ ์ฃผ์์ฌํญ: ์ค์ ํ๋ก๋์
ํ๊ฒฝ์์๋ ๋ณด์์ ๋ ์ ๊ฒฝ ์จ์ผ ํด. ์ค๋งํธ ์ปจํธ๋ํธ ๊ฐ์ฌ, ์นจํฌ ํ
์คํธ, ๋ฒ๊ทธ ๋ฐ์ดํฐ ํ๋ก๊ทธ๋จ ๋ฑ์ ๊ณ ๋ คํด๋ด!
๐ ์ถํํด!
์ฌ๊ธฐ๊น์ง ๋ฐ๋ผ์๋ค๋ฉด ์ ๋ง ๋๋จํด! ๐ ์ด์ ์ฌ๋ฌ๋ถ์:- ๋ผ๋ผ๋ฒจ๋ก ๋ณต์กํ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค ์ ์์ด
- ๋ธ๋ก์ฒด์ธ๊ณผ ์น์ ์ฐ๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ์์
- ์ค์๊ฐ ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์์ด
- ์ฑ๋ฅ ์ต์ ํ๋ฅผ ํ ์ ์์ด
- ํ๋ก๋์ ํ๊ฒฝ์ ๋ฐฐํฌํ ์ ์์ด
์ด ๋ชจ๋ ์ง์์ DAO ๋ฆฌ๋๋ณด๋๋ฟ๋ง ์๋๋ผ ๋ค๋ฅธ Web3 ํ๋ก์ ํธ์๋ ์ ์ฉํ ์ ์์ด! ๐ช
๋ง์ง๋ง์ผ๋ก...
๊ฐ๋ฐ์ ๋์์๋ ํ์ต์ ์ฐ์์ด์ผ. ์๋ก์ด ๊ธฐ์ ์ด ๊ณ์ ๋์ค๊ณ , ๋ ๋์ ๋ฐฉ๋ฒ๋ค์ด ๋ฐ๊ฒฌ๋์ง. ํ์ง๋ง ๊ธฐ๋ณธ ์๋ฆฌ๋ฅผ ์ดํดํ๊ณ ์๋ค๋ฉด ์ด๋ค ๋ณํ์๋ ์ ์ํ ์ ์์ด!
์ฌ๋ฌ๋ถ์ DAO ๋ฆฌ๋๋ณด๋ ํ๋ก์ ํธ๊ฐ ์ฑ๊ณตํ๊ธธ ๋ฐ๋ผ! ๊ทธ๋ฆฌ๊ณ ์ธ์ ๊ฐ ์ฌ๋ฌ๋ถ์ด ๋ง๋ ์์คํ ์ด ์ค์ DAO์์ ์ฌ์ฉ๋๋ ๋ ์ด ์ค๊ธธ! ๐โจ
์ง๋ฌธ์ด ์๊ฑฐ๋ ๋์์ด ํ์ํ๋ฉด ์ธ์ ๋ ์ง ์ปค๋ฎค๋ํฐ์ ๋ฌผ์ด๋ด. ์ฐ๋ฆฌ๋ ๋ชจ๋ ํจ๊ป ์ฑ์ฅํ๋ ๊ฑฐ๋๊น! ๐
Happy coding! ๐ป๐จ
๐ฏ ํต์ฌ ์์ฝ
๋ผ๋ผ๋ฒจ๋ก DAO ๊ธฐ์ฌ๋ ๋ฆฌ๋๋ณด๋๋ฅผ ๋ง๋๋ ๊ฒ์ ๋ธ๋ก์ฒด์ธ๊ณผ ์น ๊ฐ๋ฐ์ ์๋ฒฝํ ์กฐํฉ์ด์ผ!
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ๋ถํฐ ์ค์๊ฐ ์
๋ฐ์ดํธ, ๊ฒ์ด๋ฏธํผ์ผ์ด์
, ๋ณด์, ๋ฐฐํฌ๊น์ง
๋ชจ๋ ๊ณผ์ ์ ๊ฑฐ์น๋ฉด์ ์ค์ ํ๋ก์ ํธ ๊ฒฝํ์ ์์ ์ ์์ด.
๊ฐ์ฅ ์ค์ํ ๊ฑด ์์ํ๋ ๊ฑฐ์ผ! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
๋๊ธ์ ๋ถ๋ฌ์ค๋ ์ค์
๋๋ค.
๋๊ธ์ ๋ถ๋ฌ์ค์ง ๋ชปํ์ต๋๋ค. ์ฐ๊ฒฐ ์ํ๋ฅผ ํ์ธํ๊ณ ๋ค์ ์๋ํด ์ฃผ์ธ์.
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.