Blockchain

#33 NFT minting dApp 만들기 ch3

Sila 2022. 8. 5. 20:31

지난 글에 이어서 지금까진 민팅된 토큰이 종류별로, 랭크 별로 몇개가 있는지를 보여주는 기능, 토큰 리스팅 기능을 구현해보자.

 

우리가 ch2에서 컨트랙트를 수정할 때, 이중 배열을 이용해 랭크, 타입 별로 갯수를 저장해두고,

 

이 값을 가져올 수 있는 함수를 만들어두었다. 

 

지금까지 해왔던 것과 크게 다르지 않다.

 

원하는 데이터를 담을 변수(data), 그 데이터를 바꿔줄 함수(setData), useState를 선언하고,

 

컨트랙트와 상호작용해 데이터를 가져올 함수(getData)를 만든 후, 그 함수에서 setData함수를 호출한다.

 

getData함수는 적절한 때에 실행하고 그 결과값을 랜더링에 포함시키면 된다.

 

1.  token 통계 보기

/*  front/pages/index.tsx  */

// ...중략

const Home : NextPage = () => {
    // ...중략
    const [ tokenTable, setTokenTable ] = useState<string[][] | undefined> (undefined);
    
    const getTokenTable = async() => {
        try {
            if(!abcToken) return;
            
            const response = await abcToken.methods.getTokenCount().call();
            setTokentable(response)
        }
        catch(e) {
            console.error(e);
        }
    }
    
    // ...중략
    
    useEffect(() => {
        getRemain()
        getTokenTable()
    },[abcToken])
    
    return (
    <>
      <Flex bg='red.100' minH='100vh' justifyContent='center' alignItems='center'>
      <TableContainer>
          <Table>
            <Thead>
              <Tr>
                <Td>Rank/Type</Td>
                <Td>1</Td>
                <Td>2</Td>
                <Td>3</Td>
                <Td>4</Td>
              </Tr>
            </Thead>
            <Tbody>
              {
                tokenTable?.map((v,i) => {
                  return (
                    <Tr key = {i}>
                      <Td>{i+1}</Td>
                      {
                        v.map((j,w) => {
                          return (
                            <Td key={w}>{j}</Td>
                          )
                        })
                      }
                    </Tr>
                  )
                })
              }
            </Tbody>
          </Table>
        </TableContainer>
        <Text mb={4}> 남은 nft 갯수 : {remain} </Text>
        <Button colorScheme='blue'onClick={onOpen}> Minting </Button>
      </Flex>
      <MintingModal isOpen={isOpen} onClose={onClose} getRemain={getRemain} getTokenTable={getTokenTable}/>
    </>
  )
}

export default Home;

 

이중 배열을 가져와 해체해 랜더링 하는 것이므로 map method를 두 번 사용해주면 된다.

 

remain 변수때와 마찬가지로 민팅시 이 표도 실시간으로 그 값이 바뀌여야 하므로

 

MintingModal에 getTokenTable 함수를 getRemain함수와 마찬가지로 상속시켜준다.

 

( <MintingModal>에 getTokenTable이 추가됨 )

 

/*  front/components/MintingModal.tsx  */

// ...중략

interface MintingModalProps {
    // ...중략
    getTokenTable : () => Promise<void>;
}

const MintingModal:FC<MintingModalProps> = ({isOpen, onClose, getRemain, getTokenTable}) => {
    // ...중략
    
    const handleClick = async () => {
        try {
            // ...중략
            
            getMetadata(tokenURI);
            getRemain();
            getTokenTable();
        }
        catch(e) {
            console.error(e)
        }
    }
    
    // ...이하 생략
}

 

2.  마이 페이지 만들기

이제 새로운 페이지를 만들고 여기서 내 토큰 목록을 보고 토큰을 리스팅해보는 기능을 넣어보자.

 

이 부분은 솔리디티나 컨트랙트보다 nextjs의 페이징 기능과 페이지 레이아웃 등에 중점을 두고 있다.

 

components에 다음과 같이 Header, Layout 컨포넌트를 만들어주자.

 

/*  front/components/Header.tsx  */

import { Box, Button, Flex } from '@chakra-ui/react';
import Link from 'next/link';
import { FC } from 'react';
import useAccount from '../hooks/useAccount';

const Header:FC = () => {
    const {account} = useAccount();
    return (
        <Flex position="fixed" justifyContent="space-between" px="12" py="2" w="full" bg="white" >
            <Box><Link href="/"><Button size='sm' variant='ghost'>Home</Button></Link></Box>
            <Box>
                <Link href='mypage'><Button size='sm' variant='ghost'>my nft</Button></Link>
            </Box>
            <Box>{account}</Box>
        </Flex>
    );
}

export default Header;

 

/*  front/components/Layout.tsx  */

import { FC, ReactNode } from 'react';
import Header from './Header';

interface LayoutProps {
    children : ReactNode;
}

const Layout:FC<LayoutProps> = ({ children }) => {
    return (
        <>
        <Header />
            {children}
        </>
    );
}

export default Layout;

 

이 헤더를 포함한 레이아웃은 어떤 페이지에 있든 나타나게 해주고 싶다.

 

그래서 이를 _app.tsx에서 추가해줄 것이다.

 

import { ChakraProvider } from '@chakra-ui/react'
import type { AppProps } from 'next/app'
import Layout from '../components/Layout'

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ChakraProvider>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </ChakraProvider>
  );
};

export default MyApp;

 

Header 컴포넌트에서 myPage 버튼을 누르면 localhost:3000/mypage로 이동할텐데,

 

이 페이지에서 랜더링해줄 컴포넌트를 작성해주는 것이 다음으로 해줘야 할 일이다.

 

nextjs의 장점이 여기서 나오는데, 그냥 pages폴더에 이름만 잘 지어서 tsx 파일을 만들고 랜더링해줄 데이터들을 작성하면

 

따로 뭔가를 더 해줄 필요가 없다.

 

/*  front/pages/mypages.tsx  */

import { Flex } from "@chakra-ui/react";
import { FC } from "react";

const Mypage: FC = () => {
    return (
        <Flex bg='red.100' minH='100vh' justifyContent='center' alignItems='center'>
            mypage
        </Flex>
    )
};

export default Mypage;

 

잘 나오는지 확인해본 후, 우선 내가 가진 토큰들의 목록을 가져오는 기능을 넣어보자.

 

2.1 내가 가진 토큰 목록 불러오기

우선 web3, abcToken,saleToken을 가져온다.

 

/*  front/pages/mypages.tsx  */

import { Flex } from "@chakra-ui/react";
import { FC, useEffect, useState } from "react";
import MyTokenCard from "../components/MyTokenCard";

import useAccount from "../hooks/useAccount";
import useWeb3 from "../hooks/useWeb3";

import TokenData from "../interface/tokendata.interface";

const Mypage: FC = () => {
    const { account } = useAccount();
    const { abcToken, saleToken } = useWeb3();

    const [myTokens, setMyTokens] = useState<TokenData[] | undefined>(undefined)

    const getMyTokens = async () => {
        try {
            if(!account || !saleToken) return;
            
            const response = await saleToken.methods.getOwnerTokens(account).call();
            // 내가 가진 토큰들을 관련 정보를 전부 모아 가져온다.
            setMyTokens(response)
            // TokenData의 배열을 변수에 넣는다.
            
        }
        catch (e) {
            console.error(e)
        }
    }

    useEffect(() => {
        getMyTokens();
    }, [account, abcToken, saleToken])

    return (
        <Flex wrap= 'wrap' justifyContent='space-between' width="20" >
            {
                myTokens?.map((v) => {
                    return <MyTokenCard key={v.tokenId} TokenData={v} />
                })
            }
        </Flex>
    )
};

export default Mypage;

 

내가 가진 토큰들을 가져오는데, 이들을 하나의 틀에 맞춰서 가져오고 싶으므로 <MyTokenCard> 컴포넌트를 하나 더 만들어주자.

 

/*  front/components/MyTokenCard.tsx  */

import { Box, Text } from "@chakra-ui/react";
import { FC, useEffect, useState } from "react";
import { useMetaData } from "../hooks/useMetaData";
import TokenData from "../interface/tokendata.interface";
import TokenCard from "./TokenCard";

interface MyTokenCardProps {
    TokenData : TokenData;
}

const MyTokenCard : FC<MyTokenCardProps> = ({TokenData}) => {
    const { metadata, getMetaData } = useMetaData();
    const [ tokenPrice, setTokenPrice]  = useState<string>(TokenData.price)

    useEffect(() => {
        getMetaData('https://gateway.pinata.cloud/ipfs/Qmbn1xFmAkrUuEi9nhxcH3NysdsSAwYVF2sJtMghMUGUUF'+'/'+`${TokenData.Rank}`+'/'+`${TokenData.Type}`+'.json');
    }, [])
    
    return (
        <Box>
            <TokenCard metadata={metadata}></TokenCard>
        </Box>
    )
};

export default MyTokenCard;

 

getMetadata에는 내 메타데이터 json을 가지고 있는 uri를 인수로 넣어주면 된다.

 

metadata json안에 image uri에 대한 정보가 들어있어 이를 통해 이미지를 가져오는 것인데,

 

metadata와 image의 uri를 헷갈리지 않도록 주의하면서 진행한다.

 

<TokenCard>는 전에 만든 그 컴포넌트들 재사용하면 된다.

 

 

2.2 토큰 리스팅 - 위임하기

이제 내 토큰을 리스팅 하기에 앞서 현재 웹 사이트에 (dApp)에, 정확히는 saletoken 컨트랙트에 위임이 되어있는지를

 

사용자에게 알려주고, 위임 여부를 변경할 수 있는 기능을 추가해보자.

 

mypage에 approval 여부를 알려주는 함수를 추가한다.

 

/*  front/pages/mypage.tsx  */

// ...중략

const Mypage : FC = () => {
    // ...중략
    
    const [ approveStatus, setApproveStatus ] = useState<boolean>(false);
    
    // ...중략
    
    const getApproveStatus = async () =>  {
        try {
            if(!account || !abcToken || !saleToken) return;

            const response = await abcToken.methods.isApprovedForAll(account, (saleToken as any)._address).call()
            setApproveStatus(response)
        }
        catch (e) {
            console.error(e)
        }
    }
    
    useEffect(() => {
        getApproveStatus();
        getMyTokens();
    },[abcToken, saleToken, account])
    
    return (
        <>
            <Box py='12' px='12' bg='blue.100' minH='100vh'>
                <Box py={4} textAlign='center'>
                    <Text>
                        Approved : {approveStatus ? "true" : "false"}
                    </Text>
                </Box>
                <Flex wrap= 'wrap' justifyContent='space-between' width="20" >
                    {
                        myTokens?.map((v) => {
                            return <MyTokenCard key={v.tokenId} TokenData={v} />
                        })
                    }
                </Flex>
            </Box>
        </>
    );
};

export default Mypage;

 

지금은 처음 위임에 관련된 코드를 작성한 것이므로 당연히 false가 나올 것이다.

 

이 위임 여부를 바꿔주는 코드를 추가한다. 

 

/*  front/pages/mypage.tsx  */

// ...중략

const Mypage : FC = () => {
    
    // ...중략
    
    const handleClick = async () => {
        try {
            if(!account || !abcToken || !saleToken) return;
            
            const response = abcToken.methods.setApprovalForAll((saleToken as any)._address, !approveStatus)
            .send({
                from : account
            })
            
            if(response.status) {
                setApprovalStatus((prev) => !prev);
            }
        }    
        catch (e) {
            console.error(e)
        }
    }
    
}

 

이 컨트랙트와 상호작용해 위임 여부를 반전시켜주는 함수를 버튼 클릭을 통해 실행되게끔 버튼을 하나 추가해준다.

 

/*  front/pages/mypages.tsx  */

// ...중략

return (
    <>
          <Box py='12' px='12' bg='blue.100' minH='100vh'>
                <Box py={4} textAlign='center'>
                <Text>
                        Approved : {approveStatus ? "true" : "false"}
                </Text>
                <Button size='xs' ml='2' colorScheme={approveStatus ? 'red' : 'green'} onClick= { handleClick }>
                    {approveStatus ? "cancel" : "approve"}
                </Button>
                </Box>
                <Flex wrap= 'wrap' justifyContent='space-between' width="20" >
                    {
                        myTokens?.map((v) => {
                            return <MyTokenCard key={v.tokenId} TokenData={v} />
                        })
                    }
                </Flex>
            </Box>
    </>
);

 

2.3 토큰 리스팅

마지막으로 토큰을 리스팅해보자.

 

myTokenCard에서 판매중이지 않을경우 (price가 0일 경우) 가격을 입력해 제출하면 토큰을 리스팅해주고,

 

판매중일 경우 가격과 함께 판매중이라는 문구가 나오도록 해보자.

 

myTokenCard.tsx를 다음과 같이 수정한다.

 

/*  front/components/myTOkenCard.tsx  */

import { Box, Text } from "@chakra-ui/react";
import { FC, useEffect, useState } from "react";
import { useMetaData } from "../hooks/useMetaData";
import TokenData from "../interface/tokendata.interface";
import SaleInput from "./SaleInput";
import TokenCard from "./TokenCard";

interface MyTokenCardProps {
    TokenData : TokenData;
}

const MyTokenCard : FC<MyTokenCardProps> = ({TokenData}) => {
    const { metadata, getMetadata } = useMetaData();
    const [ tokenPrice, setTokenPrice] = useState<string>(TokenData.price)

    useEffect(() => {
        getMetadata(TokenData.tokenId);
    }, [])
    
    return (
        <Box>
            <TokenCard metadata={metadata}/>
            {
                tokenPrice === '0'
                ?
                <SaleInput tokenId={TokenData.tokenId} setTokenPrice = {setTokenPrice} />
                :
                <Text>"on sale" : {parseInt(tokenPrice)/10**18}</Text>
            }
        </Box>
    )
};

export default MyTokenCard;

 

판매중이라면 아래쪽의 <Text>가, 그렇지 않을 경우 <SaleInput>을 랜더링할 것이다.

 

SaleInput은 다음과 같이 따로 tsx파일을 만들어 작성한다.

 

/*  front/components/SaleInput.tsx  */

import { Button, Input, InputGroup } from "@chakra-ui/react";
import { Dispatch, FC, SetStateAction, useState } from "react";
import useAccount from "../hooks/useAccount";
import useWeb3 from "../hooks/useWeb3";

interface SaleInputProps {
    tokenId : string;
    setTokenPrice : Dispatch<SetStateAction<string>>;
}

const SaleInput:FC<SaleInputProps> = ({tokenId, setTokenPrice}) => {
    const [price, setPrice] = useState<string>("0");
    const {account} = useAccount()
    const { web3, saleToken } = useWeb3();

    const handleClick = async () => {
        try {
            if(!account || !web3 || !saleToken) return;
            const ETH_price = web3.utils.toWei(price, 'ether');
            const response = await saleToken.methods.ListingToken(tokenId, ETH_price).send({
                from : account
            })

            if(response.status) {
                setTokenPrice(ETH_price);
            }
        }
        catch (e) {
            console.error(e)
        }
    }
    
    return <>
        <InputGroup>
            <Input type='number' value={price} bg='white' onChange= {(e) => setPrice(e.target.value)}/>
        </InputGroup>
        <Button size='xs' colorScheme={'green'} onClick={handleClick}>
            request list
        </Button>
    </>;

}

export default SaleInput;

 

여기까지 하고 가격을 입력해 제출하면 가격과 함꼐 판매중이라는 문구를 볼 수 있을 것이다.

'Blockchain' 카테고리의 다른 글

ethersJS로 블록체인과 상호작용  (0) 2023.03.19
Hardhat  (1) 2022.11.02
#32 NFT Minting dApp 만들기 ch2  (0) 2022.08.05
#31 NFT minting dApp 만들기 ch1  (0) 2022.08.04
#30 ERC-721 토큰 만들기 Ch3  (0) 2022.08.02