๐ฏ g5se ๋๋ ํธ๋ ์ด๋ ์ซ์ ๊ธฐ์ต ๊ฒ์ - ์์ฐจ ๊ธฐ์ต๋ ฅ ํ ์คํธ & ์ต๊ณ ๊ธฐ๋ก ๋์
g5se-number-memory-trainer
๋ฌธ์ ์ค๋ช
๐ง ๋ฌด์์ ๋ง๋๋์?
๋จ๊ธฐ ๊ธฐ์ต๋ ฅ์ ํ ์คํธํ๋ ์ซ์ ์์ฐจ ๊ธฐ์ต ๊ฒ์์ ๋ง๋ญ๋๋ค. ํ๋ฉด์ ์งง์ ์๊ฐ ๋์ ์ซ์๊ฐ ํ์๋๊ณ , ์ฌ๋ผ์ง ํ ์ฌ์ฉ์๊ฐ ๊ธฐ์ตํด์ ์ ๋ ฅํ๋ ๋ฐฉ์์ ๋๋ค. ๋ผ์ด๋๊ฐ ์งํ๋ ์๋ก ์ซ์ ๊ฐ์๊ฐ ๋์ด๋๋ฉฐ ๋์ด๋๊ฐ ์์นํฉ๋๋ค. ์ต๊ณ ๊ธฐ๋ก์ SQLite DB์ ์ ์ฅ๋์ด ์์ ๋ณด๋์ ํ์๋ฉ๋๋ค.
โ ํ์ ๊ธฐ๋ฅ
- ๊ฒ์ ์์: "์์ํ๊ธฐ" ๋ฒํผ ํด๋ฆญ ์ ์ฒซ ๋ผ์ด๋ ์์
- ์ซ์ ํ์: ํ๋ฉด์ ๋ฌด์์ ์ซ์ ์ํ์ค๊ฐ 2์ด๊ฐ ํ์๋๊ณ ์ฌ๋ผ์ง (๋ผ์ด๋ 1: 3์๋ฆฌ, ๋ผ์ด๋ 2: 4์๋ฆฌ, ...)
- ์ ๋ ฅ ํผ: ์ซ์๊ฐ ์ฌ๋ผ์ง ํ ์ ๋ ฅ ํ๋์ "์ ์ถ" ๋ฒํผ ํ์
- ์ ๋ต ๊ฒ์ฆ: ์ ๋ ฅ๊ฐ์ด ์ ๋ต๊ณผ ์ผ์นํ๋ฉด ๋ค์ ๋ผ์ด๋๋ก, ํ๋ฆฌ๋ฉด ๊ฒ์ ์ค๋ฒ
- ์ต๊ณ ๊ธฐ๋ก ์ ์ฅ: ๊ฒ์ ์ค๋ฒ ์ ๋๋ค์ ์ ๋ ฅ๋ฐ์ ๋๋ฌํ ๋ผ์ด๋ ์๋ฅผ DB์ ์ ์ฅ
- ์์ ๋ณด๋: ์์ 10๊ฐ ๊ธฐ๋ก์ ํ ์ด๋ธ๋ก ํ์ (๋๋ค์, ๋ผ์ด๋, ๋ ์ง)
- ๋คํฌ๋ชจ๋ ๋์: modern shell ๋์์ธ ํ ํฐ ์ฌ์ฉ์ผ๋ก ์๋ ๋์
๐ ์ ํ ์ถ๊ฐ ๊ธฐ๋ฅ (๋ณด๋์ค)
- ํ์ด๋จธ ์๊ฐํ (2์ด ์นด์ดํธ๋ค์ด ํ๋ก๊ทธ๋ ์ค ๋ฐ)
- ๊ฒ์ ํจ๊ณผ์ (์ ๋ต/์ค๋ต ์ฌ์ด๋)
- ๋์ด๋ ์ ํ (์ฌ์: 2์ด ํ์, ๋ณดํต: 1.5์ด, ์ด๋ ค์: 1์ด)
- ์ ๋๋ฉ์ด์ ํจ๊ณผ (์ซ์ ํ์ด๋์ธ/์์)
- ํต๊ณ ๊ทธ๋ํ (๋ผ์ด๋๋ณ ์ฑ๊ณต๋ฅ ์ฐจํธ)
๐ฏ ํ์ต ๋ชฉํ
- g5se ํด๋ ๊ธฐ๋ฐ ์๋ ๋ผ์ฐํ ์ดํด
- modern shell ์ ์ฉ์ผ๋ก ์ฌ์ดํธ ์ผ๊ด์ฑ ์ ์ง
- ๊ฐ๋จํ ๊ฒ์ ๋ก์ง ๊ตฌํ (์ํ ๊ด๋ฆฌ, ํ์ด๋จธ)
- SQLite ์์ฒด DB ์์ฑ ๋ฐ CRUD
- JavaScript ์ด๋ฒคํธ ํธ๋ค๋ง ๋ฐ DOM ์กฐ์
- CSS ๋ณ์ ํ์ฉํ ๋คํฌ๋ชจ๋ ๋์
์ ์ฝ ์กฐ๊ฑด
- โข g5se ์ค์น๋ณธ ์์์ ์์ (Laravel/Composer/npm ์ฌ์ฉ ๊ธ์ง)
- โข ํด๋ ๊ธฐ๋ฐ ์๋ ๋ผ์ฐํ ์ฌ์ฉ โ app/router.php ๋ผ์ฐํธ ๋ฑ๋ก ๋ถํ์
- โข modern shell ํ์ ์ ์ฉ (_head.inc.php + m-shell + _nav.inc.php + m-container + _footer.inc.php)
- โข ๋์์ธ ํ ํฐ ์ฌ์ฉ (--m-surface, --m-text, --m-shadow ๋ฑ) โ ๋คํฌ๋ชจ๋ ์๋ ๋์
- โข ์์ฒด SQLite DB ์ฌ์ฉ (G5_DATA_PATH.'/modules/number-memory.sqlite')
- โข prepared statement๋ก SQL ์ธ์ ์ ๋ฐฉ์ง
- โข htmlspecialchars()๋ก XSS ๋ฐฉ์ง
- โข ์ธ๋ถ CDN ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ ๊ธ์ง โ ์์ HTML/CSS/JS๋ก ๊ตฌํ
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
g5se๊ฐ ์ค์น๋ ๋๋ ํฐ๋ฆฌ(๋ฃจํธ์ `index.php`ยท`app/`์ด ์๋ ๊ณณ)์์ ์์ ์ ์์ํฉ๋๋ค. (g5se ์ค์น๋ https://github.com/gnuboard/g5se ์ฐธ๊ณ ) **Step 1: ๊ธฐ๋ณธ ๊ณจ๊ฒฉ ๋ง๋ค๊ธฐ** `app/modules/number-memory/index.php` ํ์ผ์ ์์ฑํ๊ณ ๋ค์ ๋ด์ฉ์ ์์ฑํ์ธ์: 1. g5se modern shell ๊ณจ๊ฒฉ ์ ์ฉ: - `_common.php` ์ธํด๋ฃจ๋ - `_head.inc.php` + `head.sub.php` ์ธํด๋ฃจ๋ - `<div class="m-shell">` ๋ํผ - `_nav.inc.php` ๋ค๋น๊ฒ์ด์ - `<main class="m-container">` ๋ณธ๋ฌธ ์์ญ - `_footer.inc.php` + `tail.sub.php` ์ธํด๋ฃจ๋ 2. ํ์ด์ง ์ ๋ชฉ๊ณผ ๊ฐ๋จํ ์ค๋ช ์ถ๊ฐ: - ์ ๋ชฉ: "๐ง ์ซ์ ๊ธฐ์ต๋ ฅ ๊ฒ์" - ์ค๋ช : "ํ๋ฉด์ ํ์๋๋ ์ซ์๋ฅผ ๊ธฐ์ตํ๊ณ ์ ๋ ฅํ์ธ์. ๋ผ์ด๋๊ฐ ์งํ๋ ์๋ก ์ซ์๊ฐ ๋์ด๋ฉ๋๋ค!" 3. ๊ฒ์ ์์ญ ๋ ์ด์์: - ์ค์ ์ ๋ ฌ๋ ๊ฒ์ ์นด๋ (์ต๋ ๋๋น 500px) - "์์ํ๊ธฐ" ๋ฒํผ (์ด๊ธฐ ์ํ) - ์ซ์ ํ์ ์์ญ (์จ๊น ์ํ) - ์ ๋ ฅ ํผ ์์ญ (์จ๊น ์ํ) 4. ์คํ์ผ๋ง: - ๋์์ธ ํ ํฐ ์ฌ์ฉ: `var(--m-surface)`, `var(--m-text)`, `var(--m-shadow)`, `var(--m-radius-lg)` - ๊ฒ์ ์นด๋: ํฐ์ ๋ฐฐ๊ฒฝ(๋ผ์ดํธ), ์ด๋์ด ๋ฐฐ๊ฒฝ(๋คํฌ), ๊ทธ๋ฆผ์, ๋ผ์ด๋ ๋ชจ์๋ฆฌ, ํจ๋ฉ - ๋ฒํผ: ํ๋์ ๋ฐฐ๊ฒฝ (#3b82f6), ํฐ์ ํ ์คํธ, ํธ๋ฒ ํจ๊ณผ **์ค์**: ์์ง ๊ฒ์ ๋ก์ง์ด๋ DB๋ ๊ตฌํํ์ง ๋ง์ธ์. ๊ณจ๊ฒฉ๊ณผ UI๋ง ๋ง๋ญ๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๋ธ๋ผ์ฐ์ ์์ `/number-memory` ์ ์ ์ modern shell์ด ์ ์ฉ๋ ํ์ด์ง๊ฐ ํ์๋ฉ๋๋ค. ๋ค๋น๊ฒ์ด์ , ํธํฐ, ๋คํฌ๋ชจ๋ ํ ๊ธ์ด ์๋์ผ๋ก ๋ํ๋๊ณ , ์ค์์ ๊ฒ์ ์นด๋์ "์์ํ๊ธฐ" ๋ฒํผ์ด ๋ณด์ ๋๋ค. ๋คํฌ๋ชจ๋ ์ ํ ์ ๋ฐฐ๊ฒฝ/ํ ์คํธ ์์์ด ์์ฐ์ค๋ฝ๊ฒ ๋ณ๊ฒฝ๋ฉ๋๋ค.
**Step 2: ๊ฒ์ ๋ก์ง ๊ตฌํ (JavaScript)**
`app/modules/number-memory/index.php` ํ์ผ์ JavaScript๋ฅผ ์ถ๊ฐํ์ฌ ๊ฒ์ ๋ก์ง์ ๊ตฌํํ์ธ์:
1. ๊ฒ์ ์ํ ๊ด๋ฆฌ:
- `currentRound`: ํ์ฌ ๋ผ์ด๋ (1๋ถํฐ ์์)
- `currentNumber`: ํ์ฌ ๋ผ์ด๋์์ ํ์ํ ์ซ์ ๋ฌธ์์ด
- `gameStarted`: ๊ฒ์ ์งํ ์ฌ๋ถ
2. "์์ํ๊ธฐ" ๋ฒํผ ํด๋ฆญ ์ด๋ฒคํธ:
- ๋ฒํผ ์จ๊ธฐ๊ธฐ
- `currentRound = 1`๋ก ์ด๊ธฐํ
- `startRound()` ํจ์ ํธ์ถ
3. `startRound()` ํจ์:
- ๋ผ์ด๋ ์๋ด ํ์ ("๋ผ์ด๋ N: X์๋ฆฌ ์ซ์")
- ๋ฌด์์ ์ซ์ ์์ฑ (๋ผ์ด๋ 1: 3์๋ฆฌ, ๋ผ์ด๋ 2: 4์๋ฆฌ, ...)
- ์ซ์๋ฅผ ํฐ ๊ธ์จ๋ก 2์ด๊ฐ ํ์
- 2์ด ํ ์ซ์ ์จ๊ธฐ๊ณ ์
๋ ฅ ํผ ํ์
4. ์
๋ ฅ ํผ:
- `<input type="text" maxlength="...">`๋ก ์ซ์ ์
๋ ฅ (์ต๋ ๊ธธ์ด๋ ํ์ฌ ๋ผ์ด๋+2)
- "์ ์ถ" ๋ฒํผ
5. "์ ์ถ" ๋ฒํผ ํด๋ฆญ ์ด๋ฒคํธ:
- ์
๋ ฅ๊ฐ๊ณผ `currentNumber` ๋น๊ต
- ์ ๋ต: ์ถํ ๋ฉ์์ง ํ์ ํ `currentRound++` โ `startRound()` ์ฌํธ์ถ
- ์ค๋ต: "๊ฒ์ ์ค๋ฒ! ๋ผ์ด๋ N๊น์ง ๋๋ฌ" ๋ฉ์์ง ํ์ โ ๋๋ค์ ์
๋ ฅ ํผ ํ์
6. ์๊ฐ์ ํผ๋๋ฐฑ:
- ์ซ์ ํ์ ์ ํ์ด๋์ธ ์ ๋๋ฉ์ด์
(CSS transition)
- ์ ๋ต/์ค๋ต ์ ์์ ๋ณํ (์ด๋ก/๋นจ๊ฐ)
**์ค์**: ์์ง DB ์ ์ฅ ๊ธฐ๋ฅ์ ๊ตฌํํ์ง ๋ง์ธ์. ๊ฒ์ ํ๋ ์ด๋ง ๋์ํ๋๋ก ํฉ๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: "์์ํ๊ธฐ" ๋ฒํผ ํด๋ฆญ ์ ๊ฒ์์ด ์์๋ฉ๋๋ค. ํ๋ฉด์ 3์๋ฆฌ ์ซ์๊ฐ 2์ด๊ฐ ํ์๋๊ณ ์ฌ๋ผ์ง ํ ์ ๋ ฅ ํผ์ด ๋ํ๋ฉ๋๋ค. ์ ๋ต ์ ๋ ฅ ์ ๋ค์ ๋ผ์ด๋(4์๋ฆฌ)๋ก ์งํ๋๊ณ , ์ค๋ต ์ ๊ฒ์ ์ค๋ฒ ๋ฉ์์ง๊ฐ ํ์๋ฉ๋๋ค. ์ฝ์์ ์๋ฌ๊ฐ ์์ด์ผ ํฉ๋๋ค.
**Step 3: SQLite DB ์์ฑ ๋ฐ ๊ธฐ๋ก ์ ์ฅ**
`app/modules/number-memory/index.php` ํ์ผ ์๋จ(๋ก์ง ๋ถ๋ถ)์ DB ์ฒ๋ฆฌ๋ฅผ ์ถ๊ฐํ์ธ์:
1. SQLite ์ฐ๊ฒฐ:
- `G5_DATA_PATH.'/modules'` ๋๋ ํฐ๋ฆฌ๊ฐ ์์ผ๋ฉด ์์ฑ (`@mkdir(..., 0707, true)`)
- `new PDO('sqlite:'.G5_DATA_PATH.'/modules/number-memory.sqlite')`๋ก ์ฐ๊ฒฐ
- ์๋ฌ ๋ชจ๋: `PDO::ERRMODE_EXCEPTION`
2. ํ
์ด๋ธ ์์ฑ (์ฒซ ์ ๊ทผ ์):
```sql
CREATE TABLE IF NOT EXISTS records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nickname TEXT NOT NULL,
round INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
```
3. AJAX ์๋ํฌ์ธํธ ์ถ๊ฐ:
- `$_POST['action'] === 'save_record'` ๋ถ๊ธฐ
- `$_POST['nickname']`๊ณผ `$_POST['round']` ๋ฐ๊ธฐ
- ์
๋ ฅ๊ฐ ๊ฒ์ฆ (๋๋ค์ 1~20์, ๋ผ์ด๋ 1 ์ด์)
- prepared statement๋ก INSERT:
```php
$stmt = $pdo->prepare('INSERT INTO records (nickname, round) VALUES (?, ?)');
$stmt->execute([$nickname, $round]);
```
- JSON ์๋ต: `{"success": true, "message": "๊ธฐ๋ก์ด ์ ์ฅ๋์์ต๋๋ค!"}`
- ์๋ฌ ์: `{"success": false, "message": "์ ์ฅ ์คํจ"}`
4. JavaScript ์์ :
- ๊ฒ์ ์ค๋ฒ ์ ๋๋ค์ ์
๋ ฅ ํผ ํ์
- "๊ธฐ๋ก ์ ์ฅ" ๋ฒํผ ํด๋ฆญ ์ `fetch()`๋ก POST ์์ฒญ
- ์ฑ๊ณต ์ "์์ ๋ณด๋ ๋ณด๊ธฐ" ๋ฒํผ ํ์ (ํ์ด์ง ์๋ก๊ณ ์นจ)
**์ค์**: prepared statement ์ฌ์ฉ ํ์, htmlspecialchars()๋ก ์ถ๋ ฅ ์ด์ค์ผ์ดํ
๊ธฐ๋ ๊ฒฐ๊ณผ: ๊ฒ์ ์ค๋ฒ ์ ๋๋ค์ ์ ๋ ฅ ํผ์ด ๋ํ๋๊ณ , ๋๋ค์ ์ ๋ ฅ ํ "๊ธฐ๋ก ์ ์ฅ" ๋ฒํผ ํด๋ฆญ ์ DB์ ๊ธฐ๋ก์ด ์ ์ฅ๋ฉ๋๋ค. ๋ธ๋ผ์ฐ์ ๊ฐ๋ฐ์ ๋๊ตฌ ๋คํธ์ํฌ ํญ์์ POST ์์ฒญ์ด ์ฑ๊ณต(200)ํ๊ณ , `G5_DATA_PATH/modules/number-memory.sqlite` ํ์ผ์ด ์์ฑ๋๋ฉฐ records ํ ์ด๋ธ์ ๋ฐ์ดํฐ๊ฐ ๋ค์ด๊ฐ๋๋ค.
**Step 4: ์์ ๋ณด๋ ํ์ ๋ฐ ์ต์ข
๋ค๋ฌ๊ธฐ**
`app/modules/number-memory/index.php` ํ์ผ์ ์์ฑํ์ธ์:
1. ์์ ๋ณด๋ ๋ฐ์ดํฐ ์กฐํ:
- ํ์ด์ง ๋ก๋ ์ ์์ 10๊ฐ ๊ธฐ๋ก SELECT:
```php
$stmt = $pdo->query('SELECT nickname, round, created_at FROM records ORDER BY round DESC, created_at ASC LIMIT 10');
$records = $stmt->fetchAll(PDO::FETCH_ASSOC);
```
2. ์์ ๋ณด๋ UI:
- ๊ฒ์ ์นด๋ ์๋์ "๐ ์ต๊ณ ๊ธฐ๋ก TOP 10" ์น์
์ถ๊ฐ
- `<table>` ํ๊ทธ๋ก ์์ํ ํ์:
- ์ด: ์์, ๋๋ค์, ๋ผ์ด๋, ๋ ์ง
- ๋ฐ์ดํฐ ๋ฐ๋ณต๋ฌธ์ผ๋ก ์ถ๋ ฅ (`htmlspecialchars()` ์ ์ฉ)
- ๊ธฐ๋ก์ด ์์ ๊ฒฝ์ฐ "์์ง ๊ธฐ๋ก์ด ์์ต๋๋ค" ๋ฉ์์ง
3. ์คํ์ผ ๊ฐ์ :
- ํ
์ด๋ธ: ํ
๋๋ฆฌ, ํจ๋ฉ, ํค๋ ๋ฐฐ๊ฒฝ์ (๋์์ธ ํ ํฐ ์ฌ์ฉ)
- ์์ 1์์ ๊ธ๋ฉ๋ฌ ์ด๋ชจ์ง ๐ฅ, 2์ ๐ฅ, 3์ ๐ฅ
- ๋ ์ง ํฌ๋งท: `date('Y-m-d H:i', strtotime($record['created_at']))`
4. ๊ฒ์ ๋ฆฌ์
๋ฒํผ:
- ๊ฒ์ ์ค๋ฒ ํ "๋ค์ ๋์ ํ๊ธฐ" ๋ฒํผ ์ถ๊ฐ
- ํด๋ฆญ ์ ํ์ด์ง ์๋ก๊ณ ์นจ (location.reload())
5. ๋ฐ์ํ ๋์์ธ:
- ๋ชจ๋ฐ์ผ์์ ํ
์ด๋ธ์ด ์คํฌ๋กค ๊ฐ๋ฅํ๋๋ก `overflow-x: auto` ๋ํผ
- ๊ฒ์ ์นด๋ ์ต๋ ๋๋น 500px, ์์ ํ๋ฉด์์ 100% ๋๋น
6. ์ ๊ทผ์ฑ:
- ์
๋ ฅ ํ๋์ `aria-label` ์ถ๊ฐ
- ๋ฒํผ์ ๋ช
ํํ ํ
์คํธ ์ฌ์ฉ
**์ต์ข
ํ์ธ ์ฌํญ**:
- ๋คํฌ๋ชจ๋ ์ ํ ์ ๋ชจ๋ ์์๊ฐ ์์ฐ์ค๋ฝ๊ฒ ๋ณ๊ฒฝ๋๋์ง ํ์ธ
- SQL ์ธ์ ์
, XSS ๋ฐฉ์ด ํ์ธ
- ์ฝ์ ์๋ฌ ์๋์ง ํ์ธ
๊ธฐ๋ ๊ฒฐ๊ณผ: ์์ฑ๋ ๊ฒ์์ด ๋์ํฉ๋๋ค. ํ์ด์ง ํ๋จ์ "์ต๊ณ ๊ธฐ๋ก TOP 10" ์์ํ๊ฐ ํ์๋๊ณ , ๊ฒ์ ํ๋ ์ด ํ ๊ธฐ๋ก ์ ์ฅ ์ ์์ํ๊ฐ ์ ๋ฐ์ดํธ๋ฉ๋๋ค. ๋คํฌ๋ชจ๋ ์ ํ ์ ๋ชจ๋ UI๊ฐ ์ ์ ํ ๋ณ๊ฒฝ๋๋ฉฐ, ๋ชจ๋ฐ์ผ์์๋ ๋ ์ด์์์ด ๊นจ์ง์ง ์์ต๋๋ค. ์ฌ๋ฌ ๋ฒ ํ๋ ์ดํ์ฌ ์์๊ฐ ์ ์์ ์ผ๋ก ์์ด๋์ง ํ์ธํ ์ ์์ต๋๋ค.
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!