์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ ๋ผ๋ผ๋ฒจ๊ณผ ์†”๋ฆฌ๋””ํ‹ฐ๋กœ ๋‚˜๋งŒ์˜ ์ปค์Šคํ…€ ํ† ํฐ ๋ฐœํ–‰ ์‹œ์Šคํ…œ ์™„๋ฒฝ ๊ตฌ์ถ•ํ•˜๊ธฐ

๐Ÿš€ ๋ผ๋ผ๋ฒจ๊ณผ ์†”๋ฆฌ๋””ํ‹ฐ๋กœ ๋‚˜๋งŒ์˜ ์ปค์Šคํ…€ ํ† ํฐ ๋ฐœํ–‰ ์‹œ์Šคํ…œ ์™„๋ฒฝ ๊ตฌ์ถ•ํ•˜๊ธฐ

๋ธ”๋ก์ฒด์ธ๊ณผ ์›น ๊ฐœ๋ฐœ์˜ ๋งŒ๋‚จ, ์‹ค์ „ ํ”„๋กœ์ ํŠธ๋กœ ๋ฐฐ์šฐ๋Š” ํ† ํฐ ์ด์ฝ”๋…ธ๋ฏธ ๐ŸŽฏ

์•ˆ๋…•! ํ˜น์‹œ ๋„ˆ๋งŒ์˜ ์•”ํ˜ธํ™”ํ ํ† ํฐ์„ ๋งŒ๋“ค์–ด๋ณด๊ณ  ์‹ถ์—ˆ๋˜ ์  ์žˆ์–ด? ๐Ÿค” ์š”์ฆ˜ NFT, DeFi, ๋ฉ”ํƒ€๋ฒ„์Šค ๋“ฑ ๋ธ”๋ก์ฒด์ธ ๊ธฐ์ˆ ์ด ๋Œ€์„ธ์ธ๋ฐ, ๋ง‰์ƒ ์ง์ ‘ ํ† ํฐ์„ ๋ฐœํ–‰ํ•˜๋ ค๋ฉด ์–ด๋””์„œ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์•ผ ํ• ์ง€ ๋ง‰๋ง‰ํ•˜์ง€? ์˜ค๋Š˜์€ ๋ผ๋ผ๋ฒจ(Laravel)์ด๋ผ๋Š” ๊ฐ•๋ ฅํ•œ PHP ํ”„๋ ˆ์ž„์›Œํฌ์™€ ์†”๋ฆฌ๋””ํ‹ฐ(Solidity)๋ผ๋Š” ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์–ธ์–ด๋ฅผ ๊ฒฐํ•ฉํ•ด์„œ, ์™„์ „ํžˆ ์ž‘๋™ํ•˜๋Š” ์ปค์Šคํ…€ ํ† ํฐ ๋ฐœํ–‰ ์‹œ์Šคํ…œ์„ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ๋ ค์ค„๊ฒŒ! ๐Ÿ’ช

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

Laravel ์›น ํ”„๋ ˆ์ž„์›Œํฌ Solidity ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ํ† ํฐ ๋ฐœํ–‰ ์‹œ์Šคํ…œ ๊ตฌ์ถ• 1 ํ™˜๊ฒฝ ์„ค์ • 2 ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ 3 ๋ผ๋ผ๋ฒจ ํ†ตํ•ฉ 4 ๋ฐฐํฌ ๋ฐ ํ…Œ์ŠคํŠธ 5 ์šด์˜ ๊ด€๋ฆฌ

๐ŸŽฏ ์™œ ๋ผ๋ผ๋ฒจ๊ณผ ์†”๋ฆฌ๋””ํ‹ฐ๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ• ๊นŒ?

๋จผ์ € ์ด ๋‘ ๊ธฐ์ˆ ์„ ์™œ ์กฐํ•ฉํ•˜๋Š”์ง€๋ถ€ํ„ฐ ์ดํ•ดํ•ด๋ณด์ž! ๊ฐ๊ฐ์˜ ์—ญํ• ์ด ๋ช…ํ™•ํ•˜๊ฒŒ ๋‹ค๋ฅด๊ฑฐ๋“ . ๐Ÿง

๐Ÿ”ท ์†”๋ฆฌ๋””ํ‹ฐ(Solidity)์˜ ์—ญํ• 

์†”๋ฆฌ๋””ํ‹ฐ๋Š” ์ด๋”๋ฆฌ์›€ ๋ธ”๋ก์ฒด์ธ์—์„œ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์•ผ. ์‰ฝ๊ฒŒ ๋งํ•ด์„œ, ๋ธ”๋ก์ฒด์ธ ์œ„์—์„œ ์ž๋™์œผ๋กœ ์‹คํ–‰๋˜๋Š” ๊ณ„์•ฝ์„œ๋ฅผ ์ฝ”๋“œ๋กœ ์ž‘์„ฑํ•˜๋Š” ๊ฑฐ์ง€. ํ† ํฐ์˜ ๋ฐœํ–‰, ์ „์†ก, ์†Œ๊ฐ ๋“ฑ์˜ ํ•ต์‹ฌ ๋กœ์ง์ด ์—ฌ๊ธฐ์— ๋“ค์–ด๊ฐ€. ํ•œ๋ฒˆ ๋ฐฐํฌ๋˜๋ฉด ๋ˆ„๊ตฌ๋„ ์ž„์˜๋กœ ์ˆ˜์ •ํ•  ์ˆ˜ ์—†์–ด์„œ ์‹ ๋ขฐ์„ฑ๊ณผ ํˆฌ๋ช…์„ฑ์ด ๋ณด์žฅ๋ผ! ๐Ÿ”
๐Ÿ”ท ๋ผ๋ผ๋ฒจ(Laravel)์˜ ์—ญํ• 

๋ผ๋ผ๋ฒจ์€ 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
๐Ÿ’ก ์ดˆ๋ณด์ž ํŒ: ์ฒ˜์Œ์ด๋ผ๋ฉด Ganache GUI ๋ฒ„์ „์„ ์ถ”์ฒœํ•ด! ์‹œ๊ฐ์ ์œผ๋กœ ๋ธ”๋ก์ฒด์ธ ์ƒํƒœ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์„œ ์ดํ•ดํ•˜๊ธฐ ํ›จ์”ฌ ์‰ฌ์›Œ. ๋งˆ์น˜ ๊ฒŒ์ž„ ์น˜ํŠธํ‚ค ๋ณด๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ชจ๋“  ๊ฒŒ ๋ณด์ด๊ฑฐ๋“ ! ๐Ÿ˜„

๐Ÿš€ ๋ผ๋ผ๋ฒจ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ

์ž, ์ด์ œ ๋ผ๋ผ๋ฒจ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž!

# ๋ผ๋ผ๋ฒจ ์„ค์น˜
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 (์ตœ๋Œ€ ๋ฐœํ–‰๋Ÿ‰): ํ† ํฐ์˜ ์ตœ๋Œ€ ๊ณต๊ธ‰๋Ÿ‰์„ ์ œํ•œํ•  ์ˆ˜ ์žˆ์–ด. ๋น„ํŠธ์ฝ”์ธ์ฒ˜๋Ÿผ ํฌ์†Œ์„ฑ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์ง€!
โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ: ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋Š” ํ•œ๋ฒˆ ๋ฐฐํฌ๋˜๋ฉด ์ˆ˜์ •์ด ๋ถˆ๊ฐ€๋Šฅํ•ด! ๊ทธ๋ž˜์„œ ๋ฐฐํฌ ์ „์— ์ฒ ์ €ํ•œ ํ…Œ์ŠคํŠธ๊ฐ€ ํ•„์ˆ˜์•ผ. ์‹ค์ œ ๋ฉ”์ธ๋„ท์— ๋ฐฐํฌํ•˜๊ธฐ ์ „์—๋Š” ๋ฐ˜๋“œ์‹œ ์ „๋ฌธ๊ฐ€์˜ ๋ณด์•ˆ ๊ฐ์‚ฌ(Security Audit)๋ฅผ ๋ฐ›๋Š” ๊ฑธ ์ถ”์ฒœํ•ด. ๐Ÿ”’

๐Ÿ“ฆ 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

์„ฑ๊ณตํ•˜๋ฉด ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ๊ฐ€ ์ถœ๋ ฅ๋  ๊ฑฐ์•ผ. ์ด ์ฃผ์†Œ๋ฅผ ์ž˜ ๊ธฐ์–ตํ•ด๋‘ฌ! ๋‚˜์ค‘์— ๋ผ๋ผ๋ฒจ์—์„œ ์‚ฌ์šฉํ•  ๊ฑฐ๊ฑฐ๋“ . ๐Ÿ“

์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ํ† ํฐ ๋ฐœํ–‰ ํ† ํฐ ์ „์†ก ์ž”์•ก ์กฐํšŒ ํ† ํฐ ์†Œ๊ฐ ๋ผ๋ผ๋ฒจ ๋ฐฑ์—”๋“œ API ์—”๋“œํฌ์ธํŠธ ์‚ฌ์šฉ์ž ์ธ์ฆ ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ”„๋ก ํŠธ์—”๋“œ ๋Œ€์‹œ๋ณด๋“œ ํ† ํฐ ์ „์†ก UI ๊ฑฐ๋ž˜ ๋‚ด์—ญ ์ง€๊ฐ‘ ์—ฐ๋™

๐Ÿ”— ๋ผ๋ผ๋ฒจ๊ณผ ๋ธ”๋ก์ฒด์ธ ์—ฐ๋™ํ•˜๊ธฐ

์ž, ์ด์ œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด์•ผ! ๋ผ๋ผ๋ฒจ์—์„œ ๋ธ”๋ก์ฒด์ธ๊ณผ ํ†ต์‹ ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž. ์ด๊ฒŒ ๋ฐ”๋กœ ์›น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;
    }
}

์ด ์„œ๋น„์Šค ํด๋ž˜์Šค๊ฐ€ ๋ธ”๋ก์ฒด์ธ๊ณผ์˜ ๋ชจ๋“  ์ƒํ˜ธ์ž‘์šฉ์„ ๋‹ด๋‹นํ•ด! ๋งˆ์น˜ ํ†ต์—ญ์‚ฌ์ฒ˜๋Ÿผ ๋ผ๋ผ๋ฒจ๊ณผ ๋ธ”๋ก์ฒด์ธ ์‚ฌ์ด์—์„œ ๋ฉ”์‹œ์ง€๋ฅผ ์ „๋‹ฌํ•˜๋Š” ๊ฑฐ์ง€. ๐Ÿ—ฃ๏ธ

๐Ÿ’ก ์‹ค์ „ ํŒ: ํŠธ๋žœ์žญ์…˜ ์„œ๋ช…์€ ๋ณด์•ˆ์ƒ ๋งค์šฐ ์ค‘์š”ํ•ด! ์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๊ฐœ์ธํ‚ค๋ฅผ ์„œ๋ฒ„์— ์ €์žฅํ•˜์ง€ ๋ง๊ณ , ์‚ฌ์šฉ์ž๊ฐ€ MetaMask ๊ฐ™์€ ์ง€๊ฐ‘์œผ๋กœ ์ง์ ‘ ์„œ๋ช…ํ•˜๊ฒŒ ํ•˜๋Š” ๊ฒŒ ํ›จ์”ฌ ์•ˆ์ „ํ•ด. ์„œ๋ฒ„๋Š” ๊ทธ๋ƒฅ ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ๋งŒ ์ค€๋น„ํ•ด์ฃผ๋Š” ์—ญํ• ๋งŒ ํ•˜๋Š” ๊ฑฐ์ง€! ๐Ÿ”

๐Ÿ—„๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชจ๋ธ ์ƒ์„ฑ

๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜ ๊ธฐ๋ก์„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—๋„ ์ €์žฅํ•ด๋‘๋ฉด ์ข‹์•„. ๋น ๋ฅธ ์กฐํšŒ์™€ ํ†ต๊ณ„๋ฅผ ์œ„ํ•ด์„œ ๋ง์ด์•ผ!

# ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ
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']);
    });
});
โš ๏ธ ๋ณด์•ˆ ์ค‘์š”: API ์—”๋“œํฌ์ธํŠธ์—๋Š” ๋ฐ˜๋“œ์‹œ ์ธ์ฆ(authentication)๊ณผ ๊ถŒํ•œ(authorization) ์ฒดํฌ๋ฅผ ํ•ด์•ผ ํ•ด! ์œ„ ์˜ˆ์‹œ์—์„œ๋Š” auth:sanctum ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์‚ฌ์šฉํ–ˆ์–ด. ์‹ค์ œ ํ”„๋กœ๋•์…˜์—์„œ๋Š” Rate Limiting, CSRF ๋ณดํ˜ธ, Input Validation ๋“ฑ ์ถ”๊ฐ€ ๋ณด์•ˆ ์กฐ์น˜๊ฐ€ ํ•„์ˆ˜์•ผ! ๐Ÿ›ก๏ธ

โšก ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…๊ณผ ํ ์‹œ์Šคํ…œ

๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜์€ ์‹œ๊ฐ„์ด ์ข€ ๊ฑธ๋ ค. ์‚ฌ์šฉ์ž๊ฐ€ ๊ธฐ๋‹ค๋ฆฌ๊ฒŒ ํ•˜๋ฉด ์•ˆ ๋˜๋‹ˆ๊นŒ, ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ฒ˜๋ฆฌํ•˜๊ณ  ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ณด์ž! ๐Ÿ”„

๐Ÿ“‹ 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
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

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