javascript

quiz 유형 사이트 만들기- 07 ver2

grovy 2023. 4. 4. 21:44
728x90

퀴즈 사이트 만들기 !  ( 객관식유형 * 60문제 한번에 만들기+json형식 ) ver.2

어제 만든것에서 오늘은 좀더 소스를 다듬을거에요 

오답처리 부분 & 라벨 처리 부분 & 사용자 이름 입력 받는것 & timer 스크립트를 변경했어요 

 전체 소스는 요기 

주요 소스를 볼게요 ! 

 

<div class="quiz__wrap__cbt">
            <div class="cbt__header">
                <h2>2020년 1회 정보처리기능사 기출문제</h2>
                
            </div>
            <div class="cbt__conts">
                <div class="cbt__quiz">
                    <!-- <div class="cbt good">
                        <div class="cbt__question"><span>1</span>. 객체지향 프로그램에서 데이터를 추상화하는 단위는?</div>
                        <div class="cbt__question__img"><img src="img/gineungsaWD2023_01_01.jpg" alt="기능사"></div>
                        <div class="cbt__selects">
                            <input type="radio" id="select1">
                            ....
                            <label for="select4"><span>메시지</span></label>
                        </div>
                        <div class="cbt__desc">객체지향언어는 이다. 객체지향언어는 이다. 객체지향언어는 이다. 객체지향언어는 이다. 객체지향언어는 이다. 객체지향언어는 이다.</div>
                        <div class="cbt__keyword">객체지향언어</div>
                    </div> -->
                </div>
            </div>
            <div class="cbt__aside">
                <div class="cbt__info">
                    <div>
                        <button class="cbt__submit">제출하기</button>
                        <span class="cbt__time"></span>
                    </div>
                    <div>
                        <div class="cbt__title">수험자 : <em class="cbt__name">황상연</em></div>
                        <div class="cbt__score">
                            <span>전체 문제수 : <em class="cbt__length">0</em>문항</span>
                            <span>남은 문제수 : <em class="cbt__rest">0</em></span>
                        </div>
                    </div>
                </div>
                <div class="cbt__omr">
                    <!-- <div class="omr">
                        <input type="radio" id="omr0_1">
                        ....
                        </label>
                    </div> -->
                </div>
            </div>
            <div class="cbt__start">
                <div class="cbt__modal1">
                    <h2>정보처리 & 웹디자인 기능사</h2>
                    <div class="cbt__choice">
                        <select name="cbtTime" id="cbtTime">
                            <option value="gineungsaJC2005_02">정보처리기능사 2005년 2회</option>
							....
							<option value="gineungsaJC2011_05">정보처리기능사 2011년 5회</option>
                        </select>
                        <select name="cbtTime" id="cbtTime">
                            <option value="gineungsaWD2009_05">웹디자인기능사 2009년 5회</option>
                            ...
                            <option value="gineungsaWD2016_04">웹디자인기능사 2016년 4회</option>
                        </select>
                        <br>
                    </div>
                    <input type="text" id='cbtUserName' autofocus placeholder="이름적어주세요" /><br>
                    <button class="minimal">시작하기</button>
                </div>
            </div>
        </div>

위처럼 body영역엔 소스가 없고 

스크립트에서 소스를 cbt_quiz, cbt_omr에 소스를 뿌려 주는건 ver1과 동일해요 

모달창을 시작할때 실행되게끔 해놨어요 

데이터 불러오는 부분

이전 소스와 동일합니다 !

//데이터 불러오기
const dataQuestion = () => {
    fetch("json/gineungsaWD2012_02.json")  
    //이 json파일을 가져올거에요
    .then(res => res.json())        
    //res.send()는 send에 전해진 argument에 따라서 Content-type이 자동적으로 만들어줘요
    .then(items=> {                 
    //객체를 한번더 가공해서 출력할거에요 
        questionAll = items.map((item, index)=>{
            const formattedQuestion = {
                number : index+1,
                question: item.question
            };
            //펼침연산자로 각각의 배열에 보기 123을 넣어준다 
            const answerChoices = [...item.incorrect_answers];

            //랜덤함수
            formattedQuestion.Answer = Math.floor( Math.random()*answerChoices.length)+1; 

            // splice로 임의의 위치 formattedQuestion.Answer로 정답값을 넣어주는거에요 
            answerChoices.splice(formattedQuestion.Answer-1,0,item.correct_answer);

            //객관식보기 배열을 forEach문에 index를 넣고 값을 formattedQuestion에 넣어줘요
            //오브젝트 타입안에 배열로 보기를 넣어준다고 생각하면 편해요 
            answerChoices.forEach((choice, index)=>{
                formattedQuestion["choice"+ (index+1)]  = choice;
            })

            //hasOwnProperty는 값이 있을경우 즉 true일때 if문을 실행해요 
            //문제에 대한 해설이 있으면 출력해 줄거에요 
            if(item.hasOwnProperty("question_desc")){
                formattedQuestion.questionDesc = item.question_desc;
            }
            //문제에 대한 이미지가 있으면 출력해 줄거에요 
            if(item.hasOwnProperty("question_img")){
                formattedQuestion.questionImg = item.question_img;
            }
            //문제에 대한 desc 있으면 출력해 줄거에요 
            if(item.hasOwnProperty("desc")){
                formattedQuestion.desc = item.desc;
            }

            return formattedQuestion;
        });
        //문제 불러오기
        newQuestion(questionAll); 
        //시작시 문제갯수를 넣어준다
        document.querySelector(".cbt__score em").innerHTML = questionAll.length+"문항";
        document.querySelector(".cbt__remainder em").innerHTML = questionAll.length+"문항";

		
    })
    .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="img/${question.question_img}.jpg" onerror="this.style.display='none'"></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" name="omr${number}" id="omr${number}_1" value="${number}_0" onclick="answerSelect(this)">
                <label for="omr${number}_1"><span class="label-inner">1</span></label>
                <input type="radio" name="omr${number}" id="omr${number}_2" value="${number}_1" onclick="answerSelect(this)">
                <label for="omr${number}_2"><span class="label-inner">2</span></label>
                <input type="radio" name="omr${number}" id="omr${number}_3" value="${number}_2" onclick="answerSelect(this)">
                <label for="omr${number}_3"><span class="label-inner">3</span></label>
                <input type="radio" name="omr${number}" id="omr${number}_4" value="${number}_3" onclick="answerSelect(this)">
                <label for="omr${number}_4"><span class="label-inner">4</span></label>
            </div>
        `)
        //전체 문제수 만들기
    });
    cbtQuiz.innerHTML = exam.join('');
    cbtOmr.innerHTML = omr.join('');
    
    //문제 해설이 없을시 화면에 띄어주지않는로직
    const cbtQuestionDesc = document.querySelectorAll(".cbt__question__desc");
    cbtQuestionDesc.forEach((el,num)=>{
        if(cbtQuestionDesc[num].innerText == "undefined"){
            cbtQuestionDesc[num].classList.add("hide");
        } else {
            cbtQuestionDesc[num].classList.remove("hide");
        }
    });
}

모든정보를 가지고 있는 questionAll에 forEach문을 사용하여

마지막에 값을 넣어줘요 

 cbtQuiz.innerHTML=exam.join('');    문제넣기
 cbtOmr.innerHTML=omr.join('');       객관식보기넣기

어제 소스와 동일하고 다른 부분을 적어볼게요 

<img src="img/${question.question_img}.jpg" onerror="this.style.display='none'">

이미지 테그에서 에러발생시 화면에 출력되지 않도록 설정해줬어요! 

 

cbtQuestionDesc.forEach((el,num)=>{...)};

이 부분은 문제의 설명이 없을경우 출력을 하지 않도록 했어요 

정답 확인하기

const answerQuiz = () =>{
    const cbtSelects = document.querySelectorAll(".cbt__selects");
	
    //모든 정보를 가져와서 forEach로 값을 비교할거에요
    questionAll.forEach((question, number) =>{
    	
        const quizSelectsWrap = cbtSelects[number];
        const userSelector = `input[name=select${number}]:checked`;
        
		//객관식 보기 문제에 체크유무 상관없이 값을 가져오기
        const userAnswer = (quizSelectsWrap.querySelector(userSelector)||{}).value;
		//userAnswer의 값유무에 따라 slice하여 값을 가져와요 없으면 undefined
        const numberAnswer = userAnswer ? userAnswer.slice(-1) : undefined;
        
        //정답을 가져와서 비교해요 
        if(Number(numberAnswer)+1 == 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].innerText == "undefined"){
            quizDesc[number].classList.add("hide");
        } else {
            quizDesc[number].classList.remove("hide");
        }
    })
}

answerQuiz메서드는 제출할 때 실행합니다 제출해서 체점해요 

왼쪽, 오른쪽 객관식체크 동기화 & 남은문제출력 as-is >> to-be

//quiz >> omr 체크 동기화 ver1
const answerSelect= ()=>{
    //cbtQuiz 모든 체크 값을 가져 오는거에요 
    let checkAllRadio = cbtQuiz.querySelectorAll('input[type=radio]:checked');
	//체크한값을 오른쪽 omr에 checked할거에요
    checkAllRadio.forEach((radioNum, index)=>{
        cbtOmr.querySelector("input[id=omr"+radioNum.id.slice(6)).checked = true;
    });
    //남은문제
    chkQuizNumber(checkAllRadio);
}

//quiz >> omr 체크 동기화 ver2
const answerSelect = (elem) => {
    const answer = elem.value;
    const answerNum = answer.split("_");
    const select =document.querySelectorAll(".cbt__quiz .cbt");
    const label = select[answerNum[0]].querySelectorAll("input"); //문제번호
    label[answerNum[1]].checked=true;             

    const answerInputs = document.querySelectorAll(".cbt__selects input:checked");
    //남은문제
    cbtRest.innerHTML=questionLengh-answerInputs.length;
}

이전 포스팅에서 매개변수없이 갖왔을땐 forEach문을 돌려 전체로 체크했다면 

이번 포스팅에서는 클릭했을때 answerSelect(this) 자신의 값을가져와 퀴즈부분과 omr부분을 동기화 해줬어요 

 

숙제 & 혼자한것

  1. 모달창 버튼 누르면 닫히게 하기 ( 숙제 )
  2. img, desc 없는 건 안 나타나게 하기 ( 숙제 )
  3. 시간 표시
  4. 이름적기

1번부터 모달창 부분처리한곳 보고 갈게용

cbtStartBtn.addEventListener("click", e => {
    //모달닫기
    document.querySelector(".cbt__start").style.display = "none"; 
});

2번은 위에서 설명했어요 

 

3번 시간표시 

// 시험시간20분계산 분 * 초 * 1000 1분의1초
const countTime = 20 * 60 * 1000+1500;

// 끝나는 기준 시간(현재 시간 + 20분+2000)을 계산 updateTimer 불러올때 +1.5초추가 해서계산
// 끝나는 기준시간이에용 고정값인 시간입니다. 
let endTime = Date.now() + countTime;

function updateTimer() {
    // 남은 시간을 계산합니다.
    const leftTime = endTime - Date.now();

    // 시, 분, 초를 계산합니다. 시간은 쓰지않아요 시험시간은 20분으로 설정!
    // const hours = Math.floor(leftTime / countTime);
    const minutes = Math.floor((leftTime % countTime) / (60 * 1000));
    const seconds = Math.floor((leftTime % (60 * 1000)) / 1000);

    // 타이머를 화면에 출력합니다.
    cbtCountTime.innerText=`${minutes}분 ${seconds}초`;
    // 남은 시간이 없으면 타이머를 멈춥니다.
    if (leftTime <= 0){
    	//setInterval을 종료할땐 clearInterval(timer) 이런형식으로해요 
        //timer함수선언해서 요값이 들거가 있는거에요 
        clearInterval(timer);
        
        //시간종료 제출하기 & 시간대신 시간초과ㅜ표시
        answerQuiz();
        cbtCountTime.innerText=`시간초과ㅜ`;
    }
}
// setInterval을 이용하면 1초마다 타이머가 실행되게할수있어요
// 모달창닫는 메서드로 이동할거에요 그래야 시작과 동시에 실행되요 ! 
// const timer = setInterval(updateTimer, 1000);

구글링해서 setInterval 함수와 쓰는법 시간표시하는법을 찾아봤어요 

위 주석을 읽어보면 이해가 가실거에용

 

4. 이름적기 as-is >> to-be

//이전소스 
//로딩과 동시에 이름적지않을경우 무한반복
let inputString = prompt('이름을 입력하세요', '이름을 적어주세요');
while(inputString=='' || inputString ==undefined){
    inputString = prompt('이름을 입력하세요', '이름 왜 안적어ㅡㅡ');
}

document.querySelector(".cbt__title").innerHTML = inputString;

//수정소스
//모달창 닫기 이벤트 발생시 ! 실행하도록했어요
//input박스는 모달창으로 이동시켰어요 ! 
cbtStartBtn.addEventListener("click", e => {
    //이름가져오기 입력받은값을 가져오기 or 없으면 바보라고 출력되요 
    let inputString = document.querySelector("#cbtUserName").value;
    // (inputString == '')?document.querySelector(".cbt__title").style.display = "none":document.querySelector(".cbt__name").innerHTML = inputString ;
    (inputString == '')?document.querySelector(".cbt__name").innerHTML = "바보" : document.querySelector(".cbt__name").innerHTML = inputString ;
});