[DreamChain DApp] #24 Request Download 페이지 구현

in #kr6 years ago

이전글 - [DreamChain DApp] #23 Story Details 페이지 구현 2

본 내용은 Ethereum and Solidity: The Complete Developer's Guide을 참고해서 작성되었습니다.


이번에 꾸며볼 페이지는 아래와 같은 Downloads 요청 페이지입니다.

DreamStory에서 다음과 같이 구조체와 이 구조체를 배열로 저장하는 상태 변수를 만들었었습니다.

// download struct
    struct Download {
        // address of the downloder
        address downloader;
        // download price in wei
        uint price_wei;
        // download date
        uint date;
    }

    // download history
    Download[] public downloads;

다운로드한 계정, 금액, 시간(정확히는 블락타임)을 저장하는 구조체 배열입니다. contributor가 다운로드 요청하면 그 기록을 남기는 것입니다. 그리고 View Downloads 페이지에서 그 이력들을 보여줄 것입니다. 그런데 한가지 문제가 있습니다.

다운로드 페이지를 꾸미기 위해서, 단순히 downloads 배열만 리턴하면 될텐데, 현실은 그리 녹녹치 않습니다. 알아보니 현재 솔리디티는 구조체 배열을 리턴할 수 없습니다. 향후에 가능하게 하도록 준비중이라는 말은 보입니다만, 지금은 안되나 봅니다.

그래서 부득이하게 스마트 컨트랙트 소스 코드를 변경해야 합니다. 이전글에서 스마트 컨트랙트 수정할 것도 같이 반영하여 컴파일, 배포하도록 할것입니다.

컨트랙트 수정 및 재배포

수정할 것은 별거 없습니다. struct 배열 리턴이 안되기 때문에 배열의 index를 이용하여 하나 하나 배열 요소에 접근하는 방식을 택할 것입니다. 그러기 위해서 배열의 크기를 알아야 합니다. 사실 getSummary 함수에서 downloads 배열의 크기를 리턴하긴 하지만, 값 하나만 필요한데 다른 값들까지 리턴할 필요는 없어서 별도의 함수를 만들도록 합니다. 아래와 같이 DreamFactory.sol 파일에서 DreamStory Contract에 함수를 추가합니다. 그저 배열의 크기를 리턴하는 함수입니다.

    /*
     * Get the number of downloads
     * @return the length of downloads instance
     */
    function getDownloadsCount() public view returns (uint) {
        return downloads.length;
    }

이전글에서 언급했듯이, contribute 함수도 아래와 같이 수정합니다.

function contribute() public payable {
        // check if the money is greater than zero
        require( msg.value > 0 );
        // increase the vote counts only if the sender is in the contributor list
        if( !contributors[msg.sender] )
        {
           votes_count++;
           // set contributor address to true
           contributors[ msg.sender ]= true;
        }
    }

ethereum 폴더로 이동하여 아래와 같이 compile.js와 deploy.js를 실행합니다.

$ node compile.js
$ node deploy.js

반드시 compile.js의 결과를 확인하셔야 합니다. 혹시 에러가 발생한다면, Remix로 컨트랙트 코드를 복사하여 에러를 확인하고 수정해야 합니다.

다음으로 dream_factory.js에서 컨트랙트의 주소를 새로 배포된 주소로 변경해 줘야 합니다. 컨트랙트 몇 번 배포해 보니 어떤 어떤 작업을 해야 하는지 이제 좀 감이 오시죠?

새로 컨트랙트를 배포했기 때문에, 이전에 생성했던 DreamStory 컨트랙트들은 사라졌습니다. 이렇게요.

완전히 사라진건 아니고요. 컨트랙트 주소만 알고 있으면 언제든지 접근가능합니다. 이럴 땐, Remix가 용이하겠죠?
동일한 story를 입력하려고 Remix로 예전 컨트랙트에 접근하여 getSummary 함수로 내용을 읽어 봅니다.

이제 이정도의 작업은 익숙하시죠?

새로 배포한 컨트랙트에 접근하여 "Create a Story"를 클릭하여 스토리를 만들고, contribute도 합니다. 그런 후에 다음과 같이 Request Download 컴포넌트 구성을 합니다.

컴포넌트 구성

위 화면을 보면 이미 이전에 다 구성했던 컴포넌트들입니다. 특히 story_details 페이지와 매우 유사합니다. 여기서는 그저 필요한 것만 쏙쏙 가져와서 꾸미면 되겠습니다. 왜 story_details 페이지에서 바로 다운로드 하는 기능을 넣지 않느냐 하고 궁금해 하시는 분들이 있을 것입니다. 저도 그렇고 싶었는데, 일단 Form 태그에 이벤트 핸들러를 두 개 연결해야 하는데, 그게 잘 안됐고, Form 태그를 두 개 쓰려고 했더니 또 태그를 연달아 쓸 수 없다나 뭐라나 해서 그냥 원래 의도대로 분리하기로 했습니다. 아시는 분은 댓글 남겨주시면 고맙겠습니다.

구성은 story_details 페이지와 거의 동일하고 버튼 이름과 몇가지 레이블 이름만 변경시켰습니다. 이렇게요.

한가지 추가한 것은 Story 작가의 balance를 표시해봤습니다. 작가의 잔고를 보면 인기작가 인지 아닌지 알 수 있을거 같네요.

request_download 페이지의 소스코드에 대한 설명은 따로 하지 않겠습니다. story_detail 페이지 내용을 참고하세요.

import React, {Component } from 'react';
// import Form, Button from semantic-ui-react
import { Card, Icon } from 'semantic-ui-react';
// import Grid, Input, Form, Message, Button
import { Grid, Input, Form, Message, Button } from 'semantic-ui-react';
// import Container and Header
import { Container, Header } from 'semantic-ui-react';
// import Link, Router
import { Link, Router } from '../../routes';
// import layout
import Layout from '../../components/layout';
// import DreamStory instance
import dream_story from '../../ethereum/dream_story';
// import web3
import web3 from '../../ethereum/web3';

class RequestDownload extends Component {
  // state for form inputs
  state = {
    down_price: '',
    error_msg: '',
    loading: false
  };

  // get initial properties
  // the DreamStory address can be obtained from the argument props using the url
  // since the url includes the contract address
  static async getInitialProps( props ) {
    // get the DreamStory instance of the address
    const story= dream_story( props.query.address );
    // get summary of the story
    const summary= await story.methods.getSummary().call();
    // get author's balance
    const author_balance= await web3.eth.getBalance( summary[5] );
    // return the summary with labels
    return {
      address: props.query.address,
      balance: web3.utils.fromWei( summary[0], 'ether' ),
      author_balance: web3.utils.fromWei( author_balance, 'ether' ),
      votes_count: summary[1],
      downloads_count: summary[2],
      min_down_price: web3.utils.fromWei( summary[3], 'ether' ),
      approvers_count: summary[4],
      author: summary[5],
      story_title: summary[6],
      story: summary[7]
    };
  }

  // event handler for download button
  onDownload = async () => {
    // block default submitting the form
    event.preventDefault();
    // set button loading and clear error message
    this.setState({ loading: true, error_msg: '' });
    // catch any error while executing the following
    try {
      // get all accounts of a user and use the accounts[0] to download
      const accounts= await web3.eth.getAccounts();
      // get the DreamStory instance of the address
      const story= dream_story( this.props.address );
      // convert download price to wei
      const down_price_wei= web3.utils.toWei( this.state.down_price, 'ether' );
      // call download function using the user's first account
      // use metamask's functinality to estimate the gas limit
      await story.methods.download()
      .send({
        from: accounts[0],
        value: down_price_wei
      });
      // redirect to the download list page
      Router.replaceRoute(`/dream_stories/${this.props.address}/downloads_list`);
    } catch (error) {
      this.setState( { error_msg: error.message } );
    }
    // clear loading
    this.setState({ loading: false });
  };

  renderActionButtons() {
    return (
      <Form onSubmit={this.onDownload} error={!!this.state.error_msg}>
        <Form.Field>
          <label>Amount to pay for Download</label>
          <Input
            label="ether"
            labelPosition="right"
            placeholder='0.001'
            value={ this.state.down_price }
            onChange={ event => this.setState( { down_price: event.target.value } ) }
          />
        </Form.Field>
        <Message error header="Failed!" content={ this.state.error_msg } />
        <Button loading={this.state.loading} primary>Download</Button>
        <p></p>
        <Link route={`/dream_stories/${this.props.address}/downloads_list`}>
          <a>
            <Button primary>View Downloads</Button>
          </a>
        </Link>
      </Form>
    );
  }

  // render
  render() {
    return (
      <Layout>
        <h2>Request Download</h2>
        <Grid>
          <Grid.Column width={10}>
            <Container text>
              <Header as='h3'>{this.props.story_title}</Header>
              <p>{ this.props.story }</p>
            </Container>
          </Grid.Column>
          <Grid.Column width={6}>
            <Card>
              <Card.Content header='Story Statistics' />
              <Card.Content extra>
                <Icon name='dollar sign' />
                {this.props.author_balance} (author balance)
              </Card.Content>
              <Card.Content extra>
                <Icon name='dollar sign' />
                {this.props.balance} (balance)
              </Card.Content>
              <Card.Content extra>
                <Icon name='user' />
                {this.props.votes_count} (votes)
              </Card.Content>
              <Card.Content extra>
                <Icon name='download' />
                {this.props.downloads_count} (downloads)
              </Card.Content>
              <Card.Content extra>
                <Icon name='cart arrow down' />
                {this.props.min_down_price} (minimum download price )
              </Card.Content>
            </Card>
            { this.renderActionButtons() }
          </Grid.Column>
        </Grid>
      </Layout>
    );
  }
}

export default RequestDownload;

한가지 다른 점은 Download 트랜잭션이 완료되면 새로고침하는 것이 아니라 View Downloads 페이지로 이동하게끔 라우팅 했습니다.

(생략)
await story.methods.download()
.send({
  from: accounts[0],
  value: down_price_wei
});
// redirect to the download list page
Router.replaceRoute(`/dream_stories/${this.props.address}/downloads_list`);
(생략)

아직 view_downloads 페이지는 별다른 내용이 없습니다. 다음글에서 페이지를 구현해 보겠습니다. 이제 앞으로 페이지 하나만 더 구현하면 DreamChain DApp 개발도 마무리입니다. 그런데 왠지 컨트랙트 소스를 몇 번 더 변경할 거 같은 예감이 듭니다 ^^;

오늘의 실습: 꿈일기를 다운로드(저작권)해서 어디에 쓸 수 있을까요?

Coin Marketplace

STEEM 0.15
TRX 0.12
JST 0.025
BTC 55262.33
ETH 2465.44
USDT 1.00
SBD 2.18