์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ’Ž ๋ผ๋ผ๋ฒจ๋กœ ๋งŒ๋“œ๋Š” ๋ธ”๋ก์ฒด์ธ ๊ตฌ๋… ๊ฒฐ์ œ ์‹œ์Šคํ…œ: Web3 ์‹œ๋Œ€์˜ ์ƒˆ๋กœ์šด ์ˆ˜์ต ๋ชจ๋ธ ๊ตฌ์ถ•ํ•˜๊ธฐ

๐Ÿ’Ž ๋ผ๋ผ๋ฒจ๋กœ ๋งŒ๋“œ๋Š” ๋ธ”๋ก์ฒด์ธ ๊ตฌ๋… ๊ฒฐ์ œ ์‹œ์Šคํ…œ: Web3 ์‹œ๋Œ€์˜ ์ƒˆ๋กœ์šด ์ˆ˜์ต ๋ชจ๋ธ ๊ตฌ์ถ•ํ•˜๊ธฐ

์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ์™€ PHP์˜ ๋งŒ๋‚จ, ํƒˆ์ค‘์•™ํ™” ๊ตฌ๋… ์„œ๋น„์Šค์˜ ๋ชจ๋“  ๊ฒƒ

๐ŸŽฏ ์ด ๊ธ€์—์„œ ๋ฐฐ์šธ ๋‚ด์šฉ

๋ธ”๋ก์ฒด์ธ ๊ธฐ์ˆ ๊ณผ ๋ผ๋ผ๋ฒจ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ์™„์ „ํžˆ ์ƒˆ๋กœ์šด ํ˜•ํƒœ์˜ ๊ตฌ๋… ๊ฒฐ์ œ ์‹œ์Šคํ…œ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ด…๋‹ˆ๋‹ค. ์ „ํ†ต์ ์ธ ์‹ ์šฉ์นด๋“œ ๊ฒฐ์ œ๊ฐ€ ์•„๋‹Œ, ์•”ํ˜ธํ™”ํ ๊ธฐ๋ฐ˜์˜ ์ž๋™ ๊ตฌ๋… ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ณด๋ฉด์„œ Web3 ์‹œ๋Œ€์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋ชจ๋ธ์„ ์ง์ ‘ ๊ฒฝํ—˜ํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๐Ÿš€

๐ŸŒŸ ์™œ ๋ธ”๋ก์ฒด์ธ ๊ตฌ๋… ๊ฒฐ์ œ์ธ๊ฐ€?

์—ฌ๋Ÿฌ๋ถ„, ๋„ทํ”Œ๋ฆญ์Šค๋‚˜ ์Šคํฌํ‹ฐํŒŒ์ด ๊ฐ™์€ ๊ตฌ๋… ์„œ๋น„์Šค ์“ฐ์‹œ์ฃ ? ๐Ÿ“บ ๋งค๋‹ฌ ์ž๋™์œผ๋กœ ๊ฒฐ์ œ๋˜๋Š” ๊ทธ ํŽธ๋ฆฌํ•จ! ๊ทผ๋ฐ ์ด๊ฒŒ ๋ธ”๋ก์ฒด์ธ์œผ๋กœ ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด ์–ด๋–จ๊นŒ์š”? ๋” ๋‚ฎ์€ ์ˆ˜์ˆ˜๋ฃŒ, ๊ตญ๊ฒฝ ์—†๋Š” ๊ฒฐ์ œ, ๊ทธ๋ฆฌ๊ณ  ๋ฌด์—‡๋ณด๋‹ค ์ค‘๊ฐœ์ž ์—†๋Š” ์ง์ ‘ ๊ฑฐ๋ž˜๊ฐ€ ๊ฐ€๋Šฅํ•ด์ง‘๋‹ˆ๋‹ค.

์ „ํ†ต์ ์ธ ๊ฒฐ์ œ ์‹œ์Šคํ…œ์€ ์‹ ์šฉ์นด๋“œ์‚ฌ, ๊ฒฐ์ œ๋Œ€ํ–‰์‚ฌ(PG์‚ฌ) ๋“ฑ ์—ฌ๋Ÿฌ ์ค‘๊ฐœ์ž๋ฅผ ๊ฑฐ์น˜๋ฉด์„œ ์ˆ˜์ˆ˜๋ฃŒ๊ฐ€ 3~5%์”ฉ ๋ถ™์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ธ”๋ก์ฒด์ธ ๊ธฐ๋ฐ˜ ๊ตฌ๋… ๊ฒฐ์ œ๋Š” ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋ฅผ ํ†ตํ•ด ์ž๋™ํ™”๋˜์–ด ์ˆ˜์ˆ˜๋ฃŒ๋ฅผ 1% ๋ฏธ๋งŒ์œผ๋กœ ์ค„์ผ ์ˆ˜ ์žˆ์ฃ . ๐Ÿ’ฐ

ํŠนํžˆ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๊ธ€๋กœ๋ฒŒ ํฌ๋ฆฌ์—์ดํ„ฐ๋“ค์ด ์ž์‹ ์˜ ์žฌ๋Šฅ์„ ๊ตฌ๋… ํ˜•ํƒœ๋กœ ํŒ๋งคํ•œ๋‹ค๋ฉด? ๊ตญ๊ฐ€ ๊ฐ„ ์†ก๊ธˆ ์ œ์•ฝ ์—†์ด, ์ฆ‰์‹œ ์ •์‚ฐ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ์‹œ์Šคํ…œ์ด ๊ฐ€๋Šฅํ•ด์ง‘๋‹ˆ๋‹ค!

๐Ÿ’ก ๋ธ”๋ก์ฒด์ธ ๊ตฌ๋… ๊ฒฐ์ œ์˜ ํ•ต์‹ฌ ์žฅ์ 
  • โœ“ ๋‚ฎ์€ ์ˆ˜์ˆ˜๋ฃŒ (0.1~1% ์ˆ˜์ค€)
  • โœ“ ๊ตญ๊ฒฝ ์—†๋Š” ๊ธ€๋กœ๋ฒŒ ๊ฒฐ์ œ
  • โœ“ ์ฆ‰์‹œ ์ •์‚ฐ (T+0)
  • โœ“ ํˆฌ๋ช…ํ•œ ๊ฑฐ๋ž˜ ๋‚ด์—ญ
  • โœ“ ์ค‘๊ฐœ์ž ์—†๋Š” P2P ๊ฑฐ๋ž˜
  • โœ“ ํ”„๋กœ๊ทธ๋ž˜๋จธ๋ธ”ํ•œ ์ž๋™ํ™” (์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ)
๋ธ”๋ก์ฒด์ธ ๊ตฌ๋… ๊ฒฐ์ œ ์‹œ์Šคํ…œ ๊ตฌ์กฐ ์‚ฌ์šฉ์ž ์ง€๊ฐ‘ ์—ฐ๊ฒฐ ๊ตฌ๋… ์‹ ์ฒญ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ž๋™ ๊ฒฐ์ œ ๊ตฌ๋… ๊ด€๋ฆฌ ๋ผ๋ผ๋ฒจ ๋ฐฑ์—”๋“œ ์ƒํƒœ ๋™๊ธฐํ™” ์„œ๋น„์Šค ์ œ๊ณต ํŠธ๋žœ์žญ์…˜ ์ด๋ฒคํŠธ ๋ธ”๋ก์ฒด์ธ ๋ ˆ์ด์–ด โ€ข ERC-20 ํ† ํฐ ์ „์†ก โ€ข ๊ตฌ๋… ์ƒํƒœ ์ €์žฅ โ€ข ์ž๋™ ๊ฐฑ์‹  ๋กœ์ง โ€ข ํ™˜๋ถˆ ์ฒ˜๋ฆฌ ๋ผ๋ผ๋ฒจ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ โ€ข ์‚ฌ์šฉ์ž ์ธ์ฆ โ€ข ๊ตฌ๋… ์ƒํƒœ ์กฐํšŒ โ€ข ์ปจํ…์ธ  ์ ‘๊ทผ ์ œ์–ด โ€ข ๋Œ€์‹œ๋ณด๋“œ ์ œ๊ณต ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค โ€ข ์‚ฌ์šฉ์ž ์ •๋ณด โ€ข ๊ตฌ๋… ํžˆ์Šคํ† ๋ฆฌ โ€ข ํŠธ๋žœ์žญ์…˜ ๋กœ๊ทธ โ€ข ๋ถ„์„ ๋ฐ์ดํ„ฐ

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

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ๊ฐœ๋ฐœ์„ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ์š”? ๐Ÿ‘จโ€๐Ÿ’ป ๋จผ์ € ํ•„์š”ํ•œ ๋„๊ตฌ๋“ค์„ ์ค€๋น„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ƒ๊ฐ๋ณด๋‹ค ๋ณต์žกํ•˜์ง€ ์•Š์œผ๋‹ˆ ๊ฑฑ์ • ๋งˆ์„ธ์š”!

๐Ÿ“ฆ ํ•„์ˆ˜ ์ค€๋น„๋ฌผ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

1. PHP 8.1 ์ด์ƒ - ๋ผ๋ผ๋ฒจ 10.x๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค
2. Composer - PHP ํŒจํ‚ค์ง€ ๊ด€๋ฆฌ์ž
3. Node.js & NPM - ํ”„๋ก ํŠธ์—”๋“œ ๋นŒ๋“œ ๋„๊ตฌ
4. MySQL ๋˜๋Š” PostgreSQL - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค
5. MetaMask - ํ…Œ์ŠคํŠธ์šฉ ์ง€๊ฐ‘
6. Infura ๋˜๋Š” Alchemy ๊ณ„์ • - ๋ธ”๋ก์ฒด์ธ ๋…ธ๋“œ ์ ‘๊ทผ
7. Remix IDE - ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๊ฐœ๋ฐœ (์„ ํƒ์‚ฌํ•ญ)

๋ผ๋ผ๋ฒจ ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒˆ๋กœ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค:

# ๋ผ๋ผ๋ฒจ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
composer create-project laravel/laravel blockchain-subscription

cd blockchain-subscription

# ํ•„์š”ํ•œ ํŒจํ‚ค์ง€ ์„ค์น˜
composer require web3p/web3.php
composer require simplito/elliptic-php
composer require kornrunner/keccak

# ํ”„๋ก ํŠธ์—”๋“œ ํŒจํ‚ค์ง€
npm install ethers@5.7.2
npm install @metamask/detect-provider
๐Ÿ’ก Pro Tip

web3.php๋Š” PHP์—์„œ ์ด๋”๋ฆฌ์›€ ๋ธ”๋ก์ฒด์ธ๊ณผ ํ†ต์‹ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค. ํ”„๋ก ํŠธ์—”๋“œ์—์„œ๋Š” ethers.js๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ๋ฐฑ์—”๋“œ์—์„œ๋Š” web3.php๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ตฌ์กฐ๊ฐ€ ๊ฐ€์žฅ ์•ˆ์ •์ ์ž…๋‹ˆ๋‹ค. ๐Ÿ”ง

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

๋ธ”๋ก์ฒด์ธ ๊ตฌ๋… ์‹œ์Šคํ…œ์˜ ํ•ต์‹ฌ์€ ๋ฐ”๋กœ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ์ž…๋‹ˆ๋‹ค. Solidity ์–ธ์–ด๋กœ ์ž‘์„ฑํ•˜๋ฉฐ, ์ด๋”๋ฆฌ์›€ ๋„คํŠธ์›Œํฌ์— ๋ฐฐํฌ๋ฉ๋‹ˆ๋‹ค. ๊ตฌ๋… ๋กœ์ง์„ ์ฝ”๋“œ๋กœ ๊ตฌํ˜„ํ•˜๋ฉด ๋ˆ„๊ตฌ๋„ ์กฐ์ž‘ํ•  ์ˆ˜ ์—†๋Š” ํˆฌ๋ช…ํ•œ ์‹œ์Šคํ…œ์ด ์™„์„ฑ๋˜์ฃ ! โšก

๋จผ์ € ๊ธฐ๋ณธ์ ์ธ ๊ตฌ๋… ์ปจํŠธ๋ž™ํŠธ ๊ตฌ์กฐ๋ฅผ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

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

contract SubscriptionService is ReentrancyGuard, Ownable {
    
    // ๊ตฌ๋… ํ”Œ๋žœ ๊ตฌ์กฐ์ฒด
    struct Plan {
        uint256 price;           // ์›” ๊ตฌ๋…๋ฃŒ (ํ† ํฐ ๋‹จ์œ„)
        uint256 duration;        // ๊ตฌ๋… ๊ธฐ๊ฐ„ (์ดˆ ๋‹จ์œ„)
        bool active;             // ํ”Œ๋žœ ํ™œ์„ฑํ™” ์—ฌ๋ถ€
        string name;             // ํ”Œ๋žœ ์ด๋ฆ„
    }
    
    // ์‚ฌ์šฉ์ž ๊ตฌ๋… ์ •๋ณด
    struct Subscription {
        uint256 planId;          // ๊ตฌ๋… ์ค‘์ธ ํ”Œ๋žœ ID
        uint256 startTime;       // ๊ตฌ๋… ์‹œ์ž‘ ์‹œ๊ฐ„
        uint256 endTime;         // ๊ตฌ๋… ์ข…๋ฃŒ ์‹œ๊ฐ„
        bool autoRenew;          // ์ž๋™ ๊ฐฑ์‹  ์—ฌ๋ถ€
        bool active;             // ๊ตฌ๋… ํ™œ์„ฑํ™” ์—ฌ๋ถ€
    }
    
    // ์ƒํƒœ ๋ณ€์ˆ˜
    IERC20 public paymentToken;                          // ๊ฒฐ์ œ ํ† ํฐ (USDT, USDC ๋“ฑ)
    mapping(uint256 => Plan) public plans;               // ํ”Œ๋žœ ID => ํ”Œ๋žœ ์ •๋ณด
    mapping(address => Subscription) public subscriptions; // ์‚ฌ์šฉ์ž ์ฃผ์†Œ => ๊ตฌ๋… ์ •๋ณด
    uint256 public planCount;                            // ์ด ํ”Œ๋žœ ๊ฐœ์ˆ˜
    
    // ์ด๋ฒคํŠธ
    event PlanCreated(uint256 indexed planId, string name, uint256 price);
    event Subscribed(address indexed user, uint256 indexed planId, uint256 endTime);
    event Renewed(address indexed user, uint256 indexed planId, uint256 newEndTime);
    event Cancelled(address indexed user, uint256 indexed planId);
    event AutoRenewToggled(address indexed user, bool autoRenew);
    
    constructor(address _paymentToken) {
        paymentToken = IERC20(_paymentToken);
    }
    
    // ํ”Œ๋žœ ์ƒ์„ฑ (๊ด€๋ฆฌ์ž๋งŒ)
    function createPlan(
        string memory _name,
        uint256 _price,
        uint256 _duration
    ) external onlyOwner {
        planCount++;
        plans[planCount] = Plan({
            name: _name,
            price: _price,
            duration: _duration,
            active: true
        });
        
        emit PlanCreated(planCount, _name, _price);
    }
    
    // ๊ตฌ๋… ์‹œ์ž‘
    function subscribe(uint256 _planId) external nonReentrant {
        require(plans[_planId].active, "Plan not active");
        require(!subscriptions[msg.sender].active, "Already subscribed");
        
        Plan memory plan = plans[_planId];
        
        // ํ† ํฐ ์ „์†ก
        require(
            paymentToken.transferFrom(msg.sender, address(this), plan.price),
            "Payment failed"
        );
        
        // ๊ตฌ๋… ์ •๋ณด ์ €์žฅ
        uint256 endTime = block.timestamp + plan.duration;
        subscriptions[msg.sender] = Subscription({
            planId: _planId,
            startTime: block.timestamp,
            endTime: endTime,
            autoRenew: true,
            active: true
        });
        
        emit Subscribed(msg.sender, _planId, endTime);
    }
    
    // ๊ตฌ๋… ๊ฐฑ์‹ 
    function renew() external nonReentrant {
        Subscription storage sub = subscriptions[msg.sender];
        require(sub.active, "No active subscription");
        require(block.timestamp >= sub.endTime, "Subscription still valid");
        require(sub.autoRenew, "Auto-renew disabled");
        
        Plan memory plan = plans[sub.planId];
        require(plan.active, "Plan no longer active");
        
        // ํ† ํฐ ์ „์†ก
        require(
            paymentToken.transferFrom(msg.sender, address(this), plan.price),
            "Payment failed"
        );
        
        // ๊ตฌ๋… ๊ธฐ๊ฐ„ ์—ฐ์žฅ
        sub.endTime = block.timestamp + plan.duration;
        
        emit Renewed(msg.sender, sub.planId, sub.endTime);
    }
    
    // ๊ตฌ๋… ์ทจ์†Œ
    function cancel() external {
        Subscription storage sub = subscriptions[msg.sender];
        require(sub.active, "No active subscription");
        
        sub.active = false;
        sub.autoRenew = false;
        
        emit Cancelled(msg.sender, sub.planId);
    }
    
    // ์ž๋™ ๊ฐฑ์‹  ํ† ๊ธ€
    function toggleAutoRenew() external {
        Subscription storage sub = subscriptions[msg.sender];
        require(sub.active, "No active subscription");
        
        sub.autoRenew = !sub.autoRenew;
        
        emit AutoRenewToggled(msg.sender, sub.autoRenew);
    }
    
    // ๊ตฌ๋… ์ƒํƒœ ํ™•์ธ
    function isSubscriptionValid(address _user) external view returns (bool) {
        Subscription memory sub = subscriptions[_user];
        return sub.active && block.timestamp < sub.endTime;
    }
    
    // ๋‚จ์€ ๊ตฌ๋… ๊ธฐ๊ฐ„ ์กฐํšŒ
    function getRemainingTime(address _user) external view returns (uint256) {
        Subscription memory sub = subscriptions[_user];
        if (!sub.active || block.timestamp >= sub.endTime) {
            return 0;
        }
        return sub.endTime - block.timestamp;
    }
    
    // ์ˆ˜์ต ์ธ์ถœ (๊ด€๋ฆฌ์ž๋งŒ)
    function withdraw(uint256 _amount) external onlyOwner {
        require(
            paymentToken.transfer(owner(), _amount),
            "Withdrawal failed"
        );
    }
}
โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ

์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋Š” ๋ฐฐํฌ ํ›„ ์ˆ˜์ •์ด ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค! ๋ฐ˜๋“œ์‹œ ํ…Œ์ŠคํŠธ๋„ท(Goerli, Sepolia ๋“ฑ)์—์„œ ์ถฉ๋ถ„ํžˆ ํ…Œ์ŠคํŠธํ•œ ํ›„ ๋ฉ”์ธ๋„ท์— ๋ฐฐํฌํ•˜์„ธ์š”. OpenZeppelin์˜ ๊ฒ€์ฆ๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ”’

์œ„ ์ปจํŠธ๋ž™ํŠธ์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ๋“ค์„ ์ •๋ฆฌํ•˜๋ฉด:

ํ•จ์ˆ˜๋ช… ๊ธฐ๋Šฅ ๊ถŒํ•œ
createPlan ์ƒˆ๋กœ์šด ๊ตฌ๋… ํ”Œ๋žœ ์ƒ์„ฑ ๊ด€๋ฆฌ์ž
subscribe ๊ตฌ๋… ์‹œ์ž‘ ๋ฐ ๊ฒฐ์ œ ๋ชจ๋“  ์‚ฌ์šฉ์ž
renew ๊ตฌ๋… ๊ฐฑ์‹  (์ž๋™/์ˆ˜๋™) ๊ตฌ๋…์ž
cancel ๊ตฌ๋… ์ทจ์†Œ ๊ตฌ๋…์ž
toggleAutoRenew ์ž๋™ ๊ฐฑ์‹  ์„ค์ • ๋ณ€๊ฒฝ ๊ตฌ๋…์ž
isSubscriptionValid ๊ตฌ๋… ์œ ํšจ์„ฑ ํ™•์ธ ์ฝ๊ธฐ ์ „์šฉ
withdraw ์ˆ˜์ต ์ธ์ถœ ๊ด€๋ฆฌ์ž

๐Ÿš€ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๋ฐฐํฌํ•˜๊ธฐ

์ปจํŠธ๋ž™ํŠธ๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค๋ฉด ์ด์ œ ๋ธ”๋ก์ฒด์ธ์— ๋ฐฐํฌํ•ด์•ผ๊ฒ ์ฃ ? ๐ŸŒ Hardhat์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ฐฐํฌ ๊ณผ์ •์„ ์ž๋™ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

# Hardhat ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐํ™”
npm install --save-dev hardhat
npx hardhat

# ํ•„์š”ํ•œ ํ”Œ๋Ÿฌ๊ทธ์ธ ์„ค์น˜
npm install --save-dev @nomiclabs/hardhat-ethers ethers
npm install --save-dev @openzeppelin/contracts

๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค (scripts/deploy.js):

const hre = require("hardhat");

async function main() {
    // USDT ํ† ํฐ ์ฃผ์†Œ (ํ…Œ์ŠคํŠธ๋„ท)
    const USDT_ADDRESS = "0x..."; // Goerli USDT ์ฃผ์†Œ
    
    console.log("Deploying SubscriptionService...");
    
    const SubscriptionService = await hre.ethers.getContractFactory("SubscriptionService");
    const subscription = await SubscriptionService.deploy(USDT_ADDRESS);
    
    await subscription.deployed();
    
    console.log("SubscriptionService deployed to:", subscription.address);
    
    // ๊ธฐ๋ณธ ํ”Œ๋žœ ์ƒ์„ฑ
    console.log("Creating default plans...");
    
    // ๋ฒ ์ด์ง ํ”Œ๋žœ: ์›” $9.99 (30์ผ)
    await subscription.createPlan(
        "Basic Plan",
        hre.ethers.utils.parseUnits("9.99", 6), // USDT๋Š” 6 decimals
        30 * 24 * 60 * 60 // 30์ผ
    );
    
    // ํ”„๋กœ ํ”Œ๋žœ: ์›” $29.99 (30์ผ)
    await subscription.createPlan(
        "Pro Plan",
        hre.ethers.utils.parseUnits("29.99", 6),
        30 * 24 * 60 * 60
    );
    
    // ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ํ”Œ๋žœ: ์›” $99.99 (30์ผ)
    await subscription.createPlan(
        "Enterprise Plan",
        hre.ethers.utils.parseUnits("99.99", 6),
        30 * 24 * 60 * 60
    );
    
    console.log("Plans created successfully!");
    
    // ์ปจํŠธ๋ž™ํŠธ ์ •๋ณด ์ €์žฅ
    const fs = require('fs');
    const contractInfo = {
        address: subscription.address,
        network: hre.network.name,
        deployedAt: new Date().toISOString()
    };
    
    fs.writeFileSync(
        './contract-info.json',
        JSON.stringify(contractInfo, null, 2)
    );
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });

hardhat.config.js ์„ค์ •:

require("@nomiclabs/hardhat-ethers");
require('dotenv').config();

module.exports = {
    solidity: {
        version: "0.8.19",
        settings: {
            optimizer: {
                enabled: true,
                runs: 200
            }
        }
    },
    networks: {
        goerli: {
            url: process.env.GOERLI_RPC_URL,
            accounts: [process.env.PRIVATE_KEY],
            chainId: 5
        },
        sepolia: {
            url: process.env.SEPOLIA_RPC_URL,
            accounts: [process.env.PRIVATE_KEY],
            chainId: 11155111
        },
        mainnet: {
            url: process.env.MAINNET_RPC_URL,
            accounts: [process.env.PRIVATE_KEY],
            chainId: 1
        }
    }
};

๋ฐฐํฌ ์‹คํ–‰:

# ํ…Œ์ŠคํŠธ๋„ท ๋ฐฐํฌ
npx hardhat run scripts/deploy.js --network goerli

# ๋ฉ”์ธ๋„ท ๋ฐฐํฌ (์ถฉ๋ถ„ํ•œ ํ…Œ์ŠคํŠธ ํ›„!)
npx hardhat run scripts/deploy.js --network mainnet

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

์ด์ œ ๋ผ๋ผ๋ฒจ ๋ฐฑ์—”๋“œ์—์„œ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ์™€ ํ†ต์‹ ํ•˜๋Š” ๋ถ€๋ถ„์„ ๊ตฌํ˜„ํ•ด๋ด…์‹œ๋‹ค. ๐Ÿ”Œ ๋จผ์ € 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 = config('web3.rpc_url');
        $this->web3 = new Web3($rpcUrl);
        
        // ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ์™€ ABI ๋กœ๋“œ
        $this->contractAddress = config('web3.contract_address');
        $this->abi = json_decode(file_get_contents(storage_path('app/contract-abi.json')), true);
        
        $this->contract = new Contract($rpcUrl, $this->abi);
    }
    
    /**
     * ์‚ฌ์šฉ์ž์˜ ๊ตฌ๋… ์ƒํƒœ ํ™•์ธ
     */
    public function checkSubscription(string $userAddress): array
    {
        $subscription = null;
        
        $this->contract->at($this->contractAddress)->call('subscriptions', $userAddress, function ($err, $result) use (&$subscription) {
            if ($err !== null) {
                throw new \Exception('Failed to fetch subscription: ' . $err->getMessage());
            }
            $subscription = $result;
        });
        
        return [
            'plan_id' => $subscription['planId']->toString(),
            'start_time' => $subscription['startTime']->toString(),
            'end_time' => $subscription['endTime']->toString(),
            'auto_renew' => $subscription['autoRenew'],
            'active' => $subscription['active']
        ];
    }
    
    /**
     * ๊ตฌ๋… ์œ ํšจ์„ฑ ํ™•์ธ
     */
    public function isSubscriptionValid(string $userAddress): bool
    {
        $isValid = false;
        
        $this->contract->at($this->contractAddress)->call('isSubscriptionValid', $userAddress, function ($err, $result) use (&$isValid) {
            if ($err !== null) {
                throw new \Exception('Failed to check validity: ' . $err->getMessage());
            }
            $isValid = $result[0];
        });
        
        return $isValid;
    }
    
    /**
     * ๋‚จ์€ ๊ตฌ๋… ๊ธฐ๊ฐ„ ์กฐํšŒ (์ดˆ ๋‹จ์œ„)
     */
    public function getRemainingTime(string $userAddress): int
    {
        $remainingTime = 0;
        
        $this->contract->at($this->contractAddress)->call('getRemainingTime', $userAddress, function ($err, $result) use (&$remainingTime) {
            if ($err !== null) {
                throw new \Exception('Failed to get remaining time: ' . $err->getMessage());
            }
            $remainingTime = (int) $result[0]->toString();
        });
        
        return $remainingTime;
    }
    
    /**
     * ํ”Œ๋žœ ์ •๋ณด ์กฐํšŒ
     */
    public function getPlan(int $planId): array
    {
        $plan = null;
        
        $this->contract->at($this->contractAddress)->call('plans', $planId, function ($err, $result) use (&$plan) {
            if ($err !== null) {
                throw new \Exception('Failed to fetch plan: ' . $err->getMessage());
            }
            $plan = $result;
        });
        
        return [
            'price' => $plan['price']->toString(),
            'duration' => $plan['duration']->toString(),
            'active' => $plan['active'],
            'name' => $plan['name']
        ];
    }
    
    /**
     * ํŠธ๋žœ์žญ์…˜ ํ™•์ธ
     */
    public function getTransactionReceipt(string $txHash): ?array
    {
        $receipt = null;
        
        $this->web3->eth->getTransactionReceipt($txHash, function ($err, $result) use (&$receipt) {
            if ($err !== null) {
                return null;
            }
            $receipt = $result;
        });
        
        return $receipt;
    }
    
    /**
     * ์ด๋ฒคํŠธ ๋กœ๊ทธ ํŒŒ์‹ฑ
     */
    public function parseSubscribedEvent(array $log): array
    {
        // ์ด๋ฒคํŠธ ์‹œ๊ทธ๋‹ˆ์ฒ˜: Subscribed(address,uint256,uint256)
        $eventSignature = '0x' . hash('sha3-256', 'Subscribed(address,uint256,uint256)');
        
        if ($log['topics'][0] !== $eventSignature) {
            throw new \Exception('Invalid event signature');
        }
        
        return [
            'user' => '0x' . substr($log['topics'][1], 26),
            'plan_id' => hexdec($log['topics'][2]),
            'end_time' => hexdec($log['data'])
        ];
    }
}

์„ค์ • ํŒŒ์ผ ์ƒ์„ฑ (config/web3.php):

<?php

return [
    'rpc_url' => env('WEB3_RPC_URL', 'https://goerli.infura.io/v3/YOUR_PROJECT_ID'),
    'contract_address' => env('WEB3_CONTRACT_ADDRESS'),
    'chain_id' => env('WEB3_CHAIN_ID', 5),
    'payment_token' => env('WEB3_PAYMENT_TOKEN'),
];

๐Ÿ’พ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„

๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ์ปฌ DB์— ๋™๊ธฐํ™”ํ•˜์—ฌ ๋น ๋ฅธ ์กฐํšŒ์™€ ๋ถ„์„์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ“Š

// database/migrations/2024_01_01_000001_create_subscriptions_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('subscriptions', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->string('wallet_address', 42)->index();
            $table->unsignedBigInteger('plan_id');
            $table->timestamp('start_time');
            $table->timestamp('end_time');
            $table->boolean('auto_renew')->default(true);
            $table->boolean('active')->default(true);
            $table->string('tx_hash', 66)->nullable();
            $table->timestamps();
            
            $table->index(['wallet_address', 'active']);
            $table->index('end_time');
        });
        
        Schema::create('subscription_transactions', function (Blueprint $table) {
            $table->id();
            $table->foreignId('subscription_id')->constrained()->onDelete('cascade');
            $table->string('tx_hash', 66)->unique();
            $table->string('type', 20); // subscribe, renew, cancel
            $table->string('amount', 78); // BigNumber string
            $table->unsignedBigInteger('block_number');
            $table->timestamp('block_timestamp');
            $table->string('status', 20)->default('pending'); // pending, confirmed, failed
            $table->timestamps();
            
            $table->index('tx_hash');
            $table->index(['subscription_id', 'type']);
        });
        
        Schema::create('subscription_plans', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('chain_plan_id')->unique();
            $table->string('name');
            $table->string('price', 78);
            $table->unsignedInteger('duration'); // seconds
            $table->boolean('active')->default(true);
            $table->text('description')->nullable();
            $table->json('features')->nullable();
            $table->timestamps();
        });
    }
    
    public function down()
    {
        Schema::dropIfExists('subscription_transactions');
        Schema::dropIfExists('subscriptions');
        Schema::dropIfExists('subscription_plans');
    }
};

Eloquent ๋ชจ๋ธ ์ƒ์„ฑ:

// app/Models/Subscription.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Carbon\Carbon;

class Subscription extends Model
{
    protected $fillable = [
        'user_id',
        'wallet_address',
        'plan_id',
        'start_time',
        'end_time',
        'auto_renew',
        'active',
        'tx_hash'
    ];
    
    protected $casts = [
        'start_time' => 'datetime',
        'end_time' => 'datetime',
        'auto_renew' => 'boolean',
        'active' => 'boolean'
    ];
    
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
    
    public function plan(): BelongsTo
    {
        return $this->belongsTo(SubscriptionPlan::class, 'plan_id', 'chain_plan_id');
    }
    
    public function transactions(): HasMany
    {
        return $this->hasMany(SubscriptionTransaction::class);
    }
    
    /**
     * ๊ตฌ๋…์ด ์œ ํšจํ•œ์ง€ ํ™•์ธ
     */
    public function isValid(): bool
    {
        return $this->active && $this->end_time->isFuture();
    }
    
    /**
     * ๋‚จ์€ ์ผ์ˆ˜ ๊ณ„์‚ฐ
     */
    public function daysRemaining(): int
    {
        if (!$this->isValid()) {
            return 0;
        }
        
        return Carbon::now()->diffInDays($this->end_time, false);
    }
    
    /**
     * ๋งŒ๋ฃŒ ์ž„๋ฐ• ์—ฌ๋ถ€ (3์ผ ์ด๋‚ด)
     */
    public function isExpiringSoon(): bool
    {
        $days = $this->daysRemaining();
        return $days > 0 && $days <= 3;
    }
    
    /**
     * ์Šค์ฝ”ํ”„: ํ™œ์„ฑ ๊ตฌ๋…๋งŒ
     */
    public function scopeActive($query)
    {
        return $query->where('active', true)
                    ->where('end_time', '>', now());
    }
    
    /**
     * ์Šค์ฝ”ํ”„: ๋งŒ๋ฃŒ๋œ ๊ตฌ๋…
     */
    public function scopeExpired($query)
    {
        return $query->where('end_time', '<=', now());
    }
}

๐ŸŽจ ํ”„๋ก ํŠธ์—”๋“œ ๊ตฌํ˜„ (Vue.js + Ethers.js)

์‚ฌ์šฉ์ž๊ฐ€ MetaMask๋กœ ์ง€๊ฐ‘์„ ์—ฐ๊ฒฐํ•˜๊ณ  ๊ตฌ๋…ํ•  ์ˆ˜ ์žˆ๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ด…์‹œ๋‹ค. ๐ŸŽญ

// resources/js/components/SubscriptionManager.vue
<template>
    <div class="subscription-manager">
        <div v-if="!walletConnected" class="connect-wallet">
            <h2>์ง€๊ฐ‘ ์—ฐ๊ฒฐ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค</h2>
            <button @click="connectWallet" class="btn-primary">
                MetaMask ์—ฐ๊ฒฐ
            </button>
        </div>
        
        <div v-else>
            <div class="wallet-info">
                <p>์—ฐ๊ฒฐ๋œ ์ง€๊ฐ‘: {{ shortenAddress(account) }}</p>
                <p>์ž”์•ก: {{ balance }} USDT</p>
            </div>
            
            <div v-if="!hasSubscription" class="plans">
                <h2>๊ตฌ๋… ํ”Œ๋žœ ์„ ํƒ</h2>
                <div class="plan-grid">
                    <div 
                        v-for="plan in plans" 
                        :key="plan.id"
                        class="plan-card"
                        :class="{ selected: selectedPlan === plan.id }"
                        @click="selectPlan(plan.id)"
                    >
                        <h3>{{ plan.name }}</h3>
                        <div class="price">${{ plan.price }}</div>
                        <div class="duration">{{ plan.durationDays }}์ผ</div>
                        <ul class="features">
                            <li v-for="feature in plan.features" :key="feature">
                                โœ“ {{ feature }}
                            </li>
                        </ul>
                    </div>
                </div>
                
                <button 
                    @click="subscribe"
                    :disabled="!selectedPlan || subscribing"
                    class="btn-subscribe"
                >
                    {{ subscribing ? '์ฒ˜๋ฆฌ ์ค‘...' : '๊ตฌ๋…ํ•˜๊ธฐ' }}
                </button>
            </div>
            
            <div v-else class="current-subscription">
                <h2>ํ˜„์žฌ ๊ตฌ๋… ์ •๋ณด</h2>
                <div class="sub-info">
                    <p><strong>ํ”Œ๋žœ:</strong> {{ currentSubscription.planName }}</p>
                    <p><strong>์‹œ์ž‘์ผ:</strong> {{ formatDate(currentSubscription.startTime) }}</p>
                    <p><strong>์ข…๋ฃŒ์ผ:</strong> {{ formatDate(currentSubscription.endTime) }}</p>
                    <p><strong>๋‚จ์€ ๊ธฐ๊ฐ„:</strong> {{ daysRemaining }}์ผ</p>
                    <p>
                        <strong>์ž๋™ ๊ฐฑ์‹ :</strong> 
                        <span :class="currentSubscription.autoRenew ? 'text-success' : 'text-danger'">
                            {{ currentSubscription.autoRenew ? 'ํ™œ์„ฑํ™”' : '๋น„ํ™œ์„ฑํ™”' }}
                        </span>
                    </p>
                </div>
                
                <div class="actions">
                    <button @click="toggleAutoRenew" class="btn-secondary">
                        ์ž๋™ ๊ฐฑ์‹  {{ currentSubscription.autoRenew ? '๋„๊ธฐ' : '์ผœ๊ธฐ' }}
                    </button>
                    <button @click="cancelSubscription" class="btn-danger">
                        ๊ตฌ๋… ์ทจ์†Œ
                    </button>
                </div>
            </div>
        </div>
        
        <div v-if="txHash" class="transaction-status">
            <p>ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ: {{ txHash }}</p>
            <a :href="getEtherscanUrl(txHash)" target="_blank">
                Etherscan์—์„œ ํ™•์ธ
            </a>
        </div>
    </div>
</template>

<script>
import { ethers } from 'ethers';
import detectEthereumProvider from '@metamask/detect-provider';
import contractABI from '../contracts/SubscriptionService.json';

export default {
    name: 'SubscriptionManager',
    
    data() {
        return {
            provider: null,
            signer: null,
            contract: null,
            account: null,
            balance: '0',
            walletConnected: false,
            plans: [],
            selectedPlan: null,
            subscribing: false,
            hasSubscription: false,
            currentSubscription: null,
            txHash: null,
            contractAddress: process.env.MIX_CONTRACT_ADDRESS,
            usdtAddress: process.env.MIX_USDT_ADDRESS
        };
    },
    
    computed: {
        daysRemaining() {
            if (!this.currentSubscription) return 0;
            const now = Math.floor(Date.now() / 1000);
            const remaining = this.currentSubscription.endTime - now;
            return Math.floor(remaining / 86400);
        }
    },
    
    async mounted() {
        await this.initWeb3();
        await this.loadPlans();
    },
    
    methods: {
        async initWeb3() {
            const ethereumProvider = await detectEthereumProvider();
            
            if (!ethereumProvider) {
                alert('MetaMask๋ฅผ ์„ค์น˜ํ•ด์ฃผ์„ธ์š”!');
                return;
            }
            
            this.provider = new ethers.providers.Web3Provider(window.ethereum);
        },
        
        async connectWallet() {
            try {
                const accounts = await this.provider.send('eth_requestAccounts', []);
                this.account = accounts[0];
                this.signer = this.provider.getSigner();
                this.walletConnected = true;
                
                // ์ปจํŠธ๋ž™ํŠธ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
                this.contract = new ethers.Contract(
                    this.contractAddress,
                    contractABI.abi,
                    this.signer
                );
                
                await this.loadBalance();
                await this.checkSubscription();
                
                // ๊ณ„์ • ๋ณ€๊ฒฝ ๊ฐ์ง€
                window.ethereum.on('accountsChanged', (accounts) => {
                    this.account = accounts[0];
                    this.checkSubscription();
                });
                
            } catch (error) {
                console.error('์ง€๊ฐ‘ ์—ฐ๊ฒฐ ์‹คํŒจ:', error);
                alert('์ง€๊ฐ‘ ์—ฐ๊ฒฐ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
            }
        },
        
        async loadBalance() {
            const usdtContract = new ethers.Contract(
                this.usdtAddress,
                ['function balanceOf(address) view returns (uint256)'],
                this.provider
            );
            
            const balance = await usdtContract.balanceOf(this.account);
            this.balance = ethers.utils.formatUnits(balance, 6); // USDT๋Š” 6 decimals
        },
        
        async loadPlans() {
            try {
                const response = await axios.get('/api/subscription/plans');
                this.plans = response.data;
            } catch (error) {
                console.error('ํ”Œ๋žœ ๋กœ๋“œ ์‹คํŒจ:', error);
            }
        },
        
        async checkSubscription() {
            try {
                const sub = await this.contract.subscriptions(this.account);
                
                if (sub.active) {
                    this.hasSubscription = true;
                    this.currentSubscription = {
                        planId: sub.planId.toNumber(),
                        planName: await this.getPlanName(sub.planId.toNumber()),
                        startTime: sub.startTime.toNumber(),
                        endTime: sub.endTime.toNumber(),
                        autoRenew: sub.autoRenew,
                        active: sub.active
                    };
                }
            } catch (error) {
                console.error('๊ตฌ๋… ์ •๋ณด ์กฐํšŒ ์‹คํŒจ:', error);
            }
        },
        
        async getPlanName(planId) {
            const plan = await this.contract.plans(planId);
            return plan.name;
        },
        
        selectPlan(planId) {
            this.selectedPlan = planId;
        },
        
        async subscribe() {
            if (!this.selectedPlan) return;
            
            this.subscribing = true;
            
            try {
                // 1. USDT approve
                const plan = this.plans.find(p => p.id === this.selectedPlan);
                const amount = ethers.utils.parseUnits(plan.price.toString(), 6);
                
                const usdtContract = new ethers.Contract(
                    this.usdtAddress,
                    [
                        'function approve(address spender, uint256 amount) returns (bool)',
                        'function allowance(address owner, address spender) view returns (uint256)'
                    ],
                    this.signer
                );
                
                // ํ˜„์žฌ allowance ํ™•์ธ
                const currentAllowance = await usdtContract.allowance(
                    this.account,
                    this.contractAddress
                );
                
                if (currentAllowance.lt(amount)) {
                    console.log('USDT approve ์ง„ํ–‰ ์ค‘...');
                    const approveTx = await usdtContract.approve(
                        this.contractAddress,
                        amount
                    );
                    await approveTx.wait();
                    console.log('USDT approve ์™„๋ฃŒ');
                }
                
                // 2. ๊ตฌ๋… ์‹คํ–‰
                console.log('๊ตฌ๋… ์ง„ํ–‰ ์ค‘...');
                const tx = await this.contract.subscribe(this.selectedPlan);
                this.txHash = tx.hash;
                
                // ํŠธ๋žœ์žญ์…˜ ๋Œ€๊ธฐ
                const receipt = await tx.wait();
                console.log('๊ตฌ๋… ์™„๋ฃŒ:', receipt);
                
                // ์„œ๋ฒ„์— ๋™๊ธฐํ™”
                await axios.post('/api/subscription/sync', {
                    tx_hash: tx.hash,
                    wallet_address: this.account
                });
                
                // ๊ตฌ๋… ์ •๋ณด ์ƒˆ๋กœ๊ณ ์นจ
                await this.checkSubscription();
                
                alert('๊ตฌ๋…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!');
                
            } catch (error) {
                console.error('๊ตฌ๋… ์‹คํŒจ:', error);
                alert('๊ตฌ๋…์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค: ' + error.message);
            } finally {
                this.subscribing = false;
            }
        },
        
        async toggleAutoRenew() {
            try {
                const tx = await this.contract.toggleAutoRenew();
                await tx.wait();
                
                await this.checkSubscription();
                alert('์ž๋™ ๊ฐฑ์‹  ์„ค์ •์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
                
            } catch (error) {
                console.error('์„ค์ • ๋ณ€๊ฒฝ ์‹คํŒจ:', error);
                alert('์„ค์ • ๋ณ€๊ฒฝ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
            }
        },
        
        async cancelSubscription() {
            if (!confirm('์ •๋ง ๊ตฌ๋…์„ ์ทจ์†Œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?')) return;
            
            try {
                const tx = await this.contract.cancel();
                await tx.wait();
                
                this.hasSubscription = false;
                this.currentSubscription = null;
                
                alert('๊ตฌ๋…์ด ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
                
            } catch (error) {
                console.error('๊ตฌ๋… ์ทจ์†Œ ์‹คํŒจ:', error);
                alert('๊ตฌ๋… ์ทจ์†Œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
            }
        },
        
        shortenAddress(address) {
            if (!address) return '';
            return `${address.slice(0, 6)}...${address.slice(-4)}`;
        },
        
        formatDate(timestamp) {
            return new Date(timestamp * 1000).toLocaleDateString('ko-KR');
        },
        
        getEtherscanUrl(txHash) {
            const network = process.env.MIX_NETWORK === 'mainnet' ? '' : 'goerli.';
            return `https://${network}etherscan.io/tx/${txHash}`;
        }
    }
};
</script>

<style>.tx-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
color:#fff;
padding:40px 30px;
border-radius:16px;
text-align:center;
margin-bottom:40px;
box-shadow:0 8px 24px rgba(102,126,234,0.25);}
.tx-intro-box{background:#f8f9fa;
border-left:5px solid #667eea;
padding:20px;
margin:30px 0;
border-radius:8px;}
.tx-section{margin:50px 0;}
.tx-highlight{background:linear-gradient(120deg,#fef3c7 0%,#fde68a 100%);
padding:3px 8px;
border-radius:4px;
font-weight:600;}
.tx-tip-box{background:#ecfdf5;
border:2px solid #10b981;
padding:18px;
border-radius:12px;
margin:25px 0;}
.tx-warning-box{background:#fef2f2;
border:2px solid #ef4444;
padding:18px;
border-radius:12px;
margin:25px 0;}
.tx-info-box{background:#eff6ff;
border:2px solid #3b82f6;
padding:18px;
border-radius:12px;
margin:25px 0;}
.tx-step-box{background:#fff;
border:2px solid #e5e7eb;
padding:20px;
border-radius:12px;
margin:20px 0;
box-shadow:0 2px 8px rgba(0,0,0,0.05);}
.tx-table-wrap{overflow-x:auto;
margin:25px 0;}
.tx-table{width:100%;
border-collapse:collapse;
background:#fff;
border-radius:8px;
overflow:hidden;
box-shadow:0 2px 8px rgba(0,0,0,0.05);}
.tx-table th{background:#667eea;
color:#fff;
padding:12px;
text-align:left;
font-weight:600;}
.tx-table td{padding:12px;
border-bottom:1px solid #e5e7eb;}
.tx-table tr:last-child td{border-bottom:none;}
.tx-subtitle{color:#667eea;
font-weight:700;
font-size:1.3em;
margin:35px 0 20px 0;
padding-bottom:10px;
border-bottom:3px solid #667eea;}
.tx-point{color:#764ba2;
font-weight:600;}
.tx-emoji{font-size:1.3em;
margin:0 5px;}</style>

๐Ÿ”„ ๋ธ”๋ก์ฒด์ธ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹ ๋ฐ ๋™๊ธฐํ™”

๋ธ”๋ก์ฒด์ธ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ฐ์ง€ํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋™๊ธฐํ™”ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. โšก Laravel์˜ Job๊ณผ Queue๋ฅผ ํ™œ์šฉํ•˜๋ฉด ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// app/Jobs/SyncBlockchainEvents.php
<?php

namespace App\Jobs;

use App\Models\Subscription;
use App\Models\SubscriptionTransaction;
use App\Services\Web3Service;
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 SyncBlockchainEvents implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    private $web3Service;
    
    public function __construct()
    {
        $this->web3Service = new Web3Service();
    }
    
    public function handle()
    {
        try {
            // ๋งˆ์ง€๋ง‰ ๋™๊ธฐํ™”๋œ ๋ธ”๋ก ๋ฒˆํ˜ธ ๊ฐ€์ ธ์˜ค๊ธฐ
            $lastBlock = cache('last_synced_block', 0);
            $currentBlock = $this->web3Service->getCurrentBlockNumber();
            
            Log::info("Syncing blocks from {$lastBlock} to {$currentBlock}");
            
            // ์ด๋ฒคํŠธ ๋กœ๊ทธ ๊ฐ€์ ธ์˜ค๊ธฐ
            $events = $this->web3Service->getContractEvents($lastBlock + 1, $currentBlock);
            
            foreach ($events as $event) {
                $this->processEvent($event);
            }
            
            // ๋งˆ์ง€๋ง‰ ๋ธ”๋ก ๋ฒˆํ˜ธ ์บ์‹œ ์—…๋ฐ์ดํŠธ
            cache(['last_synced_block' => $currentBlock], now()->addDays(7));
            
            Log::info("Sync completed. Processed " . count($events) . " events");
            
        } catch (\Exception $e) {
            Log::error("Blockchain sync failed: " . $e->getMessage());
            throw $e;
        }
    }
    
    private function processEvent(array $event)
    {
        $eventName = $event['event'];
        
        switch ($eventName) {
            case 'Subscribed':
                $this->handleSubscribed($event);
                break;
                
            case 'Renewed':
                $this->handleRenewed($event);
                break;
                
            case 'Cancelled':
                $this->handleCancelled($event);
                break;
                
            case 'AutoRenewToggled':
                $this->handleAutoRenewToggled($event);
                break;
        }
    }
    
    private function handleSubscribed(array $event)
    {
        $userAddress = $event['args']['user'];
        $planId = $event['args']['planId'];
        $endTime = $event['args']['endTime'];
        
        // ์‚ฌ์šฉ์ž ์ฐพ๊ธฐ ๋˜๋Š” ์ƒ์„ฑ
        $user = User::firstOrCreate(
            ['wallet_address' => strtolower($userAddress)],
            ['name' => 'User ' . substr($userAddress, 0, 8)]
        );
        
        // ๊ตฌ๋… ์ƒ์„ฑ
        $subscription = Subscription::create([
            'user_id' => $user->id,
            'wallet_address' => strtolower($userAddress),
            'plan_id' => $planId,
            'start_time' => now(),
            'end_time' => \Carbon\Carbon::createFromTimestamp($endTime),
            'auto_renew' => true,
            'active' => true,
            'tx_hash' => $event['transactionHash']
        ]);
        
        // ํŠธ๋žœ์žญ์…˜ ๊ธฐ๋ก
        SubscriptionTransaction::create([
            'subscription_id' => $subscription->id,
            'tx_hash' => $event['transactionHash'],
            'type' => 'subscribe',
            'amount' => $this->getPlanPrice($planId),
            'block_number' => $event['blockNumber'],
            'block_timestamp' => now(),
            'status' => 'confirmed'
        ]);
        
        Log::info("New subscription created for {$userAddress}");
    }
    
    private function handleRenewed(array $event)
    {
        $userAddress = $event['args']['user'];
        $planId = $event['args']['planId'];
        $newEndTime = $event['args']['newEndTime'];
        
        $subscription = Subscription::where('wallet_address', strtolower($userAddress))
            ->where('active', true)
            ->first();
        
        if ($subscription) {
            $subscription->update([
                'end_time' => \Carbon\Carbon::createFromTimestamp($newEndTime)
            ]);
            
            SubscriptionTransaction::create([
                'subscription_id' => $subscription->id,
                'tx_hash' => $event['transactionHash'],
                'type' => 'renew',
                'amount' => $this->getPlanPrice($planId),
                'block_number' => $event['blockNumber'],
                'block_timestamp' => now(),
                'status' => 'confirmed'
            ]);
            
            Log::info("Subscription renewed for {$userAddress}");
        }
    }
    
    private function handleCancelled(array $event)
    {
        $userAddress = $event['args']['user'];
        
        $subscription = Subscription::where('wallet_address', strtolower($userAddress))
            ->where('active', true)
            ->first();
        
        if ($subscription) {
            $subscription->update([
                'active' => false,
                'auto_renew' => false
            ]);
            
            SubscriptionTransaction::create([
                'subscription_id' => $subscription->id,
                'tx_hash' => $event['transactionHash'],
                'type' => 'cancel',
                'amount' => '0',
                'block_number' => $event['blockNumber'],
                'block_timestamp' => now(),
                'status' => 'confirmed'
            ]);
            
            Log::info("Subscription cancelled for {$userAddress}");
        }
    }
    
    private function handleAutoRenewToggled(array $event)
    {
        $userAddress = $event['args']['user'];
        $autoRenew = $event['args']['autoRenew'];
        
        $subscription = Subscription::where('wallet_address', strtolower($userAddress))
            ->where('active', true)
            ->first();
        
        if ($subscription) {
            $subscription->update(['auto_renew' => $autoRenew]);
            Log::info("Auto-renew toggled for {$userAddress}: " . ($autoRenew ? 'ON' : 'OFF'));
        }
    }
    
    private function getPlanPrice(int $planId): string
    {
        $plan = $this->web3Service->getPlan($planId);
        return $plan['price'];
    }
}

์Šค์ผ€์ค„๋Ÿฌ์— ๋“ฑ๋กํ•˜์—ฌ ์ฃผ๊ธฐ์ ์œผ๋กœ ์‹คํ–‰:

// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
    // 5๋ถ„๋งˆ๋‹ค ๋ธ”๋ก์ฒด์ธ ์ด๋ฒคํŠธ ๋™๊ธฐํ™”
    $schedule->job(new SyncBlockchainEvents())
             ->everyFiveMinutes()
             ->withoutOverlapping();
    
    // ๋งค์ผ ์ž์ •์— ๋งŒ๋ฃŒ๋œ ๊ตฌ๋… ์ •๋ฆฌ
    $schedule->call(function () {
        Subscription::expired()
                   ->where('active', true)
                   ->update(['active' => false]);
    })->daily();
    
    // ๋งŒ๋ฃŒ 3์ผ ์ „ ์•Œ๋ฆผ
    $schedule->call(function () {
        $expiringSoon = Subscription::active()
            ->whereBetween('end_time', [now(), now()->addDays(3)])
            ->get();
        
        foreach ($expiringSoon as $subscription) {
            // ์ด๋ฉ”์ผ ๋˜๋Š” ํ‘ธ์‹œ ์•Œ๋ฆผ ๋ฐœ์†ก
            Mail::to($subscription->user->email)
                ->send(new SubscriptionExpiringNotification($subscription));
        }
    })->daily();
}

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

๋ธ”๋ก์ฒด์ธ ๊ฒฐ์ œ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ๋•Œ ๋ฐ˜๋“œ์‹œ ๊ณ ๋ คํ•ด์•ผ ํ•  ๋ณด์•ˆ ์‚ฌํ•ญ๋“ค์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ”

โš ๏ธ ์ค‘์š”ํ•œ ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

1. ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๋ณด์•ˆ
โ€ข Reentrancy ๊ณต๊ฒฉ ๋ฐฉ์ง€ (ReentrancyGuard ์‚ฌ์šฉ)
โ€ข Integer overflow/underflow ๋ฐฉ์ง€ (Solidity 0.8+ ์ž๋™ ์ฒดํฌ)
โ€ข Access control ์ฒ ์ €ํžˆ (Ownable, Role-based)
โ€ข ์™ธ๋ถ€ ๊ฐ์‚ฌ ๋ฐ›๊ธฐ (CertiK, OpenZeppelin ๋“ฑ)

2. ํ”„๋ผ์ด๋น— ํ‚ค ๊ด€๋ฆฌ
โ€ข ์ ˆ๋Œ€ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉ ๊ธˆ์ง€
โ€ข ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์‚ฌ์šฉ (.env)
โ€ข AWS KMS, HashiCorp Vault ๊ฐ™์€ ํ‚ค ๊ด€๋ฆฌ ์„œ๋น„์Šค ํ™œ์šฉ
โ€ข ์„œ๋ฒ„ ์ง€๊ฐ‘์€ ์ตœ์†Œ ๊ธˆ์•ก๋งŒ ๋ณด์œ 

3. ํ”„๋ก ํŠธ์—”๋“œ ๋ณด์•ˆ
โ€ข ์‚ฌ์šฉ์ž ์„œ๋ช… ๊ฒ€์ฆ
โ€ข ํŠธ๋žœ์žญ์…˜ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ
โ€ข Phishing ๋ฐฉ์ง€ (๋„๋ฉ”์ธ ํ™•์ธ)
โ€ข Rate limiting ์ ์šฉ

4. ๋ฐฑ์—”๋“œ ๋ณด์•ˆ
โ€ข API ์ธ์ฆ/์ธ๊ฐ€ (JWT, OAuth)
โ€ข CSRF ํ† ํฐ ์‚ฌ์šฉ
โ€ข SQL Injection ๋ฐฉ์ง€
โ€ข ๋ฏผ๊ฐ ๋ฐ์ดํ„ฐ ์•”ํ˜ธํ™”

์ง€๊ฐ‘ ์„œ๋ช… ๊ฒ€์ฆ ๋ฏธ๋“ค์›จ์–ด ์˜ˆ์‹œ:

// app/Http/Middleware/VerifyWalletSignature.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Elliptic\EC;
use kornrunner\Keccak;

class VerifyWalletSignature
{
    public function handle(Request $request, Closure $next)
    {
        $signature = $request->header('X-Wallet-Signature');
        $message = $request->header('X-Wallet-Message');
        $address = $request->header('X-Wallet-Address');
        
        if (!$signature || !$message || !$address) {
            return response()->json(['error' => 'Missing signature headers'], 401);
        }
        
        // ์„œ๋ช… ๊ฒ€์ฆ
        if (!$this->verifySignature($message, $signature, $address)) {
            return response()->json(['error' => 'Invalid signature'], 401);
        }
        
        // ํƒ€์ž„์Šคํƒฌํ”„ ๊ฒ€์ฆ (์žฌ์ƒ ๊ณต๊ฒฉ ๋ฐฉ์ง€)
        $timestamp = json_decode($message, true)['timestamp'] ?? 0;
        if (abs(time() - $timestamp) > 300) { // 5๋ถ„ ์ด๋‚ด
            return response()->json(['error' => 'Signature expired'], 401);
        }
        
        // ์š”์ฒญ์— ์ง€๊ฐ‘ ์ฃผ์†Œ ์ถ”๊ฐ€
        $request->merge(['verified_wallet' => strtolower($address)]);
        
        return $next($request);
    }
    
    private function verifySignature(string $message, string $signature, string $address): bool
    {
        // ์ด๋”๋ฆฌ์›€ ์„œ๋ช… ๋ฉ”์‹œ์ง€ ํ˜•์‹
        $hash = Keccak::hash("\x19Ethereum Signed Message:\n" . strlen($message) . $message, 256);
        
        // ์„œ๋ช…์—์„œ r, s, v ์ถ”์ถœ
        $r = substr($signature, 2, 64);
        $s = substr($signature, 66, 64);
        $v = hexdec(substr($signature, 130, 2));
        
        if ($v < 27) {
            $v += 27;
        }
        
        $ec = new EC('secp256k1');
        $pubkey = $ec->recoverPubKey($hash, [
            'r' => $r,
            's' => $s
        ], $v - 27);
        
        $recoveredAddress = '0x' . substr(Keccak::hash(substr(hex2bin($pubkey->encode('hex')), 1), 256), 24);
        
        return strtolower($recoveredAddress) === strtolower($address);
    }
}

๐Ÿ“Š ๋Œ€์‹œ๋ณด๋“œ ๋ฐ ๋ถ„์„

๊ด€๋ฆฌ์ž์™€ ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๋งŒ๋“ค์–ด๋ด…์‹œ๋‹ค. ๐Ÿ“ˆ ๊ตฌ๋… ํ˜„ํ™ฉ, ์ˆ˜์ต, ์‚ฌ์šฉ์ž ํ†ต๊ณ„ ๋“ฑ์„ ํ•œ๋ˆˆ์— ๋ณผ ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

// app/Http/Controllers/DashboardController.php
<?php

namespace App\Http\Controllers;

use App\Models\Subscription;
use App\Models\SubscriptionTransaction;
use App\Models\SubscriptionPlan;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;

class DashboardController extends Controller
{
    /**
     * ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ
     */
    public function admin()
    {
        $stats = [
            'total_subscriptions' => Subscription::count(),
            'active_subscriptions' => Subscription::active()->count(),
            'total_revenue' => $this->calculateTotalRevenue(),
            'monthly_revenue' => $this->calculateMonthlyRevenue(),
            'churn_rate' => $this->calculateChurnRate(),
            'mrr' => $this->calculateMRR(), // Monthly Recurring Revenue
            'arr' => $this->calculateARR(), // Annual Recurring Revenue
        ];
        
        // ํ”Œ๋žœ๋ณ„ ๊ตฌ๋… ๋ถ„ํฌ
        $planDistribution = Subscription::active()
            ->select('plan_id', DB::raw('count(*) as count'))
            ->groupBy('plan_id')
            ->with('plan')
            ->get();
        
        // ์›”๋ณ„ ์‹ ๊ทœ ๊ตฌ๋… ์ถ”์ด
        $monthlyGrowth = Subscription::select(
                DB::raw('DATE_FORMAT(created_at, "%Y-%m") as month'),
                DB::raw('count(*) as count')
            )
            ->where('created_at', '>=', now()->subMonths(12))
            ->groupBy('month')
            ->orderBy('month')
            ->get();
        
        // ์ตœ๊ทผ ํŠธ๋žœ์žญ์…˜
        $recentTransactions = SubscriptionTransaction::with(['subscription.user', 'subscription.plan'])
            ->orderBy('created_at', 'desc')
            ->limit(10)
            ->get();
        
        return view('dashboard.admin', compact(
            'stats',
            'planDistribution',
            'monthlyGrowth',
            'recentTransactions'
        ));
    }
    
    /**
     * ์‚ฌ์šฉ์ž ๋Œ€์‹œ๋ณด๋“œ
     */
    public function user(Request $request)
    {
        $user = $request->user();
        
        $subscription = Subscription::where('user_id', $user->id)
            ->where('active', true)
            ->with('plan')
            ->first();
        
        if (!$subscription) {
            return view('dashboard.user', [
                'has_subscription' => false
            ]);
        }
        
        $transactions = SubscriptionTransaction::where('subscription_id', $subscription->id)
            ->orderBy('created_at', 'desc')
            ->get();
        
        $stats = [
            'days_remaining' => $subscription->daysRemaining(),
            'total_spent' => $transactions->where('type', '!=', 'cancel')->sum('amount'),
            'renewal_count' => $transactions->where('type', 'renew')->count(),
            'is_expiring_soon' => $subscription->isExpiringSoon(),
        ];
        
        return view('dashboard.user', compact(
            'subscription',
            'transactions',
            'stats'
        ));
    }
    
    /**
     * ์ด ์ˆ˜์ต ๊ณ„์‚ฐ
     */
    private function calculateTotalRevenue(): float
    {
        return SubscriptionTransaction::where('status', 'confirmed')
            ->where('type', '!=', 'cancel')
            ->sum(DB::raw('CAST(amount AS DECIMAL(20,6))'));
    }
    
    /**
     * ์›”๊ฐ„ ์ˆ˜์ต ๊ณ„์‚ฐ
     */
    private function calculateMonthlyRevenue(): float
    {
        return SubscriptionTransaction::where('status', 'confirmed')
            ->where('type', '!=', 'cancel')
            ->whereMonth('created_at', now()->month)
            ->whereYear('created_at', now()->year)
            ->sum(DB::raw('CAST(amount AS DECIMAL(20,6))'));
    }
    
    /**
     * ์ดํƒˆ๋ฅ  ๊ณ„์‚ฐ
     */
    private function calculateChurnRate(): float
    {
        $startOfMonth = now()->startOfMonth();
        $endOfMonth = now()->endOfMonth();
        
        $activeAtStart = Subscription::where('active', true)
            ->where('created_at', '<', $startOfMonth)
            ->count();
        
        if ($activeAtStart === 0) {
            return 0;
        }
        
        $cancelled = Subscription::where('active', false)
            ->whereBetween('updated_at', [$startOfMonth, $endOfMonth])
            ->count();
        
        return ($cancelled / $activeAtStart) * 100;
    }
    
    /**
     * MRR (Monthly Recurring Revenue) ๊ณ„์‚ฐ
     */
    private function calculateMRR(): float
    {
        $activeSubscriptions = Subscription::active()->with('plan')->get();
        
        $mrr = 0;
        foreach ($activeSubscriptions as $sub) {
            $plan = $sub->plan;
            if ($plan) {
                // ํ”Œ๋žœ ๊ฐ€๊ฒฉ์„ ์›” ๋‹จ์œ„๋กœ ์ •๊ทœํ™”
                $monthlyPrice = $plan->price * (30 * 24 * 60 * 60 / $plan->duration);
                $mrr += $monthlyPrice;
            }
        }
        
        return $mrr;
    }
    
    /**
     * ARR (Annual Recurring Revenue) ๊ณ„์‚ฐ
     */
    private function calculateARR(): float
    {
        return $this->calculateMRR() * 12;
    }
    
    /**
     * API: ๊ตฌ๋… ํ†ต๊ณ„
     */
    public function stats(Request $request)
    {
        $period = $request->input('period', '30d'); // 30d, 90d, 1y
        
        $startDate = match($period) {
            '30d' => now()->subDays(30),
            '90d' => now()->subDays(90),
            '1y' => now()->subYear(),
            default => now()->subDays(30)
        };
        
        $data = [
            'new_subscriptions' => Subscription::where('created_at', '>=', $startDate)->count(),
            'renewals' => SubscriptionTransaction::where('type', 'renew')
                ->where('created_at', '>=', $startDate)
                ->count(),
            'cancellations' => SubscriptionTransaction::where('type', 'cancel')
                ->where('created_at', '>=', $startDate)
                ->count(),
            'revenue' => SubscriptionTransaction::where('status', 'confirmed')
                ->where('type', '!=', 'cancel')
                ->where('created_at', '>=', $startDate)
                ->sum(DB::raw('CAST(amount AS DECIMAL(20,6))')),
        ];
        
        return response()->json($data);
    }
}
๊ตฌ๋… ์ˆ˜์ต ๋Œ€์‹œ๋ณด๋“œ ํ™œ์„ฑ ๊ตฌ๋… 1,247 โ†‘ 12.5% vs ์ง€๋‚œ๋‹ฌ ์›”๊ฐ„ ์ˆ˜์ต (MRR) $24.8K โ†‘ 8.3% vs ์ง€๋‚œ๋‹ฌ ์—ฐ๊ฐ„ ์ˆ˜์ต (ARR) $297K โ†‘ 15.2% vs ์ž‘๋…„ ์ดํƒˆ๋ฅ  3.2% โ†“ 0.8% ๊ฐœ์„  ์›”๋ณ„ ์‹ ๊ทœ ๊ตฌ๋… ์ถ”์ด 1์›” 3์›” 5์›” 7์›” 9์›” 10์›”

๐Ÿ”ง ์ž๋™ ๊ฐฑ์‹  ์‹œ์Šคํ…œ ๊ตฌํ˜„

๊ตฌ๋… ์„œ๋น„์Šค์˜ ํ•ต์‹ฌ์€ ์ž๋™ ๊ฐฑ์‹ ์ž…๋‹ˆ๋‹ค. ๐Ÿ”„ ์‚ฌ์šฉ์ž๊ฐ€ ๋ณ„๋„ ์•ก์…˜ ์—†์ด๋„ ๊ตฌ๋…์ด ์ž๋™์œผ๋กœ ์—ฐ์žฅ๋˜์–ด์•ผ ํ•˜์ฃ . ํ•˜์ง€๋งŒ ๋ธ”๋ก์ฒด์ธ์—์„œ๋Š” ์ž๋™ ์‹คํ–‰์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ, ๋ฐฑ์—”๋“œ์—์„œ ํŠธ๋ฆฌ๊ฑฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’ก ์ž๋™ ๊ฐฑ์‹  ๋ฉ”์ปค๋‹ˆ์ฆ˜

๋ธ”๋ก์ฒด์ธ์€ ์™ธ๋ถ€์—์„œ ํŠธ๋ฆฌ๊ฑฐํ•˜์ง€ ์•Š์œผ๋ฉด ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค:

1. ์‚ฌ์šฉ์ž๊ฐ€ ๊ตฌ๋… ์‹œ ํ† ํฐ approve๋ฅผ ์ถฉ๋ถ„ํžˆ ํ•ด๋‘ 
2. ๋ฐฑ์—”๋“œ ํฌ๋ก ์žก์ด ๋งŒ๋ฃŒ ์ž„๋ฐ• ๊ตฌ๋…์„ ๊ฐ์ง€
3. ๋ฐฑ์—”๋“œ๊ฐ€ ์‚ฌ์šฉ์ž ๋Œ€์‹  renew() ํ•จ์ˆ˜ ํ˜ธ์ถœ
4. ๊ฐ€์Šค๋น„๋Š” ์„œ๋น„์Šค ์ œ๊ณต์ž๊ฐ€ ๋ถ€๋‹ด (๋˜๋Š” ๊ตฌ๋…๋ฃŒ์— ํฌํ•จ)
// app/Jobs/ProcessAutoRenewals.php
<?php

namespace App\Jobs;

use App\Models\Subscription;
use App\Services\Web3Service;
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;
use Web3\Contract;

class ProcessAutoRenewals implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    public $timeout = 300; // 5๋ถ„
    
    public function handle()
    {
        // 24์‹œ๊ฐ„ ์ด๋‚ด ๋งŒ๋ฃŒ ์˜ˆ์ •์ด๊ณ  ์ž๋™ ๊ฐฑ์‹ ์ด ์ผœ์ง„ ๊ตฌ๋… ์ฐพ๊ธฐ
        $expiringSubscriptions = Subscription::active()
            ->where('auto_renew', true)
            ->whereBetween('end_time', [now(), now()->addDay()])
            ->get();
        
        Log::info("Found {$expiringSubscriptions->count()} subscriptions to renew");
        
        foreach ($expiringSubscriptions as $subscription) {
            try {
                $this->renewSubscription($subscription);
            } catch (\Exception $e) {
                Log::error("Failed to renew subscription {$subscription->id}: " . $e->getMessage());
                
                // ์‹คํŒจ ์•Œ๋ฆผ ๋ฐœ์†ก
                $this->notifyRenewalFailure($subscription, $e->getMessage());
            }
        }
    }
    
    private function renewSubscription(Subscription $subscription)
    {
        $web3Service = new Web3Service();
        
        // 1. ์‚ฌ์šฉ์ž์˜ ํ† ํฐ allowance ํ™•์ธ
        $allowance = $web3Service->checkAllowance(
            $subscription->wallet_address,
            config('web3.contract_address')
        );
        
        $planPrice = $web3Service->getPlan($subscription->plan_id)['price'];
        
        if (bccomp($allowance, $planPrice) < 0) {
            throw new \Exception("Insufficient allowance. User needs to approve more tokens.");
        }
        
        // 2. ์„œ๋ฒ„ ์ง€๊ฐ‘์œผ๋กœ renew ํŠธ๋žœ์žญ์…˜ ์ „์†ก
        $txHash = $web3Service->renewSubscriptionForUser($subscription->wallet_address);
        
        Log::info("Renewal transaction sent: {$txHash}");
        
        // 3. ํŠธ๋žœ์žญ์…˜ ๊ธฐ๋ก
        SubscriptionTransaction::create([
            'subscription_id' => $subscription->id,
            'tx_hash' => $txHash,
            'type' => 'renew',
            'amount' => $planPrice,
            'block_number' => 0, // ๋‚˜์ค‘์— ์—…๋ฐ์ดํŠธ
            'block_timestamp' => now(),
            'status' => 'pending'
        ]);
        
        // 4. ํŠธ๋žœ์žญ์…˜ ํ™•์ธ Job ํ์— ์ถ”๊ฐ€
        ConfirmTransaction::dispatch($txHash, $subscription->id)
            ->delay(now()->addMinutes(2));
    }
    
    private function notifyRenewalFailure(Subscription $subscription, string $reason)
    {
        // ์‚ฌ์šฉ์ž์—๊ฒŒ ์ด๋ฉ”์ผ ๋ฐœ์†ก
        Mail::to($subscription->user->email)
            ->send(new RenewalFailedNotification($subscription, $reason));
        
        // ์ž๋™ ๊ฐฑ์‹  ๋น„ํ™œ์„ฑํ™”
        $subscription->update(['auto_renew' => false]);
    }
}

Web3Service์— ๊ฐฑ์‹  ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€:

// app/Services/Web3Service.php ์— ์ถ”๊ฐ€

use Web3\Personal;

/**
 * ์„œ๋ฒ„ ์ง€๊ฐ‘์œผ๋กœ ์‚ฌ์šฉ์ž ๊ตฌ๋… ๊ฐฑ์‹ 
 */
public function renewSubscriptionForUser(string $userAddress): string
{
    $serverAddress = config('web3.server_wallet_address');
    $privateKey = config('web3.server_private_key');
    
    // ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
    $contract = new Contract($this->web3->provider, $this->abi);
    $contract->at($this->contractAddress);
    
    // renew() ํ•จ์ˆ˜ ์ธ์ฝ”๋”ฉ
    $data = $contract->getData('renew');
    
    // ๊ฐ€์Šค ์ถ”์ •
    $gasEstimate = null;
    $this->web3->eth->estimateGas([
        'from' => $serverAddress,
        'to' => $this->contractAddress,
        'data' => $data
    ], function ($err, $gas) use (&$gasEstimate) {
        if ($err !== null) {
            throw new \Exception('Gas estimation failed: ' . $err->getMessage());
        }
        $gasEstimate = $gas;
    });
    
    // Nonce ๊ฐ€์ ธ์˜ค๊ธฐ
    $nonce = null;
    $this->web3->eth->getTransactionCount($serverAddress, 'pending', function ($err, $count) use (&$nonce) {
        if ($err !== null) {
            throw new \Exception('Failed to get nonce: ' . $err->getMessage());
        }
        $nonce = $count;
    });
    
    // ํŠธ๋žœ์žญ์…˜ ์„œ๋ช… ๋ฐ ์ „์†ก
    $transaction = [
        'from' => $serverAddress,
        'to' => $this->contractAddress,
        'gas' => '0x' . dechex($gasEstimate->toHex()),
        'gasPrice' => '0x' . dechex(20000000000), // 20 Gwei
        'nonce' => '0x' . dechex($nonce->toHex()),
        'data' => $data,
        'chainId' => config('web3.chain_id')
    ];
    
    // ์„œ๋ช…
    $signedTx = $this->signTransaction($transaction, $privateKey);
    
    // ์ „์†ก
    $txHash = null;
    $this->web3->eth->sendRawTransaction('0x' . $signedTx, function ($err, $hash) use (&$txHash) {
        if ($err !== null) {
            throw new \Exception('Transaction failed: ' . $err->getMessage());
        }
        $txHash = $hash;
    });
    
    return $txHash;
}

/**
 * ํŠธ๋žœ์žญ์…˜ ์„œ๋ช…
 */
private function signTransaction(array $transaction, string $privateKey): string
{
    // Ethereum ํŠธ๋žœ์žญ์…˜ ์„œ๋ช… ๋กœ์ง
    // elliptic-php ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ
    
    $ec = new \Elliptic\EC('secp256k1');
    $key = $ec->keyFromPrivate($privateKey);
    
    // RLP ์ธ์ฝ”๋”ฉ ๋ฐ ์„œ๋ช…
    // ... (๋ณต์žกํ•œ ์„œ๋ช… ๋กœ์ง)
    
    return $signedTransaction;
}
๐Ÿ’ก ๊ฐ€์Šค๋น„ ์ตœ์ ํ™” ํŒ

์ž๋™ ๊ฐฑ์‹  ์‹œ ์„œ๋น„์Šค ์ œ๊ณต์ž๊ฐ€ ๊ฐ€์Šค๋น„๋ฅผ ๋ถ€๋‹ดํ•˜๋ฏ€๋กœ, ์ตœ์ ํ™”๊ฐ€ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค:

โ€ข ๊ฐ€์Šค ๊ฐ€๊ฒฉ์ด ๋‚ฎ์€ ์‹œ๊ฐ„๋Œ€์— ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ (์ฃผ๋ง, ์ƒˆ๋ฒฝ)
โ€ข ์—ฌ๋Ÿฌ ๊ฐฑ์‹ ์„ ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ฌถ๊ธฐ (batch renewal)
โ€ข Layer 2 ์†”๋ฃจ์…˜ ํ™œ์šฉ (Polygon, Arbitrum ๋“ฑ)
โ€ข EIP-1559 ํ™œ์šฉํ•˜์—ฌ ์ ์ • ๊ฐ€์Šค๋น„ ์„ค์ •

๐ŸŒ ๋ฉ€ํ‹ฐ์ฒด์ธ ์ง€์›

์ด๋”๋ฆฌ์›€ ๋ฉ”์ธ๋„ท๋งŒ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ€์Šค๋น„๊ฐ€ ๋„ˆ๋ฌด ๋น„์Œ‰๋‹ˆ๋‹ค. ๐Ÿ’ธ ์—ฌ๋Ÿฌ ์ฒด์ธ์„ ์ง€์›ํ•˜๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ์„ ํƒํ•  ์ˆ˜ ์žˆ์–ด ์ข‹์Šต๋‹ˆ๋‹ค!

์ฒด์ธ ์žฅ์  ํ‰๊ท  ๊ฐ€์Šค๋น„ TPS
Ethereum ๊ฐ€์žฅ ์•ˆ์ „ํ•˜๊ณ  ํƒˆ์ค‘์•™ํ™” $5-50 15-30
Polygon ์ €๋ ดํ•˜๊ณ  ๋น ๋ฆ„, EVM ํ˜ธํ™˜ $0.01-0.1 7,000+
BSC ์ €๋ ด, ํฐ ์ƒํƒœ๊ณ„ $0.1-0.5 160
Arbitrum ์ด๋”๋ฆฌ์›€ L2, ์ €๋ ด $0.1-1 4,000+
Optimism ์ด๋”๋ฆฌ์›€ L2, ์•ˆ์ •์  $0.1-1 2,000+

๋ฉ€ํ‹ฐ์ฒด์ธ ์„ค์ •:

// config/chains.php
<?php

return [
    'ethereum' => [
        'chain_id' => 1,
        'name' => 'Ethereum Mainnet',
        'rpc_url' => env('ETHEREUM_RPC_URL'),
        'explorer' => 'https://etherscan.io',
        'contract_address' => env('ETHEREUM_CONTRACT_ADDRESS'),
        'usdt_address' => '0xdAC17F958D2ee523a2206206994597C13D831ec7',
    ],
    
    'polygon' => [
        'chain_id' => 137,
        'name' => 'Polygon',
        'rpc_url' => env('POLYGON_RPC_URL'),
        'explorer' => 'https://polygonscan.com',
        'contract_address' => env('POLYGON_CONTRACT_ADDRESS'),
        'usdt_address' => '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
    ],
    
    'bsc' => [
        'chain_id' => 56,
        'name' => 'BNB Smart Chain',
        'rpc_url' => env('BSC_RPC_URL'),
        'explorer' => 'https://bscscan.com',
        'contract_address' => env('BSC_CONTRACT_ADDRESS'),
        'usdt_address' => '0x55d398326f99059fF775485246999027B3197955',
    ],
    
    'arbitrum' => [
        'chain_id' => 42161,
        'name' => 'Arbitrum One',
        'rpc_url' => env('ARBITRUM_RPC_URL'),
        'explorer' => 'https://arbiscan.io',
        'contract_address' => env('ARBITRUM_CONTRACT_ADDRESS'),
        'usdt_address' => '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9',
    ],
];

์ฒด์ธ๋ณ„ Web3Service ํŒฉํ† ๋ฆฌ:

// app/Services/Web3ServiceFactory.php
<?php

namespace App\Services;

class Web3ServiceFactory
{
    public static function create(string $chain): Web3Service
    {
        $config = config("chains.{$chain}");
        
        if (!$config) {
            throw new \InvalidArgumentException("Unsupported chain: {$chain}");
        }
        
        return new Web3Service(
            $config['rpc_url'],
            $config['contract_address'],
            $config['chain_id']
        );
    }
    
    public static function getSupportedChains(): array
    {
        return array_keys(config('chains'));
    }
}

๐Ÿงช ํ…Œ์ŠคํŠธ ์ž‘์„ฑ

๋ธ”๋ก์ฒด์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ํ…Œ์ŠคํŠธ๊ฐ€ ๋งค์šฐ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ”ฌ ํ•œ ๋ฒˆ ๋ฐฐํฌํ•˜๋ฉด ์ˆ˜์ •์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ˆ๊นŒ์š”!

// tests/Feature/SubscriptionTest.php
<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use App\Models\Subscription;
use App\Models\SubscriptionPlan;
use App\Services\Web3Service;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;

class SubscriptionTest extends TestCase
{
    use RefreshDatabase;
    
    protected function setUp(): void
    {
        parent::setUp();
        
        // ํ…Œ์ŠคํŠธ ํ”Œ๋žœ ์ƒ์„ฑ
        SubscriptionPlan::create([
            'chain_plan_id' => 1,
            'name' => 'Basic Plan',
            'price' => '9990000', // 9.99 USDT (6 decimals)
            'duration' => 2592000, // 30 days
            'active' => true,
            'features' => json_encode(['Feature 1', 'Feature 2'])
        ]);
    }
    
    /** @test */
    public function user_can_subscribe_to_plan()
    {
        $user = User::factory()->create([
            'wallet_address' => '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'
        ]);
        
        // Web3Service ๋ชจํ‚น
        $web3Mock = Mockery::mock(Web3Service::class);
        $web3Mock->shouldReceive('checkSubscription')
            ->andReturn([
                'plan_id' => '1',
                'start_time' => time(),
                'end_time' => time() + 2592000,
                'auto_renew' => true,
                'active' => true
            ]);
        
        $this->app->instance(Web3Service::class, $web3Mock);
        
        // API ํ˜ธ์ถœ
        $response = $this->actingAs($user)
            ->postJson('/api/subscription/sync', [
                'tx_hash' => '0xabc123...',
                'wallet_address' => $user->wallet_address
            ]);
        
        $response->assertStatus(200);
        
        // DB ํ™•์ธ
        $this->assertDatabaseHas('subscriptions', [
            'user_id' => $user->id,
            'wallet_address' => strtolower($user->wallet_address),
            'plan_id' => 1,
            'active' => true
        ]);
    }
    
    /** @test */
    public function subscription_expires_after_end_time()
    {
        $user = User::factory()->create();
        
        $subscription = Subscription::create([
            'user_id' => $user->id,
            'wallet_address' => '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
            'plan_id' => 1,
            'start_time' => now()->subDays(31),
            'end_time' => now()->subDay(),
            'auto_renew' => false,
            'active' => true
        ]);
        
        $this->assertFalse($subscription->isValid());
        $this->assertEquals(0, $subscription->daysRemaining());
    }
    
    /** @test */
    public function auto_renewal_processes_expiring_subscriptions()
    {
        $subscription = Subscription::create([
            'user_id' => User::factory()->create()->id,
            'wallet_address' => '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
            'plan_id' => 1,
            'start_time' => now()->subDays(29),
            'end_time' => now()->addHours(12), // 12์‹œ๊ฐ„ ํ›„ ๋งŒ๋ฃŒ
            'auto_renew' => true,
            'active' => true
        ]);
        
        // Web3Service ๋ชจํ‚น
        $web3Mock = Mockery::mock(Web3Service::class);
        $web3Mock->shouldReceive('checkAllowance')
            ->andReturn('100000000'); // ์ถฉ๋ถ„ํ•œ allowance
        $web3Mock->shouldReceive('renewSubscriptionForUser')
            ->andReturn('0xtxhash123');
        
        $this->app->instance(Web3Service::class, $web3Mock);
        
        // Job ์‹คํ–‰
        $job = new \App\Jobs\ProcessAutoRenewals();
        $job->handle();
        
        // ํŠธ๋žœ์žญ์…˜ ๊ธฐ๋ก ํ™•์ธ
        $this->assertDatabaseHas('subscription_transactions', [
            'subscription_id' => $subscription->id,
            'type' => 'renew',
            'status' => 'pending'
        ]);
    }
    
    /** @test */
    public function dashboard_shows_correct_statistics()
    {
        $admin = User::factory()->create(['is_admin' => true]);
        
        // ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
        Subscription::factory()->count(10)->create(['active' => true]);
        Subscription::factory()->count(3)->create(['active' => false]);
        
        $response = $this->actingAs($admin)
            ->get('/dashboard/admin');
        
        $response->assertStatus(200);
        $response->assertViewHas('stats');
        
        $stats = $response->viewData('stats');
        $this->assertEquals(13, $stats['total_subscriptions']);
        $this->assertEquals(10, $stats['active_subscriptions']);
    }
}

๐Ÿš€ ๋ฐฐํฌ ๋ฐ ์šด์˜

๊ฐœ๋ฐœ์ด ์™„๋ฃŒ๋˜์—ˆ๋‹ค๋ฉด ์ด์ œ ์‹ค์ œ ์„œ๋น„์Šค๋ฅผ ๋ฐฐํฌํ•  ์ฐจ๋ก€์ž…๋‹ˆ๋‹ค! ๐ŸŽ‰

๐Ÿ“‹ ๋ฐฐํฌ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

1. ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ
โ˜ ํ…Œ์ŠคํŠธ๋„ท์—์„œ ์ถฉ๋ถ„ํžˆ ํ…Œ์ŠคํŠธ
โ˜ ๋ณด์•ˆ ๊ฐ์‚ฌ ์™„๋ฃŒ
โ˜ ๊ฐ€์Šค ์ตœ์ ํ™” ํ™•์ธ
โ˜ ๋ฉ”์ธ๋„ท ๋ฐฐํฌ
โ˜ Etherscan์—์„œ ์ปจํŠธ๋ž™ํŠธ ๊ฒ€์ฆ

2. ๋ฐฑ์—”๋“œ
โ˜ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • (.env)
โ˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
โ˜ ํ ์›Œ์ปค ์„ค์ • (Supervisor)
โ˜ ํฌ๋ก ์žก ์„ค์ •
โ˜ ๋กœ๊ทธ ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ •
โ˜ ๋ฐฑ์—… ์‹œ์Šคํ…œ ๊ตฌ์ถ•

3. ํ”„๋ก ํŠธ์—”๋“œ
โ˜ ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ
โ˜ CDN ์„ค์ •
โ˜ SSL ์ธ์ฆ์„œ ์„ค์น˜
โ˜ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์—ฐ๋™ ํ…Œ์ŠคํŠธ

4. ๋ชจ๋‹ˆํ„ฐ๋ง
โ˜ ์„œ๋ฒ„ ๋ชจ๋‹ˆํ„ฐ๋ง (CPU, ๋ฉ”๋ชจ๋ฆฌ, ๋””์Šคํฌ)
โ˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ชจ๋‹ˆํ„ฐ๋ง (Sentry, New Relic)
โ˜ ๋ธ”๋ก์ฒด์ธ ์ด๋ฒคํŠธ ๋ชจ๋‹ˆํ„ฐ๋ง
โ˜ ์•Œ๋ฆผ ์„ค์ • (Slack, ์ด๋ฉ”์ผ)

Supervisor ์„ค์ • ์˜ˆ์‹œ (ํ ์›Œ์ปค):

# /etc/supervisor/conf.d/laravel-worker.conf

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/var/www/html/storage/logs/worker.log
stopwaitsecs=3600

Nginx ์„ค์ •:

server {
    listen 80;
    listen [::]:80;
    server_name your-domain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name your-domain.com;
    root /var/www/html/public;

    ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    index index.php;

    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

๐Ÿ“ˆ ์„ฑ์žฅ ์ „๋žต ๋ฐ ๋งˆ์ผ€ํŒ…

๊ธฐ์ˆ ์  ๊ตฌํ˜„๋„ ์ค‘์š”ํ•˜์ง€๋งŒ, ์‹ค์ œ ์‚ฌ์šฉ์ž๋ฅผ ํ™•๋ณดํ•˜๋Š” ๊ฒƒ์ด ๋” ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค! ๐ŸŽฏ

๐Ÿ’Ž Web3 ๊ตฌ๋… ์„œ๋น„์Šค ์„ฑ์žฅ ์ „๋žต

1. ์ดˆ๊ธฐ ์‚ฌ์šฉ์ž ํ™•๋ณด
โ€ข ์—์–ด๋“œ๋ž ์บ ํŽ˜์ธ (NFT ๋˜๋Š” ํ† ํฐ)
โ€ข ์–ผ๋ฆฌ์–ด๋‹ตํ„ฐ ํ• ์ธ (์ฒซ 100๋ช… 50% ํ• ์ธ)
โ€ข ์ถ”์ฒœ ํ”„๋กœ๊ทธ๋žจ (์ถ”์ฒœ์ธ์—๊ฒŒ ๋ฆฌ์›Œ๋“œ)
โ€ข Discord/Telegram ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ตฌ์ถ•

2. ํŒŒํŠธ๋„ˆ์‹ญ
โ€ข DeFi ํ”„๋กœํ† ์ฝœ๊ณผ ํ˜‘์—…
โ€ข NFT ํ”„๋กœ์ ํŠธ์™€ ๋ฒˆ๋“ค ์ œ๊ณต
โ€ข DAO์™€ ํ˜‘๋ ฅ
โ€ข ์ธํ”Œ๋ฃจ์–ธ์„œ ๋งˆ์ผ€ํŒ…

3. ํ† ํฐ ์ด์ฝ”๋…ธ๋ฏธ
โ€ข ๊ฑฐ๋ฒ„๋„Œ์Šค ํ† ํฐ ๋ฐœํ–‰
โ€ข ์Šคํ…Œ์ดํ‚น ๋ฆฌ์›Œ๋“œ
โ€ข ๋กœ์—ดํ‹ฐ ํ”„๋กœ๊ทธ๋žจ
โ€ข DAO ์ „ํ™˜ ๋กœ๋“œ๋งต

4. ํฌ๋กœ์Šค์ฒด์ธ ํ™•์žฅ
โ€ข ์—ฌ๋Ÿฌ ์ฒด์ธ ์ง€์›์œผ๋กœ ์‚ฌ์šฉ์ž์ธต ํ™•๋Œ€
โ€ข ๋ธŒ๋ฆฟ์ง€ ํ†ตํ•ฉ
โ€ข ๋ฉ€ํ‹ฐ์ฒด์ธ ์ง€๊ฐ‘ ์ง€์›

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์ด๋Ÿฐ ๋ธ”๋ก์ฒด์ธ ๊ตฌ๋… ์‹œ์Šคํ…œ์„ ๋„์ž…ํ•˜๋ฉด, ๊ธ€๋กœ๋ฒŒ ํฌ๋ฆฌ์—์ดํ„ฐ๋“ค์ด ๊ตญ๊ฒฝ ์—†์ด ์ž์‹ ์˜ ์žฌ๋Šฅ์„ ๊ตฌ๋… ํ˜•ํƒœ๋กœ ํŒ๋งคํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ํ•œ๊ตญ์˜ ๋””์ž์ด๋„ˆ๊ฐ€ ๋ฏธ๊ตญ ๊ณ ๊ฐ์—๊ฒŒ ์›” ๊ตฌ๋… ๋””์ž์ธ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜๋ฉด์„œ, ์ฆ‰์‹œ ์•”ํ˜ธํ™”ํ๋กœ ์ •์‚ฐ๋ฐ›์„ ์ˆ˜ ์žˆ์ฃ ! ๐ŸŒ

๐ŸŽ“ ํ•™์Šต ๋ฆฌ์†Œ์Šค ๋ฐ ์ปค๋ฎค๋‹ˆํ‹ฐ

๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์€ ๊ณ„์† ์ง„ํ™”ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ตœ์‹  ์ •๋ณด๋ฅผ ๋”ฐ๋ผ๊ฐ€๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด์š”! ๐Ÿ“š

๋ฆฌ์†Œ์Šค ์„ค๋ช… ๋งํฌ
Solidity ๊ณต์‹ ๋ฌธ์„œ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์–ธ์–ด ๋ ˆํผ๋Ÿฐ์Šค docs.soliditylang.org
OpenZeppelin ๊ฒ€์ฆ๋œ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ openzeppelin.com
Ethers.js ๋ฌธ์„œ JavaScript ์ด๋”๋ฆฌ์›€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ docs.ethers.org
Laravel ๊ณต์‹ ๋ฌธ์„œ PHP ํ”„๋ ˆ์ž„์›Œํฌ ๊ฐ€์ด๋“œ laravel.com/docs
CryptoZombies ๊ฒŒ์ž„์œผ๋กœ ๋ฐฐ์šฐ๋Š” Solidity cryptozombies.io
Alchemy University ๋ฌด๋ฃŒ Web3 ๊ฐœ๋ฐœ ๊ฐ•์˜ university.alchemy.com

๐Ÿ”ฎ ๋ฏธ๋ž˜ ์ „๋ง ๋ฐ ๋ฐœ์ „ ๋ฐฉํ–ฅ

๋ธ”๋ก์ฒด์ธ ๊ตฌ๋… ๊ฒฐ์ œ๋Š” ์•„์ง ์ดˆ๊ธฐ ๋‹จ๊ณ„์ง€๋งŒ, ์—„์ฒญ๋‚œ ์ž ์žฌ๋ ฅ์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๐Ÿš€

๐ŸŒŸ ํ–ฅํ›„ ๋ฐœ์ „ ๋ฐฉํ–ฅ

1. Account Abstraction (ERC-4337)
์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ€์Šค๋น„๋ฅผ ์ง์ ‘ ๋‚ด์ง€ ์•Š์•„๋„ ๋˜๋Š” "๊ฐ€์Šค๋ฆฌ์Šค" ํŠธ๋žœ์žญ์…˜์ด ๊ฐ€๋Šฅํ•ด์ง‘๋‹ˆ๋‹ค. ์ด๋Š” Web2 ์ˆ˜์ค€์˜ UX๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์–ด ๋Œ€์ค‘ํ™”์— ํฐ ๋„์›€์ด ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

2. ์†Œ์…œ ๋กœ๊ทธ์ธ ํ†ตํ•ฉ
Magic Link, Web3Auth ๊ฐ™์€ ์„œ๋น„์Šค๋ฅผ ํ†ตํ•ด ์ด๋ฉ”์ผ์ด๋‚˜ ์†Œ์…œ ๊ณ„์ •์œผ๋กœ ์ง€๊ฐ‘์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์„ค์น˜ ์—†์ด๋„ ๋ธ”๋ก์ฒด์ธ ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ์ฃ !

3. AI์™€์˜ ๊ฒฐํ•ฉ
AI๊ฐ€ ์‚ฌ์šฉ์ž์˜ ๊ตฌ๋… ํŒจํ„ด์„ ๋ถ„์„ํ•˜์—ฌ ์ตœ์ ์˜ ํ”Œ๋žœ์„ ์ถ”์ฒœํ•˜๊ฑฐ๋‚˜, ์ž๋™์œผ๋กœ ๊ตฌ๋…์„ ๊ด€๋ฆฌํ•ด์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

4. ํฌ๋กœ์Šค์ฒด์ธ ์ƒํ˜ธ์šด์šฉ์„ฑ
LayerZero, Axelar ๊ฐ™์€ ํ”„๋กœํ† ์ฝœ์„ ํ†ตํ•ด ์—ฌ๋Ÿฌ ์ฒด์ธ ๊ฐ„ ์›ํ™œํ•œ ๊ตฌ๋… ์ด๋™์ด ๊ฐ€๋Šฅํ•ด์งˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

5. ํƒˆ์ค‘์•™ํ™” ์‹ ์› (DID)
์‚ฌ์šฉ์ž๊ฐ€ ์ž์‹ ์˜ ๊ตฌ๋… ์ด๋ ฅ๊ณผ ํ‰ํŒ์„ ๋ธ”๋ก์ฒด์ธ์— ์ €์žฅํ•˜๊ณ , ๋‹ค๋ฅธ ์„œ๋น„์Šค์—์„œ๋„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ’ฌ ๋งˆ์น˜๋ฉฐ

์ž, ์—ฌ๊ธฐ๊นŒ์ง€ ๋ผ๋ผ๋ฒจ ๊ธฐ๋ฐ˜ ๋ธ”๋ก์ฒด์ธ ๊ตฌ๋… ๊ฒฐ์ œ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•˜๋Š” ์ „ ๊ณผ์ •์„ ์‚ดํŽด๋ดค์Šต๋‹ˆ๋‹ค! ๐ŸŽ‰

์ฒ˜์Œ์—๋Š” ๋ณต์žกํ•ด ๋ณด์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ํ•˜๋‚˜์”ฉ ๋”ฐ๋ผํ•˜๋‹ค ๋ณด๋ฉด ์ƒ๊ฐ๋ณด๋‹ค ์–ด๋ ต์ง€ ์•Š๋‹ค๋Š” ๊ฑธ ๋А๋ผ์‹ค ๊ฑฐ์˜ˆ์š”. ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋กœ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๊ณ , ๋ผ๋ผ๋ฒจ๋กœ ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ค๊ณ , ๋ธ”๋ก์ฒด์ธ๊ณผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๋™๊ธฐํ™”ํ•˜๋Š” ์ด ๋ชจ๋“  ๊ณผ์ •์ด Web3 ์‹œ๋Œ€์˜ ์ƒˆ๋กœ์šด ๋น„์ฆˆ๋‹ˆ์Šค ๋ชจ๋ธ์„ ๋งŒ๋“œ๋Š” ๊ธฐ์ดˆ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

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

๋ฌผ๋ก  ์•„์ง ํ•ด๊ฒฐํ•ด์•ผ ํ•  ๊ณผ์ œ๋“ค๋„ ๋งŽ์Šต๋‹ˆ๋‹ค. ๊ฐ€์Šค๋น„ ๋ฌธ์ œ, ์‚ฌ์šฉ์ž ๊ฒฝํ—˜(UX) ๊ฐœ์„ , ๊ทœ์ œ ๋Œ€์‘ ๋“ฑ์ด ๊ทธ๊ฒƒ์ด์ฃ . ํ•˜์ง€๋งŒ ๊ธฐ์ˆ ์€ ๊ณ„์† ๋ฐœ์ „ํ•˜๊ณ  ์žˆ๊ณ , Account Abstraction, Layer 2 ์†”๋ฃจ์…˜, ํฌ๋กœ์Šค์ฒด์ธ ๋ธŒ๋ฆฟ์ง€ ๋“ฑ ๋‹ค์–‘ํ•œ ํ•ด๊ฒฐ์ฑ…๋“ค์ด ๋‚˜์˜ค๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์—ฌ๋Ÿฌ๋ถ„๋„ ์ด ๊ธ€์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž์‹ ๋งŒ์˜ ๋ธ”๋ก์ฒด์ธ ๊ตฌ๋… ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”! ์ž‘๊ฒŒ ์‹œ์ž‘ํ•ด์„œ ์ ์ง„์ ์œผ๋กœ ํ™•์žฅํ•ด๋‚˜๊ฐ€๋ฉด ๋ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ๋„ท์—์„œ ์ถฉ๋ถ„ํžˆ ์‹คํ—˜ํ•˜๊ณ , ์ปค๋ฎค๋‹ˆํ‹ฐ์˜ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์œผ๋ฉด์„œ ๊ฐœ์„ ํ•ด๋‚˜๊ฐ€๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ํ˜ผ์ž ํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์ž๋‚˜ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. ํ•จ๊ป˜ ํ˜‘๋ ฅํ•˜๋ฉด ๋” ๋น ๋ฅด๊ณ  ์•ˆ์ „ํ•˜๊ฒŒ ํ”„๋กœ์ ํŠธ๋ฅผ ์™„์„ฑํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ์š”! ๐Ÿค

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

ํ–‰์šด์„ ๋น•๋‹ˆ๋‹ค! ํ™”์ดํŒ…! ๐Ÿ’ช

๐ŸŽฏ ํ•ต์‹ฌ ์š”์•ฝ

โœ“ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋กœ ์ž๋™ํ™”๋œ ๊ตฌ๋… ์‹œ์Šคํ…œ ๊ตฌํ˜„
โœ“ ๋ผ๋ผ๋ฒจ๋กœ ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ๋ฐฑ์—”๋“œ ๊ตฌ์ถ•
โœ“ Ethers.js๋กœ ํ”„๋ก ํŠธ์—”๋“œ ์ง€๊ฐ‘ ์—ฐ๋™
โœ“ ๋ธ”๋ก์ฒด์ธ ์ด๋ฒคํŠธ์™€ DB ์‹ค์‹œ๊ฐ„ ๋™๊ธฐํ™”
โœ“ ์ž๋™ ๊ฐฑ์‹  ์‹œ์Šคํ…œ์œผ๋กœ ํŽธ๋ฆฌํ•œ ๊ตฌ๋… ๊ด€๋ฆฌ
โœ“ ๋ฉ€ํ‹ฐ์ฒด์ธ ์ง€์›์œผ๋กœ ๊ฐ€์Šค๋น„ ์ตœ์ ํ™”
โœ“ ๋ณด์•ˆ๊ณผ ํ…Œ์ŠคํŠธ๋ฅผ ์ตœ์šฐ์„ ์œผ๋กœ!

๐ŸŒŸ Web3์˜ ๋ฏธ๋ž˜๋ฅผ ํ•จ๊ป˜ ๋งŒ๋“ค์–ด๊ฐ€์š”! ๐ŸŒŸ
๋Œ“๊ธ€ ์ž‘์„ฑ

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

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

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