Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

Leeyanggoo

[JS] 퀴즈 이펙트 7-2!! 데이터를 동기화하자!! 본문

2023/JavaScript

[JS] 퀴즈 이펙트 7-2!! 데이터를 동기화하자!!

Leeyanggoo 2023. 4. 5. 03:36

 

매일 발전하는 퀴즈 이펙트!!

이번엔 사용자가 선택한 문제 보기와 우측의 OMR 선택이 함께 되도록 하고, 두 선택 모두 다 남은 문제수에 반영되도록 하는 기능을 추가했습니다.

그리고 문제에 그림이 필요하거나 추가적인 설명이 필요한 경우 나타내고 없는 경우는 감추는 것까지 포함했습니다!

 

가져올 데이터를 잘 살피자!

 

//정답 확인
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">${question.question_img}</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('');

    //desc 안 보이기
    const cbtQuestionDesc = document.querySelectorAll(".cbt__question__desc");
    cbtQuestionDesc.forEach(desc =>{
        if(desc.innerHTML == "undefined"){
            desc.style.display = "none";
        }
    })

    //img 안 보이기
    const cbtQuestionImg = document.querySelectorAll(".cbt__question__img");
    cbtQuestionImg.forEach(img =>{
        if(img.innerHTML == "undefined"){
            img.style.display = "none";
        }
    })
};

 

이번에도 문제 정보는 저번 예제에서 보았듯, exam과 omr이라는 배열을 만들어서 push()로 추가하는 방식을 사용하고 있습니다.

이렇게 push()를 이용하면 데이터가 바뀌더라도 대처하기가 쉽고, 문제마다 변하는 값이 어떤 부분인지 알기 쉽습니다.

 

newQuestion() 속의 exam과 omr이 배열로 각 HTML 요소에 데이터를 담고 있으므로, 해당 요소를 상황에 따라 표시하거나 가려야 하는 경우 해당 함수 속에서 진행하는 것이 좋습니다.

따라서 json 데이터에 불규칙하게 존재하는 데이터가 해당 HTML 요소에 들어가야 하는 경우, newQuestion() 함수 안에서 선택하고 변경해야 합니다.

 

cbtQuestionDesc와 cbtQuestionImg는 push() 메서드로 담을 배열 속의 HTML 요소를 선택하고 있습니다.

모든 문제의 "cbt__question__desc"와 "cbt__question__img"가 담아야 하므로 querySelectorAll()을 이용합니다.

question.question_desc와 question.question_img에 해당 json 데이터가 존재하지 않을 경우 "undefined"를 도출하므로, HTML 요소가 "undefined"인 경우(== "undefined") 그 요소에 display= 'none' 속성을 추가하면서 보이지 않게 하고 있습니다.

 

데이터의 진행 과정을 잘 살펴보자!

 

//정답 확인
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");
        } 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");
        }
    });
};

 

answerQuiz 함수는 사용자가 선택한 정답을 검사하고 정답 여부를 표시하는 역할을 합니다.

먼저 document.querySelectorAll(".cbt__selects")를 이용해 모든 문제의 선택지를 "cbtSelects" 변수에 저장합니다.

모든 문제의 정보를 담고 있는 questionAll=[];을 forEach를 이용해 각 문제를 반복 처리합니다.

quizSelectWrap은 현재 문제의 선택지들을 저장하는 변수입니다.

그리고 userSelector에 사용자가 선택한 <input> 태그 선택자(:checked)만 저장합니다.

"(quizSelectWrap.querySelector(userSelector) || {}).value"는 선택한 정답의 value를 userAnswer 변수에 저장하고, 선택한 정답이 없는 경우 "undefined"를 저장합니다.

사용자가 선택한 input의 value가 보기의 번호와 일치시키기 위해 slice(-1)를 사용합니다. input의 value는 "value="${number}_1"로 저장되어 있습니다.

numberAnswer에는 사용자가 선택한 보기의 번호가 저장되어 있으므로 정답 번호(question.answer)와 같다면 정답 처리를 하는 class "good"을 추가하고, 틀린 경우 "bad"를 추가합니다.

또한 본래 정답이 무엇인지 사용자에게 알려주기 위해 label에 correct를 추가하고, 앞서 감추었던 문제의 해설(cbt__desc)을 표시합니다.

 

서로 다른 onclick 이벤트를 동기화하자!

 

//문제와 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;
};

cbtSubmit.addEventListener("click", answerQuiz);
dataQuestion();

 

answerSelect2와 answerSelect 함수는 문제의 보기와 OMR의 <input> 태그 onclick 이벤트 정보를 담은 매개변수 elem을 통해 input 요소에 접근합니다.

elem.value는 사용자가 선택한 보기의 값이며, 변수 answer에 저장합니다.

input의 value가 저장된 형태는 "문제 번호_보기 번호"이므로 보기의 번호만 남기기 위해 split() 메서드를 이용합니다.

문자열 "_"를 기준으로 분할하면 [문제번호, 보기번호] 형태의 배열이 됩니다.

따라서 answerNum[0]은 문제번호이며, select[answerNum[0]]는 해당 문제의 정보가 되면서 querySelectorAll("input")은 문제의 보기를 선택하게 됩니다.

보기 번호는 1부터 시작하므로 answerNum[1]에서 1을 빼게 되면 인덱스로 활용하게 됩니다.

여기서 사용한 checked 속성은 HTML의 input 요소 상태를 나타내며, 이 속성의 값이 true로 설정되면 해당 요소가 체크된 상태로 표시됩니다.

그뒤에 체크된 보기의 데이터를 "answerInputs"에 저장하고, 전체 문제 개수에서 빼게 되면 사용자가 풀어야 할 남은 문제의 개수만 남게 됩니다.