プロジェクト共有-ヘルス#3-1クライアント機能の実装(掲示板、マネージャページに一致)


開始します。


「木業」ページを完了し、機能の実装を開始すると同時に、以前のプロジェクトとは異なり、フロントエンドとバックエンドが実現する必要がある部分に分かれています.サーバの実装は他のチームメンバーが担当するため、クライアントはまだ実装されていないサーバの応答を予測し、コードを記述する必要があるため、REST API設計段階は非常に重要である.

木材の接続ページ


木業ページが完成したらreact-router-domを使ってリンク先のページに接続し、初めて作ったアイテムのようにmatchを使う部分があるのでparamsを設定しました.
<Route path='/view/:postId' component={View} /
<Route path='/verify-email/:token' component={VerifyEmail} />
<Route path='/updatepw/:token' component={UpdatePw} />
//View 컴포넌트
export default function View({match}) {

  const postId= match.params.postId
  
  return (
    /*생략*/
    )
}
マッチング情報が表示されるViewページでは、上記コードに示すようにpathを:postIdに設定し、ViewコンポーネントからPropsのpostId(match.params.postId)を受信することができる.また,以前の項目とは異なり,会員登録時に認証コードを入力してメール認証を行う方式であり,リンク接続時に認証を行うのではなく,tokenを渡すことができるように設定されている.電子メール情報暗号化トークンを含む会員認証リンクがメールで送信され、そのリンクに接続された場合、サーバにトークンが送信されると、サーバはトークンを復号し、ユーザ認証によって論理を実現する.パスワード検索も同様に実現されるのでparamsが設定されています.

掲示板


掲示板ページは、以前は多価プロジェクトやスローポストの管理者ページで実現されていたので、複雑な論理はありませんでした.まず、サーバが応答する前に、表示されるロードページとデータがない場合に表示されるページをstateで接続し、REST APIに従って変更してマッチング情報を表示します.木業ページの作成時にスタックデータを地図として表示することが実現されたため,サーバ要求関連コード以外にあまりデータを追加しなかった.
日付、ページ、応募可能な検索語のみを表示するかどうかをstateに設定し、一致情報を要求したときに渡す.多価プロジェクトでもフィルタ要素の掲示板が多く実施されているが,これは難しい論理ではない.userEffectを使用して、フィルタ情報を変更時にデータを要求するように設定し、リンクを使用して対応するビューページにリンクし、クリック時に対応するビューページに移動します.
1ページに表示されるデータが多いため、スクロール時に表示され、押した後にスクロールアップボタンを追加し、機能実装を完了します.後で、サーバが実装されると、正常に動作しているかどうかを確認します.
export default function Board() {

  const [ page, setPage ] = useState(1)
  const [ count, setCount ] = useState(0)
  const [ selectDate, setSelectDate ] = useState(0)
  const [ selectLocation, setSelectLocation] = useState('전체')
  const [ locationForm, setLocationForm ] = useState('%')
  const [ data, setData ] = useState([])
  const [ isLoading, setIsLoading ] = useState(false)
  const [ isMatched, setIsMatched ] = useState(null)
  const [ keyword, setKeyword ] = useState(null)
  const [ ScrollY, setScrollY ] = useState(0);
  const [ btnStatus, setBtnStatus ] = useState(false);

  const handleFollow = () => {
    setScrollY(window.pageYOffset);
    if(ScrollY > 100) {
      setBtnStatus(true);
    } else {
      setBtnStatus(false);
    }
  }

  useEffect(() => {
    const watch = () => {
      window.addEventListener('scroll', handleFollow);
    }
    watch();
    return () => {
      window.removeEventListener('scroll', handleFollow);
    }
  })

  const handleTop = () => {
    window.scrollTo({
      top: 0,
      behavior: "smooth"
    });
    setScrollY(0);
    setBtnStatus(false);
  }

  const settings = {
    dots: false,
    infinite: false,
    speed: 500,
    slidesToShow: 10,
    slidesToScroll: 1,
  };

  const getDateForm = (n) => {
    let today = new Date();
    let date = new Date(today.setDate(today.getDate() + n));
    let year = date.getFullYear();
    let month = ('0' + (date.getMonth() + 1)).slice(-2);
    let day = ('0' + date.getDate()).slice(-2);

    return year + '-' + month  + '-' + day; 
  }
  
  const getDateArr = (n) => {
    let today = new Date();
    let date = new Date(today.setDate(today.getDate() + n));
    let day = date.getDate();
    return day;
  };

  const getDaysArr = (n) => {
    let today = new Date();
    let days = ["일", "월", "화", "수", "목", "금", "토"];
    let date = new Date(today.setDate(today.getDate() + n));
    return days[date.getDay()];
  };
  const dateArray = Array.from({ length: 14 }, (v, i) => getDateArr(i));
  const daysArray = Array.from({ length: 14 }, (v, i) => getDaysArr(i));

  const locationArr = ['전체', '서울', '경기', '인천', '대전', '충북', '충남', '대구', '부산', '울산', '경북', '경남', '광주', '전북', '전남', '강원', '제주']

  const handleLocation = (e) => {
    setSelectLocation(e.target.innerText)
    if(e.target.innerText==='전체') {
      setLocationForm('%')
    } else {
      setLocationForm(e.target.innerText)
    }
  }

  const hadleKeyword = (e) => {
    if(e.target.value==='') {
      setKeyword(null)
    } else {
      setKeyword(e.target.value)
    }
  }

  //데이터 요청
  const getData = async() => {
    await setIsLoading(true)
    await axios.get(`${process.env.REACT_APP_SERVER_API}/post`, {
			params: {
        date : getDateForm(selectDate),
        location : locationForm,
        page,
        isMatched,
        keyword
       }
		})
    .then((res) => {
      setData(res.data.data)
      setCount(res.data.count)
    })
    await setIsLoading(false)
  }

  const getDataPage = () => {
    axios.get(`${process.env.REACT_APP_SERVER_API}/post`, {
			params: {
        date : getDateForm(selectDate),
        location : locationForm,
        page,
        isMatched,
        keyword
       }
		})
    .then((res) => {
      setData(res.data.data);
      setCount(res.data.count);
      handleTop();
    })
  }

  useEffect(()=>{
    getData();
  },[selectDate, locationForm, isMatched])


    useEffect(()=>{
    getDataPage();
  },[page])

  return (
    <div className="board-container">
      <div className="box-banner">
        <BannerSlider />
      </div>
      <div className="box-date">
      <Slider {...settings}>
        {dateArray.map((el,i)=> {
          return(
            <DateBtn selectDate={selectDate} setSelectDate={setSelectDate} key={i} i={i} date={el} days={daysArray[i]}/>
          )
        })}
      </Slider>
      </div>
        <ul className='box-location'>
          {locationArr.map((el,i) => {
            return (
              <li className={selectLocation===el ? 'btn-selected-location' : 'btn-location'} onClick={handleLocation}>{el}</li>
            )
          })}
      </ul>
      <div className='box-filter'>
          <input type='checkbox' id='match-out' className='match-check-box' checked={isMatched} onChange={(e)=>{setIsMatched(e.target.checked)}}/>
          <label for='match-out' className='text-match'>신청 가능만 보기</label>
          <div class='search-box'>
          <input
            type='text'
            id='search'
            placeholder='헬스장 명을 입력하세요'
            onChange={hadleKeyword}
          ></input>
          <span>
            <button id='searchButton' onClick={getData}>
              <FontAwesomeIcon icon={faSearch}/>
            </button>
          </span>
        </div>
      </div>
      <div className='box-list'>
        <table className='table-data'>
          {isLoading ? (
            <tr className='box-loading'>
            <td colSpan='3'>
              <Loading/>
            </td>
          </tr>
          ) : (data.length===0 ? (
            <tr className='box-none'>
              <td colSpan='3'>일치하는 게시물이 없습니다.</td>
            </tr>
          ) :(data.map((el,i)=> {
            return(
              <RowData el={el} key={i}/>
            )
          })))}
        </table>
      </div>
      <div className='box-pagination'>
        <Pagination
          activePage={page}
          itemsCountPerPage={15}
          totalItemsCount={count}
          pageRangeDisplayed={5}
          prevPageText={'‹'}
          nextPageText={'›'}
          onChange={setPage}
        />
      </div>
      <div className={btnStatus?'btn-top':'btn-top none'} onClick={handleTop}><FontAwesomeIcon icon={faCaretUp}/></div>
    </div>
  );
}

function BannerSlider() {
  const settings = {
    dots: false,
    infinite: true,
    speed: 500,
    slidesToShow: 1,
    slidesToScroll: 1,
  };
  return (
    <div>
      <Slider {...settings}>
        <Banner />
        <Banner />
        <Banner />
      </Slider>
    </div>
  );
}

function Banner() {
  return (
    <div className="banner">
      <img className="img-banner" alt="logo" src="img/logo.svg" />
    </div>
  );
}

function DateBtn({ selectDate, setSelectDate, days, date, i }) {
  return (
    <div className={selectDate===i ? "btn-selected-date" : 'btn-date'} onClick={()=>{setSelectDate(i)}}>
      <div className={days==='토' ? 'date blue' : (days==='일' ? 'date red' : 'date')}>{date}</div>
      <div className={days==='토' ? 'days blue' : (days==='일' ? 'days red' : 'days')}>{days}</div>
    </div>
  );
}

function RowData({el}) {
  return(
    <tr>
            <td className='time'><Link to={`/view/${el.id}`} style={{ color: 'inherit', textDecoration: 'inherit' }}>{el.reserved_at.slice(11,16)}</Link></td>
            <td className='info'>
              <div className='title'><Link to={`/view/${el.id}`} style={{ color: 'inherit', textDecoration: 'inherit' }}>{el.location.address_name.slice(0,2)+' '+el.location.place_name}</Link></div>
              <div className='sub-info'><Link to={`/view/${el.id}`} style={{ color: 'inherit', textDecoration: 'inherit' }}>3{el.description.sbd} {el.description.bodyPart.join(' ')}</Link></div>
            </td>
            <td className='match'>
            <Link to={`/view/${el.id}`} style={{ color: 'inherit', textDecoration: 'inherit' }}>
              {!el.isMatched ? <div className='btn-match'>신청 가능</div> : <div className='btn-match-end'>마감</div>}
              </Link>
            </td>
          </tr>
  )
}

管理者ページ


管理者ページはオプションカード式メニューからユーザー管理、マッチング管理、クレーム履歴確認の3ページを接続し、マッチング掲示板ページと論理的に類似し、削除機能(マッチング管理、ユーザー管理)のみを追加し、迅速を実現する.
「管理者」ページには、CookieのaccessTokenをサーバに転送して検証するプロセスが追加され、管理者以外ではアクセスできません.
export default function AdminPage() {

  const [ tap, setTap ] = useState(0)
  const [modal, setModal] = useState(false) // 접근 불가 모달 창

  const checkisAdmin = () => {
    axios.get(`${process.env.REACT_APP_SERVER_API}/user/auth`, {
      withCredentials: true,
    })
    .then((res) =>{
      if(!res.data.data.isAdmin) {
        setModal(true)
      }
    })
    .catch(()=>{
      setModal(true)
    })
  }

  useEffect(()=>{
    checkisAdmin();
  },[])
  
  return (
    /*생략*/
    )
  
}
ユーザー管理と照合管理ページでデータを削除できるため、accessTokenを転送してサーバが管理者であるかどうかを検証する手順も同様に設定されています.
const deleteMatch = () => {
    axios
      .delete(`${process.env.REACT_APP_SERVER_API}/admin/post`, {
        data: { postId: deleteId },
        withCredentials: true,
      })
      .then((res) => {
        setModal(true);
      })
      .catch((res) => {
        setErrorModal(true);
      });
  };

の最後の部分


クライアントの機能実装の過程で、実際の業界では、REST AP計画がどれほど重要であるかがわかります.多くの場合、機能を実現するために必要なデータが失われたり、リクエストを追加したりする必要があるため、バックエンドを担当するチームメンバーとコミュニケーションし、機能を実現しました.以前はサーバとクライアントの機能を同時に実現していたので、実装段階でエラーをチェックできるのは良かったのですが、現在はREST APIに従って実施して後続のチェックを行うべきです.最初は不便だと思いましたが、サーバに多くのリクエストが処理され、ブランチが必要なエラーが多い場合は、バックエンドで統一されたルール処理を使用すると効率的になります.