Ver3.0
์ ์ฒด ์ด๋
Ver3.0 ๐ ๋ผ๋ผ๋ฒจ๋ก ๋ง๋๋ ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ถ ํฌ๋ช ์ฑ ํ๋ซํผ

๐ ๋ผ๋ผ๋ฒจ๋ก ๋ง๋๋ ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ถ ํฌ๋ช ์ฑ ํ๋ซํผ
ํฌ๋ช
ํ ๊ธฐ๋ถ ์ํ๊ณ๋ฅผ ์ํ ์ค์ ๊ฐ๋ฐ ๊ฐ์ด๋ ๐
๐ฏ ์ ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ถ ํ๋ซํผ์ด ํ์ํ ๊น?
์ผ, ์์งํ ๋งํด์ ๊ธฐ๋ถํ ๋ ์ ์ผ ๊ฑฑ์ ๋๋ ๊ฒ ๋ญ์ผ? ๋ฐ๋ก "๋ด๊ฐ ๋ธ ๋์ด ์ง์ง ์ ๋๋ก ์ฐ์ด๋ ๊ฑฐ ๋ง์?" ์ด๊ฑฐ์์. ๐ํต๊ณ๋ฅผ ๋ณด๋ฉด ์ถฉ๊ฒฉ์ ์ด์ผ. ๊ตญ๋ด ๊ธฐ๋ถ์์ ์ฝ 67%๊ฐ ๊ธฐ๋ถ๊ธ ์ฌ์ฉ์ฒ์ ๋ํ ๋ถ์ ์ ๊ฐ์ง๊ณ ์๋. ๊ทธ๋ฆฌ๊ณ ์ค์ ๋ก ์ผ๋ถ ๋จ์ฒด๋ค์์ ๋ถํฌ๋ช ํ ํ๊ณ ์ฒ๋ฆฌ๋ก ๋ ผ๋์ด ๋๋ ๊ฒฝ์ฐ๋ ์ข ์ข ์์์ง.
์ด๋ฐ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ฑ์ฅํ ๊ฒ ๋ฐ๋ก ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ฐ ๊ธฐ๋ถ ํ๋ซํผ์ด์ผ. ๋ธ๋ก์ฒด์ธ์ ํต์ฌ ํน์ง์ธ '๋ถ๋ณ์ฑ'๊ณผ 'ํฌ๋ช ์ฑ'์ ํ์ฉํ๋ฉด, ๋ชจ๋ ๊ธฐ๋ถ ๋ด์ญ๊ณผ ์งํ ๊ณผ์ ์ ๋๊ตฌ๋ ํ์ธํ ์ ์๊ฒ ๋ง๋ค ์ ์๊ฑฐ๋ .
๊ทธ๋ฆฌ๊ณ ์ด๊ฑธ ๋ผ๋ผ๋ฒจ(Laravel)๋ก ๊ตฌํํ๋ฉด? ๊ฐ๋ฐ ์๋๋ ๋น ๋ฅด๊ณ , ์ ์ง๋ณด์๋ ์ฝ๊ณ , ํ์ฅ์ฑ๋ ์ข์. ์์ ์ผ์์ผ์กฐ์ง! ๐
๐ก ์ด ๊ธ์์ ๋ค๋ฃฐ ํต์ฌ ๋ด์ฉ
โ ๋ธ๋ก์ฒด์ธ๊ณผ ๊ธฐ๋ถ ํ๋ซํผ์ ๋ง๋จโ ๋ผ๋ผ๋ฒจ ํ๋ ์์ํฌ ๊ธฐ๋ฐ ์ํคํ ์ฒ ์ค๊ณ
โ ์ค๋งํธ ์ปจํธ๋ํธ ์ฐ๋ ๋ฐฉ๋ฒ
โ ์ค์๊ฐ ํฌ๋ช ์ฑ ๋์๋ณด๋ ๊ตฌํ
โ ๋ณด์๊ณผ ๊ฐ์ธ์ ๋ณด ๋ณดํธ ์ ๋ต
โ ์ค์ ์ฝ๋์ ๋ฐฐํฌ ๊ฐ์ด๋
์ด ๊ธ์ ๋ค ์ฝ๊ณ ๋๋ฉด, ๋๋ ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ถ ํ๋ซํผ์ ์ง์ ๋ง๋ค ์ ์์ ๊ฑฐ์ผ! ๐ช
๐๏ธ ์์คํ ์ํคํ ์ฒ ์ค๊ณํ๊ธฐ
์, ๋ณธ๊ฒฉ์ ์ผ๋ก ์์ํด๋ณผ๊น? ๋จผ์ ์ ์ฒด ์์คํ ๊ตฌ์กฐ๋ฅผ ์ดํดํด์ผ ํด.๐ 3๊ณ์ธต ์ํคํ ์ฒ
์ฐ๋ฆฌ๊ฐ ๋ง๋ค ํ๋ซํผ์ ํฌ๊ฒ 3๊ฐ ๊ณ์ธต์ผ๋ก ๋๋ ์ ์์ด:1. ํ๋ก ํธ์๋ ๊ณ์ธต (Presentation Layer)
- Vue.js ๋๋ React๋ก ๊ตฌํํ๋ ์ฌ์ฉ์ ์ธํฐํ์ด์ค
- ๊ธฐ๋ถ์์ ์ํ ๋จ์ฒด๊ฐ ์ํธ์์ฉํ๋ ๊ณต๊ฐ
- ์ค์๊ฐ ๋์๋ณด๋์ ํฌ๋ช ์ฑ ๋ฆฌํฌํธ ์ ๊ณต
2. ๋ฐฑ์๋ ๊ณ์ธต (Application Layer)
- ๋ผ๋ผ๋ฒจ ํ๋ ์์ํฌ๊ฐ ๋ด๋นํ๋ ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง
- API ์๋ํฌ์ธํธ ์ ๊ณต
- ์ฌ์ฉ์ ์ธ์ฆ/์ธ๊ฐ ์ฒ๋ฆฌ
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ด๋ฆฌ
3. ๋ธ๋ก์ฒด์ธ ๊ณ์ธต (Blockchain Layer)
- ์ด๋๋ฆฌ์ ๋๋ ํ์ดํผ๋ ์ ํจ๋ธ๋ฆญ ๊ฐ์ ๋ธ๋ก์ฒด์ธ ๋คํธ์ํฌ
- ์ค๋งํธ ์ปจํธ๋ํธ ์คํ
- ๋ถ๋ณ ๊ฑฐ๋ ๊ธฐ๋ก ์ ์ฅ
์ด ์ธ ๊ณ์ธต์ด ์ ๊ธฐ์ ์ผ๋ก ์ฐ๊ฒฐ๋์ด์ผ ์ง์ง ํฌ๋ช ํ ํ๋ซํผ์ด ์์ฑ๋๋ ๊ฑฐ์ง! ๐จ
๐ง ๊ธฐ์ ์คํ ์ ์
์ค์ ํ๋ก์ ํธ์์ ์ฌ์ฉํ ๊ธฐ์ ์คํ์ ์ ๋ฆฌํด๋ดค์ด:| ์์ญ | ๊ธฐ์ | ์ ํ ์ด์ |
|---|---|---|
| ๋ฐฑ์๋ ํ๋ ์์ํฌ | Laravel 10.x | ๊ฐ๋ ฅํ ORM, ํ๋ถํ ์ํ๊ณ, ๋น ๋ฅธ ๊ฐ๋ฐ ์๋ |
| ๋ฐ์ดํฐ๋ฒ ์ด์ค | PostgreSQL | ํธ๋์ญ์ ์์ ์ฑ, JSON ์ง์, ํ์ฅ์ฑ |
| ๋ธ๋ก์ฒด์ธ | Ethereum (Sepolia ํ ์คํธ๋ท) | ์ฑ์ํ ์ํ๊ณ, ํ๋ถํ ๊ฐ๋ฐ ๋๊ตฌ |
| ์ค๋งํธ ์ปจํธ๋ํธ | Solidity | ์ด๋๋ฆฌ์ ํ์ค ์ธ์ด, ๊ฒ์ฆ๋ ๋ณด์ ํจํด |
| Web3 ๋ผ์ด๋ธ๋ฌ๋ฆฌ | web3.php | PHP์์ ๋ธ๋ก์ฒด์ธ ์ํธ์์ฉ |
| ํ๋ก ํธ์๋ | Vue.js 3 + Inertia.js | ๋ผ๋ผ๋ฒจ๊ณผ์ ์๋ฒฝํ ํตํฉ |
| ํ ์์คํ | Redis + Laravel Queue | ๋น๋๊ธฐ ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ์ฒ๋ฆฌ |
๐พ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ์ค๊ณ
๋ธ๋ก์ฒด์ธ๊ณผ ์ ํต์ ์ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ํจ๊ป ์ฌ์ฉํ๋ ํ์ด๋ธ๋ฆฌ๋ ์ ๊ทผ ๋ฐฉ์์ ์ทจํ ๊ฑฐ์ผ. ์๋ํ๋ฉด:- ๋ธ๋ก์ฒด์ธ: ์ค์ํ ๊ฑฐ๋ ๊ธฐ๋ก, ํด์๊ฐ, ๋ถ๋ณ ๋ฐ์ดํฐ
- ๋ฐ์ดํฐ๋ฒ ์ด์ค: ์ฌ์ฉ์ ์ ๋ณด, ๊ฒ์ ์ธ๋ฑ์ค, ์บ์ ๋ฐ์ดํฐ
์ด๋ ๊ฒ ๋ถ๋ฆฌํ๋ฉด ์ฑ๋ฅ๋ ์ข๊ณ ๋น์ฉ๋ ์ ์ฝํ ์ ์์ด! ๐ฐ
๐ ํต์ฌ ํ ์ด๋ธ ๊ตฌ์กฐ
users ํ ์ด๋ธ๊ธฐ๋ถ์์ ์ํ ๋จ์ฒด ์ ๋ณด๋ฅผ ์ ์ฅํด:
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('wallet_address')->nullable()->unique();
$table->enum('user_type', ['donor', 'beneficiary', 'admin']);
$table->boolean('is_verified')->default(false);
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
$table->softDeletes();
});
campaigns ํ
์ด๋ธ
๊ธฐ๋ถ ์บ ํ์ธ ์ ๋ณด:
๊ธฐ๋ถ ์บ ํ์ธ ์ ๋ณด:
Schema::create('campaigns', function (Blueprint $table) {
$table->id();
$table->foreignId('beneficiary_id')->constrained('users');
$table->string('title');
$table->text('description');
$table->decimal('goal_amount', 20, 2);
$table->decimal('current_amount', 20, 2)->default(0);
$table->string('smart_contract_address')->nullable();
$table->timestamp('start_date');
$table->timestamp('end_date');
$table->enum('status', ['draft', 'active', 'completed', 'cancelled']);
$table->json('metadata')->nullable();
$table->timestamps();
$table->softDeletes();
$table->index(['status', 'start_date', 'end_date']);
});
donations ํ
์ด๋ธ
๊ฐ๋ณ ๊ธฐ๋ถ ๋ด์ญ:
๊ฐ๋ณ ๊ธฐ๋ถ ๋ด์ญ:
Schema::create('donations', function (Blueprint $table) {
$table->id();
$table->foreignId('campaign_id')->constrained();
$table->foreignId('donor_id')->constrained('users');
$table->decimal('amount', 20, 2);
$table->string('currency', 10)->default('KRW');
$table->string('transaction_hash')->unique();
$table->string('block_number')->nullable();
$table->timestamp('blockchain_confirmed_at')->nullable();
$table->enum('status', ['pending', 'confirmed', 'failed']);
$table->text('message')->nullable();
$table->boolean('is_anonymous')->default(false);
$table->timestamps();
$table->index(['campaign_id', 'status']);
$table->index('transaction_hash');
});
expenditures ํ
์ด๋ธ
๊ธฐ๋ถ๊ธ ์งํ ๋ด์ญ (ํฌ๋ช ์ฑ์ ํต์ฌ!):
๊ธฐ๋ถ๊ธ ์งํ ๋ด์ญ (ํฌ๋ช ์ฑ์ ํต์ฌ!):
Schema::create('expenditures', function (Blueprint $table) {
$table->id();
$table->foreignId('campaign_id')->constrained();
$table->decimal('amount', 20, 2);
$table->string('category'); // ์: ์๋ฃ๋น, ๊ต์ก๋น, ์ํ๋น
$table->text('description');
$table->string('receipt_url')->nullable();
$table->string('transaction_hash')->unique();
$table->string('block_number')->nullable();
$table->timestamp('executed_at');
$table->timestamp('blockchain_confirmed_at')->nullable();
$table->enum('status', ['pending', 'confirmed', 'rejected']);
$table->foreignId('approved_by')->nullable()->constrained('users');
$table->timestamps();
$table->index(['campaign_id', 'status']);
});
blockchain_logs ํ
์ด๋ธ
๋ชจ๋ ๋ธ๋ก์ฒด์ธ ์ํธ์์ฉ ๋ก๊ทธ:
๋ชจ๋ ๋ธ๋ก์ฒด์ธ ์ํธ์์ฉ ๋ก๊ทธ:
Schema::create('blockchain_logs', function (Blueprint $table) {
$table->id();
$table->string('transaction_hash')->unique();
$table->string('block_number')->nullable();
$table->string('from_address');
$table->string('to_address');
$table->string('contract_address')->nullable();
$table->string('event_type'); // donation, expenditure, contract_deploy
$table->json('event_data');
$table->decimal('gas_used', 20, 0)->nullable();
$table->decimal('gas_price', 30, 0)->nullable();
$table->timestamp('blockchain_timestamp');
$table->timestamps();
$table->index('transaction_hash');
$table->index(['event_type', 'created_at']);
});
๐ก Pro Tip: ๋ฐ์ดํฐ๋ฒ ์ด์ค์๋ ๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ์ '์บ์'๋ฅผ ์ ์ฅํ๋ ๊ฑฐ์ผ. ์ค์ ์ง์ค์ ์์ฒ(Source of Truth)์ ๋ธ๋ก์ฒด์ธ์ ์์ง๋ง, ๋น ๋ฅธ ์กฐํ๋ฅผ ์ํด DB์๋ ๋ณต์ฌ๋ณธ์ ๋ฌ. ์ด๋ ๊ฒ ํ๋ฉด ์ฌ์ฉ์๊ฐ ๋์๋ณด๋๋ฅผ ๋ณผ ๋๋ง๋ค ๋ธ๋ก์ฒด์ธ์ ์ง์ ์กฐํํ ํ์๊ฐ ์์ด์ ํจ์ฌ ๋น ๋ฅด๊ฑฐ๋ ! โก
๐ ์ค๋งํธ ์ปจํธ๋ํธ ๊ฐ๋ฐ
์, ์ด์ ๋ธ๋ก์ฒด์ธ์ ์ฌ์ฅ๋ถ์ธ ์ค๋งํธ ์ปจํธ๋ํธ๋ฅผ ๋ง๋ค์ด๋ณผ ์๊ฐ์ด์ผ! ์๋ฆฌ๋ํฐ(Solidity)๋ก ์์ฑํ ๊ฑด๋ฐ, ๊ฑฑ์ ๋ง. ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์ค๋ช ํด์ค๊ฒ. ๐๐ ๊ธฐ๋ถ ์บ ํ์ธ ์ค๋งํธ ์ปจํธ๋ํธ
์ฐ๋ฆฌ๊ฐ ๋ง๋ค ์ปจํธ๋ํธ์ ํต์ฌ ๊ธฐ๋ฅ์ ์ด๊ฑฐ์ผ:1. ๊ธฐ๋ถ๊ธ ๋ฐ๊ธฐ (Donation)
2. ๊ธฐ๋ถ๊ธ ์งํํ๊ธฐ (Expenditure)
3. ํฌ๋ช ์ฑ ๋ณด์ฅ (Transparency)
4. ํ๋ถ ์ฒ๋ฆฌ (Refund - ์บ ํ์ธ ์คํจ ์)
์ ์ฒด ์ฝ๋๋ฅผ ๋ณด์:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract DonationCampaign {
address public beneficiary;
address public platformAdmin;
uint256 public goalAmount;
uint256 public currentAmount;
uint256 public deadline;
bool public isActive;
bool public goalReached;
uint256 public constant PLATFORM_FEE_PERCENT = 2; // 2% ํ๋ซํผ ์์๋ฃ
struct Donation {
address donor;
uint256 amount;
uint256 timestamp;
string message;
}
struct Expenditure {
uint256 amount;
string category;
string description;
uint256 timestamp;
bool executed;
}
Donation[] public donations;
Expenditure[] public expenditures;
mapping(address => uint256) public donorContributions;
event DonationReceived(
address indexed donor,
uint256 amount,
uint256 timestamp,
string message
);
event ExpenditureExecuted(
uint256 indexed expenditureId,
uint256 amount,
string category,
uint256 timestamp
);
event GoalReached(uint256 totalAmount, uint256 timestamp);
event RefundIssued(address indexed donor, uint256 amount);
modifier onlyBeneficiary() {
require(msg.sender == beneficiary, "Only beneficiary can call this");
_;
}
modifier onlyAdmin() {
require(msg.sender == platformAdmin, "Only admin can call this");
_;
}
modifier campaignActive() {
require(isActive, "Campaign is not active");
require(block.timestamp < deadline, "Campaign has ended");
_;
}
constructor(
address _beneficiary,
address _platformAdmin,
uint256 _goalAmount,
uint256 _durationInDays
) {
require(_beneficiary != address(0), "Invalid beneficiary address");
require(_goalAmount > 0, "Goal amount must be positive");
beneficiary = _beneficiary;
platformAdmin = _platformAdmin;
goalAmount = _goalAmount;
deadline = block.timestamp + (_durationInDays * 1 days);
isActive = true;
goalReached = false;
}
function donate(string memory _message) public payable campaignActive {
require(msg.value > 0, "Donation amount must be positive");
donations.push(Donation({
donor: msg.sender,
amount: msg.value,
timestamp: block.timestamp,
message: _message
}));
donorContributions[msg.sender] += msg.value;
currentAmount += msg.value;
emit DonationReceived(msg.sender, msg.value, block.timestamp, _message);
if (currentAmount >= goalAmount && !goalReached) {
goalReached = true;
emit GoalReached(currentAmount, block.timestamp);
}
}
function executeExpenditure(
uint256 _amount,
string memory _category,
string memory _description
) public onlyBeneficiary {
require(_amount > 0, "Amount must be positive");
require(_amount <= currentAmount, "Insufficient funds");
require(goalReached, "Goal not reached yet");
uint256 platformFee = (_amount * PLATFORM_FEE_PERCENT) / 100;
uint256 netAmount = _amount - platformFee;
expenditures.push(Expenditure({
amount: _amount,
category: _category,
description: _description,
timestamp: block.timestamp,
executed: true
}));
currentAmount -= _amount;
// ์ํ์์๊ฒ ์ก๊ธ
payable(beneficiary).transfer(netAmount);
// ํ๋ซํผ ์์๋ฃ ์ก๊ธ
payable(platformAdmin).transfer(platformFee);
emit ExpenditureExecuted(
expenditures.length - 1,
_amount,
_category,
block.timestamp
);
}
function refund() public {
require(!goalReached, "Goal was reached, no refunds");
require(block.timestamp >= deadline, "Campaign still active");
uint256 contribution = donorContributions[msg.sender];
require(contribution > 0, "No contribution found");
donorContributions[msg.sender] = 0;
currentAmount -= contribution;
payable(msg.sender).transfer(contribution);
emit RefundIssued(msg.sender, contribution);
}
function getDonationsCount() public view returns (uint256) {
return donations.length;
}
function getExpendituresCount() public view returns (uint256) {
return expenditures.length;
}
function getDonation(uint256 _index) public view returns (
address donor,
uint256 amount,
uint256 timestamp,
string memory message
) {
require(_index < donations.length, "Invalid index");
Donation memory d = donations[_index];
return (d.donor, d.amount, d.timestamp, d.message);
}
function getExpenditure(uint256 _index) public view returns (
uint256 amount,
string memory category,
string memory description,
uint256 timestamp,
bool executed
) {
require(_index < expenditures.length, "Invalid index");
Expenditure memory e = expenditures[_index];
return (e.amount, e.category, e.description, e.timestamp, e.executed);
}
function closeCampaign() public onlyAdmin {
isActive = false;
}
}
๐ ์ค๋งํธ ์ปจํธ๋ํธ ํต์ฌ ๊ฐ๋ ์ค๋ช
1. ์ํ ๋ณ์ (State Variables)- ๋ธ๋ก์ฒด์ธ์ ์๊ตฌ์ ์ผ๋ก ์ ์ฅ๋๋ ๋ฐ์ดํฐ
- `beneficiary`, `goalAmount`, `currentAmount` ๋ฑ
- ๊ฐ์ค๋น๊ฐ ๋ค๊ธฐ ๋๋ฌธ์ ์ ์คํ๊ฒ ์ฌ์ฉํด์ผ ํด
2. ์ด๋ฒคํธ (Events)
- ๋ธ๋ก์ฒด์ธ์ ๋ก๊ทธ๋ฅผ ๋จ๊ธฐ๋ ๋ฐฉ๋ฒ
- ํ๋ก ํธ์๋์์ ์ค์๊ฐ์ผ๋ก ๊ฐ์ง ๊ฐ๋ฅ
- `DonationReceived`, `ExpenditureExecuted` ๋ฑ
3. ์์ ์ (Modifiers)
- ํจ์ ์คํ ์ ์กฐ๊ฑด์ ์ฒดํฌ
- `onlyBeneficiary`, `campaignActive` ๋ฑ
- ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ๊ณผ ๋ณด์์ ๋์ฌ์ค
4. ๊ฐ์์ฑ (Visibility)
- `public`: ๋๊ตฌ๋ ํธ์ถ ๊ฐ๋ฅ
- `private`: ์ปจํธ๋ํธ ๋ด๋ถ์์๋ง
- `view`: ์ํ๋ฅผ ์ฝ๊ธฐ๋ง ํจ (๊ฐ์ค๋น ๋ฌด๋ฃ)
- `pure`: ์ํ๋ฅผ ์ฝ์ง๋ ์ฐ์ง๋ ์์
๐ ๋ผ๋ผ๋ฒจ๊ณผ ๋ธ๋ก์ฒด์ธ ์ฐ๋ํ๊ธฐ
์, ์ด์ ๋ผ๋ผ๋ฒจ์์ ์ด ์ค๋งํธ ์ปจํธ๋ํธ์ ํต์ ํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์! ์ฌ๊ธฐ์ `web3.php` ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ ๊ฑฐ์ผ.๐ฆ ํจํค์ง ์ค์น
๋จผ์ ํ์ํ ํจํค์ง๋ค์ ์ค์นํ์:composer require sc0vu/web3.php
composer require kornrunner/keccak
โ๏ธ ํ๊ฒฝ ์ค์
`.env` ํ์ผ์ ๋ธ๋ก์ฒด์ธ ๊ด๋ จ ์ค์ ์ ์ถ๊ฐํด:# Blockchain Configuration
BLOCKCHAIN_NETWORK=sepolia
BLOCKCHAIN_RPC_URL=https://sepolia.infura.io/v3/YOUR_INFURA_PROJECT_ID
BLOCKCHAIN_CHAIN_ID=11155111
# Platform Wallet
PLATFORM_WALLET_ADDRESS=0xYourPlatformWalletAddress
PLATFORM_WALLET_PRIVATE_KEY=YourPrivateKey
# Smart Contract
DONATION_CONTRACT_ADDRESS=0xYourDeployedContractAddress
# Gas Settings
GAS_LIMIT=300000
GAS_PRICE=20000000000
โ ๏ธ ๋ณด์ ์ฃผ์์ฌํญ: ์ ๋๋ก ํ๋ผ์ด๋น ํค๋ฅผ ์ฝ๋์ ํ๋์ฝ๋ฉํ๊ฑฐ๋ Git์ ์ปค๋ฐํ์ง ๋ง! ํ๊ฒฝ ๋ณ์๋ก ๊ด๋ฆฌํ๊ณ , ํ๋ก๋์
์์๋ AWS Secrets Manager๋ HashiCorp Vault ๊ฐ์ ๋ณด์ ์๋ฃจ์
์ ์ฌ์ฉํด์ผ ํด. ๐
๐๏ธ ๋ธ๋ก์ฒด์ธ ์๋น์ค ํด๋์ค ๋ง๋ค๊ธฐ
`app/Services/BlockchainService.php` ํ์ผ์ ๋ง๋ค์ด๋ณด์:<?php
namespace App\Services;
use Web3\Web3;
use Web3\Contract;
use Web3\Utils;
use Illuminate\Support\Facades\Log;
class BlockchainService
{
protected $web3;
protected $contract;
protected $contractAddress;
protected $platformWalletAddress;
protected $platformPrivateKey;
public function __construct()
{
$this->web3 = new Web3(config('blockchain.rpc_url'));
$this->contractAddress = config('blockchain.contract_address');
$this->platformWalletAddress = config('blockchain.platform_wallet');
$this->platformPrivateKey = config('blockchain.platform_private_key');
// ABI ๋ก๋ (Application Binary Interface)
$abi = json_decode(file_get_contents(storage_path('contracts/DonationCampaign.json')), true);
$this->contract = new Contract($this->web3->provider, $abi);
}
/**
* ์๋ก์ด ๊ธฐ๋ถ ์บ ํ์ธ ์ปจํธ๋ํธ ๋ฐฐํฌ
*/
public function deployCampaign(
string $beneficiaryAddress,
float $goalAmount,
int $durationInDays
): array {
try {
$goalAmountWei = Utils::toWei((string)$goalAmount, 'ether');
// ์ปจํธ๋ํธ ๋ฐฐํฌ ํธ๋์ญ์
์์ฑ
$bytecode = file_get_contents(storage_path('contracts/DonationCampaign.bin'));
$transaction = [
'from' => $this->platformWalletAddress,
'data' => $bytecode . $this->encodeConstructorParams(
$beneficiaryAddress,
$this->platformWalletAddress,
$goalAmountWei,
$durationInDays
),
'gas' => Utils::toHex(config('blockchain.gas_limit'), true),
'gasPrice' => Utils::toHex(config('blockchain.gas_price'), true),
];
// ํธ๋์ญ์
์๋ช
๋ฐ ์ ์ก
$signedTx = $this->signTransaction($transaction);
$txHash = $this->sendRawTransaction($signedTx);
// ํธ๋์ญ์
์์์ฆ ๋๊ธฐ
$receipt = $this->waitForTransactionReceipt($txHash);
return [
'success' => true,
'contract_address' => $receipt['contractAddress'],
'transaction_hash' => $txHash,
'block_number' => hexdec($receipt['blockNumber']),
];
} catch (\Exception $e) {
Log::error('Contract deployment failed: ' . $e->getMessage());
return [
'success' => false,
'error' => $e->getMessage(),
];
}
}
/**
* ๊ธฐ๋ถ ํธ๋์ญ์
์์ฑ
*/
public function processDonation(
string $contractAddress,
string $donorAddress,
float $amount,
string $message
): array {
try {
$amountWei = Utils::toWei((string)$amount, 'ether');
// donate ํจ์ ํธ์ถ ๋ฐ์ดํฐ ์ธ์ฝ๋ฉ
$functionData = $this->contract->at($contractAddress)
->getData('donate', $message);
$transaction = [
'from' => $donorAddress,
'to' => $contractAddress,
'value' => Utils::toHex($amountWei, true),
'data' => $functionData,
'gas' => Utils::toHex(config('blockchain.gas_limit'), true),
'gasPrice' => Utils::toHex(config('blockchain.gas_price'), true),
];
return [
'success' => true,
'transaction' => $transaction,
];
} catch (\Exception $e) {
Log::error('Donation processing failed: ' . $e->getMessage());
return [
'success' => false,
'error' => $e->getMessage(),
];
}
}
/**
* ๊ธฐ๋ถ๊ธ ์งํ
*/
public function executeExpenditure(
string $contractAddress,
float $amount,
string $category,
string $description
): array {
try {
$amountWei = Utils::toWei((string)$amount, 'ether');
$functionData = $this->contract->at($contractAddress)
->getData('executeExpenditure', $amountWei, $category, $description);
$transaction = [
'from' => $this->platformWalletAddress,
'to' => $contractAddress,
'data' => $functionData,
'gas' => Utils::toHex(config('blockchain.gas_limit'), true),
'gasPrice' => Utils::toHex(config('blockchain.gas_price'), true),
];
$signedTx = $this->signTransaction($transaction);
$txHash = $this->sendRawTransaction($signedTx);
$receipt = $this->waitForTransactionReceipt($txHash);
return [
'success' => true,
'transaction_hash' => $txHash,
'block_number' => hexdec($receipt['blockNumber']),
'gas_used' => hexdec($receipt['gasUsed']),
];
} catch (\Exception $e) {
Log::error('Expenditure execution failed: ' . $e->getMessage());
return [
'success' => false,
'error' => $e->getMessage(),
];
}
}
/**
* ์บ ํ์ธ ์ ๋ณด ์กฐํ
*/
public function getCampaignInfo(string $contractAddress): array
{
try {
$contract = $this->contract->at($contractAddress);
$info = [];
// ๊ฐ ์ํ ๋ณ์ ์ฝ๊ธฐ
$contract->call('beneficiary', function ($err, $result) use (&$info) {
if (!$err) $info['beneficiary'] = $result[0];
});
$contract->call('goalAmount', function ($err, $result) use (&$info) {
if (!$err) $info['goal_amount'] = Utils::fromWei($result[0], 'ether');
});
$contract->call('currentAmount', function ($err, $result) use (&$info) {
if (!$err) $info['current_amount'] = Utils::fromWei($result[0], 'ether');
});
$contract->call('goalReached', function ($err, $result) use (&$info) {
if (!$err) $info['goal_reached'] = $result[0];
});
$contract->call('getDonationsCount', function ($err, $result) use (&$info) {
if (!$err) $info['donations_count'] = hexdec($result[0]);
});
return [
'success' => true,
'data' => $info,
];
} catch (\Exception $e) {
Log::error('Failed to get campaign info: ' . $e->getMessage());
return [
'success' => false,
'error' => $e->getMessage(),
];
}
}
/**
* ์ด๋ฒคํธ ๋ก๊ทธ ์กฐํ
*/
public function getEventLogs(
string $contractAddress,
string $eventName,
int $fromBlock = 0,
int $toBlock = null
): array {
try {
$toBlock = $toBlock ?? 'latest';
$filter = [
'fromBlock' => Utils::toHex($fromBlock, true),
'toBlock' => $toBlock === 'latest' ? 'latest' : Utils::toHex($toBlock, true),
'address' => $contractAddress,
];
$logs = [];
$this->web3->eth->getLogs($filter, function ($err, $result) use (&$logs) {
if (!$err) {
$logs = $result;
}
});
return [
'success' => true,
'logs' => $logs,
];
} catch (\Exception $e) {
Log::error('Failed to get event logs: ' . $e->getMessage());
return [
'success' => false,
'error' => $e->getMessage(),
];
}
}
/**
* ํธ๋์ญ์
์๋ช
*/
protected function signTransaction(array $transaction): string
{
// ์ค์ ๊ตฌํ์์๋ eth-php ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ
// ์ฌ๊ธฐ์๋ ๊ฐ๋ตํ
return '0xSignedTransactionData';
}
/**
* Raw ํธ๋์ญ์
์ ์ก
*/
protected function sendRawTransaction(string $signedTx): string
{
$txHash = '';
$this->web3->eth->sendRawTransaction($signedTx, function ($err, $result) use (&$txHash) {
if (!$err) {
$txHash = $result;
}
});
return $txHash;
}
/**
* ํธ๋์ญ์
์์์ฆ ๋๊ธฐ
*/
protected function waitForTransactionReceipt(string $txHash, int $maxAttempts = 30): array
{
$attempts = 0;
$receipt = null;
while ($attempts < $maxAttempts) {
$this->web3->eth->getTransactionReceipt($txHash, function ($err, $result) use (&$receipt) {
if (!$err && $result) {
$receipt = $result;
}
});
if ($receipt) {
return $receipt;
}
sleep(2);
$attempts++;
}
throw new \Exception('Transaction receipt not found after ' . $maxAttempts . ' attempts');
}
/**
* ์์ฑ์ ํ๋ผ๋ฏธํฐ ์ธ์ฝ๋ฉ
*/
protected function encodeConstructorParams(...$params): string
{
// ABI ์ธ์ฝ๋ฉ ๋ก์ง
// ์ค์ ๋ก๋ ๋ ๋ณต์กํ ๊ตฌํ ํ์
return '';
}
}
๐ก ์ค์ ํ: ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์
์ ์๊ฐ์ด ๊ฑธ๋ ค. ํนํ ์ด๋๋ฆฌ์ ๋ฉ์ธ๋ท์์๋ ๋ช ๋ถ์ฉ ๊ฑธ๋ฆด ์๋ ์์ด. ๊ทธ๋์ ๋ฐ๋์ ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ฅผ ํด์ผ ํด! ๋ผ๋ผ๋ฒจ์ Queue ์์คํ
์ ํ์ฉํ๋ฉด ์๋ฒฝํ์ง. ๐
๐จ ์ปจํธ๋กค๋ฌ์ ๋น์ฆ๋์ค ๋ก์ง ๊ตฌํ
์ด์ ์ค์ API ์๋ํฌ์ธํธ๋ฅผ ๋ง๋ค์ด๋ณด์. RESTful API ์ค๊ณ ์์น์ ๋ฐ๋ฅผ ๊ฑฐ์ผ.๐ CampaignController
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Campaign;
use App\Models\User;
use App\Services\BlockchainService;
use App\Jobs\DeployCampaignContract;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\DB;
class CampaignController extends Controller
{
protected $blockchainService;
public function __construct(BlockchainService $blockchainService)
{
$this->blockchainService = $blockchainService;
}
/**
* ์บ ํ์ธ ๋ชฉ๋ก ์กฐํ
*/
public function index(Request $request)
{
$query = Campaign::with(['beneficiary', 'donations'])
->where('status', 'active');
// ํํฐ๋ง
if ($request->has('category')) {
$query->where('category', $request->category);
}
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_by', 'created_at');
$sortOrder = $request->get('sort_order', 'desc');
$query->orderBy($sortBy, $sortOrder);
// ํ์ด์ง๋ค์ด์
$campaigns = $query->paginate(12);
// ๊ฐ ์บ ํ์ธ์ ๋ธ๋ก์ฒด์ธ ์ ๋ณด ์ถ๊ฐ
$campaigns->getCollection()->transform(function ($campaign) {
if ($campaign->smart_contract_address) {
$blockchainInfo = $this->blockchainService
->getCampaignInfo($campaign->smart_contract_address);
if ($blockchainInfo['success']) {
$campaign->blockchain_data = $blockchainInfo['data'];
}
}
return $campaign;
});
return response()->json([
'success' => true,
'data' => $campaigns,
]);
}
/**
* ์บ ํ์ธ ์์ธ ์กฐํ
*/
public function show($id)
{
$campaign = Campaign::with([
'beneficiary',
'donations.donor',
'expenditures'
])->findOrFail($id);
// ๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ ์กฐํ
if ($campaign->smart_contract_address) {
$blockchainInfo = $this->blockchainService
->getCampaignInfo($campaign->smart_contract_address);
if ($blockchainInfo['success']) {
$campaign->blockchain_data = $blockchainInfo['data'];
}
// ์ต๊ทผ ์ด๋ฒคํธ ๋ก๊ทธ ์กฐํ
$logs = $this->blockchainService->getEventLogs(
$campaign->smart_contract_address,
'DonationReceived',
$campaign->blockchain_start_block ?? 0
);
if ($logs['success']) {
$campaign->recent_blockchain_events = $logs['logs'];
}
}
return response()->json([
'success' => true,
'data' => $campaign,
]);
}
/**
* ์ ์บ ํ์ธ ์์ฑ
*/
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'title' => 'required|string|max:255',
'description' => 'required|string',
'goal_amount' => 'required|numeric|min:0',
'duration_days' => 'required|integer|min:1|max:365',
'category' => 'required|string',
'beneficiary_wallet' => 'required|string|regex:/^0x[a-fA-F0-9]{40}$/',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors(),
], 422);
}
DB::beginTransaction();
try {
// ์ํ์ ํ์ธ ๋๋ ์์ฑ
$beneficiary = User::where('wallet_address', $request->beneficiary_wallet)
->first();
if (!$beneficiary) {
return response()->json([
'success' => false,
'message' => 'Beneficiary wallet not registered',
], 404);
}
// ์บ ํ์ธ ์์ฑ
$campaign = Campaign::create([
'beneficiary_id' => $beneficiary->id,
'title' => $request->title,
'description' => $request->description,
'goal_amount' => $request->goal_amount,
'start_date' => now(),
'end_date' => now()->addDays($request->duration_days),
'status' => 'draft',
'category' => $request->category,
'metadata' => $request->metadata ?? [],
]);
// ์ค๋งํธ ์ปจํธ๋ํธ ๋ฐฐํฌ ์์
ํ์ ์ถ๊ฐ
DeployCampaignContract::dispatch($campaign);
DB::commit();
return response()->json([
'success' => true,
'message' => 'Campaign created successfully. Smart contract deployment in progress.',
'data' => $campaign,
], 201);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Failed to create campaign',
'error' => $e->getMessage(),
], 500);
}
}
/**
* ์บ ํ์ธ ์์
*/
public function update(Request $request, $id)
{
$campaign = Campaign::findOrFail($id);
// ๊ถํ ์ฒดํฌ
if ($campaign->beneficiary_id !== auth()->id() && !auth()->user()->isAdmin()) {
return response()->json([
'success' => false,
'message' => 'Unauthorized',
], 403);
}
// ํ์ฑํ๋ ์บ ํ์ธ์ ์ผ๋ถ ํ๋๋ง ์์ ๊ฐ๋ฅ
if ($campaign->status === 'active') {
$allowedFields = ['description', 'metadata'];
$updateData = $request->only($allowedFields);
} else {
$updateData = $request->all();
}
$campaign->update($updateData);
return response()->json([
'success' => true,
'message' => 'Campaign updated successfully',
'data' => $campaign,
]);
}
/**
* ์บ ํ์ธ ํ์ฑํ
*/
public function activate($id)
{
$campaign = Campaign::findOrFail($id);
if (!$campaign->smart_contract_address) {
return response()->json([
'success' => false,
'message' => 'Smart contract not deployed yet',
], 400);
}
$campaign->update(['status' => 'active']);
return response()->json([
'success' => true,
'message' => 'Campaign activated successfully',
'data' => $campaign,
]);
}
/**
* ์บ ํ์ธ ํต๊ณ
*/
public function statistics($id)
{
$campaign = Campaign::with(['donations', 'expenditures'])->findOrFail($id);
$stats = [
'total_donations' => $campaign->donations->count(),
'total_amount' => $campaign->donations->sum('amount'),
'unique_donors' => $campaign->donations->unique('donor_id')->count(),
'total_expenditures' => $campaign->expenditures->count(),
'total_spent' => $campaign->expenditures->sum('amount'),
'remaining_amount' => $campaign->current_amount,
'goal_percentage' => ($campaign->current_amount / $campaign->goal_amount) * 100,
'days_remaining' => now()->diffInDays($campaign->end_date, false),
'average_donation' => $campaign->donations->avg('amount'),
];
// ์ผ๋ณ ๊ธฐ๋ถ ์ถ์ด
$dailyDonations = $campaign->donations()
->selectRaw('DATE(created_at) as date, COUNT(*) as count, SUM(amount) as total')
->groupBy('date')
->orderBy('date')
->get();
$stats['daily_trend'] = $dailyDonations;
// ์นดํ
๊ณ ๋ฆฌ๋ณ ์งํ ๋ด์ญ
$expendituresByCategory = $campaign->expenditures()
->selectRaw('category, COUNT(*) as count, SUM(amount) as total')
->groupBy('category')
->get();
$stats['expenditures_by_category'] = $expendituresByCategory;
return response()->json([
'success' => true,
'data' => $stats,
]);
}
}
๐ธ DonationController
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Campaign;
use App\Models\Donation;
use App\Services\BlockchainService;
use App\Jobs\ProcessDonationTransaction;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\DB;
class DonationController extends Controller
{
protected $blockchainService;
public function __construct(BlockchainService $blockchainService)
{
$this->blockchainService = $blockchainService;
}
/**
* ๊ธฐ๋ถํ๊ธฐ
*/
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'campaign_id' => 'required|exists:campaigns,id',
'amount' => 'required|numeric|min:0.001',
'message' => 'nullable|string|max:500',
'is_anonymous' => 'boolean',
'donor_wallet' => 'required|string|regex:/^0x[a-fA-F0-9]{40}$/',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors(),
], 422);
}
$campaign = Campaign::findOrFail($request->campaign_id);
// ์บ ํ์ธ ์ํ ํ์ธ
if ($campaign->status !== 'active') {
return response()->json([
'success' => false,
'message' => 'Campaign is not active',
], 400);
}
if (now()->gt($campaign->end_date)) {
return response()->json([
'success' => false,
'message' => 'Campaign has ended',
], 400);
}
DB::beginTransaction();
try {
// ๊ธฐ๋ถ์ ํ์ธ
$donor = auth()->user();
// ๊ธฐ๋ถ ๋ ์ฝ๋ ์์ฑ
$donation = Donation::create([
'campaign_id' => $campaign->id,
'donor_id' => $donor->id,
'amount' => $request->amount,
'currency' => 'ETH',
'message' => $request->message,
'is_anonymous' => $request->is_anonymous ?? false,
'status' => 'pending',
]);
// ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์
์ค๋น
$txData = $this->blockchainService->processDonation(
$campaign->smart_contract_address,
$request->donor_wallet,
$request->amount,
$request->message ?? ''
);
if (!$txData['success']) {
throw new \Exception('Failed to prepare blockchain transaction');
}
// ํธ๋์ญ์
์ฒ๋ฆฌ ์์
ํ์ ์ถ๊ฐ
ProcessDonationTransaction::dispatch($donation, $txData['transaction']);
DB::commit();
return response()->json([
'success' => true,
'message' => 'Donation initiated. Please confirm the transaction in your wallet.',
'data' => [
'donation' => $donation,
'transaction' => $txData['transaction'],
],
], 201);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Failed to process donation',
'error' => $e->getMessage(),
], 500);
}
}
/**
* ๊ธฐ๋ถ ๋ด์ญ ์กฐํ
*/
public function index(Request $request)
{
$query = Donation::with(['campaign', 'donor']);
// ํํฐ๋ง
if ($request->has('campaign_id')) {
$query->where('campaign_id', $request->campaign_id);
}
if ($request->has('donor_id')) {
$query->where('donor_id', $request->donor_id);
}
if ($request->has('status')) {
$query->where('status', $request->status);
}
// ์ต๋ช
๊ธฐ๋ถ๋ ๊ธฐ๋ถ์ ์ ๋ณด ์จ๊น
$donations = $query->latest()->paginate(20);
$donations->getCollection()->transform(function ($donation) {
if ($donation->is_anonymous && auth()->id() !== $donation->donor_id) {
$donation->donor = null;
$donation->donor_id = null;
}
return $donation;
});
return response()->json([
'success' => true,
'data' => $donations,
]);
}
/**
* ๊ธฐ๋ถ ์์ธ ์กฐํ
*/
public function show($id)
{
$donation = Donation::with(['campaign', 'donor'])->findOrFail($id);
// ์ต๋ช
๊ธฐ๋ถ ์ฒ๋ฆฌ
if ($donation->is_anonymous && auth()->id() !== $donation->donor_id) {
$donation->donor = null;
}
// ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์
์ ๋ณด ์กฐํ
if ($donation->transaction_hash) {
// ์ค์ ๋ก๋ ๋ธ๋ก์ฒด์ธ์์ ํธ๋์ญ์
์ ๋ณด๋ฅผ ๊ฐ์ ธ์์ผ ํจ
$donation->blockchain_confirmation = [
'confirmed' => $donation->status === 'confirmed',
'block_number' => $donation->block_number,
'confirmations' => 12, // ์์
];
}
return response()->json([
'success' => true,
'data' => $donation,
]);
}
/**
* ํธ๋์ญ์
์ํ ์
๋ฐ์ดํธ (์นํ
์ฉ)
*/
public function updateTransactionStatus(Request $request, $id)
{
$donation = Donation::findOrFail($id);
$validator = Validator::make($request->all(), [
'transaction_hash' => 'required|string',
'block_number' => 'nullable|string',
'status' => 'required|in:confirmed,failed',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors(),
], 422);
}
DB::beginTransaction();
try {
$donation->update([
'transaction_hash' => $request->transaction_hash,
'block_number' => $request->block_number,
'status' => $request->status,
'blockchain_confirmed_at' => $request->status === 'confirmed' ? now() : null,
]);
// ์บ ํ์ธ ํ์ฌ ๊ธ์ก ์
๋ฐ์ดํธ
if ($request->status === 'confirmed') {
$campaign = $donation->campaign;
$campaign->increment('current_amount', $donation->amount);
}
DB::commit();
return response()->json([
'success' => true,
'message' => 'Transaction status updated',
'data' => $donation,
]);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Failed to update transaction status',
'error' => $e->getMessage(),
], 500);
}
}
}
โก ๋น๋๊ธฐ ์์ ์ฒ๋ฆฌ (Queue Jobs)
๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ์ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฌ๋๊น, ๋ฐ๋์ ํ๋ก ์ฒ๋ฆฌํด์ผ ํด. ์ฌ์ฉ์๊ฐ ๊ธฐ๋ค๋ฆฌ๊ฒ ํ๋ฉด ์ ๋๊ฑฐ๋ ! ๐ค๐ DeployCampaignContract Job
<?php
namespace App\Jobs;
use App\Models\Campaign;
use App\Services\BlockchainService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class DeployCampaignContract implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $campaign;
public $tries = 3;
public $timeout = 300;
public function __construct(Campaign $campaign)
{
$this->campaign = $campaign;
}
public function handle(BlockchainService $blockchainService)
{
Log::info('Deploying smart contract for campaign: ' . $this->campaign->id);
try {
$beneficiary = $this->campaign->beneficiary;
$result = $blockchainService->deployCampaign(
$beneficiary->wallet_address,
$this->campaign->goal_amount,
now()->diffInDays($this->campaign->end_date)
);
if ($result['success']) {
$this->campaign->update([
'smart_contract_address' => $result['contract_address'],
'deployment_transaction_hash' => $result['transaction_hash'],
'blockchain_start_block' => $result['block_number'],
'status' => 'active',
]);
Log::info('Smart contract deployed successfully', [
'campaign_id' => $this->campaign->id,
'contract_address' => $result['contract_address'],
]);
// ๋ฐฐํฌ ์ฑ๊ณต ์๋ฆผ ๋ฐ์ก
// event(new CampaignContractDeployed($this->campaign));
} else {
throw new \Exception($result['error']);
}
} catch (\Exception $e) {
Log::error('Failed to deploy smart contract', [
'campaign_id' => $this->campaign->id,
'error' => $e->getMessage(),
]);
$this->campaign->update([
'status' => 'failed',
'metadata' => array_merge($this->campaign->metadata ?? [], [
'deployment_error' => $e->getMessage(),
]),
]);
throw $e;
}
}
public function failed(\Throwable $exception)
{
Log::error('Campaign contract deployment job failed permanently', [
'campaign_id' => $this->campaign->id,
'error' => $exception->getMessage(),
]);
// ๊ด๋ฆฌ์์๊ฒ ์๋ฆผ
// event(new CampaignDeploymentFailed($this->campaign, $exception));
}
}
๐ณ ProcessDonationTransaction Job
<?php
namespace App\Jobs;
use App\Models\Donation;
use App\Models\BlockchainLog;
use App\Services\BlockchainService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class ProcessDonationTransaction implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $donation;
public $transactionData;
public $tries = 3;
public $timeout = 300;
public function __construct(Donation $donation, array $transactionData)
{
$this->donation = $donation;
$this->transactionData = $transactionData;
}
public function handle(BlockchainService $blockchainService)
{
Log::info('Processing donation transaction', [
'donation_id' => $this->donation->id,
]);
try {
// ์ค์ ๋ก๋ ์ฌ์ฉ์๊ฐ ๋ฉํ๋ง์คํฌ ๋ฑ์์ ํธ๋์ญ์
์ ์น์ธํด์ผ ํจ
// ์ฌ๊ธฐ์๋ ํธ๋์ญ์
ํด์๋ฅผ ๋ฐ์๋ค๊ณ ๊ฐ์
// ํธ๋์ญ์
๋ชจ๋ํฐ๋ง ์์
$this->monitorTransaction($blockchainService);
} catch (\Exception $e) {
Log::error('Failed to process donation transaction', [
'donation_id' => $this->donation->id,
'error' => $e->getMessage(),
]);
$this->donation->update(['status' => 'failed']);
throw $e;
}
}
protected function monitorTransaction(BlockchainService $blockchainService)
{
// ํธ๋์ญ์
ํด์๋ก ์ํ ํ์ธ
$maxAttempts = 30;
$attempts = 0;
while ($attempts < $maxAttempts) {
$receipt = $blockchainService->getTransactionReceipt(
$this->donation->transaction_hash
);
if ($receipt) {
$this->handleTransactionConfirmed($receipt);
return;
}
sleep(5);
$attempts++;
}
throw new \Exception('Transaction not confirmed within timeout period');
}
protected function handleTransactionConfirmed(array $receipt)
{
$this->donation->update([
'block_number' => hexdec($receipt['blockNumber']),
'status' => 'confirmed',
'blockchain_confirmed_at' => now(),
]);
// ๋ธ๋ก์ฒด์ธ ๋ก๊ทธ ์ ์ฅ
BlockchainLog::create([
'transaction_hash' => $this->donation->transaction_hash,
'block_number' => hexdec($receipt['blockNumber']),
'from_address' => $receipt['from'],
'to_address' => $receipt['to'],
'contract_address' => $receipt['contractAddress'] ?? null,
'event_type' => 'donation',
'event_data' => [
'donation_id' => $this->donation->id,
'amount' => $this->donation->amount,
'campaign_id' => $this->donation->campaign_id,
],
'gas_used' => hexdec($receipt['gasUsed']),
'blockchain_timestamp' => now(),
]);
// ์บ ํ์ธ ๊ธ์ก ์
๋ฐ์ดํธ
$this->donation->campaign->increment('current_amount', $this->donation->amount);
Log::info('Donation transaction confirmed', [
'donation_id' => $this->donation->id,
'transaction_hash' => $this->donation->transaction_hash,
]);
// ๊ธฐ๋ถ ์๋ฃ ์๋ฆผ
// event(new DonationConfirmed($this->donation));
}
}
๐ ํฌ๋ช ์ฑ ๋์๋ณด๋ ๊ตฌํ
์, ์ด์ ๊ฐ์ฅ ์ค์ํ ๋ถ๋ถ! ๋ฐ๋ก ํฌ๋ช ์ฑ์ ๋ณด์ฌ์ฃผ๋ ๋์๋ณด๋์ผ. ์ด๊ฒ ์์ผ๋ฉด ๋ธ๋ก์ฒด์ธ์ ์ฐ๋ ์๋ฏธ๊ฐ ์์์? ๐๐จ Vue.js ์ปดํฌ๋ํธ - TransparencyDashboard
<template>
<div class="transparency-dashboard">
<div class="dashboard-header">
<h2>๐ ํฌ๋ช
์ฑ ๋์๋ณด๋</h2>
<p>๋ชจ๋ ๊ฑฐ๋ ๋ด์ญ์ด ๋ธ๋ก์ฒด์ธ์ ์๊ตฌ์ ์ผ๋ก ๊ธฐ๋ก๋ฉ๋๋ค</p>
</div>
<!-- ์ค์๊ฐ ํต๊ณ -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">๐ฐ</div>
<div class="stat-value">{{ formatCurrency(campaign.current_amount) }}</div>
<div class="stat-label">์ด ๊ธฐ๋ถ๊ธ</div>
<div class="stat-blockchain">
<span class="blockchain-badge">๋ธ๋ก์ฒด์ธ ๊ฒ์ฆ๋จ</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">๐ฅ</div>
<div class="stat-value">{{ campaign.donors_count }}</div>
<div class="stat-label">๊ธฐ๋ถ์ ์</div>
</div>
<div class="stat-card">
<div class="stat-icon">๐ค</div>
<div class="stat-value">{{ formatCurrency(campaign.total_spent) }}</div>
<div class="stat-label">์งํ ๊ธ์ก</div>
<div class="stat-blockchain">
<span class="blockchain-badge">๋ธ๋ก์ฒด์ธ ๊ฒ์ฆ๋จ</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">๐ต</div>
<div class="stat-value">{{ formatCurrency(campaign.remaining_amount) }}</div>
<div class="stat-label">์์ฌ ๊ธ์ก</div>
</div>
</div>
<!-- ์งํ๋ฅ -->
<div class="progress-section">
<div class="progress-header">
<span>๋ชฉํ ๋ฌ์ฑ๋ฅ </span>
<span class="progress-percentage">{{ goalPercentage }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: goalPercentage + '%' }"></div>
</div>
<div class="progress-info">
<span>ํ์ฌ: {{ formatCurrency(campaign.current_amount) }}</span>
<span>๋ชฉํ: {{ formatCurrency(campaign.goal_amount) }}</span>
</div>
</div>
<!-- ํญ ๋ฉ๋ด -->
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-button', { active: activeTab === tab.id }]"
@click="activeTab = tab.id"
>
{{ tab.icon }} {{ tab.label }}
</button>
</div>
<!-- ๊ธฐ๋ถ ๋ด์ญ -->
<div v-show="activeTab === 'donations'" class="tab-content">
<h3>๐ ๊ธฐ๋ถ ๋ด์ญ</h3>
<div class="transaction-list">
<div
v-for="donation in donations"
:key="donation.id"
class="transaction-item"
>
<div class="transaction-icon">๐ฐ</div>
<div class="transaction-details">
<div class="transaction-header">
<span class="donor-name">
{{ donation.is_anonymous ? '์ต๋ช
์ ๊ธฐ๋ถ์' : donation.donor.name }}
</span>
<span class="transaction-amount">
{{ formatCurrency(donation.amount) }} ETH
</span>
</div>
<div class="transaction-meta">
<span class="transaction-date">
{{ formatDate(donation.created_at) }}
</span>
<a
:href="getEtherscanUrl(donation.transaction_hash)"
target="_blank"
class="blockchain-link"
>
๐ ๋ธ๋ก์ฒด์ธ์์ ํ์ธ
</a>
</div>
<div v-if="donation.message" class="transaction-message">
"{{ donation.message }}"
</div>
</div>
<div class="transaction-status">
<span :class="['status-badge', donation.status]">
{{ getStatusText(donation.status) }}
</span>
</div>
</div>
</div>
<button v-if="hasMoreDonations" @click="loadMoreDonations" class="load-more">
๋ ๋ณด๊ธฐ
</button>
</div>
<!-- ์งํ ๋ด์ญ -->
<div v-show="activeTab === 'expenditures'" class="tab-content">
<h3>๐ค ์งํ ๋ด์ญ</h3>
<div class="transaction-list">
<div
v-for="expenditure in expenditures"
:key="expenditure.id"
class="transaction-item expenditure"
>
<div class="transaction-icon">๐ค</div>
<div class="transaction-details">
<div class="transaction-header">
<span class="category-badge">{{ expenditure.category }}</span>
<span class="transaction-amount">
-{{ formatCurrency(expenditure.amount) }} ETH
</span>
</div>
<div class="transaction-description">
{{ expenditure.description }}
</div>
<div class="transaction-meta">
<span class="transaction-date">
{{ formatDate(expenditure.executed_at) }}
</span>
<a
:href="getEtherscanUrl(expenditure.transaction_hash)"
target="_blank"
class="blockchain-link"
>
๐ ๋ธ๋ก์ฒด์ธ์์ ํ์ธ
</a>
<a
v-if="expenditure.receipt_url"
:href="expenditure.receipt_url"
target="_blank"
class="receipt-link"
>
๐ ์์์ฆ ๋ณด๊ธฐ
</a>
</div>
</div>
<div class="transaction-status">
<span :class="['status-badge', expenditure.status]">
{{ getStatusText(expenditure.status) }}
</span>
</div>
</div>
</div>
</div>
<!-- ๋ธ๋ก์ฒด์ธ ์ด๋ฒคํธ -->
<div v-show="activeTab === 'blockchain'" class="tab-content">
<h3>โ๏ธ ๋ธ๋ก์ฒด์ธ ์ด๋ฒคํธ</h3>
<div class="blockchain-events">
<div
v-for="event in blockchainEvents"
:key="event.id"
class="event-item"
>
<div class="event-type">{{ event.event_type }}</div>
<div class="event-details">
<div class="event-data">{{ formatEventData(event.event_data) }}</div>
<div class="event-meta">
<span>๋ธ๋ก: {{ event.block_number }}</span>
<span>๊ฐ์ค: {{ event.gas_used }}</span>
<span>{{ formatDate(event.blockchain_timestamp) }}</span>
</div>
</div>
<a
:href="getEtherscanUrl(event.transaction_hash)"
target="_blank"
class="event-link"
>
์์ธ๋ณด๊ธฐ โ
</a>
</div>
</div>
</div>
<!-- ํต๊ณ ์ฐจํธ -->
<div v-show="activeTab === 'statistics'" class="tab-content">
<h3>๐ ํต๊ณ ๋ถ์</h3>
<div class="chart-container">
<h4>์ผ๋ณ ๊ธฐ๋ถ ์ถ์ด</h4>
<canvas ref="donationTrendChart"></canvas>
</div>
<div class="chart-container">
<h4>์นดํ
๊ณ ๋ฆฌ๋ณ ์งํ ๋ด์ญ</h4>
<canvas ref="expenditureCategoryChart"></canvas>
</div>
<div class="stats-table">
<h4>์์ธ ํต๊ณ</h4>
<table>
<tr>
<td>ํ๊ท ๊ธฐ๋ถ ๊ธ์ก</td>
<td>{{ formatCurrency(statistics.average_donation) }} ETH</td>
</tr>
<tr>
<td>์ต๋ ๊ธฐ๋ถ ๊ธ์ก</td>
<td>{{ formatCurrency(statistics.max_donation) }} ETH</td>
</tr>
<tr>
<td>์ต์ ๊ธฐ๋ถ ๊ธ์ก</td>
<td>{{ formatCurrency(statistics.min_donation) }} ETH</td>
</tr>
<tr>
<td>์ฌ๊ธฐ๋ถ ๋น์จ</td>
<td>{{ statistics.repeat_donor_rate }}%</td>
</tr>
</table>
</div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue';
import { Chart } from 'chart.js/auto';
import axios from 'axios';
export default {
name: 'TransparencyDashboard',
props: {
campaignId: {
type: Number,
required: true
}
},
setup(props) {
const campaign = ref({});
const donations = ref([]);
const expenditures = ref([]);
const blockchainEvents = ref([]);
const statistics = ref({});
const activeTab = ref('donations');
const hasMoreDonations = ref(false);
const tabs = [
{ id: 'donations', label: '๊ธฐ๋ถ ๋ด์ญ', icon: '๐' },
{ id: 'expenditures', label: '์งํ ๋ด์ญ', icon: '๐ค' },
{ id: 'blockchain', label: '๋ธ๋ก์ฒด์ธ', icon: 'โ๏ธ' },
{ id: 'statistics', label: 'ํต๊ณ', icon: '๐' }
];
const goalPercentage = computed(() => {
if (!campaign.value.goal_amount) return 0;
return Math.min(
(campaign.value.current_amount / campaign.value.goal_amount) * 100,
100
).toFixed(1);
});
const loadCampaignData = async () => {
try {
const response = await axios.get(`/api/campaigns/${props.campaignId}`);
campaign.value = response.data.data;
} catch (error) {
console.error('Failed to load campaign data:', error);
}
};
const loadDonations = async () => {
try {
const response = await axios.get(`/api/donations`, {
params: { campaign_id: props.campaignId }
});
donations.value = response.data.data.data;
hasMoreDonations.value = response.data.data.next_page_url !== null;
} catch (error) {
console.error('Failed to load donations:', error);
}
};
const loadExpenditures = async () => {
try {
const response = await axios.get(`/api/expenditures`, {
params: { campaign_id: props.campaignId }
});
expenditures.value = response.data.data;
} catch (error) {
console.error('Failed to load expenditures:', error);
}
};
const loadBlockchainEvents = async () => {
try {
const response = await axios.get(`/api/blockchain-logs`, {
params: { campaign_id: props.campaignId }
});
blockchainEvents.value = response.data.data;
} catch (error) {
console.error('Failed to load blockchain events:', error);
}
};
const loadStatistics = async () => {
try {
const response = await axios.get(`/api/campaigns/${props.campaignId}/statistics`);
statistics.value = response.data.data;
// ์ฐจํธ ๊ทธ๋ฆฌ๊ธฐ
drawCharts();
} catch (error) {
console.error('Failed to load statistics:', error);
}
};
const drawCharts = () => {
// ์ผ๋ณ ๊ธฐ๋ถ ์ถ์ด ์ฐจํธ
// Chart.js ๊ตฌํ...
};
const formatCurrency = (amount) => {
return new Intl.NumberFormat('ko-KR', {
minimumFractionDigits: 2,
maximumFractionDigits: 4
}).format(amount);
};
const formatDate = (date) => {
return new Date(date).toLocaleString('ko-KR');
};
const getEtherscanUrl = (txHash) => {
const network = process.env.VUE_APP_BLOCKCHAIN_NETWORK;
const baseUrl = network === 'mainnet'
? 'https://etherscan.io'
: `https://${network}.etherscan.io`;
return `${baseUrl}/tx/${txHash}`;
};
const getStatusText = (status) => {
const statusMap = {
pending: '๋๊ธฐ์ค',
confirmed: 'ํ์ธ๋จ',
failed: '์คํจ'
};
return statusMap[status] || status;
};
const formatEventData = (data) => {
return JSON.stringify(data, null, 2);
};
onMounted(() => {
loadCampaignData();
loadDonations();
loadExpenditures();
loadBlockchainEvents();
loadStatistics();
// ์ค์๊ฐ ์
๋ฐ์ดํธ (์น์์ผ ๋๋ ํด๋ง)
setInterval(loadCampaignData, 30000);
});
return {
campaign,
donations,
expenditures,
blockchainEvents,
statistics,
activeTab,
tabs,
goalPercentage,
hasMoreDonations,
formatCurrency,
formatDate,
getEtherscanUrl,
getStatusText,
formatEventData
};
}
};
</script>
<style>.tx-header{background:linear-gradient(135deg,#f5f7fa 0%,#e8ecf1 100%);
border-radius:16px;
padding:40px;
text-align:center;
margin-bottom:40px;
box-shadow:0 4px 15px rgba(0,0,0,0.08);}
.tx-section{margin:50px 0;
padding:30px;
background:#fafbfc;
border-radius:12px;
border-left:4px solid #7c9cbf;}
.tx-highlight{background:linear-gradient(120deg,#fef6e4 0%,#f3e7d3 100%);
padding:25px;
border-radius:10px;
margin:25px 0;
border:2px solid #e8d5b7;}
.tx-tip{background:#e8f4f8;
padding:20px;
border-radius:10px;
margin:25px 0;
border-left:5px solid #4a9ebb;}
.tx-warning{background:#fff4e6;
padding:20px;
border-radius:10px;
margin:25px 0;
border-left:5px solid #f59e0b;}
.tx-table-wrap{overflow-x:auto;
margin:25px 0;}
.tx-table{width:100%;
border-collapse:collapse;
background:#fff;
border-radius:8px;
overflow:hidden;}
.tx-table th{background:#7c9cbf;
color:#fff;
padding:15px;
text-align:left;
font-weight:600;}
.tx-table td{padding:12px 15px;
border-bottom:1px solid #e5e7eb;}
.tx-step{background:#f0f4f8;
padding:20px;
margin:20px 0;
border-radius:10px;
border-left:5px solid #5a7a9b;}
.tx-card{background:#fff;
padding:25px;
border-radius:12px;
margin:20px 0;
box-shadow:0 2px 10px rgba(0,0,0,0.06);}
.tx-emphasis{color:#5a7a9b;
font-weight:600;}
.tx-subtitle{color:#6b7280;
font-size:1.1em;
margin:15px 0;}</style>
๐ฏ ๋์๋ณด๋์ ํต์ฌ ๊ธฐ๋ฅ
1. ์ค์๊ฐ ๋ฐ์ดํฐ ๋๊ธฐํ- ๋ธ๋ก์ฒด์ธ๊ณผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์ดํฐ๋ฅผ ์ค์๊ฐ์ผ๋ก ๋๊ธฐํ
- WebSocket์ด๋ ํด๋ง์ ํตํ ์๋ ์ ๋ฐ์ดํธ
2. ๋ธ๋ก์ฒด์ธ ๊ฒ์ฆ ๋ฐฐ์ง
- ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋ก๋ ๋ฐ์ดํฐ์์ ๋ช ํํ ํ์
- Etherscan ๋งํฌ๋ก ์ง์ ํ์ธ ๊ฐ๋ฅ
3. ๋ค์ํ ์๊ฐํ
- Chart.js๋ฅผ ํ์ฉํ ํต๊ณ ์ฐจํธ
- ์งํ๋ฅ ๋ฐ, ์นดํ ๊ณ ๋ฆฌ๋ณ ๋ถ์ ๋ฑ
4. ํฌ๋ช ํ ์งํ ๋ด์ญ
- ๋ชจ๋ ์ง์ถ์ ๋ํ ์์ธ ์ค๋ช
- ์์์ฆ ์ฒจ๋ถ ๊ธฐ๋ฅ
- ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ํด์ ์ ๊ณต
๐ ๋ณด์๊ณผ ๊ฐ์ธ์ ๋ณด ๋ณดํธ
๋ธ๋ก์ฒด์ธ์ ํฌ๋ช ํ์ง๋ง, ๊ทธ๋ ๋ค๊ณ ๋ชจ๋ ์ ๋ณด๋ฅผ ๋ค ๊ณต๊ฐํ๋ฉด ์ ๋ผ. ๊ฐ์ธ์ ๋ณด ๋ณดํธ๋ ์ค์ํ๊ฑฐ๋ ! ๐ก๏ธ๐ญ ์ต๋ช ๊ธฐ๋ถ ์ฒ๋ฆฌ
๊ธฐ๋ถ์๊ฐ ์ต๋ช ์ ์ํ ๊ฒฝ์ฐ, ๋ธ๋ก์ฒด์ธ์๋ ์ง๊ฐ ์ฃผ์๋ง ๊ธฐ๋กํ๊ณ ์ค์ ์ ์ ์ ๋ณด๋ ์ํธํํด์ ์ ์ฅํด์ผ ํด:<?php
namespace App\Services;
use Illuminate\Support\Facades\Crypt;
class PrivacyService
{
/**
* ๋ฏผ๊ฐํ ์ ๋ณด ์ํธํ
*/
public function encryptSensitiveData(array $data): string
{
return Crypt::encryptString(json_encode($data));
}
/**
* ์ํธํ๋ ์ ๋ณด ๋ณตํธํ
*/
public function decryptSensitiveData(string $encrypted): array
{
return json_decode(Crypt::decryptString($encrypted), true);
}
/**
* ์ง๊ฐ ์ฃผ์ ๋ง์คํน
*/
public function maskWalletAddress(string $address): string
{
if (strlen($address) < 10) {
return $address;
}
return substr($address, 0, 6) . '...' . substr($address, -4);
}
/**
* ์ต๋ช
๊ธฐ๋ถ์ ํ์๋ช
์์ฑ
*/
public function generateAnonymousName(int $donationId): string
{
$adjectives = ['๋ฐ๋ปํ', '์ ํ', '๋น๋๋', '์๋ฆ๋ค์ด', '๊ณ ๋ง์ด'];
$nouns = ['๋ง์', '์ฒ์ฌ', '๋ณ', '์๊ธธ', 'ํฌ๋ง'];
$adj = $adjectives[$donationId % count($adjectives)];
$noun = $nouns[($donationId * 7) % count($nouns)];
return $adj . ' ' . $noun;
}
}
๐ ์ค๋งํธ ์ปจํธ๋ํธ ๋ณด์ ํจํด
์ค๋งํธ ์ปจํธ๋ํธ๋ ํ๋ฒ ๋ฐฐํฌํ๋ฉด ์์ ์ด ๋ถ๊ฐ๋ฅํด. ๊ทธ๋์ ๋ณด์์ด ์ ๋ง ์ค์ํด! ๋ค์ ํจํด๋ค์ ๋ฐ๋์ ์ ์ฉํด์ผ ํด:
1. Checks-Effects-Interactions ํจํด
์ฌ์ง์ ๊ณต๊ฒฉ(Reentrancy Attack)์ ๋ฐฉ์งํ๊ธฐ ์ํ ํจํด์ด์ผ:
์ฌ์ง์ ๊ณต๊ฒฉ(Reentrancy Attack)์ ๋ฐฉ์งํ๊ธฐ ์ํ ํจํด์ด์ผ:
function withdraw(uint256 amount) public {
// 1. Checks: ์กฐ๊ฑด ํ์ธ
require(balances[msg.sender] >= amount, "Insufficient balance");
// 2. Effects: ์ํ ๋ณ๊ฒฝ
balances[msg.sender] -= amount;
// 3. Interactions: ์ธ๋ถ ํธ์ถ
payable(msg.sender).transfer(amount);
}
2. Pull over Push ํจํด
์๊ธ์ ๋ณด๋ด๋ ๋์ ๋ฐ์๊ฐ๊ฒ ํ๋ ํจํด:
์๊ธ์ ๋ณด๋ด๋ ๋์ ๋ฐ์๊ฐ๊ฒ ํ๋ ํจํด:
mapping(address => uint256) public pendingWithdrawals;
function initiateWithdrawal(uint256 amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
pendingWithdrawals[msg.sender] += amount;
}
function withdraw() public {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0);
pendingWithdrawals[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
3. ๊ธด๊ธ ์ ์ง(Circuit Breaker) ํจํด
๋ฌธ์ ๋ฐ์ ์ ์ปจํธ๋ํธ๋ฅผ ์ผ์ ์ ์งํ ์ ์๋ ๊ธฐ๋ฅ:
๋ฌธ์ ๋ฐ์ ์ ์ปจํธ๋ํธ๋ฅผ ์ผ์ ์ ์งํ ์ ์๋ ๊ธฐ๋ฅ:
bool public paused = false;
address public owner;
modifier whenNotPaused() {
require(!paused, "Contract is paused");
_;
}
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
function pause() public onlyOwner {
paused = true;
}
function unpause() public onlyOwner {
paused = false;
}
function donate() public payable whenNotPaused {
// ๊ธฐ๋ถ ๋ก์ง
}
โ ๏ธ ๋ณด์ ์ฒดํฌ๋ฆฌ์คํธ:
- โ ์ฌ์ง์ ๊ณต๊ฒฉ ๋ฐฉ์ง
- โ ์ ์ ์ค๋ฒํ๋ก์ฐ/์ธ๋ํ๋ก์ฐ ๋ฐฉ์ง (Solidity 0.8.0 ์ด์์์๋ ์๋)
- โ ๊ฐ์ค ํ๋ ๊ณ ๋ ค
- โ ์ ๊ทผ ์ ์ด ๊ตฌํ
- โ ์ธ๋ถ ํธ์ถ ์ต์ํ
- โ ์ด๋ฒคํธ ๋ก๊น
- โ ์ ๊ทธ๋ ์ด๋ ๊ฐ๋ฅ์ฑ ๊ณ ๋ ค (ํ๋ก์ ํจํด)
- โ ๊ฐ์ฌ(Audit) ๋ฐ๊ธฐ
๐ ๋ฐฐํฌ ๋ฐ ์ด์
์, ์ด์ ์ค์ ๋ก ๋ฐฐํฌํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์! ํ ์คํธ๋ท๋ถํฐ ์์ํด์ ๋ฉ์ธ๋ท๊น์ง ๋จ๊ณ๋ณ๋ก ์งํํ ๊ฑฐ์ผ.๐งช ํ ์คํธ๋ท ๋ฐฐํฌ (Sepolia)
๋จผ์ ํ ์คํธ๋ท์์ ์ถฉ๋ถํ ํ ์คํธํด์ผ ํด. ์ค์ํด๋ ์ค์ ๋์ด ๋ ์๊ฐ์ง ์์ผ๋๊น! ๐# Hardhat ์ค์น ๋ฐ ์ค์
npm install --save-dev hardhat @nomiclabs/hardhat-ethers ethers
# hardhat.config.js
require("@nomiclabs/hardhat-ethers");
module.exports = {
solidity: "0.8.19",
networks: {
sepolia: {
url: process.env.SEPOLIA_RPC_URL,
accounts: [process.env.PRIVATE_KEY],
chainId: 11155111
}
}
};
# ๋ฐฐํฌ ์คํฌ๋ฆฝํธ (scripts/deploy.js)
const hre = require("hardhat");
async function main() {
const DonationCampaign = await hre.ethers.getContractFactory("DonationCampaign");
const beneficiary = "0xBeneficiaryAddress";
const platformAdmin = "0xPlatformAdminAddress";
const goalAmount = hre.ethers.utils.parseEther("10.0");
const durationInDays = 30;
console.log("Deploying DonationCampaign...");
const campaign = await DonationCampaign.deploy(
beneficiary,
platformAdmin,
goalAmount,
durationInDays
);
await campaign.deployed();
console.log("DonationCampaign deployed to:", campaign.address);
// ๋ฐฐํฌ ์ ๋ณด ์ ์ฅ
const fs = require('fs');
const deploymentInfo = {
address: campaign.address,
network: "sepolia",
deployer: (await hre.ethers.getSigners())[0].address,
timestamp: new Date().toISOString(),
constructorArgs: {
beneficiary,
platformAdmin,
goalAmount: goalAmount.toString(),
durationInDays
}
};
fs.writeFileSync(
'deployments/sepolia.json',
JSON.stringify(deploymentInfo, null, 2)
);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
๐ญ ๋ผ๋ผ๋ฒจ ์ ํ๋ฆฌ์ผ์ด์ ๋ฐฐํฌ
๋ผ๋ผ๋ฒจ ์ฑ์ ์ผ๋ฐ์ ์ธ PHP ์ ํ๋ฆฌ์ผ์ด์ ๋ฐฐํฌ ๋ฐฉ์์ ๋ฐ๋ฅด๋ฉด ๋ผ. ๋ค๋ง ๋ธ๋ก์ฒด์ธ ๊ด๋ จ ์ค์ ์ ์ถ๊ฐ๋ก ํด์ค์ผ ํด:# .env.production
APP_ENV=production
APP_DEBUG=false
APP_URL=https://your-donation-platform.com
# Database
DB_CONNECTION=pgsql
DB_HOST=your-db-host
DB_DATABASE=donation_platform
DB_USERNAME=your-username
DB_PASSWORD=your-password
# Blockchain
BLOCKCHAIN_NETWORK=mainnet
BLOCKCHAIN_RPC_URL=https://mainnet.infura.io/v3/YOUR_PROJECT_ID
BLOCKCHAIN_CHAIN_ID=1
# Redis (for queues)
REDIS_HOST=your-redis-host
REDIS_PASSWORD=your-redis-password
# Queue
QUEUE_CONNECTION=redis
# ๋ฐฐํฌ ์คํฌ๋ฆฝํธ (deploy.sh)
#!/bin/bash
echo "๐ Starting deployment..."
# Git pull
git pull origin main
# Install dependencies
composer install --no-dev --optimize-autoloader
# Run migrations
php artisan migrate --force
# Clear and cache config
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Restart queue workers
php artisan queue:restart
# Restart PHP-FPM
sudo systemctl restart php8.2-fpm
echo "โ
Deployment completed!"
๐ ๋ชจ๋ํฐ๋ง ์ค์
์ด์ ์ค์๋ ๋ฐ๋์ ๋ชจ๋ํฐ๋ง์ ํด์ผ ํด. ํนํ ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ์ ์คํจํ ์๋ ์์ผ๋๊น!<?php
namespace App\Console\Commands;
use App\Models\Donation;
use App\Models\Campaign;
use App\Services\BlockchainService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class MonitorBlockchainTransactions extends Command
{
protected $signature = 'blockchain:monitor';
protected $description = 'Monitor pending blockchain transactions';
protected $blockchainService;
public function __construct(BlockchainService $blockchainService)
{
parent::__construct();
$this->blockchainService = $blockchainService;
}
public function handle()
{
$this->info('Monitoring blockchain transactions...');
// ๋๊ธฐ ์ค์ธ ๊ธฐ๋ถ ํธ๋์ญ์
ํ์ธ
$pendingDonations = Donation::where('status', 'pending')
->whereNotNull('transaction_hash')
->where('created_at', '>', now()->subHours(24))
->get();
foreach ($pendingDonations as $donation) {
$this->checkTransactionStatus($donation);
}
// ๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ์ DB ๋๊ธฐํ ํ์ธ
$this->syncBlockchainData();
$this->info('Monitoring completed.');
}
protected function checkTransactionStatus(Donation $donation)
{
try {
$receipt = $this->blockchainService->getTransactionReceipt(
$donation->transaction_hash
);
if ($receipt) {
if ($receipt['status'] === '0x1') {
// ์ฑ๊ณต
$donation->update([
'status' => 'confirmed',
'block_number' => hexdec($receipt['blockNumber']),
'blockchain_confirmed_at' => now(),
]);
$this->info("โ
Donation {$donation->id} confirmed");
} else {
// ์คํจ
$donation->update(['status' => 'failed']);
$this->error("โ Donation {$donation->id} failed");
// ๊ด๋ฆฌ์์๊ฒ ์๋ฆผ
Log::error('Donation transaction failed', [
'donation_id' => $donation->id,
'transaction_hash' => $donation->transaction_hash,
]);
}
} else {
// ์์ง ๋๊ธฐ ์ค
$hoursSinceDonation = $donation->created_at->diffInHours(now());
if ($hoursSinceDonation > 2) {
$this->warn("โฐ Donation {$donation->id} pending for {$hoursSinceDonation} hours");
}
}
} catch (\Exception $e) {
$this->error("Error checking donation {$donation->id}: " . $e->getMessage());
}
}
protected function syncBlockchainData()
{
$campaigns = Campaign::where('status', 'active')
->whereNotNull('smart_contract_address')
->get();
foreach ($campaigns as $campaign) {
try {
$blockchainInfo = $this->blockchainService->getCampaignInfo(
$campaign->smart_contract_address
);
if ($blockchainInfo['success']) {
$blockchainAmount = $blockchainInfo['data']['current_amount'];
$dbAmount = $campaign->current_amount;
// ๊ธ์ก ๋ถ์ผ์น ํ์ธ
if (abs($blockchainAmount - $dbAmount) > 0.0001) {
$this->warn("โ ๏ธ Amount mismatch for campaign {$campaign->id}");
$this->warn(" Blockchain: {$blockchainAmount}, DB: {$dbAmount}");
Log::warning('Campaign amount mismatch', [
'campaign_id' => $campaign->id,
'blockchain_amount' => $blockchainAmount,
'db_amount' => $dbAmount,
]);
}
}
} catch (\Exception $e) {
$this->error("Error syncing campaign {$campaign->id}: " . $e->getMessage());
}
}
}
}
// ์ค์ผ์ค๋ฌ์ ๋ฑ๋ก (app/Console/Kernel.php)
protected function schedule(Schedule $schedule)
{
// 5๋ถ๋ง๋ค ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์
๋ชจ๋ํฐ๋ง
$schedule->command('blockchain:monitor')
->everyFiveMinutes()
->withoutOverlapping();
// ๋งค์ผ ์์ ์ ํต๊ณ ์ง๊ณ
$schedule->command('statistics:aggregate')
->daily();
}
๐ก ์ค์ ํ๊ณผ ๋ฒ ์คํธ ํ๋ํฐ์ค
์ค์ ๋ก ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์ ๋ฐฐ์ด ๋ ธํ์ฐ๋ค์ ๊ณต์ ํ ๊ฒ! ์ด๊ฑฐ ์ง์ง ์ค์ํด. ๐โก ์ฑ๋ฅ ์ต์ ํ
1. ๋ธ๋ก์ฒด์ธ ์กฐํ ์ต์ํ
๋ธ๋ก์ฒด์ธ ์กฐํ๋ ๋๋ฆฌ๊ณ ๋น์ฉ์ด ๋ค์ด. ๊ทธ๋์ ์บ์ฑ์ ์ ๊ทน ํ์ฉํด์ผ ํด:
๋ธ๋ก์ฒด์ธ ์กฐํ๋ ๋๋ฆฌ๊ณ ๋น์ฉ์ด ๋ค์ด. ๊ทธ๋์ ์บ์ฑ์ ์ ๊ทน ํ์ฉํด์ผ ํด:
<?php
use Illuminate\Support\Facades\Cache;
class CampaignService
{
public function getCampaignWithBlockchainData($campaignId)
{
$cacheKey = "campaign.{$campaignId}.blockchain";
return Cache::remember($cacheKey, 300, function () use ($campaignId) {
$campaign = Campaign::findOrFail($campaignId);
if ($campaign->smart_contract_address) {
$blockchainData = $this->blockchainService
->getCampaignInfo($campaign->smart_contract_address);
$campaign->blockchain_data = $blockchainData['data'];
}
return $campaign;
});
}
}
2. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธ๋ฑ์ฑ
์์ฃผ ์กฐํํ๋ ์ปฌ๋ผ์๋ ๋ฐ๋์ ์ธ๋ฑ์ค๋ฅผ ๊ฑธ์ด์ค:
์์ฃผ ์กฐํํ๋ ์ปฌ๋ผ์๋ ๋ฐ๋์ ์ธ๋ฑ์ค๋ฅผ ๊ฑธ์ด์ค:
// ๋ง์ด๊ทธ๋ ์ด์
์์
$table->index(['campaign_id', 'status', 'created_at']);
$table->index('transaction_hash');
$table->index(['donor_id', 'created_at']);
3. Eager Loading ํ์ฉ
N+1 ์ฟผ๋ฆฌ ๋ฌธ์ ๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ๊ด๊ณ๋ฅผ ๋ฏธ๋ฆฌ ๋ก๋ํด:
N+1 ์ฟผ๋ฆฌ ๋ฌธ์ ๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ๊ด๊ณ๋ฅผ ๋ฏธ๋ฆฌ ๋ก๋ํด:
// โ ๋์ ์
$campaigns = Campaign::all();
foreach ($campaigns as $campaign) {
echo $campaign->beneficiary->name; // N+1 ์ฟผ๋ฆฌ ๋ฐ์!
}
// โ
์ข์ ์
$campaigns = Campaign::with('beneficiary', 'donations')->get();
foreach ($campaigns as $campaign) {
echo $campaign->beneficiary->name; // ๋จ 2๊ฐ์ ์ฟผ๋ฆฌ๋ง!
}
๐ ์ฌ๋ฅ๋ท๊ณผ์ ์ฐ๋
์ด๋ฐ ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ถ ํ๋ซํผ์ ๊ฐ๋ฐํ ์ ์๋ ๋ฅ๋ ฅ ์์ฒด๊ฐ ํ๋์ ์ฌ๋ฅ์ด์ผ! ์ฌ๋ฅ๋ท์์๋ ์ด๋ฐ ์ ๋ฌธ์ ์ธ ๊ฐ๋ฐ ์๋น์ค๋ฅผ ์ ๊ณตํ๊ฑฐ๋, ๋ฐ๋๋ก ํ์ํ ๊ฐ๋ฐ์๋ฅผ ์ฐพ์ ์๋ ์์ด.์๋ฅผ ๋ค์ด:
- "๋ธ๋ก์ฒด์ธ ์ค๋งํธ ์ปจํธ๋ํธ ๊ฐ๋ฐ" ์๋น์ค ๋ฑ๋ก
- "๋ผ๋ผ๋ฒจ ๊ธฐ๋ฐ Web3 ํ๋ซํผ ๊ตฌ์ถ" ํ๋ก์ ํธ ์๋ขฐ
- "๋ธ๋ก์ฒด์ธ ๋ณด์ ๊ฐ์ฌ" ์ ๋ฌธ๊ฐ ์ฐพ๊ธฐ
ํนํ ์์ฆ์ Web3์ ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ์ ์์๊ฐ ์์ฒญ ๋ง์์, ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์์ ์ ์ ๋ฌธ์ฑ์ ํ์ฉํ๋ฉด ์ข์ ๊ธฐํ๋ฅผ ๋ง๋ค ์ ์์ด! ๐
๐ ๋๋ฒ๊น ํ
๋ธ๋ก์ฒด์ธ ํธ๋์ญ์
๋๋ฒ๊น
ํธ๋์ญ์ ์ด ์คํจํ์ ๋ ์์ธ์ ์ฐพ๋ ๋ฐฉ๋ฒ:
1. Etherscan์์ ํธ๋์ญ์ ํด์ ์กฐํ
2. "State" ํญ์์ ์ํ ๋ณํ ํ์ธ
3. "Logs" ํญ์์ ์ด๋ฒคํธ ํ์ธ
4. "Internal Txns" ํญ์์ ๋ด๋ถ ํธ์ถ ํ์ธ
5. Revert ๋ฉ์์ง ํ์ธ
Hardhat Console๋ก ๋ก์ปฌ ํ ์คํธ:
ํธ๋์ญ์ ์ด ์คํจํ์ ๋ ์์ธ์ ์ฐพ๋ ๋ฐฉ๋ฒ:
1. Etherscan์์ ํธ๋์ญ์ ํด์ ์กฐํ
2. "State" ํญ์์ ์ํ ๋ณํ ํ์ธ
3. "Logs" ํญ์์ ์ด๋ฒคํธ ํ์ธ
4. "Internal Txns" ํญ์์ ๋ด๋ถ ํธ์ถ ํ์ธ
5. Revert ๋ฉ์์ง ํ์ธ
Hardhat Console๋ก ๋ก์ปฌ ํ ์คํธ:
npx hardhat console --network localhost
const Campaign = await ethers.getContractFactory("DonationCampaign");
const campaign = await Campaign.attach("0xContractAddress");
// ์ํ ํ์ธ
const goalAmount = await campaign.goalAmount();
console.log("Goal:", ethers.utils.formatEther(goalAmount));
// ํจ์ ํธ์ถ ํ
์คํธ
const tx = await campaign.donate("Test message", { value: ethers.utils.parseEther("0.1") });
await tx.wait();
console.log("Donation successful!");
๐ฐ ๊ฐ์ค๋น ์ต์ ํ
์ด๋๋ฆฌ์ ๋ฉ์ธ๋ท์์๋ ๊ฐ์ค๋น๊ฐ ๋น์ธ. ์ต์ ํ๊ฐ ํ์์ผ!
๊ฐ์ค๋น ์ ์ฝ ํ
ํฌ๋:
1. Storage ์ฌ์ฉ ์ต์ํ
- Storage๋ ๊ฐ์ฅ ๋น์ธ. ๊ฐ๋ฅํ๋ฉด Memory๋ Calldata ์ฌ์ฉ
- ๋ถํ์ํ ์ํ ๋ณ์ ์ ๊ฑฐ
2. ๋ฐฐ์ด ๋์ ๋งคํ ์ฌ์ฉ
- ๋ฐฐ์ด ์ํ๋ ๊ฐ์ค๋น๊ฐ ๋ง์ด ๋ค์ด
- ๋งคํ์ O(1) ์กฐํ ๊ฐ๋ฅ
3. Short Circuit ํ๊ฐ ํ์ฉ
- `require(a && b)` ๋์ `require(a); require(b);`
- ์ฒซ ๋ฒ์งธ ์กฐ๊ฑด์์ ์คํจํ๋ฉด ๋ ๋ฒ์งธ ๊ฒ์ฌ ์ ํจ
4. ์ด๋ฒคํธ ํ์ฉ
- ๋ฐ์ดํฐ๋ฅผ Storage์ ์ ์ฅํ๋ ๋์ Event๋ก ๋ก๊น
- ๋์ค์ ๋ก๊ทธ์์ ์กฐํ ๊ฐ๋ฅ
5. ๋ฐฐ์น ์ฒ๋ฆฌ
- ์ฌ๋ฌ ํธ๋์ญ์ ์ ํ๋๋ก ๋ฌถ์ด์ ์ฒ๋ฆฌ
- ์: ์ฌ๋ฌ ๊ธฐ๋ถ์์๊ฒ ํ ๋ฒ์ ํ๋ถ
1. Storage ์ฌ์ฉ ์ต์ํ
- Storage๋ ๊ฐ์ฅ ๋น์ธ. ๊ฐ๋ฅํ๋ฉด Memory๋ Calldata ์ฌ์ฉ
- ๋ถํ์ํ ์ํ ๋ณ์ ์ ๊ฑฐ
2. ๋ฐฐ์ด ๋์ ๋งคํ ์ฌ์ฉ
- ๋ฐฐ์ด ์ํ๋ ๊ฐ์ค๋น๊ฐ ๋ง์ด ๋ค์ด
- ๋งคํ์ O(1) ์กฐํ ๊ฐ๋ฅ
3. Short Circuit ํ๊ฐ ํ์ฉ
- `require(a && b)` ๋์ `require(a); require(b);`
- ์ฒซ ๋ฒ์งธ ์กฐ๊ฑด์์ ์คํจํ๋ฉด ๋ ๋ฒ์งธ ๊ฒ์ฌ ์ ํจ
4. ์ด๋ฒคํธ ํ์ฉ
- ๋ฐ์ดํฐ๋ฅผ Storage์ ์ ์ฅํ๋ ๋์ Event๋ก ๋ก๊น
- ๋์ค์ ๋ก๊ทธ์์ ์กฐํ ๊ฐ๋ฅ
5. ๋ฐฐ์น ์ฒ๋ฆฌ
- ์ฌ๋ฌ ํธ๋์ญ์ ์ ํ๋๋ก ๋ฌถ์ด์ ์ฒ๋ฆฌ
- ์: ์ฌ๋ฌ ๊ธฐ๋ถ์์๊ฒ ํ ๋ฒ์ ํ๋ถ
๐ ํ์ต ๋ฆฌ์์ค์ ๋ค์ ๋จ๊ณ
์ฌ๊ธฐ๊น์ง ๋ฐ๋ผ์๋ค๋ฉด ์ถํํด! ๐ ์ด์ ๊ธฐ๋ณธ์ ์ธ ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ถ ํ๋ซํผ์ ๋ง๋ค ์ ์์ ๊ฑฐ์ผ.ํ์ง๋ง ๋ฐฐ์์๋ ๋์ด ์์ง. ๋ ๊น์ด ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด ์ด๋ฐ ๊ฒ๋ค์ ์ถ์ฒํด:
๐ ์ถ์ฒ ํ์ต ์๋ฃ
๋ธ๋ก์ฒด์ธ & ์ค๋งํธ ์ปจํธ๋ํธ:- Solidity ๊ณต์ ๋ฌธ์ (docs.soliditylang.org)
- OpenZeppelin Contracts (๊ฒ์ฆ๋ ์ค๋งํธ ์ปจํธ๋ํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ)
- Ethereum.org ๊ฐ๋ฐ์ ๊ฐ์ด๋
- CryptoZombies (๊ฒ์์ผ๋ก ๋ฐฐ์ฐ๋ Solidity)
๋ผ๋ผ๋ฒจ & PHP:
- Laravel ๊ณต์ ๋ฌธ์
- Laracasts (๋น๋์ค ๊ฐ์)
- Laravel Daily (์ค์ ํ)
Web3 ๊ฐ๋ฐ:
- web3.js ๋ฌธ์
- ethers.js ๋ฌธ์
- Hardhat ๊ฐ๋ฐ ํ๊ฒฝ
- Truffle Suite
๐ ๋ค์ ๋จ๊ณ ํ๋ก์ ํธ ์์ด๋์ด
๊ธฐ๋ณธ ํ๋ซํผ์ ๋ง๋ค์๋ค๋ฉด, ์ด๋ฐ ๊ธฐ๋ฅ๋ค์ ์ถ๊ฐํด๋ณผ ์ ์์ด:1. NFT ๊ธฐ๋ถ ์ฆ์
- ๊ธฐ๋ถ์์๊ฒ NFT ํํ์ ๊ฐ์ฌ ์ฆ์ ๋ฐํ
- ๊ธฐ๋ถ ๊ธ์ก์ ๋ฐ๋ผ ๋ค๋ฅธ ๋ฑ๊ธ์ NFT
2. DAO ๊ฑฐ๋ฒ๋์ค
- ๊ธฐ๋ถ๊ธ ์ฌ์ฉ์ฒ๋ฅผ ๊ธฐ๋ถ์๋ค์ด ํฌํ๋ก ๊ฒฐ์
- ํ ํฐ ๊ธฐ๋ฐ ํฌํ ์์คํ
3. ํฌ๋ก์ค์ฒด์ธ ์ง์
- ์ด๋๋ฆฌ์๋ฟ๋ง ์๋๋ผ ํด๋ฆฌ๊ณค, BSC ๋ฑ ๋ค๋ฅธ ์ฒด์ธ ์ง์
- ๋ธ๋ฆฟ์ง๋ฅผ ํตํ ์ฒด์ธ ๊ฐ ์์ฐ ์ด๋
4. DeFi ํตํฉ
- ๊ธฐ๋ถ๊ธ์ ์คํ ์ดํนํด์ ์ด์ ์์ต ์ฐฝ์ถ
- ์์ต๊ธ๋ ๊ธฐ๋ถ์ ์ฌ์ฉ
5. ์์ ๊ธฐ๋ฅ
- ๊ธฐ๋ถ์ ์ปค๋ฎค๋ํฐ
- ์บ ํ์ธ ๊ณต์ ๋ฐ ๋ฐ์ด๋ด ๋ง์ผํ
- ๊ธฐ๋ถ ์ฑ๋ฆฐ์ง ์ด๋ฒคํธ
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์, ์ฌ๊ธฐ๊น์ง ์ ๋ง ๊ธด ์ฌ์ ์ด์์ด! ๐โโ๏ธ๐จ์ฐ๋ฆฌ๋ ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋ณธ ๊ฐ๋ ๋ถํฐ ์์ํด์, ๋ผ๋ผ๋ฒจ๋ก ์ค์ ๋์ํ๋ ๊ธฐ๋ถ ํ๋ซํผ์ ๋ง๋๋ ๋ฐฉ๋ฒ๊น์ง ๋ชจ๋ ์ดํด๋ดค์ด. ์ค๋งํธ ์ปจํธ๋ํธ ์์ฑ, ๋ฐฑ์๋ API ๊ตฌํ, ํ๋ก ํธ์๋ ๋์๋ณด๋, ๋ณด์, ๋ฐฐํฌ, ๋ชจ๋ํฐ๋ง๊น์ง... ์ ๋ง ๋ง์ ๋ด์ฉ์ ๋ค๋ค์ง?
๐ ํต์ฌ ํฌ์ธํธ ์ ๋ฆฌ
1. ๋ธ๋ก์ฒด์ธ์ ๊ฐ์น
- ํฌ๋ช ์ฑ: ๋ชจ๋ ๊ฑฐ๋๊ฐ ๊ณต๊ฐ์ ์ผ๋ก ๊ฒ์ฆ ๊ฐ๋ฅ
- ๋ถ๋ณ์ฑ: ํ๋ฒ ๊ธฐ๋ก๋ ๋ฐ์ดํฐ๋ ๋ณ๊ฒฝ ๋ถ๊ฐ
- ํ์ค์ํ: ์ค์ ๊ธฐ๊ด ์์ด๋ ์ ๋ขฐ ๊ตฌ์ถ
2. ํ์ด๋ธ๋ฆฌ๋ ์ ๊ทผ
- ๋ธ๋ก์ฒด์ธ: ์ค์ํ ๊ฑฐ๋ ๊ธฐ๋ก
- ๋ฐ์ดํฐ๋ฒ ์ด์ค: ๋น ๋ฅธ ์กฐํ์ ๊ฒ์
- ๋์ ์ฅ์ ์ ๊ฒฐํฉํ ์ค์ฉ์ ์ค๊ณ
3. ์ฌ์ฉ์ ๊ฒฝํ
- ๋ณต์กํ ๋ธ๋ก์ฒด์ธ ๊ธฐ์ ์ ์จ๊ธฐ๊ณ
- ์ง๊ด์ ์ธ ์ธํฐํ์ด์ค ์ ๊ณต
- ์ค์๊ฐ ํผ๋๋ฐฑ๊ณผ ํฌ๋ช ํ ์ ๋ณด ๊ณต๊ฐ
4. ๋ณด์๊ณผ ์ ๋ขฐ
- ์ค๋งํธ ์ปจํธ๋ํธ ๋ณด์ ํจํด ์ ์ฉ
- ๊ฐ์ธ์ ๋ณด ๋ณดํธ์ ํฌ๋ช ์ฑ์ ๊ท ํ
- ์ง์์ ์ธ ๋ชจ๋ํฐ๋ง๊ณผ ๊ฐ์ฌ
- ํฌ๋ช ์ฑ: ๋ชจ๋ ๊ฑฐ๋๊ฐ ๊ณต๊ฐ์ ์ผ๋ก ๊ฒ์ฆ ๊ฐ๋ฅ
- ๋ถ๋ณ์ฑ: ํ๋ฒ ๊ธฐ๋ก๋ ๋ฐ์ดํฐ๋ ๋ณ๊ฒฝ ๋ถ๊ฐ
- ํ์ค์ํ: ์ค์ ๊ธฐ๊ด ์์ด๋ ์ ๋ขฐ ๊ตฌ์ถ
2. ํ์ด๋ธ๋ฆฌ๋ ์ ๊ทผ
- ๋ธ๋ก์ฒด์ธ: ์ค์ํ ๊ฑฐ๋ ๊ธฐ๋ก
- ๋ฐ์ดํฐ๋ฒ ์ด์ค: ๋น ๋ฅธ ์กฐํ์ ๊ฒ์
- ๋์ ์ฅ์ ์ ๊ฒฐํฉํ ์ค์ฉ์ ์ค๊ณ
3. ์ฌ์ฉ์ ๊ฒฝํ
- ๋ณต์กํ ๋ธ๋ก์ฒด์ธ ๊ธฐ์ ์ ์จ๊ธฐ๊ณ
- ์ง๊ด์ ์ธ ์ธํฐํ์ด์ค ์ ๊ณต
- ์ค์๊ฐ ํผ๋๋ฐฑ๊ณผ ํฌ๋ช ํ ์ ๋ณด ๊ณต๊ฐ
4. ๋ณด์๊ณผ ์ ๋ขฐ
- ์ค๋งํธ ์ปจํธ๋ํธ ๋ณด์ ํจํด ์ ์ฉ
- ๊ฐ์ธ์ ๋ณด ๋ณดํธ์ ํฌ๋ช ์ฑ์ ๊ท ํ
- ์ง์์ ์ธ ๋ชจ๋ํฐ๋ง๊ณผ ๊ฐ์ฌ
๐ญ ๋ง์ง๋ง ์กฐ์ธ
๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ์ ๊ณ์ ์งํํ๊ณ ์์ด. ์๋ก์ด ๊ธฐ์ ๊ณผ ํจํด์ด ๋์์์ด ๋์ค๊ณ ์์ง. ๊ทธ๋์ ์ค์ํ ๊ฑด ๊ณ์ ๋ฐฐ์ฐ๊ณ ์คํํ๋ ์์ธ์ผ.์ฒ์์๋ ์ด๋ ต๊ฒ ๋๊ปด์ง ์ ์์ด. ๋๋ ์ฒ์ ๋ธ๋ก์ฒด์ธ์ ์ ํ์ ๋ "์ด๊ฒ ๋ญ ์๋ฆฌ์ผ?" ํ๊ฑฐ๋ . ๐ ํ์ง๋ง ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ๋ฐ๋ผํ๋ค ๋ณด๋ฉด, ์ด๋์ ๋ค๊ฐ ์ง์ ์ค๋งํธ ์ปจํธ๋ํธ๋ฅผ ์์ฑํ๊ณ ๋ฐฐํฌํ๊ณ ์์ ๊ฑฐ์ผ!
๊ทธ๋ฆฌ๊ณ ๊ธฐ์ตํด. ๊ธฐ์ ์ ๋๊ตฌ์ผ ๋ฟ์ด์ผ. ์ง์ง ์ค์ํ ๊ฑด ์ด ๊ธฐ์ ๋ก ์ด๋ค ๊ฐ์น๋ฅผ ๋ง๋ค์ด๋ผ ๊ฒ์ธ๊ฐ์ผ. ์ฐ๋ฆฌ๊ฐ ๋ง๋ ํฌ๋ช ํ ๊ธฐ๋ถ ํ๋ซํผ์ด ์ค์ ๋ก ๋ ๋ง์ ์ฌ๋๋ค์ด ๊ธฐ๋ถ์ ์ฐธ์ฌํ๊ฒ ๋ง๋ค๊ณ , ์ง์ง ๋์์ด ํ์ํ ๊ณณ์ ์์์ด ์ ๋ฌ๋๊ฒ ํ๋ค๋ฉด? ๊ทธ๊ฒ ๋ฐ๋ก ๊ฐ๋ฐ์๋ก์ ํ ์ ์๋ ๊ฐ์ฅ ๋ฉ์ง ์ผ์ด ์๋๊น? ๐
์, ์ด์ ๋ค ์ฐจ๋ก์ผ! ์ด ๊ฐ์ด๋๋ฅผ ์ฐธ๊ณ ํด์ ๋ฉ์ง ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ด. ๊ทธ๋ฆฌ๊ณ ๋งํ๋ ๋ถ๋ถ์ด ์์ผ๋ฉด ์ปค๋ฎค๋ํฐ์ ๋ฌผ์ด๋ณด๊ณ , ์ฝ๋๋ฅผ ๊ณต์ ํ๊ณ , ํจ๊ป ์ฑ์ฅํ์!
ํ์ดํ ! ๐โจ
๐ ๊ฐ์ฌํฉ๋๋ค!
์ด ๊ธด ๊ธ์ ๋๊น์ง ์ฝ์ด์ฃผ์
์ ์ ๋ง ๊ฐ์ฌํด์.
์ฌ๋ฌ๋ถ์ ์ฝ๋๊ฐ ์ธ์์ ๋ ํฌ๋ช
ํ๊ณ ์ ๋ขฐํ ์ ์๋ ๊ณณ์ผ๋ก ๋ง๋ค๊ธฐ๋ฅผ ๋ฐ๋๋๋ค.
Happy Coding! ๐ปโจ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
๋๊ธ์ ๋ถ๋ฌ์ค๋ ์ค์
๋๋ค.
๋๊ธ์ ๋ถ๋ฌ์ค์ง ๋ชปํ์ต๋๋ค. ์ฐ๊ฒฐ ์ํ๋ฅผ ํ์ธํ๊ณ ๋ค์ ์๋ํด ์ฃผ์ธ์.
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.