최근에 회사 프로젝트에서 리액트를 이용한 간단한 설문조사 앱을 만드는 중입니다.
여러 가지 시안 중에서 앱에서 많이 볼 수 있는 대화형의 설문 조사 형식이 있어, 마크업의 일부를 간단하게 공유합니다.
여기서는 앱 화면에 차례대로 설문 문제와 대답을 보여주는 일부만 공개를 하였고,
모든 문제를 마치면 결과지를 보여주는 화면은 없습니다.
구현 화면
앱 구현 설정
- 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;
반응형
'개발 > React' 카테고리의 다른 글
[react] 리액트 tailwind 다이얼로그(모달, 팝업) 구현 (0) | 2021.06.18 |
---|---|
[react] react 모달창(팝업창) 구현 (0) | 2021.06.12 |
[react] 리액트 삼항 연산자 조건부 스타일링(ft. tailwind, className) (0) | 2021.04.26 |
[react] react 상태관리 context API, useContext 사용법 (0) | 2021.04.26 |
[react] react-router-dom 설치, 라우팅하기 (0) | 2021.04.20 |
댓글