Leeyanggoo
[JS] 퀴즈 이펙트 7-3!! 데이터 활용하기!! 타이머 만들기!! 본문
완성을 향해 달려가는 퀴즈 이펙트 CBT 유형입니다!!
지난 포스팅에서 사용자의 정답과 OMR의 선택을 동기화하는 기능까지 추가해 보았는데요.
오늘은 다양한 유형의 CBT를 가져올 수 있게 하였고, 이를 모달창에서 사용자가 선택할 수 있도록 했습니다.
또한 모달창을 이용해서 사용자가 입력한 이름을 시험지에 표시했습니다.
여러 데이터를 가져오고 넘기는 게 중요합니다!
이번 예제는 코드가 길고 CSS와 JS, JSON 파일이 나뉘어 있으므로 함께 보시면서 읽기를 추천합니다!
😮💨 이번 예제 코드 보러 가기
이제 선택자와 변수 파악은 필수!!
const cbtQuiz = document.querySelector(".cbt__quiz")
const cbtOmr = document.querySelector(".cbt__omr")
const cbtSubmit = document.querySelector(".cbt__submit")
const cbtLength = document.querySelector(".cbt__score .cbt__length") //전체 문제 수
const cbtRest = document.querySelector(".cbt__score .cbt__rest") //남은 문제 수
const cbtQuizDesc = document.querySelectorAll(".cbt__question__desc")
const cbtStartBtn = document.querySelector(".cbt__start button")
const cbtTime = document.querySelector(".cbt__time") //타이머
const cbtViewSubject = document.querySelector(".cbt__view .subject")
const cbtHeader = document.querySelector(".cbt__header h2")
const cbtStart = document.querySelector(".cbt__start");
const cbtViewName = document.querySelector(".cbt__view .name")
const cbtName = document.querySelector(".cbt__name")
const cbtEnd = document.querySelector(".cbt__end")
const cbtEndBtn = document.querySelector(".cbt__end__btn")
const cbtResult = document.querySelector(".cbt__end .cbt__result")
let questionAll = []; //모든 퀴즈 정보
let questionLength = 0; //전체 문제수
let questionRest = questionLength; //남은 문제수
let questionTime = "";
let questionTimeRemain = "1000";
let questionScore = 0; //맞춘 문제 개수
이번에 쓰인 선택자와 전역변수들입니다.
여러 기능을 추가하니 꽤나 많아진 모습입니다.
오늘 추가한 기능에서 중요하게 작용하는 것들은 바로 let questionTime과 같은 전역변수들입니다.
이 변수들은 일종의 초기 설정값으로 이용합니다.
//시작하기
const startQuiz = () => {
//모달 제거
cbtStart.classList.add("hide");
//시간아 흘러라
questionTime = setInterval(reduceTime, 1000)
//사용자의 이름을 가져옴
const userName = cbtViewName.value.trim();
cbtName.innerText = userName;
};
//데이터 가져오기
const dataQuestion = (value) => {
fetch(`https://kebab000.github.io/web2023/gineungsaJSON/${value}.json`)
.then(res => res.json())
.then(items => {
questionAll = items.map((item, index) => {
const formattedQuestion = {
question: item.question,
number: index+1,
};
const answerChoices = [...item.incorrect_answers]; //오답 불러오기
formattedQuestion.answer = Math.round(Math.random() * (answerChoices.length)) + 1; //정답을 랜덤으로 불러오기
answerChoices.splice(formattedQuestion.answer-1, 0, item.correct_answer); //정답을 랜덤으로 추가
//보기를 추가하기
answerChoices.forEach((choice, index) => {
formattedQuestion["choice" + (index+1)] = choice;
});
//문제 해설이 있으면(true) 출력
if(item.hasOwnProperty("question_desc")){
formattedQuestion.question_desc = item.question_desc;
}
//문제에 이미지가 있으면 출력
if(item.hasOwnProperty("question_img")){
formattedQuestion.question_img = item.question_img;
}
//해설이 있으면 출력
if(item.hasOwnProperty("desc")){
formattedQuestion.desc = item.desc;
}
return formattedQuestion;
});
newQuestion();
//전체 문제 수
questionLength = questionAll.length;
cbtLength.innerHTML = questionLength;
cbtRest.innerHTML = questionLength;
})
.catch((err) => console.log(err));
}
//문제 만들기
const newQuestion = () => {
const exam = [];
const omr = [];
questionAll.forEach((question, number) => {
exam.push(`
<div class="cbt">
<div class="cbt__question"><span>${question.number}</span>. ${question.question}</div>
<div class="cbt__question__img"><img src="https://kebab000.github.io/web2023/gineungsaJPG/${question.question_img}.jpg" alt="시험이미지"></div>
<div class="cbt__question__desc">${question.question_desc}</div>
<div class="cbt__selects">
<input type="radio" id="select${number}_1" name="select${number}" value="${number}_1" onclick="answerSelect2(this)">
<label for="select${number}_1"><span>${question.choice1}</span></label>
<input type="radio" id="select${number}_2" name="select${number}" value="${number}_2" onclick="answerSelect2(this)">
<label for="select${number}_2"><span>${question.choice2}</span></label>
<input type="radio" id="select${number}_3" name="select${number}" value="${number}_3" onclick="answerSelect2(this)">
<label for="select${number}_3"><span>${question.choice3}</span></label>
<input type="radio" id="select${number}_4" name="select${number}" value="${number}_4" onclick="answerSelect2(this)">
<label for="select${number}_4"><span>${question.choice4}</span></label>
</div>
<div class="cbt__desc hide">${question.desc}</div>
</div>
`);
omr.push(`
<div class="omr">
<strong>${question.number}</strong>
<input type="radio" id="omr${number}_1" name="omr${number}", value="${number}_1" onclick="answerSelect(this)">
<label for="omr${number}_1">
<span class="label-inner">1</span>
</label>
<input type="radio" id="omr${number}_2" name="omr${number}", value="${number}_2" onclick="answerSelect(this)">
<label for="omr${number}_2">
<span class="label-inner">2</span>
</label>
<input type="radio" id="omr${number}_3" name="omr${number}", value="${number}_3" onclick="answerSelect(this)">
<label for="omr${number}_3">
<span class="label-inner">3</span>
</label>
<input type="radio" id="omr${number}_4" name="omr${number}", value="${number}_4" onclick="answerSelect(this)">
<label for="omr${number}_4">
<span class="label-inner">4</span>
</label>
</div>
`);
});
cbtQuiz.innerHTML = exam.join('');
cbtOmr.innerHTML = omr.join('');
//설명
document.querySelectorAll(".cbt__question__desc").forEach(desc => {
if(desc.innerText === "undefined"){
desc.classList.add("hide")
}
});
//이미지
document.querySelectorAll(".cbt__question__img").forEach(img => {
let src = img.querySelector("img").src;
if(src.includes("undefined")){
img.classList.add("hide");
}
});
};
//정답 확인
const answerQuiz = () => {
const cbtSelects = document.querySelectorAll(".cbt__selects");
questionAll.forEach((question, number) => {
const quizSelectWrap = cbtSelects[number];
const userSelector = `input[name=select${number}]:checked`;
const userAnswer = (quizSelectWrap.querySelector(userSelector) || {}).value;
const numberAnswer = userAnswer ? userAnswer.slice(-1) : undefined;
//사용자의 정답 확인
if(numberAnswer == question.answer){
cbtSelects[number].parentElement.classList.add("good");
questionScore++
} else {
cbtSelects[number].parentElement.classList.add("bad");
//오답 체크
const label = cbtSelects[number].querySelectorAll("label");
label[question.answer-1].classList.add("correct")
}
//설명 추가
const quizDesc = document.querySelectorAll(".cbt__desc");
if(quizDesc[number].innerHTML == "undefined"){
quizDesc[number].classList.add("hide");
} else {
quizDesc[number].classList.remove("hide");
}
cbtEnd.classList.remove("hide");
cbtResult.innerHTML = `
맞춘 문제 : ${questionScore}개<br>
점수 : ${Math.ceil((questionScore / questionAll.length) * 100)}점<br><br>
${(Math.ceil((questionScore / questionAll.length) * 100) > 60) ? '합격입니다🤗' : '불합격입니다😥'}
`
});
};
//문제와 OMR의 선택을 연동
const answerSelect2 = (elem) => {
const answer = elem.value;
const answerNum = answer.split("_")
const select = document.querySelectorAll(".cbt__omr .omr"); //전체 문항 수 100개
const label = select[answerNum[0]].querySelectorAll("input"); //한 문제의 보기 4개
label[answerNum[1]-1].checked = true;
const answerInputs = document.querySelectorAll(".cbt__selects input:checked")
cbtRest.innerHTML = questionLength - answerInputs.length;
};
const answerSelect = (elem) => {
const answer = elem.value;
const answerNum = answer.split("_")
const select = document.querySelectorAll(".cbt__quiz .cbt"); //전체 문항 수 100개
const label = select[answerNum[0]].querySelectorAll("input"); //한 문제의 보기 4개
label[answerNum[1]-1].checked = true;
const answerInputs = document.querySelectorAll(".cbt__selects input:checked")
cbtRest.innerHTML = questionLength - answerInputs.length;
};
//문제 선택
const changeSelect = (e) => {
let selectValue = e.value;
let selectText = e.options[e.selectedIndex].text;
cbtViewSubject.innerHTML = selectText;
cbtHeader.innerHTML = selectText;
dataQuestion(selectValue)
}
//시간 설정
const reduceTime = () => {
questionTimeRemain--;
if(questionTimeRemain == 0) endQuiz();
cbtTime.innerHTML = displayTime();
}
//시간 표시
const displayTime = () => {
if(questionTimeRemain <= 0){
return "0분 00초";
} else {
let minutes = Math.floor(questionTimeRemain / 60);
let seconds = questionTimeRemain % 60
return `${minutes}분 ${seconds}초`;
//초 단위가 한 자리면 앞에 0 붙이기
}
}
//시험 끝
const endQuiz = () => {
answerQuiz()
};
//모달 끄기
const endModal = () => {
cbtEnd.classList.add("hide");
};
cbtStartBtn.addEventListener("click", startQuiz);
cbtSubmit.addEventListener("click", answerQuiz);
cbtEndBtn.addEventListener("click", endModal);
// dataQuestion();
setInterval() 메서드로 타이머를 만들자!
이번 차례에서 새롭게 추가한 기능은 바로 타이머 기능입니다.
setInterval() 메서드는 두 개의 인자를 가질 수 있습니다.
첫 번째 인자는 반복 실행할 콜백 함수이고, 두 번째 인자는 콜백 함수를 실행할 시간 간격(밀리초)입니다.
이번 예제에서는 콜백 함수 reduceTime()을 분리해서 위치시켰습니다.
뒤에 붙는 1000은 1초(1000밀리초)를 의미합니다.
즉 문제를 푸는 시간을 저장한 questionTimeRemain 변수가 1초 마다 줄어들기 때문에(--) 이를 화면으로 출력하면 타이머가 되는 것입니다.
다양한 value값을 이용하자!
이번 예제에는 여러 요소의 value를 이용하여 데이터를 저장하고 이용했습니다.
dataQuestion() 함수나 onclick 함수의 데이터도 HTML의 input과 button, label 등의 요소에서 가져왔습니다.
이는 모두 각 요소가 받아들이는 value 데이터를 참고하여 이용합니다.
이번에 불러오는 json 파일들은 changeSelect() 함수의 인자를 dataQuestion이 selectValue로 받아와서, option에 있는 기능사의 여러 회차 이름을 불러오고 있습니다.
또한 이번에 만든 타이머 기능도 남은 시간(questionTimeRemain)의 value인 남은 시간들을 활용하고 있습니다.
만약 남은 시간이 0초가 되면 endQuiz() 함수가 실행되면서 endQuiz() 함수는 다시 answerQuiz를 실행하게 됩니다.
모달창을 만들어서 문제를 안내하자!
모달창이란 웹 페이지에서 사용자와 상호작용할 때 사용되는 팝업 창입니다.
일반적으로 어떤 정보를 제공하거나 사용자 입력을 요청하고, 다른 요소와의 상호작용을 일시적으로 막는 데 사용됩니다.
따라서 사용자가 문제를 풀기 위해 페이지를 키는 순간, 다른 요소를 선택할 수 없게 하고 먼저 어떤 회차의 문제를 풀지 선택하고 이름을 적게 유도하게 됩니다.
HTML <select> 태그는 드롭다운을 목록을 생성하기 위해 사용됩니다.
일반으로 <option> 태그와 함께 사용되며 사용자는 드롭다운 목록에 있는 option 중 하나를 선택할 수 있습니다.
우리는 option에 기능사 문제를 모두 넣고 이를 웹에서 json으로 불러오고 있습니다.
또한 모달창에 띄운 input은 사용자가 이름을 입력하게 합니다.
사용자가 input에 입력한 데이터는 cbtViewName.value.trim()으로 문자열 반환을 하고, 이를 userName 변수에 저장합니다.
그리고 cbtName에 사용자가 입력한 이름을 innerText로 출력합니다.
또한 시험이 끝나게 되면 두 번째 모달창(.cbt__end modal2)이 실행됩니다.
두 번째 모달창은 사용가 맞춘 문제의 개수와 점수를 표시하고 합격점을 알려줍니다.
questionScore는 사용자가 문제를 맞춘 경우 1씩 증가하며 몇 문제를 맞췄는지 알 수 있는 데이터가 들어있습니다.
이를 전체 문제의 개수에서 나누고 100을 곱한 뒤, Math.ceil() 메서드를 이용해 올림하여 사용자의 점수를 출력합니다.
이를 활용하면 사용자가 합격 점수인 60점을 넘은 경우 표시할 메시지도 충분히 만들 수 있습니다.
😮💨 이전 포스팅 보러 가기
😮💨 이번 예제 보러 가기