๐ฏ ์ค๋์ ํ์ ํ์ต ์นด๋ - ๋งค์ผ ํ ๊ธ์์ฉ ์ตํ๊ธฐ
daily-hanja-learning-card
๋ฌธ์ ์ค๋ช
๐ ํ๋ก์ ํธ ๊ฐ์
๋งค์ผ ํ ๊ธ์์ฉ ํ์๋ฅผ ํ์ตํ๊ณ ๋ณต์ตํ ์ ์๋ ํ์ ํ์ต ์นด๋ ํ๋ซํผ์ ๋๋ค. ๊ทธ๋๋ณด๋5 SE ์์์ ๋์ํ๋ฉฐ, ์ฌ์ฉ์๊ฐ ์ค๋์ ํ์๋ฅผ ํ์ธํ๊ณ ํด์ฆ๋ก ๋ณต์ตํ๋ฉฐ ํ์ต ์งํ๋ฅ ์ ์ถ์ ํ ์ ์์ต๋๋ค.
โจ ํต์ฌ ๊ธฐ๋ฅ
ํ์ ๊ธฐ๋ฅ:
- ๐ ์ค๋์ ํ์ ์นด๋: ๋งค์ผ ํ๋์ฉ ์๋ก์ด ํ์๋ฅผ ์๊ฐ (ํ์, ํ์, ๋ถ์, ๋ป, ์๋ฌธ ํฌํจ)
- ๐ ํ์ต ์์นด์ด๋ธ: ์ง๊ธ๊น์ง ํ์ตํ ํ์ ๋ชฉ๋ก ๋ณด๊ธฐ (๋ ์ง๋ณ ์ ๋ ฌ)
- โ ๋ณต์ต ํด์ฆ: ๋๋ค์ผ๋ก ํ์ตํ ํ์ ์ค 5๊ฐ๋ฅผ ๊ณจ๋ผ ํ์ ๋ง์ถ๊ธฐ ํด์ฆ
- ๐ ํ์ต ํต๊ณ: ์ด ํ์ต ์ผ์, ์ฐ์ ํ์ต ์ผ์, ํด์ฆ ์ ๋ต๋ฅ ํ์
- ๐จ ์นด๋ ํ๋ฆฝ ์ ๋๋ฉ์ด์ : ํ์ ์นด๋๋ฅผ ํด๋ฆญํ๋ฉด ๋ท๋ฉด(์์ธ ์ ๋ณด)์ผ๋ก ๋ค์งํ๋ ์ธํฐ๋์
๋ณด๋์ค ๊ธฐ๋ฅ:
- ๐ ๋ถ๋งํฌ ๊ธฐ๋ฅ: ์ด๋ ค์ด ํ์๋ฅผ ์ฆ๊ฒจ์ฐพ๊ธฐ๋ก ์ ์ฅ
- ๐ ํ์ต ๋ฑ์ง: ์ฐ์ 7์ผ, 30์ผ, 100์ผ ํ์ต ์ ๋ฑ์ง ํ๋
- ๐ ํ์ ๊ฒ์: ํ์์ด๋ ํ์๋ก ๊ฒ์ํ์ฌ ๊ณผ๊ฑฐ ํ์ต ๋ด์ฉ ์ฐพ๊ธฐ
- ๐ฑ ๋ฐ์ํ ๋์์ธ: ๋ชจ๋ฐ์ผ์์๋ ์พ์ ํ ํ์ต ๊ฒฝํ
๐ฏ ํ์ต ๋ชฉํ
- g5se์ modern shell์ ํ์ฉํ ์ผ๊ด๋ UI ๊ตฌํ
- ํด๋ ๊ธฐ๋ฐ ์๋ ๋ผ์ฐํ ์ดํด
- SQLite๋ฅผ ํ์ฉํ ๋ชจ๋ ๋ ๋ฆฝ ๋ฐ์ดํฐ ๊ด๋ฆฌ
- CSS ๋ณ์๋ฅผ ํ์ฉํ ๋คํฌ๋ชจ๋ ๋์
- ๋ฐ๋๋ผ JavaScript๋ก ์นด๋ ํ๋ฆฝ, ํด์ฆ ๋ก์ง ๊ตฌํ
- prepared statement๋ก SQL ์ธ์ ์ ๋ฐฉ์ง
๐ ๊ธฐ์ ์คํ
- ๋ฐฑ์๋: PHP (g5se ์ปจ๋ฒค์ )
- ๋ฐ์ดํฐ๋ฒ ์ด์ค: SQLite (๋ชจ๋ ์ ์ฉ DB)
- ํ๋ก ํธ์๋: HTML5, CSS3 (๋์์ธ ํ ํฐ), Vanilla JavaScript
- UI ํ๋ ์์ํฌ: g5se modern shell
๐ ํ๋ก์ ํธ ๊ตฌ์กฐ
app/modules/hanja/
โโโ index.php # ๋ฉ์ธ ํ์ด์ง (์ค๋์ ํ์)
โโโ archive.php # ํ์ต ์์นด์ด๋ธ
โโโ quiz.php # ๋ณต์ต ํด์ฆ
data/modules/
โโโ hanja.sqlite # ํ์ ํ์ต ๋ฐ์ดํฐ
์ ์ฝ ์กฐ๊ฑด
- โข g5se ์ค์น๋ณธ ์์์ ์์ (https://github.com/gnuboard/g5se main ๋ธ๋์น)
- โข ํด๋ ๊ธฐ๋ฐ ์๋ ๋ผ์ฐํ ์ฌ์ฉ (app/modules/hanja/index.php โ /hanja)
- โข modern shell ํ์ ์ ์ฉ (_head.inc.php + m-shell + _nav.inc.php + m-container + _footer.inc.php)
- โข ๋์์ธ ํ ํฐ(CSS ๋ณ์) ์ฌ์ฉ์ผ๋ก ๋คํฌ๋ชจ๋ ์๋ ๋์
- โข ๋ชจ๋ ์ ์ฉ SQLite ํ์ผ ์ฌ์ฉ (G5_DATA_PATH.'/modules/hanja.sqlite')
- โข prepared statement๋ก SQL ์ธ์ ์ ๋ฐฉ์ง
- โข ์ธ๋ถ ๋น๋ ๋๊ตฌ(npm/vite) ์ฌ์ฉ ๊ธ์ง
- โข CDN ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ต์ํ, ์์ HTML/CSS/JS ๊ถ์ฅ
- โข ๋ชจ๋ ์ฌ์ฉ์ ์ ๋ ฅ์ htmlspecialchars()๋ก ์ด์ค์ผ์ดํ
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
g5se๊ฐ ์ค์น๋ ๋๋ ํฐ๋ฆฌ(๋ฃจํธ์ `index.php`ยท`app/`์ด ์๋ ๊ณณ)์์ ์์
์ ์์ํฉ๋๋ค. (g5se ์ค์น๋ https://github.com/gnuboard/g5se ์ฐธ๊ณ )
## Step 1: ํ๋ก์ ํธ ๊ธฐ๋ณธ ๊ตฌ์กฐ ์์ฑ
### 1) ๋ชจ๋ ํด๋ ์์ฑ
```bash
mkdir -p app/modules/hanja
```
### 2) ๋ฉ์ธ ํ์ด์ง ์์ฑ (app/modules/hanja/index.php)
๋ค์ ๋ด์ฉ์ผ๋ก ํ์ผ์ ์์ฑํ์ธ์:
```php
<?php
include_once('./_common.php');
// ๋ชจ๋ DB ์ด๊ธฐํ
@mkdir(G5_DATA_PATH.'/modules', 0707, true);
$db = new PDO('sqlite:'.G5_DATA_PATH.'/modules/hanja.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// ํ
์ด๋ธ ์์ฑ (์ต์ด ์คํ ์)
$db->exec("
CREATE TABLE IF NOT EXISTS hanja_cards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hanja TEXT NOT NULL,
reading TEXT NOT NULL,
meaning TEXT NOT NULL,
strokes INTEGER,
radical TEXT,
examples TEXT,
learned_date DATE,
is_bookmarked INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
");
$db->exec("
CREATE TABLE IF NOT EXISTS hanja_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
total_days INTEGER DEFAULT 0,
streak_days INTEGER DEFAULT 0,
quiz_attempts INTEGER DEFAULT 0,
quiz_correct INTEGER DEFAULT 0,
last_study_date DATE
)
");
// ํต๊ณ ๋ ์ฝ๋ ์ด๊ธฐํ
$statsCheck = $db->query("SELECT COUNT(*) FROM hanja_stats")->fetchColumn();
if ($statsCheck == 0) {
$db->exec("INSERT INTO hanja_stats (total_days, streak_days) VALUES (0, 0)");
}
// ์ํ ํ์ ๋ฐ์ดํฐ ์ฝ์
(์ต์ด ์คํ ์)
$count = $db->query("SELECT COUNT(*) FROM hanja_cards")->fetchColumn();
if ($count == 0) {
$sampleData = [
['ๅคง', 'ํฐ ๋', 'ํฌ๋ค, ์๋ํ๋ค', 3, 'ๅคง', 'ๅคงๅญธ(๋ํ), ๅคง้ๆฐๅ(๋ํ๋ฏผ๊ตญ)'],
['ๅฐ', '์์ ์', '์๋ค', 3, 'ๅฐ', 'ๅฐๅญธๆ ก(์ํ๊ต), ๅคงๅฐ(๋์)'],
['ไธญ', '๊ฐ์ด๋ฐ ์ค', '๊ฐ์ด๋ฐ, ์', 4, 'ไธจ', 'ไธญๅญธๆ ก(์คํ๊ต), ไธญๅคฎ(์ค์)'],
['ไบบ', '์ฌ๋ ์ธ', '์ฌ๋', 2, 'ไบบ', 'ไบบ้(์ธ๊ฐ), ไบบ็(์ธ์)'],
['ๆฅ', '๋ ์ผ', 'ํด, ๋ ', 4, 'ๆฅ', 'ๆฅๆฌ(์ผ๋ณธ), ๆฏๆฅ(๋งค์ผ)'],
];
$stmt = $db->prepare("
INSERT INTO hanja_cards (hanja, reading, meaning, strokes, radical, examples)
VALUES (?, ?, ?, ?, ?, ?)
");
foreach ($sampleData as $data) {
$stmt->execute($data);
}
}
// ์ค๋์ ํ์ ๊ฐ์ ธ์ค๊ธฐ (์์ง ํ์ตํ์ง ์์ ์ฒซ ๋ฒ์งธ ํ์)
$todayHanja = $db->query("
SELECT * FROM hanja_cards
WHERE learned_date IS NULL
ORDER BY id ASC
LIMIT 1
")->fetch(PDO::FETCH_ASSOC);
// ํต๊ณ ๊ฐ์ ธ์ค๊ธฐ
$stats = $db->query("SELECT * FROM hanja_stats LIMIT 1")->fetch(PDO::FETCH_ASSOC);
require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<style>
.hanja-card {
perspective: 1000px;
margin: 32px auto;
max-width: 400px;
height: 500px;
cursor: pointer;
}
.hanja-card-inner {
position: relative;
width: 100%;
height: 100%;
transition: transform 0.6s;
transform-style: preserve-3d;
}
.hanja-card.flipped .hanja-card-inner {
transform: rotateY(180deg);
}
.hanja-card-front,
.hanja-card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
border-radius: var(--m-radius-lg);
background: var(--m-surface);
box-shadow: var(--m-shadow);
padding: 40px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.hanja-card-back {
transform: rotateY(180deg);
}
.hanja-display {
font-size: 120px;
font-weight: bold;
color: var(--m-primary);
margin-bottom: 16px;
}
.hanja-reading {
font-size: 24px;
color: var(--m-text);
margin-bottom: 8px;
}
.hanja-meaning {
font-size: 18px;
color: var(--m-text-muted);
margin-bottom: 24px;
}
.hanja-details {
width: 100%;
text-align: left;
}
.hanja-detail-row {
display: flex;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid var(--m-text-muted);
opacity: 0.3;
}
.hanja-detail-label {
font-weight: bold;
color: var(--m-text);
}
.hanja-detail-value {
color: var(--m-text-muted);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
margin-top: 32px;
}
.stat-card {
background: var(--m-surface);
padding: 20px;
border-radius: var(--m-radius-lg);
box-shadow: var(--m-shadow);
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: var(--m-primary);
}
.stat-label {
font-size: 14px;
color: var(--m-text-muted);
margin-top: 8px;
}
.action-buttons {
display: flex;
gap: 12px;
margin-top: 32px;
}
.btn {
padding: 12px 24px;
border-radius: var(--m-radius-lg);
border: none;
font-size: 16px;
cursor: pointer;
transition: opacity 0.2s;
}
.btn:hover {
opacity: 0.8;
}
.btn-primary {
background: var(--m-primary);
color: white;
}
.btn-secondary {
background: var(--m-surface);
color: var(--m-text);
box-shadow: var(--m-shadow);
}
.flip-hint {
text-align: center;
color: var(--m-text-muted);
font-size: 14px;
margin-top: 16px;
}
</style>
<div class="m-shell">
<?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
<main class="m-container" style="padding: 32px 20px 48px;">
<h1 style="text-align: center; color: var(--m-text); margin-bottom: 8px;">๐ฏ ์ค๋์ ํ์</h1>
<p style="text-align: center; color: var(--m-text-muted); margin-bottom: 32px;">๋งค์ผ ํ ๊ธ์์ฉ ์ตํ๊ฐ๋ ํ์ ํ์ต</p>
<?php if ($todayHanja): ?>
<div class="hanja-card" id="hanjaCard">
<div class="hanja-card-inner">
<div class="hanja-card-front">
<div class="hanja-display"><?php echo htmlspecialchars($todayHanja['hanja']); ?></div>
<div class="hanja-reading"><?php echo htmlspecialchars($todayHanja['reading']); ?></div>
<div class="hanja-meaning"><?php echo htmlspecialchars($todayHanja['meaning']); ?></div>
<p style="color: var(--m-text-muted); font-size: 14px;">์นด๋๋ฅผ ํด๋ฆญํ์ฌ ๋ค์ง์ด๋ณด์ธ์</p>
</div>
<div class="hanja-card-back">
<h3 style="color: var(--m-text); margin-bottom: 24px;">์์ธ ์ ๋ณด</h3>
<div class="hanja-details">
<div class="hanja-detail-row">
<span class="hanja-detail-label">ํ์</span>
<span class="hanja-detail-value"><?php echo htmlspecialchars($todayHanja['strokes']); ?>ํ</span>
</div>
<div class="hanja-detail-row">
<span class="hanja-detail-label">๋ถ์</span>
<span class="hanja-detail-value"><?php echo htmlspecialchars($todayHanja['radical']); ?></span>
</div>
<div class="hanja-detail-row">
<span class="hanja-detail-label">์๋ฌธ</span>
<span class="hanja-detail-value"><?php echo htmlspecialchars($todayHanja['examples']); ?></span>
</div>
</div>
</div>
</div>
</div>
<p class="flip-hint">๐ก Tip: ์นด๋๋ฅผ ํด๋ฆญํ๋ฉด ์๋ค๋ก ๋ค์ง์ ์ ์์ต๋๋ค</p>
<div class="action-buttons" style="justify-content: center;">
<button class="btn btn-primary" onclick="markAsLearned(<?php echo $todayHanja['id']; ?>)">โ
ํ์ต ์๋ฃ</button>
<a href="/hanja/archive" class="btn btn-secondary">๐ ํ์ต ์์นด์ด๋ธ</a>
</div>
<?php else: ?>
<div style="text-align: center; padding: 60px 20px;">
<p style="font-size: 48px; margin-bottom: 16px;">๐</p>
<h2 style="color: var(--m-text); margin-bottom: 8px;">๋ชจ๋ ํ์๋ฅผ ํ์ตํ์ต๋๋ค!</h2>
<p style="color: var(--m-text-muted);">๋ณต์ต ํด์ฆ๋ก ์ค๋ ฅ์ ํ์ธํด๋ณด์ธ์</p>
<div class="action-buttons" style="justify-content: center; margin-top: 24px;">
<a href="/hanja/quiz" class="btn btn-primary">ํด์ฆ ํ๊ธฐ</a>
<a href="/hanja/archive" class="btn btn-secondary">ํ์ต ๊ธฐ๋ก ๋ณด๊ธฐ</a>
</div>
</div>
<?php endif; ?>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value"><?php echo $stats['total_days']; ?></div>
<div class="stat-label">์ด ํ์ต ์ผ์</div>
</div>
<div class="stat-card">
<div class="stat-value"><?php echo $stats['streak_days']; ?></div>
<div class="stat-label">์ฐ์ ํ์ต ์ผ์</div>
</div>
<div class="stat-card">
<div class="stat-value">
<?php
if ($stats['quiz_attempts'] > 0) {
echo round(($stats['quiz_correct'] / $stats['quiz_attempts']) * 100);
} else {
echo 0;
}
?>%
</div>
<div class="stat-label">ํด์ฆ ์ ๋ต๋ฅ </div>
</div>
</div>
</main>
<?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<script>
// ์นด๋ ํ๋ฆฝ ์ ๋๋ฉ์ด์
document.getElementById('hanjaCard')?.addEventListener('click', function() {
this.classList.toggle('flipped');
});
// ํ์ต ์๋ฃ ์ฒ๋ฆฌ
function markAsLearned(id) {
if (!confirm('์ด ํ์๋ฅผ ํ์ต ์๋ฃ๋ก ํ์ํ์๊ฒ ์ต๋๊น?')) return;
fetch('/hanja/index.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'action=mark_learned&id=' + id
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('ํ์ต ์๋ฃ! ๋ค์ ํ์๋ก ์ด๋ํฉ๋๋ค.');
location.reload();
} else {
alert('์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
}
});
}
</script>
<?php include_once('./tail.sub.php'); ?>
```
### 3) AJAX ์ฒ๋ฆฌ ์ถ๊ฐ
์ ํ์ผ์ ๋งจ ์(`include_once('./_common.php');` ๋ฐ๋ก ์๋)์ ๋ค์ ์ฝ๋๋ฅผ ์ถ๊ฐํ์ธ์:
```php
// AJAX ์์ฒญ ์ฒ๋ฆฌ
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
@mkdir(G5_DATA_PATH.'/modules', 0707, true);
$db = new PDO('sqlite:'.G5_DATA_PATH.'/modules/hanja.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
if ($_POST['action'] === 'mark_learned') {
$id = intval($_POST['id']);
$today = date('Y-m-d');
// ํ์๋ฅผ ํ์ต ์๋ฃ๋ก ํ์
$stmt = $db->prepare("UPDATE hanja_cards SET learned_date = ? WHERE id = ?");
$stmt->execute([$today, $id]);
// ํต๊ณ ์
๋ฐ์ดํธ
$stats = $db->query("SELECT * FROM hanja_stats LIMIT 1")->fetch(PDO::FETCH_ASSOC);
$lastStudyDate = $stats['last_study_date'];
$newStreakDays = $stats['streak_days'];
if ($lastStudyDate === null || $lastStudyDate !== $today) {
$newTotalDays = $stats['total_days'] + 1;
// ์ฐ์ ์ผ์ ๊ณ์ฐ
if ($lastStudyDate === date('Y-m-d', strtotime('-1 day'))) {
$newStreakDays = $stats['streak_days'] + 1;
} else {
$newStreakDays = 1;
}
$stmt = $db->prepare("
UPDATE hanja_stats
SET total_days = ?, streak_days = ?, last_study_date = ?
");
$stmt->execute([$newTotalDays, $newStreakDays, $today]);
}
header('Content-Type: application/json');
echo json_encode(['success' => true]);
exit;
}
}
```
์ด์ ๋ธ๋ผ์ฐ์ ์์ `/hanja`๋ก ์ ์ํ๋ฉด ์ค๋์ ํ์ ํ์ต ํ์ด์ง๊ฐ ํ์๋ฉ๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: - `/hanja` URL๋ก ์ ์ ๊ฐ๋ฅํ ๋ฉ์ธ ํ์ด์ง ์์ฑ - modern shell์ด ์ ์ฉ๋์ด ์ฌ์ดํธ nav/footer๊ฐ ํ์๋จ - SQLite DB๊ฐ ์๋ ์์ฑ๋๊ณ ์ํ ํ์ ๋ฐ์ดํฐ ์ฝ์ ๋จ - ์ค๋์ ํ์๊ฐ ํฐ ์นด๋๋ก ํ์๋๊ณ , ํด๋ฆญ ์ ์๋ค๋ก ํ๋ฆฝ๋๋ ์ ๋๋ฉ์ด์ ๋์ - ํ์ต ์๋ฃ ๋ฒํผ ํด๋ฆญ ์ AJAX๋ก DB ์ ๋ฐ์ดํธ ํ ํ์ด์ง ์๋ก๊ณ ์นจ - ํ์ต ํต๊ณ(์ด ํ์ต ์ผ์, ์ฐ์ ์ผ์, ํด์ฆ ์ ๋ต๋ฅ )๊ฐ ํ๋จ์ ํ์๋จ - ๋คํฌ๋ชจ๋ ์ ํ ์ ์์์ด ์์ฐ์ค๋ฝ๊ฒ ๋ณ๊ฒฝ๋จ
## Step 2: ํ์ต ์์นด์ด๋ธ ํ์ด์ง ๊ตฌํ
์ง๊ธ๊น์ง ํ์ตํ ํ์ ๋ชฉ๋ก์ ๋ ์ง๋ณ๋ก ํ์ธํ ์ ์๋ ์์นด์ด๋ธ ํ์ด์ง๋ฅผ ๋ง๋ญ๋๋ค.
### 1) ์์นด์ด๋ธ ํ์ด์ง ์์ฑ (app/modules/hanja/archive.php)
๋ค์ ๋ด์ฉ์ผ๋ก ํ์ผ์ ์์ฑํ์ธ์:
```php
<?php
include_once('./_common.php');
// ๋ชจ๋ DB ์ฐ๊ฒฐ
@mkdir(G5_DATA_PATH.'/modules', 0707, true);
$db = new PDO('sqlite:'.G5_DATA_PATH.'/modules/hanja.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// ๊ฒ์ ํ๋ผ๋ฏธํฐ
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$filterBookmarked = isset($_GET['bookmarked']) ? true : false;
// ํ์ตํ ํ์ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
$query = "SELECT * FROM hanja_cards WHERE learned_date IS NOT NULL";
$params = [];
if ($search !== '') {
$query .= " AND (hanja LIKE ? OR reading LIKE ? OR meaning LIKE ?)";
$params[] = "%$search%";
$params[] = "%$search%";
$params[] = "%$search%";
}
if ($filterBookmarked) {
$query .= " AND is_bookmarked = 1";
}
$query .= " ORDER BY learned_date DESC";
$stmt = $db->prepare($query);
$stmt->execute($params);
$learnedCards = $stmt->fetchAll(PDO::FETCH_ASSOC);
// ๋ถ๋งํฌ ํ ๊ธ ์ฒ๋ฆฌ
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'toggle_bookmark') {
$id = intval($_POST['id']);
$stmt = $db->prepare("UPDATE hanja_cards SET is_bookmarked = NOT is_bookmarked WHERE id = ?");
$stmt->execute([$id]);
header('Content-Type: application/json');
echo json_encode(['success' => true]);
exit;
}
require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<style>
.search-bar {
display: flex;
gap: 12px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.search-input {
flex: 1;
min-width: 200px;
padding: 12px 16px;
border: 1px solid var(--m-text-muted);
border-radius: var(--m-radius-lg);
background: var(--m-surface);
color: var(--m-text);
font-size: 16px;
}
.search-input::placeholder {
color: var(--m-text-muted);
}
.filter-checkbox {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: var(--m-surface);
border-radius: var(--m-radius-lg);
cursor: pointer;
}
.hanja-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
margin-top: 24px;
}
.hanja-card-mini {
background: var(--m-surface);
border-radius: var(--m-radius-lg);
box-shadow: var(--m-shadow);
padding: 24px;
position: relative;
transition: transform 0.2s, box-shadow 0.2s;
}
.hanja-card-mini:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0,0,0,0.15);
}
.hanja-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.hanja-char {
font-size: 48px;
font-weight: bold;
color: var(--m-primary);
}
.bookmark-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
transition: transform 0.2s;
}
.bookmark-btn:hover {
transform: scale(1.2);
}
.hanja-info {
margin-bottom: 12px;
}
.hanja-reading-mini {
font-size: 18px;
font-weight: bold;
color: var(--m-text);
margin-bottom: 4px;
}
.hanja-meaning-mini {
font-size: 14px;
color: var(--m-text-muted);
margin-bottom: 8px;
}
.hanja-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: var(--m-text-muted);
}
.hanja-date {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--m-text-muted);
opacity: 0.3;
font-size: 12px;
color: var(--m-text-muted);
}
.empty-state {
text-align: center;
padding: 60px 20px;
}
.empty-state-icon {
font-size: 64px;
margin-bottom: 16px;
}
.btn {
padding: 12px 24px;
border-radius: var(--m-radius-lg);
border: none;
font-size: 16px;
cursor: pointer;
transition: opacity 0.2s;
text-decoration: none;
display: inline-block;
}
.btn:hover {
opacity: 0.8;
}
.btn-primary {
background: var(--m-primary);
color: white;
}
</style>
<div class="m-shell">
<?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
<main class="m-container" style="padding: 32px 20px 48px;">
<h1 style="color: var(--m-text); margin-bottom: 8px;">๐ ํ์ต ์์นด์ด๋ธ</h1>
<p style="color: var(--m-text-muted); margin-bottom: 32px;">์ง๊ธ๊น์ง ํ์ตํ ํ์๋ฅผ ํ์ธํ์ธ์</p>
<form method="GET" class="search-bar">
<input
type="text"
name="search"
class="search-input"
placeholder="ํ์, ํ์, ๋ป์ผ๋ก ๊ฒ์..."
value="<?php echo htmlspecialchars($search); ?>"
>
<label class="filter-checkbox">
<input type="checkbox" name="bookmarked" <?php echo $filterBookmarked ? 'checked' : ''; ?> onchange="this.form.submit()">
<span style="color: var(--m-text);">๐ ๋ถ๋งํฌ๋ง ๋ณด๊ธฐ</span>
</label>
<button type="submit" class="btn btn-primary">๊ฒ์</button>
</form>
<?php if (count($learnedCards) > 0): ?>
<div class="hanja-grid">
<?php foreach ($learnedCards as $card): ?>
<div class="hanja-card-mini">
<div class="hanja-header">
<div class="hanja-char"><?php echo htmlspecialchars($card['hanja']); ?></div>
<button
class="bookmark-btn"
onclick="toggleBookmark(<?php echo $card['id']; ?>, this)"
title="๋ถ๋งํฌ"
>
<?php echo $card['is_bookmarked'] ? 'โญ' : 'โ'; ?>
</button>
</div>
<div class="hanja-info">
<div class="hanja-reading-mini"><?php echo htmlspecialchars($card['reading']); ?></div>
<div class="hanja-meaning-mini"><?php echo htmlspecialchars($card['meaning']); ?></div>
<div class="hanja-meta">
<span>ํ์: <?php echo htmlspecialchars($card['strokes']); ?>ํ</span>
<span>๋ถ์: <?php echo htmlspecialchars($card['radical']); ?></span>
</div>
</div>
<div class="hanja-date">
ํ์ต์ผ: <?php echo htmlspecialchars($card['learned_date']); ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-state">
<div class="empty-state-icon">๐</div>
<h2 style="color: var(--m-text); margin-bottom: 8px;">
<?php echo $search !== '' || $filterBookmarked ? '๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค' : '์์ง ํ์ตํ ํ์๊ฐ ์์ต๋๋ค'; ?>
</h2>
<p style="color: var(--m-text-muted); margin-bottom: 24px;">
<?php echo $search !== '' || $filterBookmarked ? '๋ค๋ฅธ ๊ฒ์์ด๋ก ์๋ํด๋ณด์ธ์' : '์ค๋์ ํ์๋ถํฐ ์์ํด๋ณด์ธ์!'; ?>
</p>
<a href="/hanja" class="btn btn-primary">์ค๋์ ํ์ ๋ณด๊ธฐ</a>
</div>
<?php endif; ?>
<div style="margin-top: 32px; text-align: center;">
<a href="/hanja" class="btn btn-primary">โ ๋ฉ์ธ์ผ๋ก</a>
<a href="/hanja/quiz" class="btn btn-primary" style="margin-left: 12px;">ํด์ฆ ํ๊ธฐ โ</a>
</div>
</main>
<?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<script>
function toggleBookmark(id, btn) {
fetch('/hanja/archive', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'action=toggle_bookmark&id=' + id
})
.then(response => response.json())
.then(data => {
if (data.success) {
// ๋ถ๋งํฌ ์์ด์ฝ ํ ๊ธ
btn.textContent = btn.textContent === 'โญ' ? 'โ' : 'โญ';
}
});
}
</script>
<?php include_once('./tail.sub.php'); ?>
```
์ด์ `/hanja/archive`๋ก ์ ์ํ๋ฉด ํ์ตํ ํ์ ๋ชฉ๋ก์ ํ์ธํ ์ ์์ต๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: - `/hanja/archive` URL๋ก ํ์ต ์์นด์ด๋ธ ํ์ด์ง ์ ์ ๊ฐ๋ฅ - ํ์ต ์๋ฃํ ํ์๊ฐ ๊ทธ๋ฆฌ๋ ํํ๋ก ์นด๋๋ก ํ์๋จ - ๊ฒ์์ฐฝ์์ ํ์/ํ์/๋ป์ผ๋ก ๊ฒ์ ๊ฐ๋ฅ - ๋ถ๋งํฌ ํํฐ ์ฒดํฌ๋ฐ์ค๋ก ๋ถ๋งํฌํ ํ์๋ง ํํฐ๋ง ๊ฐ๋ฅ - ๊ฐ ์นด๋์ ๋ถ๋งํฌ ๋ฒํผ(โ/โญ) ํด๋ฆญ ์ AJAX๋ก ํ ๊ธ๋๊ณ ์ฆ์ ์์ด์ฝ ๋ณ๊ฒฝ - ํธ๋ฒ ์ ์นด๋๊ฐ ์๋ก ์ด์ง ์ฌ๋ผ๊ฐ๋ ์ ๋๋ฉ์ด์ - ํ์ตํ ํ์๊ฐ ์์ ๊ฒฝ์ฐ ๋น ์ํ ๋ฉ์์ง ํ์ - ๋คํฌ๋ชจ๋์์๋ ์์ฐ์ค๋ฌ์ด ์์ ์ ํ
## Step 3: ๋ณต์ต ํด์ฆ ํ์ด์ง ๊ตฌํ
ํ์ตํ ํ์ ์ค ๋๋ค์ผ๋ก 5๊ฐ๋ฅผ ์ ํํ์ฌ ํ์ ๋ง์ถ๊ธฐ ํด์ฆ๋ฅผ ์ ๊ณตํ๋ ํ์ด์ง๋ฅผ ๋ง๋ญ๋๋ค.
### 1) ํด์ฆ ํ์ด์ง ์์ฑ (app/modules/hanja/quiz.php)
๋ค์ ๋ด์ฉ์ผ๋ก ํ์ผ์ ์์ฑํ์ธ์:
```php
<?php
include_once('./_common.php');
// ๋ชจ๋ DB ์ฐ๊ฒฐ
@mkdir(G5_DATA_PATH.'/modules', 0707, true);
$db = new PDO('sqlite:'.G5_DATA_PATH.'/modules/hanja.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// ํด์ฆ ์ ์ถ ์ฒ๋ฆฌ
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'submit_quiz') {
$answers = json_decode($_POST['answers'], true);
$correctCount = 0;
foreach ($answers as $answer) {
$stmt = $db->prepare("SELECT reading FROM hanja_cards WHERE id = ?");
$stmt->execute([$answer['id']]);
$correct = $stmt->fetchColumn();
if (strtolower(trim($answer['answer'])) === strtolower(trim($correct))) {
$correctCount++;
}
}
// ํต๊ณ ์
๋ฐ์ดํธ
$stmt = $db->prepare("
UPDATE hanja_stats
SET quiz_attempts = quiz_attempts + ?, quiz_correct = quiz_correct + ?
");
$stmt->execute([count($answers), $correctCount]);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'correct' => $correctCount,
'total' => count($answers)
]);
exit;
}
// ํ์ตํ ํ์ ์ค ๋๋ค 5๊ฐ ์ ํ
$quizCards = $db->query("
SELECT * FROM hanja_cards
WHERE learned_date IS NOT NULL
ORDER BY RANDOM()
LIMIT 5
")->fetchAll(PDO::FETCH_ASSOC);
require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<style>
.quiz-container {
max-width: 600px;
margin: 0 auto;
}
.quiz-progress {
display: flex;
gap: 8px;
margin-bottom: 32px;
justify-content: center;
}
.progress-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--m-text-muted);
opacity: 0.3;
transition: all 0.3s;
}
.progress-dot.active {
background: var(--m-primary);
opacity: 1;
transform: scale(1.3);
}
.progress-dot.correct {
background: #22c55e;
opacity: 1;
}
.progress-dot.wrong {
background: #ef4444;
opacity: 1;
}
.quiz-card {
background: var(--m-surface);
border-radius: var(--m-radius-lg);
box-shadow: var(--m-shadow);
padding: 48px 32px;
text-align: center;
margin-bottom: 24px;
}
.quiz-number {
color: var(--m-text-muted);
font-size: 14px;
margin-bottom: 16px;
}
.quiz-hanja {
font-size: 96px;
font-weight: bold;
color: var(--m-primary);
margin-bottom: 32px;
}
.quiz-input {
width: 100%;
max-width: 400px;
padding: 16px 20px;
font-size: 18px;
border: 2px solid var(--m-text-muted);
border-radius: var(--m-radius-lg);
background: var(--m-surface);
color: var(--m-text);
text-align: center;
transition: border-color 0.2s;
}
.quiz-input:focus {
outline: none;
border-color: var(--m-primary);
}
.quiz-input::placeholder {
color: var(--m-text-muted);
}
.quiz-hint {
margin-top: 16px;
font-size: 14px;
color: var(--m-text-muted);
}
.quiz-buttons {
display: flex;
gap: 12px;
justify-content: center;
margin-top: 24px;
}
.btn {
padding: 12px 32px;
border-radius: var(--m-radius-lg);
border: none;
font-size: 16px;
cursor: pointer;
transition: opacity 0.2s;
}
.btn:hover {
opacity: 0.8;
}
.btn-primary {
background: var(--m-primary);
color: white;
}
.btn-secondary {
background: var(--m-surface);
color: var(--m-text);
box-shadow: var(--m-shadow);
}
.result-card {
background: var(--m-surface);
border-radius: var(--m-radius-lg);
box-shadow: var(--m-shadow);
padding: 48px 32px;
text-align: center;
}
.result-score {
font-size: 72px;
font-weight: bold;
color: var(--m-primary);
margin-bottom: 16px;
}
.result-message {
font-size: 24px;
color: var(--m-text);
margin-bottom: 32px;
}
.result-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 16px;
margin-top: 32px;
}
.result-stat {
padding: 16px;
background: var(--m-surface);
border-radius: var(--m-radius-lg);
box-shadow: var(--m-shadow);
}
.result-stat-value {
font-size: 32px;
font-weight: bold;
color: var(--m-primary);
}
.result-stat-label {
font-size: 14px;
color: var(--m-text-muted);
margin-top: 4px;
}
</style>
<div class="m-shell">
<?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
<main class="m-container" style="padding: 32px 20px 48px;">
<h1 style="text-align: center; color: var(--m-text); margin-bottom: 8px;">โ๏ธ ๋ณต์ต ํด์ฆ</h1>
<p style="text-align: center; color: var(--m-text-muted); margin-bottom: 32px;">ํ์๋ฅผ ๋ณด๊ณ ํ์์ ์
๋ ฅํ์ธ์</p>
<?php if (count($quizCards) < 5): ?>
<div class="result-card">
<div style="font-size: 64px; margin-bottom: 16px;">๐</div>
<h2 style="color: var(--m-text); margin-bottom: 8px;">ํด์ฆ๋ฅผ ์์ํ๋ ค๋ฉด ์ต์ 5๊ฐ์ ํ์๋ฅผ ํ์ตํด์ผ ํฉ๋๋ค</h2>
<p style="color: var(--m-text-muted); margin-bottom: 24px;">ํ์ฌ <?php echo count($quizCards); ?>๊ฐ ํ์ต ์๋ฃ</p>
<a href="/hanja" class="btn btn-primary">ํ์ตํ๋ฌ ๊ฐ๊ธฐ</a>
</div>
<?php else: ?>
<div class="quiz-container">
<div class="quiz-progress" id="quizProgress">
<?php for ($i = 0; $i < 5; $i++): ?>
<div class="progress-dot <?php echo $i === 0 ? 'active' : ''; ?>"></div>
<?php endfor; ?>
</div>
<div id="quizArea">
<div class="quiz-card">
<div class="quiz-number">๋ฌธ์ <span id="currentNumber">1</span> / 5</div>
<div class="quiz-hanja" id="quizHanja"><?php echo htmlspecialchars($quizCards[0]['hanja']); ?></div>
<input
type="text"
class="quiz-input"
id="quizInput"
placeholder="ํ์์ ์
๋ ฅํ์ธ์ (์: ํฐ ๋)"
autocomplete="off"
>
<div class="quiz-hint">๐ก Tip: ๋์ด์ฐ๊ธฐ์ ๋์๋ฌธ์๋ ๊ตฌ๋ถํ์ง ์์ต๋๋ค</div>
</div>
<div class="quiz-buttons">
<button class="btn btn-secondary" onclick="skipQuestion()">๊ฑด๋๋ฐ๊ธฐ</button>
<button class="btn btn-primary" onclick="submitAnswer()">๋ค์</button>
</div>
</div>
<div id="resultArea" style="display: none;">
<div class="result-card">
<div class="result-score" id="resultScore"></div>
<div class="result-message" id="resultMessage"></div>
<div class="result-details">
<div class="result-stat">
<div class="result-stat-value" id="correctCount"></div>
<div class="result-stat-label">์ ๋ต</div>
</div>
<div class="result-stat">
<div class="result-stat-value" id="wrongCount"></div>
<div class="result-stat-label">์ค๋ต</div>
</div>
</div>
<div style="margin-top: 32px;">
<a href="/hanja/quiz" class="btn btn-primary">๋ค์ ํ๊ธฐ</a>
<a href="/hanja" class="btn btn-secondary" style="margin-left: 12px;">๋ฉ์ธ์ผ๋ก</a>
</div>
</div>
</div>
</div>
<?php endif; ?>
</main>
<?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<script>
const quizData = <?php echo json_encode($quizCards); ?>;
let currentIndex = 0;
let answers = [];
const quizInput = document.getElementById('quizInput');
const quizHanja = document.getElementById('quizHanja');
const currentNumber = document.getElementById('currentNumber');
const progressDots = document.querySelectorAll('.progress-dot');
// Enter ํค๋ก ์ ์ถ
quizInput?.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
submitAnswer();
}
});
function submitAnswer() {
const answer = quizInput.value.trim();
if (answer === '') {
alert('๋ต์ ์
๋ ฅํด์ฃผ์ธ์!');
return;
}
// ๋ต์ ์ ์ฅ
answers.push({
id: quizData[currentIndex].id,
answer: answer
});
// ๋ค์ ๋ฌธ์ ๋ก
nextQuestion();
}
function skipQuestion() {
answers.push({
id: quizData[currentIndex].id,
answer: ''
});
nextQuestion();
}
function nextQuestion() {
currentIndex++;
if (currentIndex < quizData.length) {
// ๋ค์ ๋ฌธ์ ํ์
quizHanja.textContent = quizData[currentIndex].hanja;
quizInput.value = '';
quizInput.focus();
currentNumber.textContent = currentIndex + 1;
// ์งํ๋ฅ ์
๋ฐ์ดํธ
progressDots.forEach((dot, index) => {
dot.classList.remove('active');
if (index === currentIndex) {
dot.classList.add('active');
}
});
} else {
// ํด์ฆ ์๋ฃ
finishQuiz();
}
}
function finishQuiz() {
// ์๋ฒ์ ๋ต์ ์ ์ถ
fetch('/hanja/quiz', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'action=submit_quiz&answers=' + encodeURIComponent(JSON.stringify(answers))
})
.then(response => response.json())
.then(data => {
showResult(data.correct, data.total);
});
}
function showResult(correct, total) {
const score = Math.round((correct / total) * 100);
const wrong = total - correct;
document.getElementById('quizArea').style.display = 'none';
document.getElementById('resultArea').style.display = 'block';
document.getElementById('resultScore').textContent = score + '์ ';
document.getElementById('correctCount').textContent = correct;
document.getElementById('wrongCount').textContent = wrong;
let message = '';
if (score === 100) {
message = '๐ ์๋ฒฝํฉ๋๋ค!';
} else if (score >= 80) {
message = '๐ ํ๋ฅญํด์!';
} else if (score >= 60) {
message = '๐ ์ํ์ด์!';
} else {
message = '๐ช ์กฐ๊ธ๋ง ๋ ํ๋ด์ธ์!';
}
document.getElementById('resultMessage').textContent = message;
// ์งํ๋ฅ ์ ์
๋ฐ์ดํธ
progressDots.forEach((dot, index) => {
dot.classList.remove('active');
if (index < correct) {
dot.classList.add('correct');
} else {
dot.classList.add('wrong');
}
});
}
</script>
<?php include_once('./tail.sub.php'); ?>
```
์ด์ `/hanja/quiz`๋ก ์ ์ํ๋ฉด ๋ณต์ต ํด์ฆ๋ฅผ ํ ์ ์์ต๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: - `/hanja/quiz` URL๋ก ํด์ฆ ํ์ด์ง ์ ์ ๊ฐ๋ฅ - ํ์ตํ ํ์๊ฐ 5๊ฐ ๋ฏธ๋ง์ด๋ฉด ์๋ด ๋ฉ์์ง ํ์ - 5๊ฐ ์ด์์ด๋ฉด ๋๋ค์ผ๋ก 5๋ฌธ์ ์ถ์ - ์๋จ์ ์งํ๋ฅ ์ (dot) 5๊ฐ๊ฐ ํ์๋๊ณ ํ์ฌ ๋ฌธ์ ๊ฐ ๊ฐ์กฐ๋จ - ํ์๊ฐ ํฌ๊ฒ ํ์๋๊ณ ์ ๋ ฅ์ฐฝ์ ํ์ ์ ๋ ฅ ๊ฐ๋ฅ - Enter ํค ๋๋ '๋ค์' ๋ฒํผ์ผ๋ก ์ ์ถ - '๊ฑด๋๋ฐ๊ธฐ' ๋ฒํผ์ผ๋ก ๋น ๋ต์ ์ ์ถ ๊ฐ๋ฅ - 5๋ฌธ์ ์๋ฃ ํ ๊ฒฐ๊ณผ ํ๋ฉด์ผ๋ก ์ ํ (์ ์, ์ ๋ต/์ค๋ต ์, ๋ฉ์์ง) - ์งํ๋ฅ ์ ์ด ์ ๋ต์ ์ด๋ก์, ์ค๋ต์ ๋นจ๊ฐ์์ผ๋ก ๋ณ๊ฒฝ - ํต๊ณ DB์ ํด์ฆ ์๋ ํ์์ ์ ๋ต ์๊ฐ ๋์ ๋จ - ๋คํฌ๋ชจ๋์์๋ ์์ฐ์ค๋ฌ์ด UI
## Step 4: ์ฌ์ฉ์ ๊ฒฝํ ๊ฐ์ ๋ฐ ๋ง๋ฌด๋ฆฌ
๋ง์ง๋ง์ผ๋ก ์ ์ฒด์ ์ธ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์ ํ๊ณ ์ธ๋ถ ๊ธฐ๋ฅ์ ๋ณด๊ฐํฉ๋๋ค.
### 1) ๋ฉ์ธ ํ์ด์ง ๊ฐ์ (app/modules/hanja/index.php ์์ )
๊ธฐ์กด ๋ฉ์ธ ํ์ด์ง์ `<main>` ์น์
์๋จ์ ๋ค์ ์๋ด ๋ฐฐ๋๋ฅผ ์ถ๊ฐํ์ธ์ (`<h1>` ํ๊ทธ ๋ฐ๋ก ์):
```php
<?php
// ํ์ต ๊ฐ๋ฅํ ํ์ ์ ํ์ธ
$remainingCount = $db->query("SELECT COUNT(*) FROM hanja_cards WHERE learned_date IS NULL")->fetchColumn();
?>
<?php if ($remainingCount > 0): ?>
<div style="background: linear-gradient(135deg, var(--m-primary) 0%, #6366f1 100%); color: white; padding: 20px 24px; border-radius: var(--m-radius-lg); margin-bottom: 24px; box-shadow: var(--m-shadow);">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px;">
<div>
<div style="font-size: 14px; opacity: 0.9; margin-bottom: 4px;">ํ์ต ์งํ๋ฅ </div>
<div style="font-size: 24px; font-weight: bold;">
<?php
$totalCount = $db->query("SELECT COUNT(*) FROM hanja_cards")->fetchColumn();
$learnedCount = $totalCount - $remainingCount;
echo "$learnedCount / $totalCount";
?>
</div>
</div>
<div style="font-size: 14px; opacity: 0.9;">
<?php echo round(($learnedCount / $totalCount) * 100); ?>% ์๋ฃ
</div>
</div>
<div style="background: rgba(255,255,255,0.2); height: 8px; border-radius: 4px; margin-top: 12px; overflow: hidden;">
<div style="background: white; height: 100%; width: <?php echo round(($learnedCount / $totalCount) * 100); ?>%; transition: width 0.3s;"></div>
</div>
</div>
<?php endif; ?>
```
### 2) ์์นด์ด๋ธ ํ์ด์ง์ ํต๊ณ ์์ฝ ์ถ๊ฐ (app/modules/hanja/archive.php ์์ )
๊ฒ์ ๋ฐ ์์ ๋ค์ ํต๊ณ ์น์
์ ์ถ๊ฐํ์ธ์:
```php
<?php
// ํต๊ณ ๊ณ์ฐ
$totalLearned = count($learnedCards);
$bookmarkedCount = $db->query("SELECT COUNT(*) FROM hanja_cards WHERE is_bookmarked = 1")->fetchColumn();
$thisWeekCount = $db->query("
SELECT COUNT(*) FROM hanja_cards
WHERE learned_date >= date('now', '-7 days')
")->fetchColumn();
?>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 32px;">
<div style="background: var(--m-surface); padding: 20px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); text-align: center;">
<div style="font-size: 36px; font-weight: bold; color: var(--m-primary);"><?php echo $totalLearned; ?></div>
<div style="font-size: 14px; color: var(--m-text-muted); margin-top: 4px;">์ด ํ์ต ํ์</div>
</div>
<div style="background: var(--m-surface); padding: 20px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); text-align: center;">
<div style="font-size: 36px; font-weight: bold; color: #f59e0b;"><?php echo $bookmarkedCount; ?></div>
<div style="font-size: 14px; color: var(--m-text-muted); margin-top: 4px;">๋ถ๋งํฌ</div>
</div>
<div style="background: var(--m-surface); padding: 20px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); text-align: center;">
<div style="font-size: 36px; font-weight: bold; color: #22c55e;"><?php echo $thisWeekCount; ?></div>
<div style="font-size: 14px; color: var(--m-text-muted); margin-top: 4px;">์ด๋ฒ ์ฃผ ํ์ต</div>
</div>
</div>
```
### 3) ํด์ฆ ํ์ด์ง์ ํํธ ๊ธฐ๋ฅ ์ถ๊ฐ (app/modules/hanja/quiz.php ์์ )
ํด์ฆ ์นด๋ ์์ ์
๋ ฅ์ฐฝ ์๋์ ํํธ ๋ฒํผ์ ์ถ๊ฐํ์ธ์:
```html
<!-- ๊ธฐ์กด quiz-hint div ์๋์ ์ถ๊ฐ -->
<button class="btn btn-secondary" onclick="showHint()" id="hintBtn" style="margin-top: 16px;">
๐ก ํํธ ๋ณด๊ธฐ
</button>
<div id="hintText" style="display: none; margin-top: 12px; padding: 12px; background: rgba(99, 102, 241, 0.1); border-radius: 8px; color: var(--m-text);">
ํ์: <span id="hintStrokes"></span>ํ |
๋ถ์: <span id="hintRadical"></span>
</div>
```
๊ทธ๋ฆฌ๊ณ `<script>` ์น์
์ ๋ค์ ํจ์๋ฅผ ์ถ๊ฐํ์ธ์:
```javascript
function showHint() {
const current = quizData[currentIndex];
document.getElementById('hintStrokes').textContent = current.strokes;
document.getElementById('hintRadical').textContent = current.radical;
document.getElementById('hintText').style.display = 'block';
document.getElementById('hintBtn').style.display = 'none';
}
// nextQuestion ํจ์ ์์ ํํธ ์ด๊ธฐํ ์ฝ๋ ์ถ๊ฐ
function nextQuestion() {
currentIndex++;
if (currentIndex < quizData.length) {
// ... ๊ธฐ์กด ์ฝ๋ ...
// ํํธ ์ด๊ธฐํ
document.getElementById('hintText').style.display = 'none';
document.getElementById('hintBtn').style.display = 'inline-block';
} else {
finishQuiz();
}
}
```
### 4) ๋ฐ์ํ ๋์์ธ ๊ฐ์
๋ชจ๋ ํ์ด์ง์ `<style>` ์น์
๋์ ๋ค์ ๋ฏธ๋์ด ์ฟผ๋ฆฌ๋ฅผ ์ถ๊ฐํ์ธ์:
```css
@media (max-width: 640px) {
.hanja-display {
font-size: 80px;
}
.quiz-hanja {
font-size: 72px;
}
.hanja-grid {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: 1fr;
}
.action-buttons {
flex-direction: column;
}
.btn {
width: 100%;
}
}
```
### 5) ์ต์ข
ํ
์คํธ
๋ค์ ์๋๋ฆฌ์ค๋ฅผ ํ
์คํธํ์ธ์:
1. **๋ฉ์ธ ํ์ด์ง**
- `/hanja` ์ ์
- ํ์ต ์งํ๋ฅ ๋ฐฐ๋ ํ์ธ
- ํ์ ์นด๋ ํด๋ฆญํ์ฌ ํ๋ฆฝ ์ ๋๋ฉ์ด์
ํ์ธ
- ํ์ต ์๋ฃ ๋ฒํผ ํด๋ฆญ ํ ํต๊ณ ์
๋ฐ์ดํธ ํ์ธ
2. **์์นด์ด๋ธ ํ์ด์ง**
- `/hanja/archive` ์ ์
- ํต๊ณ ์์ฝ ํ์ธ
- ๊ฒ์ ๊ธฐ๋ฅ ํ
์คํธ
- ๋ถ๋งํฌ ํ ๊ธ ํ
์คํธ
- ๋ถ๋งํฌ ํํฐ ํ
์คํธ
3. **ํด์ฆ ํ์ด์ง**
- `/hanja/quiz` ์ ์
- 5๋ฌธ์ ํ์ด (์ ๋ต/์ค๋ต ์์ด์)
- ํํธ ๊ธฐ๋ฅ ์ฌ์ฉ
- ๊ฑด๋๋ฐ๊ธฐ ๊ธฐ๋ฅ ์ฌ์ฉ
- ๊ฒฐ๊ณผ ํ๋ฉด ํ์ธ
- ํต๊ณ ์
๋ฐ์ดํธ ํ์ธ
4. **๋คํฌ๋ชจ๋**
- nav์ ๋คํฌ๋ชจ๋ ํ ๊ธ ๋ฒํผ ํด๋ฆญ
- ๋ชจ๋ ํ์ด์ง์์ ์์ ์ ํ ํ์ธ
5. **๋ฐ์ํ**
- ๋ธ๋ผ์ฐ์ ์ฐฝ ํฌ๊ธฐ ์กฐ์
- ๋ชจ๋ฐ์ผ ํ๋ฉด์์ ๋ ์ด์์ ํ์ธ
๋ชจ๋ ๊ธฐ๋ฅ์ด ์ ์ ๋์ํ๋ฉด ์์ฑ์
๋๋ค! ๐
๊ธฐ๋ ๊ฒฐ๊ณผ: - ๋ฉ์ธ ํ์ด์ง์ ํ์ต ์งํ๋ฅ ๋ฐฐ๋๊ฐ ํ์๋๊ณ ํ๋ก๊ทธ๋ ์ค ๋ฐ๊ฐ ์ ๋๋ฉ์ด์ ์ผ๋ก ์ฑ์์ง - ์์นด์ด๋ธ ํ์ด์ง์ ์ด ํ์ต ํ์ ์, ๋ถ๋งํฌ ์, ์ด๋ฒ ์ฃผ ํ์ต ์ ํต๊ณ ์นด๋๊ฐ ํ์๋จ - ํด์ฆ ํ์ด์ง์ ํํธ ๋ฒํผ์ด ์ถ๊ฐ๋๊ณ ํด๋ฆญ ์ ํ์์ ๋ถ์ ์ ๋ณด ํ์ - ๋ชจ๋ ํ์ด์ง๊ฐ ๋ชจ๋ฐ์ผ ํ๋ฉด์์ 1์ด ๊ทธ๋ฆฌ๋๋ก ์ ํ๋๊ณ ๋ฒํผ์ด ์ ์ฒด ๋๋น๋ก ํ์ฅ๋จ - ๋คํฌ๋ชจ๋ ์ ํ ์ ๋ชจ๋ ์์์ ์์์ด ์์ฐ์ค๋ฝ๊ฒ ๋ณ๊ฒฝ๋จ - ๋ชจ๋ ์ธํฐ๋์ (์นด๋ ํ๋ฆฝ, ๋ถ๋งํฌ ํ ๊ธ, ํด์ฆ ์ ์ถ)์ด ๋ถ๋๋ฝ๊ฒ ๋์ - SQLite DB์ ๋ฐ์ดํฐ๊ฐ ์ ์์ ์ผ๋ก ์ ์ฅ๋๊ณ ์กฐํ๋จ - ์ ์ฒด์ ์ผ๋ก ์์ฑ๋ ๋์ ํ์ ํ์ต ํ๋ซํผ์ด ๊ตฌํ๋จ
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!