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

๐ ๋ผ๋ผ๋ฒจ๋ก ๋ธ๋ก์ฒด์ธ ๊ฒฐ์ ๊ด๋ฆฌ์ ํ์ด์ง ๋ง๋ค๊ธฐ
Web3 ์๋, ๋น์ ์ ๊ฒฐ์ ์์คํ ์ ๋ธ๋ก์ฒด์ธ์ผ๋ก ์ ๊ทธ๋ ์ด๋ํ์!
๐ก ์๋
, ์น๊ตฌ! ์ค๋์ ์ ๋ง ํฅ๋ฏธ์ง์งํ ์ฃผ์ ๋ฅผ ๊ฐ์ง๊ณ ์์ด. ๋ฐ๋ก ๋ผ๋ผ๋ฒจ(Laravel)์ ์ฌ์ฉํด์ ๋ธ๋ก์ฒด์ธ ๊ฒฐ์ ๊ด๋ฆฌ์ ํ์ด์ง๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ์ด์ผ. ์์ฆ Web3๊ฐ ๋์ธ์์? ์ํธํํ ๊ฒฐ์ , NFT ๊ฑฐ๋, ์ค๋งํธ ์ปจํธ๋ํธ... ์ด๋ฐ ๊ฒ๋ค์ด ์ ์ ๋ ์ฐ๋ฆฌ ์ผ์์ผ๋ก ๋ค์ด์ค๊ณ ์์ด.
๊ทผ๋ฐ ๋ง์ ๊ฐ๋ฐํ๋ ค๊ณ ํ๋ฉด "์ด๋์๋ถํฐ ์์ํด์ผ ํ์ง?" ํ๋ ๋ง๋งํจ์ด ์์ง? ๊ฑฑ์ ๋ง! ๋ด๊ฐ ์น๊ตฌํํ
์ค๋ช
ํ๋ฏ์ด ์ฐจ๊ทผ์ฐจ๊ทผ ์๋ ค์ค๊ฒ. ์ด ๊ธ์ ๋ค ์ฝ๊ณ ๋๋ฉด ๋๋ ๋ธ๋ก์ฒด์ธ ๊ฒฐ์ ์์คํ
์ ๊ตฌ์ถํ ์ ์์ ๊ฑฐ์ผ! ๐ฏ
๐ฏ ์ ๋ผ๋ผ๋ฒจ๊ณผ ๋ธ๋ก์ฒด์ธ์ ํจ๊ป ์ฌ์ฉํ ๊น?
๋จผ์ ์ด ์กฐํฉ์ด ์ ์ข์์ง๋ถํฐ ์ด์ผ๊ธฐํด๋ณด์. ๋ผ๋ผ๋ฒจ์ PHP ๊ธฐ๋ฐ์ ์น ํ๋ ์์ํฌ ์ค์์ ๊ฐ์ฅ ์ธ๊ธฐ ์๋ ๋
์์ด์ผ. ๊น๋ํ ๋ฌธ๋ฒ, ๊ฐ๋ ฅํ ๊ธฐ๋ฅ, ๊ทธ๋ฆฌ๊ณ ์์ฒญ๋ ์ปค๋ฎค๋ํฐ ์ง์์ด ์์ง. ๋ฐ๋ฉด ๋ธ๋ก์ฒด์ธ์ ํ์ค์ํ, ํฌ๋ช
์ฑ, ๋ณด์์ฑ์ด๋ผ๋ ๊ฐ๋ ฅํ ๋ฌด๊ธฐ๋ฅผ ๊ฐ์ง๊ณ ์์ด.
์ด ๋์ ํฉ์น๋ฉด? ์๋ฒฝํ ์๋์ง๊ฐ ๋์ค๋ ๊ฑฐ์ผ! ๋ผ๋ผ๋ฒจ์ ํธ๋ฆฌํ ๊ฐ๋ฐ ํ๊ฒฝ์์ ๋ธ๋ก์ฒด์ธ์ ๊ฐ๋ ฅํ ๋ณด์์ฑ์ ํ์ฉํ ์ ์๊ฑฐ๋ . ํนํ ๊ฒฐ์ ์์คํ
์์๋ ์ด๋ฐ ์กฐํฉ์ด ์ ๋ง ๋น์ ๋ฐํด. ๐
๐ฅ ๋ผ๋ผ๋ฒจ + ๋ธ๋ก์ฒด์ธ์ ์ฃผ์ ์ฅ์
1. ํฌ๋ช
ํ ๊ฑฐ๋ ๊ธฐ๋ก
๋ชจ๋ ๊ฒฐ์ ๋ด์ญ์ด ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋ก๋์ด ์๋ณ์กฐ๊ฐ ๋ถ๊ฐ๋ฅํด. ๋๊ตฌ๋ ๊ฒ์ฆํ ์ ์๋ ํฌ๋ช
ํ ์์คํ
์ด์ง.
2. ๋น ๋ฅธ ๊ฐ๋ฐ ์๋
๋ผ๋ผ๋ฒจ์ ๊ฐ๋ ฅํ ORM(Eloquent)๊ณผ ๋ฏธ๋ค์จ์ด ์์คํ
์ ํ์ฉํ๋ฉด ๊ฐ๋ฐ ์๊ฐ์ ๋ํญ ๋จ์ถํ ์ ์์ด.
3. ํ์ฅ์ฑ
๋ผ๋ผ๋ฒจ์ ๋ชจ๋์ ๊ตฌ์กฐ ๋๋ถ์ ๋์ค์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๊ฑฐ๋ ์์ ํ๊ธฐ๊ฐ ์ ๋ง ์ฌ์.
4. ๋ณด์์ฑ
๋ธ๋ก์ฒด์ธ์ ์ํธํ ๊ธฐ์ ๊ณผ ๋ผ๋ผ๋ฒจ์ ๋ด์ฅ ๋ณด์ ๊ธฐ๋ฅ์ด ๊ฒฐํฉ๋๋ฉด ํดํน ๊ฑฑ์ ์ ํฌ๊ฒ ์ค์ผ ์ ์์ง.
5. ๊ธ๋ก๋ฒ ๊ฒฐ์ ์ง์
์ํธํํ๋ฅผ ์ฌ์ฉํ๋ฉด ๊ตญ๊ฒฝ ์๋ ๊ฒฐ์ ๊ฐ ๊ฐ๋ฅํด. ํ์ ์์๋ฃ๋ ์ ์ฝํ ์ ์๊ณ !
๐ ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ ์ค๋นํ๊ธฐ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์์ํด๋ณผ๊น? ๋จผ์ ๊ฐ๋ฐ ํ๊ฒฝ์ ์ธํ ํด์ผ ํด. ๊ฑฑ์ ๋ง, ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ๋ฐ๋ผ์ค๋ฉด ๋ผ! ๐
๐ฆ Step 1: ํ์ ๋๊ตฌ ์ค์น
๋ผ๋ผ๋ฒจ์ ์ฌ์ฉํ๋ ค๋ฉด ๋ช ๊ฐ์ง ๊ธฐ๋ณธ ๋๊ตฌ๋ค์ด ํ์ํด. ์ด๋ฏธ ์ค์น๋์ด ์๋ค๋ฉด ๋ฒ์ ๋ง ํ์ธํ๋ฉด ๋ผ.
ํ์ํ ์ํํธ์จ์ด ๋ชฉ๋ก:
PHP 8.1 ์ด์ Composer MySQL ๋๋ PostgreSQL Node.js & NPM Git
๐ Step 2: ๋ผ๋ผ๋ฒจ ํ๋ก์ ํธ ์์ฑ
ํฐ๋ฏธ๋์ ์ด๊ณ ๋ค์ ๋ช ๋ น์ด๋ฅผ ์ ๋ ฅํด๋ณด์:
composer create-project laravel/laravel blockchain-payment-admin
cd blockchain-payment-admin
php artisan serve
์ด์ ๋ธ๋ผ์ฐ์ ์์ http://localhost:8000์ ์ ์ํ๋ฉด ๋ผ๋ผ๋ฒจ ํ์ ํ์ด์ง๊ฐ ๋ณด์ผ ๊ฑฐ์ผ. ์ถํํด! ์ฒซ ๋ฒ์งธ ๋จ๊ณ ์๋ฃ! ๐
๐ Step 3: ๋ธ๋ก์ฒด์ธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น
๋ธ๋ก์ฒด์ธ๊ณผ ํต์ ํ๋ ค๋ฉด ์ ์ฉ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ํ์ํด. ์ด๋๋ฆฌ์ ๊ธฐ๋ฐ์ผ๋ก ์์ ํ๋ค๋ฉด Web3.php๊ฐ ์ข์ ์ ํ์ด์ผ.
composer require web3p/web3.php
composer require kornrunner/keccak
composer require simplito/elliptic-php
๋ง์ฝ ๋ค๋ฅธ ๋ธ๋ก์ฒด์ธ ๋คํธ์ํฌ๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ํด๋น ๋คํธ์ํฌ์ SDK๋ฅผ ์ค์นํ๋ฉด ๋ผ. ์๋ฅผ ๋ค์ด ๋นํธ์ฝ์ธ์ด๋ผ๋ฉด BitWasp, ์๋ผ๋๋ผ๋ฉด Solana PHP SDK๋ฅผ ์ฌ์ฉํ ์ ์์ด.
๐๏ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณํ๊ธฐ
๋ธ๋ก์ฒด์ธ ๊ฒฐ์ ๊ด๋ฆฌ ์์คํ
์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ์ ๋ง ์ค์ํด. ๋ธ๋ก์ฒด์ธ์๋ ๋ชจ๋ ๊ฑฐ๋ ๋ด์ญ์ด ์ ์ฅ๋์ง๋ง, ๋น ๋ฅธ ์กฐํ์ ๊ด๋ฆฌ๋ฅผ ์ํด์๋ ๋ก์ปฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ํ์ํ๊ฑฐ๋ .
์๊ฐํด๋ด. ๋งค๋ฒ ๋ธ๋ก์ฒด์ธ์ ์กฐํํ๋ฉด ์๊ฐ๋ ์ค๋ ๊ฑธ๋ฆฌ๊ณ ๋น์ฉ๋ ๋ฐ์ํ ์ ์์ด. ๊ทธ๋์ ์ฐ๋ฆฌ๋ ํ์ด๋ธ๋ฆฌ๋ ๋ฐฉ์์ ์ฌ์ฉํ ๊ฑฐ์ผ!
๐ ํต์ฌ ํ ์ด๋ธ ๊ตฌ์กฐ
์ฐ๋ฆฌ๊ฐ ๋ง๋ค ์์คํ ์๋ ์ต์ํ ์ด๋ฐ ํ ์ด๋ธ๋ค์ด ํ์ํด:
1๏ธโฃ Users ํ ์ด๋ธ
๊ด๋ฆฌ์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ ๊ธฐ๋ณธ ํ ์ด๋ธ์ด์ผ.
php artisan make:migration create_users_table
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->enum('role', ['admin', 'user'])->default('user');
$table->string('wallet_address')->nullable();
$table->boolean('is_verified')->default(false);
$table->timestamps();
});
}
2๏ธโฃ Wallets ํ ์ด๋ธ
์ฌ์ฉ์์ ์ํธํํ ์ง๊ฐ ์ ๋ณด๋ฅผ ๊ด๋ฆฌํ๋ ํ ์ด๋ธ์ด์ผ.
public function up()
{
Schema::create('wallets', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('blockchain_type'); // ethereum, bitcoin, etc.
$table->string('address')->unique();
$table->string('public_key')->nullable();
$table->decimal('balance', 20, 8)->default(0);
$table->timestamp('last_synced_at')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
3๏ธโฃ Transactions ํ ์ด๋ธ
๋ชจ๋ ๊ฒฐ์ ๊ฑฐ๋๋ฅผ ์ถ์ ํ๋ ํต์ฌ ํ ์ด๋ธ์ด์ผ. ์ด๊ฒ ์ ์ผ ์ค์ํด!
public function up()
{
Schema::create('transactions', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->foreignId('wallet_id')->constrained();
$table->string('transaction_hash')->unique();
$table->string('from_address');
$table->string('to_address');
$table->decimal('amount', 20, 8);
$table->string('currency'); // ETH, BTC, USDT, etc.
$table->decimal('fee', 20, 8)->default(0);
$table->enum('status', ['pending', 'confirmed', 'failed', 'cancelled']);
$table->enum('type', ['deposit', 'withdrawal', 'payment', 'refund']);
$table->integer('block_number')->nullable();
$table->integer('confirmations')->default(0);
$table->text('metadata')->nullable(); // JSON ๋ฐ์ดํฐ
$table->timestamp('confirmed_at')->nullable();
$table->timestamps();
$table->index(['user_id', 'status']);
$table->index('transaction_hash');
});
}
4๏ธโฃ Payment_Settings ํ ์ด๋ธ
๊ฒฐ์ ์์คํ ์ ๊ฐ์ข ์ค์ ์ ์ ์ฅํ๋ ํ ์ด๋ธ์ด์ผ.
public function up()
{
Schema::create('payment_settings', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->text('value');
$table->string('type'); // string, integer, boolean, json
$table->text('description')->nullable();
$table->boolean('is_public')->default(false);
$table->timestamps();
});
}
์ด์ ๋ง์ด๊ทธ๋ ์ด์ ์ ์คํํด๋ณด์:
php artisan migrate
๐ ๋ธ๋ก์ฒด์ธ ์ฐ๋ ์๋น์ค ๊ตฌ์ถํ๊ธฐ
์, ์ด์ ๊ฐ์ฅ ํต์ฌ์ ์ธ ๋ถ๋ถ์ด์ผ. ๋ธ๋ก์ฒด์ธ๊ณผ ์ค์ ๋ก ํต์ ํ๋ ์๋น์ค๋ฅผ ๋ง๋ค์ด๋ณด์. ์ด๋๋ฆฌ์์ ์์๋ก ์ค๋ช ํ ๊ฒ. ๋ค๋ฅธ ๋ธ๋ก์ฒด์ธ๋ ์๋ฆฌ๋ ๋น์ทํ๋๊น ์์ฉํ๋ฉด ๋ผ! ๐จ
๐ Web3 ์๋น์ค ํด๋์ค ๋ง๋ค๊ธฐ
๋จผ์ ๋ธ๋ก์ฒด์ธ๊ณผ ํต์ ํ ์๋น์ค ํด๋์ค๋ฅผ ๋ง๋ค์ด์ผ ํด. ์ด ํด๋์ค๊ฐ ๋ชจ๋ ๋ธ๋ก์ฒด์ธ ์์ ์ ์ค์ฌ์ด ๋ ๊ฑฐ์ผ.
php artisan make:service BlockchainService
namespace App\Services;
use Web3\Web3;
use Web3\Contract;
use Web3\Utils;
use Exception;
class BlockchainService
{
protected $web3;
protected $contractAddress;
protected $contractAbi;
public function __construct()
{
// Infura ๋๋ ์์ฒด ๋
ธ๋ URL ์ฌ์ฉ
$nodeUrl = config('blockchain.node_url');
$this->web3 = new Web3($nodeUrl);
$this->contractAddress = config('blockchain.contract_address');
$this->contractAbi = json_decode(
file_get_contents(storage_path('contracts/PaymentContract.json')),
true
);
}
/**
* ์ง๊ฐ ์์ก ์กฐํ
*/
public function getBalance(string $address)
{
$balance = null;
$this->web3->eth->getBalance($address, function ($err, $result) use (&$balance) {
if ($err !== null) {
throw new Exception('์์ก ์กฐํ ์คํจ: ' . $err->getMessage());
}
$balance = $result;
});
// Wei๋ฅผ Ether๋ก ๋ณํ
return Utils::fromWei($balance, 'ether');
}
/**
* ๊ฑฐ๋ ๋ด์ญ ์กฐํ
*/
public function getTransaction(string $txHash)
{
$transaction = null;
$this->web3->eth->getTransactionByHash($txHash, function ($err, $tx) use (&$transaction) {
if ($err !== null) {
throw new Exception('๊ฑฐ๋ ์กฐํ ์คํจ: ' . $err->getMessage());
}
$transaction = $tx;
});
return $transaction;
}
/**
* ๊ฑฐ๋ ํ์ธ ์ ์กฐํ
*/
public function getConfirmations(string $txHash)
{
$transaction = $this->getTransaction($txHash);
if (!$transaction || !isset($transaction->blockNumber)) {
return 0;
}
$currentBlock = null;
$this->web3->eth->blockNumber(function ($err, $block) use (&$currentBlock) {
if ($err === null) {
$currentBlock = $block;
}
});
$txBlock = hexdec($transaction->blockNumber);
$current = hexdec($currentBlock);
return max(0, $current - $txBlock + 1);
}
/**
* ๊ฒฐ์ ์ ์ก
*/
public function sendPayment(string $fromAddress, string $toAddress, float $amount, string $privateKey)
{
try {
$amountInWei = Utils::toWei((string)$amount, 'ether');
// ํธ๋์ญ์
๋ฐ์ดํฐ ์ค๋น
$transactionData = [
'from' => $fromAddress,
'to' => $toAddress,
'value' => '0x' . dechex($amountInWei),
'gas' => '0x5208', // 21000 gas
'gasPrice' => $this->getGasPrice(),
];
// ํธ๋์ญ์
์๋ช
๋ฐ ์ ์ก
$signedTx = $this->signTransaction($transactionData, $privateKey);
$txHash = null;
$this->web3->eth->sendRawTransaction($signedTx, function ($err, $hash) use (&$txHash) {
if ($err !== null) {
throw new Exception('์ ์ก ์คํจ: ' . $err->getMessage());
}
$txHash = $hash;
});
return $txHash;
} catch (Exception $e) {
throw new Exception('๊ฒฐ์ ์ ์ก ์ค ์ค๋ฅ: ' . $e->getMessage());
}
}
/**
* ํ์ฌ ๊ฐ์ค ๊ฐ๊ฒฉ ์กฐํ
*/
protected function getGasPrice()
{
$gasPrice = null;
$this->web3->eth->gasPrice(function ($err, $price) use (&$gasPrice) {
if ($err === null) {
$gasPrice = $price;
}
});
return $gasPrice;
}
/**
* ํธ๋์ญ์
์๋ช
*/
protected function signTransaction(array $transaction, string $privateKey)
{
// ์ค์ ๊ตฌํ์์๋ ์ ์ ํ ์๋ช
๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ
// ๋ณด์์ ์ํด private key๋ ์ ๋ ํด๋ผ์ด์ธํธ์ ๋
ธ์ถ๋๋ฉด ์ ๋จ!
// ์ฌ๊ธฐ์๋ ๊ฐ๋
์ ์ธ ์ฝ๋๋ง ํ์
return '0x...'; // ์๋ช
๋ ํธ๋์ญ์
}
/**
* ์ค๋งํธ ์ปจํธ๋ํธ ํจ์ ํธ์ถ
*/
public function callContractFunction(string $functionName, array $params = [])
{
$contract = new Contract($this->web3->provider, $this->contractAbi);
$contract->at($this->contractAddress);
$result = null;
$contract->call($functionName, $params, function ($err, $data) use (&$result) {
if ($err !== null) {
throw new Exception('์ปจํธ๋ํธ ํธ์ถ ์คํจ: ' . $err->getMessage());
}
$result = $data;
});
return $result;
}
}
โ๏ธ ํ๊ฒฝ ์ค์ ํ์ผ ๊ตฌ์ฑ
๋ธ๋ก์ฒด์ธ ๊ด๋ จ ์ค์ ์ ๋ณ๋ ํ์ผ๋ก ๊ด๋ฆฌํ๋ฉด ํธํด. config ํด๋์ ์ ํ์ผ์ ๋ง๋ค์ด๋ณด์.
return [
// ๋
ธ๋ URL (Infura, Alchemy, ๋๋ ์์ฒด ๋
ธ๋)
'node_url' => env('BLOCKCHAIN_NODE_URL', 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID'),
// ๋คํธ์ํฌ ์ค์
'network' => env('BLOCKCHAIN_NETWORK', 'mainnet'), // mainnet, testnet, ropsten, etc.
// ์ค๋งํธ ์ปจํธ๋ํธ ์ฃผ์
'contract_address' => env('BLOCKCHAIN_CONTRACT_ADDRESS'),
// ๊ฐ์ค ์ค์
'gas_limit' => env('BLOCKCHAIN_GAS_LIMIT', 21000),
'gas_price_multiplier' => env('BLOCKCHAIN_GAS_PRICE_MULTIPLIER', 1.2),
// ํ์ธ ์ ์ค์
'required_confirmations' => env('BLOCKCHAIN_REQUIRED_CONFIRMATIONS', 12),
// ์ง์ํ๋ ์ํธํํ
'supported_currencies' => [
'ETH' => [
'name' => 'Ethereum',
'decimals' => 18,
'min_amount' => 0.001,
],
'USDT' => [
'name' => 'Tether USD',
'decimals' => 6,
'contract_address' => env('USDT_CONTRACT_ADDRESS'),
'min_amount' => 1,
],
'USDC' => [
'name' => 'USD Coin',
'decimals' => 6,
'contract_address' => env('USDC_CONTRACT_ADDRESS'),
'min_amount' => 1,
],
],
// ์นํ
์ค์
'webhook_url' => env('BLOCKCHAIN_WEBHOOK_URL'),
'webhook_secret' => env('BLOCKCHAIN_WEBHOOK_SECRET'),
];
๊ทธ๋ฆฌ๊ณ .env ํ์ผ์ ์ค์ ๊ฐ๋ค์ ์ ๋ ฅํด:
BLOCKCHAIN_NODE_URL=https://mainnet.infura.io/v3/your_project_id
BLOCKCHAIN_NETWORK=mainnet
BLOCKCHAIN_CONTRACT_ADDRESS=0x...
BLOCKCHAIN_REQUIRED_CONFIRMATIONS=12
USDT_CONTRACT_ADDRESS=0xdac17f958d2ee523a2206206994597c13d831ec7
USDC_CONTRACT_ADDRESS=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
๐จ ๊ด๋ฆฌ์ ํ์ด์ง ์ปจํธ๋กค๋ฌ ๋ง๋ค๊ธฐ
์ด์ ์ค์ ๊ด๋ฆฌ์๊ฐ ์ฌ์ฉํ ํ์ด์ง์ ๋ก์ง์ ๋ง๋ค์ด๋ณด์. ์ปจํธ๋กค๋ฌ๋ ์ฌ์ฉ์์ ์์ฒญ์ ๋ฐ์์ ์ฒ๋ฆฌํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ๋ ์ญํ ์ ํด. MVC ํจํด์ ํต์ฌ์ด์ง! ๐ฏ
php artisan make:controller Admin/PaymentController
php artisan make:controller Admin/DashboardController
php artisan make:controller Admin/TransactionController
๐ ๋์๋ณด๋ ์ปจํธ๋กค๋ฌ
๊ด๋ฆฌ์๊ฐ ์ฒซ ํ๋ฉด์์ ๋ณผ ์ ์๋ ํต๊ณ์ ์์ฝ ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ ์ปจํธ๋กค๋ฌ์ผ.
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Transaction;
use App\Models\User;
use App\Models\Wallet;
use App\Services\BlockchainService;
use Illuminate\Http\Request;
use Carbon\Carbon;
class DashboardController extends Controller
{
protected $blockchainService;
public function __construct(BlockchainService $blockchainService)
{
$this->blockchainService = $blockchainService;
}
public function index()
{
// ์ค๋์ ํต๊ณ
$todayStats = $this->getTodayStats();
// ์ต๊ทผ ๊ฑฐ๋ ๋ด์ญ
$recentTransactions = Transaction::with(['user', 'wallet'])
->orderBy('created_at', 'desc')
->limit(10)
->get();
// ์๋ณ ๊ฑฐ๋๋ ์ฐจํธ ๋ฐ์ดํฐ
$monthlyData = $this->getMonthlyChartData();
// ๋๊ธฐ์ค์ธ ๊ฑฐ๋
$pendingTransactions = Transaction::where('status', 'pending')
->count();
// ํ์ฑ ์ฌ์ฉ์ ์
$activeUsers = User::where('is_verified', true)
->whereHas('transactions', function($query) {
$query->where('created_at', '>=', Carbon::now()->subDays(30));
})
->count();
return view('admin.dashboard', compact(
'todayStats',
'recentTransactions',
'monthlyData',
'pendingTransactions',
'activeUsers'
));
}
protected function getTodayStats()
{
$today = Carbon::today();
return [
'total_transactions' => Transaction::whereDate('created_at', $today)->count(),
'total_volume' => Transaction::whereDate('created_at', $today)
->where('status', 'confirmed')
->sum('amount'),
'successful_rate' => $this->calculateSuccessRate($today),
'average_confirmation_time' => $this->getAverageConfirmationTime($today),
];
}
protected function calculateSuccessRate($date)
{
$total = Transaction::whereDate('created_at', $date)->count();
if ($total === 0) {
return 0;
}
$successful = Transaction::whereDate('created_at', $date)
->where('status', 'confirmed')
->count();
return round(($successful / $total) * 100, 2);
}
protected function getAverageConfirmationTime($date)
{
$transactions = Transaction::whereDate('created_at', $date)
->where('status', 'confirmed')
->whereNotNull('confirmed_at')
->get();
if ($transactions->isEmpty()) {
return 0;
}
$totalSeconds = 0;
foreach ($transactions as $tx) {
$totalSeconds += $tx->created_at->diffInSeconds($tx->confirmed_at);
}
return round($totalSeconds / $transactions->count() / 60, 2); // ๋ถ ๋จ์
}
protected function getMonthlyChartData()
{
$months = [];
$volumes = [];
for ($i = 11; $i >= 0; $i--) {
$date = Carbon::now()->subMonths($i);
$months[] = $date->format('Y-m');
$volume = Transaction::whereYear('created_at', $date->year)
->whereMonth('created_at', $date->month)
->where('status', 'confirmed')
->sum('amount');
$volumes[] = $volume;
}
return [
'labels' => $months,
'data' => $volumes,
];
}
/**
* ์ค์๊ฐ ๋ธ๋ก์ฒด์ธ ์ํ ์กฐํ
*/
public function getBlockchainStatus()
{
try {
// ์ต์ ๋ธ๋ก ๋ฒํธ
$latestBlock = $this->blockchainService->getLatestBlockNumber();
// ํ์ฌ ๊ฐ์ค ๊ฐ๊ฒฉ
$gasPrice = $this->blockchainService->getCurrentGasPrice();
// ๋คํธ์ํฌ ์ํ
$networkStatus = $this->blockchainService->getNetworkStatus();
return response()->json([
'success' => true,
'data' => [
'latest_block' => $latestBlock,
'gas_price' => $gasPrice,
'network_status' => $networkStatus,
'timestamp' => now(),
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => '๋ธ๋ก์ฒด์ธ ์ํ ์กฐํ ์คํจ: ' . $e->getMessage()
], 500);
}
}
}
๐ณ ๊ฑฐ๋ ๊ด๋ฆฌ ์ปจํธ๋กค๋ฌ
๋ชจ๋ ๊ฑฐ๋๋ฅผ ์กฐํํ๊ณ ๊ด๋ฆฌํ๋ ์ปจํธ๋กค๋ฌ์ผ. ํํฐ๋ง, ๊ฒ์, ์์ธ ์กฐํ ๋ฑ์ ๊ธฐ๋ฅ์ด ๋ค์ด๊ฐ.
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Transaction;
use App\Services\BlockchainService;
use Illuminate\Http\Request;
class TransactionController extends Controller
{
protected $blockchainService;
public function __construct(BlockchainService $blockchainService)
{
$this->blockchainService = $blockchainService;
}
/**
* ๊ฑฐ๋ ๋ชฉ๋ก ์กฐํ
*/
public function index(Request $request)
{
$query = Transaction::with(['user', 'wallet']);
// ์ํ ํํฐ
if ($request->has('status') && $request->status !== 'all') {
$query->where('status', $request->status);
}
// ํ์
ํํฐ
if ($request->has('type') && $request->type !== 'all') {
$query->where('type', $request->type);
}
// ๋ ์ง ๋ฒ์ ํํฐ
if ($request->has('date_from')) {
$query->whereDate('created_at', '>=', $request->date_from);
}
if ($request->has('date_to')) {
$query->whereDate('created_at', '<=', $request->date_to);
}
// ๊ฒ์ (ํธ๋์ญ์
ํด์, ์ฃผ์, ์ฌ์ฉ์๋ช
)
if ($request->has('search') && $request->search) {
$search = $request->search;
$query->where(function($q) use ($search) {
$q->where('transaction_hash', 'like', "%{$search}%")
->orWhere('from_address', 'like', "%{$search}%")
->orWhere('to_address', 'like', "%{$search}%")
->orWhereHas('user', function($userQuery) use ($search) {
$userQuery->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
});
});
}
// ์ ๋ ฌ
$sortBy = $request->get('sort_by', 'created_at');
$sortOrder = $request->get('sort_order', 'desc');
$query->orderBy($sortBy, $sortOrder);
// ํ์ด์ง๋ค์ด์
$transactions = $query->paginate(20);
return view('admin.transactions.index', compact('transactions'));
}
/**
* ๊ฑฐ๋ ์์ธ ์กฐํ
*/
public function show($id)
{
$transaction = Transaction::with(['user', 'wallet'])->findOrFail($id);
// ๋ธ๋ก์ฒด์ธ์์ ์ต์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
try {
$blockchainData = $this->blockchainService->getTransaction($transaction->transaction_hash);
$confirmations = $this->blockchainService->getConfirmations($transaction->transaction_hash);
// ๋ฐ์ดํฐ๋ฒ ์ด์ค ์
๋ฐ์ดํธ
if ($confirmations !== $transaction->confirmations) {
$transaction->update(['confirmations' => $confirmations]);
}
} catch (\Exception $e) {
$blockchainData = null;
$confirmations = $transaction->confirmations;
}
return view('admin.transactions.show', compact('transaction', 'blockchainData', 'confirmations'));
}
/**
* ๊ฑฐ๋ ๋๊ธฐํ (๋ธ๋ก์ฒด์ธ์์ ์ต์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ)
*/
public function sync($id)
{
try {
$transaction = Transaction::findOrFail($id);
// ๋ธ๋ก์ฒด์ธ์์ ์ ๋ณด ์กฐํ
$blockchainTx = $this->blockchainService->getTransaction($transaction->transaction_hash);
if (!$blockchainTx) {
return response()->json([
'success' => false,
'message' => '๋ธ๋ก์ฒด์ธ์์ ๊ฑฐ๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.'
], 404);
}
// ํ์ธ ์ ์
๋ฐ์ดํธ
$confirmations = $this->blockchainService->getConfirmations($transaction->transaction_hash);
// ์ํ ์
๋ฐ์ดํธ
$status = $transaction->status;
$requiredConfirmations = config('blockchain.required_confirmations');
if ($confirmations >= $requiredConfirmations && $status === 'pending') {
$status = 'confirmed';
$transaction->confirmed_at = now();
}
$transaction->update([
'confirmations' => $confirmations,
'status' => $status,
'block_number' => hexdec($blockchainTx->blockNumber ?? 0),
]);
return response()->json([
'success' => true,
'message' => '๊ฑฐ๋ ์ ๋ณด๊ฐ ๋๊ธฐํ๋์์ต๋๋ค.',
'data' => $transaction
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => '๋๊ธฐํ ์คํจ: ' . $e->getMessage()
], 500);
}
}
/**
* ๊ฑฐ๋ ์ทจ์ (pending ์ํ๋ง ๊ฐ๋ฅ)
*/
public function cancel($id)
{
try {
$transaction = Transaction::findOrFail($id);
if ($transaction->status !== 'pending') {
return response()->json([
'success' => false,
'message' => 'pending ์ํ์ ๊ฑฐ๋๋ง ์ทจ์ํ ์ ์์ต๋๋ค.'
], 400);
}
$transaction->update(['status' => 'cancelled']);
return response()->json([
'success' => true,
'message' => '๊ฑฐ๋๊ฐ ์ทจ์๋์์ต๋๋ค.'
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => '์ทจ์ ์คํจ: ' . $e->getMessage()
], 500);
}
}
/**
* ๊ฑฐ๋ ํต๊ณ ๋ด๋ณด๋ด๊ธฐ (CSV)
*/
public function export(Request $request)
{
$query = Transaction::with(['user', 'wallet']);
// ํํฐ ์ ์ฉ (index ๋ฉ์๋์ ๋์ผ)
// ... (ํํฐ ๋ก์ง ์๋ต)
$transactions = $query->get();
$filename = 'transactions_' . now()->format('Y-m-d_His') . '.csv';
$headers = [
'Content-Type' => 'text/csv',
'Content-Disposition' => "attachment; filename=\"{$filename}\"",
];
$callback = function() use ($transactions) {
$file = fopen('php://output', 'w');
// CSV ํค๋
fputcsv($file, [
'ID', '๊ฑฐ๋ ํด์', '์ฌ์ฉ์', '๋ณด๋ธ ์ฃผ์', '๋ฐ๋ ์ฃผ์',
'๊ธ์ก', 'ํตํ', '์์๋ฃ', '์ํ', 'ํ์
',
'ํ์ธ ์', '์์ฑ์ผ์', 'ํ์ธ์ผ์'
]);
// ๋ฐ์ดํฐ ํ
foreach ($transactions as $tx) {
fputcsv($file, [
$tx->id,
$tx->transaction_hash,
$tx->user->name ?? 'N/A',
$tx->from_address,
$tx->to_address,
$tx->amount,
$tx->currency,
$tx->fee,
$tx->status,
$tx->type,
$tx->confirmations,
$tx->created_at,
$tx->confirmed_at ?? 'N/A',
]);
}
fclose($file);
};
return response()->stream($callback, 200, $headers);
}
}
๐ญ ํ๋ก ํธ์๋ ๋ทฐ ๋ง๋ค๊ธฐ
๋ฐฑ์๋ ๋ก์ง์ ์์ฑํ์ผ๋ ์ด์ ์ฌ์ฉ์๊ฐ ์ค์ ๋ก ๋ณผ ํ๋ฉด์ ๋ง๋ค์ด์ผ ํด. ๋ผ๋ผ๋ฒจ์ Blade ํ ํ๋ฆฟ ์์ง์ ์ฌ์ฉํ๋ฉด ์ ๋ง ํธํ๊ฒ ๋ง๋ค ์ ์์ด! ๐
๐ ๋ ์ด์์ ํ ํ๋ฆฟ
๋จผ์ ๋ชจ๋ ํ์ด์ง์์ ๊ณตํต์ผ๋ก ์ฌ์ฉํ ๋ ์ด์์์ ๋ง๋ค์.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('title', '๋ธ๋ก์ฒด์ธ ๊ฒฐ์ ๊ด๋ฆฌ์') - Admin</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Alpine.js -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
@stack('styles')
</head>
<body class="bg-gray-50">
<div class="flex h-screen">
<!-- ์ฌ์ด๋๋ฐ -->
<aside class="w-64 bg-gradient-to-b from-indigo-600 to-purple-700 text-white">
<div class="p-6">
<h1 class="text-2xl font-bold">๐ Blockchain Pay</h1>
<p class="text-sm text-indigo-200 mt-1">๊ด๋ฆฌ์ ๋์๋ณด๋</p>
</div>
<nav class="mt-6">
<a href="{{ route('admin.dashboard') }}"
class="flex items-center px-6 py-3 hover:bg-white/10 transition">
<span class="mr-3">๐</span>
๋์๋ณด๋
</a>
<a href="{{ route('admin.transactions.index') }}"
class="flex items-center px-6 py-3 hover:bg-white/10 transition">
<span class="mr-3">๐ณ</span>
๊ฑฐ๋ ๊ด๋ฆฌ
</a>
<a href="{{ route('admin.users.index') }}"
class="flex items-center px-6 py-3 hover:bg-white/10 transition">
<span class="mr-3">๐ฅ</span>
์ฌ์ฉ์ ๊ด๋ฆฌ
</a>
<a href="{{ route('admin.wallets.index') }}"
class="flex items-center px-6 py-3 hover:bg-white/10 transition">
<span class="mr-3">๐</span>
์ง๊ฐ ๊ด๋ฆฌ
</a>
<a href="{{ route('admin.settings') }}"
class="flex items-center px-6 py-3 hover:bg-white/10 transition">
<span class="mr-3">โ๏ธ</span>
์ค์
</a>
</nav>
</aside>
<!-- ๋ฉ์ธ ์ปจํ
์ธ -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- ํค๋ -->
<header class="bg-white shadow-sm">
<div class="flex items-center justify-between px-6 py-4">
<h2 class="text-xl font-semibold text-gray-800">
@yield('page-title', '๋์๋ณด๋')
</h2>
<div class="flex items-center space-x-4">
<!-- ๋ธ๋ก์ฒด์ธ ์ํ -->
<div class="flex items-center space-x-2" x-data="blockchainStatus()">
<div class="w-2 h-2 rounded-full"
:class="status === 'online' ? 'bg-green-500' : 'bg-red-500'">
</div>
<span class="text-sm text-gray-600" x-text="statusText"></span>
</div>
<!-- ์ฌ์ฉ์ ๋ฉ๋ด -->
<div class="relative" x-data="{ open: false }">
<button @click="open = !open"
class="flex items-center space-x-2 text-gray-700 hover:text-gray-900">
<span>{{ auth()->user()->name }}</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div x-show="open"
@click.away="open = false"
class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg py-2">
<a href="{{ route('admin.profile') }}"
class="block px-4 py-2 text-gray-700 hover:bg-gray-100">
ํ๋กํ
</a>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit"
class="block w-full text-left px-4 py-2 text-gray-700 hover:bg-gray-100">
๋ก๊ทธ์์
</button>
</form>
</div>
</div>
</div>
</div>
</header>
<!-- ๋ฉ์ธ ์ปจํ
์ธ ์์ญ -->
<main class="flex-1 overflow-y-auto p-6">
@if(session('success'))
<div class="mb-4 p-4 bg-green-100 border border-green-400 text-green-700 rounded-lg">
{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="mb-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg">
{{ session('error') }}
</div>
@endif
@yield('content')
</main>
</div>
</div>
<script>
// ๋ธ๋ก์ฒด์ธ ์ํ ์ฒดํฌ
function blockchainStatus() {
return {
status: 'checking',
statusText: 'ํ์ธ ์ค...',
init() {
this.checkStatus();
setInterval(() => this.checkStatus(), 30000); // 30์ด๋ง๋ค ์ฒดํฌ
},
async checkStatus() {
try {
const response = await fetch('/admin/blockchain/status');
const data = await response.json();
if (data.success) {
this.status = 'online';
this.statusText = '๋ธ๋ก์ฒด์ธ ์ฐ๊ฒฐ๋จ';
} else {
this.status = 'offline';
this.statusText = '์ฐ๊ฒฐ ์คํจ';
}
} catch (error) {
this.status = 'offline';
this.statusText = '์ฐ๊ฒฐ ์คํจ';
}
}
}
}
</script>
@stack('scripts')
</body>
</html>
๐ ๋์๋ณด๋ ๋ทฐ
๊ด๋ฆฌ์๊ฐ ์ฒ์ ๋ณด๋ ๋ฉ์ธ ๋์๋ณด๋ ํ๋ฉด์ด์ผ. ์ฃผ์ ํต๊ณ์ ์ฐจํธ๋ฅผ ๋ณด์ฌ์ค.
@extends('layouts.admin')
@section('title', '๋์๋ณด๋')
@section('page-title', '๐ ๋์๋ณด๋')
@section('content')
<div class="space-y-6">
<!-- ํต๊ณ ์นด๋ -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- ์ค๋์ ๊ฑฐ๋ -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">์ค๋์ ๊ฑฐ๋</p>
<p class="text-3xl font-bold text-gray-900 mt-2">
{{ number_format($todayStats['total_transactions']) }}
</p>
</div>
<div class="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
<span class="text-2xl">๐ณ</span>
</div>
</div>
</div>
<!-- ๊ฑฐ๋๋ -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">์ค๋์ ๊ฑฐ๋๋</p>
<p class="text-3xl font-bold text-gray-900 mt-2">
{{ number_format($todayStats['total_volume'], 4) }} ETH
</p>
</div>
<div class="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
<span class="text-2xl">๐ฐ</span>
</div>
</div>
</div>
<!-- ์ฑ๊ณต๋ฅ -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">์ฑ๊ณต๋ฅ </p>
<p class="text-3xl font-bold text-gray-900 mt-2">
{{ $todayStats['successful_rate'] }}%
</p>
</div>
<div class="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center">
<span class="text-2xl">๐</span>
</div>
</div>
</div>
<!-- ๋๊ธฐ์ค -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">๋๊ธฐ์ค์ธ ๊ฑฐ๋</p>
<p class="text-3xl font-bold text-gray-900 mt-2">
{{ number_format($pendingTransactions) }}
</p>
</div>
<div class="w-12 h-12 bg-yellow-100 rounded-full flex items-center justify-center">
<span class="text-2xl">โณ</span>
</div>
</div>
</div>
</div>
<!-- ์ฐจํธ -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- ์๋ณ ๊ฑฐ๋๋ ์ฐจํธ -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">์๋ณ ๊ฑฐ๋๋</h3>
<canvas id="monthlyChart"></canvas>
</div>
<!-- ๊ฑฐ๋ ํ์
๋ถํฌ -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">๊ฑฐ๋ ํ์
๋ถํฌ</h3>
<canvas id="typeChart"></canvas>
</div>
</div>
<!-- ์ต๊ทผ ๊ฑฐ๋ -->
<div class="bg-white rounded-lg shadow">
<div class="p-6 border-b border-gray-200">
<h3 class="text-lg font-semibold text-gray-900">์ต๊ทผ ๊ฑฐ๋ ๋ด์ญ</h3>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">๊ฑฐ๋ ํด์</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">์ฌ์ฉ์</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">๊ธ์ก</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ํ์
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">์ํ</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">์๊ฐ</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@foreach($recentTransactions as $tx)
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<a href="{{ route('admin.transactions.show', $tx->id) }}"
class="text-indigo-600 hover:text-indigo-900 font-mono text-sm">
{{ Str::limit($tx->transaction_hash, 20) }}
</a>
</td>
<td class="px-6 py-4 whitespace-nowrap">
{{ $tx->user->name ?? 'N/A' }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
{{ number_format($tx->amount, 4) }} {{ $tx->currency }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-800">
{{ $tx->type }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
@if($tx->status === 'confirmed')
<span class="px-2 py-1 text-xs rounded-full bg-green-100 text-green-800">
โ ํ์ธ๋จ
</span>
@elseif($tx->status === 'pending')
<span class="px-2 py-1 text-xs rounded-full bg-yellow-100 text-yellow-800">
โณ ๋๊ธฐ์ค
</span>
@else
<span class="px-2 py-1 text-xs rounded-full bg-red-100 text-red-800">
โ {{ $tx->status }}
</span>
@endif
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ $tx->created_at->diffForHumans() }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="p-4 border-t border-gray-200">
<a href="{{ route('admin.transactions.index') }}"
class="text-indigo-600 hover:text-indigo-900 text-sm font-medium">
๋ชจ๋ ๊ฑฐ๋ ๋ณด๊ธฐ โ
</a>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
// ์๋ณ ๊ฑฐ๋๋ ์ฐจํธ
const monthlyCtx = document.getElementById('monthlyChart').getContext('2d');
new Chart(monthlyCtx, {
type: 'line',
data: {
labels: @json($monthlyData['labels']),
datasets: [{
label: '๊ฑฐ๋๋ (ETH)',
data: @json($monthlyData['data']),
borderColor: 'rgb(99, 102, 241)',
backgroundColor: 'rgba(99, 102, 241, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
</script>
@endpush
๐ ์ค์๊ฐ ๊ฑฐ๋ ๋ชจ๋ํฐ๋ง ๊ตฌํํ๊ธฐ
๋ธ๋ก์ฒด์ธ ๊ฒฐ์ ์์คํ ์์ ๊ฐ์ฅ ์ค์ํ ๊ฑด ์ค์๊ฐ์ฑ์ด์ผ. ๊ฑฐ๋๊ฐ ๋ฐ์ํ๋ฉด ์ฆ์ ๊ฐ์งํ๊ณ ์ฒ๋ฆฌํด์ผ ํ๊ฑฐ๋ . ์ด๋ฅผ ์ํด ์น์์ผ๊ณผ ํ ์์คํ ์ ํ์ฉํ ๊ฑฐ์ผ! ๐
๐ก Laravel Echo์ Pusher ์ค์
์ค์๊ฐ ์๋ฆผ์ ์ํด Laravel Echo์ Pusher๋ฅผ ์ฌ์ฉํ ๊ฑฐ์ผ. ๋ฌผ๋ก Redis๋ Socket.io๋ฅผ ์ฌ์ฉํด๋ ๋ผ!
composer require pusher/pusher-php-server
npm install --save-dev laravel-echo pusher-js
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=your_app_id
PUSHER_APP_KEY=your_app_key
PUSHER_APP_SECRET=your_app_secret
PUSHER_APP_CLUSTER=ap3
๐ฏ ์ด๋ฒคํธ ํด๋์ค ์์ฑ
๊ฑฐ๋ ์ํ๊ฐ ๋ณ๊ฒฝ๋ ๋ ๋ฐ์์ํฌ ์ด๋ฒคํธ๋ฅผ ๋ง๋ค์.
php artisan make:event TransactionStatusChanged
namespace App\Events;
use App\Models\Transaction;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class TransactionStatusChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $transaction;
public $oldStatus;
public $newStatus;
public function __construct(Transaction $transaction, $oldStatus, $newStatus)
{
$this->transaction = $transaction;
$this->oldStatus = $oldStatus;
$this->newStatus = $newStatus;
}
/**
* ๋ธ๋ก๋์บ์คํธํ ์ฑ๋
*/
public function broadcastOn()
{
return [
new Channel('transactions'),
new Channel('user.' . $this->transaction->user_id),
];
}
/**
* ๋ธ๋ก๋์บ์คํธ ๋ฐ์ดํฐ
*/
public function broadcastWith()
{
return [
'transaction_id' => $this->transaction->id,
'transaction_hash' => $this->transaction->transaction_hash,
'old_status' => $this->oldStatus,
'new_status' => $this->newStatus,
'amount' => $this->transaction->amount,
'currency' => $this->transaction->currency,
'confirmations' => $this->transaction->confirmations,
'timestamp' => now()->toIso8601String(),
];
}
/**
* ๋ธ๋ก๋์บ์คํธ ์ด๋ฒคํธ ์ด๋ฆ
*/
public function broadcastAs()
{
return 'transaction.status.changed';
}
}
โ๏ธ ๋ธ๋ก์ฒด์ธ ๋ชจ๋ํฐ๋ง Job ๋ง๋ค๊ธฐ
์ฃผ๊ธฐ์ ์ผ๋ก ๋ธ๋ก์ฒด์ธ์ ์ฒดํฌํ๊ณ ๊ฑฐ๋ ์ํ๋ฅผ ์ ๋ฐ์ดํธํ๋ Job์ด์ผ.
php artisan make:job MonitorBlockchainTransactions
namespace App\Jobs;
use App\Models\Transaction;
use App\Services\BlockchainService;
use App\Events\TransactionStatusChanged;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class MonitorBlockchainTransactions implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $blockchainService;
public function __construct()
{
$this->blockchainService = app(BlockchainService::class);
}
public function handle()
{
// pending ์ํ์ธ ๊ฑฐ๋๋ค ์กฐํ
$pendingTransactions = Transaction::where('status', 'pending')
->where('created_at', '>=', now()->subDays(7)) // 7์ผ ์ด๋ด
->get();
foreach ($pendingTransactions as $transaction) {
try {
$this->checkTransaction($transaction);
} catch (\Exception $e) {
Log::error('๊ฑฐ๋ ๋ชจ๋ํฐ๋ง ์คํจ', [
'transaction_id' => $transaction->id,
'error' => $e->getMessage()
]);
}
}
}
protected function checkTransaction(Transaction $transaction)
{
// ๋ธ๋ก์ฒด์ธ์์ ๊ฑฐ๋ ์ ๋ณด ์กฐํ
$blockchainTx = $this->blockchainService->getTransaction($transaction->transaction_hash);
if (!$blockchainTx) {
// ๊ฑฐ๋๋ฅผ ์ฐพ์ ์ ์์ผ๋ฉด ์ผ์ ์๊ฐ ํ ์คํจ ์ฒ๋ฆฌ
if ($transaction->created_at->diffInHours(now()) > 24) {
$this->updateTransactionStatus($transaction, 'failed');
}
return;
}
// ํ์ธ ์ ์
๋ฐ์ดํธ
$confirmations = $this->blockchainService->getConfirmations($transaction->transaction_hash);
$transaction->confirmations = $confirmations;
// ๋ธ๋ก ๋ฒํธ ์
๋ฐ์ดํธ
if (isset($blockchainTx->blockNumber)) {
$transaction->block_number = hexdec($blockchainTx->blockNumber);
}
// ํ์ํ ํ์ธ ์ ์ฒดํฌ
$requiredConfirmations = config('blockchain.required_confirmations');
if ($confirmations >= $requiredConfirmations) {
$this->updateTransactionStatus($transaction, 'confirmed');
$transaction->confirmed_at = now();
}
$transaction->save();
}
protected function updateTransactionStatus(Transaction $transaction, $newStatus)
{
$oldStatus = $transaction->status;
$transaction->status = $newStatus;
$transaction->save();
// ์ด๋ฒคํธ ๋ฐ์
event(new TransactionStatusChanged($transaction, $oldStatus, $newStatus));
// ์ฌ์ฉ์์๊ฒ ์๋ฆผ ์ ์ก
$this->notifyUser($transaction, $newStatus);
}
protected function notifyUser(Transaction $transaction, $status)
{
// ์ด๋ฉ์ผ, SMS, ํธ์ ์๋ฆผ ๋ฑ ๋ฐ์ก
// ์ค์ ๊ตฌํ์ ํ๋ก์ ํธ ์๊ตฌ์ฌํญ์ ๋ฐ๋ผ ๋ค๋ฆ
Log::info('์ฌ์ฉ์ ์๋ฆผ ์ ์ก', [
'user_id' => $transaction->user_id,
'transaction_id' => $transaction->id,
'status' => $status
]);
}
}
โฐ ์ค์ผ์ค๋ฌ ์ค์
์ด Job์ ์ฃผ๊ธฐ์ ์ผ๋ก ์คํํ๋๋ก ์ค์ผ์ค๋ฌ์ ๋ฑ๋กํ์.
protected function schedule(Schedule $schedule)
{
// 1๋ถ๋ง๋ค ๋ธ๋ก์ฒด์ธ ๊ฑฐ๋ ๋ชจ๋ํฐ๋ง
$schedule->job(new MonitorBlockchainTransactions)
->everyMinute()
->withoutOverlapping();
// ๋งค์ผ ์์ ์ ์ค๋๋ ๊ฑฐ๋ ์ ๋ฆฌ
$schedule->call(function () {
Transaction::where('status', 'pending')
->where('created_at', '<', now()->subDays(30))
->update(['status' => 'expired']);
})->daily();
// ๋งค์๊ฐ ์ง๊ฐ ์์ก ๋๊ธฐํ
$schedule->job(new SyncWalletBalances)
->hourly();
}
์ค์ผ์ค๋ฌ๋ฅผ ์คํํ๋ ค๋ฉด ์๋ฒ์ cron์ ๋ค์์ ์ถ๊ฐํด์ผ ํด:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
๐ก๏ธ ๋ณด์ ๊ฐํํ๊ธฐ
๋ธ๋ก์ฒด์ธ ๊ฒฐ์ ์์คํ
์์ ๋ณด์์ ์ ๋ง์ ๋ง ์ค์ํด! ํ ๋ฒ ์ค์ํ๋ฉด ๋์ด ๋ ์๊ฐ๋๊น ๋ง์ด์ผ. ๐ฑ
์ฌ๋ฌ ๋ณด์ ๋ ์ด์ด๋ฅผ ์ถ๊ฐํด์ ์์คํ
์ ํผํผํ๊ฒ ๋ง๋ค์ด๋ณด์!
๐ 1. 2๋จ๊ณ ์ธ์ฆ (2FA) ๊ตฌํ
๊ด๋ฆฌ์ ๊ณ์ ์ ๋ฐ๋์ 2FA๋ฅผ ์ฌ์ฉํด์ผ ํด. Google Authenticator๋ฅผ ํ์ฉํด๋ณด์.
composer require pragmarx/google2fa-laravel
php artisan make:migration add_two_factor_to_users_table
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('two_factor_secret')->nullable();
$table->text('two_factor_recovery_codes')->nullable();
$table->boolean('two_factor_enabled')->default(false);
});
}
๐ 2. Private Key ์์ ํ๊ฒ ๊ด๋ฆฌํ๊ธฐ
Private Key๋ ์ ๋ ํ๋ฌธ์ผ๋ก ์ ์ฅํ๋ฉด ์ ๋ผ! ์ํธํํด์ ์ ์ฅํ๊ณ , ์ฌ์ฉํ ๋๋ง ๋ณตํธํํด์ผ ํด.
namespace App\Services;
use Illuminate\Support\Facades\Crypt;
use Exception;
class KeyManagementService
{
/**
* Private Key ์ํธํ ์ ์ฅ
*/
public function storePrivateKey(string $privateKey, int $userId)
{
try {
$encrypted = Crypt::encryptString($privateKey);
// ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ
// ์ค์ ๋ก๋ ๋ณ๋์ ๋ณด์ ์ ์ฅ์(HSM, KMS ๋ฑ) ์ฌ์ฉ ๊ถ์ฅ
return $encrypted;
} catch (Exception $e) {
throw new Exception('Private Key ์ ์ฅ ์คํจ: ' . $e->getMessage());
}
}
/**
* Private Key ๋ณตํธํ
*/
public function retrievePrivateKey(string $encryptedKey)
{
try {
return Crypt::decryptString($encryptedKey);
} catch (Exception $e) {
throw new Exception('Private Key ๋ณตํธํ ์คํจ: ' . $e->getMessage());
}
}
/**
* Private Key ๊ฒ์ฆ
*/
public function validatePrivateKey(string $privateKey)
{
// Private Key ํ์ ๊ฒ์ฆ
if (strlen($privateKey) !== 64) {
return false;
}
if (!ctype_xdigit($privateKey)) {
return false;
}
return true;
}
}
๐ 3. ๊ฐ์ฌ ๋ก๊ทธ ์์คํ
๋ชจ๋ ์ค์ํ ์์ ์ ๊ธฐ๋กํด๋๋ฉด ๋์ค์ ๋ฌธ์ ๊ฐ ์๊ฒผ์ ๋ ์ถ์ ํ ์ ์์ด.
php artisan make:migration create_audit_logs_table
public function up()
{
Schema::create('audit_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained();
$table->string('action'); // login, transaction_create, settings_change, etc.
$table->string('model_type')->nullable();
$table->unsignedBigInteger('model_id')->nullable();
$table->json('old_values')->nullable();
$table->json('new_values')->nullable();
$table->string('ip_address');
$table->text('user_agent')->nullable();
$table->timestamps();
$table->index(['user_id', 'action']);
$table->index('created_at');
});
}
namespace App\Services;
use App\Models\AuditLog;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
class AuditLogService
{
public function log(string $action, $model = null, array $oldValues = [], array $newValues = [])
{
$request = request();
AuditLog::create([
'user_id' => Auth::id(),
'action' => $action,
'model_type' => $model ? get_class($model) : null,
'model_id' => $model ? $model->id : null,
'old_values' => $oldValues,
'new_values' => $newValues,
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
}
}
โก 4. Rate Limiting
API ํธ์ถ์ ์ ํํด์ ๋ฌด์ฐจ๋ณ ๋์ ๊ณต๊ฒฉ์ ๋ฐฉ์ดํ์.
namespace App\Http\Middleware;
use Closure;
use Illuminate\Cache\RateLimiter;
use Symfony\Component\HttpFoundation\Response;
class ApiRateLimiter
{
protected $limiter;
public function __construct(RateLimiter $limiter)
{
$this->limiter = $limiter;
}
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
$key = $this->resolveRequestSignature($request);
if ($this->limiter->tooManyAttempts($key, $maxAttempts)) {
return response()->json([
'message' => '๋๋ฌด ๋ง์ ์์ฒญ์
๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.',
'retry_after' => $this->limiter->availableIn($key)
], Response::HTTP_TOO_MANY_REQUESTS);
}
$this->limiter->hit($key, $decayMinutes * 60);
$response = $next($request);
return $this->addHeaders(
$response,
$maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts)
);
}
protected function resolveRequestSignature($request)
{
return sha1(
$request->method() .
'|' . $request->server('SERVER_NAME') .
'|' . $request->path() .
'|' . $request->ip()
);
}
protected function calculateRemainingAttempts($key, $maxAttempts)
{
return $this->limiter->retriesLeft($key, $maxAttempts);
}
protected function addHeaders($response, $maxAttempts, $remainingAttempts)
{
$response->headers->add([
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => $remainingAttempts,
]);
return $response;
}
}
๐งช ํ ์คํธ ์ฝ๋ ์์ฑํ๊ธฐ
ํ
์คํธ ์ฝ๋๋ ์ ํ์ด ์๋๋ผ ํ์์ผ! ํนํ ๋์ด ์ค๊ฐ๋ ์์คํ
์์๋ ๋๋์ฑ ๊ทธ๋. ๋ฒ๊ทธ ํ๋๊ฐ ํฐ ์์ค๋ก ์ด์ด์ง ์ ์๊ฑฐ๋ . ๐ฐ
๋ผ๋ผ๋ฒจ์ PHPUnit์ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํ๋๊น ํ
์คํธ ์์ฑ์ด ์ ๋ง ์ฌ์!
๐ฏ Feature ํ ์คํธ
์ค์ ์ฌ์ฉ์ ์๋๋ฆฌ์ค๋ฅผ ํ ์คํธํ๋ Feature ํ ์คํธ๋ถํฐ ๋ง๋ค์ด๋ณด์.
php artisan make:test TransactionTest
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use App\Models\Transaction;
use App\Models\Wallet;
use Illuminate\Foundation\Testing\RefreshDatabase;
class TransactionTest extends TestCase
{
use RefreshDatabase;
protected $admin;
protected $user;
protected $wallet;
protected function setUp(): void
{
parent::setUp();
// ํ
์คํธ ๋ฐ์ดํฐ ์ค๋น
$this->admin = User::factory()->create(['role' => 'admin']);
$this->user = User::factory()->create();
$this->wallet = Wallet::factory()->create(['user_id' => $this->user->id]);
}
/** @test */
public function admin_can_view_transaction_list()
{
$this->actingAs($this->admin);
Transaction::factory()->count(5)->create();
$response = $this->get(route('admin.transactions.index'));
$response->assertStatus(200);
$response->assertViewIs('admin.transactions.index');
$response->assertViewHas('transactions');
}
/** @test */
public function admin_can_view_transaction_details()
{
$this->actingAs($this->admin);
$transaction = Transaction::factory()->create([
'user_id' => $this->user->id,
'wallet_id' => $this->wallet->id,
]);
$response = $this->get(route('admin.transactions.show', $transaction->id));
$response->assertStatus(200);
$response->assertSee($transaction->transaction_hash);
}
/** @test */
public function admin_can_sync_transaction_from_blockchain()
{
$this->actingAs($this->admin);
$transaction = Transaction::factory()->create([
'status' => 'pending',
'confirmations' => 0,
]);
// ๋ธ๋ก์ฒด์ธ ์๋น์ค ๋ชจํน
$this->mock(BlockchainService::class, function ($mock) {
$mock->shouldReceive('getTransaction')
->andReturn((object)['blockNumber' => '0x123456']);
$mock->shouldReceive('getConfirmations')
->andReturn(15);
});
$response = $this->post(route('admin.transactions.sync', $transaction->id));
$response->assertStatus(200);
$response->assertJson(['success' => true]);
$transaction->refresh();
$this->assertEquals('confirmed', $transaction->status);
$this->assertEquals(15, $transaction->confirmations);
}
/** @test */
public function regular_user_cannot_access_admin_pages()
{
$this->actingAs($this->user);
$response = $this->get(route('admin.dashboard'));
$response->assertStatus(403); // Forbidden
}
/** @test */
public function transaction_status_changes_trigger_event()
{
Event::fake();
$transaction = Transaction::factory()->create(['status' => 'pending']);
$transaction->update(['status' => 'confirmed']);
Event::assertDispatched(TransactionStatusChanged::class, function ($event) use ($transaction) {
return $event->transaction->id === $transaction->id
&& $event->oldStatus === 'pending'
&& $event->newStatus === 'confirmed';
});
}
/** @test */
public function transaction_export_generates_csv_file()
{
$this->actingAs($this->admin);
Transaction::factory()->count(10)->create();
$response = $this->get(route('admin.transactions.export'));
$response->assertStatus(200);
$response->assertHeader('content-type', 'text/csv; charset=UTF-8');
}
}
๐ฌ Unit ํ ์คํธ
๊ฐ๋ณ ๋ฉ์๋๋ ํด๋์ค๋ฅผ ํ ์คํธํ๋ Unit ํ ์คํธ๋ ๋ง๋ค์.
php artisan make:test BlockchainServiceTest --unit
namespace Tests\Unit;
use Tests\TestCase;
use App\Services\BlockchainService;
class BlockchainServiceTest extends TestCase
{
protected $service;
protected function setUp(): void
{
parent::setUp();
$this->service = new BlockchainService();
}
/** @test */
public function it_can_get_balance_from_address()
{
$address = '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb';
$balance = $this->service->getBalance($address);
$this->assertIsNumeric($balance);
$this->assertGreaterThanOrEqual(0, $balance);
}
/** @test */
public function it_validates_ethereum_address_format()
{
$validAddress = '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb';
$invalidAddress = 'not-an-address';
$this->assertTrue($this->service->isValidAddress($validAddress));
$this->assertFalse($this->service->isValidAddress($invalidAddress));
}
/** @test */
public function it_converts_wei_to_ether_correctly()
{
$wei = '1000000000000000000'; // 1 ETH in Wei
$ether = $this->service->weiToEther($wei);
$this->assertEquals(1.0, $ether);
}
/** @test */
public function it_handles_blockchain_connection_errors_gracefully()
{
$this->expectException(\Exception::class);
// ์๋ชป๋ ๋
ธ๋ URL๋ก ์๋น์ค ์์ฑ
$service = new BlockchainService('http://invalid-node-url');
$service->getBalance('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb');
}
}
ํ ์คํธ๋ฅผ ์คํํด๋ณด์:
# ๋ชจ๋ ํ
์คํธ ์คํ
php artisan test
# ํน์ ํ
์คํธ๋ง ์คํ
php artisan test --filter TransactionTest
# ์ปค๋ฒ๋ฆฌ์ง ๋ฆฌํฌํธ ์์ฑ
php artisan test --coverage
๐ ๋ฐฐํฌ ๋ฐ ์ด์
๊ฐ๋ฐ์ด ๋๋ฌ์ผ๋ฉด ์ด์ ์ค์ ์๋ฒ์ ๋ฐฐํฌํด์ผ์ง! ๋ฐฐํฌ ๊ณผ์ ๊ณผ ์ด์ ํ์ ์๋ ค์ค๊ฒ. ๐
๐ฆ ํ๋ก๋์ ํ๊ฒฝ ์ค์
ํ๋ก๋์ ํ๊ฒฝ์์๋ ๊ฐ๋ฐ ํ๊ฒฝ๊ณผ ๋ค๋ฅธ ์ค์ ์ด ํ์ํด.
APP_ENV=production
APP_DEBUG=false
APP_URL=https://your-domain.com
# ๋ฐ์ดํฐ๋ฒ ์ด์ค
DB_CONNECTION=mysql
DB_HOST=your-db-host
DB_DATABASE=your-db-name
DB_USERNAME=your-db-user
DB_PASSWORD=your-secure-password
# ์บ์ ๋๋ผ์ด๋ฒ
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
# Redis
REDIS_HOST=your-redis-host
REDIS_PASSWORD=your-redis-password
# ๋ธ๋ก์ฒด์ธ (๋ฉ์ธ๋ท)
BLOCKCHAIN_NODE_URL=https://mainnet.infura.io/v3/your-project-id
BLOCKCHAIN_NETWORK=mainnet
# ๋ณด์
SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=strict
๐ง ๋ฐฐํฌ ์คํฌ๋ฆฝํธ
๋ฐฐํฌ๋ฅผ ์๋ํํ๋ ์คํฌ๋ฆฝํธ๋ฅผ ๋ง๋ค์ด๋๋ฉด ํธํด.
#!/bin/bash
echo "๐ ๋ฐฐํฌ ์์..."
# Git ์ต์ ์ฝ๋ ๊ฐ์ ธ์ค๊ธฐ
git pull origin main
# Composer ์์กด์ฑ ์ค์น
composer install --no-dev --optimize-autoloader
# NPM ๋น๋
npm install
npm run build
# ์บ์ ์ต์ ํ
php artisan config:cache
php artisan route:cache
php artisan view:cache
# ๋ง์ด๊ทธ๋ ์ด์
์คํ
php artisan migrate --force
# ํ ์์ปค ์ฌ์์
php artisan queue:restart
# ์ค์ผ์ค๋ฌ ํ์ธ
php artisan schedule:list
echo "โ
๋ฐฐํฌ ์๋ฃ!"
๐ณ Docker ์ปจํ ์ด๋ํ
Docker๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋ค ํ๊ฒฝ์์๋ ๋์ผํ๊ฒ ์คํํ ์ ์์ด.
FROM php:8.2-fpm
# ์์คํ
ํจํค์ง ์ค์น
RUN apt-get update && apt-get install -y \
git \
curl \
libpng-dev \
libonig-dev \
libxml2-dev \
zip \
unzip
# PHP ํ์ฅ ์ค์น
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd
# Composer ์ค์น
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# ์์
๋๋ ํ ๋ฆฌ ์ค์
WORKDIR /var/www
# ์ ํ๋ฆฌ์ผ์ด์
ํ์ผ ๋ณต์ฌ
COPY . .
# ์์กด์ฑ ์ค์น
RUN composer install --no-dev --optimize-autoloader
# ๊ถํ ์ค์
RUN chown -R www-data:www-data /var/www
EXPOSE 9000
CMD ["php-fpm"]
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: blockchain-payment-app
restart: unless-stopped
working_dir: /var/www
volumes:
- ./:/var/www
networks:
- blockchain-network
nginx:
image: nginx:alpine
container_name: blockchain-payment-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./:/var/www
- ./nginx.conf:/etc/nginx/conf.d/default.conf
networks:
- blockchain-network
mysql:
image: mysql:8.0
container_name: blockchain-payment-db
restart: unless-stopped
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql-data:/var/lib/mysql
networks:
- blockchain-network
redis:
image: redis:alpine
container_name: blockchain-payment-redis
restart: unless-stopped
networks:
- blockchain-network
queue:
build:
context: .
dockerfile: Dockerfile
container_name: blockchain-payment-queue
restart: unless-stopped
command: php artisan queue:work --tries=3
volumes:
- ./:/var/www
networks:
- blockchain-network
networks:
blockchain-network:
driver: bridge
volumes:
mysql-data:
๐ ๋ชจ๋ํฐ๋ง ์ค์
์ด์ ์ค์๋ ์์คํ ์ํ๋ฅผ ์ค์๊ฐ์ผ๋ก ๋ชจ๋ํฐ๋งํด์ผ ํด.
# Laravel Telescope (๊ฐ๋ฐ/์คํ
์ด์ง)
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate
# Sentry (์๋ฌ ์ถ์ )
composer require sentry/sentry-laravel
php artisan sentry:publish --dsn=your-sentry-dsn
# New Relic (์ฑ๋ฅ ๋ชจ๋ํฐ๋ง)
# ๋๋ DataDog, Prometheus + Grafana
์ด๋ ๊ฒ ํ๋ฉด ์๋ฌ๊ฐ ๋ฐ์ํ๊ฑฐ๋ ์ฑ๋ฅ ์ด์๊ฐ ์๊ฒผ์ ๋ ์ฆ์ ์๋ฆผ์ ๋ฐ์ ์ ์์ด!
๐ก ์ค์ ํ๊ณผ ๋ฒ ์คํธ ํ๋ํฐ์ค
๋ง์ง๋ง์ผ๋ก ์ค์ ์ด์ํ๋ฉด์ ๋ฐฐ์ด ๊ฟํ๋ค์ ๊ณต์ ํ ๊ฒ! ์ด๊ฑฐ ์์์ผ๋ฉด ์ํ์ฐฉ์ค๋ฅผ ๋ง์ด ์ค์ผ ์ ์์์ ํ ๋ฐ... ๐
๐ฏ ์ฑ๋ฅ ์ต์ ํ
N+1 ์ฟผ๋ฆฌ ๋ฌธ์ ๋ฅผ ์กฐ์ฌํด! Eager Loading์ ์ ๊ทน ํ์ฉํ์.
$transactions = Transaction::all();
foreach ($transactions as $tx) {
echo $tx->user->name; // N+1 ์ฟผ๋ฆฌ ๋ฐ์!
}
$transactions = Transaction::with('user', 'wallet')->get();
foreach ($transactions as $tx) {
echo $tx->user->name; // ๋จ 2๊ฐ์ ์ฟผ๋ฆฌ๋ง ์คํ!
}
์์ฃผ ์กฐํ๋์ง๋ง ์ ๋ณํ์ง ์๋ ๋ฐ์ดํฐ๋ ์บ์ฑํ์.
// ๋ธ๋ก์ฒด์ธ ๊ฐ์ค ๊ฐ๊ฒฉ (1๋ถ๋ง๋ค ๊ฐฑ์ )
$gasPrice = Cache::remember('gas_price', 60, function () {
return $this->blockchainService->getCurrentGasPrice();
});
// ์ฌ์ฉ์ ํต๊ณ (10๋ถ๋ง๋ค ๊ฐฑ์ )
$userStats = Cache::remember("user_stats_{$userId}", 600, function () use ($userId) {
return $this->calculateUserStatistics($userId);
});
์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฌ๋ ์์ ์ ํ๋ก ์ฒ๋ฆฌํ์.
// ๋๊ธฐ ์ฒ๋ฆฌ (๋๋ฆผ)
$this->blockchainService->syncAllTransactions(); // 30์ด ์์
// ๋น๋๊ธฐ ์ฒ๋ฆฌ (๋น ๋ฆ)
dispatch(new SyncAllTransactionsJob())->onQueue('blockchain'); // ์ฆ์ ๋ฐํ
๐ก๏ธ ๋ณด์ ์ฒดํฌ๋ฆฌ์คํธ
๐ฐ ๋น์ฉ ์ ๊ฐ ํ
1. ๊ฐ์ค๋น ์ต์ ํ
๊ฑฐ๋๋ฅผ ๋ณด๋ผ ๋ ๊ฐ์ค ๊ฐ๊ฒฉ์ด ๋ฎ์ ์๊ฐ๋๋ฅผ ํ์ฉํ์. ์ฃผ๋ง์ด๋ ์๋ฒฝ ์๊ฐ๋๊ฐ ๋ณดํต ์ ๋ ดํด.
2. ๋ฐฐ์น ์ฒ๋ฆฌ
์ฌ๋ฌ ๊ฑฐ๋๋ฅผ ํ ๋ฒ์ ์ฒ๋ฆฌํ๋ฉด ๊ฐ์ค๋น๋ฅผ ์ ์ฝํ ์ ์์ด.
3. Layer 2 ์๋ฃจ์
ํ์ฉ
Polygon, Arbitrum ๊ฐ์ Layer 2๋ฅผ ์ฌ์ฉํ๋ฉด ์์๋ฃ๋ฅผ ํฌ๊ฒ ์ค์ผ ์ ์์ด.
4. ํ
์คํธ๋ท ์ ๊ทน ํ์ฉ
๊ฐ๋ฐ ๋จ๊ณ์์๋ ๋ฐ๋์ ํ
์คํธ๋ท์ ์ฌ์ฉํด์ ์ค์ ๋น์ฉ์ด ๋ค์ง ์๋๋ก ํ์.
๐จ UX ๊ฐ์ ์์ด๋์ด
1. ์ค์๊ฐ ๊ฑฐ๋ ์ํ ํ์
์ฌ์ฉ์๊ฐ ๊ฑฐ๋ ์งํ ์ํฉ์ ์ค์๊ฐ์ผ๋ก ๋ณผ ์ ์๊ฒ ํ์. ๋ถ์๊ฐ์ ์ค์ผ ์ ์์ด.
2. ์์ ํ์ธ ์๊ฐ ํ์
"์ฝ 5๋ถ ํ ํ์ธ ์๋ฃ ์์ "์ฒ๋ผ ์์ ์๊ฐ์ ๋ณด์ฌ์ฃผ๋ฉด ์ข์.
3. ๊ฑฐ๋ ํ์คํ ๋ฆฌ ํํฐ๋ง
๋ ์ง, ๊ธ์ก, ์ํ๋ณ๋ก ์ฝ๊ฒ ํํฐ๋งํ ์ ์๊ฒ ํ์.
4. ๋ชจ๋ฐ์ผ ์ต์ ํ
์์ฆ์ ๋ชจ๋ฐ์ผ์์ ๋ง์ด ์ ์ํ๋๊น ๋ฐ์ํ ๋์์ธ์ ํ์!
5. ๋คํฌ ๋ชจ๋ ์ง์
๊ด๋ฆฌ์๋ค์ด ๋ฐค์ ์์
ํ ๋ ๋์ด ํธํ๋๋ก ๋คํฌ ๋ชจ๋๋ฅผ ์ ๊ณตํ์.
๐ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ ๋ง ๊ธด ์ฌ์ ์ด์์ด. ๋ผ๋ผ๋ฒจ๋ก ๋ธ๋ก์ฒด์ธ ๊ฒฐ์ ๊ด๋ฆฌ์ ํ์ด์ง๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์ฒ์๋ถํฐ ๋๊น์ง ๋ค๋ค๋ดค์ด. ๐
์ด์ ๋๋ Web3 ์๋์ ๊ฐ๋ฐ์๋ก์ ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ฐ ๊ฒฐ์ ์์คํ
์ ๊ตฌ์ถํ ์ ์๊ฒ ๋์ด! ๋ฌผ๋ก ์ฒ์์๋ ์ด๋ ค์ธ ์ ์์ง๋ง, ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ๋ฐ๋ผํ๋ค ๋ณด๋ฉด ๊ธ๋ฐฉ ์ต์ํด์ง ๊ฑฐ์ผ.
ํต์ฌ ํฌ์ธํธ๋ฅผ ๋ค์ ์ ๋ฆฌํ๋ฉด:
1. ํํํ ๊ธฐ์ด ๊ตฌ์กฐ
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ๋ถํฐ ์ ๋๋ก ํด์ผ ๋์ค์ ํ์ฅํ๊ธฐ ์ฌ์.
2. ๋ธ๋ก์ฒด์ธ ํต์
Web3 ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํด์ ๋ธ๋ก์ฒด์ธ๊ณผ ์์ ํ๊ฒ ํต์ ํ์.
3. ์ค์๊ฐ ๋ชจ๋ํฐ๋ง
ํ์ ์ค์ผ์ค๋ฌ๋ฅผ ํ์ฉํด์ ๊ฑฐ๋๋ฅผ ์ค์๊ฐ์ผ๋ก ์ถ์ ํ์.
4. ๋ณด์ ์ต์ฐ์
Private Key ๊ด๋ฆฌ, 2FA, ๊ฐ์ฌ ๋ก๊ทธ ๋ฑ ๋ณด์์ ์๋ฌด๋ฆฌ ๊ฐ์กฐํด๋ ์ง๋์น์ง ์์.
5. ํ
์คํธ์ ๋ชจ๋ํฐ๋ง
์๋ํ๋ ํ
์คํธ์ ๋ชจ๋ํฐ๋ง์ผ๋ก ์์ ์ ์ธ ์๋น์ค๋ฅผ ์ ์งํ์.
ํน์ ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ์ด๋ Web3 ํ๋ก์ ํธ์ ๊ด์ฌ์ด ์๋ค๋ฉด, ์ฌ๋ฅ๋ท(https://www.jaenung.net)์์ ๊ด๋ จ ์ ๋ฌธ๊ฐ๋ค์ ๋ง๋๋ณผ ์ ์์ด. ์ค๋งํธ ์ปจํธ๋ํธ ๊ฐ๋ฐ, ๋ธ๋ก์ฒด์ธ ์ปจ์คํ
, NFT ํ๋ก์ ํธ ๋ฑ ๋ค์ํ ์ฌ๋ฅ์ ๊ฑฐ๋ํ ์ ์๋ ํ๋ซํผ์ด๋๊น ํ๋ฒ ๋๋ฌ๋ณด๋ ๊ฒ๋ ์ข์ ๊ฑฐ์ผ!
๊ทธ๋ฆฌ๊ณ ์ด ๊ธ์์ ๋ค๋ฃฌ ๋ด์ฉ์ ๊ธฐ๋ณธ์ ์ธ ๊ตฌ์กฐ์ ๊ฐ๋
์ด์ผ. ์ค์ ํ๋ก๋์
ํ๊ฒฝ์์๋ ๋ ๋ง์ ๊ณ ๋ ค์ฌํญ๋ค์ด ์์ด. ์๋ฅผ ๋ค๋ฉด:
โข ๋ฉํฐ ์ฒด์ธ ์ง์ (์ด๋๋ฆฌ์, ๋ฐ์ด๋ธ์ค ์ค๋งํธ ์ฒด์ธ, ํด๋ฆฌ๊ณค ๋ฑ)
โข ํ ํฐ ์ค์ ๊ธฐ๋ฅ
โข DeFi ํ๋กํ ์ฝ ์ฐ๋
โข NFT ๊ฑฐ๋ ์ง์
โข ์ง๊ฐ ์ฐ๋ (MetaMask, WalletConnect)
โข ์ค๋งํธ ์ปจํธ๋ํธ ์๋ ๋ฐฐํฌ
โข ๊ฐ์ค๋น ์๋ ์ต์ ํ
โข ๋ค๊ตญ์ด ์ง์
โข ์ปดํ๋ผ์ด์ธ์ค (KYC/AML)
์ด๋ฐ ๊ธฐ๋ฅ๋ค์ ์ถ๊ฐํ๋ฉด ๋์ฑ ๊ฐ๋ ฅํ ์์คํ
์ด ๋ ๊ฑฐ์ผ. ํ์ง๋ง ์ฒ์๋ถํฐ ๋ชจ๋ ๊ฑธ ๋ค ๊ตฌํํ๋ ค๊ณ ํ์ง ๋ง๊ณ , MVP(Minimum Viable Product)๋ถํฐ ๋ง๋ค์ด์ ์ ์ง์ ์ผ๋ก ๊ฐ์ ํด ๋๊ฐ๋ ๊ฒ ์ข์.
๋ธ๋ก์ฒด์ธ ๊ธฐ์ ์ ๊ณ์ ๋ฐ์ ํ๊ณ ์์ด. ์๋ก์ด ํ๋กํ ์ฝ, ๋ ๋น ๋ฅธ ๋คํธ์ํฌ, ๋ ์ ๋ ดํ ์์๋ฃ... ์ด๋ฐ ๋ณํ๋ค์ ๊ณ์ ์ฃผ์ํ๋ฉด์ ์์คํ
์ ์
๋ฐ์ดํธํด ๋๊ฐ์ผ ํด.
๋ง์ง๋ง์ผ๋ก, ๊ฐ๋ฐํ๋ฉด์ ๋งํ๋ ๋ถ๋ถ์ด ์์ผ๋ฉด ๊ณต์ ๋ฌธ์๋ฅผ ๊ผญ ์ฝ์ด๋ด. ๋ผ๋ผ๋ฒจ ๊ณต์ ๋ฌธ์(https://laravel.com/docs)์ Web3.js ๋ฌธ์(https://web3js.readthedocs.io)๋ ์ ๋ง ์ ๋์ด ์์ด. ๊ทธ๋ฆฌ๊ณ ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ!
์, ์ด์ ์ ๋ง๋ก ๋์ด์ผ! ๋์ ๋ธ๋ก์ฒด์ธ ๊ฒฐ์ ์์คํ
๊ฐ๋ฐ์ ์์ํ ๊ฒ. ํ์ดํ
! ๐ชโจ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.