์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ ๋ผ๋ผ๋ฒจ์—์„œ ERC-20 ํ† ํฐ ์ƒ์„ฑํ•˜๊ณ  ๋ฐฐํฌ๊นŒ์ง€ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐Ÿš€ ๋ผ๋ผ๋ฒจ์—์„œ ERC-20 ํ† ํฐ ์ƒ์„ฑํ•˜๊ณ  ๋ฐฐํฌ๊นŒ์ง€ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

PHP ํ”„๋ ˆ์ž„์›Œํฌ์™€ ๋ธ”๋ก์ฒด์ธ์˜ ํ™˜์ƒ์ ์ธ ๋งŒ๋‚จ! ๐Ÿ”—

์•ˆ๋…•ํ•˜์„ธ์š”! ๐Ÿ˜Š ์˜ค๋Š˜์€ ์ง„์งœ ํ•ซํ•œ ์ฃผ์ œ๋กœ ์ฐพ์•„์™”์–ด์š” ใ…‹ใ…‹ใ…‹
๋ผ๋ผ๋ฒจ(Laravel)์ด๋ผ๋Š” PHP ํ”„๋ ˆ์ž„์›Œํฌ๋กœ ERC-20 ํ† ํฐ์„ ๋งŒ๋“ค๊ณ  ๋ฐฐํฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”!

"์–ด? ๋ผ๋ผ๋ฒจ์€ ์›น ๊ฐœ๋ฐœ ํ”„๋ ˆ์ž„์›Œํฌ์ธ๋ฐ ๋ธ”๋ก์ฒด์ธ์ด๋ž‘ ๋ฌด์Šจ ์ƒ๊ด€์ด์•ผ?" ๋ผ๊ณ  ์ƒ๊ฐํ•˜์‹ค ์ˆ˜ ์žˆ๋Š”๋ฐ์š”,
์‚ฌ์‹ค ๋ผ๋ผ๋ฒจ์„ ๋ฐฑ์—”๋“œ๋กœ ํ™œ์šฉํ•˜๋ฉด์„œ ์ด๋”๋ฆฌ์›€ ๋ธ”๋ก์ฒด์ธ๊ณผ ํ†ต์‹ ํ•˜๋Š” ๊ฑด ์ƒ๊ฐ๋ณด๋‹ค ๊ฐ„๋‹จํ•˜๋‹ต๋‹ˆ๋‹ค! ๐Ÿ’ก

์ด ๊ธ€์—์„œ๋Š” ์‹ค์ „ ์ฝ”๋“œ์™€ ํ•จ๊ป˜ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ์ฐจ๊ทผ์ฐจ๊ทผ ์•Œ๋ ค๋“œ๋ฆด ๊ฑฐ์˜ˆ์š”~

๐Ÿ”ง Laravel PHP ํ”„๋ ˆ์ž„์›Œํฌ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฐฑ์—”๋“œ ๋กœ์ง API ์„œ๋ฒ„ โ›“๏ธ Ethereum ๋ธ”๋ก์ฒด์ธ ๋„คํŠธ์›Œํฌ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ERC-20 ํ† ํฐ ํƒˆ์ค‘์•™ํ™” Web3.js / Ethers.js ๐Ÿช™ ERC-20 ํ† ํฐ

๐Ÿค” ERC-20 ํ† ํฐ์ด ๋ญ”๋ฐ?

๋จผ์ € ๊ธฐ๋ณธ๋ถ€ํ„ฐ ์งš๊ณ  ๋„˜์–ด๊ฐˆ๊ฒŒ์š”!
ERC-20์€ ์ด๋”๋ฆฌ์›€ ๋ธ”๋ก์ฒด์ธ์—์„œ ํ† ํฐ์„ ๋งŒ๋“ค ๋•Œ ๋”ฐ๋ผ์•ผ ํ•˜๋Š” ํ‘œ์ค€ ๊ทœ๊ฒฉ์ด์—์š”.

์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด "ํ† ํฐ ๋งŒ๋“ค ๋•Œ ์ด๋Ÿฐ ๊ธฐ๋Šฅ๋“ค์€ ๊ผญ ๋„ฃ์–ด์•ผ ํ•ด!"๋ผ๊ณ  ์ •ํ•ด๋†“์€ ๋ฃฐ์ด์ฃ  ใ…‹ใ…‹ใ…‹
๋•๋ถ„์— ๋ชจ๋“  ERC-20 ํ† ํฐ์€ ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ์ž‘๋™ํ•ด์„œ ํ˜ธํ™˜์„ฑ์ด ์—„์ฒญ ์ข‹์•„์š”! ๐ŸŽฏ

๐Ÿ“‹ ERC-20 ํ† ํฐ์˜ ํ•„์ˆ˜ ๊ธฐ๋Šฅ๋“ค:

โ€ข totalSupply() - ์ „์ฒด ํ† ํฐ ๋ฐœํ–‰๋Ÿ‰ ์กฐํšŒ
โ€ข balanceOf(address) - ํŠน์ • ์ฃผ์†Œ์˜ ํ† ํฐ ์ž”์•ก ์กฐํšŒ
โ€ข transfer(to, amount) - ํ† ํฐ ์ „์†ก
โ€ข approve(spender, amount) - ํ† ํฐ ์‚ฌ์šฉ ๊ถŒํ•œ ๋ถ€์—ฌ
โ€ข transferFrom(from, to, amount) - ๊ถŒํ•œ ๋ฐ›์€ ํ† ํฐ ์ „์†ก
โ€ข allowance(owner, spender) - ์Šน์ธ๋œ ํ† ํฐ๋Ÿ‰ ์กฐํšŒ

์ด 6๊ฐ€์ง€ ํ•จ์ˆ˜๋งŒ ๊ตฌํ˜„ํ•˜๋ฉด ERC-20 ํ† ํฐ ์™„์„ฑ! ๐Ÿ’ช

๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์ค€๋น„ํ•˜๊ธฐ

๋ณธ๊ฒฉ์ ์œผ๋กœ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์— ํ•„์š”ํ•œ ๊ฒƒ๋“ค์„ ์ค€๋น„ํ•ด๋ณผ๊ฒŒ์š”!
์ƒ๊ฐ๋ณด๋‹ค ์ค€๋น„๋ฌผ์ด ๋งŽ์ง€๋งŒ ํ•˜๋‚˜์”ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์„ค์น˜ํ•˜๋ฉด ์–ด๋ ต์ง€ ์•Š์•„์š” ๐Ÿ˜Š

STEP 1๏ธโƒฃ ๊ธฐ๋ณธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ธํŒ…
ํ•„์ˆ˜ ์„ค์น˜ ํ•ญ๋ชฉ:

1. PHP 8.0 ์ด์ƒ - ๋ผ๋ผ๋ฒจ ์ตœ์‹  ๋ฒ„์ „ ๊ตฌ๋™์„ ์œ„ํ•ด ํ•„์š”ํ•ด์š”
2. Composer - PHP ํŒจํ‚ค์ง€ ๊ด€๋ฆฌ์ž (๋ผ๋ผ๋ฒจ ์„ค์น˜์— ํ•„์ˆ˜!)
3. Node.js & NPM - ํ”„๋ก ํŠธ์—”๋“œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ด€๋ฆฌ์šฉ
4. MySQL ๋˜๋Š” PostgreSQL - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค (ํ† ํฐ ๊ฑฐ๋ž˜ ๋‚ด์—ญ ์ €์žฅ์šฉ)
5. MetaMask - ์ด๋”๋ฆฌ์›€ ์ง€๊ฐ‘ (ํ…Œ์ŠคํŠธ์šฉ ํ•„์ˆ˜ ์•„์ดํ…œ! ๐ŸฆŠ)
STEP 2๏ธโƒฃ ๋ผ๋ผ๋ฒจ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
ํ„ฐ๋ฏธ๋„ ๋ช…๋ น์–ด
composer create-project laravel/laravel erc20-token-project
cd erc20-token-project
php artisan serve

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด http://localhost:8000 ์—์„œ ๋ผ๋ผ๋ฒจ์ด ์‹คํ–‰๋ผ์š”!
"Laravel" ํ™˜์˜ ํŽ˜์ด์ง€๊ฐ€ ๋ณด์ด๋ฉด ์„ฑ๊ณต ใ…‹ใ…‹ใ…‹ ๐ŸŽ‰

STEP 3๏ธโƒฃ Web3 ๊ด€๋ จ ํŒจํ‚ค์ง€ ์„ค์น˜

๋ผ๋ผ๋ฒจ์—์„œ ์ด๋”๋ฆฌ์›€ ๋ธ”๋ก์ฒด์ธ๊ณผ ํ†ต์‹ ํ•˜๋ ค๋ฉด Web3 ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ํ•„์š”ํ•ด์š”!
PHP์šฉ Web3 ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•ด๋ณผ๊ฒŒ์š”:

Composer ํŒจํ‚ค์ง€ ์„ค์น˜
composer require sc0vu/web3.php
composer require kornrunner/keccak

web3.php๋Š” PHP์—์„œ ์ด๋”๋ฆฌ์›€ ๋…ธ๋“œ์™€ ํ†ต์‹ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ˆ์š”!
keccak์€ ์ด๋”๋ฆฌ์›€ ์ฃผ์†Œ ์ƒ์„ฑํ•  ๋•Œ ํ•„์š”ํ•œ ํ•ด์‹œ ํ•จ์ˆ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ตฌ์š” ๐Ÿ”

๐Ÿ’ก ๊ฟ€ํŒ!
์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ ๊ด€๋ จ ๋ฉ˜ํ† ๋ฅผ ์ฐพ์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์—์š”!
์‹ค์ „ ๊ฒฝํ—˜์ด ์žˆ๋Š” ๊ฐœ๋ฐœ์ž์—๊ฒŒ ์ง์ ‘ ๋ฐฐ์šฐ๋ฉด ์‚ฝ์งˆ ์‹œ๊ฐ„์„ ์—„์ฒญ ์ค„์ผ ์ˆ˜ ์žˆ๊ฑฐ๋“ ์š” ใ…Žใ…Ž

๐Ÿ“ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ž‘์„ฑํ•˜๊ธฐ

์ด์ œ ์ง„์งœ ํ•ต์‹ฌ์ธ ERC-20 ํ† ํฐ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ณผ๊ฒŒ์š”!
์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋Š” Solidity๋ผ๋Š” ์–ธ์–ด๋กœ ์ž‘์„ฑํ•˜๋Š”๋ฐ, ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ž‘ ๋น„์Šทํ•ด์„œ ๋ฐฐ์šฐ๊ธฐ ์‰ฌ์›Œ์š” ๐Ÿ‘

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ
์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋Š” ํ•œ๋ฒˆ ๋ฐฐํฌํ•˜๋ฉด ์ˆ˜์ •์ด ๋ถˆ๊ฐ€๋Šฅํ•ด์š”!
๊ทธ๋ž˜์„œ ํ…Œ์ŠคํŠธ๋„ท์—์„œ ์ถฉ๋ถ„ํžˆ ํ…Œ์ŠคํŠธํ•œ ํ›„์— ๋ฉ”์ธ๋„ท์— ๋ฐฐํฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค!
STEP 1๏ธโƒฃ Solidity ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ๊ตฌ์ถ•

์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๊ฐœ๋ฐœ์„ ์œ„ํ•ด Hardhat์ด๋‚˜ Truffle ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”.
์—ฌ๊ธฐ์„œ๋Š” ์š”์ฆ˜ ํ•ซํ•œ Hardhat์„ ์‚ฌ์šฉํ•ด๋ณผ๊ฒŒ์š”!

Hardhat ์„ค์น˜ ๋ฐ ์ดˆ๊ธฐํ™”
npm install --save-dev hardhat
npx hardhat init

์ดˆ๊ธฐํ™” ๊ณผ์ •์—์„œ "Create a JavaScript project"๋ฅผ ์„ ํƒํ•˜๋ฉด ๋ผ์š”!
๊ทธ๋Ÿฌ๋ฉด contracts, scripts, test ํด๋”๊ฐ€ ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค ๐Ÿ“

STEP 2๏ธโƒฃ ERC-20 ํ† ํฐ ์ปจํŠธ๋ž™ํŠธ ์ž‘์„ฑ

์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ํ† ํฐ ์ปจํŠธ๋ž™ํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ณผ๊ฒŒ์š”!
OpenZeppelin ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ์ „ํ•˜๊ณ  ๊ฒ€์ฆ๋œ ERC-20 ๊ตฌํ˜„์„ ์‰ฝ๊ฒŒ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด์š” ๐Ÿ›ก๏ธ

OpenZeppelin ์„ค์น˜
npm install @openzeppelin/contracts
contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is ERC20, Ownable {
    // ํ† ํฐ ์ดˆ๊ธฐ ๋ฐœํ–‰๋Ÿ‰ (1์–ต ๊ฐœ)
    uint256 private constant INITIAL_SUPPLY = 100000000 * 10**18;
    
    constructor() ERC20("MyAwesomeToken", "MAT") Ownable(msg.sender) {
        // ์ปจํŠธ๋ž™ํŠธ ๋ฐฐํฌ์ž์—๊ฒŒ ์ „์ฒด ํ† ํฐ ๋ฐœํ–‰
        _mint(msg.sender, INITIAL_SUPPLY);
    }
    
    // ํ† ํฐ ์ถ”๊ฐ€ ๋ฐœํ–‰ ํ•จ์ˆ˜ (์†Œ์œ ์ž๋งŒ ๊ฐ€๋Šฅ)
    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
    
    // ํ† ํฐ ์†Œ๊ฐ ํ•จ์ˆ˜
    function burn(uint256 amount) public {
        _burn(msg.sender, amount);
    }
}

์ด ์ฝ”๋“œ ์ง„์งœ ๊ฐ„๋‹จํ•˜์ฃ ? ใ…‹ใ…‹ใ…‹
OpenZeppelin ๋•๋ถ„์— ๋ณต์žกํ•œ ERC-20 ๋กœ์ง์„ ์ง์ ‘ ๊ตฌํ˜„ํ•  ํ•„์š”๊ฐ€ ์—†์–ด์š”!

์ฝ”๋“œ ์„ค๋ช…:
โ€ข ERC20 - ๊ธฐ๋ณธ ERC-20 ๊ธฐ๋Šฅ ์ œ๊ณต
โ€ข Ownable - ์†Œ์œ ์ž ๊ถŒํ•œ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ
โ€ข constructor - ํ† ํฐ ์ด๋ฆ„๊ณผ ์‹ฌ๋ณผ ์„ค์ •, ์ดˆ๊ธฐ ๋ฐœํ–‰
โ€ข mint - ์ถ”๊ฐ€ ํ† ํฐ ๋ฐœํ–‰ (์†Œ์œ ์ž๋งŒ ๊ฐ€๋Šฅ)
โ€ข burn - ํ† ํฐ ์†Œ๊ฐ ๊ธฐ๋Šฅ

๐ŸŽจ ํ† ํฐ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ํฌ์ธํŠธ:

1. ํ† ํฐ ์ด๋ฆ„ - "MyAwesomeToken" ๋ถ€๋ถ„์„ ์›ํ•˜๋Š” ์ด๋ฆ„์œผ๋กœ ๋ณ€๊ฒฝ
2. ํ† ํฐ ์‹ฌ๋ณผ - "MAT" ๋ถ€๋ถ„์„ ์›ํ•˜๋Š” ํ‹ฐ์ปค๋กœ ๋ณ€๊ฒฝ (๋ณดํ†ต 3-4๊ธ€์ž)
3. ์ดˆ๊ธฐ ๋ฐœํ–‰๋Ÿ‰ - INITIAL_SUPPLY ๊ฐ’ ์กฐ์ •
4. ์†Œ์ˆ˜์  ์ž๋ฆฌ - ๊ธฐ๋ณธ 18์ž๋ฆฌ (์ด๋”๋ฆฌ์›€ ํ‘œ์ค€)

์˜ˆ๋ฅผ ๋“ค์–ด "์žฌ๋Šฅ์ฝ”์ธ(JAENUNG)"์„ ๋งŒ๋“ ๋‹ค๋ฉด:
ERC20("JaenungCoin", "JAENUNG") ์ด๋ ‡๊ฒŒ ์ˆ˜์ •ํ•˜๋ฉด ๋ผ์š”! ๐Ÿช™
์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๊ตฌ์กฐ ERC20 ํ‘œ์ค€ transfer() balanceOf() approve() ์ปค์Šคํ…€ ๊ธฐ๋Šฅ mint() burn() pause() ๋ณด์•ˆ Ownable ReentrancyGuard SafeMath ๐Ÿ”’ ๋ธ”๋ก์ฒด์ธ์— ์˜๊ตฌ ์ €์žฅ

๐Ÿงช ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

๋ฐฐํฌํ•˜๊ธฐ ์ „์— ๊ผญ! ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด์•ผ ํ•ด์š”!
๋ฒ„๊ทธ๊ฐ€ ์žˆ๋Š” ์ปจํŠธ๋ž™ํŠธ๋ฅผ ๋ฐฐํฌํ•˜๋ฉด ๋ˆ์ด ๋‚ ์•„๊ฐˆ ์ˆ˜ ์žˆ๊ฑฐ๋“ ์š” ๐Ÿ˜ฑ

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ
test/MyToken.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("MyToken Contract", function () {
    let myToken;
    let owner;
    let addr1;
    let addr2;

    beforeEach(async function () {
        [owner, addr1, addr2] = await ethers.getSigners();
        const MyToken = await ethers.getContractFactory("MyToken");
        myToken = await MyToken.deploy();
    });

    describe("๋ฐฐํฌ ํ…Œ์ŠคํŠธ", function () {
        it("์˜ฌ๋ฐ”๋ฅธ ์ด๋ฆ„๊ณผ ์‹ฌ๋ณผ์ด ์„ค์ •๋˜์–ด์•ผ ํ•จ", async function () {
            expect(await myToken.name()).to.equal("MyAwesomeToken");
            expect(await myToken.symbol()).to.equal("MAT");
        });

        it("์ดˆ๊ธฐ ๋ฐœํ–‰๋Ÿ‰์ด ์†Œ์œ ์ž์—๊ฒŒ ํ• ๋‹น๋˜์–ด์•ผ ํ•จ", async function () {
            const ownerBalance = await myToken.balanceOf(owner.address);
            expect(await myToken.totalSupply()).to.equal(ownerBalance);
        });
    });

    describe("ํ† ํฐ ์ „์†ก ํ…Œ์ŠคํŠธ", function () {
        it("๊ณ„์ • ๊ฐ„ ํ† ํฐ ์ „์†ก์ด ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ", async function () {
            await myToken.transfer(addr1.address, 1000);
            const addr1Balance = await myToken.balanceOf(addr1.address);
            expect(addr1Balance).to.equal(1000);
        });

        it("์ž”์•ก ๋ถ€์กฑ ์‹œ ์ „์†ก ์‹คํŒจํ•ด์•ผ ํ•จ", async function () {
            const initialOwnerBalance = await myToken.balanceOf(owner.address);
            await expect(
                myToken.connect(addr1).transfer(owner.address, 1)
            ).to.be.reverted;
            expect(await myToken.balanceOf(owner.address)).to.equal(
                initialOwnerBalance
            );
        });
    });

    describe("Mint ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ", function () {
        it("์†Œ์œ ์ž๋Š” ์ƒˆ ํ† ํฐ์„ ๋ฐœํ–‰ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ", async function () {
            await myToken.mint(addr1.address, 5000);
            expect(await myToken.balanceOf(addr1.address)).to.equal(5000);
        });

        it("์†Œ์œ ์ž๊ฐ€ ์•„๋‹Œ ๊ณ„์ •์€ ํ† ํฐ ๋ฐœํ–‰ ๋ถˆ๊ฐ€", async function () {
            await expect(
                myToken.connect(addr1).mint(addr2.address, 5000)
            ).to.be.reverted;
        });
    });
});

ํ…Œ์ŠคํŠธ ์‹คํ–‰์€ ์ด๋ ‡๊ฒŒ ํ•ด์š”:

ํ…Œ์ŠคํŠธ ์‹คํ–‰ ๋ช…๋ น์–ด
npx hardhat test

๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ•˜๋ฉด ์ดˆ๋ก์ƒ‰ ์ฒดํฌ๋งˆํฌ๊ฐ€ ์ญ‰ ๋‚˜์™€์š”! โœ…โœ…โœ…
ํ•˜๋‚˜๋ผ๋„ ์‹คํŒจํ•˜๋ฉด ์ฝ”๋“œ๋ฅผ ๋‹ค์‹œ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค ใ… ใ… 

๐Ÿš€ ํ…Œ์ŠคํŠธ๋„ท์— ๋ฐฐํฌํ•˜๊ธฐ

์ด์ œ ์ง„์งœ ๋ธ”๋ก์ฒด์ธ์— ๋ฐฐํฌํ•  ์ฐจ๋ก€์˜ˆ์š”!
์ฒ˜์Œ์—” ํ…Œ์ŠคํŠธ๋„ท(Sepolia, Goerli ๋“ฑ)์— ๋ฐฐํฌํ•ด์„œ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒŒ ์ข‹์•„์š” ๐ŸŽฏ

STEP 1๏ธโƒฃ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •

๋จผ์ € ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— .env ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์š”:

.env ํŒŒ์ผ
PRIVATE_KEY=์—ฌ๊ธฐ์—_๋ฉ”ํƒ€๋งˆ์Šคํฌ_๊ฐœ์ธํ‚ค_์ž…๋ ฅ
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/YOUR_INFURA_PROJECT_ID
ETHERSCAN_API_KEY=์ด๋”์Šค์บ”_API_ํ‚ค
โš ๏ธ ๋ณด์•ˆ ๊ฒฝ๊ณ !
๊ฐœ์ธํ‚ค๋Š” ์ ˆ๋Œ€ ๊นƒํ—ˆ๋ธŒ์— ์˜ฌ๋ฆฌ๋ฉด ์•ˆ ๋ผ์š”!
.gitignore์— .env ํŒŒ์ผ์„ ๊ผญ ์ถ”๊ฐ€ํ•˜์„ธ์š”!
๊ฐœ์ธํ‚ค๊ฐ€ ๋…ธ์ถœ๋˜๋ฉด ์ง€๊ฐ‘์˜ ๋ชจ๋“  ์ž์‚ฐ์„ ์žƒ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค! ๐Ÿ”
STEP 2๏ธโƒฃ Hardhat ์„ค์ • ํŒŒ์ผ ์ˆ˜์ •
hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

module.exports = {
    solidity: "0.8.20",
    networks: {
        sepolia: {
            url: process.env.SEPOLIA_RPC_URL,
            accounts: [process.env.PRIVATE_KEY],
            chainId: 11155111
        },
        hardhat: {
            chainId: 1337
        }
    },
    etherscan: {
        apiKey: process.env.ETHERSCAN_API_KEY
    }
};
STEP 3๏ธโƒฃ ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ ์ž‘์„ฑ
scripts/deploy.js
const hre = require("hardhat");

async function main() {
    console.log("๐Ÿš€ ํ† ํฐ ๋ฐฐํฌ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค...");
    
    // ์ปจํŠธ๋ž™ํŠธ ํŒฉํ† ๋ฆฌ ๊ฐ€์ ธ์˜ค๊ธฐ
    const MyToken = await hre.ethers.getContractFactory("MyToken");
    
    // ๋ฐฐํฌ ์‹คํ–‰
    console.log("โณ ๋ฐฐํฌ ์ค‘...");
    const myToken = await MyToken.deploy();
    
    await myToken.waitForDeployment();
    
    const address = await myToken.getAddress();
    console.log("โœ… MyToken ๋ฐฐํฌ ์™„๋ฃŒ!");
    console.log("๐Ÿ“ ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ:", address);
    
    // ๋ฐฐํฌ ์ •๋ณด ์ถœ๋ ฅ
    console.log("\n๐Ÿ“Š ํ† ํฐ ์ •๋ณด:");
    console.log("์ด๋ฆ„:", await myToken.name());
    console.log("์‹ฌ๋ณผ:", await myToken.symbol());
    console.log("์ด ๋ฐœํ–‰๋Ÿ‰:", await myToken.totalSupply());
    
    console.log("\n๐ŸŽ‰ ๋ฐฐํฌ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!");
    console.log("์ด๋”์Šค์บ”์—์„œ ํ™•์ธ: https://sepolia.etherscan.io/address/" + address);
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });
STEP 4๏ธโƒฃ ํ…Œ์ŠคํŠธ๋„ท ETH ๋ฐ›๊ธฐ

๋ฐฐํฌํ•˜๋ ค๋ฉด ๊ฐ€์Šค๋น„๋กœ ์“ธ ํ…Œ์ŠคํŠธ๋„ท ETH๊ฐ€ ํ•„์š”ํ•ด์š”!
Sepolia Faucet์—์„œ ๋ฌด๋ฃŒ๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์š”:

๐Ÿšฐ ํ…Œ์ŠคํŠธ๋„ท ETH ๋ฐ›๋Š” ๊ณณ:

โ€ข Sepolia Faucet - https://sepoliafaucet.com/
โ€ข Alchemy Faucet - https://sepoliafaucet.com/
โ€ข Infura Faucet - https://www.infura.io/faucet/sepolia

๋ฉ”ํƒ€๋งˆ์Šคํฌ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด 0.5 ETH ์ •๋„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์š”!
ํ•˜๋ฃจ์— ํ•œ ๋ฒˆ์”ฉ ๋ฐ›์„ ์ˆ˜ ์žˆ์œผ๋‹ˆ ํ•„์š”ํ•˜๋ฉด ๋งค์ผ ๋ฐ›์œผ์„ธ์š” ใ…‹ใ…‹ใ…‹
STEP 5๏ธโƒฃ ๋ฐฐํฌ ์‹คํ–‰!
๋ฐฐํฌ ๋ช…๋ น์–ด
npx hardhat run scripts/deploy.js --network sepolia

์งœ์ž”! ๐ŸŽŠ ์ด์ œ ๋‹น์‹ ์˜ ํ† ํฐ์ด ๋ธ”๋ก์ฒด์ธ์— ์˜ฌ๋ผ๊ฐ”์–ด์š”!
์ถœ๋ ฅ๋œ ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ๋ฅผ ์ž˜ ์ €์žฅํ•ด๋‘์„ธ์š”. ๋‚˜์ค‘์— ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค!

๐Ÿ’ก Pro Tip!
๋ฐฐํฌ ํ›„ ์ด๋”์Šค์บ”์—์„œ ์ปจํŠธ๋ž™ํŠธ๋ฅผ verify(๊ฒ€์ฆ)ํ•˜๋ฉด ์†Œ์Šค์ฝ”๋“œ๊ฐ€ ๊ณต๊ฐœ๋˜์–ด ์‹ ๋ขฐ๋„๊ฐ€ ์˜ฌ๋ผ๊ฐ€์š”!
npx hardhat verify --network sepolia ์ปจํŠธ๋ž™ํŠธ์ฃผ์†Œ ๋ช…๋ น์–ด๋กœ ๊ฐ„๋‹จํžˆ ํ•  ์ˆ˜ ์žˆ์–ด์š”!

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

๋“œ๋””์–ด ๋ผ๋ผ๋ฒจ ํŒŒํŠธ์˜ˆ์š”! ใ…‹ใ…‹ใ…‹
์ด์ œ ๋ผ๋ผ๋ฒจ ๋ฐฑ์—”๋“œ์—์„œ ๋ฐฐํฌํ•œ ํ† ํฐ๊ณผ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ์š” ๐ŸŽฏ

STEP 1๏ธโƒฃ Web3 ์„œ๋น„์Šค ํด๋ž˜์Šค ์ƒ์„ฑ

๋ผ๋ผ๋ฒจ์—์„œ ๋ธ”๋ก์ฒด์ธ๊ณผ ํ†ต์‹ ํ•˜๋Š” ์„œ๋น„์Šค ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”:

app/Services/Web3Service.php
<?php

namespace App\Services;

use Web3\Web3;
use Web3\Contract;
use Web3\Utils;

class Web3Service
{
    private $web3;
    private $contract;
    private $contractAddress;
    private $abi;
    
    public function __construct()
    {
        // Infura ๋˜๋Š” Alchemy RPC URL ์‚ฌ์šฉ
        $rpcUrl = env('ETHEREUM_RPC_URL');
        $this->web3 = new Web3($rpcUrl);
        
        $this->contractAddress = env('TOKEN_CONTRACT_ADDRESS');
        $this->abi = json_decode(file_get_contents(storage_path('app/abi/MyToken.json')), true);
        
        $this->contract = new Contract($rpcUrl, $this->abi);
    }
    
    /**
     * ํ† ํฐ ์ž”์•ก ์กฐํšŒ
     */
    public function getBalance($address)
    {
        $balance = null;
        
        $this->contract->at($this->contractAddress)->call('balanceOf', $address, function ($err, $result) use (&$balance) {
            if ($err !== null) {
                throw new \Exception('์ž”์•ก ์กฐํšŒ ์‹คํŒจ: ' . $err->getMessage());
            }
            $balance = $result[0]->toString();
        });
        
        // Wei๋ฅผ Ether๋กœ ๋ณ€ํ™˜ (18 decimals)
        return bcdiv($balance, bcpow('10', '18'), 18);
    }
    
    /**
     * ํ† ํฐ ์ „์†ก
     */
    public function transfer($fromPrivateKey, $toAddress, $amount)
    {
        $eth = $this->web3->eth;
        
        // Wei๋กœ ๋ณ€ํ™˜
        $amountInWei = bcmul($amount, bcpow('10', '18'));
        
        // ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
        $data = $this->contract->at($this->contractAddress)->getData('transfer', $toAddress, $amountInWei);
        
        // ๊ฐ€์Šค ์ถ”์ •
        $gas = null;
        $eth->estimateGas([
            'to' => $this->contractAddress,
            'data' => $data
        ], function ($err, $result) use (&$gas) {
            if ($err !== null) {
                throw new \Exception('๊ฐ€์Šค ์ถ”์ • ์‹คํŒจ: ' . $err->getMessage());
            }
            $gas = $result->toString();
        });
        
        // ํŠธ๋žœ์žญ์…˜ ์ „์†ก
        // ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” ์„œ๋ช… ๋ฐ ์ „์†ก ๋กœ์ง ์ถ”๊ฐ€ ํ•„์š”
        
        return [
            'success' => true,
            'gas' => $gas,
            'data' => $data
        ];
    }
    
    /**
     * ํ† ํฐ ์ด ๋ฐœํ–‰๋Ÿ‰ ์กฐํšŒ
     */
    public function getTotalSupply()
    {
        $supply = null;
        
        $this->contract->at($this->contractAddress)->call('totalSupply', function ($err, $result) use (&$supply) {
            if ($err !== null) {
                throw new \Exception('์ด ๋ฐœํ–‰๋Ÿ‰ ์กฐํšŒ ์‹คํŒจ: ' . $err->getMessage());
            }
            $supply = $result[0]->toString();
        });
        
        return bcdiv($supply, bcpow('10', '18'), 18);
    }
    
    /**
     * ํ† ํฐ ์ด๋ฆ„ ์กฐํšŒ
     */
    public function getTokenName()
    {
        $name = null;
        
        $this->contract->at($this->contractAddress)->call('name', function ($err, $result) use (&$name) {
            if ($err !== null) {
                throw new \Exception('์ด๋ฆ„ ์กฐํšŒ ์‹คํŒจ: ' . $err->getMessage());
            }
            $name = $result[0];
        });
        
        return $name;
    }
    
    /**
     * ํ† ํฐ ์‹ฌ๋ณผ ์กฐํšŒ
     */
    public function getTokenSymbol()
    {
        $symbol = null;
        
        $this->contract->at($this->contractAddress)->call('symbol', function ($err, $result) use (&$symbol) {
            if ($err !== null) {
                throw new \Exception('์‹ฌ๋ณผ ์กฐํšŒ ์‹คํŒจ: ' . $err->getMessage());
            }
            $symbol = $result[0];
        });
        
        return $symbol;
    }
}
STEP 2๏ธโƒฃ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •

๋ผ๋ผ๋ฒจ์˜ .env ํŒŒ์ผ์— ๋ธ”๋ก์ฒด์ธ ๊ด€๋ จ ์„ค์ •์„ ์ถ”๊ฐ€ํ•ด์š”:

.env ์ถ”๊ฐ€ ๋‚ด์šฉ
ETHEREUM_RPC_URL=https://sepolia.infura.io/v3/YOUR_PROJECT_ID
TOKEN_CONTRACT_ADDRESS=0x๋ฐฐํฌํ•œ_์ปจํŠธ๋ž™ํŠธ_์ฃผ์†Œ
ETHEREUM_CHAIN_ID=11155111
STEP 3๏ธโƒฃ ์ปจํŠธ๋กค๋Ÿฌ ์ƒ์„ฑ
ํ„ฐ๋ฏธ๋„
php artisan make:controller TokenController
app/Http/Controllers/TokenController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Services\Web3Service;

class TokenController extends Controller
{
    private $web3Service;
    
    public function __construct(Web3Service $web3Service)
    {
        $this->web3Service = $web3Service;
    }
    
    /**
     * ํ† ํฐ ์ •๋ณด ์กฐํšŒ
     */
    public function getTokenInfo()
    {
        try {
            $info = [
                'name' => $this->web3Service->getTokenName(),
                'symbol' => $this->web3Service->getTokenSymbol(),
                'totalSupply' => $this->web3Service->getTotalSupply(),
            ];
            
            return response()->json([
                'success' => true,
                'data' => $info
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => $e->getMessage()
            ], 500);
        }
    }
    
    /**
     * ์ฃผ์†Œ๋ณ„ ์ž”์•ก ์กฐํšŒ
     */
    public function getBalance(Request $request)
    {
        $request->validate([
            'address' => 'required|string|size:42'
        ]);
        
        try {
            $balance = $this->web3Service->getBalance($request->address);
            
            return response()->json([
                'success' => true,
                'address' => $request->address,
                'balance' => $balance
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => $e->getMessage()
            ], 500);
        }
    }
    
    /**
     * ํ† ํฐ ์ „์†ก
     */
    public function transfer(Request $request)
    {
        $request->validate([
            'from_private_key' => 'required|string',
            'to_address' => 'required|string|size:42',
            'amount' => 'required|numeric|min:0'
        ]);
        
        try {
            $result = $this->web3Service->transfer(
                $request->from_private_key,
                $request->to_address,
                $request->amount
            );
            
            return response()->json([
                'success' => true,
                'data' => $result
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => $e->getMessage()
            ], 500);
        }
    }
}
STEP 4๏ธโƒฃ ๋ผ์šฐํŠธ ์„ค์ •
routes/api.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\TokenController;

Route::prefix('token')->group(function () {
    Route::get('/info', [TokenController::class, 'getTokenInfo']);
    Route::post('/balance', [TokenController::class, 'getBalance']);
    Route::post('/transfer', [TokenController::class, 'transfer']);
});

์ด์ œ API ์—”๋“œํฌ์ธํŠธ๊ฐ€ ์ค€๋น„๋์–ด์š”! ๐ŸŽ‰
http://localhost:8000/api/token/info ๊ฐ™์€ ์ฃผ์†Œ๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์–ด์š”!

๋ผ๋ผ๋ฒจ โ†” ๋ธ”๋ก์ฒด์ธ ํ†ต์‹  ํ๋ฆ„ ์‚ฌ์šฉ์ž ์›น ๋ธŒ๋ผ์šฐ์ € HTTP ์š”์ฒญ Laravel Controller Web3Service RPC ํ˜ธ์ถœ Infura/Alchemy RPC ๋…ธ๋“œ ๋ธ”๋ก์ฒด์ธ ์ฟผ๋ฆฌ Ethereum ๋ธ”๋ก์ฒด์ธ ์‘๋‹ต ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋œ ๋ฐ์ดํ„ฐ JSON ์‘๋‹ต ์ฃผ์š” API ์—”๋“œํฌ์ธํŠธ GET /api/token/info ํ† ํฐ ๊ธฐ๋ณธ ์ •๋ณด ์กฐํšŒ (์ด๋ฆ„, ์‹ฌ๋ณผ, ์ด ๋ฐœํ–‰๋Ÿ‰) POST /api/token/balance ์ฃผ์†Œ๋ณ„ ์ž”์•ก ์กฐํšŒ (address ํŒŒ๋ผ๋ฏธํ„ฐ ํ•„์š”) POST /api/token/transfer ํ† ํฐ ์ „์†ก ์‹คํ–‰ (from, to, amount) ๐Ÿ’ก ์‹ค์ „ ํ™œ์šฉ ์˜ˆ์‹œ โ€ข ์‚ฌ์šฉ์ž ๋Œ€์‹œ๋ณด๋“œ์—์„œ ํ† ํฐ ์ž”์•ก ์‹ค์‹œ๊ฐ„ ํ‘œ์‹œ โ€ข ํ† ํฐ ์ „์†ก ๊ธฐ๋Šฅ์„ ์›น ์ธํ„ฐํŽ˜์ด์Šค๋กœ ์ œ๊ณต โ€ข ๊ฑฐ๋ž˜ ๋‚ด์—ญ์„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅํ•˜์—ฌ ํžˆ์Šคํ† ๋ฆฌ ๊ด€๋ฆฌ

๐ŸŽจ ํ”„๋ก ํŠธ์—”๋“œ ๊ตฌํ˜„ํ•˜๊ธฐ

๋ฐฑ์—”๋“œ API๊ฐ€ ์ค€๋น„๋์œผ๋‹ˆ ์ด์ œ ์‚ฌ์šฉ์ž๊ฐ€ ๋ณผ ํ™”๋ฉด์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”!
Vue.js๋‚˜ React๋ฅผ ์“ธ ์ˆ˜๋„ ์žˆ์ง€๋งŒ, ์—ฌ๊ธฐ์„œ๋Š” ๊ฐ„๋‹จํ•˜๊ฒŒ Blade ํ…œํ”Œ๋ฆฟ์œผ๋กœ ํ•ด๋ณผ๊ฒŒ์š” ๐Ÿ˜Š

STEP 1๏ธโƒฃ ํ† ํฐ ๋Œ€์‹œ๋ณด๋“œ ๋ทฐ ์ƒ์„ฑ
resources/views/token/dashboard.blade.php
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ํ† ํฐ ๋Œ€์‹œ๋ณด๋“œ</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
        .container { max-width: 800px; margin: 0 auto; }
        .card { background: white; border-radius: 12px; padding: 25px; margin-bottom: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        .card-title { font-size: 24px; font-weight: bold; color: #2d3748; margin-bottom: 20px; }
        .info-row { display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid #e2e8f0; }
        .info-label { font-weight: 600; color: #4a5568; }
        .info-value { color: #667eea; font-weight: bold; }
        .input-group { margin-bottom: 15px; }
        .input-group label { display: block; margin-bottom: 5px; font-weight: 600; color: #4a5568; }
        .input-group input { width: 100%; padding: 10px; border: 2px solid #e2e8f0; border-radius: 8px; font-size: 14px; }
        .btn { padding: 12px 24px; border: none; border-radius: 8px; font-weight: bold; cursor: pointer; font-size: 16px; }
        .btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
        .btn-primary:hover { opacity: 0.9; }
        .loading { text-align: center; padding: 20px; color: #667eea; }
    </style>
</head>
<body>
    <div class="container">
        <div class="card">
            <h1 class="card-title">๐Ÿช™ ํ† ํฐ ์ •๋ณด</h1>
            <div id="tokenInfo" class="loading">๋กœ๋”ฉ ์ค‘...</div>
        </div>

        <div class="card">
            <h2 class="card-title">๐Ÿ’ฐ ์ž”์•ก ์กฐํšŒ</h2>
            <div class="input-group">
                <label>์ง€๊ฐ‘ ์ฃผ์†Œ</label>
                <input type="text" id="addressInput" placeholder="0x...">
            </div>
            <button class="btn btn-primary" onclick="checkBalance()">์ž”์•ก ํ™•์ธ</button>
            <div id="balanceResult" style="margin-top: 15px;"></div>
        </div>

        <div class="card">
            <h2 class="card-title">๐Ÿ“ค ํ† ํฐ ์ „์†ก</h2>
            <div class="input-group">
                <label>๋ฐ›๋Š” ์ฃผ์†Œ</label>
                <input type="text" id="toAddress" placeholder="0x...">
            </div>
            <div class="input-group">
                <label>์ „์†ก ์ˆ˜๋Ÿ‰</label>
                <input type="number" id="amount" placeholder="0.0">
            </div>
            <button class="btn btn-primary" onclick="transferToken()">์ „์†กํ•˜๊ธฐ</button>
            <div id="transferResult" style="margin-top: 15px;"></div>
        </div>
    </div>

    <script>
        // ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ํ† ํฐ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
        window.onload = function() {
            loadTokenInfo();
        };

        async function loadTokenInfo() {
            try {
                const response = await axios.get('/api/token/info');
                const data = response.data.data;
                
                document.getElementById('tokenInfo').innerHTML = `
                    <div class="info-row">
                        <span class="info-label">ํ† ํฐ ์ด๋ฆ„</span>
                        <span class="info-value">${data.name}</span>
                    </div>
                    <div class="info-row">
                        <span class="info-label">ํ† ํฐ ์‹ฌ๋ณผ</span>
                        <span class="info-value">${data.symbol}</span>
                    </div>
                    <div class="info-row">
                        <span class="info-label">์ด ๋ฐœํ–‰๋Ÿ‰</span>
                        <span class="info-value">${parseFloat(data.totalSupply).toLocaleString()} ${data.symbol}</span>
                    </div>
                `;
            } catch (error) {
                document.getElementById('tokenInfo').innerHTML = 
                    '<p style="color: red;">์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.</p>';
            }
        }

        async function checkBalance() {
            const address = document.getElementById('addressInput').value;
            
            if (!address || !address.startsWith('0x')) {
                alert('์˜ฌ๋ฐ”๋ฅธ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”!');
                return;
            }

            try {
                const response = await axios.post('/api/token/balance', { address });
                const balance = response.data.balance;
                
                document.getElementById('balanceResult').innerHTML = `
                    <div style="padding: 15px; background: #f0f4ff; border-radius: 8px;">
                        <strong>์ž”์•ก:</strong> ${parseFloat(balance).toLocaleString()} ํ† ํฐ
                    </div>
                `;
            } catch (error) {
                document.getElementById('balanceResult').innerHTML = 
                    '<p style="color: red;">์ž”์•ก ์กฐํšŒ ์‹คํŒจ</p>';
            }
        }

        async function transferToken() {
            const toAddress = document.getElementById('toAddress').value;
            const amount = document.getElementById('amount').value;

            if (!toAddress || !amount) {
                alert('๋ชจ๋“  ํ•„๋“œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”!');
                return;
            }

            try {
                // ์‹ค์ œ๋กœ๋Š” MetaMask ์—ฐ๋™ ํ•„์š”
                alert('์‹ค์ œ ์ „์†ก ๊ธฐ๋Šฅ์€ MetaMask ์—ฐ๋™์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค!');
                
                // const response = await axios.post('/api/token/transfer', {
                //     to_address: toAddress,
                //     amount: amount
                // });
                
                document.getElementById('transferResult').innerHTML = `
                    <div style="padding: 15px; background: #d1f2eb; border-radius: 8px;">
                        <strong>์ „์†ก ์ค€๋น„ ์™„๋ฃŒ!</strong> MetaMask๋กœ ์„œ๋ช…ํ•˜์„ธ์š”.
                    </div>
                `;
            } catch (error) {
                document.getElementById('transferResult').innerHTML = 
                    '<p style="color: red;">์ „์†ก ์‹คํŒจ</p>';
            }
        }
    </script>
</body>
</html>
STEP 2๏ธโƒฃ ๋ผ์šฐํŠธ ์ถ”๊ฐ€
routes/web.php
<?php

use Illuminate\Support\Facades\Route;

Route::get('/token/dashboard', function () {
    return view('token.dashboard');
});

์ด์ œ http://localhost:8000/token/dashboard๋กœ ์ ‘์†ํ•˜๋ฉด
๊น”๋”ํ•œ ํ† ํฐ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์–ด์š”! ๐ŸŽจ

๐ŸฆŠ MetaMask ์—ฐ๋™ํ•˜๊ธฐ

์ง„์งœ ๋ธ”๋ก์ฒด์ธ ์•ฑ์ฒ˜๋Ÿผ ๋งŒ๋“ค๋ ค๋ฉด MetaMask ์—ฐ๋™์ด ํ•„์ˆ˜์˜ˆ์š”!
์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์ง€๊ฐ‘์œผ๋กœ ์„œ๋ช…ํ•˜๊ณ  ํŠธ๋žœ์žญ์…˜์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด๋ณผ๊ฒŒ์š” ๐Ÿ”

Ethers.js๋กœ MetaMask ์—ฐ๋™
MetaMask ์—ฐ๋™ ์Šคํฌ๋ฆฝํŠธ
<script src="https://cdn.ethers.io/lib/ethers-5.2.umd.min.js"></script>
<script>
    let provider;
    let signer;
    let contract;
    
    const contractAddress = '0x๋ฐฐํฌํ•œ_์ปจํŠธ๋ž™ํŠธ_์ฃผ์†Œ';
    const contractABI = [
        // ABI๋Š” ์ปดํŒŒ์ผ ํ›„ ์ƒ์„ฑ๋œ JSON์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ
        "function balanceOf(address) view returns (uint256)",
        "function transfer(address to, uint256 amount) returns (bool)",
        "function name() view returns (string)",
        "function symbol() view returns (string)"
    ];
    
    // MetaMask ์—ฐ๊ฒฐ
    async function connectWallet() {
        if (typeof window.ethereum !== 'undefined') {
            try {
                // ์ง€๊ฐ‘ ์—ฐ๊ฒฐ ์š”์ฒญ
                await window.ethereum.request({ method: 'eth_requestAccounts' });
                
                provider = new ethers.providers.Web3Provider(window.ethereum);
                signer = provider.getSigner();
                contract = new ethers.Contract(contractAddress, contractABI, signer);
                
                const address = await signer.getAddress();
                console.log('์—ฐ๊ฒฐ๋œ ์ฃผ์†Œ:', address);
                
                alert('์ง€๊ฐ‘ ์—ฐ๊ฒฐ ์„ฑ๊ณต! ๐ŸŽ‰');
                
                // ์ž”์•ก ์ž๋™ ์กฐํšŒ
                await updateBalance(address);
                
            } catch (error) {
                console.error('์—ฐ๊ฒฐ ์‹คํŒจ:', error);
                alert('์ง€๊ฐ‘ ์—ฐ๊ฒฐ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
            }
        } else {
            alert('MetaMask๋ฅผ ์„ค์น˜ํ•ด์ฃผ์„ธ์š”!');
            window.open('https://metamask.io/download/', '_blank');
        }
    }
    
    // ์ž”์•ก ์กฐํšŒ
    async function updateBalance(address) {
        try {
            const balance = await contract.balanceOf(address);
            const formattedBalance = ethers.utils.formatEther(balance);
            
            document.getElementById('userBalance').innerHTML = `
                <strong>๋‚ด ์ž”์•ก:</strong> ${parseFloat(formattedBalance).toLocaleString()} ํ† ํฐ
            `;
        } catch (error) {
            console.error('์ž”์•ก ์กฐํšŒ ์‹คํŒจ:', error);
        }
    }
    
    // ํ† ํฐ ์ „์†ก
    async function sendToken() {
        const toAddress = document.getElementById('sendToAddress').value;
        const amount = document.getElementById('sendAmount').value;
        
        if (!toAddress || !amount) {
            alert('์ฃผ์†Œ์™€ ์ˆ˜๋Ÿ‰์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”!');
            return;
        }
        
        try {
            // Wei๋กœ ๋ณ€ํ™˜
            const amountInWei = ethers.utils.parseEther(amount);
            
            // ํŠธ๋žœ์žญ์…˜ ์ „์†ก
            const tx = await contract.transfer(toAddress, amountInWei);
            
            alert('ํŠธ๋žœ์žญ์…˜ ์ „์†ก ์ค‘... โณ');
            
            // ํŠธ๋žœ์žญ์…˜ ์™„๋ฃŒ ๋Œ€๊ธฐ
            await tx.wait();
            
            alert('์ „์†ก ์™„๋ฃŒ! โœ…');
            
            // ์ž”์•ก ์—…๋ฐ์ดํŠธ
            const address = await signer.getAddress();
            await updateBalance(address);
            
        } catch (error) {
            console.error('์ „์†ก ์‹คํŒจ:', error);
            alert('์ „์†ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค: ' + error.message);
        }
    }
    
    // ๋„คํŠธ์›Œํฌ ๋ณ€๊ฒฝ ๊ฐ์ง€
    window.ethereum.on('chainChanged', () => {
        window.location.reload();
    });
    
    // ๊ณ„์ • ๋ณ€๊ฒฝ ๊ฐ์ง€
    window.ethereum.on('accountsChanged', (accounts) => {
        if (accounts.length === 0) {
            alert('์ง€๊ฐ‘ ์—ฐ๊ฒฐ์ด ํ•ด์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
        } else {
            updateBalance(accounts[0]);
        }
    });
</script>

์ด์ œ ์‚ฌ์šฉ์ž๊ฐ€ MetaMask๋กœ ์ง์ ‘ ํŠธ๋žœ์žญ์…˜์— ์„œ๋ช…ํ•  ์ˆ˜ ์žˆ์–ด์š”!
๋ผ๋ผ๋ฒจ ๋ฐฑ์—”๋“œ๋Š” ์ฝ๊ธฐ ์ „์šฉ ์ž‘์—…๋งŒ ํ•˜๊ณ , ์“ฐ๊ธฐ ์ž‘์—…์€ ํด๋ผ์ด์–ธํŠธ์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฑฐ์ฃ  ใ…‹ใ…‹ใ…‹

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

๐Ÿ“Š ๊ฑฐ๋ž˜ ๋‚ด์—ญ ์ €์žฅํ•˜๊ธฐ

๋ธ”๋ก์ฒด์ธ์˜ ๋ชจ๋“  ๊ฑฐ๋ž˜๋Š” ์˜๊ตฌ์ ์œผ๋กœ ๊ธฐ๋ก๋˜์ง€๋งŒ,
์‚ฌ์šฉ์ž ํŽธ์˜๋ฅผ ์œ„ํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—๋„ ์ €์žฅํ•ด๋‘๋ฉด ์ข‹์•„์š”!
๋น ๋ฅธ ์กฐํšŒ์™€ ํ†ต๊ณ„ ๋ถ„์„์ด ๊ฐ€๋Šฅํ•˜๊ฑฐ๋“ ์š” ๐Ÿ“ˆ

STEP 1๏ธโƒฃ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ
ํ„ฐ๋ฏธ๋„
php artisan make:migration create_token_transactions_table
database/migrations/xxxx_create_token_transactions_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('token_transactions', function (Blueprint $table) {
            $table->id();
            $table->string('transaction_hash', 66)->unique();
            $table->string('from_address', 42);
            $table->string('to_address', 42);
            $table->decimal('amount', 36, 18);
            $table->string('status', 20)->default('pending');
            $table->integer('block_number')->nullable();
            $table->decimal('gas_used', 20, 0)->nullable();
            $table->decimal('gas_price', 20, 0)->nullable();
            $table->timestamp('confirmed_at')->nullable();
            $table->timestamps();
            
            $table->index('from_address');
            $table->index('to_address');
            $table->index('status');
        });
    }

    public function down()
    {
        Schema::dropIfExists('token_transactions');
    }
};
๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰
php artisan migrate
STEP 2๏ธโƒฃ ๋ชจ๋ธ ์ƒ์„ฑ
app/Models/TokenTransaction.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class TokenTransaction extends Model
{
    protected $fillable = [
        'transaction_hash',
        'from_address',
        'to_address',
        'amount',
        'status',
        'block_number',
        'gas_used',
        'gas_price',
        'confirmed_at'
    ];
    
    protected $casts = [
        'amount' => 'decimal:18',
        'gas_used' => 'decimal:0',
        'gas_price' => 'decimal:0',
        'confirmed_at' => 'datetime'
    ];
    
    // ์ƒํƒœ๋ณ„ ์กฐํšŒ
    public function scopePending($query)
    {
        return $query->where('status', 'pending');
    }
    
    public function scopeConfirmed($query)
    {
        return $query->where('status', 'confirmed');
    }
    
    public function scopeFailed($query)
    {
        return $query->where('status', 'failed');
    }
    
    // ์ฃผ์†Œ๋ณ„ ์กฐํšŒ
    public function scopeByAddress($query, $address)
    {
        return $query->where('from_address', $address)
                    ->orWhere('to_address', $address);
    }
}
STEP 3๏ธโƒฃ ๊ฑฐ๋ž˜ ๋‚ด์—ญ ์ €์žฅ ์„œ๋น„์Šค
app/Services/TransactionService.php
<?php

namespace App\Services;

use App\Models\TokenTransaction;
use Carbon\Carbon;

class TransactionService
{
    private $web3Service;
    
    public function __construct(Web3Service $web3Service)
    {
        $this->web3Service = $web3Service;
    }
    
    /**
     * ํŠธ๋žœ์žญ์…˜ ์ €์žฅ
     */
    public function saveTransaction($txHash, $fromAddress, $toAddress, $amount)
    {
        return TokenTransaction::create([
            'transaction_hash' => $txHash,
            'from_address' => $fromAddress,
            'to_address' => $toAddress,
            'amount' => $amount,
            'status' => 'pending'
        ]);
    }
    
    /**
     * ํŠธ๋žœ์žญ์…˜ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
     */
    public function updateTransactionStatus($txHash)
    {
        $transaction = TokenTransaction::where('transaction_hash', $txHash)->first();
        
        if (!$transaction) {
            return null;
        }
        
        try {
            // ๋ธ”๋ก์ฒด์ธ์—์„œ ํŠธ๋žœ์žญ์…˜ ์ •๋ณด ์กฐํšŒ
            $receipt = $this->web3Service->getTransactionReceipt($txHash);
            
            if ($receipt) {
                $transaction->update([
                    'status' => $receipt['status'] ? 'confirmed' : 'failed',
                    'block_number' => hexdec($receipt['blockNumber']),
                    'gas_used' => hexdec($receipt['gasUsed']),
                    'confirmed_at' => Carbon::now()
                ]);
            }
            
            return $transaction;
        } catch (\Exception $e) {
            return null;
        }
    }
    
    /**
     * ์‚ฌ์šฉ์ž๋ณ„ ๊ฑฐ๋ž˜ ๋‚ด์—ญ ์กฐํšŒ
     */
    public function getUserTransactions($address, $limit = 20)
    {
        return TokenTransaction::byAddress($address)
            ->orderBy('created_at', 'desc')
            ->limit($limit)
            ->get();
    }
    
    /**
     * ๋Œ€๊ธฐ ์ค‘์ธ ํŠธ๋žœ์žญ์…˜ ํ™•์ธ
     */
    public function checkPendingTransactions()
    {
        $pending = TokenTransaction::pending()->get();
        
        foreach ($pending as $tx) {
            $this->updateTransactionStatus($tx->transaction_hash);
        }
    }
}

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

โšก ์‹ค์‹œ๊ฐ„ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹

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

์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๊ตฌํ˜„
app/Services/EventListenerService.php
<?php

namespace App\Services;

use Web3\Web3;
use Web3\Contract;

class EventListenerService
{
    private $web3;
    private $contract;
    
    public function __construct()
    {
        $rpcUrl = env('ETHEREUM_RPC_URL');
        $this->web3 = new Web3($rpcUrl);
        
        $abi = json_decode(file_get_contents(storage_path('app/abi/MyToken.json')), true);
        $this->contract = new Contract($rpcUrl, $abi);
    }
    
    /**
     * Transfer ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹
     */
    public function listenTransferEvents($fromBlock = 'latest')
    {
        $contractAddress = env('TOKEN_CONTRACT_ADDRESS');
        
        // Transfer ์ด๋ฒคํŠธ ํ•„ํ„ฐ
        $filter = [
            'fromBlock' => $fromBlock,
            'toBlock' => 'latest',
            'address' => $contractAddress,
            'topics' => [
                // Transfer(address,address,uint256) ์ด๋ฒคํŠธ ์‹œ๊ทธ๋‹ˆ์ฒ˜
                '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
            ]
        ];
        
        $this->web3->eth->getLogs($filter, function ($err, $logs) {
            if ($err !== null) {
                \Log::error('์ด๋ฒคํŠธ ์กฐํšŒ ์‹คํŒจ: ' . $err->getMessage());
                return;
            }
            
            foreach ($logs as $log) {
                $this->processTransferEvent($log);
            }
        });
    }
    
    /**
     * Transfer ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
     */
    private function processTransferEvent($log)
    {
        // ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ
        $from = '0x' . substr($log->topics[1], 26);
        $to = '0x' . substr($log->topics[2], 26);
        $amount = hexdec($log->data);
        
        // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ
        \App\Models\TokenTransaction::create([
            'transaction_hash' => $log->transactionHash,
            'from_address' => $from,
            'to_address' => $to,
            'amount' => bcdiv($amount, bcpow('10', '18'), 18),
            'status' => 'confirmed',
            'block_number' => hexdec($log->blockNumber),
            'confirmed_at' => now()
        ]);
        
        // ์•Œ๋ฆผ ๋ฐœ์†ก ๋“ฑ ์ถ”๊ฐ€ ์ฒ˜๋ฆฌ
        $this->sendNotification($from, $to, $amount);
    }
    
    /**
     * ์•Œ๋ฆผ ๋ฐœ์†ก
     */
    private function sendNotification($from, $to, $amount)
    {
        // ์ด๋ฉ”์ผ, ํ‘ธ์‹œ ์•Œ๋ฆผ ๋“ฑ ๊ตฌํ˜„
        \Log::info("ํ† ํฐ ์ „์†ก ๊ฐ์ง€: {$from} -> {$to}, ์ˆ˜๋Ÿ‰: {$amount}");
    }
}
์Šค์ผ€์ค„๋Ÿฌ๋กœ ์ฃผ๊ธฐ์  ์ฒดํฌ
app/Console/Kernel.php
<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    protected function schedule(Schedule $schedule)
    {
        // 1๋ถ„๋งˆ๋‹ค ๋Œ€๊ธฐ ์ค‘์ธ ํŠธ๋žœ์žญ์…˜ ํ™•์ธ
        $schedule->call(function () {
            $service = app(\App\Services\TransactionService::class);
            $service->checkPendingTransactions();
        })->everyMinute();
        
        // 5๋ถ„๋งˆ๋‹ค ์ƒˆ๋กœ์šด ์ด๋ฒคํŠธ ํ™•์ธ
        $schedule->call(function () {
            $service = app(\App\Services\EventListenerService::class);
            $service->listenTransferEvents();
        })->everyFiveMinutes();
    }
}

์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ์‹คํ–‰ํ•˜๋ ค๋ฉด:

ํ„ฐ๋ฏธ๋„
php artisan schedule:work

์ด์ œ ๋ธ”๋ก์ฒด์ธ์˜ ๋ชจ๋“  ํ† ํฐ ์ „์†ก์ด ์ž๋™์œผ๋กœ ๊ฐ์ง€๋˜๊ณ  ์ €์žฅ๋ผ์š”! ๐ŸŽฏ

๐Ÿ›ก๏ธ ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ

๋ธ”๋ก์ฒด์ธ ์•ฑ์€ ๋ˆ๊ณผ ์ง๊ฒฐ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ณด์•ˆ์ด ์ •๋ง ์ค‘์š”ํ•ด์š”!
๊ผญ ์ง€์ผœ์•ผ ํ•  ๋ณด์•ˆ ์›์น™๋“ค์„ ์ •๋ฆฌํ•ด๋ณผ๊ฒŒ์š” ๐Ÿ”

๋ณด์•ˆ ํ•ญ๋ชฉ ์„ค๋ช… ๊ตฌํ˜„ ๋ฐฉ๋ฒ•
๊ฐœ์ธํ‚ค ๊ด€๋ฆฌ ์ ˆ๋Œ€ ์„œ๋ฒ„์— ์ €์žฅ ๊ธˆ์ง€ MetaMask ๋“ฑ ํด๋ผ์ด์–ธํŠธ ์ง€๊ฐ‘ ์‚ฌ์šฉ
ํ™˜๊ฒฝ ๋ณ€์ˆ˜ .env ํŒŒ์ผ ๋ณด์•ˆ .gitignore ์ถ”๊ฐ€, ์•”ํ˜ธํ™” ์ €์žฅ
API ์ธ์ฆ ๋ฌด๋‹จ ์ ‘๊ทผ ๋ฐฉ์ง€ Laravel Sanctum, JWT ํ† ํฐ
Rate Limiting ๊ณผ๋„ํ•œ ์š”์ฒญ ์ฐจ๋‹จ Laravel ๋ฏธ๋“ค์›จ์–ด ํ™œ์šฉ
์ž…๋ ฅ ๊ฒ€์ฆ ์•…์˜์  ๋ฐ์ดํ„ฐ ์ฐจ๋‹จ Validation Rules ์—„๊ฒฉํžˆ ์ ์šฉ
์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๊ฐ์‚ฌ ์ฝ”๋“œ ์ทจ์•ฝ์  ์ ๊ฒ€ ์ „๋ฌธ ๊ฐ์‚ฌ ์—…์ฒด ์˜๋ขฐ
โš ๏ธ ๋ฉ”์ธ๋„ท ๋ฐฐํฌ ์ „ ํ•„์ˆ˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

โœ… ํ…Œ์ŠคํŠธ๋„ท์—์„œ ์ถฉ๋ถ„ํ•œ ํ…Œ์ŠคํŠธ ์™„๋ฃŒ
โœ… ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๋ณด์•ˆ ๊ฐ์‚ฌ ์™„๋ฃŒ
โœ… ๊ฐœ์ธํ‚ค ๊ด€๋ฆฌ ๋ฐฉ์‹ ์žฌํ™•์ธ
โœ… ์—๋Ÿฌ ํ•ธ๋“ค๋ง ์™„๋ฒฝํžˆ ๊ตฌํ˜„
โœ… ๊ฐ€์Šค๋น„ ์ตœ์ ํ™” ์™„๋ฃŒ
โœ… ๋ฐฑ์—… ๋ฐ ๋ณต๊ตฌ ๊ณ„ํš ์ˆ˜๋ฆฝ
โœ… ๋ฒ•์  ๊ฒ€ํ†  ์™„๋ฃŒ (ํ† ํฐ ๋ฐœํ–‰ ๊ด€๋ จ ๊ทœ์ œ ํ™•์ธ)

ํ•˜๋‚˜๋ผ๋„ ๋น ๋œจ๋ฆฌ๋ฉด ํฐ ์†์‹ค์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด์š”! ๐Ÿ˜ฐ
๐Ÿ”’ ์ถ”๊ฐ€ ๋ณด์•ˆ ๊ฐ•ํ™” ํŒ:

1. Multi-Sig ์ง€๊ฐ‘ ์‚ฌ์šฉ
์ค‘์š”ํ•œ ํ† ํฐ ๊ด€๋ฆฌ๋Š” ๋‹ค์ค‘ ์„œ๋ช… ์ง€๊ฐ‘์œผ๋กœ!
Gnosis Safe ๊ฐ™์€ ์„œ๋น„์Šค ํ™œ์šฉํ•˜์„ธ์š”.

2. ํƒ€์ž„๋ฝ ๊ตฌํ˜„
์ค‘์š”ํ•œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์€ ์ผ์ • ์‹œ๊ฐ„ ํ›„์— ์‹คํ–‰๋˜๋„๋ก ์„ค์ •

3. ๊ธด๊ธ‰ ์ •์ง€ ๊ธฐ๋Šฅ
๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ ์ปจํŠธ๋ž™ํŠธ๋ฅผ ์ผ์‹œ ์ •์ง€ํ•  ์ˆ˜ ์žˆ๋Š” pause() ํ•จ์ˆ˜ ๊ตฌํ˜„

4. ์—…๊ทธ๋ ˆ์ด๋“œ ๊ฐ€๋Šฅํ•œ ์ปจํŠธ๋ž™ํŠธ
Proxy ํŒจํ„ด์œผ๋กœ ๋ฒ„๊ทธ ์ˆ˜์ • ๊ฐ€๋Šฅํ•˜๊ฒŒ ์„ค๊ณ„

5. ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ
์ด์ƒ ๊ฑฐ๋ž˜ ๊ฐ์ง€ ๋ฐ ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ์‹œ์Šคํ…œ ๊ตฌ์ถ•

๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™”

๋ธ”๋ก์ฒด์ธ ํ†ต์‹ ์€ ๋А๋ฆด ์ˆ˜ ์žˆ์–ด์„œ ์ตœ์ ํ™”๊ฐ€ ์ค‘์š”ํ•ด์š”!
์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ•˜๋Š” ๋ช‡ ๊ฐ€์ง€ ํŒ์„ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š” โšก

๐Ÿ“ˆ ์„ฑ๋Šฅ ๊ฐœ์„  ์ „๋žต:

1. ์บ์‹ฑ ํ™œ์šฉ
์ž์ฃผ ์กฐํšŒ๋˜๋Š” ๋ฐ์ดํ„ฐ๋Š” Redis์— ์บ์‹ฑํ•˜์„ธ์š”!
// ํ† ํฐ ์ •๋ณด ์บ์‹ฑ (1์‹œ๊ฐ„)
$tokenInfo = Cache::remember('token_info', 3600, function () {
    return [
        'name' => $this->web3Service->getTokenName(),
        'symbol' => $this->web3Service->getTokenSymbol(),
        'totalSupply' => $this->web3Service->getTotalSupply()
    ];
});

2. ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ
์—ฌ๋Ÿฌ ์ฃผ์†Œ์˜ ์ž”์•ก์„ ํ•œ ๋ฒˆ์— ์กฐํšŒํ•˜๋Š” Multicall ํŒจํ„ด ์‚ฌ์šฉ

3. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ์ž‘์—…์€ Queue๋กœ ์ฒ˜๋ฆฌ
// ํŠธ๋žœ์žญ์…˜ ํ™•์ธ ์ž‘์—…์„ ํ๋กœ ์ฒ˜๋ฆฌ
dispatch(new CheckTransactionJob($txHash))->delay(now()->addMinutes(1));

4. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ธ๋ฑ์‹ฑ
์ž์ฃผ ๊ฒ€์ƒ‰ํ•˜๋Š” ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค ์ถ”๊ฐ€

5. RPC ๋…ธ๋“œ ์„ ํƒ
๋น ๋ฅด๊ณ  ์•ˆ์ •์ ์ธ ๋…ธ๋“œ ์‚ฌ์šฉ (Infura, Alchemy, QuickNode ๋“ฑ)

๐Ÿ’ฐ ๊ฐ€์Šค๋น„ ์ตœ์ ํ™”

์ด๋”๋ฆฌ์›€ ๊ฐ€์Šค๋น„๋Š” ๋น„์Œ€ ์ˆ˜ ์žˆ์–ด์š”! ๐Ÿ˜ฑ
์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์ž‘์„ฑํ•˜๋ฉด ๊ฐ€์Šค๋น„๋ฅผ ๋งŽ์ด ์ ˆ์•ฝํ•  ์ˆ˜ ์žˆ์–ด์š”!

โ›ฝ ๊ฐ€์Šค๋น„ ์ ˆ์•ฝ ํ…Œํฌ๋‹‰:

1. ๋ณ€์ˆ˜ ํƒ€์ž… ์ตœ์ ํ™”
uint256 ๋Œ€์‹  uint128 ์‚ฌ์šฉ (ํ•„์š”ํ•œ ๊ฒฝ์šฐ)

2. ์ƒํƒœ ๋ณ€์ˆ˜ ์ตœ์†Œํ™”
๋ธ”๋ก์ฒด์ธ์— ์ €์žฅํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ตœ์†Œํ™”ํ•˜์„ธ์š”

3. ์ด๋ฒคํŠธ ํ™œ์šฉ
์ €์žฅ ๋Œ€์‹  ์ด๋ฒคํŠธ๋กœ ๋กœ๊ทธ ๋‚จ๊ธฐ๊ธฐ (ํ›จ์”ฌ ์ €๋ ด!)

4. ๋ฐฐ์น˜ ์ „์†ก
์—ฌ๋Ÿฌ ๊ฑด์„ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•˜๋Š” ํ•จ์ˆ˜ ๊ตฌํ˜„
function batchTransfer(address[] memory recipients, uint256[] memory amounts) public {
    require(recipients.length == amounts.length, "Length mismatch");
    for (uint i = 0; i < recipients.length; i++) {
        _transfer(msg.sender, recipients[i], amounts[i]);
    }
}

5. Layer 2 ์†”๋ฃจ์…˜ ๊ณ ๋ ค
Polygon, Arbitrum ๊ฐ™์€ L2์— ๋ฐฐํฌํ•˜๋ฉด ๊ฐ€์Šค๋น„๊ฐ€ 1/100 ์ˆ˜์ค€! ๐ŸŽ‰

๐ŸŽ“ ์‹ค์ „ ํ™œ์šฉ ์‚ฌ๋ก€

์ด์ œ ๋ฐฐ์šด ๋‚ด์šฉ์„ ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์•Œ์•„๋ณผ๊ฒŒ์š”!
๋‹ค์–‘ํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋ชจ๋ธ์— ์ ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š” ๐Ÿ’ก

๐ŸŒŸ ํ™œ์šฉ ์‚ฌ๋ก€ 1: ๋ฆฌ์›Œ๋“œ ํฌ์ธํŠธ ์‹œ์Šคํ…œ

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์‚ฌ์šฉ์ž ํ™œ๋™์— ๋”ฐ๋ผ ํ† ํฐ์„ ๋ณด์ƒ์œผ๋กœ ์ง€๊ธ‰!
โ€ข ๊ธ€ ์ž‘์„ฑ ์‹œ 10 ํ† ํฐ
โ€ข ๋Œ“๊ธ€ ์ž‘์„ฑ ์‹œ 2 ํ† ํฐ
โ€ข ์ถ”์ฒœ ๋ฐ›์œผ๋ฉด 5 ํ† ํฐ

ํ† ํฐ์€ ํ”Œ๋žซํผ ๋‚ด์—์„œ ํ”„๋ฆฌ๋ฏธ์—„ ๊ธฐ๋Šฅ ์ด์šฉ๊ถŒ์œผ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ!
๋ธ”๋ก์ฒด์ธ์— ๊ธฐ๋ก๋˜๋‹ˆ ํˆฌ๋ช…ํ•˜๊ณ  ์กฐ์ž‘ ๋ถˆ๊ฐ€๋Šฅํ•˜์ฃ  ใ…‹ใ…‹ใ…‹
๐ŸŒŸ ํ™œ์šฉ ์‚ฌ๋ก€ 2: NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค

ERC-20 ํ† ํฐ์„ ๊ฒฐ์ œ ์ˆ˜๋‹จ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” NFT ๊ฑฐ๋ž˜์†Œ!
โ€ข ์ž‘ํ’ˆ ๊ตฌ๋งค ์‹œ ํ† ํฐ์œผ๋กœ ๊ฒฐ์ œ
โ€ข ํŒ๋งค ์ˆ˜์ˆ˜๋ฃŒ๋ฅผ ํ† ํฐ์œผ๋กœ ์ง•์ˆ˜
โ€ข ์Šคํ…Œ์ดํ‚นํ•˜๋ฉด ๊ฑฐ๋ž˜ ์ˆ˜์ˆ˜๋ฃŒ ํ• ์ธ

๋ผ๋ผ๋ฒจ๋กœ ๋ฐฑ์—”๋“œ ๊ตฌ์ถ•ํ•˜๊ณ , ํ† ํฐ ๊ฒฐ์ œ ์‹œ์Šคํ…œ ์—ฐ๋™!
๐ŸŒŸ ํ™œ์šฉ ์‚ฌ๋ก€ 3: DAO(ํƒˆ์ค‘์•™ํ™” ์ž์œจ ์กฐ์ง)

ํ† ํฐ ๋ณด์œ ์ž๊ฐ€ ํ”„๋กœ์ ํŠธ ์˜์‚ฌ๊ฒฐ์ •์— ์ฐธ์—ฌ!
โ€ข 1 ํ† ํฐ = 1 ํˆฌํ‘œ๊ถŒ
โ€ข ์ œ์•ˆ ์ƒ์„ฑ ๋ฐ ํˆฌํ‘œ
โ€ข ์ž๋™ ์‹คํ–‰๋˜๋Š” ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ

๋ผ๋ผ๋ฒจ๋กœ ํˆฌํ‘œ UI ์ œ๊ณต, ๋ธ”๋ก์ฒด์ธ์—์„œ ์‹ค์ œ ํˆฌํ‘œ ์ฒ˜๋ฆฌ!
๐ŸŒŸ ํ™œ์šฉ ์‚ฌ๋ก€ 4: ํฌ๋ผ์šฐ๋“œํŽ€๋”ฉ ํ”Œ๋žซํผ

ํ”„๋กœ์ ํŠธ ํ›„์›์ž์—๊ฒŒ ํ† ํฐ ๋ฐœํ–‰!
โ€ข ๋ชฉํ‘œ ๊ธˆ์•ก ๋‹ฌ์„ฑ ์‹œ ์ž๋™ ๋ถ„๋ฐฐ
โ€ข ํ† ํฐ์œผ๋กœ ํ”„๋กœ์ ํŠธ ํ˜œํƒ ์ œ๊ณต
โ€ข 2์ฐจ ๊ฑฐ๋ž˜ ๊ฐ€๋Šฅ (์œ ๋™์„ฑ ํ™•๋ณด)

ํˆฌ๋ช…ํ•œ ์ž๊ธˆ ๊ด€๋ฆฌ์™€ ์ž๋™ํ™”๋œ ๋ถ„๋ฐฐ ์‹œ์Šคํ…œ!
ํ† ํฐ ์ƒํƒœ๊ณ„ ๊ตฌ์ถ• ๋กœ๋“œ๋งต 1๏ธโƒฃ ํ† ํฐ ์„ค๊ณ„ ์œ ํ‹ธ๋ฆฌํ‹ฐ ์ •์˜ ํ† ํฌ๋…ธ๋ฏน์Šค ์„ค๊ณ„ ๋ฒ•์  ๊ฒ€ํ†  2๏ธโƒฃ ๊ฐœ๋ฐœ & ํ…Œ์ŠคํŠธ ์ปจํŠธ๋ž™ํŠธ ์ž‘์„ฑ ํ…Œ์ŠคํŠธ๋„ท ๋ฐฐํฌ ๋ณด์•ˆ ๊ฐ์‚ฌ 3๏ธโƒฃ ๋ฉ”์ธ๋„ท ๋ฐฐํฌ ์ปจํŠธ๋ž™ํŠธ ๋ฐฐํฌ ๊ฒ€์ฆ ์™„๋ฃŒ ์œ ๋™์„ฑ ๊ณต๊ธ‰ 4๏ธโƒฃ ์ƒํƒœ๊ณ„ ๊ตฌ์ถ• ์ปค๋ฎค๋‹ˆํ‹ฐ ํ˜•์„ฑ ํŒŒํŠธ๋„ˆ์‹ญ ๋งˆ์ผ€ํŒ… 5๏ธโƒฃ ์„ฑ์žฅ & ํ™•์žฅ ๊ฑฐ๋ž˜์†Œ ์ƒ์žฅ ๊ธฐ๋Šฅ ํ™•์žฅ ๊ธ€๋กœ๋ฒŒ ์ง„์ถœ ๐ŸŽฏ ์„ฑ๊ณต์„ ์œ„ํ•œ ํ•ต์‹ฌ ์š”์†Œ ๐Ÿ’Ž ๋ช…ํ™•ํ•œ ๊ฐ€์น˜ ์ œ์•ˆ ํ† ํฐ์ด ์™œ ํ•„์š”ํ•œ์ง€ ๋ช…ํ™•ํžˆ ์ •์˜ํ•˜๊ธฐ ๐Ÿ”’ ๋ณด์•ˆ ์ตœ์šฐ์„  ๊ฐ์‚ฌ ๋ฐ›๊ณ  ์ฒ ์ €ํžˆ ํ…Œ์ŠคํŠธ ๐Ÿ‘ฅ ์ปค๋ฎค๋‹ˆํ‹ฐ ์ค‘์‹ฌ ์‚ฌ์šฉ์ž์™€ ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๊ธฐ ๐Ÿ“ˆ ์ง€์† ๊ฐ€๋Šฅ์„ฑ ์žฅ๊ธฐ์  ๊ด€์ ์˜ ํ† ํฌ๋…ธ๋ฏน์Šค ์„ค๊ณ„

๐ŸŽฌ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€! ์—ฌ๊ธฐ๊นŒ์ง€ ์˜ค๋А๋ผ ์ˆ˜๊ณ  ๋งŽ์œผ์…จ์–ด์š”! ๐Ÿ‘๐Ÿ‘๐Ÿ‘

๋ผ๋ผ๋ฒจ์—์„œ ERC-20 ํ† ํฐ์„ ์ƒ์„ฑํ•˜๊ณ  ๋ฐฐํฌํ•˜๋Š” ์ „ ๊ณผ์ •์„ ๋‹ค๋ค„๋ดค๋Š”๋ฐ์š”,
์ƒ๊ฐ๋ณด๋‹ค ๋ณต์žกํ•˜์ง€ ์•Š์ฃ ? ใ…‹ใ…‹ใ…‹

๋ฌผ๋ก  ์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋” ๋งŽ์€ ๊ณ ๋ ค์‚ฌํ•ญ์ด ์žˆ์ง€๋งŒ,
์ด ๊ฐ€์ด๋“œ๊ฐ€ ์—ฌ๋Ÿฌ๋ถ„์˜ ๋ธ”๋ก์ฒด์ธ ํ”„๋กœ์ ํŠธ ์‹œ์ž‘์— ๋„์›€์ด ๋˜์—ˆ์œผ๋ฉด ์ข‹๊ฒ ์–ด์š”! ๐Ÿš€

ํ•ต์‹ฌ ์ •๋ฆฌ:
โœ… ERC-20์€ ์ด๋”๋ฆฌ์›€ ํ† ํฐ์˜ ํ‘œ์ค€ ๊ทœ๊ฒฉ
โœ… OpenZeppelin์œผ๋กœ ์•ˆ์ „ํ•œ ์ปจํŠธ๋ž™ํŠธ ๊ตฌํ˜„
โœ… Hardhat์œผ๋กœ ๊ฐœ๋ฐœ ๋ฐ ๋ฐฐํฌ
โœ… ๋ผ๋ผ๋ฒจ๋กœ ๋ฐฑ์—”๋“œ API ๊ตฌ์ถ•
โœ… Web3.php๋กœ ๋ธ”๋ก์ฒด์ธ ํ†ต์‹ 
โœ… MetaMask๋กœ ์‚ฌ์šฉ์ž ์ง€๊ฐ‘ ์—ฐ๋™
โœ… ๋ณด์•ˆ๊ณผ ์ตœ์ ํ™”๋Š” ํ•„์ˆ˜!

๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์ด ์ฒ˜์Œ์ด๋ผ๋ฉด ์žฌ๋Šฅ๋„ท์—์„œ ๊ฒฝํ—˜ ๋งŽ์€ ๋ฉ˜ํ† ๋ฅผ ์ฐพ์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์—์š”!
์‹ค์ „ ๊ฒฝํ—˜์ด ์žˆ๋Š” ๊ฐœ๋ฐœ์ž์—๊ฒŒ ๋ฐฐ์šฐ๋ฉด ์‹œํ–‰์ฐฉ์˜ค๋ฅผ ๋งŽ์ด ์ค„์ผ ์ˆ˜ ์žˆ๊ฑฐ๋“ ์š” ๐Ÿ˜Š

์—ฌ๋Ÿฌ๋ถ„๋„ ๋ฉ‹์ง„ ํ† ํฐ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”!
Web3 ์„ธ์ƒ์—์„œ ๋งŒ๋‚˜์š”! ๐ŸŒโœจ

๐ŸŽ‰ ์ถ•ํ•˜ํ•ฉ๋‹ˆ๋‹ค!

๋‹น์‹ ์€ ์ด์ œ ๋ผ๋ผ๋ฒจ ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์ž์ž…๋‹ˆ๋‹ค! ๐ŸŽ“
๊ณ„์† ํ•™์Šตํ•˜๊ณ , ์‹คํ—˜ํ•˜๊ณ , ๋ฉ‹์ง„ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”!

Happy Coding! ๐Ÿ’ป๐Ÿš€

๐Ÿ“š ์ด ๊ธ€์ด ๋„์›€์ด ๋˜์…จ๋‚˜์š”?
์žฌ๋Šฅ๋„ท์—์„œ ๋” ๋งŽ์€ ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ ์ง€์‹์„ ๊ณต์œ ํ•˜๊ณ  ์žˆ์–ด์š”!
๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๋‹ค๋ฉด ์–ธ์ œ๋“  ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•ด์ฃผ์„ธ์š” ๐Ÿ˜Š

ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ๊ฐœ๋ฐœ์ž ์ปค๋ฎค๋‹ˆํ‹ฐ, ์žฌ๋Šฅ๋„ท! ๐ŸŒŸ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

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