๐ฏ g5se ์ค๋์ ์์ด ๋ฌธ์ฅ ๋ง๋ค๊ธฐ - ๋จ์ด ์นด๋ ์กฐํฉ ๊ฒ์
g5se-sentence-builder-game
๋ฌธ์ ์ค๋ช
๐ฏ ํ๋ก์ ํธ ๊ฐ์
๋๋ค์ผ๋ก ์ฃผ์ด์ง ์์ด ๋จ์ด ์นด๋๋ค์ ๋๋๊ทธ ์ค ๋๋กญ์ผ๋ก ์กฐํฉํ์ฌ ๋ฌธ๋ฒ์ ๋ง๋ ๋ฌธ์ฅ์ ๋ง๋๋ ํ์ต ๊ฒ์์ ๋๋ค. ์ฌ์ฉ์๋ ์ฃผ์ด์ง ๋จ์ด๋ค์ ์ฌ๋ฐ๋ฅธ ์์๋ก ๋ฐฐ์ดํ๊ณ , ์ ์ถ ๋ฒํผ์ ๋๋ฌ ์ ๋ต ์ฌ๋ถ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค. SQLite DB์ ๋ฌธ์ฅ ํ ํ๋ฆฟ๊ณผ ์ ๋ต์ ์ ์ฅํ๊ณ , ๋งค ์ ์๋ง๋ค ๋๋ค์ผ๋ก ๋ฌธ์ ๋ฅผ ์ถ์ ํฉ๋๋ค.
โ ํ์ ๊ธฐ๋ฅ
- ๋๋ค ๋ฌธ์ ์ถ์ : ํ์ด์ง ๋ก๋ ์ DB์์ ๋๋์ผ๋ก 1๊ฐ ๋ฌธ์ฅ ํ ํ๋ฆฟ ์ ํ
- ๋จ์ด ์นด๋ ์๊ธฐ: ์ ๋ต ๋ฌธ์ฅ์ ๋จ์ด ๋จ์๋ก ๋ถ๋ฆฌํ์ฌ ๋ฌด์์ ์์๋ก ์นด๋ ๋ฐฐ์น
- ๋๋๊ทธ ์ค ๋๋กญ: ๋จ์ด ์นด๋๋ฅผ ๋๋๊ทธํ์ฌ ์ ๋ต ์์ญ์ ์์๋๋ก ๋ฐฐ์น
- ์ ๋ต ํ์ธ: ์ฌ์ฉ์๊ฐ ๋ฐฐ์ดํ ๋ฌธ์ฅ๊ณผ ์ ๋ต์ ๋น๊ตํ์ฌ ์ฑ๊ณต/์คํจ ๋ฉ์์ง ํ์
- ๋ค์ ๋์ : ํ์ฌ ๋ฌธ์ ๋ฅผ ๋ฆฌ์ ํ์ฌ ์ฒ์๋ถํฐ ๋ค์ ํ๊ธฐ
- ๋ค์ ๋ฌธ์ : ์๋ก์ด ๋ฌธ์ ๋ฅผ ๋๋ค์ผ๋ก ๋ถ๋ฌ์ค๊ธฐ
- ๋คํฌ๋ชจ๋ ๋์: modern shell์ ๋์์ธ ํ ํฐ ์ฌ์ฉ์ผ๋ก ์๋ ๋คํฌ๋ชจ๋ ์ง์
๐ ์ ํ ์ถ๊ฐ ๊ธฐ๋ฅ (๋ณด๋์ค)
- ํํธ ๊ธฐ๋ฅ: ์ฒซ ๋จ์ด ์์น๋ฅผ ์ด์ง ๋ณด์ฌ์ฃผ๋ ํํธ ๋ฒํผ
- ๋์ด๋๋ณ ํํฐ: ์ฌ์ด/์ค๊ฐ/์ด๋ ค์ด ๋ฌธ์ฅ ํ ํ๋ฆฟ ์ ํ
- ํต๊ณ ํ์: ์ธ์ ๋์ ๋ง์ถ ๋ฌธ์ ์ / ์๋ ํ์ ํ์
- ์ ๋๋ฉ์ด์ : ์นด๋ ๋๋กญ ์ ๋ถ๋๋ฌ์ด ์ ํ ํจ๊ณผ
- ์์ฑ ํผ๋๋ฐฑ: ์ ๋ต/์ค๋ต ์ ํจ๊ณผ์ ์ฌ์ (Web Audio API)
๐ ๊ธฐ์ ์คํ
- ๋ฐฑ์๋: PHP 8.2+ (g5se ์ปจ๋ฒค์ )
- DB: SQLite (๋ชจ๋ ์ ์ฉ ํ์ผ)
- ํ๋ก ํธ: HTML5 Drag & Drop API, Vanilla JS
- ์คํ์ผ: modern shell ๋์์ธ ํ ํฐ + ์ปค์คํ CSS
๐ ์ฃผ์ ํ์ผ ๊ตฌ์กฐ
app/modules/sentence-builder/
โโโ index.php # ๋ฉ์ธ ๊ฒ์ ํ์ด์ง
โโโ (DB๋ data/modules/sentence-builder.sqlite ์๋ ์์ฑ)
์ ์ฝ ์กฐ๊ฑด
- โข g5se ์ค์น๋ณธ ์์์ ์์ (https://github.com/gnuboard/g5se main ๋ธ๋์น)
- โข ํด๋ ๊ธฐ๋ฐ ์๋ ๋ผ์ฐํ ์ฌ์ฉ โ app/modules/sentence-builder/index.php ์์ฑํ๋ฉด /sentence-builder๋ก ์๋ ์ ๊ทผ
- โข modern shell ํ์ ์ ์ฉ (_head.inc.php + m-shell + _nav.inc.php + m-container + _footer.inc.php + tail.sub.php)
- โข ๋์์ธ ํ ํฐ ์ฌ์ฉ (--m-surface, --m-text, --m-shadow ๋ฑ) โ ๋คํฌ๋ชจ๋ ์๋ ๋์
- โข ๋ชจ๋ ์ ์ฉ SQLite DB: G5_DATA_PATH.'/modules/sentence-builder.sqlite' ๊ฒฝ๋ก์ ์์ฑ
- โข DB ์คํค๋ง๋ ์ฒซ ์ ์ ์ CREATE TABLE IF NOT EXISTS๋ก ์๋ ์์ฑ (๋ง์ด๊ทธ๋ ์ด์ ๋ถํ์)
- โข prepared statement ์ฌ์ฉ์ผ๋ก SQL ์ธ์ ์ ๋ฐฉ์ง
- โข ์ฌ์ฉ์ ์ ๋ ฅ์ htmlspecialchars()๋ก XSS ๋ฐฉ์ง
- โข HTML5 Drag & Drop API ์ฌ์ฉ (์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ธ์ง)
- โข ์ธ๋ถ ๋น๋ ๋๊ตฌ(npm/vite) ์ฌ์ฉ ๊ธ์ง, CDN ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๊ฐ๊ธ์ ๋ฐฐ์ ํ๊ณ ์์ JS๋ก ๊ตฌํ
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
g5se๊ฐ ์ค์น๋ ๋๋ ํฐ๋ฆฌ(๋ฃจํธ์ `index.php`ยท`app/`์ด ์๋ ๊ณณ)์์ ์์
์ ์์ํฉ๋๋ค. (g5se ์ค์น๋ https://github.com/gnuboard/g5se ์ฐธ๊ณ )
์ด์ `app/modules/sentence-builder/index.php` ํ์ผ์ ์์ฑํ๊ณ , ๋ค์ ๊ธฐ๋ฅ์ ๊ตฌํํ์ธ์:
1. **modern shell ๊ณจ๊ฒฉ ์ ์ฉ**:
- `_common.php` ์ธํด๋ฃจ๋ (์๋๊ฒฝ๋ก `./_common.php`)
- `require_once(G5_THEME_PATH.'/modern/_head.inc.php');`
- `include_once('./head.sub.php');`
- `<div class="m-shell">` ์ปจํ
์ด๋
- `<?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>`
- `<main class="m-container" style="padding: 32px 20px 48px;">`๋ก ๋ณธ๋ฌธ ์์ญ
- `<?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>`
- `<?php include_once('./tail.sub.php'); ?>`
2. **SQLite DB ์ด๊ธฐํ**:
- `G5_DATA_PATH.'/modules'` ๋๋ ํฐ๋ฆฌ๊ฐ ์์ผ๋ฉด `@mkdir(G5_DATA_PATH.'/modules', 0707, true);`๋ก ์์ฑ
- `new PDO('sqlite:'.G5_DATA_PATH.'/modules/sentence-builder.sqlite')`๋ก ์ฐ๊ฒฐ
- ํ
์ด๋ธ ์คํค๋ง (์ฒซ ์ ์ ์ ์๋ ์์ฑ):
```sql
CREATE TABLE IF NOT EXISTS sentences (
id INTEGER PRIMARY KEY AUTOINCREMENT,
answer TEXT NOT NULL,
difficulty TEXT DEFAULT 'easy',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
```
- ์ด๊ธฐ ์ํ ๋ฐ์ดํฐ 3๊ฐ INSERT (์ด๋ฏธ ์์ผ๋ฉด ์คํต):
- "The cat is sleeping" (easy)
- "She likes reading books" (easy)
- "We are learning English" (easy)
3. **ํ์ด์ง ํ์ดํ & ์ค๋ช
**:
- ํค๋: "๐ฏ ์ค๋์ ์์ด ๋ฌธ์ฅ ๋ง๋ค๊ธฐ"
- ๋ถ์ : "๋จ์ด ์นด๋๋ฅผ ์ฌ๋ฐ๋ฅธ ์์๋ก ๋ฐฐ์ดํ์ฌ ๋ฌธ์ฅ์ ์์ฑํ์ธ์!"
4. **๊ฒ์ ์์ญ HTML ๊ตฌ์กฐ**:
- ๋ฌธ์ ๋ฒํธ ํ์ ์์ญ (`#problem-info`)
- ๋จ์ด ์นด๋ ํ ์์ญ (`#word-pool`) โ ๋๋๊ทธ ๊ฐ๋ฅํ ์นด๋๋ค์ด ๋ค์ด๊ฐ ๊ณณ
- ์ ๋ต ์กฐ๋ฆฝ ์์ญ (`#answer-zone`) โ ๋๋กญ์กด์ผ๋ก ์ฌ์ฉ์๊ฐ ์นด๋๋ฅผ ๋ฐฐ์นํ๋ ๊ณณ
- ๋ฒํผ ๊ทธ๋ฃน:
- "์ ๋ต ํ์ธ" ๋ฒํผ (`#check-btn`)
- "๋ค์ ๋์ " ๋ฒํผ (`#reset-btn`)
- "๋ค์ ๋ฌธ์ " ๋ฒํผ (`#next-btn`)
- ํผ๋๋ฐฑ ๋ฉ์์ง ์์ญ (`#feedback`) โ ์ ๋ต/์ค๋ต ๊ฒฐ๊ณผ ํ์
5. **์คํ์ผ๋ง (์ธ๋ผ์ธ `<style>` ๋ธ๋ก)**:
- ๋์์ธ ํ ํฐ ์ฌ์ฉ:
- ๋ฐฐ๊ฒฝ: `background: var(--m-surface);`
- ํ
์คํธ: `color: var(--m-text);`
- ๊ทธ๋ฆผ์: `box-shadow: var(--m-shadow);`
- ๋ผ์ด๋: `border-radius: var(--m-radius-lg);`
- ๋จ์ด ์นด๋ ์คํ์ผ:
- ์ธ๋ผ์ธ ๋ธ๋ก, ํจ๋ฉ, ์ปค์ ํฌ์ธํฐ
- ๋๋๊ทธ ์ ๋ฐํฌ๋ช
ํจ๊ณผ
- ์ ๋ต ์กฐ๋ฆฝ ์์ญ:
- ์ต์ ๋์ด 80px, ์ ์ ํ
๋๋ฆฌ
- ์นด๋๊ฐ ์์ ๋ "์ฌ๊ธฐ์ ๋จ์ด๋ฅผ ๋๋๊ทธํ์ธ์" placeholder ํ์
- ๋ฒํผ:
- primary ์์ ๋ฐฐ๊ฒฝ, ํฐ์ ํ
์คํธ
- hover ์ ์ฝ๊ฐ ์ด๋์์ง๋ ํจ๊ณผ
**์ถ๋ ฅ**: modern shell์ด ์ ์ฉ๋ ํ์ด์ง๊ฐ `/sentence-builder` URL๋ก ์ ๊ทผ ๊ฐ๋ฅํ๊ณ , DB ํ
์ด๋ธ์ด ์๋ ์์ฑ๋๋ฉฐ ์ํ ๋ฐ์ดํฐ 3๊ฐ๊ฐ ์ฝ์
๋ ์ํ์ฌ์ผ ํฉ๋๋ค. ์์ง JS ๋ก์ง์ ์์ผ๋ฏ๋ก ์นด๋๋ ์ ์ ์ผ๋ก๋ง ํ์๋ฉ๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: ํ์ด์ง๊ฐ /sentence-builder๋ก ์ ๊ทผ ๊ฐ๋ฅํ๋ฉฐ, modern shell์ nav/footer๊ฐ ํฌํจ๋๊ณ , DB ํ ์ด๋ธ์ด ์๋ ์์ฑ๋์ด ์ด๊ธฐ ์ํ ๋ฌธ์ฅ 3๊ฐ๊ฐ ์ฝ์ ๋ฉ๋๋ค. ํ์ด์ง์๋ ๊ฒ์ ์์ญ HTML ๊ตฌ์กฐ์ ๊ธฐ๋ณธ ์คํ์ผ์ด ์ ์ฉ๋์ด ์์ผ๋ ์์ง ์ธํฐ๋์ ์ ๋์ํ์ง ์์ต๋๋ค.
์ด์ **๋๋ค ๋ฌธ์ ์ถ์ ๋ก์ง**๊ณผ **๋จ์ด ์นด๋ ์์ฑ JS**๋ฅผ ๊ตฌํํ์ธ์:
1. **PHP ๋ก์ง (ํ์ด์ง ์๋จ)**:
- DB์์ `SELECT * FROM sentences ORDER BY RANDOM() LIMIT 1`๋ก ๋๋ค ๋ฌธ์ฅ 1๊ฐ ๊ฐ์ ธ์ค๊ธฐ
- ์ ๋ต ๋ฌธ์ฅ์ ๊ณต๋ฐฑ ๊ธฐ์ค์ผ๋ก `explode(' ', $answer)`ํ์ฌ ๋จ์ด ๋ฐฐ์ด ์์ฑ
- ๋จ์ด ๋ฐฐ์ด์ `shuffle()`๋ก ์๊ธฐ (๋ฌด์์ ์์)
- ์์ธ ๋จ์ด ๋ฐฐ์ด์ JSON์ผ๋ก ์ธ์ฝ๋ฉํ์ฌ JS ๋ณ์์ ์ ๋ฌ:
```php
<script>
const shuffledWords = <?= json_encode($shuffled_words) ?>;
const correctAnswer = <?= json_encode($answer) ?>;
const problemId = <?= $problem['id'] ?>;
</script>
```
2. **JS ๋ก์ง (์ธ๋ผ์ธ `<script>` ๋ธ๋ก)**:
- ํ์ด์ง ๋ก๋ ์ `shuffledWords` ๋ฐฐ์ด์ ์ํํ๋ฉฐ ๊ฐ ๋จ์ด๋ฅผ `<div class="word-card" draggable="true">` ์์๋ก ์์ฑ
- ์์ฑ๋ ์นด๋๋ฅผ `#word-pool`์ append
- ๊ฐ ์นด๋์ `data-word` ์์ฑ์ผ๋ก ๋จ์ด ์ ์ฅ
- ๋ฌธ์ ์ ๋ณด ํ์: `#problem-info`์ "๋ฌธ์ #${problemId}" ์ถ๋ ฅ
3. **๋๋๊ทธ ์ค ๋๋กญ ์ด๋ฒคํธ ๋ฆฌ์ค๋**:
- `dragstart`: ๋๋๊ทธ ์์ ์ ์นด๋ ๋ฐ์ดํฐ๋ฅผ `dataTransfer`์ ์ ์ฅ
- `dragover`: ๋๋กญ์กด ์์์ `preventDefault()`๋ก ๋๋กญ ํ์ฉ
- `drop`: ๋๋กญ ์ ์นด๋๋ฅผ `#answer-zone`์ผ๋ก ์ด๋
- ์นด๋๋ฅผ `#answer-zone`์์ ๋ค์ `#word-pool`๋ก ๋๋๋ฆฌ๊ธฐ๋ ๊ฐ๋ฅํ๋๋ก ์๋ฐฉํฅ ๋๋๊ทธ ์ง์
4. **์นด๋ ๋ฐฐ์น ์๊ฐ ํผ๋๋ฐฑ**:
- ๋๋๊ทธ ์ค์ธ ์นด๋๋ `opacity: 0.5` ์ ์ฉ
- ๋๋กญ์กด์ ์นด๋๊ฐ hover๋ ๋ ๋ฐฐ๊ฒฝ์ ์ด์ง ๋ณ๊ฒฝ
**์ถ๋ ฅ**: ํ์ด์ง ๋ก๋ ์ ๋๋ค ๋ฌธ์ ๊ฐ ์ถ์ ๋๊ณ , ๋จ์ด ์นด๋๋ค์ด ์์ธ ์์๋ก ํ์๋ฉ๋๋ค. ์นด๋๋ฅผ ๋๋๊ทธํ์ฌ ์ ๋ต ์์ญ์ผ๋ก ์ด๋ํ ์ ์์ผ๋ฉฐ, ์์๋ฅผ ์์ ๋กญ๊ฒ ์กฐ์ ํ ์ ์์ต๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: ํ์ด์ง ๋ก๋ ์ DB์์ ๋๋ค ๋ฌธ์ฅ์ด ์ ํ๋๊ณ , ๋จ์ด ์นด๋๊ฐ ๋ฌด์์ ์์๋ก ์์ฑ๋ฉ๋๋ค. ๋๋๊ทธ ์ค ๋๋กญ์ผ๋ก ์นด๋๋ฅผ ์ ๋ต ์์ญ๊ณผ ๋จ์ด ํ ์ฌ์ด์์ ์์ ๋กญ๊ฒ ์ด๋ํ ์ ์์ต๋๋ค.
์ด์ **์ ๋ต ํ์ธ ๋ก์ง**๊ณผ **ํผ๋๋ฐฑ ์์คํ
**์ ๊ตฌํํ์ธ์:
1. **์ ๋ต ํ์ธ ๋ฒํผ (`#check-btn`) ํด๋ฆญ ์ด๋ฒคํธ**:
- `#answer-zone` ์์ ๋ชจ๋ `.word-card` ์์๋ฅผ ์์๋๋ก ์ฝ์ด์ ๋ฐฐ์ด๋ก ๋ง๋ค๊ธฐ
- ๋ฐฐ์ด์ ๊ณต๋ฐฑ์ผ๋ก joinํ์ฌ ์ฌ์ฉ์๊ฐ ๋ง๋ ๋ฌธ์ฅ ์์ฑ
- `correctAnswer`์ ๋น๊ต:
- ์ผ์นํ๋ฉด: `#feedback`์ "โ
์ ๋ต์
๋๋ค!" ์ด๋ก์ ๋ฉ์์ง ํ์
- ๋ถ์ผ์นํ๋ฉด: `#feedback`์ "โ ๋ค์ ์๋ํด๋ณด์ธ์. ์ ๋ต: {correctAnswer}" ๋นจ๊ฐ์ ๋ฉ์์ง ํ์
2. **๋ค์ ๋์ ๋ฒํผ (`#reset-btn`) ํด๋ฆญ ์ด๋ฒคํธ**:
- `#answer-zone`์ ๋ชจ๋ ์นด๋๋ฅผ ๋ค์ `#word-pool`๋ก ์ด๋
- `#feedback` ๋ฉ์์ง ์ด๊ธฐํ
- ์นด๋ ์์๋ฅผ ๋ค์ ์์ด์ ์ฌ๋ฐฐ์น (์ ํ ์ฌํญ)
3. **๋ค์ ๋ฌธ์ ๋ฒํผ (`#next-btn`) ํด๋ฆญ ์ด๋ฒคํธ**:
- ํ์ด์ง๋ฅผ ์๋ก๊ณ ์นจํ์ฌ ์๋ก์ด ๋๋ค ๋ฌธ์ ๋ก๋
- ๋๋ AJAX๋ก ์ ๋ฌธ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ ๋์ ์ผ๋ก ๊ต์ฒด (๊ณ ๊ธ ์ต์
)
4. **ํผ๋๋ฐฑ ๋ฉ์์ง ์คํ์ผ**:
- ์ ๋ต ์: ์ด๋ก์ ๋ฐฐ๊ฒฝ, ๊ตต์ ๊ธ์จ, ์ฒดํฌ ์์ด์ฝ
- ์ค๋ต ์: ๋นจ๊ฐ์ ๋ฐฐ๊ฒฝ, ์ ๋ต ํํธ ํ์
- ์ ๋๋ฉ์ด์
: fade-in ํจ๊ณผ๋ก ๋ถ๋๋ฝ๊ฒ ๋ํ๋๊ธฐ
**์ถ๋ ฅ**: ์ฌ์ฉ์๊ฐ ์นด๋๋ฅผ ๋ฐฐ์ดํ๊ณ "์ ๋ต ํ์ธ" ๋ฒํผ์ ๋๋ฅด๋ฉด ์ ๋ต ์ฌ๋ถ๊ฐ ํ์ ๋์ด ํผ๋๋ฐฑ ๋ฉ์์ง๊ฐ ํ์๋ฉ๋๋ค. "๋ค์ ๋์ "์ผ๋ก ๋ฆฌ์
ํ๊ฑฐ๋ "๋ค์ ๋ฌธ์ "๋ก ์ ๋ฌธ์ ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ ๋ต ํ์ธ ๋ฒํผ์ ๋๋ฅด๋ฉด ์ฌ์ฉ์๊ฐ ๋ฐฐ์ดํ ๋ฌธ์ฅ๊ณผ ์ ๋ต์ ๋น๊ตํ์ฌ ์ฑ๊ณต/์คํจ ๋ฉ์์ง๊ฐ ํ์๋ฉ๋๋ค. ๋ค์ ๋์ ๋ฒํผ์ผ๋ก ํ์ฌ ๋ฌธ์ ๋ฅผ ๋ฆฌ์ ํ๊ณ , ๋ค์ ๋ฌธ์ ๋ฒํผ์ผ๋ก ์๋ก์ด ๋ฌธ์ ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค. ๋ชจ๋ ๋ฒํผ์ด ์ ์ ๋์ํ๋ฉฐ ๊ฒ์ ํ๋ก์ฐ๊ฐ ์์ฑ๋ฉ๋๋ค.
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!