오늘은 지금까지 했던 퀴즈유형들과 다르게 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/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 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="select1"><span>클래스</span></label>
<input type="radio" id="select2">
<label for="select2"><span>메소드</span></label>
<input type="radio" id="select3">
<label for="select3"><span>상속</span></label>
<input type="radio" id="select4">
<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>
<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 class="omr">
<strong>1</strong>
<input type="radio" id="omr0_1">
<label for="omr0_1">
<span class="label-inner">1</span>
</label>
<input type="radio" id="omr0_2">
<label for="omr0_2">
<span class="label-inner">2</span>
</label>
<input type="radio" id="omr0_3">
<label for="omr0_3">
<span class="label-inner">3</span>
</label>
<input type="radio" id="omr0_4">
<label for="omr0_4">
<span class="label-inner">4</span>
</label>
</div> -->
</div>
</div>
<div class="cbt__submit">제출하기</div>
<div class="cbt__time">59분 10초</div>
</div>
</main>
<!-- //main -->
<!-- <footer id="footer">
<a href="mailto:1346zany@gmail.com">1346zany@gmail.com</a>
</footer> -->
<!-- //footer -->
</body>
</html>
이번에는 CBT유형과 OMR유형으로 만들었으므로 dog__wrap 대신 메인에 quiz__wrap__cbt를 만들어 주었습니다.
cbt__quiz안 요소들을 주석처리 한 이유는 같은 문제를 스크립트로 다르게 표현 해 줄 것이기 때문에 같다는 의미로 주석처리 해두었습니다. omr도 마찬가지 입니다.
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/0.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/1.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: #75b5ff;
color: #fff;
padding: 10px 20px;
text-align: center;
}
.cbt__time {
position: fixed;
right: 160px;
top: 80px;
padding-left: 17px;
background: #5cbeff;
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: #82bbfd;
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;
line-height: 100%;
}
.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: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__selects label.correct span::before {
border-color: red;
box-shadow: inset 0 0 0 10px red;
color: #fff;
}
.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");
}
기존에 사용하던 CSS의 이름들을 변경하면 전에 만든 퀴즈 이펙트들 모두 바뀌어버리므로 아래에 cbt유형을 새로 만들어서 넣어주었습니다. 각각 텍스트 옆 아이콘들은 svg를 background-image로 변환시켜 넣어주었습니다.
json
[
{
"subject": "소프트웨어 설계",
"question": "GoF(Gang of Four)의 디자인 패턴에서 행위 패턴에 속하는 것은?",
"question_img": "gisa2020_01_01",
"correct_answer": "Visitor",
"incorrect_answers": ["Builder","Prototype","Bridge"],
"keyword": "디자인 패턴",
"keyword_num": "24"
},{
"subject": "소프트웨어 설계",
"question": "객체지향 프로그램에서 데이터를 추상화하는 단위는?",
"correct_answer": "클래스",
"incorrect_answers": ["메소드","상속성","메시지"],
"keyword": "객체지향",
"keyword_num": "22",
"desc": "객체지향 구성 요소에는 클래스(Class), 객체(Object), 인스턴스(Instance), 메시지(Message), 메소드(Method) 등이 있다."
},{
"subject": "소프트웨어 설계",
"question": "객체지향 기법에서 클래스들 사이의 '부분-전체(part-shole)' 관계 또는 '부분(is-a-part-of)'의 관계로 설명되는 연관성을 나태나는 용어는?",
"correct_answer": "집단화",
"incorrect_answers": ["일반화","추상화","캡슐화"],
"keyword": "객체지향",
"keyword_num": "22"
},{
"subject": "소프트웨어 설계",
"question": "객체지향 분석 방법론 중 E-R 다이어그램을 사용하여 객체의 행위를 모델링하여, 객체 식별, 구조 식별, 주체 정의, 속성 및 관계 정의, 서비스 정의 등의 과정으로 구성되는 것은?",
"correct_answer": "Coad와 Yourdon 방법",
"incorrect_answers": ["Booch 방법","Jacobson 방법","Wirfs-Brocks 방법"],
"desc": "객체지향 분석 방법론에는 Rumbaugh(럼바우), Booch, Jacobson, Coad와 Yourdon, Wirfs-brock 등이 있다.",
"keyword": "객체지향 분석 방법론",
"keyword_num": "23"
},{
"subject": "소프트웨어 설계",
"question": "코드 설계에서 일정한 일련번호를 부여하는 방식의 코드는?",
"correct_answer": "순차 코드",
"incorrect_answers": ["연상 코드","블록 코드","표의 숫자 코드"]
},{
"subject": "소프트웨어 설계",
"question": "소프트웨어 설계 시 구축된 플랫폼의 성능 특성 분석에 사용되는 측정 항목이 아닌 것은?",
"correct_answer": "서버 튜닝(Server Tunning)",
"incorrect_answers": ["응답시간(Response Time)","가용성(Availability)","사용률(Utilization)"]
},...
그리고 이번엔 json을 이용해서 문제의 정보를 배열 안에 객체들로 담아주었습니다.
저는 문제가 너무 많으므로 6문제 정도 축약해서 가져왔습니다.
script
<script>
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") //fetch, then은 규칙(json파일 갖고 올거임) res는 변수 이름 (respons) , items도 변수 이름
.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;//정답 불러오기(랜덤)
console.log(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.infoQuestionDesc = item.question_desc;
}
//문제에 대한 이미지가 있으면 출력
if(item.hasOwnProperty("question_img")){
formattedQuestion.infoQuestionImg = 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;
console.log(userAnswer)
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>
선택자로 요소들을 선택해줍니다.
questionAll 안에 모든 퀴즈 정보를 담습니다.
fetch와 then은 json 파일을 가져오기 위한 규칙이고 res는 respons의 약자로 변수 이름이며, items도 변수 이름입니다.
formattedQuestion 안에 질문과 문제 번호를 넣어준 뒤 answerChoices 안에 오답을 불러와 정답 외에 다른 문제들이 나올 수 있도록 해줍니다.
그리고 formattedQuestion.answer에 Math.random()을 사용해 정답을 랜덤으로 불러오고 Math.floor로 반올림 해줍니다.
Array.splice() 메서드는 배열에서 원소를 추가/삭제할 수 있는 메서드로, 첫 번째 인자는 추가/삭제를 시작할 인덱스, 두 번째 인자는 삭제할 개수, 그리고 세 번째 이후의 인자들은 추가할 요소들입니다. answerChoices.splice(formattedQuestion.answer-1,0,item.correct_answer)는 formattedQuestion.answer-1 인덱스 위치에 item.correct_answer를 추가하라는 의미입니다.
그리고 forEach문으로 보기를 추가하고 if문에서 hasOwnProperty를 사용해 해당되는 것이 있으면 출력되도록 합니다.
hasOwnProperty 메서드는 객체가 특정 프로퍼티를 갖고 있는지를 나타내는 불리언 값을 반환합니다.
catch() 메소드는 프로미스가 거부됐을 때 실행되는 함수입니다.
만약 fetch() 요청이 실패하면(예를 들면 네트워크 오류 등으로 인해 서버에서 응답을 받지 못한 경우) 프로미스는 거부됩니다. 이 경우 catch() 메소드가 호출되어 콘솔에 에러 메시지를 출력합니다.
push메서드로 안에 문제와 omr이 나타나도록 해줍니다.
push() 메서드는 배열의 끝에 하나 이상의 요소를 추가합니다.
newQuestion() 함수에서는 questionAll이라는 배열에서 각 문제를 가져와서, HTML 코드로 변환한 뒤 exam이라는 배열에 저장합니다.exam 배열은 문제 리스트를, omr 배열은 OMR(Optical Mark Recognition) 시트를 나타냅니다.
answerQuiz() 함수에서는 questionAll 배열에서 문제와 정답을 가져와서, 사용자가 선택한 답안과 비교합니다.
사용자가 선택한 답안이 정답과 일치하면 해당 문제의 HTML 요소에 "good" 클래스를 추가하여 정답임을 표시하고, 일치하지 않으면 "bad" 클래스를 추가하여 오답임을 표시합니다.
이 때, 정답인 보기의 텍스트에는 "correct" 클래스를 추가하여 사용자가 정답인 보기를 쉽게 확인할 수 있도록 합니다.
마지막으로 cbtSubmit 버튼을 클릭하면 answerQuiz() 함수가 실행되도록 이벤트 리스너를 추가하고, dataQuestion() 함수를 실행하여 퀴즈 데이터를 불러옵니다.