본문 바로가기
💻CODING/react. vue

[react] 리액트 챗봇 스타일의 대화형 설문 조사 화면 구현

by 코딩하는 갓디노 2021. 6. 7.

react 대화형 설문조사 화면 구현

 

최근에 회사 프로젝트에서 리액트를 이용한 간단한 설문조사 앱을 만드는 중입니다.
여러 가지 시안 중에서 앱에서 많이 볼 수 있는 대화형의 설문 조사 형식이 있어, 마크업의 일부를 간단하게 공유합니다. 

여기서는 앱 화면에 차례대로 설문 문제와 대답을 보여주는 일부만 공개를 하였고,
모든 문제를 마치면 결과지를 보여주는 화면은 없습니다.

구현 화면

react 대화형 설문조사

 

앱 구현 설정

  • npm: npm 패키지 설정(create react 폴더 설정)
  • style css: tailwind 설치
  • 중앙 상태 관리: context api
  • 화면 구현 순서: 테스트 문제, 보기 5문항 -> 나의 대답 선택 -> 나의 대답 결과 -> 다음 문제  

 

폴더 구조

 

리액트 대화형 설문 테스트 화면

formData.js - 설문 조사 데이터
설문 조사 문항과 내용은 예시를 위해 카카오같이가치의 내용을 가져왔다. 

const formData = [
  {
    id: 1,
    question: "항상 무엇이든 할 준비가 되어 있다.",
    meaning: "무슨 일이든 대처할 수 있는 만반의 준비가 되어있나요?",
  },
  {
    id: 2,
    question: "세밀한 부분에도 주의를 기울인다.",
    meaning: "일이나 과제를 진행할 때 어떠 스타일인가요?",
  },
  {
    id: 3,
    question: "질서 정연한 것을 좋아한다.",
    meaning: "평소 어떤 성향인지 떠올려보세요.",
  },
  {
    id: 4,
    question: "어질러지면 즉각 청소한다.",
    meaning: "지금 당신의 방이 어떤 상태인지 떠올려보세요.",
  },
  {
    id: 5,
    question: "계획한 것을 그대로 실행한다.",
    meaning: "평소 어떤 성향인지 떠올려보세요.",
  },
  {
    id: 6,
    question: "일에 대해서는 가혹할만큼 열심히 한다.",
    meaning: "업무나 과제가 주어졌을 때 어떻게 행동하나요?",
  },
  {
    id: 7,
    question: "내 물건들을 잘 정돈하지 않는 편이다.",
    meaning: "지금 당신의 방이나 책상은 어떤 상태이지 떠올려보세요.",
  },
  {
    id: 8,
    question: "일을 엉망으로 만들 때가 많다.",
    meaning: "평소 어떤 성향인지 떠올려보세요.",
  },
];

export default formData;

 

InnerHeader.js - MainContent.js의 header 부분

import React, { useContext } from "react";
import { MainContext } from "../datas/Store";
import formData from "../datas/FormData";

const InnerHeader = () => {
  const { i } = useContext(MainContext);
  const progress = Math.floor(((i + 1) / formData.length) * 100);

  return (
    <div className="fixed w-full z-10 inset-x-0 bg-white">
      <div className="border-b-2 m-2 pb-2 text-center">
        <div className="font-semibold text-md text-gray-600">설문 조사</div>
      </div>

      <div className="border-b-2 my-2 mx-3">
        <div className="flex items-center justify-between">
          <div>
            <span className="text-xs font-semibold inline-block">
              {i + 1}/{formData.length}
            </span>
          </div>
          <div className="text-right">
            <span className="text-xs font-semibold inline-block">
              {progress} %
            </span>
          </div>
        </div>
        <div className="overflow-hidden h-2 mb-2 text-xs flex rounded bg-blue-200">
          <div
            style={{ width: progress + "%" }}
            className="shadow-none whitespace-nowrap rounded text-white justify-center bg-blue-500"
          ></div>
        </div>
      </div>
    </div>
  );
};

export default InnerHeader;

 

InnerContent.js - MainContent.js의 Content 부분

import React, { useState, useContext, useRef } from "react";
import formData from "../datas/FormData"; //설문조사 데이터
import { MainContext } from "../datas/Store"; //context api 데이터

const InnerContent = () => {
  const { i, onSubmitForm, onClickNext } = useContext(MainContext);
  const [info, setInfo] = useState([ 
    {
      id: '',
      question: '',
      meaning: '',
      answer: '',
      bgColor: () => { },
    }
  ]);
  const [formClose, setFormClose] = useState(false);
  const onCloseForm = () => { //보기 보기바 토글 기능 처리
    setFormClose(!formClose);
  }

//백업용 복사를 사여 info에 넣어주면서, 나의 대답을 함께 넣어준 후 map으로 출력
  const infoSave = (data) => { 
    let formDataCopy = formData.slice(); //백업용 복사
    setInfo(info => info.concat({
      id: formDataCopy[i].id, //번호
      question: formDataCopy[i].question, //질문
      meaning: formDataCopy[i].meaning, //질문 정의
      answer: data, //나의 대답
      bgColor: changeBgColor(i) //배경 색상
    }))
  }

  const scrollTarget = useRef([]);

  const scrollToRef = () => { //useRef를 이용하여 부드럽게 auto 스크롤 처리
    scrollTarget.current.scrollIntoView({ block: "start", behavior: "smooth" });
  }

  const onChangeAnswer = (e) => {
    let answered = {
      [e.target.name]: e.target.value
    }

    if (!answered) {
      return;
    } else {
      onSubmitForm(e.target.value); //store에 대답 보내줌-나중 결과 페이지용
      infoSave(e.target.value); //info에 대답 넣어줌
      if (i < formData.length - 1) {
        scrollToRef(); //auto 스크롤 처리
      }
      onClickNext(); //다음 문제 출력
    }
  }

  const changeBgColor = (i) => { //박스 배경 색상 변경 출력
    const bgColor = ['gray', 'yellow', 'red', 'green', 'blue', 'purple', 'pink'];
    let num = i % (bgColor.length);
    return bgColor[num];
  }

  return (
    <>
      <div className="absolute top-24 mx-2">
        <div className="flex flex-col space-y-4 py-3 overflow-y-auto scrollbar-thumb-blue 
        scrollbar-thumb-rounded scrollbar-track-blue-lighter scrollbar-w-2 scrolling-touch">
          {/* 이전 문제 */}
          {info ? (info.map((item) => {
            return (
              <>
                {item.id && <div key={item.id} className={"chat-message flex gap-1 " 
                + (item.name)}>
                  <div className="flex flex-col order-2 items-start">
                    <div className={"px-3 py-3 rounded-xl bg-" + (item.bgColor) 
                    + "-50 text-gray-600"}>
                      <div className="text-sm font-semibold">
                        <span className="mr-2">{item.id}.</span>
                        {item.question}
                      </div>
                      <div className="flex justify-between border border-gray-400 text-left 
                      text-xs p-2 mt-3 rounded-md">
                        <div>
                          <p>{item.meaning}</p>
                        </div>
                        <div className="w-8 h-8 min-w-8 rounded-full bg-gray-300">
                          <img
                            src="https://placeimg.com/50/50/people"
                            alt="My profile"
                            className="order-1"
                          />
                        </div>
                      </div>
                    </div>
                  </div>
                  <img
                    src="https://placeimg.com/50/50/people"
                    alt="My profile"
                    className="w-10 h-10 rounded-full order-1"
                  />
                </div>}

                {/* 테스트 대답 */}
                {item.id && <div className={"chat-message flex gap-1 justify-end"}>
                  <div className="flex flex-col order-2 items-start">
                  //박스 배경 색상 입력 처리
                    <div className={"px-3 py-3 rounded-xl bg-" + (item.bgColor) + "-50 text-gray-600"}>
                      <div className="flex justify-between gap-4 text-xs font-semibold">
                        <div>
                          <span>{item.id}. </span>
                        </div>
                        <div>나의 대답은? {item.answer} 번</div>
                      </div>
                    </div>
                  </div>
                  <img
                    src="https://placeimg.com/50/50/people"
                    alt="My profile"
                    className="w-10 h-10 rounded-full order-2"
                  />
                </div>}
              </>
            )
          })) : ''}

          {/* 다음 문제 출현 */}
          {i !== formData.length - 1 ? (
            <div>
              <div ref={scrollTarget} className={"next answer mb-64 chat-message flex gap-1 " 
              + (formData[i].name)}>
                <div className="flex flex-col order-2 items-start">
                  <div className="px-3 py-3 rounded-xl bg-gray-200 text-gray-600">
                    <div className="text-sm font-semibold">
                      <span className="mr-2">{i + 1}.</span>
                      {formData[i].question} //다음 문제
                    </div>
                    <div className="flex justify-between border border-gray-400 text-left
                    p-2 mt-3 rounded-md">
                      <div className="w-10/12">
                        <p>{formData[i].meaning}</p>
                      </div>
                      <div className="w-8 h-8 rounded-full bg-gray-300">
                        <img
                          src="https://placeimg.com/50/50/people"
                          alt="My profile"
                          className="order-1"
                        />
                      </div>
                    </div>
                  </div>
                </div>
                <img
                  src="https://placeimg.com/50/50/people"
                  alt="My profile"
                  className="w-10 h-10 rounded-full order-1"
                />
              </div>
              <div className="block h-80"></div>
            </div>) : ''}
        </div>
      </div>
      
      {/* 보기 5개 */} //보기 바 클릭시 토글로 show, hide 처리
      <div className={"fixed bottom-0 left-0 z-10 w-full flex-shrink-0 transition-all 
      transform duration-300 " 
      + (formClose ? " translate-y-full" : "")}>
        <span onClick={onCloseForm} className='absolute cursor-pointer -top-6 right-2 
        block w-16 h-8 rounded-md bg-gray-700 m-auto'>
          {formClose ? (<svg xmlns="http://www.w3.org/2000/svg" className="animate-bounce 
          h-5 w-5 text-white font-bold 
          m-auto mt-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
            d="M5 11l7-7 7 7M5 19l7-7 7 7" />
          </svg>) : (<svg xmlns="http://www.w3.org/2000/svg" className="animate-bounce 
          h-5 w-5 text-white font-bold m-auto mt-2" 
          fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
            d="M19 13l-7 7-7-7m14-8l-7 7-7-7" />
          </svg>)
          }
        </span>
        <form className='relative border-t border-gray-200 pb-2 px-2 bg-white'>
          <div key={formData[i].id} className="text-left">
            <label className="flex items-center text-gray-500 bg-gray-100 mt-2 py-2 px-2 
            hover:bg-gray-600 hover:text-white">
              <input
                type="radio"
                onChange={
                  onChangeAnswer
                }
                className="form-radio h-4 w-4"
                name={formData[i].id}
                value={0}
                required
              />
              <span className="ml-1 text-sm">
                1. 전혀 그렇지 않다.
              </span>
            </label>

            <label className="flex items-center text-gray-500 bg-gray-100 mt-2 py-2 px-2 
            hover:bg-gray-600 hover:text-white">
              <input
                type="radio"
                onChange={
                  onChangeAnswer
                }
                className="form-radio h-4 w-4"
                name={formData[i].id}
                value={1}
                required
              />
              <span className="ml-1 text-sm">
                2. 그렇지 않은 편이다.
              </span>
            </label>

            <label className="flex items-center text-gray-500 bg-gray-100 mt-2 py-2 px-2 
            hover:bg-gray-600 hover:text-white">
              <input
                type="radio"
                onChange={
                  onChangeAnswer
                }
                className="form-radio h-4 w-4"
                name={formData[i].id}
                value={2}
                required
              />
              <span className="ml-1 text-sm">
                3. 중간이다.
              </span>
            </label>

            <label className="flex items-center text-gray-500 bg-gray-100 mt-2 py-2 px-2 
            hover:bg-gray-600 hover:text-white">
              <input
                type="radio"
                onChange={
                  onChangeAnswer
                }
                className="form-radio h-4 w-4"
                name={formData[i].id}
                value={3}
                required
              />
              <span className="ml-1 text-sm">
                4. 그런편이다.
              </span>
            </label>

            <label className="flex items-center text-gray-500 bg-gray-100 mt-2 py-2 px-2 
            hover:bg-gray-600 hover:text-white">
              <input
                type="radio"
                onChange={
                  onChangeAnswer
                }
                className="form-radio h-4 w-4"
                name={formData[i].id}
                value={4}
                required
              />
              <span className="ml-1 text-sm">
                5. 매우 그렇다.
              </span>
            </label>
          </div>
        </form>
      </div>
    </>
  );
};

export default InnerContent;

 

store.js - 중앙 데이터 관리

import React, { useState, createContext } from "react";
import formData from "../datas/FormData";

export const MainContext = createContext({
  onClickNext: () => {},
  onSubmitForm: (symptom: any, answer: any) => {},
});

const Store = (props: any) => {
  let [i, setI] = useState(0);

  const onClickNext = () => { //다음 번호
    if (i < formData.length - 1) {
      i = i + 1;
    } else if ((i = formData.length - 1)) {
      return;
    }
    return setI(i);
  };

  const onClickPrev = () => { //이전 번호
    if (i > 0) {
      i = i - 1;
    } else if ((i = 0)) {
      i = formData.length - 1;
    }
    return setI(i);
  };

  const onSubmitForm = (data: any) => { //result 페이지 도출용
    if (data === null) {
      return;
    } else {
      //console.log(data);
      setResultData(resultData.concat(data));

      const resultSubmit = {
        id: reportId,
        ...data,
      };
      // axios으로 데이터 보내줌
      // axios
      //   .put(common.SERVER_URL + "/report", resultSubmit)
      //   .then((res) => {
      //     console.log("res", res);
      //   })
      //   .catch((err) => console.log(err));

      //HTMLFormElement.reset();
    }
  };

  const onClickBack = () => {
    //setResultData("");
    setI(0);
  };

  return (
    <MainContext.Provider
      value={{
        i,
        onClickNext,
        onClickPrev,
        onSubmitForm,
      }}
    >
      {props.children}
    </MainContext.Provider>
  );
};

export default Store;
반응형

댓글