Fast Blinking Hello Kitty

SITE 만들기

퀴즈 사이트 만들기

코른이되고싶은코린이 2023. 4. 3. 23:47

728x90

퀴즈 사이트 만들기

이번에는 CBT(컴퓨터 따위로 작성된 훈련 프로그램에 따라 교육하는 작업)유형과 사이드에 OMR카드 효과를 준 퀴즈 사이트를 만들어 보겠습니다.

코드블럭

<!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/reset.css">
    <link rel="stylesheet" href="CSS/quiz.css">
    
    <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>
            <div class="cbt__aside">
                <div class="cbt__info">
                    <div>
                        <div class="cbt__title">수험자 : <em>이은지</em></div>
                        <div class="cbt__score">
                            <span>전체 문제수 : <em>60</em>문항</span>
                            <span>남은 문제수 : <em>59문항</em></span>
                        </div>
                    </div>
                </div>
                <div class="cbt__omr">
                   
                </div>
            </div>
            <div class="cbt__submit">제출하기</div>
            <div class="cbt__time">59분 10초</div>
        </div>
    </main>
    <!-- //main -->
    <!-- <footer id="footer">
        <a href="mailto:lee3ll@naver.com">lee3ll@naver.com</a>
    </footer> -->
    <!-- //footer -->

    <script>
        const cbt = document.querySelectorAll(".cbt");
        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");

        let questionAll = [];  //모든 퀴즈 정보

        //데이터 가져오기
        const dataQuestion = () => {
            fetch("json/gisa2020_01.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.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 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"></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="omr${number}" id="omr${number}_1" value="${number}_0">
                        <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">
                        <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">
                        <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">
                        <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 quizSelectsWrap = cbtSelects[number];
                const userSelector = `input[name=select${number}]:checked`;
                const userAnswer = (quizSelectsWrap.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();
        </script>
</body>
</html>

보충설명

🎈 HTML 문서 내에 있는 .cbt, .cbt__quiz, .cbt__omr, .cbt__submit, .cbt__title em 클래스에 대한 참조를 생성한 다음, questionAll 배열을 선언합니다. dataQuestion 함수는 fetch() 메소드를 사용하여 JSON 파일에서 데이터를 가져오고, 각 항목을 새로운 객체로 변환합니다. 객체에는 문제, 번호, 답안 및 보기가 포함됩니다

 

🎈 answerQuiz 함수는 사용자가 제출한 답안을 확인하고 정답과 일치하는 경우 console.log()를 호출합니다.

 

🎈 선택자를 만들어 해당 HTML의 요소들을 가져와 변수에 저장합니다.

 

🎈 let questionAll = []; 빈매열을 선언하여 이곳에 모든 퀴즈 정보를 저장합니다.

 

🎈 fetch("jason/gisa2020_01.json") 을 통해 서버로부터 JSON 파일을 가져와서 questionAll 배열에 저장합니다. 가져온 JSON 파일을 questionAll 배열에 저장하기 전에 각 항목을 원하는 형식으로 만들어줍니다. 

 

🎈 const newQuestion = () => { } newQuestion 함수를 호출하여 문제를 만듭니다.

 

🎈 const answerQuiz = () => {  } questionAll 배열의 모든 항목을 돌면서, 각 항목에 해당하는 HTML 코드를 exam 배열과 omr 배열에 저장합니다. exam 배열과 omr 배열에 저장된 HTML 코드를 cbtQuiz와 cbtOmr 요소의 innerHTML에 적용하여 문제와 OMR카드를 만듭니다.

 

🎈 cbt__selects 클래스를 가진 HTML 요소를 가져와 questionAll 배열의 모든 항목에 대해 돌면서, 각 항목의 정답 여부를 판단합니다. 정답이면 "정답입니다."를 콘솔에 출력하고, 오답이면 "오답입니다."를 콘솔에 출력합니다.

 

CSS

/* cbt 유형 */
.quiz__wrap__cbt {
    padding: 0 20px;
    font-family: 'PyeongChang';
}
.cbt__conts {
    width: calc(100% - 300px);
    background-color: #fff;
}
.cbt__header {
    width: calc(100% - 300px);
    background-color: #fff;
    border: 8px ridge #cacaca;
    margin-bottom: 20px;
    padding: 10px 20px;
    background-color: #e3edff;
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.cbt__aside {
    position: fixed;
    right: 20px;
    top: 135px;
    height: calc(100vh - 150px);
    width: 280px;
    background-color: #fff;
    border: 8px ridge #cacaca;
    overflow-y: auto;
}
.cbt__quiz {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
}
.cbt__quiz .cbt {
    width: 49%;
    border: 8px ridge #cacaca;
    margin-bottom: 10px;
    padding: 20px;
}
.cbt__quiz .cbt {
    position: relative;
}
.cbt__quiz .cbt.good::after {
    content: '';
    background-image: url(../img/O.png);
    background-size: contain;
    background-repeat: no-repeat;
    width: 200px;
    height: 200px;
    position: absolute;
    left: 0;
    top: 0;
}
.cbt__quiz .cbt.bad::after {
    content: '';
    background-image: url(../img/X.png);
    background-size: contain;
    background-repeat: no-repeat;
    width: 200px;
    height: 200px;
    position: absolute;
    left: 0;
    top: 0;
}
.cbt__info {
    background-color: #e3edff;
}
.cbt__info > div {
    border-bottom: 4px ridge #cacaca;
}
.cbt__info > div:first-child {
    background-color: #546482;
    color: #fff;
    padding: 10px 20px;
    text-align: center;
}
.cbt__time {
    position: fixed;
    right: 160px;
    top: 80px;
    padding-left: 17px;
    background: #423cff;
    padding: 10px 24px 10px 40px;
    border-radius: 40px;
    color: #fff;
}
.cbt__time::before {
    content: '';
    position: absolute;
    left: 15px;
    top: 9px;
    width: 22px;
    height: 22px;
    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='%23fff' class='w-6 h-6'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z' /%3E%3C/svg%3E");
}
.cbt__submit {
    position: fixed;
    right: 20px;
    top: 80px;
    padding-left: 20px;
    background: #ff3c3c;
    display: inline-block;
    padding: 10px 27px 10px 44px;
    border-radius: 40px;
    color: #fff;
}
.cbt__submit::before {
    content: '';
    position: absolute;
    left: 18px;
    top: 9px;
    width: 22px;
    height: 22px;
    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='%23fff' 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%0A");
}
.cbt__info > div:last-child {
    padding: 20px;
}
.cbt__title {
    text-decoration: underline;
    text-underline-offset: 4px;
    margin-bottom: 5px;
}
.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: #313e55;
    color: #fff;
    font-family: 'Helvetica Neue';
    margin-right: 10px;
    font-weight: bold;
}
.cbt__omr .omr label {
    box-shadow: 0 0 0 1px #313e55;
    cursor: pointer;
    line-height: 0.4;
    text-align: center;
    width: 28px;
    height: 8px;
    font-family: 'Helvetica Neue';
    position: relative;
}
.cbt__omr .omr label::after {
    background-color: #555;
    content: "";
    display: block;
    position: absolute;
    top: 0;
    left: 0;
    width: 0;
    height: 100%;
    z-index: 1;
    transition: width 0.1s linear;
}
.cbt__omr .omr input[type=radio]:checked + label::after {
    width: 100%;
}
.cbt__omr .omr .label-inner {
    background-color: #fff;
    padding: 0.25em 0.13em;
    transform: translateY(-0.25em);
    width: 20px;
    color: #313e55;
}
.cbt__question {
    font-size: 1.4rem;
    margin-bottom: 10px;
}
.cbt__question__img img {
    max-width: 400px;
    margin-bottom: 15px;
}
.cbt__question__desc {
    border: 2px solid #cacaca;
    padding: 10px;
    margin-bottom: 15px;
}
.cbt__selects {
    margin-bottom: 15px;
}
.cbt__selects label {
    display: flex;
}

.cbt__selects label span {
    font-size: 1rem;
    padding: 10px 10px 10px 30px;
    cursor: pointer;
    color: #444;
    position: relative;
}
.cbt__selects label span::before {
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    width: 20px;
    height: 20px;
    border: 1px solid #444;
    border-radius: 50%;
    text-align: center;
    font-family: 'PyeongChang';
    font-weight: bold;
    line-height: 1.4;
    font-size: 0.83em;
    transition: all 0.25s;
}
.cbt__selects label.correct span::before {
    border-color: red;
    box-shadow: inset 0 0 0 10px red;
    color: #fff;
}
.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;
    box-shadow: inset 0 0 0 10px #000;
    border-color: #000;
}
.cbt__desc {
    background-color: #e3edff;
    padding: 10px 10px 10px 40px;
    margin-bottom: 5px;
    position: relative;
}
.cbt__desc.hide {
    display: none;
} 
.cbt__desc::before {
    content: '';
    position: absolute;
    left: 16px; 
    top: 10px;
    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='M15.59 14.37a6 6 0 01-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 006.16-12.12A14.98 14.98 0 009.631 8.41m5.96 5.96a14.926 14.926 0 01-5.841 2.58m-.119-8.54a6 6 0 00-7.381 5.84h4.8m2.581-5.84a14.927 14.927 0 00-2.58 5.84m2.699 2.7c-.103.021-.207.041-.311.06a15.09 15.09 0 01-2.448-2.448 14.9 14.9 0 01.06-.312m-2.24 2.39a4.493 4.493 0 00-1.757 4.306 4.493 4.493 0 004.306-1.758M16.5 9a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0z' /%3E%3C/svg%3E%0A");
}
.cbt__keyword {
    background-color: #ffe1c4;
    padding: 10px 20px 10px 40px;
    margin-bottom: 5px;
    position: relative;
    display: inline-block;
    border-radius: 40px;
}
.cbt__keyword::before {
    content: '';
    position: absolute;
    left: 16px; 
    top: 10px;
    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.5 18.75h-9m9 0a3 3 0 013 3h-15a3 3 0 013-3m9 0v-3.375c0-.621-.503-1.125-1.125-1.125h-.871M7.5 18.75v-3.375c0-.621.504-1.125 1.125-1.125h.872m5.007 0H9.497m5.007 0a7.454 7.454 0 01-.982-3.172M9.497 14.25a7.454 7.454 0 00.981-3.172M5.25 4.236c-.982.143-1.954.317-2.916.52A6.003 6.003 0 007.73 9.728M5.25 4.236V4.5c0 2.108.966 3.99 2.48 5.228M5.25 4.236V2.721C7.456 2.41 9.71 2.25 12 2.25c2.291 0 4.545.16 6.75.47v1.516M7.73 9.728a6.726 6.726 0 002.748 1.35m8.272-6.842V4.5c0 2.108-.966 3.99-2.48 5.228m2.48-5.492a46.32 46.32 0 012.916.52 6.003 6.003 0 01-5.395 4.972m0 0a6.726 6.726 0 01-2.749 1.35m0 0a6.772 6.772 0 01-3.044 0' /%3E%3C/svg%3E%0A");
}

미디어쿼리

@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 {
        position: relative;
    }
    .cbt__header {
        width: 100%;
        flex-direction: column;
    }
    .cbt__header h2{
        margin-bottom: 10px;
    }
    .cbt__conts {
        width: 100%;
    }
    .cbt__aside {
        display: none;
    }
}

🎈 미디어 쿼리를 만들어 반응형웹디자인을 구현합니다.