
가볍게 파일 2개로 구성한 사이트입니다.
index.php
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
session_start();
// 점수 초기화
if (!isset($_SESSION['correct_cnt'])) $_SESSION['correct_cnt'] = 0;
if (!isset($_SESSION['wrong_cnt'])) $_SESSION['wrong_cnt'] = 0;
if (!isset($_SESSION['total_cnt'])) $_SESSION['total_cnt'] = 0;
// CSV 읽기
$rows = [];
if (($handle = fopen(__DIR__ . "/quiz_db.csv", "r")) !== false) {
while (($data = fgetcsv($handle, 0, ",", "\"", "")) !== false) {
$rows[] = $data; // [번호,문제,선택지,정답,해설]
}
fclose($handle);
}
if (!$rows) die("문제를 불러올 수 없습니다.");
// 문항수 확인 변수
$total_questions = count($rows);
// ==== 공통 상태 변수 ====
$mode = 'question'; // 'question' | 'graded' | 'final'
$userAnswer = '';
$correctFull = '';
$explanation = '';
$isCorrect = false;
// 점수 초기화 요청
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['reset_score'])) {
$_SESSION['correct_cnt'] = 0;
$_SESSION['wrong_cnt'] = 0;
$_SESSION['total_cnt'] = 0;
header("Location: /"); // 또는 "index.php"
exit;
}
$isPost = ($_SERVER['REQUEST_METHOD'] === 'POST');
if ($isPost && isset($_POST['show_result'])) {
// 최종결과 보기: 채점/카운트 증가 없이 결과만
$mode = 'final';
} elseif ($isPost) {
// 제출 후 채점 단계
$mode = 'graded';
$userAnswer = $_POST['answer'] ?? '';
$correct = $_POST['correct'] ?? ''; // 번호만 (예: ①)
$explanation = $_POST['explanation'] ?? '';
$optionsRaw = $_POST['options_raw'] ?? '';
// 정답 비교: 맨 앞 "①" 등 번호만 추출
$answer_no = mb_substr(trim($userAnswer), 0, 1, "UTF-8");
$correct_no = mb_substr(trim($correct), 0, 1, "UTF-8");
$isCorrect = ($answer_no === $correct_no);
// 정답 번호로 전체 텍스트 찾기
$correctFull = $correct;
if ($optionsRaw) {
$optsArr = explode("\n", $optionsRaw);
foreach ($optsArr as $optItem) {
$optItem = trim($optItem);
if (mb_substr($optItem, 0, 1, 'UTF-8') === $correct_no) {
$correctFull = $optItem;
break;
}
}
}
// 실제 문제 제출일 때만 카운트 증가
$_SESSION['total_cnt']++;
if ($isCorrect) {
$_SESSION['correct_cnt']++;
} else {
$_SESSION['wrong_cnt']++;
}
} else {
// 새 문제 출제
$mode = 'question';
$idx = array_rand($rows);
$q = $rows[$idx];
$options = explode("\n", $q[2]);
}
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>정보보안기사</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
:root {
--bg: #f4f5f7;
--card-bg: #ffffff;
--primary: #2563eb;
--danger: #dc2626;
--text-main: #111827;
--text-sub: #6b7280;
--border-soft: #e5e7eb;
--radius-lg: 16px;
--radius-md: 10px;
--shadow-soft: 0 10px 25px rgba(15, 23, 42, 0.08);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: radial-gradient(circle at top, #e5edff 0, #f4f5f7 45%, #eef2ff 100%);
color: var(--text-main);
}
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px 12px;
}
.app-header {
margin-bottom: 16px;
text-align: center;
}
.app-header h1 {
margin: 0;
font-size: 1.4rem;
letter-spacing: 0.04em;
color: var(--primary);
}
.card {
width: 100%;
max-width: 640px;
background: var(--card-bg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-soft);
padding: 20px 20px 18px;
}
.stats {
display: flex;
flex-wrap: wrap;
gap: 6px 16px;
margin-bottom: 10px;
font-size: 0.9rem;
color: var(--text-sub);
}
.stats strong {
color: var(--text-main);
}
.card h2 {
margin-top: 4px;
margin-bottom: 12px;
font-size: 1.05rem;
}
.options {
margin-bottom: 16px;
}
.options label {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
margin-bottom: 6px;
border-radius: var(--radius-md);
border: 1px solid transparent;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease, transform 0.05s ease;
}
.options label:hover {
background: #f9fafb;
border-color: var(--border-soft);
transform: translateY(-1px);
}
.options input[type="radio"] {
accent-color: var(--primary);
}
button {
font-family: inherit;
font-size: 0.95rem;
padding: 8px 14px;
border-radius: 999px;
border: none;
cursor: pointer;
transition: background 0.15s ease, box-shadow 0.15s ease, transform 0.05s ease;
}
.btn-primary {
background: var(--primary);
color: #fff;
box-shadow: 0 4px 10px rgba(37, 99, 235, 0.25);
}
.btn-primary:hover:not(:disabled) {
background: #1d4ed8;
transform: translateY(-1px);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: default;
box-shadow: none;
}
.btn-ghost {
background: transparent;
color: var(--text-sub);
}
.btn-ghost:hover {
background: #f3f4f6;
}
.actions-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
align-items: center;
}
.result-ok {
color: #16a34a;
}
.result-bad {
color: var(--danger);
}
.explanation {
margin-top: 4px;
padding: 8px 10px;
border-radius: var(--radius-md);
background: #f9fafb;
font-size: 0.9rem;
}
.app-footer {
margin-top: 16px;
font-size: 0.75rem;
color: var(--text-sub);
text-align: center;
}
@media (max-width: 480px) {
.card {
padding: 16px 14px 14px;
}
.app-header h1 {
font-size: 1.2rem;
}
}
</style>
</head>
<body>
<div class="app">
<header class="app-header">
<h1>정보보안기사</h1>
</header>
<main class="card">
<?php if ($mode === 'graded'): ?>
<div class="stats">
<span>총 문제 수: <strong><?= $total_questions ?></strong> 문항</span>
<span>진행: <strong><?= $_SESSION['total_cnt'] ?></strong> 문제</span>
<span>정답: <strong><?= $_SESSION['correct_cnt'] ?></strong></span>
<span>오답: <strong><?= $_SESSION['wrong_cnt'] ?></strong></span>
</div>
<?php
$score = ($_SESSION['total_cnt'] > 0)
? round($_SESSION['correct_cnt'] * 100 / $_SESSION['total_cnt'])
: 0;
echo "<p><b>점수: {$score}점 (100점 만점 기준)</b></p>";
?>
<h2>채점 결과</h2>
<p>당신의 답: <?= htmlspecialchars($userAnswer) ?></p>
<p>정답: <b><?= htmlspecialchars($correctFull) ?></b></p>
<?php if ($isCorrect): ?>
<h3 class="result-ok">정답입니다!</h3>
<?php else: ?>
<h3 class="result-bad">오답입니다.</h3>
<?php endif; ?>
<div class="explanation">
<?= nl2br(htmlspecialchars($explanation)) ?>
</div>
<div class="actions-row">
<button type="button" id="nextbtn" class="btn-primary"
onclick="location.href=location.pathname;">다음 문제</button>
<form method="post" action="">
<input type="hidden" name="show_result" value="1">
<button type="submit" id="resultbtn" class="btn-ghost">최종결과 보기</button>
</form>
</div>
<br>
<p style="margin-top:10px; font-size:0.8rem; color:var(--text-sub);">
다음 문제 : 0 / 엔터 / 스페이스
</p>
<script>
document.addEventListener('keydown', function(e) {
if (
e.key === '0' || e.code === 'Numpad0' ||
e.key === 'Enter' || e.code === 'Enter' ||
e.key === ' ' || e.code === 'Space'
) {
document.getElementById('nextbtn').click();
}
});
</script>
<?php elseif ($mode === 'final'): ?>
<h2>최종 결과</h2>
<p>총 <?= $_SESSION['total_cnt'] ?> 문제 중
<?= $_SESSION['correct_cnt'] ?>개 맞춤,
<?= $_SESSION['wrong_cnt'] ?>개 틀림</p>
<?php
$score = ($_SESSION['total_cnt'] > 0)
? round($_SESSION['correct_cnt'] * 100 / $_SESSION['total_cnt'])
: 0;
echo "<p><b>점수: {$score}점 (100점 만점 기준)</b></p>";
?>
<div class="actions-row">
<form method="post" action="" id="resetform">
<button type="submit" name="reset_score" value="1"
id="resetbtn" class="btn-primary">처음부터 다시</button>
</form>
</div>
<script>
document.addEventListener('keydown', function(e) {
if (
e.key === '0' || e.code === 'Numpad0' ||
e.key === 'Enter' || e.code === 'Enter' ||
e.key === ' ' || e.code === 'Space'
) {
document.getElementById('resetbtn').click();
}
});
</script>
<?php else: /* $mode === 'question' */ ?>
<div class="stats">
<span>총 문제 수: <strong><?= $total_questions ?></strong> 문항</span>
<span>진행: <strong><?= $_SESSION['total_cnt'] ?></strong> 문제</span>
<span>정답: <strong><?= $_SESSION['correct_cnt'] ?></strong></span>
<span>오답: <strong><?= $_SESSION['wrong_cnt'] ?></strong></span>
</div>
<h2><?= htmlspecialchars($q[0]) ?>. <?= htmlspecialchars($q[1]) ?></h2>
<form id="quizform" method="post" action="">
<div class="options">
<?php foreach ($options as $i => $opt): ?>
<?php $opt = trim($opt); if ($opt === '') continue; ?>
<?php $oid = "opt".($i+1); ?>
<label for="<?= $oid ?>">
<input type="radio" id="<?= $oid ?>" name="answer"
value="<?= htmlspecialchars($opt) ?>" required>
<?= htmlspecialchars($opt) ?>
</label>
<?php endforeach; ?>
</div>
<input type="hidden" name="correct" value="<?= htmlspecialchars(trim($q[3])) ?>">
<input type="hidden" name="explanation" value="<?= htmlspecialchars($q[4]) ?>">
<input type="hidden" name="options_raw" value="<?= htmlspecialchars($q[2]) ?>">
<button type="submit" id="submitbtn" class="btn-primary">제출</button>
</form>
<br>
<p style="margin-top:10px; font-size:0.8rem; color:var(--text-sub);">
문항 선택 : 1 ~ 4, 제출 : 0 / 엔터 / 스페이스
</p>
<script>
document.addEventListener('keydown', function(e) {
if (e.key === '1' || e.code === 'Numpad1') document.getElementById('opt1').checked = true;
if (e.key === '2' || e.code === 'Numpad2') document.getElementById('opt2').checked = true;
if (e.key === '3' || e.code === 'Numpad3') document.getElementById('opt3').checked = true;
if (e.key === '4' || e.code === 'Numpad4') document.getElementById('opt4').checked = true;
if (
e.key === '0' || e.code === 'Numpad0' ||
e.key === 'Enter' || e.code === 'Enter' ||
e.key === ' ' || e.code === 'Space'
) {
document.getElementById('submitbtn').click();
}
});
document.getElementById('quizform').addEventListener('submit', function(e) {
document.getElementById('submitbtn').disabled = true;
});
</script>
<?php endif; ?>
</main>
<footer class="app-footer">
<small>만든이. 코코비비 / <a href="https://open.kakao.com/me/jaesung">문의 및 개선사항 요청</a></small>
</footer>
</div>
</body>
</html>
quiz_db.csv
"1","대칭키 암호화 알고리즘으로 128비트 블록 크기를 사용하며, 키 길이가 128, 192, 256비트를 지원하는 암호화 알고리즘은?","① DES
② AES
③ RSA
④ ECC","②","AES(Advanced Encryption Standard)는 128비트 블록 크기와 128/192/256비트 키 길이를 지원하는 대칭키 암호화 표준입니다."
"2","SQL Injection 공격을 방어하기 위한 가장 효과적인 방법은?","① 방화벽 설치
② Prepared Statement 사용
③ 안티바이러스 설치
④ 네트워크 분리","②","Prepared Statement(준비된 문장)를 사용하면 SQL 쿼리와 데이터를 분리하여 SQL Injection 공격을 효과적으로 방어할 수 있습니다."
"3","OSI 7계층 모델에서 종단 간 신뢰성 있는 데이터 전송을 담당하는 계층은?","① 네트워크 계층
② 세션 계층
③ 전송 계층
④ 응용 계층","③","전송 계층(Transport Layer)은 종단 간 신뢰성 있는 데이터 전송을 담당하며 TCP, UDP 프로토콜이 동작합니다."
"4","다음 중 디지털 서명의 주요 목적이 아닌 것은?","① 기밀성
② 무결성
③ 인증
④ 부인방지","①","디지털 서명은 무결성, 인증, 부인방지를 제공하지만, 기밀성은 제공하지 않습니다. 기밀성은 암호화를 통해 제공됩니다."
"5","ISMS-P 인증 기준에서 정보보호 및 개인정보보호 관리체계의 주기적 검토 및 개선 주기는?","① 6개월
② 1년
③ 2년
④ 3년","②","ISMS-P 인증에서 관리체계는 최소 연 1회 이상 정기적으로 검토 및 개선되어야 합니다."
728x90
