์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŽจ ์†”๋ฆฌ๋””ํ‹ฐ๋กœ NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค ๋งŒ๋“ค๊ธฐ

๐ŸŽจ ์†”๋ฆฌ๋””ํ‹ฐ๋กœ NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค ๋งŒ๋“ค๊ธฐ

๋ธ”๋ก์ฒด์ธ ์œ„์—์„œ ๋‚˜๋งŒ์˜ ๋””์ง€ํ„ธ ์•„ํŠธ ๊ฑฐ๋ž˜์†Œ๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ์‹ค์ „ ๊ฐ€์ด๋“œ

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

์ด ๊ธ€์„ ์ฝ๊ณ  ๋‚˜๋ฉด ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ถ€ํ„ฐ ์‹ค์ œ ์ž‘๋™ํ•˜๋Š” NFT ๊ฑฐ๋ž˜ ์‹œ์Šคํ…œ๊นŒ์ง€ ๋งŒ๋“ค ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ. ๋ณต์žกํ•ด ๋ณด์ด์ง€๋งŒ ํ•˜๋‚˜์”ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ๋”ฐ๋ผ์˜ค๋ฉด ์ƒ๊ฐ๋ณด๋‹ค ์žฌ๋ฏธ์žˆ์–ด! ๐Ÿ˜Š

๐ŸŽฏ NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค๋ž€ ๋ฌด์—‡์ธ๊ฐ€?

NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค๋Š” ์‰ฝ๊ฒŒ ๋งํ•ด์„œ ๋””์ง€ํ„ธ ์ž์‚ฐ์„ ์‚ฌ๊ณ ํŒŒ๋Š” ์˜จ๋ผ์ธ ์žฅํ„ฐ์•ผ. ์˜คํ”ˆ์”จ(OpenSea)๋‚˜ ๋ผ๋ฆฌ๋ธ”(Rarible) ๊ฐ™์€ ํ”Œ๋žซํผ์„ ์ƒ๊ฐํ•˜๋ฉด ๋ผ. ํ•˜์ง€๋งŒ ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ค ๊ฑด ๋‹จ์ˆœํžˆ ์›น์‚ฌ์ดํŠธ๊ฐ€ ์•„๋‹ˆ๋ผ ๋ธ”๋ก์ฒด์ธ ์œ„์—์„œ ์ž‘๋™ํ•˜๋Š” ํƒˆ์ค‘์•™ํ™”๋œ ๊ฑฐ๋ž˜ ์‹œ์Šคํ…œ์ด์•ผ.

๐Ÿ’ก ํ•ต์‹ฌ ๊ฐœ๋…
NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค๋Š” ํฌ๊ฒŒ ์„ธ ๊ฐ€์ง€ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด:
1. NFT ๋ฐœํ–‰(Minting) - ๋””์ง€ํ„ธ ์ž์‚ฐ์„ ๋ธ”๋ก์ฒด์ธ์— ๋“ฑ๋ก
2. NFT ํŒ๋งค(Listing) - ๊ฐ€๊ฒฉ์„ ์ •ํ•˜๊ณ  ํŒ๋งค ๋“ฑ๋ก
3. NFT ๊ตฌ๋งค(Purchase) - ์•”ํ˜ธํ™”ํ๋กœ NFT ๊ตฌ๋งค

์ „ํ†ต์ ์ธ ์˜จ๋ผ์ธ ๋งˆ์ผ“๊ณผ ๋‹ค๋ฅธ ์ ์€ ์ค‘๊ฐœ์ž๊ฐ€ ์—†๋‹ค๋Š” ๊ฑฐ์•ผ. ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๊ฐ€ ๋ชจ๋“  ๊ฑฐ๋ž˜๋ฅผ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ , ๊ฑฐ๋ž˜ ๋‚ด์—ญ์€ ๋ธ”๋ก์ฒด์ธ์— ์˜๊ตฌ์ ์œผ๋กœ ๊ธฐ๋ก๋ผ. ์ด๊ฒŒ ๋ฐ”๋กœ Web3์˜ ๋งค๋ ฅ์ด์ง€! ๐ŸŒ

Creator Smart Contract Buyer Mint NFT Transfer NFT Marketplace Flow 1. Creator uploads digital art 2. Smart Contract mints NFT token 3. Buyer purchases with cryptocurrency 4. Ownership transfers automatically

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

๋ณธ๊ฒฉ์ ์œผ๋กœ ์ฝ”๋”ฉ์— ๋“ค์–ด๊ฐ€๊ธฐ ์ „์— ํ•„์š”ํ•œ ๋„๊ตฌ๋“ค์„ ์ค€๋น„ํ•ด์•ผ ํ•ด. ๊ฑฑ์ • ๋งˆ, ํ•˜๋‚˜๋„ ์–ด๋ ต์ง€ ์•Š์•„! ๐Ÿ˜‰

๐Ÿ“ฆ Node.js & npm

์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋Ÿฐํƒ€์ž„ ํ™˜๊ฒฝ์ด์•ผ. ํŒจํ‚ค์ง€ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ํ•„์ˆ˜์ ์ด์ง€. ์ตœ์‹  LTS ๋ฒ„์ „์„ ์„ค์น˜ํ•˜๋ฉด ๋ผ.

โšก Hardhat

์ด๋”๋ฆฌ์›€ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์ด์•ผ. ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋ฅผ ์ปดํŒŒ์ผํ•˜๊ณ  ํ…Œ์ŠคํŠธํ•˜๊ณ  ๋ฐฐํฌํ•˜๋Š” ๋ชจ๋“  ๊ฑธ ๋„์™€์ค˜.

๐ŸฆŠ MetaMask

๋ธŒ๋ผ์šฐ์ € ์ง€๊ฐ‘์ด์•ผ. ํ…Œ์ŠคํŠธ๋„ท์— ์—ฐ๊ฒฐํ•˜๊ณ  ๊ฑฐ๋ž˜๋ฅผ ์Šน์ธํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•ด. ํฌ๋กฌ ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ์œผ๋กœ ์„ค์น˜ํ•˜๋ฉด ๋ผ.

Step 1: ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐํ™”

ํ„ฐ๋ฏธ๋„์„ ์—ด๊ณ  ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•ด๋ณด์ž:

mkdir nft-marketplace
cd nft-marketplace
npm init -y
npm install --save-dev hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers

์ด ๋ช…๋ น์–ด๋“ค์€ ํ”„๋กœ์ ํŠธ ํด๋”๋ฅผ ๋งŒ๋“ค๊ณ  ํ•„์š”ํ•œ ํŒจํ‚ค์ง€๋“ค์„ ์„ค์น˜ํ•ด์ค˜. Hardhat์€ ์šฐ๋ฆฌ์˜ ๋ฉ”์ธ ๊ฐœ๋ฐœ ๋„๊ตฌ๊ณ , ethers.js๋Š” ์ด๋”๋ฆฌ์›€๊ณผ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ.

Step 2: Hardhat ์„ค์ •
npx hardhat

์ด ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ํ”„๋กœ์ ํŠธ ํƒ€์ž…์„ ์„ ํƒํ•˜๋ผ๊ณ  ๋‚˜์™€. "Create a basic sample project"๋ฅผ ์„ ํƒํ•˜๋ฉด ๊ธฐ๋ณธ ๊ตฌ์กฐ๊ฐ€ ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋ผ. ์ •๋ง ํŽธ๋ฆฌํ•˜์ง€? ๐ŸŽ‰

๐Ÿ”ฅ Pro Tip
๊ฐœ๋ฐœ ์ค‘์—๋Š” ๋กœ์ปฌ ๋ธ”๋ก์ฒด์ธ ๋„คํŠธ์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„. Hardhat์ด ์ œ๊ณตํ•˜๋Š” ๋กœ์ปฌ ๋…ธ๋“œ๋Š” ๋น ๋ฅด๊ณ  ๋ฌด๋ฃŒ์•ผ. ์‹ค์ œ ๋ฐฐํฌ๋Š” ํ…Œ์ŠคํŠธ๋„ท(Goerli, Sepolia)์—์„œ ์ถฉ๋ถ„ํžˆ ํ…Œ์ŠคํŠธํ•œ ํ›„์— ๋ฉ”์ธ๋„ท์œผ๋กœ ๊ฐ€๋Š” ๊ฒŒ ์•ˆ์ „ํ•ด!

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

์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์†”๋ฆฌ๋””ํ‹ฐ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณผ ์‹œ๊ฐ„์ด์•ผ! NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค๋Š” ํฌ๊ฒŒ ๋‘ ๊ฐœ์˜ ์ปจํŠธ๋ž™ํŠธ๋กœ ๊ตฌ์„ฑ๋ผ:

1. NFT ์ปจํŠธ๋ž™ํŠธ - ERC-721 ํ‘œ์ค€์„ ๋”ฐ๋ฅด๋Š” NFT ํ† ํฐ
2. ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค ์ปจํŠธ๋ž™ํŠธ - NFT ๊ฑฐ๋ž˜๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋กœ์ง

๐ŸŽจ NFT ์ปจํŠธ๋ž™ํŠธ ๋งŒ๋“ค๊ธฐ

๋จผ์ € contracts ํด๋”์— NFT.sol ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด๋ณด์ž:

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

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract NFT is ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;
    address contractAddress;

    constructor(address marketplaceAddress) ERC721("Metaverse Tokens", "METT") {
        contractAddress = marketplaceAddress;
    }

    function createToken(string memory tokenURI) public returns (uint) {
        _tokenIds.increment();
        uint256 newItemId = _tokenIds.current();

        _mint(msg.sender, newItemId);
        _setTokenURI(newItemId, tokenURI);
        setApprovalForAll(contractAddress, true);
        
        return newItemId;
    }
}

์ฝ”๋“œ๋ฅผ ํ•˜๋‚˜์”ฉ ๋œฏ์–ด๋ณด์ž! ๐Ÿ”

์ฝ”๋“œ ๋ถ„์„

pragma solidity ^0.8.20 - ์†”๋ฆฌ๋””ํ‹ฐ ๋ฒ„์ „์„ ๋ช…์‹œํ•ด. 0.8.20 ์ด์ƒ์—์„œ ์ž‘๋™ํ•œ๋‹ค๋Š” ๋œป์ด์•ผ.

import ๋ฌธ๋“ค - OpenZeppelin ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๊ฐ€์ ธ์™€. ์ด๋ฏธ ๊ฒ€์ฆ๋œ ์•ˆ์ „ํ•œ ์ฝ”๋“œ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋Š” ๊ฑฐ์ง€. ๋ฐ”ํ€ด๋ฅผ ๋‹ค์‹œ ๋ฐœ๋ช…ํ•  ํ•„์š” ์—†์ž–์•„? ๐Ÿ˜Š

ERC721URIStorage - NFT์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(์ด๋ฏธ์ง€, ์„ค๋ช… ๋“ฑ)๋ฅผ ์ €์žฅํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ํ™•์žฅ ๊ธฐ๋Šฅ์ด์•ผ.

Counters - ํ† ํฐ ID๋ฅผ ์ž๋™์œผ๋กœ ์ฆ๊ฐ€์‹œ์ผœ์ฃผ๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ์•ผ. 1, 2, 3... ์ด๋Ÿฐ ์‹์œผ๋กœ ๊ณ ์œ ํ•œ ID๋ฅผ ๋ถ€์—ฌํ•ด.

createToken ํ•จ์ˆ˜ - ์‹ค์ œ๋กœ NFT๋ฅผ ๋ฐœํ–‰ํ•˜๋Š” ํ•จ์ˆ˜์•ผ. tokenURI๋Š” IPFS๋‚˜ ๋‹ค๋ฅธ ์ €์žฅ์†Œ์— ์žˆ๋Š” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ฃผ์†Œ๋ฅผ ๊ฐ€๋ฆฌ์ผœ.
โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ
OpenZeppelin ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋จผ์ € ์„ค์น˜ํ•ด์•ผ ํ•ด:
npm install @openzeppelin/contracts
์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ๋ณด์•ˆ ๊ฐ์‚ฌ๋ฅผ ๊ฑฐ์นœ ํ‘œ์ค€ ๊ตฌํ˜„์ฒด๋ผ์„œ ์ง์ ‘ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ํ›จ์”ฌ ์•ˆ์ „ํ•ด!

๐Ÿช ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค ์ปจํŠธ๋ž™ํŠธ ๋งŒ๋“ค๊ธฐ

์ด์ œ ์ง„์งœ ํ•ต์‹ฌ์ธ ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค ์ปจํŠธ๋ž™ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. NFTMarketplace.sol ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด:

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

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract NFTMarketplace is ReentrancyGuard {
    using Counters for Counters.Counter;
    Counters.Counter private _itemIds;
    Counters.Counter private _itemsSold;

    address payable owner;
    uint256 listingPrice = 0.025 ether;

    constructor() {
        owner = payable(msg.sender);
    }

    struct MarketItem {
        uint itemId;
        address nftContract;
        uint256 tokenId;
        address payable seller;
        address payable owner;
        uint256 price;
        bool sold;
    }

    mapping(uint256 => MarketItem) private idToMarketItem;

    event MarketItemCreated (
        uint indexed itemId,
        address indexed nftContract,
        uint256 indexed tokenId,
        address seller,
        address owner,
        uint256 price,
        bool sold
    );

    function getListingPrice() public view returns (uint256) {
        return listingPrice;
    }

    function createMarketItem(
        address nftContract,
        uint256 tokenId,
        uint256 price
    ) public payable nonReentrant {
        require(price > 0, "Price must be at least 1 wei");
        require(msg.value == listingPrice, "Price must be equal to listing price");

        _itemIds.increment();
        uint256 itemId = _itemIds.current();

        idToMarketItem[itemId] = MarketItem(
            itemId,
            nftContract,
            tokenId,
            payable(msg.sender),
            payable(address(0)),
            price,
            false
        );

        IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);

        emit MarketItemCreated(
            itemId,
            nftContract,
            tokenId,
            msg.sender,
            address(0),
            price,
            false
        );
    }

    function createMarketSale(
        address nftContract,
        uint256 itemId
    ) public payable nonReentrant {
        uint price = idToMarketItem[itemId].price;
        uint tokenId = idToMarketItem[itemId].tokenId;
        
        require(msg.value == price, "Please submit the asking price");

        idToMarketItem[itemId].seller.transfer(msg.value);
        IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
        idToMarketItem[itemId].owner = payable(msg.sender);
        idToMarketItem[itemId].sold = true;
        _itemsSold.increment();
        
        payable(owner).transfer(listingPrice);
    }

    function fetchMarketItems() public view returns (MarketItem[] memory) {
        uint itemCount = _itemIds.current();
        uint unsoldItemCount = _itemIds.current() - _itemsSold.current();
        uint currentIndex = 0;

        MarketItem[] memory items = new MarketItem[](unsoldItemCount);
        for (uint i = 0; i < itemCount; i++) {
            if (idToMarketItem[i + 1].owner == address(0)) {
                uint currentId = i + 1;
                MarketItem storage currentItem = idToMarketItem[currentId];
                items[currentIndex] = currentItem;
                currentIndex += 1;
            }
        }
        return items;
    }

    function fetchMyNFTs() public view returns (MarketItem[] memory) {
        uint totalItemCount = _itemIds.current();
        uint itemCount = 0;
        uint currentIndex = 0;

        for (uint i = 0; i < totalItemCount; i++) {
            if (idToMarketItem[i + 1].owner == msg.sender) {
                itemCount += 1;
            }
        }

        MarketItem[] memory items = new MarketItem[](itemCount);
        for (uint i = 0; i < totalItemCount; i++) {
            if (idToMarketItem[i + 1].owner == msg.sender) {
                uint currentId = i + 1;
                MarketItem storage currentItem = idToMarketItem[currentId];
                items[currentIndex] = currentItem;
                currentIndex += 1;
            }
        }
        return items;
    }

    function fetchItemsCreated() public view returns (MarketItem[] memory) {
        uint totalItemCount = _itemIds.current();
        uint itemCount = 0;
        uint currentIndex = 0;

        for (uint i = 0; i < totalItemCount; i++) {
            if (idToMarketItem[i + 1].seller == msg.sender) {
                itemCount += 1;
            }
        }

        MarketItem[] memory items = new MarketItem[](itemCount);
        for (uint i = 0; i < totalItemCount; i++) {
            if (idToMarketItem[i + 1].seller == msg.sender) {
                uint currentId = i + 1;
                MarketItem storage currentItem = idToMarketItem[currentId];
                items[currentIndex] = currentItem;
                currentIndex += 1;
            }
        }
        return items;
    }
}

์™€, ์ฝ”๋“œ๊ฐ€ ์ข€ ๊ธธ์ง€? ํ•˜์ง€๋งŒ ๊ฑฑ์ • ๋งˆ! ํ•˜๋‚˜์”ฉ ๋œฏ์–ด๋ณด๋ฉด ์ƒ๊ฐ๋ณด๋‹ค ๋‹จ์ˆœํ•ด. ๐Ÿค“

ํ•ต์‹ฌ ๊ตฌ์กฐ ์ดํ•ดํ•˜๊ธฐ

MarketItem ๊ตฌ์กฐ์ฒด - ํŒ๋งค ์ค‘์ธ NFT์˜ ์ •๋ณด๋ฅผ ๋‹ด๋Š” ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์•ผ. ๋ˆ„๊ฐ€ ํŒ”๊ณ , ์–ผ๋งˆ์— ํŒ”๊ณ , ํŒ”๋ ธ๋Š”์ง€ ๋“ฑ์˜ ์ •๋ณด๊ฐ€ ๋“ค์–ด๊ฐ€.

mapping - ์•„์ดํ…œ ID์™€ MarketItem์„ ์—ฐ๊ฒฐํ•˜๋Š” ํ•ด์‹œ๋งต์ด์•ผ. ๋น ๋ฅธ ์กฐํšŒ๋ฅผ ์œ„ํ•ด ์‚ฌ์šฉํ•ด.

event - ๋ธ”๋ก์ฒด์ธ์— ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธฐ๋Š” ๊ฑฐ์•ผ. ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ด ์ด๋ฒคํŠธ๋ฅผ ๊ฐ์ง€ํ•ด์„œ UI๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ์–ด.

nonReentrant - ์žฌ์ง„์ž… ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•˜๋Š” modifier์•ผ. ๋ณด์•ˆ์„ ์œ„ํ•ด ๊ผญ ํ•„์š”ํ•ด!

๐Ÿ” ์ฃผ์š” ํ•จ์ˆ˜ ์„ค๋ช…

createMarketItem ํ•จ์ˆ˜

์ด ํ•จ์ˆ˜๋Š” NFT๋ฅผ ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค์— ๋“ฑ๋กํ•˜๋Š” ์—ญํ• ์„ ํ•ด. ํŒ๋งค์ž๋Š” listing fee๋ฅผ ์ง€๋ถˆํ•˜๊ณ  NFT๋ฅผ ์ปจํŠธ๋ž™ํŠธ์— ์ „์†กํ•ด. ๊ทธ๋Ÿฌ๋ฉด ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค๊ฐ€ NFT๋ฅผ ๋ณด๊ด€ํ•˜๋‹ค๊ฐ€ ๊ตฌ๋งค์ž๊ฐ€ ๋‚˜ํƒ€๋‚˜๋ฉด ์ „๋‹ฌํ•˜๋Š” ๊ฑฐ์ง€.

ํ•ต์‹ฌ ๋กœ์ง:

1. ๊ฐ€๊ฒฉ์ด 0๋ณด๋‹ค ํฐ์ง€ ํ™•์ธ
2. listing fee๊ฐ€ ์ •ํ™•ํžˆ ์ง€๋ถˆ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
3. ์ƒˆ๋กœ์šด ๋งˆ์ผ“ ์•„์ดํ…œ ์ƒ์„ฑ
4. NFT๋ฅผ ํŒ๋งค์ž์—๊ฒŒ์„œ ์ปจํŠธ๋ž™ํŠธ๋กœ ์ „์†ก
5. ์ด๋ฒคํŠธ ๋ฐœ์ƒ

createMarketSale ํ•จ์ˆ˜

์‹ค์ œ ๊ตฌ๋งค๊ฐ€ ์ผ์–ด๋‚˜๋Š” ํ•จ์ˆ˜์•ผ! ๊ตฌ๋งค์ž๊ฐ€ ์ •ํ™•ํ•œ ๊ธˆ์•ก์„ ๋ณด๋‚ด๋ฉด:

1. ํŒ๋งค์ž์—๊ฒŒ ํŒ๋งค ๊ธˆ์•ก ์ „์†ก
2. NFT๋ฅผ ๊ตฌ๋งค์ž์—๊ฒŒ ์ „์†ก
3. ์†Œ์œ ๊ถŒ ์—…๋ฐ์ดํŠธ
4. ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค ์šด์˜์ž์—๊ฒŒ listing fee ์ „์†ก

๋ชจ๋“  ๊ฒŒ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ๋ผ. ์ค‘๊ฐœ์ž๊ฐ€ ํ•„์š” ์—†๋Š” ๊ฑฐ์ง€! ์ด๊ฒŒ ๋ฐ”๋กœ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ์˜ ๋งˆ๋ฒ•์ด์•ผ. โœจ

์กฐํšŒ ํ•จ์ˆ˜๋“ค

fetchMarketItems() - ํ˜„์žฌ ํŒ๋งค ์ค‘์ธ ๋ชจ๋“  NFT ๋ชฉ๋ก์„ ๊ฐ€์ ธ์™€
fetchMyNFTs() - ๋‚ด๊ฐ€ ๊ตฌ๋งคํ•œ NFT ๋ชฉ๋ก
fetchItemsCreated() - ๋‚ด๊ฐ€ ํŒ๋งค ๋“ฑ๋กํ•œ NFT ๋ชฉ๋ก

์ด ํ•จ์ˆ˜๋“ค์€ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ UI๋ฅผ ๊ตฌ์„ฑํ•  ๋•Œ ์‚ฌ์šฉ๋ผ. ์‚ฌ์šฉ์ž๊ฐ€ ์ž์‹ ์˜ ์ปฌ๋ ‰์…˜์„ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๊ฑฐ์ง€.

Smart Contract Architecture NFT Contract ERC721 Standard Token Minting Metadata Storage Token Management Transfer Functions Approval System URI Storage IPFS Integration Metadata Links Counter System Unique Token IDs Marketplace Contract Listing System Create Market Item Set Price Trading Logic Purchase Function Payment Processing Query Functions Fetch Market Items User Collections Security Features ReentrancyGuard API Interaction between contracts via function calls

๐Ÿงช ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•˜๊ธฐ

์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ์œผ๋ฉด ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•ด์•ผ๊ฒ ์ง€? ๋ธ”๋ก์ฒด์ธ์—์„œ๋Š” ํ•œ ๋ฒˆ ๋ฐฐํฌํ•˜๋ฉด ์ˆ˜์ •์ด ์–ด๋ ต๊ธฐ ๋•Œ๋ฌธ์— ํ…Œ์ŠคํŠธ๊ฐ€ ์ •๋ง ์ค‘์š”ํ•ด! ๐Ÿ’ฏ

test ํด๋”์— marketplace-test.js ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด๋ณด์ž:

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("NFTMarketplace", function () {
  let nft;
  let marketplace;
  let owner;
  let buyer;
  let listingPrice;

  beforeEach(async function () {
    [owner, buyer] = await ethers.getSigners();

    const NFT = await ethers.getContractFactory("NFT");
    const Marketplace = await ethers.getContractFactory("NFTMarketplace");

    marketplace = await Marketplace.deploy();
    await marketplace.deployed();

    nft = await NFT.deploy(marketplace.address);
    await nft.deployed();

    listingPrice = await marketplace.getListingPrice();
  });

  it("Should create and execute market sales", async function () {
    const tokenURI = "https://ipfs.io/ipfs/QmTest123";
    
    await nft.createToken(tokenURI);
    await nft.createToken(tokenURI);

    const auctionPrice = ethers.utils.parseUnits("1", "ether");

    await marketplace.createMarketItem(nft.address, 1, auctionPrice, {
      value: listingPrice,
    });
    await marketplace.createMarketItem(nft.address, 2, auctionPrice, {
      value: listingPrice,
    });

    await marketplace
      .connect(buyer)
      .createMarketSale(nft.address, 1, { value: auctionPrice });

    let items = await marketplace.fetchMarketItems();
    expect(items.length).to.equal(1);

    items = await marketplace.connect(buyer).fetchMyNFTs();
    expect(items.length).to.equal(1);
  });

  it("Should return correct listing price", async function () {
    const price = await marketplace.getListingPrice();
    expect(price).to.equal(ethers.utils.parseEther("0.025"));
  });

  it("Should fail if price is not met", async function () {
    const tokenURI = "https://ipfs.io/ipfs/QmTest456";
    await nft.createToken(tokenURI);

    const auctionPrice = ethers.utils.parseUnits("1", "ether");
    await marketplace.createMarketItem(nft.address, 1, auctionPrice, {
      value: listingPrice,
    });

    await expect(
      marketplace
        .connect(buyer)
        .createMarketSale(nft.address, 1, { 
          value: ethers.utils.parseUnits("0.5", "ether") 
        })
    ).to.be.revertedWith("Please submit the asking price");
  });
});

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๋ ค๋ฉด:

npx hardhat test

๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ•˜๋ฉด ์ดˆ๋ก์ƒ‰ ์ฒดํฌ๋งˆํฌ๊ฐ€ ๋œฐ ๊ฑฐ์•ผ! โœ… ๋งŒ์•ฝ ์‹คํŒจํ•˜๋ฉด ์–ด๋””๊ฐ€ ๋ฌธ์ œ์ธ์ง€ ํ™•์ธํ•˜๊ณ  ์ˆ˜์ •ํ•ด์•ผ ํ•ด.

๐ŸŽฏ ํ…Œ์ŠคํŠธ ํŒ
ํ…Œ์ŠคํŠธ๋Š” ๋‹ค์–‘ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ปค๋ฒ„ํ•ด์•ผ ํ•ด:
- ์ •์ƒ์ ์ธ ๊ฑฐ๋ž˜ ํ๋ฆ„
- ์ž˜๋ชป๋œ ๊ฐ€๊ฒฉ ์ž…๋ ฅ
- ๊ถŒํ•œ ์—†๋Š” ์ ‘๊ทผ ์‹œ๋„
- ๊ฒฝ๊ณ„๊ฐ’ ํ…Œ์ŠคํŠธ(0 ETH, ๋งค์šฐ ํฐ ๊ธˆ์•ก ๋“ฑ)

์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ 80% ์ด์ƒ ์œ ์ง€ํ•˜๋Š” ๊ฒŒ ์ข‹์•„!

๐Ÿš€ ๋ฐฐํฌํ•˜๊ธฐ

ํ…Œ์ŠคํŠธ๊นŒ์ง€ ์™„๋ฃŒํ–ˆ์œผ๋ฉด ์ด์ œ ์‹ค์ œ ๋ธ”๋ก์ฒด์ธ์— ๋ฐฐํฌํ•  ์ฐจ๋ก€์•ผ! ๋จผ์ € ํ…Œ์ŠคํŠธ๋„ท์— ๋ฐฐํฌํ•ด์„œ ์‹ค์ œ ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธํ•ด๋ณด์ž. ๐ŸŒ

๐Ÿ“ ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ ์ž‘์„ฑ

scripts ํด๋”์— deploy.js ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด:

const hre = require("hardhat");

async function main() {
  const NFTMarketplace = await hre.ethers.getContractFactory("NFTMarketplace");
  const marketplace = await NFTMarketplace.deploy();
  await marketplace.deployed();
  console.log("NFTMarketplace deployed to:", marketplace.address);

  const NFT = await hre.ethers.getContractFactory("NFT");
  const nft = await NFT.deploy(marketplace.address);
  await nft.deployed();
  console.log("NFT contract deployed to:", nft.address);
}

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

โš™๏ธ Hardhat ์„ค์ • ํŒŒ์ผ ์ˆ˜์ •

hardhat.config.js ํŒŒ์ผ์„ ์—ด์–ด์„œ ๋„คํŠธ์›Œํฌ ์„ค์ •์„ ์ถ”๊ฐ€ํ•ด:

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

module.exports = {
  solidity: "0.8.20",
  networks: {
    hardhat: {
      chainId: 1337
    },
    sepolia: {
      url: process.env.SEPOLIA_RPC_URL,
      accounts: [process.env.PRIVATE_KEY]
    },
    polygon: {
      url: process.env.POLYGON_RPC_URL,
      accounts: [process.env.PRIVATE_KEY]
    }
  }
};
๐Ÿ”’ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ
์ ˆ๋Œ€๋กœ private key๋ฅผ ์ฝ”๋“œ์— ์ง์ ‘ ๋„ฃ์œผ๋ฉด ์•ˆ ๋ผ! .env ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์„œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ•ด์•ผ ํ•ด:

SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/YOUR_PROJECT_ID

PRIVATE_KEY=your_private_key_here

๊ทธ๋ฆฌ๊ณ  .gitignore์— .env๋ฅผ ๊ผญ ์ถ”๊ฐ€ํ•ด์„œ GitHub์— ์˜ฌ๋ผ๊ฐ€์ง€ ์•Š๊ฒŒ ํ•ด!

๐ŸŽฌ ์‹ค์ œ ๋ฐฐํฌ ์‹คํ–‰

Sepolia ํ…Œ์ŠคํŠธ๋„ท์— ๋ฐฐํฌํ•˜๋ ค๋ฉด:

npx hardhat run scripts/deploy.js --network sepolia

๋ฐฐํฌ๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ๊ฐ€ ์ถœ๋ ฅ๋ผ. ์ด ์ฃผ์†Œ๋ฅผ ์ž˜ ์ €์žฅํ•ด๋‘ฌ์•ผ ํ•ด! ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ด ์ฃผ์†Œ๋กœ ์ปจํŠธ๋ž™ํŠธ์™€ ํ†ต์‹ ํ•˜๊ฑฐ๋“ . ๐Ÿ“

๐Ÿ’ฐ ํ…Œ์ŠคํŠธ๋„ท ์ด๋” ๋ฐ›๊ธฐ
ํ…Œ์ŠคํŠธ๋„ท์— ๋ฐฐํฌํ•˜๋ ค๋ฉด ํ…Œ์ŠคํŠธ์šฉ ์ด๋”๊ฐ€ ํ•„์š”ํ•ด. ๋‹ค์Œ faucet์—์„œ ๋ฌด๋ฃŒ๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด:
- Sepolia Faucet: https://sepoliafaucet.com/
- Goerli Faucet: https://goerlifaucet.com/

MetaMask ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ํ…Œ์ŠคํŠธ ์ด๋”๋ฅผ ๋ณด๋‚ด์ค˜. ๋ณดํ†ต 0.5 ETH ์ •๋„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด!

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

์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋งŒ ์žˆ์œผ๋ฉด ๋ญํ•ด? ์‚ฌ์šฉ์ž๊ฐ€ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ํ•„์š”ํ•˜์ง€! React๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ฐ„๋‹จํ•œ ํ”„๋ก ํŠธ์—”๋“œ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. ๐Ÿ–ผ๏ธ

๐Ÿ“ฆ ํ•„์š”ํ•œ ํŒจํ‚ค์ง€ ์„ค์น˜

npm install ethers@5.7.2 axios ipfs-http-client
npx create-react-app client
cd client
npm install ethers@5.7.2 axios

๐Ÿ”— Web3 ์—ฐ๊ฒฐ ์„ค์ •

๋จผ์ € MetaMask์™€ ์—ฐ๊ฒฐํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์–ด์•ผ ํ•ด. src/utils/web3.js ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด:

import { ethers } from 'ethers';
import NFTMarketplace from '../artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.json';
import NFT from '../artifacts/contracts/NFT.sol/NFT.json';

const marketplaceAddress = "YOUR_MARKETPLACE_CONTRACT_ADDRESS";
const nftAddress = "YOUR_NFT_CONTRACT_ADDRESS";

export async function connectWallet() {
  if (typeof window.ethereum !== 'undefined') {
    try {
      await window.ethereum.request({ method: 'eth_requestAccounts' });
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      const signer = provider.getSigner();
      return { provider, signer };
    } catch (error) {
      console.error("User denied account access");
      throw error;
    }
  } else {
    alert('Please install MetaMask!');
    throw new Error('MetaMask not installed');
  }
}

export async function getMarketplaceContract(signer) {
  return new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer);
}

export async function getNFTContract(signer) {
  return new ethers.Contract(nftAddress, NFT.abi, signer);
}

๐Ÿ–ผ๏ธ NFT ๋ชฉ๋ก ํ‘œ์‹œํ•˜๊ธฐ

ํŒ๋งค ์ค‘์ธ NFT ๋ชฉ๋ก์„ ๋ณด์—ฌ์ฃผ๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž:

import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import { connectWallet, getMarketplaceContract, getNFTContract } from './utils/web3';

function Marketplace() {
  const [nfts, setNfts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    loadNFTs();
  }, []);

  async function loadNFTs() {
    try {
      const { provider, signer } = await connectWallet();
      const marketplaceContract = await getMarketplaceContract(signer);
      const nftContract = await getNFTContract(signer);

      const data = await marketplaceContract.fetchMarketItems();

      const items = await Promise.all(data.map(async i => {
        const tokenUri = await nftContract.tokenURI(i.tokenId);
        const meta = await fetch(tokenUri).then(res => res.json());
        
        let price = ethers.utils.formatUnits(i.price.toString(), 'ether');
        
        return {
          price,
          tokenId: i.tokenId.toNumber(),
          seller: i.seller,
          owner: i.owner,
          image: meta.image,
          name: meta.name,
          description: meta.description,
        };
      }));

      setNfts(items);
      setLoading(false);
    } catch (error) {
      console.error("Error loading NFTs:", error);
      setLoading(false);
    }
  }

  async function buyNft(nft) {
    try {
      const { signer } = await connectWallet();
      const marketplaceContract = await getMarketplaceContract(signer);

      const price = ethers.utils.parseUnits(nft.price.toString(), 'ether');
      const transaction = await marketplaceContract.createMarketSale(
        nftAddress,
        nft.tokenId,
        { value: price }
      );

      await transaction.wait();
      alert('NFT purchased successfully!');
      loadNFTs();
    } catch (error) {
      console.error("Error buying NFT:", error);
      alert('Purchase failed!');
    }
  }

  if (loading) return <div>Loading...</div>;

  return (
    <div className="marketplace">
      <h1>NFT Marketplace</h1>
      <div className="nft-grid">
        {nfts.map((nft, i) => (
          <div key={i} className="nft-card">
            <img src={nft.image} alt={nft.name} />
            <h3>{nft.name}</h3>
            <p>{nft.description}</p>
            <p className="price">{nft.price} ETH</p>
            <button onClick={() => buyNft(nft)}>Buy Now</button>
          </div>
        ))}
      </div>
    </div>
  );
}

export default Marketplace;

์ด ์ฝ”๋“œ๋Š” ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค์—์„œ ํŒ๋งค ์ค‘์ธ ๋ชจ๋“  NFT๋ฅผ ๊ฐ€์ ธ์™€์„œ ์นด๋“œ ํ˜•ํƒœ๋กœ ๋ณด์—ฌ์ค˜. ๊ฐ ์นด๋“œ์—๋Š” ๊ตฌ๋งค ๋ฒ„ํŠผ์ด ์žˆ์–ด์„œ ํด๋ฆญํ•˜๋ฉด MetaMask๊ฐ€ ์—ด๋ฆฌ๊ณ  ๊ฑฐ๋ž˜๋ฅผ ์Šน์ธํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ˜Ž

๐ŸŽจ UI/UX ํŒ
์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ•˜๋ ค๋ฉด:
- ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ๋ช…ํ™•ํžˆ ํ‘œ์‹œ
- ๊ฑฐ๋ž˜ ์ง„ํ–‰ ์ƒํ™ฉ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณด์—ฌ์ฃผ๊ธฐ
- ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์นœ์ ˆํ•˜๊ฒŒ ์ž‘์„ฑ
- ๋ชจ๋ฐ”์ผ ๋ฐ˜์‘ํ˜• ๋””์ž์ธ ์ ์šฉ
- ์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์ตœ์ ํ™”

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•  ๋•Œ๋„ ์ด๋Ÿฐ ์„ธ์‹ฌํ•œ ๋ฐฐ๋ ค๊ฐ€ ์ค‘์š”ํ•ด!

๐Ÿ“ธ IPFS๋กœ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅํ•˜๊ธฐ

NFT์˜ ์ด๋ฏธ์ง€์™€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” ์–ด๋””์— ์ €์žฅํ•ด์•ผ ํ• ๊นŒ? ๋ธ”๋ก์ฒด์ธ์— ์ง์ ‘ ์ €์žฅํ•˜๋ฉด ๊ฐ€์Šค๋น„๊ฐ€ ์—„์ฒญ๋‚˜๊ฒŒ ๋น„์‹ธ์ ธ. ๊ทธ๋ž˜์„œ ๋ณดํ†ต IPFS(InterPlanetary File System)๋ฅผ ์‚ฌ์šฉํ•ด! ๐ŸŒ

๐Ÿ”ง IPFS ํด๋ผ์ด์–ธํŠธ ์„ค์ •

import { create } from 'ipfs-http-client';

const projectId = 'YOUR_INFURA_PROJECT_ID';
const projectSecret = 'YOUR_INFURA_PROJECT_SECRET';
const auth = 'Basic ' + Buffer.from(projectId + ':' + projectSecret).toString('base64');

const client = create({
  host: 'ipfs.infura.io',
  port: 5001,
  protocol: 'https',
  headers: {
    authorization: auth,
  },
});

export async function uploadToIPFS(file) {
  try {
    const added = await client.add(file);
    const url = `https://ipfs.io/ipfs/${added.path}`;
    return url;
  } catch (error) {
    console.error('Error uploading file:', error);
    throw error;
  }
}

export async function uploadMetadata(metadata) {
  try {
    const added = await client.add(JSON.stringify(metadata));
    const url = `https://ipfs.io/ipfs/${added.path}`;
    return url;
  } catch (error) {
    console.error('Error uploading metadata:', error);
    throw error;
  }
}

๐ŸŽจ NFT ์ƒ์„ฑ ์ปดํฌ๋„ŒํŠธ

์ด์ œ ์‚ฌ์šฉ์ž๊ฐ€ ์ž์‹ ์˜ ์ž‘ํ’ˆ์„ NFT๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด๋ณด์ž:

import React, { useState } from 'react';
import { uploadToIPFS, uploadMetadata } from './utils/ipfs';
import { connectWallet, getNFTContract, getMarketplaceContract } from './utils/web3';
import { ethers } from 'ethers';

function CreateNFT() {
  const [fileUrl, setFileUrl] = useState(null);
  const [formInput, updateFormInput] = useState({ 
    price: '', 
    name: '', 
    description: '' 
  });

  async function onChange(e) {
    const file = e.target.files[0];
    try {
      const url = await uploadToIPFS(file);
      setFileUrl(url);
    } catch (error) {
      console.error('Error uploading file:', error);
    }
  }

  async function createMarket() {
    const { name, description, price } = formInput;
    if (!name || !description || !price || !fileUrl) {
      alert('Please fill all fields!');
      return;
    }

    try {
      const metadata = {
        name,
        description,
        image: fileUrl,
      };

      const metadataUrl = await uploadMetadata(metadata);
      
      await mintAndList(metadataUrl, price);
    } catch (error) {
      console.error('Error creating NFT:', error);
    }
  }

  async function mintAndList(tokenURI, price) {
    try {
      const { signer } = await connectWallet();
      const nftContract = await getNFTContract(signer);
      const marketplaceContract = await getMarketplaceContract(signer);

      let transaction = await nftContract.createToken(tokenURI);
      let tx = await transaction.wait();
      
      let event = tx.events[0];
      let value = event.args[2];
      let tokenId = value.toNumber();

      const priceInWei = ethers.utils.parseUnits(price, 'ether');
      const listingPrice = await marketplaceContract.getListingPrice();

      transaction = await marketplaceContract.createMarketItem(
        nftContract.address,
        tokenId,
        priceInWei,
        { value: listingPrice }
      );
      
      await transaction.wait();
      alert('NFT created and listed successfully!');
    } catch (error) {
      console.error('Error minting NFT:', error);
      alert('Failed to create NFT!');
    }
  }

  return (
    <div className="create-nft">
      <h1>Create New NFT</h1>
      <input
        placeholder="Asset Name"
        onChange={e => updateFormInput({ ...formInput, name: e.target.value })}
      />
      <textarea
        placeholder="Asset Description"
        onChange={e => updateFormInput({ ...formInput, description: e.target.value })}
      />
      <input
        placeholder="Asset Price in ETH"
        type="number"
        onChange={e => updateFormInput({ ...formInput, price: e.target.value })}
      />
      <input
        type="file"
        name="Asset"
        onChange={onChange}
      />
      {fileUrl && (
        <img src={fileUrl} alt="Preview" style={{ width: '300px' }} />
      )}
      <button onClick={createMarket}>Create NFT</button>
    </div>
  );
}

export default CreateNFT;

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

๐Ÿ’ก IPFS ๋Œ€์•ˆ
IPFS ์™ธ์—๋„ ๋‹ค๋ฅธ ํƒˆ์ค‘์•™ํ™” ์ €์žฅ์†Œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด:
- Arweave: ์˜๊ตฌ ์ €์žฅ์„ ๋ณด์žฅํ•˜๋Š” ๋ธ”๋ก์ฒด์ธ ๊ธฐ๋ฐ˜ ์Šคํ† ๋ฆฌ์ง€
- Filecoin: IPFS์˜ ์ธ์„ผํ‹ฐ๋ธŒ ๋ ˆ์ด์–ด
- Pinata: IPFS ํ˜ธ์ŠคํŒ… ์„œ๋น„์Šค (์‚ฌ์šฉํ•˜๊ธฐ ์‰ฌ์›€)

๊ฐ๊ฐ ์žฅ๋‹จ์ ์ด ์žˆ์œผ๋‹ˆ ํ”„๋กœ์ ํŠธ ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž๊ฒŒ ์„ ํƒํ•˜๋ฉด ๋ผ!
NFT Creation Flow Upload Image Store on IPFS Mint NFT List on Market Detailed Process Frontend User uploads file Enters metadata Sets price Confirms transaction IPFS Storage Upload image Get image hash Upload metadata Get metadata URI Smart Contract Call createToken Mint NFT token Assign token ID Set token URI Marketplace Create market item Transfer to contract Set sale price Emit event

๐Ÿ” ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ

NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค๋Š” ์‹ค์ œ ๋ˆ์ด ์˜ค๊ฐ€๋Š” ์‹œ์Šคํ…œ์ด์•ผ. ๊ทธ๋ž˜์„œ ๋ณด์•ˆ์ด ์ •๋ง ์ค‘์š”ํ•ด! ํ•ด์ปค๋“ค์€ ํ•ญ์ƒ ์ทจ์•ฝ์ ์„ ์ฐพ๊ณ  ์žˆ๊ฑฐ๋“ . ๐Ÿ˜ฑ

๐Ÿ›ก๏ธ ์žฌ์ง„์ž… ๊ณต๊ฒฉ ๋ฐฉ์ง€

ReentrancyGuard๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰ ์ค‘์ผ ๋•Œ ๋‹ค์‹œ ํ˜ธ์ถœ๋˜๋Š” ๊ฑธ ๋ง‰์•„์•ผ ํ•ด. ์ด๊ฑด ๊ฐ€์žฅ ํ”ํ•œ ๊ณต๊ฒฉ ๋ฐฉ๋ฒ• ์ค‘ ํ•˜๋‚˜์•ผ.

๐Ÿ’ฐ ์ •์ˆ˜ ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ

Solidity 0.8.0 ์ด์ƒ์—์„œ๋Š” ์ž๋™์œผ๋กœ ์ฒดํฌ๋˜์ง€๋งŒ, ๊ทธ๋ž˜๋„ SafeMath ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์•ˆ์ „ํ•ด.

๐Ÿ”‘ ๊ถŒํ•œ ๊ด€๋ฆฌ

onlyOwner ๊ฐ™์€ modifier๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ค‘์š”ํ•œ ํ•จ์ˆ˜๋Š” ๊ถŒํ•œ์ด ์žˆ๋Š” ์‚ฌ๋žŒ๋งŒ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์•ผ ํ•ด.

โœ… ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ

๋ชจ๋“  ์ž…๋ ฅ๊ฐ’์€ require ๋ฌธ์œผ๋กœ ๊ฒ€์ฆํ•ด์•ผ ํ•ด. ๊ฐ€๊ฒฉ์ด 0๋ณด๋‹ค ํฐ์ง€, ์ฃผ์†Œ๊ฐ€ ์œ ํšจํ•œ์ง€ ๋“ฑ์„ ์ฒดํฌํ•˜๋Š” ๊ฑฐ์ง€.

๐Ÿ” ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

ํ•„์ˆ˜ ๋ณด์•ˆ ๊ฒ€ํ†  ํ•ญ๋ชฉ

โœ“ ์žฌ์ง„์ž… ๊ณต๊ฒฉ ๋ฐฉ์–ด
๋ชจ๋“  ์ƒํƒœ ๋ณ€๊ฒฝ์€ ์™ธ๋ถ€ ํ˜ธ์ถœ ์ „์— ์™„๋ฃŒํ•ด์•ผ ํ•ด. Checks-Effects-Interactions ํŒจํ„ด์„ ๋”ฐ๋ฅด๋Š” ๊ฑฐ์ง€.

function createMarketSale(address nftContract, uint256 itemId) 
    public payable nonReentrant {
    // 1. Checks
    require(msg.value == price, "Incorrect price");
    
    // 2. Effects (์ƒํƒœ ๋ณ€๊ฒฝ)
    idToMarketItem[itemId].sold = true;
    idToMarketItem[itemId].owner = payable(msg.sender);
    _itemsSold.increment();
    
    // 3. Interactions (์™ธ๋ถ€ ํ˜ธ์ถœ)
    seller.transfer(msg.value);
    IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
}

โœ“ ๊ฐ€์Šค ์ตœ์ ํ™”
๋ถˆํ•„์š”ํ•œ ์ €์žฅ์†Œ ์ฝ๊ธฐ/์“ฐ๊ธฐ๋ฅผ ์ค„์ด๊ณ , ๋ฃจํ”„๋ฅผ ์ตœ์†Œํ™”ํ•ด์•ผ ํ•ด. ๊ฐ€์Šค๋น„๊ฐ€ ๋„ˆ๋ฌด ๋น„์‹ธ๋ฉด ์‚ฌ์šฉ์ž๋“ค์ด ๋– ๋‚˜๊ฑฐ๋“ !

โœ“ ์ด๋ฒคํŠธ ๋กœ๊น…
๋ชจ๋“  ์ค‘์š”ํ•œ ์ƒํƒœ ๋ณ€๊ฒฝ์€ ์ด๋ฒคํŠธ๋กœ ๊ธฐ๋กํ•ด์•ผ ํ•ด. ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ถ”์ ํ•˜๊ธฐ๋„ ์‰ฝ๊ณ , ํˆฌ๋ช…์„ฑ๋„ ๋†’์•„์ ธ.

โœ“ ์—…๊ทธ๋ ˆ์ด๋“œ ๊ฐ€๋Šฅ์„ฑ
ํ”„๋ก์‹œ ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋ฉด ์ปจํŠธ๋ž™ํŠธ๋ฅผ ์—…๊ทธ๋ ˆ์ด๋“œํ•  ์ˆ˜ ์žˆ์–ด. ํ•˜์ง€๋งŒ ๋ณต์žกํ•˜๋‹ˆ๊นŒ ์ฒ˜์Œ์—๋Š” ๊ฐ„๋‹จํ•˜๊ฒŒ ์‹œ์ž‘ํ•˜๋Š” ๊ฒŒ ์ข‹์•„.

โš ๏ธ ๊ฐ์‚ฌ(Audit) ๋ฐ›๊ธฐ
์‹ค์ œ ๋ฉ”์ธ๋„ท์— ๋ฐฐํฌํ•˜๊ธฐ ์ „์—๋Š” ๋ฐ˜๋“œ์‹œ ์ „๋ฌธ๊ฐ€์˜ ๋ณด์•ˆ ๊ฐ์‚ฌ๋ฅผ ๋ฐ›์•„์•ผ ํ•ด. CertiK, OpenZeppelin, Trail of Bits ๊ฐ™์€ ํšŒ์‚ฌ๋“ค์ด ์œ ๋ช…ํ•ด. ๋น„์šฉ์ด ๋“ค์ง€๋งŒ ํ•ดํ‚น๋‹นํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค๋Š” ํ›จ์”ฌ ์ €๋ ดํ•˜์ง€!

โšก ๊ฐ€์Šค ์ตœ์ ํ™” ๊ธฐ๋ฒ•

์ด๋”๋ฆฌ์›€์˜ ๊ฐ€์Šค๋น„๋Š” ์ •๋ง ๋น„์‹ธ. ํŠนํžˆ ๋„คํŠธ์›Œํฌ๊ฐ€ ํ˜ผ์žกํ•  ๋•Œ๋Š” ๋”์šฑ ๊ทธ๋ž˜. ๊ทธ๋ž˜์„œ ๊ฐ€์Šค ์ตœ์ ํ™”๋Š” ํ•„์ˆ˜์•ผ! ๐Ÿ’ธ

๐Ÿ“Š ๊ฐ€์Šค๋ฅผ ๋งŽ์ด ์“ฐ๋Š” ์ž‘์—…๋“ค

๊ฐ€์Šค ๋น„์šฉ ์ˆœ์œ„
1. ์ €์žฅ์†Œ(Storage) ์“ฐ๊ธฐ - 20,000 ๊ฐ€์Šค
2. ์ €์žฅ์†Œ ์ฝ๊ธฐ - 2,100 ๊ฐ€์Šค
3. ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ - 3 ๊ฐ€์Šค
4. ๊ณ„์‚ฐ ์ž‘์—… - 3-5 ๊ฐ€์Šค

์ €์žฅ์†Œ ์ž‘์—…์ด ์••๋„์ ์œผ๋กœ ๋น„์‹ธ๋‹ค๋Š” ๊ฑธ ์•Œ ์ˆ˜ ์žˆ์ง€? ๊ทธ๋ž˜์„œ ์ €์žฅ์†Œ ์‚ฌ์šฉ์„ ์ตœ์†Œํ™”ํ•˜๋Š” ๊ฒŒ ํ•ต์‹ฌ์ด์•ผ!

๐ŸŽฏ ์ตœ์ ํ™” ๊ธฐ๋ฒ•๋“ค

1. ๋ณ€์ˆ˜ ํŒจํ‚น(Variable Packing)

Solidity๋Š” 32๋ฐ”์ดํŠธ ๋‹จ์œ„๋กœ ์ €์žฅ์†Œ๋ฅผ ์‚ฌ์šฉํ•ด. ๊ทธ๋ž˜์„œ ์ž‘์€ ๋ณ€์ˆ˜๋“ค์„ ๋ฌถ์–ด์„œ ์„ ์–ธํ•˜๋ฉด ๊ฐ€์Šค๋ฅผ ์ ˆ์•ฝํ•  ์ˆ˜ ์žˆ์–ด:

// ๋‚˜์œ ์˜ˆ - 3๊ฐœ์˜ ์Šฌ๋กฏ ์‚ฌ์šฉ
uint256 a;  // 32 bytes
uint128 b;  // 16 bytes
uint128 c;  // 16 bytes

// ์ข‹์€ ์˜ˆ - 2๊ฐœ์˜ ์Šฌ๋กฏ ์‚ฌ์šฉ
uint256 a;  // 32 bytes
uint128 b;  // 16 bytes
uint128 c;  // 16 bytes (๊ฐ™์€ ์Šฌ๋กฏ์— ์ €์žฅ๋จ)
2. ๋ฉ”๋ชจ๋ฆฌ vs ์ €์žฅ์†Œ

๊ฐ€๋Šฅํ•˜๋ฉด memory๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ๊ผญ ํ•„์š”ํ•  ๋•Œ๋งŒ storage๋ฅผ ์‚ฌ์šฉํ•ด:

// ๋น„ํšจ์œจ์ 
function getItems() public view returns (MarketItem[] memory) {
    MarketItem[] storage items = new MarketItem[](itemCount);
    // ...
}

// ํšจ์œจ์ 
function getItems() public view returns (MarketItem[] memory) {
    MarketItem[] memory items = new MarketItem[](itemCount);
    // ...
}
3. ๋ฃจํ”„ ์ตœ์ ํ™”

๋ฃจํ”„ ์•ˆ์—์„œ ์ €์žฅ์†Œ๋ฅผ ์ฝ์ง€ ๋ง๊ณ , ํ•œ ๋ฒˆ๋งŒ ์ฝ์–ด์„œ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅํ•ด:

// ๋น„ํšจ์œจ์  - ๋งค๋ฒˆ ์ €์žฅ์†Œ ์ฝ๊ธฐ
for (uint i = 0; i < items.length; i++) {
    if (items[i].sold) { ... }
}

// ํšจ์œจ์  - ํ•œ ๋ฒˆ๋งŒ ์ฝ๊ธฐ
uint length = items.length;
for (uint i = 0; i < length; i++) {
    MarketItem memory item = items[i];
    if (item.sold) { ... }
}
๐Ÿ”ฅ Pro Tip: Layer 2 ์†”๋ฃจ์…˜
๊ฐ€์Šค๋น„๊ฐ€ ๋„ˆ๋ฌด ๋ถ€๋‹ด๋œ๋‹ค๋ฉด Layer 2 ์†”๋ฃจ์…˜์„ ๊ณ ๋ คํ•ด๋ด:
- Polygon (Matic): ์ด๋”๋ฆฌ์›€๊ณผ ํ˜ธํ™˜๋˜๋ฉด์„œ ๊ฐ€์Šค๋น„๊ฐ€ ๊ฑฐ์˜ ๋ฌด๋ฃŒ
- Arbitrum: Optimistic Rollup ๊ธฐ์ˆ  ์‚ฌ์šฉ
- Optimism: ๋˜ ๋‹ค๋ฅธ Optimistic Rollup
- zkSync: Zero-Knowledge Rollup

ํŠนํžˆ Polygon์€ NFT ํ”„๋กœ์ ํŠธ์—์„œ ๋งŽ์ด ์‚ฌ์šฉํ•ด. OpenSea๋„ ์ง€์›ํ•˜๊ณ !

๐ŸŽญ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ํ•˜๊ธฐ

๊ธฐ๋ณธ ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค๊ฐ€ ์™„์„ฑ๋๋‹ค๋ฉด ์ด์ œ ๋” ์žฌ๋ฏธ์žˆ๋Š” ๊ธฐ๋Šฅ๋“ค์„ ์ถ”๊ฐ€ํ•ด๋ณผ ์ฐจ๋ก€์•ผ! ๐ŸŽ‰

๐Ÿ”จ ๊ฒฝ๋งค ์‹œ์Šคํ…œ

๊ณ ์ • ๊ฐ€๊ฒฉ ํŒ๋งค๋งŒ ์žˆ์œผ๋ฉด ์žฌ๋ฏธ์—†์ž–์•„? ๊ฒฝ๋งค ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด๋ณด์ž:

struct Auction {
    uint256 tokenId;
    address payable seller;
    uint256 startPrice;
    uint256 highestBid;
    address payable highestBidder;
    uint256 endTime;
    bool ended;
}

mapping(uint256 => Auction) public auctions;

function createAuction(
    address nftContract,
    uint256 tokenId,
    uint256 startPrice,
    uint256 duration
) public {
    require(IERC721(nftContract).ownerOf(tokenId) == msg.sender, "Not owner");
    
    IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
    
    auctions[tokenId] = Auction({
        tokenId: tokenId,
        seller: payable(msg.sender),
        startPrice: startPrice,
        highestBid: 0,
        highestBidder: payable(address(0)),
        endTime: block.timestamp + duration,
        ended: false
    });
}

function placeBid(uint256 tokenId) public payable {
    Auction storage auction = auctions[tokenId];
    
    require(block.timestamp < auction.endTime, "Auction ended");
    require(msg.value > auction.highestBid, "Bid too low");
    require(msg.value >= auction.startPrice, "Below start price");
    
    if (auction.highestBidder != address(0)) {
        auction.highestBidder.transfer(auction.highestBid);
    }
    
    auction.highestBid = msg.value;
    auction.highestBidder = payable(msg.sender);
}

function endAuction(address nftContract, uint256 tokenId) public {
    Auction storage auction = auctions[tokenId];
    
    require(block.timestamp >= auction.endTime, "Auction not ended");
    require(!auction.ended, "Already ended");
    
    auction.ended = true;
    
    if (auction.highestBidder != address(0)) {
        IERC721(nftContract).transferFrom(
            address(this),
            auction.highestBidder,
            tokenId
        );
        auction.seller.transfer(auction.highestBid);
    } else {
        IERC721(nftContract).transferFrom(
            address(this),
            auction.seller,
            tokenId
        );
    }
}

๊ฒฝ๋งค ์‹œ์Šคํ…œ์€ ์ข€ ๋” ๋ณต์žกํ•˜์ง€๋งŒ ์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ ์ธ๊ธฐ๊ฐ€ ๋งŽ์•„. ํŠนํžˆ ํฌ๊ท€ํ•œ NFT๋Š” ๊ฒฝ๋งค๋กœ ํŒ๋งคํ•˜๋ฉด ๋” ๋†’์€ ๊ฐ€๊ฒฉ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ๐Ÿ’ฐ

๐Ÿ’Ž ๋กœ์—ดํ‹ฐ ์‹œ์Šคํ…œ

ํฌ๋ฆฌ์—์ดํ„ฐ๊ฐ€ 2์ฐจ ํŒ๋งค์—์„œ๋„ ์ˆ˜์ต์„ ์–ป์„ ์ˆ˜ ์žˆ๊ฒŒ ๋กœ์—ดํ‹ฐ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด๋ณด์ž:

mapping(uint256 => address) public creators;
mapping(uint256 => uint256) public royaltyPercentage;

function setRoyalty(uint256 tokenId, uint256 percentage) public {
    require(creators[tokenId] == msg.sender, "Not creator");
    require(percentage <= 10, "Max 10%");
    royaltyPercentage[tokenId] = percentage;
}

function createMarketSaleWithRoyalty(
    address nftContract,
    uint256 itemId
) public payable nonReentrant {
    uint price = idToMarketItem[itemId].price;
    uint tokenId = idToMarketItem[itemId].tokenId;
    
    require(msg.value == price, "Incorrect price");
    
    uint royalty = (price * royaltyPercentage[tokenId]) / 100;
    uint sellerAmount = price - royalty;
    
    if (royalty > 0 && creators[tokenId] != address(0)) {
        payable(creators[tokenId]).transfer(royalty);
    }
    
    idToMarketItem[itemId].seller.transfer(sellerAmount);
    IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
    
    idToMarketItem[itemId].owner = payable(msg.sender);
    idToMarketItem[itemId].sold = true;
    _itemsSold.increment();
}

๋กœ์—ดํ‹ฐ ์‹œ์Šคํ…œ์€ ํฌ๋ฆฌ์—์ดํ„ฐ๋“ค์—๊ฒŒ ์ •๋ง ์ค‘์š”ํ•ด. ์ž์‹ ์˜ ์ž‘ํ’ˆ์ด ๊ณ„์† ๊ฑฐ๋ž˜๋  ๋•Œ๋งˆ๋‹ค ์ˆ˜์ต์„ ์–ป์„ ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ! ๋ณดํ†ต 5-10% ์ •๋„๋กœ ์„ค์ •ํ•ด. ๐ŸŽจ

๐Ÿ† ์ปฌ๋ ‰์…˜ ๊ธฐ๋Šฅ

NFT๋ฅผ ์ปฌ๋ ‰์…˜๋ณ„๋กœ ๋ฌถ์–ด์„œ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด๋ณด์ž:

struct Collection {
    string name;
    string symbol;
    address creator;
    uint256[] tokenIds;
}

mapping(uint256 => Collection) public collections;
uint256 public collectionCount;

function createCollection(
    string memory name,
    string memory symbol
) public returns (uint256) {
    collectionCount++;
    
    collections[collectionCount] = Collection({
        name: name,
        symbol: symbol,
        creator: msg.sender,
        tokenIds: new uint256[](0)
    });
    
    return collectionCount;
}

function addToCollection(
    uint256 collectionId,
    uint256 tokenId
) public {
    require(
        collections[collectionId].creator == msg.sender,
        "Not collection owner"
    );
    
    collections[collectionId].tokenIds.push(tokenId);
}

์ปฌ๋ ‰์…˜ ๊ธฐ๋Šฅ์ด ์žˆ์œผ๋ฉด ์‚ฌ์šฉ์ž๋“ค์ด ๊ด€๋ จ๋œ NFT๋“ค์„ ํ•œ๋ˆˆ์— ๋ณผ ์ˆ˜ ์žˆ์–ด์„œ ์ข‹์•„. ์˜ˆ๋ฅผ ๋“ค์–ด "Bored Ape Yacht Club" ๊ฐ™์€ ์œ ๋ช… ์ปฌ๋ ‰์…˜๋“ค์„ ์ƒ๊ฐํ•ด๋ด! ๐Ÿต

๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ ์ง€์›

์š”์ฆ˜์€ ๋ชจ๋ฐ”์ผ์—์„œ NFT๋ฅผ ๊ฑฐ๋ž˜ํ•˜๋Š” ์‚ฌ๋žŒ๋“ค์ด ์ •๋ง ๋งŽ์•„. ๊ทธ๋ž˜์„œ ๋ชจ๋ฐ”์ผ ์ง€์›์€ ํ•„์ˆ˜์•ผ! ๐Ÿ“ฑ

๋ชจ๋ฐ”์ผ ์ง€๊ฐ‘ ์—ฐ๋™
- WalletConnect: ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” ํ”„๋กœํ† ์ฝœ
- MetaMask Mobile: ๋ชจ๋ฐ”์ผ ์•ฑ ์ง€์›
- Coinbase Wallet: ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฌ์šด ์ธํ„ฐํŽ˜์ด์Šค
- Trust Wallet: ๋‹ค์–‘ํ•œ ์ฒด์ธ ์ง€์›

๐Ÿ”— WalletConnect ํ†ตํ•ฉ

import WalletConnect from "@walletconnect/client";
import QRCodeModal from "@walletconnect/qrcode-modal";

const connector = new WalletConnect({
  bridge: "https://bridge.walletconnect.org",
  qrcodeModal: QRCodeModal,
});

async function connectMobile() {
  if (!connector.connected) {
    await connector.createSession();
  }
  
  connector.on("connect", (error, payload) => {
    if (error) {
      throw error;
    }
    
    const { accounts, chainId } = payload.params[0];
    console.log("Connected:", accounts[0]);
  });
}

async function sendTransaction(tx) {
  if (!connector.connected) {
    await connectMobile();
  }
  
  const result = await connector.sendTransaction(tx);
  return result;
}

WalletConnect๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด QR ์ฝ”๋“œ๋ฅผ ์Šค์บ”ํ•ด์„œ ๋ชจ๋ฐ”์ผ ์ง€๊ฐ‘๊ณผ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ์–ด. ์ •๋ง ํŽธ๋ฆฌํ•˜์ง€? ๐Ÿ˜Š

๐ŸŽจ ๋ฐ˜์‘ํ˜• ๋””์ž์ธ ํŒ
- ํ„ฐ์น˜ ์˜์—ญ์„ ์ถฉ๋ถ„ํžˆ ํฌ๊ฒŒ (์ตœ์†Œ 44x44px)
- ์Šคํฌ๋กค ์„ฑ๋Šฅ ์ตœ์ ํ™”
- ์ด๋ฏธ์ง€ lazy loading
- ๋ชจ๋ฐ”์ผ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฉ”๋‰ด
- ๊ฐ€๋กœ/์„ธ๋กœ ๋ชจ๋“œ ๋ชจ๋‘ ์ง€์›

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ๋ชจ๋ฐ”์ผ UX๋Š” ์ •๋ง ์ค‘์š”ํ•ด!

๐Ÿ” ๊ฒ€์ƒ‰ ๋ฐ ํ•„ํ„ฐ๋ง

NFT๊ฐ€ ๋งŽ์•„์ง€๋ฉด ์›ํ•˜๋Š” ๊ฑธ ์ฐพ๊ธฐ ์–ด๋ ค์›Œ์ ธ. ๊ทธ๋ž˜์„œ ๊ฒ€์ƒ‰๊ณผ ํ•„ํ„ฐ๋ง ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•ด! ๐Ÿ”Ž

๐ŸŽฏ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ์—์„œ ํ•„ํ„ฐ๋ง

function fetchItemsByPriceRange(
    uint256 minPrice,
    uint256 maxPrice
) public view returns (MarketItem[] memory) {
    uint totalItemCount = _itemIds.current();
    uint itemCount = 0;
    
    for (uint i = 0; i < totalItemCount; i++) {
        if (
            idToMarketItem[i + 1].owner == address(0) &&
            idToMarketItem[i + 1].price >= minPrice &&
            idToMarketItem[i + 1].price <= maxPrice
        ) {
            itemCount += 1;
        }
    }
    
    MarketItem[] memory items = new MarketItem[](itemCount);
    uint currentIndex = 0;
    
    for (uint i = 0; i < totalItemCount; i++) {
        if (
            idToMarketItem[i + 1].owner == address(0) &&
            idToMarketItem[i + 1].price >= minPrice &&
            idToMarketItem[i + 1].price <= maxPrice
        ) {
            items[currentIndex] = idToMarketItem[i + 1];
            currentIndex += 1;
        }
    }
    
    return items;
}

function fetchItemsBySeller(address seller) 
    public view returns (MarketItem[] memory) {
    uint totalItemCount = _itemIds.current();
    uint itemCount = 0;
    
    for (uint i = 0; i < totalItemCount; i++) {
        if (idToMarketItem[i + 1].seller == seller) {
            itemCount += 1;
        }
    }
    
    MarketItem[] memory items = new MarketItem[](itemCount);
    uint currentIndex = 0;
    
    for (uint i = 0; i < totalItemCount; i++) {
        if (idToMarketItem[i + 1].seller == seller) {
            items[currentIndex] = idToMarketItem[i + 1];
            currentIndex += 1;
        }
    }
    
    return items;
}

๐Ÿ–ฅ๏ธ ํ”„๋ก ํŠธ์—”๋“œ ๊ฒ€์ƒ‰ UI

function SearchAndFilter() {
  const [nfts, setNfts] = useState([]);
  const [filteredNfts, setFilteredNfts] = useState([]);
  const [searchTerm, setSearchTerm] = useState('');
  const [priceRange, setPriceRange] = useState({ min: 0, max: 100 });
  const [sortBy, setSortBy] = useState('newest');

  useEffect(() => {
    filterAndSort();
  }, [searchTerm, priceRange, sortBy, nfts]);

  function filterAndSort() {
    let filtered = nfts.filter(nft => {
      const matchesSearch = nft.name.toLowerCase()
        .includes(searchTerm.toLowerCase());
      const matchesPrice = nft.price >= priceRange.min && 
        nft.price <= priceRange.max;
      return matchesSearch && matchesPrice;
    });

    if (sortBy === 'price-low') {
      filtered.sort((a, b) => a.price - b.price);
    } else if (sortBy === 'price-high') {
      filtered.sort((a, b) => b.price - a.price);
    } else if (sortBy === 'newest') {
      filtered.sort((a, b) => b.tokenId - a.tokenId);
    }

    setFilteredNfts(filtered);
  }

  return (
    <div className="search-filter">
      <input
        type="text"
        placeholder="Search NFTs..."
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      
      <div className="price-filter">
        <label>Price Range:</label>
        <input
          type="number"
          placeholder="Min"
          value={priceRange.min}
          onChange={(e) => setPriceRange({
            ...priceRange,
            min: parseFloat(e.target.value)
          })}
        />
        <input
          type="number"
          placeholder="Max"
          value={priceRange.max}
          onChange={(e) => setPriceRange({
            ...priceRange,
            max: parseFloat(e.target.value)
          })}
        />
      </div>
      
      <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
        <option value="newest">Newest First</option>
        <option value="price-low">Price: Low to High</option>
        <option value="price-high">Price: High to Low</option>
      </select>
      
      <div className="nft-grid">
        {filteredNfts.map((nft, i) => (
          <NFTCard key={i} nft={nft} />
        ))}
      </div>
    </div>
  );
}

๊ฒ€์ƒ‰๊ณผ ํ•„ํ„ฐ๋ง์ด ์žˆ์œผ๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ํ›จ์”ฌ ์ข‹์•„์ ธ. ํŠนํžˆ NFT๊ฐ€ ์ˆ˜์ฒœ ๊ฐœ๊ฐ€ ๋˜๋ฉด ํ•„์ˆ˜ ๊ธฐ๋Šฅ์ด์•ผ! ๐ŸŽฏ

๐Ÿ“Š ๋ถ„์„ ๋ฐ ํ†ต๊ณ„

๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค๋ฅผ ์šด์˜ํ•˜๋‹ค ๋ณด๋ฉด ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ํ†ต๊ณ„๊ฐ€ ํ•„์š”ํ•ด. ๊ฑฐ๋ž˜๋Ÿ‰, ์ธ๊ธฐ NFT, ํ™œ์„ฑ ์‚ฌ์šฉ์ž ๋“ฑ์„ ์ถ”์ ํ•ด์•ผ ํ•˜๊ฑฐ๋“ ! ๐Ÿ“ˆ

๐Ÿ“‰ ์˜จ์ฒด์ธ ํ†ต๊ณ„

struct Statistics {
    uint256 totalVolume;
    uint256 totalSales;
    uint256 totalListings;
    uint256 activeUsers;
}

Statistics public stats;

function updateStats(uint256 salePrice) internal {
    stats.totalVolume += salePrice;
    stats.totalSales += 1;
}

function getMarketStats() public view returns (
    uint256 totalVolume,
    uint256 totalSales,
    uint256 totalListings,
    uint256 averagePrice
) {
    totalVolume = stats.totalVolume;
    totalSales = stats.totalSales;
    totalListings = _itemIds.current() - _itemsSold.current();
    
    if (stats.totalSales > 0) {
        averagePrice = stats.totalVolume / stats.totalSales;
    } else {
        averagePrice = 0;
    }
    
    return (totalVolume, totalSales, totalListings, averagePrice);
}

function getTopSellers(uint256 limit) 
    public view returns (address[] memory, uint256[] memory) {
    // ํŒ๋งค๋Ÿ‰ ๊ธฐ์ค€ ์ƒ์œ„ ํŒ๋งค์ž ๋ฐ˜ํ™˜
    // ์‹ค์ œ ๊ตฌํ˜„์€ ๋” ๋ณต์žกํ•˜์ง€๋งŒ ๊ฐœ๋…์€ ์ดํ•ดํ–ˆ์„ ๊ฑฐ์•ผ!
}

๐Ÿ“Š ํ”„๋ก ํŠธ์—”๋“œ ๋Œ€์‹œ๋ณด๋“œ

function Dashboard() {
  const [stats, setStats] = useState({
    totalVolume: 0,
    totalSales: 0,
    totalListings: 0,
    averagePrice: 0
  });

  useEffect(() => {
    loadStats();
  }, []);

  async function loadStats() {
    const { signer } = await connectWallet();
    const marketplace = await getMarketplaceContract(signer);
    
    const data = await marketplace.getMarketStats();
    
    setStats({
      totalVolume: ethers.utils.formatEther(data.totalVolume),
      totalSales: data.totalSales.toNumber(),
      totalListings: data.totalListings.toNumber(),
      averagePrice: ethers.utils.formatEther(data.averagePrice)
    });
  }

  return (
    <div className="dashboard">
      <h1>Marketplace Statistics</h1>
      
      <div className="stats-grid">
        <div className="stat-card">
          <h3>Total Volume</h3>
          <p>{stats.totalVolume} ETH</p>
        </div>
        
        <div className="stat-card">
          <h3>Total Sales</h3>
          <p>{stats.totalSales}</p>
        </div>
        
        <div className="stat-card">
          <h3>Active Listings</h3>
          <p>{stats.totalListings}</p>
        </div>
        
        <div className="stat-card">
          <h3>Average Price</h3>
          <p>{stats.averagePrice} ETH</p>
        </div>
      </div>
    </div>
  );
}

ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค์˜ ๊ฑด๊ฐ• ์ƒํƒœ๋ฅผ ํ•œ๋ˆˆ์— ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์–ด. ํˆฌ์ž์ž๋“ค์—๊ฒŒ ๋ณด์—ฌ์ฃผ๊ธฐ๋„ ์ข‹๊ณ ! ๐Ÿ’ผ

๐ŸŒŸ ์‹ค์ „ ํŒ๊ณผ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

์ด์ œ ๊ฑฐ์˜ ๋‹ค ์™”์–ด! ๋งˆ์ง€๋ง‰์œผ๋กœ ์‹ค์ „์—์„œ ์œ ์šฉํ•œ ํŒ๋“ค์„ ๊ณต์œ ํ• ๊ฒŒ. ๐ŸŽ“

๐Ÿงช ์ถฉ๋ถ„ํ•œ ํ…Œ์ŠคํŠธ

๋ฉ”์ธ๋„ท ๋ฐฐํฌ ์ „์— ํ…Œ์ŠคํŠธ๋„ท์—์„œ ์ตœ์†Œ 2์ฃผ๋Š” ํ…Œ์ŠคํŠธํ•ด๋ด. ์‹ค์ œ ์‚ฌ์šฉ์ž๋“ค์„ ์ดˆ๋Œ€ํ•ด์„œ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์•„.

๐Ÿ“ ๋ฌธ์„œํ™”

์ฝ”๋“œ ์ฃผ์„๊ณผ README๋ฅผ ์ž˜ ์ž‘์„ฑํ•ด. ๋‚˜์ค‘์— ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ฐธ์—ฌํ•˜๊ฑฐ๋‚˜, ๋ฏธ๋ž˜์˜ ๋‚ด๊ฐ€ ๋ณผ ๋•Œ ๋„์›€์ด ๋ผ.

๐Ÿ”„ ๋ฒ„์ „ ๊ด€๋ฆฌ

Git์„ ์‚ฌ์šฉํ•ด์„œ ์ฝ”๋“œ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ , ์˜๋ฏธ ์žˆ๋Š” ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€๋ฅผ ์ž‘์„ฑํ•ด. ๋‚˜์ค‘์— ๋ฒ„๊ทธ๋ฅผ ์ฐพ์„ ๋•Œ ์ •๋ง ์œ ์šฉํ•ด!

๐Ÿ‘ฅ ์ปค๋ฎค๋‹ˆํ‹ฐ

Discord๋‚˜ Telegram ์ปค๋ฎค๋‹ˆํ‹ฐ๋ฅผ ๋งŒ๋“ค์–ด. ์‚ฌ์šฉ์ž๋“ค์˜ ํ”ผ๋“œ๋ฐฑ์„ ๋“ฃ๊ณ  ๊ฐœ์„ ํ•˜๋Š” ๊ฒŒ ์„ฑ๊ณต์˜ ์—ด์‡ ์•ผ.

๐Ÿš€ ๋ฐฐํฌ ํ›„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

๋Ÿฐ์นญ ์ „ ์ตœ์ข… ํ™•์ธ

โœ“ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ
- ๋ณด์•ˆ ๊ฐ์‚ฌ ์™„๋ฃŒ
- ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ 80% ์ด์ƒ
- ๊ฐ€์Šค ์ตœ์ ํ™” ์™„๋ฃŒ
- ์ด๋ฒคํŠธ ๋กœ๊น… ํ™•์ธ

โœ“ ํ”„๋ก ํŠธ์—”๋“œ
- ๋ชจ๋“  ๊ธฐ๋Šฅ ๋™์ž‘ ํ™•์ธ
- ๋ชจ๋ฐ”์ผ ๋ฐ˜์‘ํ˜• ํ…Œ์ŠคํŠธ
- ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๊ตฌํ˜„
- ๋กœ๋”ฉ ์ƒํƒœ ํ‘œ์‹œ

โœ“ ์ธํ”„๋ผ
- IPFS ๋ฐฑ์—… ์„ค์ •
- RPC ์—”๋“œํฌ์ธํŠธ ์ด์ค‘ํ™”
- ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ ๊ตฌ์ถ•
- ๋ฐฑ์—… ๊ณ„ํš ์ˆ˜๋ฆฝ

โœ“ ๋ฒ•๋ฅ  ๋ฐ ๊ทœ์ •
- ์ด์šฉ์•ฝ๊ด€ ์ž‘์„ฑ
- ๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ๋ฐฉ์นจ
- ์ €์ž‘๊ถŒ ์ •์ฑ…
- ์„ธ๊ธˆ ๊ด€๋ จ ๊ณ ๋ ค์‚ฌํ•ญ

๐Ÿ’ก ์„ฑ๊ณต์ ์ธ ๋Ÿฐ์นญ์„ ์œ„ํ•œ ๋งˆ์ผ€ํŒ…
- Twitter, Discord์—์„œ ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ตฌ์ถ•
- ์ธํ”Œ๋ฃจ์–ธ์„œ์™€ ํ˜‘์—…
- ์ดˆ๊ธฐ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ธ์„ผํ‹ฐ๋ธŒ ์ œ๊ณต
- ํ”„๋ ˆ์Šค ๋ฆด๋ฆฌ์Šค ๋ฐฐํฌ
- ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๊ฐœ๋ฐœ ์žฌ๋Šฅ ๊ณต์œ 

๊ธฐ์ˆ ๋งŒํผ ๋งˆ์ผ€ํŒ…๋„ ์ค‘์š”ํ•ด. ์ข‹์€ ์ œํ’ˆ์„ ๋งŒ๋“ค์—ˆ๋‹ค๋ฉด ์‚ฌ๋žŒ๋“ค์—๊ฒŒ ์•Œ๋ ค์•ผ์ง€!

๐ŸŽ“ ํ•™์Šต ๋ฆฌ์†Œ์Šค

NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค ๊ฐœ๋ฐœ์€ ๊ณ„์† ๋ฐฐ์›Œ์•ผ ํ•˜๋Š” ๋ถ„์•ผ์•ผ. ์—ฌ๊ธฐ ์œ ์šฉํ•œ ๋ฆฌ์†Œ์Šค๋“ค์„ ์ •๋ฆฌํ•ด๋ดค์–ด! ๐Ÿ“š

์ถ”์ฒœ ํ•™์Šต ์ž๋ฃŒ

๊ณต์‹ ๋ฌธ์„œ
- Solidity ๊ณต์‹ ๋ฌธ์„œ: https://docs.soliditylang.org/
- Hardhat ๋ฌธ์„œ: https://hardhat.org/docs
- OpenZeppelin ๋ฌธ์„œ: https://docs.openzeppelin.com/
- Ethers.js ๋ฌธ์„œ: https://docs.ethers.io/

์˜จ๋ผ์ธ ๊ฐ•์˜
- CryptoZombies: ๊ฒŒ์ž„์œผ๋กœ ๋ฐฐ์šฐ๋Š” ์†”๋ฆฌ๋””ํ‹ฐ
- Buildspace: ์‹ค์ „ ํ”„๋กœ์ ํŠธ ์ค‘์‹ฌ
- Alchemy University: ๋ฌด๋ฃŒ ๋ธ”๋ก์ฒด์ธ ๊ต์œก

์ปค๋ฎค๋‹ˆํ‹ฐ
- Ethereum Stack Exchange
- Reddit r/ethdev
- Discord ๊ฐœ๋ฐœ์ž ์ปค๋ฎค๋‹ˆํ‹ฐ๋“ค

๐Ÿ› ๏ธ ์œ ์šฉํ•œ ๋„๊ตฌ๋“ค

Remix IDE

๋ธŒ๋ผ์šฐ์ €์—์„œ ๋ฐ”๋กœ ์†”๋ฆฌ๋””ํ‹ฐ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋Š” ์˜จ๋ผ์ธ IDE์•ผ. ์ดˆ๋ณด์ž์—๊ฒŒ ์ตœ๊ณ !

Tenderly

์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋””๋ฒ„๊น… ๋„๊ตฌ. ํŠธ๋žœ์žญ์…˜์„ ์‹œ๊ฐ์ ์œผ๋กœ ๋ถ„์„ํ•  ์ˆ˜ ์žˆ์–ด.

Etherscan

๋ธ”๋ก์ฒด์ธ ์ต์Šคํ”Œ๋กœ๋Ÿฌ. ๋ฐฐํฌ๋œ ์ปจํŠธ๋ž™ํŠธ๋ฅผ ํ™•์ธํ•˜๊ณ  ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์–ด.

The Graph

๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ๋ฅผ ์ฟผ๋ฆฌํ•˜๋Š” ์ธ๋ฑ์‹ฑ ํ”„๋กœํ† ์ฝœ. ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ์กฐํšŒ์— ์œ ์šฉํ•ด.

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

์™€, ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์ง€? ๐ŸŽ‰ ์šฐ๋ฆฌ๋Š” NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ๋งŒ๋“ค์–ด๋ดค์–ด. ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ž‘์„ฑ๋ถ€ํ„ฐ ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๋™, ๋ณด์•ˆ, ์ตœ์ ํ™”๊นŒ์ง€ ์ •๋ง ๋งŽ์€ ๊ฑธ ๋ฐฐ์› ์–ด!

์†”๋ฆฌ๋””ํ‹ฐ๋กœ NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค๋ฅผ ๋งŒ๋“œ๋Š” ๊ฑด ์‰ฝ์ง€ ์•Š์€ ๋„์ „์ด์•ผ. ํ•˜์ง€๋งŒ ํ•œ ๋ฒˆ ๋งŒ๋“ค๊ณ  ๋‚˜๋ฉด ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์˜ ํ•ต์‹ฌ์„ ์ดํ•ดํ•˜๊ฒŒ ๋ผ. ์ด ์ง€์‹์€ ๋‹ค๋ฅธ DApp์„ ๋งŒ๋“ค ๋•Œ๋„ ์ •๋ง ์œ ์šฉํ•  ๊ฑฐ์•ผ. ๐Ÿ’ช

๐ŸŒŸ ๋‹ค์Œ ๋‹จ๊ณ„
์ด์ œ ๊ธฐ๋ณธ์„ ๋งˆ์Šคํ„ฐํ–ˆ์œผ๋‹ˆ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋‚˜์•„๊ฐ€๋ณด์ž:
- ๋‹ค๋ฅธ ๋ธ”๋ก์ฒด์ธ(Polygon, Solana ๋“ฑ)์— ๋ฐฐํฌํ•ด๋ณด๊ธฐ
- NFT ๋Œ€์—ฌ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
- DAO ๊ฑฐ๋ฒ„๋„Œ์Šค ํ†ตํ•ฉ
- ํฌ๋กœ์Šค์ฒด์ธ ๋ธŒ๋ฆฟ์ง€ ๊ตฌํ˜„
- AI ๊ธฐ๋ฐ˜ NFT ์ถ”์ฒœ ์‹œ์Šคํ…œ

๊ฐ€๋Šฅ์„ฑ์€ ๋ฌด๊ถ๋ฌด์ง„ํ•ด! ๐Ÿš€

๊ธฐ์–ตํ•ด, ์™„๋ฒฝํ•œ ์ฝ”๋“œ๋Š” ์—†์–ด. ๊ณ„์† ๊ฐœ์„ ํ•˜๊ณ  ๋ฐฐ์šฐ๋ฉด์„œ ๋ฐœ์ „ํ•˜๋Š” ๊ฑฐ์•ผ. ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์ฐธ์—ฌํ•˜๊ณ , ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค๊ณผ ์†Œํ†ตํ•˜๊ณ , ์ž์‹ ์˜ ํ”„๋กœ์ ํŠธ๋ฅผ ๊ณต์œ ํ•ด๋ด. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์ž์‹ ์˜ ๊ฐœ๋ฐœ ๊ฒฝํ—˜์„ ๋‚˜๋ˆ„๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ!

๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์€ ์ •๋ง ํฅ๋ฏธ์ง„์ง„ํ•œ ๋ถ„์•ผ์•ผ. ๊ธฐ์ˆ ์ด ๋น ๋ฅด๊ฒŒ ๋ฐœ์ „ํ•˜๊ณ  ์žˆ๊ณ , ์ƒˆ๋กœ์šด ๊ธฐํšŒ๋“ค์ด ๊ณ„์† ์ƒ๊ฒจ๋‚˜๊ณ  ์žˆ์–ด. ์ง€๊ธˆ ์‹œ์ž‘ํ•˜๋Š” ๊ฒŒ ์™„๋ฒฝํ•œ ํƒ€์ด๋ฐ์ด์•ผ! โฐ

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

ํ™”์ดํŒ…! ๋„Œ ํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ’ชโœจ

์ด ๊ธ€์ด ๋„์›€์ด ๋๋‹ค๋ฉด ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค๊ณผ๋„ ๊ณต์œ ํ•ด์ค˜. ํ•จ๊ป˜ ๋ฐฐ์šฐ๊ณ  ์„ฑ์žฅํ•˜๋Š” ๊ฒŒ Web3์˜ ์ •์‹ ์ด๋‹ˆ๊นŒ! ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ์œผ๋ฉด ์–ธ์ œ๋“  ์ปค๋ฎค๋‹ˆํ‹ฐ์— ๋ฌผ์–ด๋ด. ์šฐ๋ฆฌ ๋ชจ๋‘ ์ฒ˜์Œ์—” ์ดˆ๋ณด์˜€์–ด. ๐Ÿ˜Š

๊ทธ๋Ÿผ ํ–‰์šด์„ ๋นŒ์–ด! ๋ฉ‹์ง„ NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ด. ์–ธ์  ๊ฐ€ ๋„ค๊ฐ€ ๋งŒ๋“  ํ”Œ๋žซํผ์—์„œ ๊ฑฐ๋ž˜ํ•˜๋Š” ๋‚ ์„ ๊ธฐ๋Œ€ํ• ๊ฒŒ! ๐ŸŽจ๐Ÿš€

Happy Coding! ๐ŸŽ‰

Build the future of digital ownership

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

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

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

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