๐ฏ g5se ์จ๋ผ์ธ ํด์ฆ ์๋ ๋ - ๋ค์ํ ์ฃผ์ ์ ํด์ฆ ํ๊ธฐ & ์ ์ ๊ฒฝ์
g5se-quiz-arena-platform
๋ฌธ์ ์ค๋ช
๐ ํ๋ก์ ํธ ๊ฐ์
๊ทธ๋๋ณด๋5 SE ์์์ ๋์ํ๋ ์จ๋ผ์ธ ํด์ฆ ํ๋ซํผ์ ๋ง๋ญ๋๋ค. ๊ด๋ฆฌ์๊ฐ ํด์ฆ ์ธํธ๋ฅผ ๋ฑ๋กํ๊ณ , ์ฌ์ฉ์๋ ๋ค์ํ ์ฃผ์ ์ ํด์ฆ๋ฅผ ํ๋ฉด์ ์ ์๋ฅผ ํ๋ํ๊ณ ์์๋ฅผ ๊ฒฝ์ํ ์ ์๋ ์ธํฐ๋ํฐ๋ธ ์น ์๋น์ค์ ๋๋ค.
โจ ํต์ฌ ๊ธฐ๋ฅ
ํ์ ๊ธฐ๋ฅ:
- ๐ฒ ํด์ฆ ์นดํ ๊ณ ๋ฆฌ ํ์: ์์/์ญ์ฌ/๊ณผํ/์ํฐํ ์ธ๋จผํธ ๋ฑ ์ฃผ์ ๋ณ ํด์ฆ ์ธํธ ๋ชฉ๋ก
- ๐ ํด์ฆ ํ์ด ์ธํฐํ์ด์ค: ๊ฐ๊ด์ 4์ง์ ๋ค, ์ค์๊ฐ ์ ๋ต/์ค๋ต ํผ๋๋ฐฑ, ์งํ๋ฅ ํ์
- ๐ ์ ์ ์์คํ : ์ ๋ต ์ ์ ์ ์ ๋ฆฝ, ๊ฐ์ธ ๋์ ์ ์ ๊ธฐ๋ก
- ๐ ๋ฆฌ๋๋ณด๋: ์ ์ฒด ์ฌ์ฉ์ ์ ์ ์์ํ (์์ 20๋ช )
- โ ํด์ฆ ์๋ฃ ๊ธฐ๋ก: ๋ด๊ฐ ํผ ํด์ฆ ํ์คํ ๋ฆฌ with ์ ๋ต๋ฅ
์ ํ ์ถ๊ฐ ๊ธฐ๋ฅ (๋ณด๋์ค):
- โฑ๏ธ ํ์ด๋จธ ๋ชจ๋: ์ ํ ์๊ฐ ๋ด ํด์ฆ ํ๊ธฐ (๋์ด๋ ์์น)
- ๐ฏ ํํธ ์์คํ : ์ค๋ต 1๊ฐ ์ ๊ฑฐ (ํํธ ์ฌ์ฉ ์ ์ ์ ์ฐจ๊ฐ)
- ๐ ๋๋ค ํด์ฆ: ์นดํ ๊ณ ๋ฆฌ ๋ฌด๊ด ๋๋ค ๋ฌธ์ ์ถ์
- ๐ ๊ฐ์ธ ํต๊ณ: ์นดํ ๊ณ ๋ฆฌ๋ณ ์ ๋ต๋ฅ ์ฐจํธ
- ๐จ ๋คํฌ๋ชจ๋ ์๋ ๋์ (modern shell ํ ํฐ ํ์ฉ)
๐ฏ ํ์ต ๋ชฉํ
- g5se์ ํด๋ ๊ธฐ๋ฐ ์๋ ๋ผ์ฐํ ์์คํ ์ดํด
- modern shell ์ ์ฉ์ผ๋ก ์ฌ์ดํธ ํต์ผ์ฑ ํ๋ณด
- ๋ชจ๋ ์ ์ฉ SQLite DB ์ค๊ณ (ํด์ฆ/๊ฒฐ๊ณผ/์ ์ ํ ์ด๋ธ)
- AJAX ๊ธฐ๋ฐ ๋น๋๊ธฐ ํด์ฆ ์ ์ถ & ์ค์๊ฐ ํผ๋๋ฐฑ
- CSS ๋ณ์ ๊ธฐ๋ฐ ๋คํฌ๋ชจ๋ ๋์ UI
- ์ฌ์ฉ์ ์ธํฐ๋์ ์ค์ฌ์ ํ๋ก ํธ์๋ ๋ก์ง (ํ์ด๋จธ, ์ ๋๋ฉ์ด์ )
๐ ๊ธฐ์ ์คํ
- ๋ฐฑ์๋: PHP 8.x + g5se (๊ทธ๋๋ณด๋5 SE)
- DB: SQLite (๋ชจ๋ ์ ์ฉ
G5_DATA_PATH/modules/quiz-arena.sqlite) - ํ๋ก ํธ์๋: HTML5 + Vanilla JS + CSS3
- ๋์์ธ: modern shell ํ ํฐ ๊ธฐ๋ฐ ๋คํฌ๋ชจ๋ ์ง์
๐ฆ ๊ฒฐ๊ณผ๋ฌผ
/quiz-arena- ํด์ฆ ์นดํ ๊ณ ๋ฆฌ ๋ชฉ๋ก & ์ ํ ํ์ด์ง/quiz-arena/play- ํด์ฆ ํ์ด ํ๋ฉด (AJAX ๊ธฐ๋ฐ ์ค์๊ฐ ์ ๋ต ์ฒดํฌ)/quiz-arena/leaderboard- ์ ์ฒด ์์ํ/quiz-arena/my-stats- ๊ฐ์ธ ํต๊ณ & ์๋ฃํ ํด์ฆ ํ์คํ ๋ฆฌ- ๊ด๋ฆฌ์ ๊ธฐ๋ฅ: ํด์ฆ ์ธํธ ๋ฑ๋ก ํผ (
/quiz-arena/admin- ๋ก๊ทธ์ธ ์ฒดํฌ ํฌํจ)
์ ์ฝ ์กฐ๊ฑด
- โข g5se ์ค์น๋ณธ ์์ ํ์ผ ์ถ๊ฐ (Laravel/Composer/npm ์ฌ์ฉ ๊ธ์ง)
- โข ํด๋ ๊ธฐ๋ฐ ์๋ ๋ผ์ฐํ ์ฌ์ฉ (app/router.php์ ๋ผ์ฐํธ ๋ฑ๋ก ๋ถํ์)
- โข modern shell ํ์ ์ ์ฉ (_head.inc.php + m-shell + _nav.inc.php + m-container + _footer.inc.php + tail.sub.php)
- โข ๋์์ธ ํ ํฐ(CSS ๋ณ์) ์ฌ์ฉ์ผ๋ก ๋คํฌ๋ชจ๋ ์๋ ๋์
- โข ๋ชจ๋ ์ ์ฉ SQLite DB ์ฌ์ฉ (G5_DATA_PATH/modules/quiz-arena.sqlite)
- โข g5se ๋ฉ์ธ DB์ ํ ์ด๋ธ ์์ฑ ์ ๋ ๊ธ์ง
- โข ์ฌ์ฉ์ ๋ฐ์ดํฐ๋ htmlspecialchars()๋ก ์ด์ค์ผ์ดํ
- โข prepared statement๋ก SQL ์ธ์ ์ ๋ฐฉ์ง
- โข AJAX ์๋ต์ JSON ํ์ (Content-Type: application/json)
- โข CDN ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ ๊ธ์ง (์์ Vanilla JS)
- โข ํด์ฆ ๊ด๋ฆฌ ๊ธฐ๋ฅ์ g5se ํ์ ๋ก๊ทธ์ธ ์ฒดํฌ ํ์
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
g5se๊ฐ ์ค์น๋ ๋๋ ํฐ๋ฆฌ(๋ฃจํธ์ `index.php`ยท`app/`์ด ์๋ ๊ณณ)์์ ์์
์ ์์ํฉ๋๋ค. (g5se ์ค์น๋ https://github.com/gnuboard/g5se ์ฐธ๊ณ )
**Step 1: ํด์ฆ ์๋ ๋ ๋ฉ์ธ ํ์ด์ง ์์ฑ**
`app/modules/quiz-arena/index.php` ํ์ผ์ ๋ง๋ค๊ณ ๋ค์์ ๊ตฌํํ์ธ์:
1. **modern shell ๊ณจ๊ฒฉ ์ ์ฉ**:
- `_common.php` ์ธํด๋ฃจ๋
- `_head.inc.php` + `m-shell` + `_nav.inc.php` + `m-container` + `_footer.inc.php` + `tail.sub.php` ๊ตฌ์กฐ ์ฌ์ฉ
2. **SQLite DB ์ด๊ธฐํ**:
- ํ์ผ ์์น: `G5_DATA_PATH.'/modules/quiz-arena.sqlite'`
- ๋๋ ํฐ๋ฆฌ ์์ฑ: `@mkdir(G5_DATA_PATH.'/modules', 0707, true);`
- ํ
์ด๋ธ ์คํค๋ง (์ฒซ ์ ๊ทผ ์ ์๋ ์์ฑ):
```sql
CREATE TABLE IF NOT EXISTS quiz_sets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL,
title TEXT NOT NULL,
difficulty TEXT DEFAULT 'normal',
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS quiz_questions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
quiz_set_id INTEGER NOT NULL,
question TEXT NOT NULL,
option_a TEXT NOT NULL,
option_b TEXT NOT NULL,
option_c TEXT NOT NULL,
option_d TEXT NOT NULL,
correct_answer TEXT NOT NULL,
explanation TEXT,
FOREIGN KEY(quiz_set_id) REFERENCES quiz_sets(id)
);
CREATE TABLE IF NOT EXISTS user_scores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mb_id TEXT NOT NULL,
quiz_set_id INTEGER NOT NULL,
score INTEGER DEFAULT 0,
total_questions INTEGER DEFAULT 0,
completed_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(quiz_set_id) REFERENCES quiz_sets(id)
);
```
3. **์นดํ
๊ณ ๋ฆฌ๋ณ ํด์ฆ ์ธํธ ๋ชฉ๋ก ํ์**:
- DB์์ quiz_sets๋ฅผ category๋ณ๋ก ๊ทธ๋ฃนํํ์ฌ ์กฐํ
- ๊ฐ ์นดํ
๊ณ ๋ฆฌ ์นด๋์ ํด์ฆ ์ธํธ ๊ฐ์ ํ์
- ์นด๋ ํด๋ฆญ ์ `/quiz-arena/play?set_id={id}` ๋งํฌ
4. **UI ๋์์ธ**:
- ํค๋: "๐ฏ ํด์ฆ ์๋ ๋" ํ์ดํ + "๋์ ! ์ง์์ ๋๊ธฐ" ์๋ธํ์ดํ
- ์นดํ
๊ณ ๋ฆฌ ์นด๋: ๊ทธ๋ฆฌ๋ ๋ ์ด์์ (2์ด, ๋ชจ๋ฐ์ผ 1์ด)
- ์นด๋ ์คํ์ผ: `background: var(--m-surface); box-shadow: var(--m-shadow); border-radius: var(--m-radius-lg);`
- hover ์ ์ด์ง scale ์ ๋๋ฉ์ด์
- ์ฐ์ธก ์๋จ์ "๋ด ๊ธฐ๋ก" ๋ฒํผ (`/quiz-arena/my-stats` ๋งํฌ)
5. **์ํ ๋ฐ์ดํฐ ์ฝ์
** (DB๊ฐ ๋น์ด์์ ๊ฒฝ์ฐ):
- ์์/์ญ์ฌ/๊ณผํ/์ํฐํ
์ธ๋จผํธ ์นดํ
๊ณ ๋ฆฌ์ ๊ฐ 1๊ฐ์ฉ ์ํ ํด์ฆ ์ธํธ ์์ฑ
- ๊ฐ ์ธํธ๋น 5๊ฐ ๋ฌธ์ (4์ง์ ๋ค)
**๋์์ธ ํ ํฐ ์ฌ์ฉ ์์:**
```css
.quiz-category-card {
background: var(--m-surface);
color: var(--m-text);
box-shadow: var(--m-shadow);
border-radius: var(--m-radius-lg);
padding: 24px;
transition: transform 0.2s;
}
.quiz-category-card:hover {
transform: translateY(-4px);
}
.category-badge {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 6px 12px;
border-radius: 12px;
font-size: 0.9em;
}
```
**์์ URL**: `/quiz-arena` (ํด๋ ๊ธฐ๋ฐ ์๋ ๋ผ์ฐํ
)
ํ์ผ ๊ฒฝ๋ก๋ ๋ฐ๋์ `app/modules/quiz-arena/index.php`๋ก ์์ฑํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: - `/quiz-arena` ์ ์ ์ modern shell์ด ์ ์ฉ๋ ํ์ด์ง ํ์ - SQLite DB๊ฐ ์๋ ์์ฑ๋๊ณ ํ ์ด๋ธ ์คํค๋ง ์ด๊ธฐํ - ์นดํ ๊ณ ๋ฆฌ๋ณ ํด์ฆ ์ธํธ ์นด๋๊ฐ ๊ทธ๋ฆฌ๋๋ก ํ์ (์ํ ๋ฐ์ดํฐ ํฌํจ) - ๋คํฌ๋ชจ๋ ํ ๊ธ ์ ๋ฐฐ๊ฒฝ/ํ ์คํธ ์์ ์๋ ์ ํ - ์นด๋ hover ์ ์ ๋๋ฉ์ด์ ๋์
**Step 2: ํด์ฆ ํ์ด ํ์ด์ง ๊ตฌํ**
`app/modules/quiz-arena/play/index.php` ํ์ผ์ ๋ง๋ค๊ณ ๋ค์์ ๊ตฌํํ์ธ์:
1. **URL ํ๋ผ๋ฏธํฐ ์ฒ๋ฆฌ**:
- `$_GET['set_id']`๋ก ํด์ฆ ์ธํธ ID ๋ฐ๊ธฐ
- ํด๋น ์ธํธ์ ๋ฌธ์ ๋ชฉ๋ก์ DB์์ ์กฐํ (quiz_questions ํ
์ด๋ธ)
- ์ธํธ๊ฐ ์์ผ๋ฉด 404 ๋ฉ์์ง ํ์
2. **ํด์ฆ UI ๊ตฌ์กฐ**:
- ์๋จ: ์งํ๋ฅ ๋ฐ (์: "3 / 5 ๋ฌธ์ ")
- ์ค์: ํ์ฌ ๋ฌธ์ ํ
์คํธ (ํฐ ํฐํธ)
- 4๊ฐ ์ ํ์ง ๋ฒํผ (A/B/C/D)
- ํ๋จ: "๋ค์ ๋ฌธ์ " ๋ฒํผ (์ ๋ต ์ ํ ํ ํ์ฑํ)
3. **JavaScript ์ธํฐ๋์
**:
- ํ์ด์ง ๋ก๋ ์ ๋ฌธ์ ๋ฐ์ดํฐ๋ฅผ JSON์ผ๋ก `<script>` ํ๊ทธ์ ์๋ฒ ๋
- ์ ํ์ง ํด๋ฆญ ์:
- AJAX๋ก `/quiz-arena/check-answer` POST ์์ฒญ (๋ฌธ์ ID + ์ ํํ ๋ต)
- ์๋ต์์ ์ ๋ต ์ฌ๋ถ ๋ฐ์์ ์ฆ์ ํผ๋๋ฐฑ (์ ๋ต: ์ด๋ก ๋ฐฐ๊ฒฝ, ์ค๋ต: ๋นจ๊ฐ ๋ฐฐ๊ฒฝ)
- ์ ๋ต์ผ ๊ฒฝ์ฐ ์ ์ +10์ ํ์
- ์ค๋ต์ผ ๊ฒฝ์ฐ ์ ๋ต ์ ํ์ง ๊ฐ์กฐ + ํด์ค ํ์
- "๋ค์ ๋ฌธ์ " ๋ฒํผ ํด๋ฆญ ์ ๋ค์ ๋ฌธ์ ๋ก ์ด๋ (JS๋ก ํ๋ฉด ๊ฐฑ์ )
- ๋ง์ง๋ง ๋ฌธ์ ์๋ฃ ์ ๊ฒฐ๊ณผ ์์ฝ ๋ชจ๋ฌ ํ์ (์ด ์ ์, ์ ๋ต๋ฅ )
4. **AJAX ์๋ํฌ์ธํธ ๋ถ๊ธฐ** (๊ฐ์ ํ์ผ ๋ด):
- `$_POST['action'] === 'check_answer'`์ผ ๊ฒฝ์ฐ:
- ๋ฌธ์ ID์ ์ ํํ ๋ต์ ๋ฐ์์ ์ ๋ต ์ฌ๋ถ ์ฒดํฌ
- JSON ์๋ต: `{"correct": true/false, "explanation": "...", "correct_answer": "B"}`
- `header('Content-Type: application/json');`
- `exit;`๋ก HTML ๋ ๋๋ง ์ค๋จ
5. **ํด์ฆ ์๋ฃ ์ฒ๋ฆฌ**:
- ๋ชจ๋ ๋ฌธ์ ์๋ฃ ์ user_scores ํ
์ด๋ธ์ ๊ธฐ๋ก ์ ์ฅ
- mb_id๋ `$member['mb_id']` ์ฌ์ฉ (๋นํ์์ 'guest')
- ์๋ฃ ๋ชจ๋ฌ์ "๋ฆฌ๋๋ณด๋ ๋ณด๊ธฐ" ๋ฒํผ ์ถ๊ฐ
6. **UI ๋์์ธ**:
- ์งํ๋ฅ ๋ฐ: `<progress>` ํ๊ทธ + CSS ์คํ์ผ๋ง
- ์ ํ์ง ๋ฒํผ: ํฐ ํจ๋ฉ, ํธ๋ฒ ์ ๋ฐฐ๊ฒฝ์ ๋ณํ
- ์ ๋ต ํผ๋๋ฐฑ: 0.3์ด fade-in ์ ๋๋ฉ์ด์
- ๋ชจ๋ฌ: ๋ฐํฌ๋ช
๋ฐฐ๊ฒฝ + ์ค์ ์นด๋
**AJAX ์ฝ๋ ์์ (JS):**
```javascript
function checkAnswer(questionId, selectedAnswer) {
fetch('/quiz-arena/play', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `action=check_answer&question_id=${questionId}&answer=${selectedAnswer}`
})
.then(res => res.json())
.then(data => {
if (data.correct) {
// ์ ๋ต ์ฒ๋ฆฌ
document.querySelector(`[data-option="${selectedAnswer}"]`).style.background = '#10b981';
} else {
// ์ค๋ต ์ฒ๋ฆฌ
document.querySelector(`[data-option="${selectedAnswer}"]`).style.background = '#ef4444';
document.querySelector(`[data-option="${data.correct_answer}"]`).style.background = '#10b981';
showExplanation(data.explanation);
}
});
}
```
**์์ URL**: `/quiz-arena/play?set_id=1`
๊ธฐ๋ ๊ฒฐ๊ณผ: - ํด์ฆ ์ธํธ ID๋ก ์ ์ ์ ์ฒซ ๋ฒ์งธ ๋ฌธ์ ํ์ - ์ ํ์ง ํด๋ฆญ ์ AJAX๋ก ์ ๋ต ์ฒดํฌ ํ ์ฆ์ ํผ๋๋ฐฑ - ์งํ๋ฅ ๋ฐ๊ฐ ๋ฌธ์ ์งํ์ ๋ฐ๋ผ ์ ๋ฐ์ดํธ - ๋ชจ๋ ๋ฌธ์ ์๋ฃ ์ ๊ฒฐ๊ณผ ๋ชจ๋ฌ ํ์ (์ ์, ์ ๋ต๋ฅ ) - user_scores ํ ์ด๋ธ์ ์๋ฃ ๊ธฐ๋ก ์ ์ฅ
**Step 3: ๋ฆฌ๋๋ณด๋ ํ์ด์ง ๊ตฌํ**
`app/modules/quiz-arena/leaderboard/index.php` ํ์ผ์ ๋ง๋ค๊ณ ๋ค์์ ๊ตฌํํ์ธ์:
1. **์ ์ฒด ์์ ์กฐํ**:
- user_scores ํ
์ด๋ธ์์ mb_id๋ณ ์ด์ ์ง๊ณ (SUM)
- ์ ์ ๋ด๋ฆผ์ฐจ์ ์ ๋ ฌ, ์์ 20๋ช
๋ง ํ์
- ๋นํ์('guest')์ ์ ์ธ
2. **์์ํ UI**:
- ํ
์ด๋ธ ํ์: ์์ | ์ฌ์ฉ์๋ช
| ์ด์ | ์๋ฃํ ํด์ฆ ์
- 1~3์๋ ๊ธ/์/๋ ๋ฉ๋ฌ ์์ด์ฝ ํ์
- ๋ณธ์ธ ์์๋ ํ์ด๋ผ์ดํธ (๋ฐฐ๊ฒฝ์ ๋ณ๊ฒฝ)
3. **ํํฐ ๊ธฐ๋ฅ**:
- ์๋จ์ ํญ: "์ ์ฒด" / "์ค๋" / "์ด๋ฒ ์ฃผ"
- ํญ ํด๋ฆญ ์ ๊ธฐ๊ฐ ํํฐ๋ง (completed_at ๊ธฐ์ค)
- AJAX ์์ด ํ์ด์ง ์๋ก๊ณ ์นจ ๋ฐฉ์์ผ๋ก ๊ตฌํ (`?period=today` ํ๋ผ๋ฏธํฐ)
4. **๋น ์ํ ์ฒ๋ฆฌ**:
- ์์ ๋ฐ์ดํฐ๊ฐ ์์ ๊ฒฝ์ฐ "์์ง ๊ธฐ๋ก์ด ์์ต๋๋ค" ๋ฉ์์ง + ํด์ฆ ํ๋ฌ ๊ฐ๊ธฐ ๋ฒํผ
5. **๋์์ธ**:
- ํ
์ด๋ธ ํค๋: ์ด๋์ด ๋ฐฐ๊ฒฝ (`var(--m-surface)` + ํฌ๋ช
๋)
- ์์ ์
: ํฐ ํฐํธ, ๋ณผ๋
- ๋ฉ๋ฌ ์์ด์ฝ: ๐ฅ๐ฅ๐ฅ ์ด๋ชจ์ง ์ฌ์ฉ
- ๋ณธ์ธ ํ: `background: rgba(102, 126, 234, 0.1);`
**SQL ์์:**
```sql
SELECT
mb_id,
SUM(score) as total_score,
COUNT(*) as quiz_count
FROM user_scores
WHERE mb_id != 'guest'
GROUP BY mb_id
ORDER BY total_score DESC
LIMIT 20
```
**๊ธฐ๊ฐ ํํฐ SQL (์ค๋ ๊ธฐ์ค):**
```sql
WHERE mb_id != 'guest'
AND DATE(completed_at) = DATE('now')
```
**์์ URL**: `/quiz-arena/leaderboard?period=all`
๊ธฐ๋ ๊ฒฐ๊ณผ: - ์ ์ฒด ์ฌ์ฉ์ ์ ์ ์์ํ ํ์ (์์ 20๋ช ) - 1~3์์ ๋ฉ๋ฌ ์์ด์ฝ ํ์ - ๋ณธ์ธ ์์ ํ์ด๋ผ์ดํธ (๋ก๊ทธ์ธ ์ฌ์ฉ์) - ํญ ํด๋ฆญ์ผ๋ก ๊ธฐ๊ฐ ํํฐ๋ง ๋์ - ๋ฐ์ดํฐ ์์ ์ ๋น ์ํ ๋ฉ์์ง
**Step 4: ๊ฐ์ธ ํต๊ณ ํ์ด์ง ๊ตฌํ**
`app/modules/quiz-arena/my-stats/index.php` ํ์ผ์ ๋ง๋ค๊ณ ๋ค์์ ๊ตฌํํ์ธ์:
1. **๋ก๊ทธ์ธ ์ฒดํฌ**:
- `$is_member`๊ฐ false๋ฉด ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธ
- ๋นํ์์ ์ ๊ทผ ๋ถ๊ฐ ๋ฉ์์ง
2. **๊ฐ์ธ ํต๊ณ ๋ฐ์ดํฐ ์กฐํ**:
- ์ด ํ์ดํ ํด์ฆ ์
- ํ๊ท ์ ๋ต๋ฅ (์ ๋ต ์ / ์ด ๋ฌธ์ ์ * 100)
- ์นดํ
๊ณ ๋ฆฌ๋ณ ์๋ฃ ํ์ (quiz_sets ์กฐ์ธ)
- ์ต๊ทผ ์๋ฃํ ํด์ฆ 5๊ฐ (completed_at ๋ด๋ฆผ์ฐจ์)
3. **ํต๊ณ ์นด๋ UI**:
- ์๋จ: ํฐ ์ซ์๋ก "์ด ํ์ด ํด์ฆ: 12๊ฐ" / "ํ๊ท ์ ๋ต๋ฅ : 85%"
- ์ค๊ฐ: ์นดํ
๊ณ ๋ฆฌ๋ณ ์๋ฃ ํ์ ๋ฐ ์ฐจํธ (CSS `width` ํผ์ผํธ๋ก ์๊ฐํ)
- ํ๋จ: ์ต๊ทผ ์๋ฃ ํด์ฆ ๋ชฉ๋ก (์ ๋ชฉ, ์ ์, ๋ ์ง)
4. **๋ฐ ์ฐจํธ ๊ตฌํ** (์์ CSS):
```html
<div class="stat-bar">
<div class="stat-label">์์</div>
<div class="stat-progress" style="width: 70%;">7ํ</div>
</div>
```
- ๊ฐ ์นดํ
๊ณ ๋ฆฌ๋ณ ์๋ฃ ํ์๋ฅผ ์ต๋๊ฐ ๋๋น ๋น์จ๋ก ๊ณ์ฐ
- ๋ฐ ์์: ์นดํ
๊ณ ๋ฆฌ๋ง๋ค ๋ค๋ฅธ ๊ทธ๋ผ๋์ธํธ
5. **๋น ์ํ ์ฒ๋ฆฌ**:
- ์์ง ํด์ฆ๋ฅผ ํ์ง ์์ ๊ฒฝ์ฐ "์ฒซ ํด์ฆ๋ฅผ ํ์ด๋ณด์ธ์!" ๋ฉ์์ง + ํด์ฆ ๋ชฉ๋ก ๋ฒํผ
6. **๋์์ธ**:
- ํต๊ณ ์นด๋: `background: var(--m-surface); padding: 32px;`
- ์ซ์๋ ํฐ ํฐํธ (48px) + ๋ณผ๋
- ๋ฐ ์ฐจํธ: ์ ๋๋ฉ์ด์
(ํ์ด์ง ๋ก๋ ์ 0%์์ ์ค์ ๊ฐ๊น์ง 0.5์ด transition)
**SQL ์์ (์นดํ
๊ณ ๋ฆฌ๋ณ ์ง๊ณ):**
```sql
SELECT
qs.category,
COUNT(*) as count
FROM user_scores us
JOIN quiz_sets qs ON us.quiz_set_id = qs.id
WHERE us.mb_id = :mb_id
GROUP BY qs.category
```
**์์ URL**: `/quiz-arena/my-stats`
๊ธฐ๋ ๊ฒฐ๊ณผ: - ๋ก๊ทธ์ธ ์ฌ์ฉ์๋ง ์ ๊ทผ ๊ฐ๋ฅ - ๊ฐ์ธ ํต๊ณ ์์ฝ ์นด๋ ํ์ (์ด ํด์ฆ ์, ํ๊ท ์ ๋ต๋ฅ ) - ์นดํ ๊ณ ๋ฆฌ๋ณ ์๋ฃ ํ์ ๋ฐ ์ฐจํธ ํ์ - ์ต๊ทผ ์๋ฃ ํด์ฆ 5๊ฐ ๋ชฉ๋ก - ํด์ฆ ๊ธฐ๋ก ์์ ์ ๋น ์ํ ๋ฉ์์ง
**Step 5: ๊ด๋ฆฌ์ ํด์ฆ ๋ฑ๋ก ํผ ๊ตฌํ**
`app/modules/quiz-arena/admin/index.php` ํ์ผ์ ๋ง๋ค๊ณ ๋ค์์ ๊ตฌํํ์ธ์:
1. **๊ด๋ฆฌ์ ๊ถํ ์ฒดํฌ**:
- `$member['mb_level']`์ด 10 ์ด์์ธ์ง ํ์ธ (g5se ์ต๊ณ ๊ด๋ฆฌ์)
- ๊ถํ ์์ผ๋ฉด "์ ๊ทผ ๊ถํ์ด ์์ต๋๋ค" ๋ฉ์์ง + ํ์ผ๋ก ๋์๊ฐ๊ธฐ ๋ฒํผ
2. **ํด์ฆ ์ธํธ ๋ฑ๋ก ํผ**:
- ์
๋ ฅ ํ๋:
- ์นดํ
๊ณ ๋ฆฌ (select: ์์/์ญ์ฌ/๊ณผํ/์ํฐํ
์ธ๋จผํธ/๊ธฐํ)
- ํด์ฆ ์ธํธ ์ ๋ชฉ (text)
- ๋์ด๋ (select: ์ฌ์/๋ณดํต/์ด๋ ค์)
- "๋ฌธ์ ์ถ๊ฐ" ๋ฒํผ ํด๋ฆญ ์ ๋ฌธ์ ์
๋ ฅ ํ๋ ๋์ ์ถ๊ฐ (JS)
3. **๋ฌธ์ ์
๋ ฅ ํ๋** (๋์ ์ถ๊ฐ):
- ๋ฌธ์ ํ
์คํธ (textarea)
- ์ ํ์ง A/B/C/D (text input 4๊ฐ)
- ์ ๋ต ์ ํ (radio button)
- ํด์ค (textarea, ์ ํ์ฌํญ)
- "์ด ๋ฌธ์ ์ญ์ " ๋ฒํผ
4. **ํผ ์ ์ถ ์ฒ๋ฆฌ**:
- POST ์์ฒญ ์:
- quiz_sets ํ
์ด๋ธ์ ์ธํธ ์ ๋ณด INSERT
- ๋ฐํ๋ set_id๋ก ๊ฐ ๋ฌธ์ ๋ฅผ quiz_questions ํ
์ด๋ธ์ INSERT
- ํธ๋์ญ์
์ฌ์ฉ (SQLite `BEGIN`/`COMMIT`)
- ์ฑ๊ณต ์ "ํด์ฆ ์ธํธ๊ฐ ๋ฑ๋ก๋์์ต๋๋ค" ๋ฉ์์ง + ๋ชฉ๋ก์ผ๋ก ๋์๊ฐ๊ธฐ
5. **๊ธฐ์กด ํด์ฆ ์ธํธ ๋ชฉ๋ก ํ์**:
- ํผ ํ๋จ์ ๋ฑ๋ก๋ ํด์ฆ ์ธํธ ํ
์ด๋ธ
- ๊ฐ ํ: ์ ๋ชฉ | ์นดํ
๊ณ ๋ฆฌ | ๋ฌธ์ ์ | ์ญ์ ๋ฒํผ
- ์ญ์ ๋ฒํผ ํด๋ฆญ ์ ํ์ธ ๋ชจ๋ฌ + AJAX DELETE ์์ฒญ
6. **UI ๋์์ธ**:
- ํผ ๋ ์ด์์: 2์ด ๊ทธ๋ฆฌ๋ (๋ ์ด๋ธ ์ข์ธก, ์
๋ ฅ ์ฐ์ธก)
- ๋ฌธ์ ์นด๋: ์ ์ ํ
๋๋ฆฌ + ๋ฐฐ๊ฒฝ์ ๊ตฌ๋ถ
- "๋ฌธ์ ์ถ๊ฐ" ๋ฒํผ: ์ ์ ์คํ์ผ + hover ์ ๋ฐฐ๊ฒฝ ๋ณํ
**JavaScript ๋์ ํ๋ ์ถ๊ฐ ์์:**
```javascript
let questionCount = 1;
document.getElementById('add-question-btn').addEventListener('click', () => {
const container = document.getElementById('questions-container');
const questionHtml = `
<div class="question-card" data-index="${questionCount}">
<h4>๋ฌธ์ ${questionCount}</h4>
<textarea name="questions[${questionCount}][text]" placeholder="๋ฌธ์ ํ
์คํธ" required></textarea>
<input type="text" name="questions[${questionCount}][option_a]" placeholder="A" required>
<input type="text" name="questions[${questionCount}][option_b]" placeholder="B" required>
<input type="text" name="questions[${questionCount}][option_c]" placeholder="C" required>
<input type="text" name="questions[${questionCount}][option_d]" placeholder="D" required>
<label><input type="radio" name="questions[${questionCount}][correct]" value="A" required> ์ ๋ต: A</label>
<textarea name="questions[${questionCount}][explanation]" placeholder="ํด์ค (์ ํ)"></textarea>
<button type="button" onclick="this.parentElement.remove()">์ญ์ </button>
</div>
`;
container.insertAdjacentHTML('beforeend', questionHtml);
questionCount++;
});
```
**์์ URL**: `/quiz-arena/admin`
๊ธฐ๋ ๊ฒฐ๊ณผ: - ๊ด๋ฆฌ์๋ง ์ ๊ทผ ๊ฐ๋ฅ (๊ถํ ์ฒดํฌ) - ํด์ฆ ์ธํธ ๋ฑ๋ก ํผ ํ์ - "๋ฌธ์ ์ถ๊ฐ" ๋ฒํผ์ผ๋ก ๋ฌธ์ ํ๋ ๋์ ์ถ๊ฐ - ํผ ์ ์ถ ์ quiz_sets + quiz_questions ํ ์ด๋ธ์ ๋ฐ์ดํฐ ์ ์ฅ - ๊ธฐ์กด ํด์ฆ ์ธํธ ๋ชฉ๋ก ํ์ + ์ญ์ ๊ธฐ๋ฅ
**Step 6: ํ์ด๋จธ ๋ชจ๋ & ํํธ ์์คํ
(๋ณด๋์ค ๊ธฐ๋ฅ)**
`app/modules/quiz-arena/play/index.php` ํ์ผ์ ์์ ํ์ฌ ๋ค์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ์ธ์:
1. **ํ์ด๋จธ ๋ชจ๋ ํ์ฑํ**:
- URL ํ๋ผ๋ฏธํฐ `?mode=timer` ๊ฐ์ง
- ๊ฐ ๋ฌธ์ ๋น 30์ด ์ ํ ์๊ฐ
- ํ๋ฉด ์๋จ์ ์นด์ดํธ๋ค์ด ํ์ด๋จธ ํ์ (ํฐ ํฐํธ)
- ์๊ฐ ์ด๊ณผ ์ ์๋์ผ๋ก ์ค๋ต ์ฒ๋ฆฌ + ๋ค์ ๋ฌธ์ ๋ก ์ด๋
2. **ํ์ด๋จธ UI**:
- ์ํ ํ๋ก๊ทธ๋ ์ค ๋ฐ (SVG `<circle>` ์ฌ์ฉ)
- ๋จ์ ์๊ฐ์ด 10์ด ์ดํ์ผ ๋ ๋นจ๊ฐ์์ผ๋ก ๋ณ๊ฒฝ + ๊น๋นก์ ์ ๋๋ฉ์ด์
- ์๊ฐ ์ด๊ณผ ์ "์๊ฐ ์ด๊ณผ!" ๋ฉ์์ง + ์ ๋ต ํ์
3. **ํํธ ์์คํ
**:
- ํ๋ฉด ์ฐ์ธก ์๋จ์ "๐ก ํํธ ์ฌ์ฉ" ๋ฒํผ
- ํด๋ฆญ ์:
- ์ค๋ต ์ ํ์ง 1๊ฐ๋ฅผ ๋๋ค์ผ๋ก ๋นํ์ฑํ (ํ๋ฆฌ๊ฒ ํ์)
- ํํธ ์ฌ์ฉ ํ์ ์ ํ (ํด์ฆ๋น 3ํ)
- ํํธ ์ฌ์ฉ ์ ์ ์ -5์ ์ฐจ๊ฐ
- ํํธ ์ฌ์ฉ ํ์ ํ์ ("ํํธ ๋จ์: 2/3")
4. **์ ์ ๊ณ์ฐ ๋ก์ง ์์ **:
- ๊ธฐ๋ณธ ์ ์: ์ ๋ต ์ +10์
- ํ์ด๋จธ ๋ชจ๋: ๋จ์ ์๊ฐ์ ๋น๋กํ ๋ณด๋์ค ์ ์ (์ต๋ +5์ )
- ํํธ ์ฌ์ฉ: -5์ ์ฐจ๊ฐ
- ์ต์ข
์ ์ = ๊ธฐ๋ณธ ์ ์ + ์๊ฐ ๋ณด๋์ค - ํํธ ํ๋ํฐ
5. **ํ์ด๋จธ JavaScript ๊ตฌํ**:
```javascript
let timeLeft = 30;
const timerInterval = setInterval(() => {
timeLeft--;
document.getElementById('timer-display').textContent = timeLeft;
updateTimerProgress(timeLeft / 30 * 100);
if (timeLeft <= 10) {
document.getElementById('timer-display').classList.add('warning');
}
if (timeLeft <= 0) {
clearInterval(timerInterval);
handleTimeout();
}
}, 1000);
function handleTimeout() {
// ์ค๋ต ์ฒ๋ฆฌ ๋ก์ง
showMessage('์๊ฐ ์ด๊ณผ!');
setTimeout(() => nextQuestion(), 2000);
}
```
6. **ํํธ ๊ธฐ๋ฅ JavaScript**:
```javascript
let hintsRemaining = 3;
function useHint() {
if (hintsRemaining <= 0) {
alert('ํํธ๋ฅผ ๋ชจ๋ ์ฌ์ฉํ์ต๋๋ค');
return;
}
const wrongOptions = ['A', 'B', 'C', 'D'].filter(opt => opt !== correctAnswer);
const randomWrong = wrongOptions[Math.floor(Math.random() * wrongOptions.length)];
document.querySelector(`[data-option="${randomWrong}"]`).style.opacity = '0.3';
document.querySelector(`[data-option="${randomWrong}"]`).disabled = true;
hintsRemaining--;
currentScore -= 5;
updateHintDisplay();
}
```
7. **UI ๊ฐ์ **:
- ํ์ด๋จธ ๋ชจ๋ ์ ํ ํ ๊ธ ์ค์์น (๋ฉ์ธ ํ์ด์ง์ ์ถ๊ฐ)
- ํํธ ๋ฒํผ: ๋ฐํฌ๋ช
๋ฐฐ๊ฒฝ + ์์ด์ฝ
- ์๊ฐ ์ด๊ณผ ์ ์ ๋๋ฉ์ด์
ํจ๊ณผ (shake)
**์์ URL**: `/quiz-arena/play?set_id=1&mode=timer`
๊ธฐ๋ ๊ฒฐ๊ณผ: - ํ์ด๋จธ ๋ชจ๋ ํ์ฑํ ์ ๊ฐ ๋ฌธ์ ๋น 30์ด ์นด์ดํธ๋ค์ด - ์๊ฐ ์ด๊ณผ ์ ์๋ ์ค๋ต ์ฒ๋ฆฌ + ๋ค์ ๋ฌธ์ ์ด๋ - ํํธ ๋ฒํผ ํด๋ฆญ ์ ์ค๋ต 1๊ฐ ๋นํ์ฑํ (์ ์ ์ฐจ๊ฐ) - ํ์ด๋จธ ์งํ์ ๋ฐ๋ผ UI ์์ ๋ณ๊ฒฝ (10์ด ์ดํ ๋นจ๊ฐ) - ์ต์ข ์ ์์ ์๊ฐ ๋ณด๋์ค + ํํธ ํ๋ํฐ ๋ฐ์
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!