728x90
반응형

 

 

 

 

완성 화면

 

 

 

 

 

 

 

코드 보기 / CSS

 

.quiz__wrap__cbt {
    padding:  0 20px;
    font-family: 'ElandChoice';
}
.cbt__content {
    width: calc(100% - 300px);
    background-color: #fff;
}
.cbt__header {
    width: calc(100% - 300px);
    background-color: #f0d2fa;
    border: 8px ridge #440460;
    margin-bottom: 20px;
    padding: 10px 20px;
    background-color: #f1d8fe;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.cbt__aside {
    position: fixed;
    right: 20px;
    top: 180px;
    height: calc(100vh - 140px);
    width: 280px;
    background-color: #fff;
    border: 8px ridge #440460;
    overflow-y: auto;
}
.cbt__quiz {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
}
.cbt__quiz .cbt {
    width: 49%;
    border: 8px ridge #440460;
    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: #f1d8fe;
}
.cbt__info > div {
    border-bottom: 5px ridge #440460;
}
.cbt__info > div:first-child {
    background-color: #f1d8fe;
    color: #000;
    padding: 10px 20px;
    text-align: center;
}
.cbt__time {    
    position: fixed;
    right: 180px;
    top:70px;
    padding-left: 20px;
    background: #440460;
    padding: 10px 27px 10px 44px;
    border-radius: 15px;
    color: #fff;
    margin: 10px 0;
}
.cbt__time::before{
    content: '';
    position: absolute;
    top: 5px;
    left: 10px;
    width: 22px;
    height: 22px;
    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");
}
.cbt__submit {
    position: fixed;
    right: 175px;
    top:100px;
    padding-left: 20px;
    background: #440460;
    cursor: pointer;
    padding: 10px 27px 10px 44px;
    border-radius: 15px;
    color: #fff;
    margin: 25px 0;
}
.cbt__submit::before{
    content: '';
    position: absolute;
    top: 5px;
    left: 10px;
    width: 22px;
    height: 22px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-file-text' 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='M14 3v4a1 1 0 0 0 1 1h4' /%3E%3Cpath d='M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z' /%3E%3Cline x1='9' y1='9' x2='10' y2='9' /%3E%3Cline x1='9' y1='13' x2='15' y2='13' /%3E%3Cline x1='9' y1='17' x2='15' y2='17' /%3E%3C/svg%3E");
}
.cbt__info > div:last-child {
    padding: 20px;
}
.cbt__title {
    text-decoration: underline;
    text-underline-offset: 5px;
    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: #eabcfb;
    font-family: 'Helvetica Neue';
    margin-right: 10px;
}
.cbt__omr .omr label {
    box-shadow: 0 0 0 1px #eabcfb;
    cursor: pointer;
    line-height: 0.5;
    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: #440460;
}
.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: #555;
    position: relative;
}
.cbt__selects label span::before {
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    width: 20px;
    height: 20px;
    border: 1px solid #333;
    border-radius: 30%;
    text-align: center;
    font-family: 'Helvetia Neue';
    font-weight: bold;
    line-height: 1.3;
    font-size: 0.83em;
    transition: all 0.5s;
}
.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: -99999px;
}
.cbt__selects input:checked + label span::before {
    color: #fff;
    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: #eabcfb;
    padding: 10px 20px 10px 40px;
    margin-bottom: 5px;
    position: relative;
    border-radius: 15px;
}
.cbt__desc.hide{
    display: none;
}
.cbt__desc::before {
    content: '';
    position: absolute;
    left: 10px;
    top:5px;
    width: 24px;
    height: 24px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-pencil' width='24' height='24' viewBox='0 0 24 24' stroke-width='2.5' stroke='%23ff4500' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M4 20h4l10.5 -10.5a1.5 1.5 0 0 0 -4 -4l-10.5 10.5v4' /%3E%3Cline x1='13.5' y1='6.5' x2='17.5' y2='10.5' /%3E%3C/svg%3E");
}
.cbt__keyword {
    background-color: #eabcfb;
    padding: 10px 20px 10px 40px;
    margin-bottom: 5px;
    position: relative;
    display: inline-block;
    border-radius: 35px;
}
.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' class='icon icon-tabler icon-tabler-notebook' width='20' height='20' viewBox='0 0 24 24' stroke-width='2.5' stroke='%23000000' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M6 4h11a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-11a1 1 0 0 1 -1 -1v-14a1 1 0 0 1 1 -1m3 0v18' /%3E%3Cline x1='13' y1='8' x2='15' y2='8' /%3E%3Cline x1='13' y1='12' x2='15' y2='12' /%3E%3C/svg%3E");
}

 

 

 

 

 

 

코드 보기 / JAVASCRIPT

 

const cbt = document.querySelectorAll(".cbt");
            const cbtQuiz = document.querySelector(".cbt__quiz");
            const cbtOmr = document.querySelector(".cbt__omr");
            const cbtSubmit = document.querySelector(".cbt__submit");
    
            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('');
        }
        
            //정답 확인
            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();

 

 

 

데이터 가져오기

 

dataQuestion 함수는 fetch() 메서드를 사용하여 "json/gisa2020_01.json"에 위치한 JSON 파일로 HTTP 요청을 수행한다.

그 후 응답 객체에 대해 then() 메서드가 호출된다.

then() 메서드는 응답이 수신되면 실행되는 콜백 함수를 취하며, 응답 데이터는 json() 메서드를 사용하여 JSON 형식으로 변환되고, 결과 Promise에 대해 또 다른 then() 메서드가 호출됩니다. then() 메서드는 다시 JSON 데이터를 매개변수로 받는 콜백 함수를 취한다.


이 콜백 함수 내부에서는 JSON 데이터에 대해 map() 메서드가 호출된다.

이 메서드는 JSON 데이터 배열의 각 항목을 반복하고 제공된 콜백 함수를 실행하여 새로운 배열을 만들며, 콜백 함수는 두 개의 매개변수를 취하는데 그 매개변수는 현재 반복 중인 항목과 그 인덱스가 된다.

map() 콜백 함수 내부에서는 formattedQuestion이라는 새로운 객체가 생성된다.

이 객체는 question 속성을 포함하고 있으며 현재 반복 중인 항목의 question 속성으로 설정되며 number 속성도 포함하고 있으며 현재 인덱스에 1을 더한 값으로 설정된다.

그런 다음 전체 항목에서 올바르지 않은 답변 선택지로 구성된 answerChoices 배열이 만들어지고 전개 연산자(...)를 사용하여 채워진다.

올바른 답변 선택지는 splice() 메서드를 사용하여 answerChoices 배열에 무작위로 인덱스에 삽입되며, 올바른 답변이 삽입되는 인덱스는 Math.floor() 및 Math.random() 메서드를 사용하여 무작위 숫자를 생성하여 결정된다.

다음으로, answerChoices 배열의 각 답변 선택지를 반복하는 루프가 실행된다.

forEach루프 내부에서는 대괄호 표기법을 사용하여 동적으로 생성된 속성 키를 사용하여 formattedQuestion 객체에 새 속성이 추가된다.

속성 키는 현재 반복 중인 답변 선택지의 인덱스를 기반으로 동적으로 생성되며, 속성 값은 답변 선택지 자체로 설정된다.

현재 반복 중인 항목에 desc 속성이 있다면, formattedQuestion 객체는 desc 속성을 포함하도록 업데이트 되며, 이 속성은 desc 속성의 값으로 설정된다.
그리고 map() 콜백 함수에 의해 formattedQuestion 객체가 반환되고, 이 객체는 questionAll 배열에 추가된다.

 JSON 데이터 배열의 모든 항목이 반복되면, newQuestion() 함수가 호출된다.

 

 

 

 

 

fetch() 메서드

: JavaScript에서 네트워크 요청을 보내고 응답을 받아오는 기능을 담당한다.

일반적으로 웹 애플리케이션에서 API 호출이나 서버로부터 데이터를 가져올 때 사용되며,  Promise 객체를 반환한다.

Promise 객체는 비동기 작업을 처리하기 위한 객체로, 작업이 완료되면 이행(resolve) 상태나 거부(reject) 상태가 되며, fetch() 메서드가 반환하는 Promise 객체는 HTTP 응답을 나타내는 Response 객체를 이행(resolve)한다.

 

 

 

 


then() 메서드

:Promise 객체의 상태가 이행(resolve) 상태가 되었을 때 호출되며, 이때 Response 객체를 인자로 받는다.

then() 메서드 안에서는 Response 객체를 가지고 데이터를 추출하고 처리할 수 있다.

 

 

 

 

문제 만들기


newQuestion 함수는 questionAll 배열의 모든 질문 객체를 반복하면서 HTML 요소들을 동적으로 생성하고 cbtQuiz 및 cbtOmr 요소의 내부 HTML로 추가한다.

각 질문 객체의 number, question, choice1 ~ choice4, 그리고 desc 속성 값을 이용하여 각 질문에 대한 HTML 문서 객체를 만든다.

exam 배열에는 cbtQuiz 요소에 들어갈 질문 HTML 코드가 추가되고, omr 배열에는 cbtOmr 요소에 들어갈 OMR HTML 코드가 추가되며, 마지막으로 cbtQuiz 및 cbtOmr 요소의 내부 HTML을 배열 요소를 구분자로 구분하여 exam 및 omr 배열의 값을 이용하여 대체한다.

 

 

 

 

정답 확인하기


answerQuiz 함수는 cbt__selects 클래스를 가진 HTML 요소를 찾아 각 문제의 사용자 선택지와 정답을 비교한다.
사용자의 선택지가 정답과 일치하면 해당 문제의 부모 요소에 good 클래스를 추가하고, 사용자의 선택지가 정답과 일치하지 않으면 해당 문제의 부모 요소에 bad 클래스를 추가하여 정답을 표시하며
해당 문제의 설명을 숨기거나 보여준다.


answerSelect 함수는 onclick 이벤트 시에 사용되며,
사용자가 선택지를 클릭하면 실행된다.
또한 cbtSubmit 버튼(답안지를 제출하는 버튼)에 click 이벤트 리스너를 추가하고 dataQuestion() 함수를 호출한다.

 

 

 

 

 

 

 

+ Recent posts