7-2. UploadPhoto Component

  1. UploadPhoto 컴포넌트의 역할

  2. 컴포넌트 코드

  3. 컨트랙트와의 상호작용

  4. 리덕스 스토어의 데이터 업데이트: updateFeed 함수

1) UploadPhoto component's role

UploadPhoto 컴포넌트는 Klaytn 블록체인에 사진을 업로드하는 요청을 처리합니다. 다음과 같은 과정을 거쳐 이루어집니다.

  1. 트랜잭션을 전송하여 스마트 컨트랙트의 uploadPhoto 메서드를 호출합니다. uploadPhoto 메서드에서 새로운 ERC-721 토큰이 발행됩니다.

  2. 트랜잭션 전송 후 Toast 컴포넌트를 사용하여 트랜잭션 생명 주기로 진행 상황을 나타냅니다.

  3. 트랜잭션이 블록에 담기면 로컬의 리덕스 스토어에서 새로운 PhotoData로 업데이트합니다.

Limiting content size The maximum size of a single transaction is 32KB. So we restrict the input data (photo and descriptions) not to exceed 30KB to send it over safely.

  • 문자열 데이터의 크기는 2KB로 제한됩니다.

  • Photo is compressed to be less than 28KB using imageCompression() function.

2) Component code

// src/components/UploadPhoto.js

import React, { Component } from 'react'
import { connect } from 'react-redux'
import imageCompression from 'utils/imageCompression';
import ui from 'utils/ui'
import Input from 'components/Input'
import InputFile from 'components/InputFile'
import Textarea from 'components/Textarea'
import Button from 'components/Button'

import * as photoActions from 'redux/actions/photos'

import './UploadPhoto.scss'

// Set a limit of contents
const MAX_IMAGE_SIZE = 30 * 1024 // 30KB
const MAX_IMAGE_SIZE_MB = 30 / 1024 // 30KB

class UploadPhoto extends Component {
  state = {
    file: '',
    fileName: '',
    location: '',
    caption: '',
    warningMessage: '',
    isCompressing: false,
  }

  handleInputChange = (e) => {
    this.setState({
      [e.target.name]: e.target.value,
    })
  }

  handleFileChange = (e) => {
    const file = e.target.files[0]

    /**
     * If image size is bigger than MAX_IMAGE_SIZE(30KB),
     * Compress the image to load it on transaction
     * cf. Maximum transaction input data size: 32KB
     */
    if (file.size > MAX_IMAGE_SIZE) {
      this.setState({
        isCompressing: true,
      })
      return this.compressImage(file)
    }

    return this.setState({
      file,
      fileName: file.name,
    })
  }

  handleSubmit = (e) => {
    e.preventDefault()
    const { file, fileName, location, caption } = this.state
    this.props.uploadPhoto(file, fileName, location, caption)
    ui.hideModal()
  }

  compressImage = async (imageFile) => {
    try {
      const compressedFile = await imageCompression(imageFile, MAX_IMAGE_SIZE_MB)
      this.setState({
        isCompressing: false,
        file: compressedFile,
        fileName: compressedFile.name,
      })
    } catch (error) {
      this.setState({
        isCompressing: false,
        warningMessage: '* Fail to compress image'
      })
    }
  }

  render() {
    const { fileName, location, caption, isCompressing, warningMessage } = this.state
    return (
      <form className="UploadPhoto" onSubmit={this.handleSubmit}>
        <InputFile
          className="UploadPhoto__file"
          name="file"
          label="Search file"
          fileName={isCompressing ? 'Compressing image...' : fileName}
          onChange={this.handleFileChange}
          err={warningMessage}
          accept=".png, .jpg, .jpeg"
          required
        />
        <Input
          className="UploadPhoto__location"
          name="location"
          label="Location"
          value={location}
          onChange={this.handleInputChange}
          placeholder="Where did you take this photo?"
          required
        />
        <Textarea
          className="UploadPhoto__caption"
          name="caption"
          value={caption}
          label="Caption"
          onChange={this.handleInputChange}
          placeholder="Upload your memories"
          required
        />
        <Button
          className="UploadPhoto__upload"
          type="submit"
          title="Upload"
        />
      </form>
    )
  }
}

const mapDispatchToProps = (dispatch) => ({
  uploadPhoto: (file, fileName, location, caption) =>
    dispatch(photoActions.uploadPhoto(file, fileName, location, caption)),
})

export default connect(null, mapDispatchToProps)(UploadPhoto)

3) Interact with contract

Klaytn에 사진 데이터를 쓰는 함수를 만들어보겠습니다. Send transaction to contract: uploadPhoto Unlike read-only function calls, writing data incurs a transaction fee. 트랜잭션 수수료는 사용한 gas의 양에 따라 결정됩니다. gas는 트랜잭션을 처리하는 데에 필요한 계산량을 나타내는 측정 단위입니다.

따라서 트랜잭션을 보내려면 fromgas 두 속성이 있어야 합니다.

  1. 트랜잭션에 올릴 수 있도록 사진 파일을 바이트 문자열로 변환합니다.

    (In Klaystagram contract, we defined photo fotmat as bytes in PhotoData struct)

    • FileReader를 사용하여 ArrayBuffer로 사진 데이터를 읽어옵니다.

    • ArrayBuffer를 16진수 문자열로 변환합니다.

    • Add Prefix 0x to satisfy bytes format

  2. Invoke the contract method: uploadPhoto

    • from: An account that sends this transaction and pays the transaction fee.

    • gas: The maximum amount of gas that the from account is willing to pay for this transaction.

  3. After sending the transaction, show progress along the transaction lifecycle using Toast component.

  4. If the transaction successfully gets into a block, call updateFeed function to add the new photo into the feed page.

// src/redux/actions/photo.js

export const uploadPhoto = (
  file,
  fileName,
  location,
  caption
) => (dispatch) => {
  // 1. Convert photo file as a hex string to load on transaction
  const reader = new window.FileReader()
  reader.readAsArrayBuffer(file)
  reader.onloadend = () => {
    const buffer = Buffer.from(reader.result)

    // Add prefix `0x` to hexString to recognize hexString as bytes by contract
    const hexString = "0x" + buffer.toString('hex')

    // 2. Invoke the contract method: uploadPhoto
    // Send transaction with photo file(hexString) and descriptions
    try{
      KlaystagramContract.methods.uploadPhoto(hexString, fileName, location, caption).send({
        from: getWallet().address,
        gas: '200000000',
      }, (error, txHash) => {
        if (error) throw error;

        // 3. After sending the transaction,
        // show progress along the transaction lifecycle using `Toast` component.
        ui.showToast({
          status: 'pending',
          message: `Sending a transaction... (uploadPhoto)`,
          txHash,
        })
      })
        .then((receipt) => {
          ui.showToast({
            status: receipt.status ? 'success' : 'fail',
            message: `Received receipt! It means your transaction is
            in klaytn block (#${receipt.blockNumber}) (uploadPhoto)`,
            link: receipt.transactionHash,
          })

          // 4. If the transaction successfully gets into a block,
          // call `updateFeed` function to add the new photo into the feed page.
          if(receipt.status) {
            const tokenId = receipt.events.PhotoUploaded.returnValues[0]
            dispatch(updateFeed(tokenId))
          }
        })
    } catch (error) {
      ui.showToast({
        status: 'error',
        message: error.toString(),
      })
    }
  }
}

cf) Transaction life cycle

After sending transaction, you can get transaction life cycle (transactionHash, receipt, error).

  • transactionHash 이벤트는 서명된 트랜잭션 인스턴스가 올바르게 구성되면 발생합니다. 네트워크에 트랜잭션을 전송하기 전에 트랜잭션 해시를 구할 수 있습니다.

  • receipt 이벤트는 트랜잭션 영수증을 받을 때 발생합니다. 이는 트랜잭션이 블록에 들어갔음을 의미합니다. receipt.blockNumber으로 블록 번호를 확인할 수 있습니다.

  • error 이벤트는 어떤 문제가 있을 때 발생합니다.

4. 피드 페이지의 사진 업데이트: updateFeed

After successfully sending the transaction to the contract, FeedPage needs to be updated. In order to update the photo feed, we need to get the new photo data we've just uploaded. tokenId와 함께 getPhoto()를 호출합니다. tokenId는 트랜잭션 영수증에서 찾을 수 있습니다. 그다음 새 사진의 데이터를 로컬 리덕스 스토어에 저장하세요.

// src/redux/actions/photo.js

/**
 * 1. Call contract method: getPhoto()
 * To get new photo data we've just uploaded,
 * call `getPhoto()` with tokenId from receipt after sending transaction
*/
const updateFeed = (tokenId) => (dispatch, getState) => {
  KlaystagramContract.methods.getPhoto(tokenId).call()
    .then((newPhoto) => {
      const { photos: { feed } } = getState()
      const newFeed = [feedParser(newPhoto), ...feed]

      // 2. 리덕스 스토어에 새로운 피드를 업데이트합니다.
      dispatch(setFeed(newFeed))
    })
}

Last updated