Animated Turtle

Javascript

퀴즈화면 만들기7

훙구 2023. 4. 3. 19:33

...

728x90
반응형

CBT형식의 시험지 만들기

HTML 작성하기

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>퀴즈 이펙트07</title>

    <link rel="stylesheet" href="css/quiz.css">
    <link rel="stylesheet" href="css/reset.css">
    <link href="https://unpkg.com/pattern.css" rel="stylesheet">

    <!-- 파비콘 -->
    <link rel="shortcut icon" type="image/x-icon" href="img/favicon.png">
    <link rel="apple-touch-icon" sizes="114x114" href="img/favicon.png">
    <link rel="apple-touch-icon" href="img/favicon.png">

</head>
<body>
    <header id="header">
        <h1><a href="../javascript14.html">Quiz</a> <em>객관식 CBT 유형</em></h1>
        <ul>
            <li><a href="quizEffect01.html">1</a></li>
            <li><a href="quizEffect02.html">2</a></li>
            <li><a href="quizEffect03.html">3</a></li>
            <li><a href="quizEffect04.html">4</a></li>
            <li><a href="quizEffect05.html">5</a></li>
            <li><a href="quizEffect06.html">6</a></li>
            <li class="active"><a href="quizEffect07.html">7</a></li>
        </ul>
    </header>
    <!-- //header -->

    <main id="main">
        <div class="quiz__wrap__cbt">
            <div class="cbt__header">
                <h2>2020년 1회 정보처리기능사 기출문제</h2>
            </div>
            <div class="cbt__conts">
                <div class="cbt__quiz">
                </div>
            </div>
            <!-- cbt__conts -->
            <div class="cbt__button">
                <div class="cbt__time">59분 10초</div>
                <div class="cbt__submit">제출하기</div>
            </div>
            <div class="cbt__aside">
                <div class="cbt__info">
                    <div class="cbt__name">
                        <div class="cbt__title">수험자 : <em></em></div>
                        <div class="cbt__score">
                            <span>전체 문항 : <em></em> 문항</span>
                            <span>남은 문항 : <em></em> 문항</span>
                        </div>
                    </div>
                </div>
                <!-- cbt__info -->
                <div class="cbt__omr">
                </div>
                <!-- cbt__omr -->
            </div>
            <!-- cbt__aside -->
        </div>
        <!-- quiz__wrap__cbt -->
    </main>
    <!-- //main -->
</body>
</html>

HTML 정리해보기

  • 기존에 사용하던 강아지(dog__wrap부분)와 다른 방식인 CBT형식으로 만들기위해 html의 구조를 다르게 변경했습니다.
  • 구조는 크게 좌, 우를 나누어 좌측에는 문제를 나열하고 우측에는 omr형식의 답지를 고정하여 두는 구조입니다.
  • 문제가 많아 script로 문제와 omr카드를 출력하기 때문에 문제가 들어갈 cbt__quiz부분과 omr형식의 답지가 들어갈 cbt__omr부분을 만들어 놓기만 했습니다.

CSS 작성하기

.quiz__wrap__cbt {
     padding: 0 20px;
     font-family: PyeongChang;
}

.cbt__header {
     width: calc(100% - 300px);
     background-color: #e1fae5;
     border: 5px ridge #cacaca;
     margin-bottom: 20px;
     padding: 20px;
     display: flex;
     justify-content: space-between;
     align-items: center;
}

.cbt__header > div > div {
     border: 1px solid #000;
     background-color: #1c6e2a;
     display: inline-block;
     margin-left: 5px;
     color: #fff;
     padding: 10px 20px;
     border-radius: 20px;
}

.cbt__conts {
     width: calc(100% - 300px);
     background-color: #fdfdfd;
     margin-bottom: 20px;
}

.cbt__button {
     width: 280px;
     position: fixed;
     right: 20px;
     top: 90px;
     padding: 10px 0;
     display: flex;
     justify-content: space-between;
}

.cbt__time {
     position: relative;
     padding: 10px 10px 10px 50px;
     background-color: #52cc00;
     border-radius: 30px;
     font-size: 18px;
     cursor: pointer;
     transition: all 0.3s;
}

.cbt__time::before {
     content: '';
     background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-alarm' width='24' height='24' viewBox='0 0 24 24' stroke-width='2' stroke='%23ffffff' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Ccircle cx='12' cy='13' r='7' /%3E%3Cpolyline points='12 10 12 13 14 13' /%3E%3Cline x1='7' y1='4' x2='4.25' y2='6' /%3E%3Cline x1='17' y1='4' x2='19.75' y2='6' /%3E%3C/svg%3E");     position: absolute;
     left: 12px;
     top: 9px;
     width: 22px;
     height: 22px;
}

.cbt__time:hover {
     background-color: #0f5800;
}

.cbt__submit {
     position: relative;
     border-radius: 30px;
     padding: 10px 10px 10px 50px;
     background-color: #6aa5ff;
     font-size: 18px;
     cursor: pointer;
     transition: all 0.3s;
}

.cbt__submit::before {
     content: '';
     background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-edit' width='24' height='24' viewBox='0 0 24 24' stroke-width='2' stroke='%23ffffff' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M9 7h-3a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-3' /%3E%3Cpath d='M9 15h3l8.5 -8.5a1.5 1.5 0 0 0 -3 -3l-8.5 8.5v3' /%3E%3Cline x1='16' y1='5' x2='19' y2='8' /%3E%3C/svg%3E");     position: absolute;
     left: 12px;
     top: 9px;
     width: 22px;
     height: 22px;
}

.cbt__submit:hover {
     background-color: #00567e;
}

.cbt__aside {
     position: fixed;
     right: 20px;
     top: 160px;
     width: 280px;
     height: calc(100vh - 140px);
     background-color: #fff;
     border: 5px ridge #cacaca;
     overflow-y: auto;
}

.cbt__quiz {
     display: flex;
     flex-wrap: wrap;
     justify-content: space-between;
}

.cbt__quiz .cbt {
     width: 49%;
     border: 5px ridge #cacaca;
     margin-bottom: 10px;
     padding: 10px;
}

.cbt__info {
     background-color: #e1fae5;
     border-bottom: 5px ridge #cacaca;
}

.cbt__info .cbt__name {
     text-align: center;
     padding: 10px 0;
}

.cbt__info .cbt__title {
     text-decoration: underline;
     font-size: 20px;
     text-underline-offset: 4px;
     margin-bottom: 8px;
}

.cbt__info span {
     display: inline-block;
}

.cbt__omr {
     padding: 20px;
}

.cbt__omr .omr {
     margin: 5px 0;
     display: grid;
     grid-template-columns: 50px 38px 38px 38px 38px;
     grid-template-rows: 20px;
     align-items: center;
}

.cbt__omr .omr input {
     opacity: 0;
     position: absolute;
     width: 0;
     height: 0;
}

.cbt__omr .omr strong {
     display: inline-block;
     text-align: center;
     padding: 2px;
     background-color: #374739;
     color: #fff;
     font-family: 'Helvetica Neue';
     margin-right: 10px;
     font-weight: bold;
}

.cbt__omr .omr label {
     box-shadow: 0 0 0 1px #000;
     cursor: pointer;
     line-height: 0.4;
     text-align: center;
     width: 28px;
     height: 8px;
     position: relative;
     font-family: 'Helvetica Neue';
     position: relative;
}

.cbt__omr .omr label::after {
     position: absolute;
     background-color: #555;
     content: '';
     display: block;
     top: 0;
     left: 0;
     width: 0;
     height: 100%;
     z-index: 1;
     transition: width 0.1s linear;
}

.cbt__omr .omr .label-inner {
     background-color: #fff;
     padding: 0.25em 0.13em;
     transform: translateY(-0.25em);
     width: 20px;
     color: #000;
}

.cbt__omr .omr input[type=radio]:checked + label::after {
     width: 100%;
}

.cbt__question {
     font-size: 1.4rem;
     margin-bottom: 10px;
}

.cbt__question__img img {
     max-width: 400px;
     margin-bottom: 10px;
}

.cbt__question__desc {
     border: 2px solid #cacaca;
     padding: 10px;
     margin-bottom: 15px;
     line-height: 1.5;
}

.cbt__selects {
     margin-bottom: 15px;
}

.cbt__selects label {
     display: flex;
}

.cbt__selects label span {
     font-size: 1rem;
     line-height: 1.5;
     padding: 10px 10px 10px 30px;
     cursor: pointer;
     color: #444;
     position: relative;
}

.cbt__selects label span::before {
     content: '1';
     position: absolute;
     left: 0;
     top: 50%;
     transform: translateY(-50%);
     width: 20px;
     height: 20px;
     border: 1px solid #444;
     /* box-shadow: 0 0 0 1px #000; */
     border-radius: 50%;
     text-align: center;
     font-family: 'Helvetica Neue';
     font-weight: bold;
     line-height: 1.1;
     font-size: 0.83em;
     transition: all 0.3s linear;
}

.cbt__selects label:nth-of-type(1) span::before {
     content: '1';
}

.cbt__selects label:nth-of-type(2) span::before {
     content: '2';
}

.cbt__selects label:nth-of-type(3) span::before {
     content: '3';
}

.cbt__selects label:nth-of-type(4) span::before {
     content: '4';
}

.cbt__selects input {
     position: absolute;
     left: -9999px;
}

.cbt__selects input:checked + label span::before {
     color: #fff;
     /* background-color: #000; */
     box-shadow: inset 0 0 0 10px #000;
     border-color: #000;
}

.cbt__selects label.correct span::before {
     border-color: red;
     box-shadow: inset 0 0 0 10px red;
     color: #fff;
 }

.cbt__desc {
     background-color: #e1fae5;
     padding: 10px 10px 10px 40px;
     margin-bottom: 5px;
     position: relative;
}

.cbt__desc.hide {
     display: none;
}

.cbt__desc::before {
     content: '';
     position: absolute;
     left: 13px;
     top: 11px;
     width: 20px;
     height: 20px;
     background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor' class='w-6 h-6'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M12 18v-5.25m0 0a6.01 6.01 0 001.5-.189m-1.5.189a6.01 6.01 0 01-1.5-.189m3.75 7.478a12.06 12.06 0 01-4.5 0m3.75 2.383a14.406 14.406 0 01-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 10-7.517 0c.85.493 1.509 1.333 1.509 2.316V18' /%3E%3C/svg%3E ");}

.cbt__keyword {
     background-color: #e6ffad;
     border-radius: 50px;
     padding: 10px 20px 10px 40px;
     position: relative;
}

.cbt__keyword::before {
     content: '';
     position: absolute;
     left: 13px;
     top: 11px;
     width: 20px;
     height: 20px;
     background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor' class='w-6 h-6'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10' /%3E%3C/svg%3E ");
}
.cbt__quiz .cbt.good {
     position: relative;
}

.cbt__quiz .cbt.good::after {
     content: '';
     background-image: url("../img/good.png");
     background-size: contain;
     background-repeat: no-repeat;
     width: 200px;
     height: 200px;
     position: absolute;
     left: -25px;
     top: -30px;
}

.cbt__quiz .cbt.bad {
     position: relative;
}

.cbt__quiz .cbt.bad::after {
     content: '';
     background-image: url("../img/bad.png");
     background-size: contain;
     background-repeat: no-repeat;
     width: 200px;
     height: 200px;
     position: absolute;
     left: -40px;
     top: -45px;
}


@media (min-width : 1400px){
     .cbt__quiz .cbt {
          width: 32.3333%;
     }
}
@media (max-width : 960px){
     .cbt__quiz .cbt {
          width: 100%;
     }
}
@media (max-width : 800px){
     .cbt__aside {
          display: none;
     }
     .cbt__header {
          width: 100%;
          flex-direction: column;
     }
     .cbt__header h2 {
          margin-bottom: 10px;
     }
     .cbt__conts {
          width: 100%;
     }
     .cbt__button {
          left: 30%;
          top: 90%;
     }
}

CSS 정리해보기

  • 남은 시간과 남은 문항수를 알려주는 cbt__button박스와 omr카드를 적은 cbt__aside박는 position: fixed를 해주어 문제를 풀어 내려가는 동안에도 스크롤바 밖으로 나가지 않도록 했습니다.
  • cbt__aside에 overflow-y: auto 의 값을 주어 넘치는 문항수에 대한 omr 카드가 cbt__aside박스 밖으로 나가지 않고 별도의 스크롤바가 생기게 만들어 주었습니다.
  • 기본으로 생성되는 radio타입의 input박스 모양을 없애고 가상요소를 이용해 새로운 모양을 만들었습니다.

Javascript 작성하기

const cbtQuiz = document.querySelector(".cbt__quiz");
const cbtOmr = document.querySelector(".cbt__omr");
const cbtSubmit = document.querySelector(".cbt__submit");
const cbtName = document.querySelector(".cbt__title em");
const cbtScore = document.querySelector(".cbt__score em");

let questionAll = [];


// 문제 불러오기
const dataQuestion = () => {
    fetch("json/gisa2020_01.json")
    .then(res => res.json())
    .then(items => { 
        // console.log(items)
        questionAll = items.map((item, index) => {
            const formattedQuestion = {
                question: item.question,
                number: index +1,
            }
            const answerChoices = [...item.incorrect_answers];
            formattedQuestion.Answer = Math.floor(Math.random() * (answerChoices.length) +1 );
            answerChoices.splice(formattedQuestion.Answer -1, 0, item.correct_answer);

            answerChoices.forEach((choice, index) => {
                formattedQuestion["choice" + (index + 1)] = choice;
            });

            //문제에 대한 해설이 있다면 출력하기
            if(item.hasOwnProperty("question_desc")){
                formattedQuestion.questionDesc = item.question_desc;
            }

            //문제에 대한 이미지가 있다면 출력하기
            if(item.hasOwnProperty("question_img")){
                formattedQuestion.questionImg = item.question_img;
            }

            //해설이 있다면 출력하기
            if(item.hasOwnProperty("desc")){
                formattedQuestion.Desc = item.desc;
            }

            // console.log(formattedQuestion);
            return formattedQuestion;
        });
        newQuestion();  // 문제 만들기
    })
    .catch((err) => console.log(err));

}

// 문제 만들기
const exam = [];
const omr = [];

const newQuestion = () => {
    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"></div>
                <div class="cbt__selects">
                    <input type="radio" id="select${number}_1" name="select${number}" value="${number+1}_1" onclick="answerSelect(this)">
                    <label for="select${number}_1"><span>${question.choice1}</span></label>
                    <input type="radio" id="select${number}_2" name="select${number}" value="${number+1}_2" onclick="answerSelect(this)">
                    <label for="select${number}_2"><span>${question.choice2}</span></label>
                    <input type="radio" id="select${number}_3" name="select${number}" value="${number+1}_3" onclick="answerSelect(this)">
                    <label for="select${number}_3"><span>${question.choice3}</span></label>
                    <input type="radio" id="select${number}_4" name="select${number}" value="${number+1}_4" onclick="answerSelect(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="select${number}" id="omr${number}_1" value="${number}_0">
                <label for="omr${number}_1">
                    <span class="label-inner">1</span>
                </label>
                <input type="radio" name="select${number}" id="omr${number}_2" value="${number}_0">
                <label for="omr${number}_2">
                    <span class="label-inner">2</span>
                </label>
                <input type="radio" name="select${number}" id="omr${number}_3" value="${number}_0">
                <label for="omr${number}_3">
                    <span class="label-inner">3</span>
                </label>
                <input type="radio" name="select${number}" id="omr${number}_4" value="${number}_0">
                <label for="omr${number}_4">
                    <span class="label-inner">4</span>
                </label>
            </div>
        `)
    });

    cbtQuiz.innerHTML = exam.join('');
    cbtOmr.innerHTML = omr.join('');
    // cbtName.innerHTML = prompt("수험자 성함을 적어주세요.");


};

// 정답 확인

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){
            console.log("정답")
            cbtSelects[number].parentElement.classList.add("good");
        } else{
            console.log("오답")
            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")
        }
    });
};

const answerSelect = () => {};

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

javascript 정리해보기

  • dataQuestion 함수
  • fetch(경로/이름.json)     - json파일 가져오기
  • .then(res => res.json()) - 가져온 json파일을 변수 res에 json파일 형식으로 저장
  • .then(items => {            - 안에 있는 내용을 items라고 명명 & 화살표함수
  • questionAll(빈배열)에 map()을 사용하여 items에 있는 내용을 새로운 배열로 저장합니다.
  • formattedQuestion 이라는 변수에 문제의 정보를 객체 형식으로 불러옵니다.
  • map()메서드의 요소자리에는 item이 인덱스 자리에는 index가 들어가있으므로 item == items 각각의 요소 입니다.
  • 따라서 item.question 이란 json파일에 하나의 배열안에 저장되어있는 객체형식 요소에서 키 question에 해당하는 결과값을 의미합니다.
  • answerChoices에 json파일에 있는 incorrect_answers배열을 저장합니다.
  • formattedQuestion에 Answer의 값으로 Math.random을 사용해 난수에 answerChoices.length를 곱해 번호의 범위를 정하고 그 중 하나의 값을 Math.floor(버림)를 사용해 정수로 받아와 저장합니다.
  • answerChoices에 splice를 통해 받아온 Answer값의 순서에 item.correct_answer(정답)을 넣어줍니다.
  • answerChoices에 forEach를 사용하여 "choice"(문자열)에 번호를 추가하여 저장합니다.
  • if(item.hasOwnProperty())을 사용해 각각의 내용이 있다면 그 내용을 추가해줍니다.
  • 문제를 출력하는 newQuestion함수를 실행합니다.

 

  • newQuestion 함수
  • questionAll.forEach를 사용해 questionAll의 요소 개수만큼 반복하여 함수를 실행합니다.
  • 미리 만들어놓은 빈 배열 exam과 omr에 push를 통해 변수를 포함한 본문 내용을 넣어줍니다.

 

  • cbtQuiz(".cbt__quiz")에 exam을 innerHTML해줍니다. (join으로 쉼표제거)
  • cbtOmr(".cbt__omr")에 omr을 innerHTML해줍니다. (join으로 쉼표제거)

 

  • answerQuiz 함수
  • cbtSelects에 각각의 cbt__selects들을 저장합니다.
  • questionAll.forEach를 사용해 qustionAll의 요소 개수만큼 반복하여 함수를 실행합니다.
  • quizSelectWrap = cbtSelect의 number 번째
  • userSelector = 체크된 (name이 select${number}인) input박스
  • userAnswer = number 번째 cbtSelects에서 체크된 (name이 select${number}인) input박스 혹은 아무 것도 없는 값의 value
  • numberAnswer = userAnswer의 값이 있다면 userAnswer.slice(-1) (userAnswer의 마지막글자), 없다면 undefined
  • numberAnswer와 question.Answer의 값이 같다면 cbtSelects[number]의 parentElement(부모요소)의 classList에 "good"을 추가 / 같지 않다면 "bad"를 추가하고 정답인 label의 classList에 "correct"를 추가
728x90
반응형