Ver3.0 ๐ ๋ผ๋ผ๋ฒจ๊ณผ ์๋ฆฌ๋ํฐ๋ก ๋๋ง์ ์ปค์คํ ํ ํฐ ๋ฐํ ์์คํ ์๋ฒฝ ๊ตฌ์ถํ๊ธฐ

๐ ๋ผ๋ผ๋ฒจ๊ณผ ์๋ฆฌ๋ํฐ๋ก ๋๋ง์ ์ปค์คํ ํ ํฐ ๋ฐํ ์์คํ ์๋ฒฝ ๊ตฌ์ถํ๊ธฐ
๋ธ๋ก์ฒด์ธ๊ณผ ์น ๊ฐ๋ฐ์ ๋ง๋จ, ์ค์ ํ๋ก์ ํธ๋ก ๋ฐฐ์ฐ๋ ํ ํฐ ์ด์ฝ๋ ธ๋ฏธ ๐ฏ
์๋ ! ํน์ ๋๋ง์ ์ํธํํ ํ ํฐ์ ๋ง๋ค์ด๋ณด๊ณ ์ถ์๋ ์ ์์ด? ๐ค ์์ฆ NFT, DeFi, ๋ฉํ๋ฒ์ค ๋ฑ ๋ธ๋ก์ฒด์ธ ๊ธฐ์ ์ด ๋์ธ์ธ๋ฐ, ๋ง์ ์ง์ ํ ํฐ์ ๋ฐํํ๋ ค๋ฉด ์ด๋์๋ถํฐ ์์ํด์ผ ํ ์ง ๋ง๋งํ์ง? ์ค๋์ ๋ผ๋ผ๋ฒจ(Laravel)์ด๋ผ๋ ๊ฐ๋ ฅํ PHP ํ๋ ์์ํฌ์ ์๋ฆฌ๋ํฐ(Solidity)๋ผ๋ ์ค๋งํธ ์ปจํธ๋ํธ ์ธ์ด๋ฅผ ๊ฒฐํฉํด์, ์์ ํ ์๋ํ๋ ์ปค์คํ ํ ํฐ ๋ฐํ ์์คํ ์ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์๋ ค์ค๊ฒ! ๐ช
์ด ๊ธ์ ๋ค ์ฝ๊ณ ๋๋ฉด, ๋๋ ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ฐ์ ํ ํฐ ์์คํ ์ ๊ตฌ์ถํ ์ ์์ ๊ฑฐ์ผ. ์ค์ ๋ก ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ์ด๋ฐ ๊ธฐ์ ์ ํ์ฉํด ์ฌ๋ฅ ๊ฑฐ๋์ ํ ํฐ ์ด์ฝ๋ ธ๋ฏธ๋ฅผ ์ ๋ชฉ์ํฌ ์ ์๊ฑฐ๋ ! ์, ๊ทธ๋ผ ์์ํด๋ณผ๊น? ๐
๐ฏ ์ ๋ผ๋ผ๋ฒจ๊ณผ ์๋ฆฌ๋ํฐ๋ฅผ ํจ๊ป ์ฌ์ฉํ ๊น?
๋จผ์ ์ด ๋ ๊ธฐ์ ์ ์ ์กฐํฉํ๋์ง๋ถํฐ ์ดํดํด๋ณด์! ๊ฐ๊ฐ์ ์ญํ ์ด ๋ช ํํ๊ฒ ๋ค๋ฅด๊ฑฐ๋ . ๐ง
์๋ฆฌ๋ํฐ๋ ์ด๋๋ฆฌ์ ๋ธ๋ก์ฒด์ธ์์ ์ค๋งํธ ์ปจํธ๋ํธ๋ฅผ ์์ฑํ๋ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด์ผ. ์ฝ๊ฒ ๋งํด์, ๋ธ๋ก์ฒด์ธ ์์์ ์๋์ผ๋ก ์คํ๋๋ ๊ณ์ฝ์๋ฅผ ์ฝ๋๋ก ์์ฑํ๋ ๊ฑฐ์ง. ํ ํฐ์ ๋ฐํ, ์ ์ก, ์๊ฐ ๋ฑ์ ํต์ฌ ๋ก์ง์ด ์ฌ๊ธฐ์ ๋ค์ด๊ฐ. ํ๋ฒ ๋ฐฐํฌ๋๋ฉด ๋๊ตฌ๋ ์์๋ก ์์ ํ ์ ์์ด์ ์ ๋ขฐ์ฑ๊ณผ ํฌ๋ช ์ฑ์ด ๋ณด์ฅ๋ผ! ๐
๋ผ๋ผ๋ฒจ์ PHP ๊ธฐ๋ฐ์ ์น ํ๋ ์์ํฌ๋ก, ์ฌ์ฉ์ ์ธํฐํ์ด์ค์ ๋ฐฑ์๋ ๋ก์ง์ ๋ด๋นํด. ๋ธ๋ก์ฒด์ธ๊ณผ ์ง์ ์ํธ์์ฉํ๋ฉด์ ์ฌ์ฉ์์๊ฒ ์น์ํ ์น ํ๊ฒฝ์ ์ ๊ณตํ๋ ๊ฑฐ์ง. ํ์๊ฐ์ , ๋ก๊ทธ์ธ, ๋์๋ณด๋, ํ ํฐ ์ ์ก ์์ฒญ ๋ฑ ์ฌ์ฉ์ ๊ฒฝํ(UX)์ ์ฑ ์์ง๋ ๋ถ๋ถ์ด์ผ. ๐ฅ๏ธ
๊ฒฐ๊ตญ ์ด ๋์ ํฉ์น๋ฉด? ๋ธ๋ก์ฒด์ธ์ ๋ณด์์ฑ๊ณผ ํฌ๋ช ์ฑ + ์น์ ํธ๋ฆฌํจ๊ณผ ์ ๊ทผ์ฑ์ ๋์์ ๊ฐ์ง ์๋ฒฝํ ์์คํ ์ด ํ์ํ๋ ๊ฑฐ์ผ! ๋ง์น ์ํผํ์ด๋ก ํ์ ๊ฐ์ ๋๋? ๐ฆธโโ๏ธ๐ฆธโโ๏ธ
๐ ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ ์ค๋นํ๊ธฐ
๋ณธ๊ฒฉ์ ์ผ๋ก ์์ํ๊ธฐ ์ ์ ํ์ํ ๋๊ตฌ๋ค์ ์ค๋นํด์ผ ํด. ์๋ฆฌํ๊ธฐ ์ ์ ์ฌ๋ฃ ์์งํ๋ ๊ฒ์ฒ๋ผ ๋ง์ด์ผ! ๐จโ๐ณ
๐ฆ ํ์ ์ค์น ํญ๋ชฉ
1. Node.js์ npm
์๋ฐ์คํฌ๋ฆฝํธ ๋ฐํ์ ํ๊ฒฝ์ด์ผ. ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ ๋๊ตฌ๋ค์ด ๋๋ถ๋ถ Node.js ๊ธฐ๋ฐ์ด๋ผ์ ํ์์ผ!
# Node.js ์ค์น ํ์ธ
node --version
npm --version
2. Composer
PHP ์์กด์ฑ ๊ด๋ฆฌ ๋๊ตฌ๋ก, ๋ผ๋ผ๋ฒจ ์ค์น์ ํ์ํด.
# Composer ์ค์น ํ์ธ
composer --version
3. PHP 8.0 ์ด์
๋ผ๋ผ๋ฒจ ์ต์ ๋ฒ์ ์ ์ฌ์ฉํ๋ ค๋ฉด PHP 8.0 ์ด์์ด ํ์ํด.
4. MySQL ๋๋ PostgreSQL
์ฌ์ฉ์ ์ ๋ณด, ํธ๋์ญ์
๊ธฐ๋ก ๋ฑ์ ์ ์ฅํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ผ.
5. Ganache
๋ก์ปฌ ์ด๋๋ฆฌ์ ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ ํ๊ฒฝ์ด์ผ. ์ค์ ์ด๋๋ฆฌ์ ๋คํธ์ํฌ์ ๋ฐฐํฌํ๊ธฐ ์ ์ ํ
์คํธํ ์ ์์ด!
6. MetaMask
๋ธ๋ผ์ฐ์ ๊ธฐ๋ฐ ์ด๋๋ฆฌ์ ์ง๊ฐ์ด์ผ. ํ
์คํธํ ๋ ์ ์ฉํ๊ฒ ์ฐ์ฌ.
7. Truffle ๋๋ Hardhat
์ค๋งํธ ์ปจํธ๋ํธ ๊ฐ๋ฐ, ํ
์คํธ, ๋ฐฐํฌ๋ฅผ ๋์์ฃผ๋ ํ๋ ์์ํฌ์ผ.
# Truffle ์ค์น
npm install -g truffle
# ๋๋ Hardhat ์ค์น
npm install --save-dev hardhat
๐ ๋ผ๋ผ๋ฒจ ํ๋ก์ ํธ ์์ฑ
์, ์ด์ ๋ผ๋ผ๋ฒจ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด์!
# ๋ผ๋ผ๋ฒจ ์ค์น
composer create-project laravel/laravel token-system
# ํ๋ก์ ํธ ๋๋ ํ ๋ฆฌ๋ก ์ด๋
cd token-system
# ๊ฐ๋ฐ ์๋ฒ ์คํ
php artisan serve
๋ธ๋ผ์ฐ์ ์์ http://localhost:8000์ ์ ์ํ๋ฉด ๋ผ๋ผ๋ฒจ ํ์ ํ์ด์ง๊ฐ ๋ณด์ผ ๊ฑฐ์ผ! ๐
โ๏ธ ํ๊ฒฝ ์ค์ ํ์ผ ๊ตฌ์ฑ
.env ํ์ผ์ ์ด์ด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์ ์ ํด์ค:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=token_system
DB_USERNAME=root
DB_PASSWORD=your_password
# ๋ธ๋ก์ฒด์ธ ๊ด๋ จ ์ค์ ์ถ๊ฐ
BLOCKCHAIN_NETWORK=http://127.0.0.1:7545
CONTRACT_ADDRESS=
OWNER_PRIVATE_KEY=
๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์์ฑํ๊ณ ๋ง์ด๊ทธ๋ ์ด์ ์ ์คํํด:
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ฑ (MySQL CLI์์)
CREATE DATABASE token_system;
# ๋ผ๋ผ๋ฒจ ๋ง์ด๊ทธ๋ ์ด์
์คํ
php artisan migrate
๐ ERC-20 ํ ํฐ ์ค๋งํธ ์ปจํธ๋ํธ ์์ฑํ๊ธฐ
์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ํ ํฐ์ ๋ง๋ค์ด๋ณผ ์๊ฐ์ด์ผ! ERC-20์ ์ด๋๋ฆฌ์์์ ๊ฐ์ฅ ๋๋ฆฌ ์ฌ์ฉ๋๋ ํ ํฐ ํ์ค์ด์ผ. ๋ง์น USB ๊ท๊ฒฉ์ฒ๋ผ, ๋ชจ๋ ๊ฑฐ๋์์ ์ง๊ฐ์ด ์ด ํ์ค์ ๋ฐ๋ฅด๋ฉด ํธํ์ฑ์ด ๋ณด์ฅ๋์ง! ๐
๐ Truffle ํ๋ก์ ํธ ์ด๊ธฐํ
๋ผ๋ผ๋ฒจ ํ๋ก์ ํธ ๋ฃจํธ์ ๋ธ๋ก์ฒด์ธ ํด๋๋ฅผ ๋ง๋ค์:
# ๋ธ๋ก์ฒด์ธ ๋๋ ํ ๋ฆฌ ์์ฑ
mkdir blockchain
cd blockchain
# Truffle ์ด๊ธฐํ
truffle init
์ด๋ ๊ฒ ํ๋ฉด ๋ค์๊ณผ ๊ฐ์ ๊ตฌ์กฐ๊ฐ ์์ฑ๋ผ:
blockchain/
โโโ contracts/ # ์ค๋งํธ ์ปจํธ๋ํธ ์์ค ์ฝ๋
โโโ migrations/ # ๋ฐฐํฌ ์คํฌ๋ฆฝํธ
โโโ test/ # ํ
์คํธ ํ์ผ
โโโ truffle-config.js # ์ค์ ํ์ผ
โ๏ธ ํ ํฐ ์ปจํธ๋ํธ ์์ฑ
contracts/MyToken.sol ํ์ผ์ ์์ฑํ๊ณ ๋ค์ ์ฝ๋๋ฅผ ์์ฑํด:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
// ํ ํฐ์ ์์์ ์๋ฆฌ์
uint8 private _decimals;
// ์ต๋ ๋ฐํ๋ (์ต์
)
uint256 private _cap;
// ์ด๋ฒคํธ ์ ์
event TokensMinted(address indexed to, uint256 amount);
event TokensBurned(address indexed from, uint256 amount);
constructor(
string memory name,
string memory symbol,
uint8 decimals_,
uint256 initialSupply,
uint256 cap_
) ERC20(name, symbol) {
_decimals = decimals_;
_cap = cap_;
// ์ด๊ธฐ ๊ณต๊ธ๋ ๋ฐํ
_mint(msg.sender, initialSupply * 10 ** decimals_);
}
// ์์์ ์๋ฆฌ์ ๋ฐํ
function decimals() public view virtual override returns (uint8) {
return _decimals;
}
// ์ต๋ ๋ฐํ๋ ๋ฐํ
function cap() public view returns (uint256) {
return _cap;
}
// ํ ํฐ ๋ฐํ (์์ ์๋ง ๊ฐ๋ฅ)
function mint(address to, uint256 amount) public onlyOwner {
require(to != address(0), "Cannot mint to zero address");
if (_cap > 0) {
require(totalSupply() + amount <= _cap, "Cap exceeded");
}
_mint(to, amount);
emit TokensMinted(to, amount);
}
// ํ ํฐ ์๊ฐ
function burn(uint256 amount) public {
_burn(msg.sender, amount);
emit TokensBurned(msg.sender, amount);
}
// ํน์ ์ฃผ์์ ํ ํฐ ์๊ฐ (์น์ธ ํ์)
function burnFrom(address account, uint256 amount) public {
uint256 currentAllowance = allowance(account, msg.sender);
require(currentAllowance >= amount, "Burn amount exceeds allowance");
_approve(account, msg.sender, currentAllowance - amount);
_burn(account, amount);
emit TokensBurned(account, amount);
}
// ์ผ๊ด ์ ์ก ๊ธฐ๋ฅ
function batchTransfer(address[] memory recipients, uint256[] memory amounts)
public
returns (bool)
{
require(recipients.length == amounts.length, "Arrays length mismatch");
require(recipients.length > 0, "Empty arrays");
for (uint256 i = 0; i < recipients.length; i++) {
require(recipients[i] != address(0), "Invalid recipient");
_transfer(msg.sender, recipients[i], amounts[i]);
}
return true;
}
}
์! ์ฝ๋๊ฐ ์ข ๊ธธ์ง? ํ๋์ฉ ๋ฏ์ด๋ณด์! ๐
1. ERC20 ์์: OpenZeppelin์ ๊ฒ์ฆ๋ ERC-20 ๊ตฌํ์ ์์๋ฐ์์ ๊ธฐ๋ณธ ๊ธฐ๋ฅ(์ ์ก, ์์ก ์กฐํ ๋ฑ)์ ์๋์ผ๋ก ๊ฐ์ ธ์.
2. Ownable ์์: ์์ ์ ๊ถํ ๊ด๋ฆฌ ๊ธฐ๋ฅ์ ์ถ๊ฐํด. ํน์ ํจ์๋ ์์ ์๋ง ์คํํ ์ ์๊ฒ ๋ง๋ค์ด.
3. mint ํจ์: ์๋ก์ด ํ ํฐ์ ๋ฐํํ๋ ํจ์์ผ. ์์ ์๋ง ํธ์ถํ ์ ์์ด์ ๋ฌด๋ถ๋ณํ ๋ฐํ์ ๋ง์.
4. burn ํจ์: ํ ํฐ์ ์๊ฐํด์ ์ด ๊ณต๊ธ๋์ ์ค์ผ ์ ์์ด. ๋ํ๋ ์ด์ ๋ฉ์ปค๋์ฆ์ ์ ์ฉํด!
5. batchTransfer ํจ์: ์ฌ๋ฌ ์ฃผ์์ ํ ๋ฒ์ ํ ํฐ์ ๋ณด๋ผ ์ ์์ด. ์์ด๋๋ํ ๋ ๊ฐ์ค๋น๋ฅผ ์ ์ฝํ ์ ์์ง!
6. cap (์ต๋ ๋ฐํ๋): ํ ํฐ์ ์ต๋ ๊ณต๊ธ๋์ ์ ํํ ์ ์์ด. ๋นํธ์ฝ์ธ์ฒ๋ผ ํฌ์์ฑ์ ๋ง๋ค ์ ์์ง!
๐ฆ OpenZeppelin ์ค์น
์ ์ฝ๋์์ OpenZeppelin ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ผ๋ ์ค์นํด์ค์ผ ํด:
# blockchain ๋๋ ํ ๋ฆฌ์์ ์คํ
npm init -y
npm install @openzeppelin/contracts
โ๏ธ Truffle ์ค์
truffle-config.js ํ์ผ์ ์์ ํด:
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 7545, // Ganache ๊ธฐ๋ณธ ํฌํธ
network_id: "*",
},
testnet: {
provider: () => new HDWalletProvider(
process.env.MNEMONIC,
`https://rinkeby.infura.io/v3/${process.env.INFURA_KEY}`
),
network_id: 4,
gas: 5500000,
}
},
compilers: {
solc: {
version: "0.8.17",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
}
}
};
๐ ๋ฐฐํฌ ์คํฌ๋ฆฝํธ ์์ฑ
migrations/2_deploy_token.js ํ์ผ์ ์์ฑํด:
const MyToken = artifacts.require("MyToken");
module.exports = function(deployer) {
const name = "JaeNung Token";
const symbol = "JNT";
const decimals = 18;
const initialSupply = 1000000; // 100๋ง ๊ฐ
const cap = 10000000; // ์ต๋ 1000๋ง ๊ฐ
deployer.deploy(
MyToken,
name,
symbol,
decimals,
initialSupply,
cap
);
};
์ด์ Ganache๋ฅผ ์คํํ๊ณ ์ปจํธ๋ํธ๋ฅผ ๋ฐฐํฌํด๋ณด์!
# ์ปดํ์ผ
truffle compile
# ๋ฐฐํฌ
truffle migrate --network development
์ฑ๊ณตํ๋ฉด ์ปจํธ๋ํธ ์ฃผ์๊ฐ ์ถ๋ ฅ๋ ๊ฑฐ์ผ. ์ด ์ฃผ์๋ฅผ ์ ๊ธฐ์ตํด๋ฌ! ๋์ค์ ๋ผ๋ผ๋ฒจ์์ ์ฌ์ฉํ ๊ฑฐ๊ฑฐ๋ . ๐
๐ ๋ผ๋ผ๋ฒจ๊ณผ ๋ธ๋ก์ฒด์ธ ์ฐ๋ํ๊ธฐ
์, ์ด์ ๊ฐ์ฅ ์ค์ํ ๋ถ๋ถ์ด์ผ! ๋ผ๋ผ๋ฒจ์์ ๋ธ๋ก์ฒด์ธ๊ณผ ํต์ ํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์. ์ด๊ฒ ๋ฐ๋ก ์น2์ ์น3์ ๋ค๋ฆฌ ์ญํ ์ ํ๋ ๊ฑฐ์ง! ๐
๐ Web3.php ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น
PHP์์ ์ด๋๋ฆฌ์๊ณผ ํต์ ํ๋ ค๋ฉด Web3.php ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ํ์ํด:
# ๋ผ๋ผ๋ฒจ ํ๋ก์ ํธ ๋ฃจํธ์์ ์คํ
composer require web3p/web3.php
๋๋ ๋ ๊ฐ๋จํ ๋ฐฉ๋ฒ์ผ๋ก Guzzle HTTP ํด๋ผ์ด์ธํธ๋ฅผ ์ฌ์ฉํด์ JSON-RPC ํธ์ถ์ ์ง์ ๋ง๋ค ์๋ ์์ด:
composer require guzzlehttp/guzzle
๐ ๏ธ ๋ธ๋ก์ฒด์ธ ์๋น์ค ํด๋์ค ์์ฑ
app/Services/BlockchainService.php ํ์ผ์ ๋ง๋ค์ด:
<?php
namespace App\Services;
use Web3\Web3;
use Web3\Contract;
use Web3\Utils;
class BlockchainService
{
private $web3;
private $contract;
private $contractAddress;
private $ownerAddress;
private $ownerPrivateKey;
public function __construct()
{
$this->web3 = new Web3(env('BLOCKCHAIN_NETWORK'));
$this->contractAddress = env('CONTRACT_ADDRESS');
$this->ownerPrivateKey = env('OWNER_PRIVATE_KEY');
// ์ปจํธ๋ํธ ABI ๋ก๋
$abi = json_decode(file_get_contents(
base_path('blockchain/build/contracts/MyToken.json')
), true)['abi'];
$this->contract = new Contract(
$this->web3->provider,
$abi
);
$this->contract->at($this->contractAddress);
}
/**
* ํ ํฐ ์์ก ์กฐํ
*/
public function getBalance($address)
{
$balance = null;
$this->contract->call('balanceOf', $address, function ($err, $result) use (&$balance) {
if ($err !== null) {
throw new \Exception($err->getMessage());
}
$balance = $result[0]->toString();
});
return $balance;
}
/**
* ํ ํฐ ์ ์ก
*/
public function transfer($from, $to, $amount, $privateKey)
{
// ํธ๋์ญ์
๋ฐ์ดํฐ ์์ฑ
$data = $this->contract->getData('transfer', $to, $amount);
// ๊ฐ์ค ์ถ์
$gas = $this->estimateGas($from, $this->contractAddress, $data);
// ํธ๋์ญ์
์๋ช
๋ฐ ์ ์ก
$txHash = $this->sendSignedTransaction([
'from' => $from,
'to' => $this->contractAddress,
'gas' => $gas,
'data' => $data
], $privateKey);
return $txHash;
}
/**
* ํ ํฐ ๋ฐํ (์์ ์๋ง)
*/
public function mint($to, $amount)
{
$data = $this->contract->getData('mint', $to, $amount);
$txHash = $this->sendSignedTransaction([
'from' => $this->ownerAddress,
'to' => $this->contractAddress,
'gas' => '200000',
'data' => $data
], $this->ownerPrivateKey);
return $txHash;
}
/**
* ํ ํฐ ์๊ฐ
*/
public function burn($from, $amount, $privateKey)
{
$data = $this->contract->getData('burn', $amount);
$txHash = $this->sendSignedTransaction([
'from' => $from,
'to' => $this->contractAddress,
'gas' => '100000',
'data' => $data
], $privateKey);
return $txHash;
}
/**
* ์ด ๊ณต๊ธ๋ ์กฐํ
*/
public function totalSupply()
{
$supply = null;
$this->contract->call('totalSupply', function ($err, $result) use (&$supply) {
if ($err !== null) {
throw new \Exception($err->getMessage());
}
$supply = $result[0]->toString();
});
return $supply;
}
/**
* ํธ๋์ญ์
์์์ฆ ์กฐํ
*/
public function getTransactionReceipt($txHash)
{
$receipt = null;
$this->web3->eth->getTransactionReceipt($txHash, function ($err, $result) use (&$receipt) {
if ($err !== null) {
throw new \Exception($err->getMessage());
}
$receipt = $result;
});
return $receipt;
}
/**
* ๊ฐ์ค ์ถ์
*/
private function estimateGas($from, $to, $data)
{
$gas = null;
$this->web3->eth->estimateGas([
'from' => $from,
'to' => $to,
'data' => $data
], function ($err, $result) use (&$gas) {
if ($err !== null) {
throw new \Exception($err->getMessage());
}
$gas = $result->toString();
});
return $gas;
}
/**
* ์๋ช
๋ ํธ๋์ญ์
์ ์ก
*/
private function sendSignedTransaction($transaction, $privateKey)
{
// ์ค์ ๊ตฌํ์์๋ ํธ๋์ญ์
์๋ช
๋ก์ง ํ์
// ethers.js๋ web3.js๋ฅผ Node.js๋ก ํธ์ถํ๊ฑฐ๋
// PHP ์ํธํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ
$txHash = null;
// ๊ฐ๋จํ ์์ (์ค์ ๋ก๋ ๋ ๋ณต์กํจ)
$this->web3->eth->sendTransaction($transaction, function ($err, $result) use (&$txHash) {
if ($err !== null) {
throw new \Exception($err->getMessage());
}
$txHash = $result;
});
return $txHash;
}
}
์ด ์๋น์ค ํด๋์ค๊ฐ ๋ธ๋ก์ฒด์ธ๊ณผ์ ๋ชจ๋ ์ํธ์์ฉ์ ๋ด๋นํด! ๋ง์น ํต์ญ์ฌ์ฒ๋ผ ๋ผ๋ผ๋ฒจ๊ณผ ๋ธ๋ก์ฒด์ธ ์ฌ์ด์์ ๋ฉ์์ง๋ฅผ ์ ๋ฌํ๋ ๊ฑฐ์ง. ๐ฃ๏ธ
๐๏ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ชจ๋ธ ์์ฑ
๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ๊ธฐ๋ก์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์๋ ์ ์ฅํด๋๋ฉด ์ข์. ๋น ๋ฅธ ์กฐํ์ ํต๊ณ๋ฅผ ์ํด์ ๋ง์ด์ผ!
# ๋ง์ด๊ทธ๋ ์ด์
์์ฑ
php artisan make:migration create_transactions_table
php artisan make:migration create_wallets_table
database/migrations/xxxx_create_transactions_table.php:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTransactionsTable extends Migration
{
public function up()
{
Schema::create('transactions', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('tx_hash')->unique();
$table->string('from_address');
$table->string('to_address');
$table->string('amount');
$table->enum('type', ['transfer', 'mint', 'burn']);
$table->enum('status', ['pending', 'confirmed', 'failed'])->default('pending');
$table->integer('block_number')->nullable();
$table->text('error_message')->nullable();
$table->timestamps();
$table->index(['user_id', 'created_at']);
$table->index('tx_hash');
});
}
public function down()
{
Schema::dropIfExists('transactions');
}
}
database/migrations/xxxx_create_wallets_table.php:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateWalletsTable extends Migration
{
public function up()
{
Schema::create('wallets', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('address')->unique();
$table->text('encrypted_private_key')->nullable();
$table->boolean('is_primary')->default(true);
$table->timestamps();
$table->index('user_id');
$table->index('address');
});
}
public function down()
{
Schema::dropIfExists('wallets');
}
}
# ๋ง์ด๊ทธ๋ ์ด์
์คํ
php artisan migrate
์ด์ ๋ชจ๋ธ๋ ๋ง๋ค์ด์ค:
# ๋ชจ๋ธ ์์ฑ
php artisan make:model Transaction
php artisan make:model Wallet
app/Models/Transaction.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Transaction extends Model
{
protected $fillable = [
'user_id',
'tx_hash',
'from_address',
'to_address',
'amount',
'type',
'status',
'block_number',
'error_message'
];
protected $casts = [
'block_number' => 'integer',
];
public function user()
{
return $this->belongsTo(User::class);
}
public function scopePending($query)
{
return $query->where('status', 'pending');
}
public function scopeConfirmed($query)
{
return $query->where('status', 'confirmed');
}
}
๐ฎ ์ปจํธ๋กค๋ฌ์ API ์๋ํฌ์ธํธ ๊ตฌํ
์ด์ ์ฌ์ฉ์๊ฐ ์ค์ ๋ก ์ฌ์ฉํ ์ ์๋ API๋ฅผ ๋ง๋ค์ด๋ณด์! ์ด๊ฒ ๋ฐ๋ก ํ๋ก ํธ์๋์ ๋ฐฑ์๋๋ฅผ ์ฐ๊ฒฐํ๋ ๋ค๋ฆฌ์ผ. ๐
๐ฏ ํ ํฐ ์ปจํธ๋กค๋ฌ ์์ฑ
# ์ปจํธ๋กค๋ฌ ์์ฑ
php artisan make:controller Api/TokenController
app/Http/Controllers/Api/TokenController.php:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\BlockchainService;
use App\Models\Transaction;
use App\Models\Wallet;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\DB;
class TokenController extends Controller
{
private $blockchain;
public function __construct(BlockchainService $blockchain)
{
$this->blockchain = $blockchain;
}
/**
* ํ ํฐ ์์ก ์กฐํ
*/
public function balance(Request $request)
{
$validator = Validator::make($request->all(), [
'address' => 'required|string|size:42'
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors()
], 422);
}
try {
$balance = $this->blockchain->getBalance($request->address);
return response()->json([
'success' => true,
'data' => [
'address' => $request->address,
'balance' => $balance,
'formatted_balance' => number_format($balance / 1e18, 4)
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => '์์ก ์กฐํ ์คํจ: ' . $e->getMessage()
], 500);
}
}
/**
* ํ ํฐ ์ ์ก
*/
public function transfer(Request $request)
{
$validator = Validator::make($request->all(), [
'to_address' => 'required|string|size:42',
'amount' => 'required|numeric|min:0.0001'
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors()
], 422);
}
DB::beginTransaction();
try {
$user = $request->user();
$wallet = $user->wallets()->where('is_primary', true)->first();
if (!$wallet) {
throw new \Exception('์ง๊ฐ์ ์ฐพ์ ์ ์์ต๋๋ค.');
}
// Wei ๋จ์๋ก ๋ณํ (1 ํ ํฐ = 10^18 Wei)
$amountInWei = bcmul($request->amount, '1000000000000000000');
// ์์ก ํ์ธ
$balance = $this->blockchain->getBalance($wallet->address);
if (bccomp($balance, $amountInWei) < 0) {
throw new \Exception('์์ก์ด ๋ถ์กฑํฉ๋๋ค.');
}
// ํธ๋์ญ์
์ ์ก
$txHash = $this->blockchain->transfer(
$wallet->address,
$request->to_address,
$amountInWei,
decrypt($wallet->encrypted_private_key)
);
// DB์ ๊ธฐ๋ก
$transaction = Transaction::create([
'user_id' => $user->id,
'tx_hash' => $txHash,
'from_address' => $wallet->address,
'to_address' => $request->to_address,
'amount' => $amountInWei,
'type' => 'transfer',
'status' => 'pending'
]);
DB::commit();
return response()->json([
'success' => true,
'data' => [
'transaction_id' => $transaction->id,
'tx_hash' => $txHash,
'status' => 'pending',
'message' => '์ ์ก์ด ์์๋์์ต๋๋ค. ๋ธ๋ก์ฒด์ธ ํ์ธ๊น์ง ์ฝ๊ฐ์ ์๊ฐ์ด ์์๋ฉ๋๋ค.'
]
]);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => '์ ์ก ์คํจ: ' . $e->getMessage()
], 500);
}
}
/**
* ํ ํฐ ๋ฐํ (๊ด๋ฆฌ์ ์ ์ฉ)
*/
public function mint(Request $request)
{
// ๊ด๋ฆฌ์ ๊ถํ ํ์ธ
if (!$request->user()->is_admin) {
return response()->json([
'success' => false,
'message' => '๊ถํ์ด ์์ต๋๋ค.'
], 403);
}
$validator = Validator::make($request->all(), [
'to_address' => 'required|string|size:42',
'amount' => 'required|numeric|min:1'
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors()
], 422);
}
try {
$amountInWei = bcmul($request->amount, '1000000000000000000');
$txHash = $this->blockchain->mint(
$request->to_address,
$amountInWei
);
Transaction::create([
'user_id' => $request->user()->id,
'tx_hash' => $txHash,
'from_address' => '0x0000000000000000000000000000000000000000',
'to_address' => $request->to_address,
'amount' => $amountInWei,
'type' => 'mint',
'status' => 'pending'
]);
return response()->json([
'success' => true,
'data' => [
'tx_hash' => $txHash,
'message' => 'ํ ํฐ ๋ฐํ์ด ์์๋์์ต๋๋ค.'
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => '๋ฐํ ์คํจ: ' . $e->getMessage()
], 500);
}
}
/**
* ํ ํฐ ์๊ฐ
*/
public function burn(Request $request)
{
$validator = Validator::make($request->all(), [
'amount' => 'required|numeric|min:0.0001'
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors()
], 422);
}
try {
$user = $request->user();
$wallet = $user->wallets()->where('is_primary', true)->first();
$amountInWei = bcmul($request->amount, '1000000000000000000');
$txHash = $this->blockchain->burn(
$wallet->address,
$amountInWei,
decrypt($wallet->encrypted_private_key)
);
Transaction::create([
'user_id' => $user->id,
'tx_hash' => $txHash,
'from_address' => $wallet->address,
'to_address' => '0x0000000000000000000000000000000000000000',
'amount' => $amountInWei,
'type' => 'burn',
'status' => 'pending'
]);
return response()->json([
'success' => true,
'data' => [
'tx_hash' => $txHash,
'message' => 'ํ ํฐ ์๊ฐ์ด ์์๋์์ต๋๋ค.'
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => '์๊ฐ ์คํจ: ' . $e->getMessage()
], 500);
}
}
/**
* ํธ๋์ญ์
์ํ ์กฐํ
*/
public function transactionStatus($txHash)
{
try {
$transaction = Transaction::where('tx_hash', $txHash)->first();
if (!$transaction) {
return response()->json([
'success' => false,
'message' => 'ํธ๋์ญ์
์ ์ฐพ์ ์ ์์ต๋๋ค.'
], 404);
}
// ๋ธ๋ก์ฒด์ธ์์ ์ต์ ์ํ ํ์ธ
$receipt = $this->blockchain->getTransactionReceipt($txHash);
if ($receipt && $receipt->status) {
$transaction->update([
'status' => 'confirmed',
'block_number' => hexdec($receipt->blockNumber)
]);
}
return response()->json([
'success' => true,
'data' => [
'tx_hash' => $transaction->tx_hash,
'status' => $transaction->status,
'block_number' => $transaction->block_number,
'from' => $transaction->from_address,
'to' => $transaction->to_address,
'amount' => number_format($transaction->amount / 1e18, 4),
'type' => $transaction->type,
'created_at' => $transaction->created_at
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => '์กฐํ ์คํจ: ' . $e->getMessage()
], 500);
}
}
/**
* ์ฌ์ฉ์ ํธ๋์ญ์
๋ด์ญ
*/
public function history(Request $request)
{
$user = $request->user();
$transactions = Transaction::where('user_id', $user->id)
->orderBy('created_at', 'desc')
->paginate(20);
return response()->json([
'success' => true,
'data' => $transactions
]);
}
/**
* ํ ํฐ ํต๊ณ
*/
public function statistics()
{
try {
$totalSupply = $this->blockchain->totalSupply();
$stats = [
'total_supply' => number_format($totalSupply / 1e18, 2),
'total_holders' => Wallet::distinct('address')->count(),
'total_transactions' => Transaction::count(),
'pending_transactions' => Transaction::pending()->count(),
'confirmed_transactions' => Transaction::confirmed()->count()
];
return response()->json([
'success' => true,
'data' => $stats
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'ํต๊ณ ์กฐํ ์คํจ: ' . $e->getMessage()
], 500);
}
}
}
๐ฃ๏ธ API ๋ผ์ฐํธ ์ค์
routes/api.php์ ๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํด:
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\TokenController;
Route::middleware('auth:sanctum')->group(function () {
// ํ ํฐ ์์ก ์กฐํ
Route::get('/token/balance', [TokenController::class, 'balance']);
// ํ ํฐ ์ ์ก
Route::post('/token/transfer', [TokenController::class, 'transfer']);
// ํ ํฐ ์๊ฐ
Route::post('/token/burn', [TokenController::class, 'burn']);
// ํธ๋์ญ์
๋ด์ญ
Route::get('/token/history', [TokenController::class, 'history']);
// ํธ๋์ญ์
์ํ ์กฐํ
Route::get('/token/transaction/{txHash}', [TokenController::class, 'transactionStatus']);
// ํ ํฐ ํต๊ณ
Route::get('/token/statistics', [TokenController::class, 'statistics']);
// ๊ด๋ฆฌ์ ์ ์ฉ
Route::middleware('admin')->group(function () {
Route::post('/token/mint', [TokenController::class, 'mint']);
});
});
โก ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ ๊ณผ ํ ์์คํ
๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ์ ์๊ฐ์ด ์ข ๊ฑธ๋ ค. ์ฌ์ฉ์๊ฐ ๊ธฐ๋ค๋ฆฌ๊ฒ ํ๋ฉด ์ ๋๋๊น, ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ฒ๋ฆฌํ๊ณ ์ํ๋ฅผ ์ ๋ฐ์ดํธํ๋ ์์คํ ์ ๋ง๋ค์ด๋ณด์! ๐
๐ Job ํด๋์ค ์์ฑ
# Job ์์ฑ
php artisan make:job CheckTransactionStatus
app/Jobs/CheckTransactionStatus.php:
<?php
namespace App\Jobs;
use App\Models\Transaction;
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;
class CheckTransactionStatus implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $transaction;
public $tries = 10; // ์ต๋ ์ฌ์๋ ํ์
public $backoff = 30; // ์ฌ์๋ ๊ฐ๊ฒฉ (์ด)
public function __construct(Transaction $transaction)
{
$this->transaction = $transaction;
}
public function handle(BlockchainService $blockchain)
{
try {
// ์ด๋ฏธ ํ์ธ๋ ํธ๋์ญ์
์ ์คํต
if ($this->transaction->status !== 'pending') {
return;
}
// ๋ธ๋ก์ฒด์ธ์์ ์์์ฆ ์กฐํ
$receipt = $blockchain->getTransactionReceipt($this->transaction->tx_hash);
if ($receipt) {
// ํธ๋์ญ์
์ฑ๊ณต
if ($receipt->status === '0x1') {
$this->transaction->update([
'status' => 'confirmed',
'block_number' => hexdec($receipt->blockNumber)
]);
// ์ฌ์ฉ์์๊ฒ ์๋ฆผ ์ ์ก (์ต์
)
// event(new TransactionConfirmed($this->transaction));
}
// ํธ๋์ญ์
์คํจ
else {
$this->transaction->update([
'status' => 'failed',
'error_message' => 'Transaction reverted'
]);
}
} else {
// ์์ง ๋ง์ด๋๋์ง ์์ - ์ฌ์๋
$this->release(30);
}
} catch (\Exception $e) {
// ์๋ฌ ๋ก๊น
\Log::error('Transaction status check failed: ' . $e->getMessage(), [
'transaction_id' => $this->transaction->id,
'tx_hash' => $this->transaction->tx_hash
]);
// ์ฌ์๋
if ($this->attempts() < $this->tries) {
$this->release(60);
} else {
// ์ต๋ ์ฌ์๋ ์ด๊ณผ
$this->transaction->update([
'status' => 'failed',
'error_message' => 'Max retry attempts exceeded'
]);
}
}
}
}
โฐ ์ค์ผ์ค๋ฌ ์ค์
์ฃผ๊ธฐ์ ์ผ๋ก pending ์ํ์ธ ํธ๋์ญ์ ๋ค์ ์ฒดํฌํ๋ ์ค์ผ์ค๋ฌ๋ฅผ ๋ง๋ค์ด:
app/Console/Kernel.php:
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Models\Transaction;
use App\Jobs\CheckTransactionStatus;
class Kernel extends ConsoleKernel
{
protected function schedule(Schedule $schedule)
{
// 1๋ถ๋ง๋ค pending ํธ๋์ญ์
์ฒดํฌ
$schedule->call(function () {
$pendingTransactions = Transaction::where('status', 'pending')
->where('created_at', '>', now()->subHours(24))
->get();
foreach ($pendingTransactions as $transaction) {
CheckTransactionStatus::dispatch($transaction);
}
})->everyMinute();
// 24์๊ฐ ์ด์ pending์ธ ํธ๋์ญ์
์ ์คํจ ์ฒ๋ฆฌ
$schedule->call(function () {
Transaction::where('status', 'pending')
->where('created_at', '<', now()->subHours(24))
->update([
'status' => 'failed',
'error_message' => 'Transaction timeout'
]);
})->hourly();
}
}
์ค์ผ์ค๋ฌ๋ฅผ ์คํํ๋ ค๋ฉด cron์ ๋ฑ๋กํด์ผ ํด:
# crontab -e
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
๐ ํ ์์ปค ์คํ
ํ๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด ์์ปค๋ฅผ ์คํํด์ผ ํด:
# ๊ฐ๋ฐ ํ๊ฒฝ
php artisan queue:work
# ํ๋ก๋์
ํ๊ฒฝ (Supervisor ์ฌ์ฉ ๊ถ์ฅ)
php artisan queue:work --daemon --tries=3 --timeout=90
ํ๋ก๋์ ์์๋ Supervisor๋ฅผ ์ฌ์ฉํด์ ์์ปค๋ฅผ ๊ด๋ฆฌํ๋ ๊ฒ ์ข์:
# /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /path-to-your-project/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/path-to-your-project/storage/logs/worker.log
๐จ ํ๋ก ํธ์๋ ๊ตฌํ (Vue.js ์์)
๋ฐฑ์๋๊ฐ ์์ฑ๋์ผ๋ ์ด์ ์ฌ์ฉ์๊ฐ ์ค์ ๋ก ์ฌ์ฉํ ์ธํฐํ์ด์ค๋ฅผ ๋ง๋ค์ด๋ณด์! Vue.js๋ก ๊ฐ๋จํ ๋์๋ณด๋๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ. ๋ฌผ๋ก React๋ ๋ค๋ฅธ ํ๋ ์์ํฌ๋ฅผ ์จ๋ ๋ผ! ๐ผ๏ธ
๐ฆ Vue.js ์ค์น
# Laravel Mix ์ฌ์ฉ
npm install vue@next vue-loader@next
# ๋๋ Vite ์ฌ์ฉ (Laravel 9+)
npm install vue@next @vitejs/plugin-vue
๐ฐ ํ ํฐ ๋์๋ณด๋ ์ปดํฌ๋ํธ
resources/js/components/TokenDashboard.vue:
<template>
<div class="token-dashboard">
<div class="dashboard-header">
<h2>๐ช ๋ด ํ ํฐ ์ง๊ฐ</h2>
<button @click="refreshBalance" class="refresh-btn">
๐ ์๋ก๊ณ ์นจ
</button>
</div>
<div class="balance-card">
<div class="balance-label">๋ณด์ ํ ํฐ</div>
<div class="balance-amount">
{{ formattedBalance }} JNT
</div>
<div class="wallet-address">
{{ shortenAddress(walletAddress) }}
</div>
</div>
<div class="action-cards">
<div class="action-card">
<h3>๐ธ ํ ํฐ ์ ์ก</h3>
<form @submit.prevent="sendTokens">
<input
v-model="transferForm.toAddress"
type="text"
placeholder="๋ฐ๋ ์ฃผ์ (0x...)"
required
/>
<input
v-model.number="transferForm.amount"
type="number"
step="0.0001"
placeholder="์๋"
required
/>
<button type="submit" :disabled="sending">
{{ sending ? '์ ์ก ์ค...' : '์ ์กํ๊ธฐ' }}
</button>
</form>
</div>
<div class="action-card">
<h3>๐ฅ ํ ํฐ ์๊ฐ</h3>
<form @submit.prevent="burnTokens">
<input
v-model.number="burnAmount"
type="number"
step="0.0001"
placeholder="์๊ฐํ ์๋"
required
/>
<button type="submit" :disabled="burning">
{{ burning ? '์๊ฐ ์ค...' : '์๊ฐํ๊ธฐ' }}
</button>
</form>
</div>
</div>
<div class="transaction-history">
<h3>๐ ๊ฑฐ๋ ๋ด์ญ</h3>
<div v-if="transactions.length === 0" class="no-transactions">
์์ง ๊ฑฐ๋ ๋ด์ญ์ด ์์ต๋๋ค.
</div>
<div v-else class="transaction-list">
<div
v-for="tx in transactions"
:key="tx.id"
class="transaction-item"
:class="'status-' + tx.status"
>
<div class="tx-type">
{{ getTypeEmoji(tx.type) }} {{ getTypeLabel(tx.type) }}
</div>
<div class="tx-details">
<div>์๋: {{ tx.amount }} JNT</div>
<div>์ํ: {{ getStatusLabel(tx.status) }}</div>
<div class="tx-hash">
<a :href="getEtherscanUrl(tx.tx_hash)" target="_blank">
{{ shortenHash(tx.tx_hash) }}
</a>
</div>
</div>
<div class="tx-time">
{{ formatTime(tx.created_at) }}
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'TokenDashboard',
data() {
return {
walletAddress: '',
balance: '0',
transactions: [],
transferForm: {
toAddress: '',
amount: 0
},
burnAmount: 0,
sending: false,
burning: false
};
},
computed: {
formattedBalance() {
return parseFloat(this.balance).toLocaleString('ko-KR', {
minimumFractionDigits: 4,
maximumFractionDigits: 4
});
}
},
mounted() {
this.loadWalletInfo();
this.loadTransactions();
// 30์ด๋ง๋ค ์๋ ์๋ก๊ณ ์นจ
setInterval(() => {
this.refreshBalance();
this.loadTransactions();
}, 30000);
},
methods: {
async loadWalletInfo() {
try {
const response = await axios.get('/api/user/wallet');
this.walletAddress = response.data.address;
await this.refreshBalance();
} catch (error) {
console.error('์ง๊ฐ ์ ๋ณด ๋ก๋ ์คํจ:', error);
this.$toast.error('์ง๊ฐ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.');
}
},
async refreshBalance() {
try {
const response = await axios.get('/api/token/balance', {
params: { address: this.walletAddress }
});
this.balance = response.data.data.formatted_balance;
} catch (error) {
console.error('์์ก ์กฐํ ์คํจ:', error);
}
},
async loadTransactions() {
try {
const response = await axios.get('/api/token/history');
this.transactions = response.data.data.data;
} catch (error) {
console.error('๊ฑฐ๋ ๋ด์ญ ๋ก๋ ์คํจ:', error);
}
},
async sendTokens() {
if (!this.validateAddress(this.transferForm.toAddress)) {
this.$toast.error('์ฌ๋ฐ๋ฅธ ์ฃผ์๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.');
return;
}
this.sending = true;
try {
const response = await axios.post('/api/token/transfer', this.transferForm);
this.$toast.success('์ ์ก์ด ์์๋์์ต๋๋ค!');
this.transferForm.toAddress = '';
this.transferForm.amount = 0;
// ํธ๋์ญ์
๋ชฉ๋ก ์๋ก๊ณ ์นจ
setTimeout(() => {
this.loadTransactions();
}, 2000);
} catch (error) {
this.$toast.error(error.response?.data?.message || '์ ์ก ์คํจ');
} finally {
this.sending = false;
}
},
async burnTokens() {
if (!confirm(`${this.burnAmount} JNT๋ฅผ ์๊ฐํ์๊ฒ ์ต๋๊น? ์ด ์์
์ ๋๋๋ฆด ์ ์์ต๋๋ค.`)) {
return;
}
this.burning = true;
try {
await axios.post('/api/token/burn', {
amount: this.burnAmount
});
this.$toast.success('ํ ํฐ ์๊ฐ์ด ์์๋์์ต๋๋ค!');
this.burnAmount = 0;
setTimeout(() => {
this.loadTransactions();
this.refreshBalance();
}, 2000);
} catch (error) {
this.$toast.error(error.response?.data?.message || '์๊ฐ ์คํจ');
} finally {
this.burning = false;
}
},
validateAddress(address) {
return /^0x[a-fA-F0-9]{40}$/.test(address);
},
shortenAddress(address) {
if (!address) return '';
return `${address.slice(0, 6)}...${address.slice(-4)}`;
},
shortenHash(hash) {
return `${hash.slice(0, 10)}...${hash.slice(-8)}`;
},
getTypeEmoji(type) {
const emojis = {
transfer: '๐ธ',
mint: '๐',
burn: '๐ฅ'
};
return emojis[type] || '๐';
},
getTypeLabel(type) {
const labels = {
transfer: '์ ์ก',
mint: '๋ฐํ',
burn: '์๊ฐ'
};
return labels[type] || type;
},
getStatusLabel(status) {
const labels = {
pending: 'โณ ๋๊ธฐ์ค',
confirmed: 'โ
์๋ฃ',
failed: 'โ ์คํจ'
};
return labels[status] || status;
},
getEtherscanUrl(txHash) {
// ํ
์คํธ๋ท์ด๋ฉด rinkeby, ๋ฉ์ธ๋ท์ด๋ฉด etherscan.io
return `https://rinkeby.etherscan.io/tx/${txHash}`;
},
formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return '๋ฐฉ๊ธ ์ ';
if (minutes < 60) return `${minutes}๋ถ ์ `;
if (hours < 24) return `${hours}์๊ฐ ์ `;
return `${days}์ผ ์ `;
}
}
};
</script>
<style scoped>
.token-dashboard {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.refresh-btn {
background: #667eea;
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.balance-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 16px;
text-align: center;
margin-bottom: 30px;
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3);
}
.balance-label {
font-size: 16px;
opacity: 0.9;
margin-bottom: 10px;
}
.balance-amount {
font-size: 48px;
font-weight: bold;
margin-bottom: 10px;
}
.wallet-address {
font-size: 14px;
opacity: 0.8;
font-family: monospace;
}
.action-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.action-card {
background: white;
border: 2px solid #e0e0e0;
border-radius: 12px;
padding: 20px;
}
.action-card h3 {
margin-top: 0;
color: #2d3436;
}
.action-card input {
width: 100%;
padding: 12px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
}
.action-card button {
width: 100%;
padding: 12px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
}
.action-card button:disabled {
background: #ccc;
cursor: not-allowed;
}
.transaction-history {
background: white;
border-radius: 12px;
padding: 20px;
}
.transaction-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f0f0f0;
}
.transaction-item:last-child {
border-bottom: none;
}
.status-pending {
background: #fff3e0;
}
.status-confirmed {
background: #e8f5e9;
}
.status-failed {
background: #ffebee;
}
.tx-hash a {
color: #667eea;
text-decoration: none;
font-family: monospace;
font-size: 12px;
}
.no-transactions {
text-align: center;
padding: 40px;
color: #999;
}
</style>
์! ์ด์ ์์ ํ ์๋ํ๋ ํ ํฐ ๋์๋ณด๋๊ฐ ์๊ฒผ์ด! ์ฌ์ฉ์๋ ์ด ์ธํฐํ์ด์ค๋ฅผ ํตํด ํ ํฐ์ ์ ์กํ๊ณ , ์๊ฐํ๊ณ , ๊ฑฐ๋ ๋ด์ญ์ ํ์ธํ ์ ์์ด. ๐
๐ ๋ฐฐํฌ ๋ฐ ํ ์คํธ ์ ๋ต
๊ฐ๋ฐ์ด ๋๋ฌ๋ค๊ณ ๋ฐ๋ก ๋ฉ์ธ๋ท์ ๋ฐฐํฌํ๋ฉด ์ ๋ผ! ๋จ๊ณ๋ณ๋ก ์ฒ ์ ํ๊ฒ ํ ์คํธํด์ผ ํด. ๐งช
1๋จ๊ณ: ๋ก์ปฌ ํ ์คํธ (Ganache)
- ๊ฐ๋ฐ ์ค ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉ
- ๋น ๋ฅธ ํผ๋๋ฐฑ
- ๊ฐ์ค๋น ์์
- ์์ ํ ์ ์ด ๊ฐ๋ฅ
2๋จ๊ณ: ํ ์คํธ๋ท ๋ฐฐํฌ (Rinkeby, Goerli, Sepolia)
- ์ค์ ๋คํธ์ํฌ์ ์ ์ฌํ ํ๊ฒฝ
- ๋ฌด๋ฃ ํ ์คํธ ์ด๋ ์ฌ์ฉ
- ๊ณต๊ฐ์ ์ผ๋ก ๊ฒ์ฆ ๊ฐ๋ฅ
- ๋ค๋ฅธ ๊ฐ๋ฐ์์ ํ์ ๊ฐ๋ฅ
3๋จ๊ณ: ๋ฉ์ธ๋ท ๋ฐฐํฌ
- ์ค์ ์ด๋ ์ฌ์ฉ
- ๋๋๋ฆด ์ ์์
- ๋ณด์ ๊ฐ์ฌ ํ์
- ์ ์ง์ ๋กค์์ ๊ถ์ฅ
๐งช ๋จ์ ํ ์คํธ ์์ฑ
์ค๋งํธ ์ปจํธ๋ํธ ํ ์คํธ (test/MyToken.test.js):
const MyToken = artifacts.require("MyToken");
contract("MyToken", accounts => {
let token;
const owner = accounts[0];
const user1 = accounts[1];
const user2 = accounts[2];
beforeEach(async () => {
token = await MyToken.new(
"Test Token",
"TEST",
18,
1000000,
10000000
);
});
describe("๋ฐฐํฌ ํ
์คํธ", () => {
it("์ฌ๋ฐ๋ฅธ ์ด๋ฆ๊ณผ ์ฌ๋ณผ์ ๊ฐ์ ธ์ผ ํจ", async () => {
const name = await token.name();
const symbol = await token.symbol();
assert.equal(name, "Test Token");
assert.equal(symbol, "TEST");
});
it("์ด๊ธฐ ๊ณต๊ธ๋์ด ์์ ์์๊ฒ ํ ๋น๋์ด์ผ ํจ", async () => {
const balance = await token.balanceOf(owner);
const expected = web3.utils.toBN("1000000000000000000000000"); // 1M tokens
assert.equal(balance.toString(), expected.toString());
});
});
describe("์ ์ก ํ
์คํธ", () => {
it("ํ ํฐ์ ์ ์กํ ์ ์์ด์ผ ํจ", async () => {
const amount = web3.utils.toWei("100", "ether");
await token.transfer(user1, amount, { from: owner });
const balance = await token.balanceOf(user1);
assert.equal(balance.toString(), amount);
});
it("์์ก๋ณด๋ค ๋ง์ ๊ธ์ก์ ์ ์กํ ์ ์์ด์ผ ํจ", async () => {
const amount = web3.utils.toWei("2000000", "ether");
try {
await token.transfer(user1, amount, { from: owner });
assert.fail("์์ธ๊ฐ ๋ฐ์ํด์ผ ํจ");
} catch (error) {
assert.include(error.message, "revert");
}
});
});
describe("๋ฐํ ํ
์คํธ", () => {
it("์์ ์๋ ํ ํฐ์ ๋ฐํํ ์ ์์ด์ผ ํจ", async () => {
const amount = web3.utils.toWei("1000", "ether");
await token.mint(user1, amount, { from: owner });
const balance = await token.balanceOf(user1);
assert.equal(balance.toString(), amount);
});
it("์์ ์๊ฐ ์๋๋ฉด ๋ฐํํ ์ ์์ด์ผ ํจ", async () => {
const amount = web3.utils.toWei("1000", "ether");
try {
await token.mint(user2, amount, { from: user1 });
assert.fail("์์ธ๊ฐ ๋ฐ์ํด์ผ ํจ");
} catch (error) {
assert.include(error.message, "Ownable");
}
});
it("์ต๋ ๋ฐํ๋์ ์ด๊ณผํ ์ ์์ด์ผ ํจ", async () => {
const amount = web3.utils.toWei("20000000", "ether");
try {
await token.mint(user1, amount, { from: owner });
assert.fail("์์ธ๊ฐ ๋ฐ์ํด์ผ ํจ");
} catch (error) {
assert.include(error.message, "Cap exceeded");
}
});
});
describe("์๊ฐ ํ
์คํธ", () => {
it("ํ ํฐ์ ์๊ฐํ ์ ์์ด์ผ ํจ", async () => {
const initialBalance = await token.balanceOf(owner);
const burnAmount = web3.utils.toWei("1000", "ether");
await token.burn(burnAmount, { from: owner });
const newBalance = await token.balanceOf(owner);
const expected = initialBalance.sub(web3.utils.toBN(burnAmount));
assert.equal(newBalance.toString(), expected.toString());
});
});
});
ํ ์คํธ ์คํ:
# ๋ชจ๋ ํ
์คํธ ์คํ
truffle test
# ํน์ ํ์ผ๋ง ํ
์คํธ
truffle test test/MyToken.test.js
# ์ปค๋ฒ๋ฆฌ์ง ํ์ธ
npm install --save-dev solidity-coverage
truffle run coverage
๐ ํ ์คํธ๋ท ๋ฐฐํฌ
Rinkeby ํ ์คํธ๋ท์ ๋ฐฐํฌํด๋ณด์:
# HDWalletProvider ์ค์น
npm install @truffle/hdwallet-provider
# .env ํ์ผ์ ์ถ๊ฐ
MNEMONIC="your twelve word mnemonic here"
INFURA_KEY="your-infura-project-id"
truffle-config.js ์์ :
require('dotenv').config();
const HDWalletProvider = require('@truffle/hdwallet-provider');
module.exports = {
networks: {
rinkeby: {
provider: () => new HDWalletProvider(
process.env.MNEMONIC,
`https://rinkeby.infura.io/v3/${process.env.INFURA_KEY}`
),
network_id: 4,
gas: 5500000,
confirmations: 2,
timeoutBlocks: 200,
skipDryRun: true
}
}
};
๋ฐฐํฌ ์คํ:
# Rinkeby ํ
์คํธ๋ท์ ๋ฐฐํฌ
truffle migrate --network rinkeby
# ๋ฐฐํฌ ํ ์ปจํธ๋ํธ ์ฃผ์๋ฅผ .env์ ์ ์ฅ
CONTRACT_ADDRESS=0x...
ํ ์คํธ ์ด๋ ๋ฐ๊ธฐ:
- ๐ Rinkeby Faucet: https://faucet.rinkeby.io/
- ๐ Goerli Faucet: https://goerlifaucet.com/
- โ ๋ชจ๋ ๋จ์ ํ ์คํธ ํต๊ณผ
- โ ํ ์คํธ๋ท์์ ์ถฉ๋ถํ ๊ธฐ๊ฐ ์ด์
- โ ๋ณด์ ๊ฐ์ฌ ์๋ฃ
- โ ๊ฐ์ค ์ต์ ํ ํ์ธ
- โ ๊ธด๊ธ ์ค์ง ๋ฉ์ปค๋์ฆ ๊ตฌํ
- โ ์ ๊ทธ๋ ์ด๋ ์ ๋ต ์๋ฆฝ
- โ ๋ฒ์ ๊ฒํ ์๋ฃ
- โ ๋ฐฑ์ ๋ฐ ๋ณต๊ตฌ ๊ณํ ์๋ฆฝ
๐ก๏ธ ๋ณด์ ๊ณ ๋ ค์ฌํญ
๋ธ๋ก์ฒด์ธ ํ๋ก์ ํธ์์ ๋ณด์์ ์ ํ์ด ์๋๋ผ ํ์์ผ! ํ๋ฒ ํดํน๋นํ๋ฉด ๋๋๋ฆด ์ ์๊ฑฐ๋ . ๐ฑ
1. ์ฌ์ง์ ๊ณต๊ฒฉ (Reentrancy Attack)
- ์ธ๋ถ ์ปจํธ๋ํธ ํธ์ถ ์ ๋ฐ์
- Checks-Effects-Interactions ํจํด ์ฌ์ฉ
- ReentrancyGuard ์ฌ์ฉ
2. ์ ์ ์ค๋ฒํ๋ก์ฐ/์ธ๋ํ๋ก์ฐ
- Solidity 0.8.0+ ์๋ ์ฒดํฌ
- SafeMath ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ (์ด์ ๋ฒ์ )
3. ์ ๊ทผ ์ ์ด ์ทจ์ฝ์
- Ownable, AccessControl ์ฌ์ฉ
- ํจ์ ๊ฐ์์ฑ ๋ช ํํ ์ง์
- ๊ถํ ๊ฒ์ฆ ์ฒ ์ ํ
4. ํ๋ก ํธ๋ฌ๋
- ํธ๋์ญ์ ์์ ์กฐ์
- Commit-Reveal ํจํด ์ฌ์ฉ
- ๊ฐ๊ฒฉ ์ฌ๋ฆฌํผ์ง ์ค์
5. ๊ฐ์ธํค ๊ด๋ฆฌ
- ์๋ฒ์ ๊ฐ์ธํค ์ ์ฅ ๊ธ์ง
- ํ๋์จ์ด ์ง๊ฐ ์ฌ์ฉ
- ๋ฉํฐ์๊ทธ ์ง๊ฐ ํ์ฉ
๐ ๋ณด์ ๊ฐํ ์ฝ๋
ReentrancyGuard ์ ์ฉ:
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract MyToken is ERC20, Ownable, ReentrancyGuard {
function transfer(address to, uint256 amount)
public
override
nonReentrant
returns (bool)
{
return super.transfer(to, amount);
}
}
Pausable ๊ธฐ๋ฅ ์ถ๊ฐ (๊ธด๊ธ ์ค์ง):
import "@openzeppelin/contracts/security/Pausable.sol";
contract MyToken is ERC20, Ownable, Pausable {
function pause() public onlyOwner {
_pause();
}
function unpause() public onlyOwner {
_unpause();
}
function transfer(address to, uint256 amount)
public
override
whenNotPaused
returns (bool)
{
return super.transfer(to, amount);
}
}
Rate Limiting (์ ์ก ์ ํ):
contract MyToken is ERC20, Ownable {
mapping(address => uint256) private _lastTransferTime;
uint256 private constant TRANSFER_COOLDOWN = 1 minutes;
function transfer(address to, uint256 amount)
public
override
returns (bool)
{
require(
block.timestamp >= _lastTransferTime[msg.sender] + TRANSFER_COOLDOWN,
"Transfer cooldown not met"
);
_lastTransferTime[msg.sender] = block.timestamp;
return super.transfer(to, amount);
}
}
๐ ๋ชจ๋ํฐ๋ง๊ณผ ๋ถ์
์์คํ ์ ๋ฐฐํฌํ ํ์๋ ์ง์์ ์ธ ๋ชจ๋ํฐ๋ง์ด ํ์ํด! ๋ฌธ์ ๊ฐ ์๊ธฐ๋ฉด ๋น ๋ฅด๊ฒ ๋์ํด์ผ ํ๊ฑฐ๋ . ๐
๐ ๋์๋ณด๋ ๊ตฌํ
๊ด๋ฆฌ์์ฉ ํต๊ณ API:
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Transaction;
use App\Models\Wallet;
use App\Services\BlockchainService;
use Illuminate\Support\Facades\DB;
class AnalyticsController extends Controller
{
public function dashboard(BlockchainService $blockchain)
{
$stats = [
// ํ ํฐ ํต๊ณ
'total_supply' => $blockchain->totalSupply(),
'total_holders' => Wallet::distinct('address')->count(),
// ํธ๋์ญ์
ํต๊ณ
'total_transactions' => Transaction::count(),
'today_transactions' => Transaction::whereDate('created_at', today())->count(),
'pending_transactions' => Transaction::pending()->count(),
// ์ผ๋ณ ๊ฑฐ๋๋
'daily_volume' => Transaction::where('created_at', '>=', now()->subDays(30))
->where('status', 'confirmed')
->groupBy(DB::raw('DATE(created_at)'))
->select(
DB::raw('DATE(created_at) as date'),
DB::raw('SUM(amount) as volume'),
DB::raw('COUNT(*) as count')
)
->get(),
// ์์ ํ๋
'top_holders' => $this->getTopHolders($blockchain, 10),
// ์ต๊ทผ ๊ฑฐ๋
'recent_transactions' => Transaction::with('user')
->orderBy('created_at', 'desc')
->limit(20)
->get()
];
return response()->json([
'success' => true,
'data' => $stats
]);
}
private function getTopHolders(BlockchainService $blockchain, $limit)
{
$wallets = Wallet::all();
$holders = [];
foreach ($wallets as $wallet) {
$balance = $blockchain->getBalance($wallet->address);
$holders[] = [
'address' => $wallet->address,
'balance' => $balance,
'user_id' => $wallet->user_id
];
}
usort($holders, function($a, $b) {
return bccomp($b['balance'], $a['balance']);
});
return array_slice($holders, 0, $limit);
}
}
๐ ์๋ฆผ ์์คํ
์ค์ํ ์ด๋ฒคํธ ๋ฐ์ ์ ์๋ฆผ์ ๋ณด๋ด๋ ์์คํ :
<?php
namespace App\Listeners;
use App\Events\LargeTransactionDetected;
use App\Notifications\LargeTransactionAlert;
use Illuminate\Support\Facades\Notification;
class NotifyLargeTransaction
{
public function handle(LargeTransactionDetected $event)
{
$transaction = $event->transaction;
// ๊ด๋ฆฌ์์๊ฒ ์๋ฆผ
$admins = User::where('is_admin', true)->get();
Notification::send($admins, new LargeTransactionAlert($transaction));
// ์ฌ๋ ์๋ฆผ
\Log::channel('slack')->warning('Large transaction detected', [
'tx_hash' => $transaction->tx_hash,
'amount' => $transaction->amount,
'from' => $transaction->from_address,
'to' => $transaction->to_address
]);
}
}
๐ ์ค์ ํ์ฉ ์ฌ๋ก
์ด์ ์ด ์์คํ ์ ์ด๋ป๊ฒ ํ์ฉํ ์ ์๋์ง ์์๋ณด์! ์ค์ ๋ก ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ์ด๋ฐ ํ ํฐ ์์คํ ์ ๋์ ํ๋ฉด ์์ฒญ๋ ๊ฐ๋ฅ์ฑ์ด ์ด๋ ค! ๐
1. ์ฌ๋ฅ ๊ฑฐ๋ ํ๋ซํผ
- ์ฌ๋ฅ ๊ฑฐ๋ ์ ํ ํฐ์ผ๋ก ๊ฒฐ์
- ์ฐ์ ์ฌ๋ฅ์ธ์๊ฒ ํ ํฐ ๋ณด์
- ํ ํฐ ํ๋์๊ฒ ํ๋ซํผ ์์๋ฃ ํ ์ธ
- ๊ฑฐ๋ฒ๋์ค ํฌํ๊ถ ๋ถ์ฌ
2. ์ปค๋ฎค๋ํฐ ๋ณด์ ์์คํ
- ์ข์ ์ฝํ ์ธ ์์ฑ ์ ํ ํฐ ์ง๊ธ
- ํ๋ ๊ธฐ์ฌ๋์ ๋ฐ๋ผ ํ ํฐ ๋ถ๋ฐฐ
- ํ ํฐ์ผ๋ก ํ๋ฆฌ๋ฏธ์ ๊ธฐ๋ฅ ์ด์ฉ
3. ๋ก์ดํฐ ํ๋ก๊ทธ๋จ
- ๊ตฌ๋งค ์ ํ ํฐ ์ ๋ฆฝ
- ํ ํฐ์ผ๋ก ํ ์ธ ํํ
- VIP ๋ฑ๊ธ ์์คํ
4. ํฌ๋ผ์ฐ๋ํ๋ฉ
- ํ๋ก์ ํธ ํ์ ์ ํ ํฐ ๋ฐํ
- ์์ต ๋ฐฐ๋ถ ์๋ํ
- ํฌ๋ช ํ ์๊ธ ๊ด๋ฆฌ
5. DAO (ํ์ค์ํ ์์จ ์กฐ์ง)
- ํ ํฐ ๊ธฐ๋ฐ ์์ฌ๊ฒฐ์
- ์๋ํ๋ ๊ฑฐ๋ฒ๋์ค
- ํฌ๋ช ํ ์ฌ๋ฌด ๊ด๋ฆฌ
๐ง ๋ฌธ์ ํด๊ฒฐ ๊ฐ์ด๋
๊ฐ๋ฐํ๋ค ๋ณด๋ฉด ์ฌ๋ฌ ๋ฌธ์ ์ ๋ถ๋ชํ๊ฒ ๋ผ. ์์ฃผ ๋ฐ์ํ๋ ๋ฌธ์ ๋ค๊ณผ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์ ๋ฆฌํด๋ดค์ด! ๐ ๏ธ
| ๋ฌธ์ | ์์ธ | ํด๊ฒฐ ๋ฐฉ๋ฒ |
|---|---|---|
| ๊ฐ์ค๋น ๋ถ์กฑ ์๋ฌ | ํธ๋์ญ์ ์คํ์ ํ์ํ ๊ฐ์ค๊ฐ ๋ถ์กฑ | ๊ฐ์ค ๋ฆฌ๋ฐ์ ๋์ด๊ฑฐ๋, ์ง๊ฐ์ ์ด๋ ์ถฉ์ |
| Nonce too low | ํธ๋์ญ์ ์์ ๋ฒํธ ์ถฉ๋ | Pending ํธ๋์ญ์ ์ทจ์ ํ ์ฌ์๋ |
| ์ปจํธ๋ํธ ํธ์ถ ์คํจ | ABI ๋ถ์ผ์น ๋๋ ์ฃผ์ ์ค๋ฅ | ์ปจํธ๋ํธ ์ฃผ์์ ABI ์ฌํ์ธ |
| ํธ๋์ญ์ ํ์์์ | ๋คํธ์ํฌ ํผ์ก ๋๋ ๊ฐ์ค๋น ๋๋ฌด ๋ฎ์ | ๊ฐ์ค ๊ฐ๊ฒฉ ๋์ด๊ฑฐ๋ ์ฌ์๋ |
| ๊ฐ์ธํค ๋ณตํธํ ์คํจ | ์ํธํ ํค ๋ถ์ผ์น | APP_KEY ํ์ธ ๋ฐ ์ฌ์ํธํ |
| CORS ์๋ฌ | ํ๋ก ํธ์๋-๋ฐฑ์๋ ๋๋ฉ์ธ ๋ถ์ผ์น | Laravel CORS ์ค์ ํ์ธ |
๐ ๋๋ฒ๊น ํ
1. ํธ๋์ญ์ ์คํจ ์์ธ ํ์
// ํธ๋์ญ์
์์์ฆ์์ ์๋ฌ ํ์ธ
const receipt = await web3.eth.getTransactionReceipt(txHash);
if (receipt.status === false) {
// ์คํจํ ํธ๋์ญ์
// Etherscan์์ ์์ธ ์๋ฌ ํ์ธ
}
2. ์ด๋ฒคํธ ๋ก๊ทธ ํ์ฉ
// ์ค๋งํธ ์ปจํธ๋ํธ์์ ์ด๋ฒคํธ ๋ฐ์
emit Transfer(from, to, amount);
// ๋ผ๋ผ๋ฒจ์์ ์ด๋ฒคํธ ๋ฆฌ์ค๋
$events = $contract->getPastEvents('Transfer', [
'fromBlock' => $fromBlock,
'toBlock' => 'latest'
]);
3. ๋ก์ปฌ ๋ธ๋ก์ฒด์ธ ์ํ ํ์ธ
# Ganache CLI๋ก ๊ณ์ ํ์ธ
ganache-cli --accounts 10 --defaultBalanceEther 100
# ๋ธ๋ก ๋์ด ํ์ธ
truffle console
> web3.eth.getBlockNumber()
๐ ์ฑ๋ฅ ์ต์ ํ
์์คํ ์ด ์ปค์ง์๋ก ์ฑ๋ฅ ์ต์ ํ๊ฐ ์ค์ํด์ ธ. ๋ช ๊ฐ์ง ํ์ ์๋ ค์ค๊ฒ! โก
1. ๊ฐ์ค ์ต์ ํ
- ๋ถํ์ํ storage ์ฌ์ฉ ์ต์ํ
- uint256 ๋์ ์ ์ ํ ํฌ๊ธฐ ์ฌ์ฉ
- ๋ฐ๋ณต๋ฌธ ์ต์ํ
- ํจ์ ๊ฐ์์ฑ ๋ช ํํ (external vs public)
2. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ต์ ํ
- ์ธ๋ฑ์ค ์ ์ ํ ์ค์
- ์ฟผ๋ฆฌ ์ต์ ํ (N+1 ๋ฌธ์ ํด๊ฒฐ)
- ์บ์ฑ ํ์ฉ (Redis)
- ํ์ด์ง๋ค์ด์ ๊ตฌํ
3. API ์ต์ ํ
- Rate Limiting ์ค์
- ์๋ต ์บ์ฑ
- ๋ถํ์ํ ๋ฐ์ดํฐ ์ ๊ฑฐ
- Gzip ์์ถ
4. ํ๋ก ํธ์๋ ์ต์ ํ
- ์ฝ๋ ์คํ๋ฆฌํ
- Lazy Loading
- ์ด๋ฏธ์ง ์ต์ ํ
- CDN ํ์ฉ
๐ ์บ์ฑ ๊ตฌํ
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
class CachedBlockchainService extends BlockchainService
{
public function getBalance($address)
{
$cacheKey = "balance:{$address}";
return Cache::remember($cacheKey, 60, function () use ($address) {
return parent::getBalance($address);
});
}
public function totalSupply()
{
return Cache::remember('total_supply', 300, function () {
return parent::totalSupply();
});
}
public function invalidateBalanceCache($address)
{
Cache::forget("balance:{$address}");
}
}
๐ ์ถ๊ฐ ํ์ต ์๋ฃ
๋ ๊น์ด ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด ์ด ์๋ฃ๋ค์ ์ฐธ๊ณ ํด๋ด! ๐
๊ณต์ ๋ฌธ์:
- Solidity ๊ณต์ ๋ฌธ์: https://docs.soliditylang.org/
- Laravel ๊ณต์ ๋ฌธ์: https://laravel.com/docs
- OpenZeppelin ๋ฌธ์: https://docs.openzeppelin.com/
- Web3.js ๋ฌธ์: https://web3js.readthedocs.io/
ํ์ต ํ๋ซํผ:
- CryptoZombies (๊ฒ์์ผ๋ก ๋ฐฐ์ฐ๋ Solidity)
- Ethereum.org ํํ ๋ฆฌ์ผ
- Laracasts (Laravel ๊ฐ์)
- Udemy ๋ธ๋ก์ฒด์ธ ๊ฐ์
์ปค๋ฎค๋ํฐ:
- Ethereum Stack Exchange
- Laravel ํ๊ตญ ์ปค๋ฎค๋ํฐ
- Reddit r/ethdev
- Discord ๊ฐ๋ฐ์ ์ฑ๋
๋๊ตฌ:
- Remix IDE (์จ๋ผ์ธ Solidity ํธ์ง๊ธฐ)
- Etherscan (๋ธ๋ก์ฒด์ธ ํ์๊ธฐ)
- Tenderly (๋๋ฒ๊น ๋๊ตฌ)
- MythX (๋ณด์ ๋ถ์ ๋๊ตฌ)
๐ฏ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ ๋ง ๊ธด ์ฌ์ ์ด์์ด! ๐ ์ฐ๋ฆฌ๋ ๋ผ๋ผ๋ฒจ๊ณผ ์๋ฆฌ๋ํฐ๋ฅผ ๊ฒฐํฉํด์ ์์ ํ ์๋ํ๋ ์ปค์คํ ํ ํฐ ๋ฐํ ์์คํ ์ ๋ง๋ค์ด๋ดค์ด. ์ค๋งํธ ์ปจํธ๋ํธ ์์ฑ๋ถํฐ ์น ์ธํฐํ์ด์ค ๊ตฌํ, ๋ฐฐํฌ, ๋ณด์, ์ต์ ํ๊น์ง ์ ๊ณผ์ ์ ๋ค๋ค์ง!
์ด์ ๋๋ ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ์๋ก์ ํ ๊ฑธ์ ๋ ๋์๊ฐ ์ ์์ด. ํ์ง๋ง ๊ธฐ์ตํด! ์ด๊ฑด ์์์ผ ๋ฟ์ด์ผ. ๋ธ๋ก์ฒด์ธ ๊ธฐ์ ์ ๊ณ์ ๋ฐ์ ํ๊ณ ์๊ณ , ๋ฐฐ์ธ ๊ฒ ์ ๋ง ๋ง์. ๊พธ์คํ ๊ณต๋ถํ๊ณ ์คํํ๋ฉด์ ๋๋ง์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ด! ๐ช
1. ์๊ฒ ์์ํด: ์ฒ์๋ถํฐ ๊ฑฐ์ฐฝํ ํ๋ก์ ํธ๋ณด๋ค๋ ๊ฐ๋จํ ํ ํฐ๋ถํฐ ์์ํด๋ด.
2. ๋ณด์์ ์ต์ฐ์ ์ผ๋ก: ๋ธ๋ก์ฒด์ธ์์๋ ํ ๋ฒ์ ์ค์๊ฐ ์น๋ช ์ ์ผ ์ ์์ด. ํญ์ ๋ณด์์ ๋จผ์ ์๊ฐํด.
3. ์ปค๋ฎค๋ํฐ ํ์ฉ: ํผ์ ๊ณ ๋ฏผํ์ง ๋ง๊ณ ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํ๊ณ ๋์์ ๊ตฌํด.
4. ์ค์ ๊ฒฝํ: ์ด๋ก ๋ง์ผ๋ก๋ ๋ถ์กฑํด. ์ง์ ๋ง๋ค๊ณ ๋ฐฐํฌํด๋ณด๋ฉด์ ๊ฒฝํ์ ์์.
5. ์ง์์ ํ์ต: ๋ธ๋ก์ฒด์ธ ๊ธฐ์ ์ ๋น ๋ฅด๊ฒ ๋ณํํด. ์ต์ ํธ๋ ๋๋ฅผ ๊ณ์ ๋ฐ๋ผ๊ฐ์ผ ํด.
6. ๋ฒ์ ๊ฒํ : ํ ํฐ ๋ฐํ์ ๋ฒ์ ์ด์๊ฐ ์์ ์ ์์ด. ๋ฐ๋์ ์ ๋ฌธ๊ฐ์ ์๋ดํด.
7. ์ฌ์ฉ์ ์ค์ฌ: ๊ธฐ์ ๋ณด๋ค ์ค์ํ ๊ฑด ์ฌ์ฉ์ ๊ฒฝํ์ด์ผ. ํธ๋ฆฌํ๊ณ ์ง๊ด์ ์ธ ์๋น์ค๋ฅผ ๋ง๋ค์ด.
๋ธ๋ก์ฒด์ธ๊ณผ ์น ๊ฐ๋ฐ์ ๋ง๋จ์ ์ ๋ง ํฅ๋ฏธ์ง์งํ ๋ถ์ผ์ผ! ํ์ค์ํ, ํฌ๋ช ์ฑ, ์ ๋ขฐ์ฑ์ด๋ผ๋ ๋ธ๋ก์ฒด์ธ์ ์ฅ์ ๊ณผ ํธ๋ฆฌํจ, ์ ๊ทผ์ฑ์ด๋ผ๋ ์น์ ์ฅ์ ์ ๊ฒฐํฉํ๋ฉด ๋ฌด๊ถ๋ฌด์งํ ๊ฐ๋ฅ์ฑ์ด ์ด๋ ค. ๐
๋๋ ์ด์ ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์ ํ ํฐ ์ด์ฝ๋ ธ๋ฏธ๋ฅผ ์ ๋ชฉ์ํค๊ฑฐ๋, ์์ ํ ์๋ก์ด ๋ธ๋ก์ฒด์ธ ์๋น์ค๋ฅผ ๋ง๋ค ์ ์์ด! ์์๋ ฅ์ ๋ฐํํด์ ์ธ์์ ๋ฐ๊ฟ ํ์ ์ ์ธ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ด! ๐ก
ํน์ ๋งํ๋ ๋ถ๋ถ์ด ์๊ฑฐ๋ ๋์์ด ํ์ํ๋ฉด ์ธ์ ๋ ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํด. ์ฐ๋ฆฌ ๋ชจ๋ ํจ๊ป ์ฑ์ฅํ๋ ๊ฑฐ๋๊น! ํ์ดํ ! ๐ฅ
๐ ๋น์ ์ ๋ธ๋ก์ฒด์ธ ์ฌ์ ์ ์์ํฉ๋๋ค!
Keep Building, Keep Learning, Keep Innovating! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.