Amplify & React에서 미팅용 화제 투고 & 화제 추첨 홈페이지 앱을 만들었어요.

기존에 쓴 것일단 움직여!Cloud9 &React & Amplify &GraphiQL의 환경 구축을 바탕으로 앰플리프 앤 리액트로 웹 애플리케이션을 실제 제작했다.
신입사원 환영회에서 이용할 예정이었으나 웹 앱에 의존하지 않아도 흥분해 소장했다.그 후에 다른 기회를 썼어요.
참고한 문장은 환경을 구축할 때의 문장 외에도 다음과 같은 문장이 있다
https://qiita.com/laughingman/items/cab91e7dabc952247953
https://zenn.dev/enish/articles/3c760395d54d89

전제 조건


일단 움직여!Cloud9 &React & Amplify &GraphiQL의 환경 구축가 시작지점이기 때문에 환경을 구축하는 전제에서 대화를 나눈다.
기본적으로 상세한 설명을 건너뛰면 필요한 파일을 덮어쓰면 시작할 수 있다.(대략,)

만든 물건




위의 그림에서 보듯이 나는 이런 것을 했다.
간단하게 정리를 하자면.
  • 주제를 뽑는 투고와 화제
  • 남이 투고한 화제와 자신이 투고한 화제를 실시간으로 반영
  • 기고와 추첨을 라벨로 구분
  • 최신 발언을 최고치
  • 로 표시
  • 최신 15가지 화제 집중
  • 30분 동안 발표된 화제
  • 에 집중 표시
  • 에 나타난 화제가 1건 이하일 경우 추첨할 수 없음
  • 화제 추첨에서 다른 사람은 투고할 수 있지만 추첨한 사람은 투고할 수 없다
  • 이런 느낌.

    이 보도에서 언급하지 마라


    화제에 한정된 기고와 화제의 추첨 기사이기 때문에 실제 여러 사람이 사용하려면 EC2에 React의 포트 해방과 자원 정책을 설정해야 하지만 이 글은 다루지 않는다.

    schema의 정의 변경


    다음 파일을 수정하여 schema의 정의를 수정하고 백엔드에 반영합니다.
  • react 프로젝트 부하/amplify/backend/appi/{api 이름}/schema.graphql
  • 원래 정의를 모두 삭제하고 로 덮어씁니다.
    schema.graphql
    type Post
      @model
      @key(
        name: "SortByCreatedAt"
        fields: ["postOwnerId", "createdAt"]
        queryField: "listPostsSortByCreatedAt"
      )
     {
      id: ID!
      postOwnerId: String!
      question: String!
      createdAt: String!
      updatedAt: String
    }
    
    덮어쓰면 다음 명령을 사용하여 서버에 반영됩니다.명령은 Cloud9의 터미널에서 React 항목에 할당되어 실행됩니다.
    amplify push -y
    
    다음 세 가지를 충족시키기 위해 GraphiQL의 Filter와sort를 사용했다.
  • 최신 15가지 화제 집중
  • 30분 동안 발표된 화제
  • 에 집중 표시
  • 최신 발언을 최고치
  • 로 표시
    @model을 줄 때 자동으로 생성되는'createdAt'와'updatedAt'를 사용하여 Filter와sort를 진행합니다.sort용 해석기에서'listPostsSortByCreatedAt'를 정의하고'createdAt'sort를 사용합니다.
    이 부분에 대한 자세한 설명은 App입니다.js쪽에서 진행해.

    App.js의 실현


    태그 추가


    탭, 다음 명령을 사용하여 React 프로젝트에 라이브러리를 추가합니다.리액트 프로젝트는 야런으로 통일된 것이기 때문에 야런으로 추가합니다.
    yarn add react-tabs
    
    다음은 App입니다.js로 라이브러리 가져오기
    import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
    import 'react-tabs/style/react-tabs.css';
    
    "아무튼 움직이고 싶어!"이런 사람들은 프로그램 라이브러리를 가져온 후에 앱을 사용할 수 있다.전체 js의 코드를 복사하고 외관을 수정한 다음cloud9의 터미널에서'yarnstart'가 이동하는 것을 볼 수 있습니다.

    화제 투고의 처리


    투고 화제의 문자열을'postedQuestion '으로 설정하고, 앞부분의 현재 날짜를sort용'createdAt' 로 설정합니다.전단의 현재 날짜와 시간을 설정하는 것은 가져오는 처리에서 전단의 날짜와 시간을 추출하기 위해서입니다.발언 처리 후 화제 문자열을 빈 문자로 설정합니다.
    또 화제를 입력하지 않기 위해 기고문도 전혀 처리하지 않고 검증하고 있다.
      createPost = async() => {
    
        // バリデーションチェック
        if (this.state.postedQuestion === '') {
          return
        }
    
        // 新規登録 mutation
        const createPostInput = {
          question: this.state.postedQuestion,
          postOwnerId: "validid",
          createdAt: new Date(),
        }
    
        // 登録処理
        try {
          await API.graphql(graphqlOperation(mutations.createPost, { input: createPostInput }))
          this.setState({ postedQuestion: "" })
        }
        catch (e) {
          console.log(e)
        }
      }
    

    잡담


    postOwnnerId에 관해서는 일의 논리와 직접적인 관계가 없지만 @key의fields는 PK와 SK의 제약이 있기 때문에(fields의 첫 번째 파라미터는 PK,fields의 두 번째 파라미터는sort조건) schema로 정의하여 조건을 추출할 때 값을 지정한다.이 점에 대해서는 특제 해석기를 배운 뒤 필요 없는 논리가 없는 아름다운 실현으로 수정했다.
    ("validid"문자열이 설정되어 있지만, 이 문자열은 판정 조건과 표시에 사용되지 않습니다. 설정하지 않으면 오류가 발생할 수 있습니다. "학습 부족 ~")

    Filter와sort의 화제를 처리하기


    상술한 세 가지 일을 만족시키기 위해 다음과 같은 처리를 실시하였다.
  • 최신 15가지 화제 집중
  • 30분 동안 발표된 화제
  • 에 집중 표시
  • 최신 발언을 최고치
  • 로 표시
    투고한 화제를 조건으로 얻은 처리는 다음과 같다.
      getFilterdQuestion = async() => {
    
        // 30分前までの質問に絞り込み
        const postedMinutesAgo = 30
        let targetDate = new Date()
        targetDate = targetDate.setMinutes(targetDate.getMinutes() - postedMinutesAgo)
        let dateString = new Date(targetDate).toISOString()
    
        try {
          const filterObj = {
            and: [{
              question: { ne: "" },
              updatedAt: { ge: dateString }
            }]
          }
          // 表示する質問の件数
          const questionLimit = 15
          // 表示する質問の数を設定して、最新の質問を上に表示する
          let posts = await API.graphql({
            query: query.listPostsSortByCreatedAt,
            variables: {
              postOwnerId: "validid",
              sortDirection: "DESC",
              filter: filterObj,
              limit: questionLimit,
            }
          })
          let questionList = posts.data.listPostsSortByCreatedAt.items.filter((post) => {
            return this.checkQuestion(post)
          })
          // 質問が複数投稿されるまで抽選できないように制御
          if (questionList.length >= 2) {
            this.setState({
              isQuestionExist: false,
            })
          }
          this.setState({
            posts: questionList
          })
        }
        catch (e) {
          console.log(e)
        }
      }
    
    중요한 점은 API입니다.graphiql 매개 변수의'variables'부분으로 지정합니다.내가 이 상세한 상황을 설명할게.
    sortDirection은 schema의 @key에서fields의 두 번째 인자로 지정된createdAt의sort조건입니다.filter는 지정한 변수'filterObj'에서'화제는 빈 문자가 아니며 업데이트dAt는 현재 시간의 30분 이내'라는 조건을 설정합니다.limit은 취득 건수의 상한선이다.
    (postOwnnerId는 앞서 말한 바와 같이 이번에 제작된 웹 응용 프로그램과 직접적인 관계가 없다.)
    두 개 이상의 이슈가 있어 이슈를 추첨할 수 있기 때문에 취득 건수에 따라 버튼 표시용 변수를 판정한다.
    // 質問が複数投稿されるまで抽選できないように制御
    if (questionList.length >= 2) {
        this.setState({
          isQuestionExist: false,
        })
    }
    
    다음 처리에서 얻은 모든 항목을 수조에 대입하여 표시합니다
    this.setState({
        posts: questionList
    })
    
    특수 제작된 회전 변압기를 배우고 있기 때문에
    현재 배열된 화제에서 30분이 넘는 화제를 삭제한 후, 화제의 추출 조건을 현재 배열에 없는 최신 화제로 삭제하고, 30분 이내에 발표된 화제와 현재 배열 수가 15개를 넘지 않는 건수를 더한다.
    그렇게 말하고 싶었지만 그러지 못해서 아쉬워요.
    같은 이유로 앞쪽에서 얻은 화제의 배열을 조작할 수 있도록 방법을 준비했습니다.
      // フロント側でやるfilter
      checkQuestion(content) {
        return true
      }
    
    사실상 진짜 반환일 뿐이다.

    화제 투고 시 실시간 반영


    mutation의createPost를 터치점으로 현재 화제의 배열을 실시간으로 반영합니다.위에서 언급한 바와 같이 추출 조건을 엄격하고 예쁘게 실시할 수 없기 때문에subscription에서도 상응하는 조건에 따라 화제를 모았다.
          API.graphql({
            query: subscriptions.onCreatePost,
          }).subscribe({
            next: (data) => {
              this.getFilterdQuestion()
            },
            error: (err) => {
              console.log("subscription error : ", err);
            }
          });
    

    화제 추첨 실시


    주제는 아니지만 간단히 접해보자.
    시작 버튼을 누르면 일정한 간격의 랜덤 수를 생성하고 랜덤 수에 따라 화제 그룹의 X번째 화제를 표시합니다.시작 버튼을 눌렀을 때 투고하지 않기 위해 발언 버튼을 비활성화했다.
    정지 버튼을 누르면 일정 간격의 무작위 생성 처리를 멈추고 발언 버튼을 활성화합니다.
      start = () => {
        let ruoState = setInterval(() => {
          const maxLen = this.state.posts.length
          // 0〜maxLenの範囲でランダムな数値を作成
          var idx = Math.floor(Math.random() * maxLen)
          this.setState({ selectedQuestion: this.state.posts[idx].question })
        }, 80);
        this.setState({ nowDrawing: ruoState })
      }
    
      stop = () => {
        if (this.state.nowDrawing) {
          clearInterval(this.state.nowDrawing)
        }
        this.setState({ nowDrawing: false })
      }
    
    버튼의 설치는 다음과 같습니다.
    <button className='selectButton' onClick={this.start} disabled={this.state.nowDrawing || this.state.isQuestionExist}>スタート</button>
    <button className='selectButton' onClick={this.stop} disabled={!this.state.nowDrawing}>ストップ</button>
    

    화제를 나타내다


    조건에 맞는 화제는 수령 처리 중이기 때문에 화제 배열 중의 화제를 순서대로 표시하기만 하면 된다.
    <div className='childA-block'>
            {this.state.posts.map((post,idx) => {
                return <div key={idx}><div className='questionTitle'>{post.question}</div></div>})
            }
    </div>
    

    App.전체 js


    App.js의 전체상은 다음과 같다.
    App.js
    import { Component } from 'react';
    import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
    import 'react-tabs/style/react-tabs.css';
    import './App.css';
    import { API, graphqlOperation } from 'aws-amplify';
    import * as query from './graphql/queries';
    import * as subscriptions from './graphql/subscriptions';
    import * as mutations from './graphql/mutations';
    
    class App extends Component {
    
      state = {
        postedQuestion: "",
        selectedQuestion: "話題は何かな?",
        posts: [],
        nowDrawing: false,
        isQuestionExist: true,
      }
    
      async componentDidMount() {
    
        try {
          this.getFilterdQuestion()
          API.graphql({
            query: subscriptions.onCreatePost,
          }).subscribe({
            next: (data) => {
              this.getFilterdQuestion()
            },
            error: (err) => {
              console.log("subscription error : ", err);
            }
          });
        }
        catch (e) {
          console.log("error : ", e);
        }
      }
    
      // フロント側でやるfilter
      checkQuestion(content) {
        return true
      }
    
      getFilterdQuestion = async() => {
    
        // 30分前までの質問に絞り込み
        const postedMinutesAgo = 30
        let targetDate = new Date()
        targetDate = targetDate.setMinutes(targetDate.getMinutes() - postedMinutesAgo)
        let dateString = new Date(targetDate).toISOString()
    
        try {
          const filterObj = {
            and: [{
              question: { ne: "" },
              updatedAt: { ge: dateString }
            }]
          }
          // 表示する質問の件数
          const questionLimit = 15
          // 表示する質問の数を設定して、最新の質問を上に表示する
          let posts = await API.graphql({
            query: query.listPostsSortByCreatedAt,
            variables: {
              postOwnerId: "validid",
              sortDirection: "DESC",
              filter: filterObj,
              limit: questionLimit,
            }
          })
          let questionList = posts.data.listPostsSortByCreatedAt.items.filter((post) => {
            return this.checkQuestion(post)
          })
          // 質問が複数投稿されるまで抽選できないように制御
          if (questionList.length >= 2) {
            this.setState({
              isQuestionExist: false,
            })
          }
          this.setState({
            posts: questionList
          })
        }
        catch (e) {
          console.log(e)
        }
      }
    
      createPost = async() => {
    
        // バリデーションチェック
        if (this.state.postedQuestion === '') {
          return
        }
    
        // 新規登録 mutation
        const createPostInput = {
          question: this.state.postedQuestion,
          postOwnerId: "validid",
          createdAt: new Date(),
        }
    
        // 登録処理
        try {
          await API.graphql(graphqlOperation(mutations.createPost, { input: createPostInput }))
          this.setState({ postedQuestion: "" })
        }
        catch (e) {
          console.log(e)
        }
      }
    
      onChange = e => {
        this.setState({
          [e.target.name]: e.target.value
        })
      }
    
      start = () => {
        let ruoState = setInterval(() => {
          const maxLen = this.state.posts.length
          // 0〜maxLenの範囲でランダムな数値を作成
          var idx = Math.floor(Math.random() * maxLen)
          this.setState({ selectedQuestion: this.state.posts[idx].question })
        }, 80);
        this.setState({ nowDrawing: ruoState })
      }
    
      stop = () => {
        if (this.state.nowDrawing) {
          clearInterval(this.state.nowDrawing)
        }
        this.setState({ nowDrawing: false })
      }
    
      render() {
        return (
          <Tabs>
            <TabList>
              <Tab>話題を投稿</Tab>
              <Tab>話題を抽選</Tab>
            </TabList>
        
            <TabPanel className='tabPanel'>
              <div>
                <div className='questionTitle'>話題</div>
                <input className='questionInput' value={this.state.postedQuestion} name="postedQuestion" maxLength={20} onChange={this.onChange}></input>
                <button className='postButton' onClick={this.createPost} disabled={this.state.nowDrawing}>投稿</button>
              </div>
              <div className='childA-block'>
                {this.state.posts.map((post,idx) => {
                  return <div key={idx}><div className='questionTitle'>{post.question}</div></div>})
                }
              </div>
            </TabPanel>
            <TabPanel className='tabPanel'>
              <div className='mainQuestion'>{this.state.selectedQuestion}</div>
              <div>
                <button className='selectButton' onClick={this.start} disabled={this.state.nowDrawing || this.state.isQuestionExist}>スタート</button>
                <button className='selectButton' onClick={this.stop} disabled={!this.state.nowDrawing}>ストップ</button>
              </div>
              <div className='parent-block'>
                <div className='childA-block'>
                  {this.state.posts.map((post,idx) => {
                    return <div key={idx}><div className='questionTitle'>{post.question}</div></div>})
                  }
                </div>
              </div>
            </TabPanel>
          </Tabs>
        );
      }
    }
    
    export default App;
    

    외관의 수정


    주제가 아니기 때문에 대상 파일의 상응하는 부분이나 전체를 열거하지 않고 상세하게 설명한다.
    index.css
    body {
      background-image: url("https://1.bp.blogspot.com/-MJ9kxWWrLg4/XLAcxVsOVCI/AAAAAAABSS0/lUn-55yy1wg182e8UZGGP5Xy8cwNrLYtgCLcBGAs/s800/bg_chiheisen_green.jpg");
      background-repeat: repeat-x;
      margin: 0;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
        'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
        sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
    }
    
    code {
      font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
        monospace;
    }
    
    public/index.html
    -   <title>React App</title>
    +   <title>聞きたいことは?</title>
    
    App.css
    .App {
      text-align: center;
      height: 100%; 
    
      background-position: center;
      background-repeat: no-repeat;
    }
    
    .box{
     display: inline-block;
     background-color:  #ccc;
    }
    
    .parent-block {
      position: relative;
      height: 600px;
      overflow: scroll;
    }
    
    .childA-block {
      position: relative;
      display: inline-block;
      vertical-align: top;
    }
    
    .mainQuestion {
      font-weight: bold;
      color: red;
      font-size: 65px;
      position: relative;
      display: inline-block;
    }
    
    .kanpaiImg {
      width: 60px;
    }
    
    .selectButton {
      font-size: 50px;
      margin-top: 10px;
      margin: 20px;
      border-radius: 40px;
    }
    
    .namelabel {
      font-size: 30px;
    }
    
    .questionInput {
      width: 600px;
      font-size: 30px;
      margin: 10px;
    }
    
    .postButton {
      border-radius: 20px;
      font-size: 25px;
    }
    
    .imgData {
      width: 500px;
      margin-right: 10px;
    }
    
    .questionTitle {
      font-size: 25px;
      font-weight: bold;
    }
    
    .tabPanel {
      text-align: center;
    }
    
    그리고 클라우드 9 의 종착역.
    yarn start
    
    완료.

    감상


    "아무튼 먼저 움직이면 돼!"그렇다면 이 정도도 괜찮지만 특수 제작된 회전 변압기를 잘 배워야 한다며 "일에 따라 실시하고 필요 없는 논리로 이룰 수 없다"고 말했다.

    좋은 웹페이지 즐겨찾기