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