퀴즈 사이트 만들기
이번에는 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;
}
}
🎈 미디어 쿼리를 만들어 반응형웹디자인을 구현합니다.