๐ฒ ์ฃผ์ฌ์ ๊ฒ์ ์ปฌ๋ ์ - ๋ค์ํ ํ๋ฅ ๊ฒ์ ํ๋ ์ด๊ทธ๋ผ์ด๋
dice-game-collection
๋ฌธ์ ์ค๋ช
๐ฏ ํ๋ก์ ํธ ๊ฐ์
๊ทธ๋๋ณด๋5 SE ์์์ ๋์ํ๋ ์ฃผ์ฌ์ ๊ฒ์ ์ปฌ๋ ์ ํ๋ซํผ์ ๋ง๋ญ๋๋ค. ์ฌ๋ฌ ์ข ๋ฅ์ ์ฃผ์ฌ์ ๊ฒ์(์ผ๋ฐ ์ฃผ์ฌ์, ํน์ ์ฃผ์ฌ์, ๋ค๋ฉด์ฒด ์ฃผ์ฌ์ ๋ฑ)์ ํ ๊ณณ์์ ์ฆ๊ธฐ๊ณ , ๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋กํ๋ฉฐ, ์ ์ฒด ํ๋ ์ด์ด ํต๊ณ๋ฅผ ํ์ธํ ์ ์๋ ์ธํฐ๋ํฐ๋ธ ์น ์๋น์ค์ ๋๋ค.
ํ์ ๊ธฐ๋ฅ
-
๊ฒ์ ์ปฌ๋ ์ ๋ฉ์ธ ํ์ด์ง
- 6์ข ๋ฅ ์ด์์ ๋ค์ํ ์ฃผ์ฌ์ ๊ฒ์ ์นด๋ ๊ทธ๋ฆฌ๋ ๋ ์ด์์
- ๊ฐ ๊ฒ์๋ณ ์ธ๋ค์ผ, ์ค๋ช , ํ๋ ์ด ํ์ ํ์
- ํธ๋ฒ ์ 3D ์นด๋ ํธํธ ํจ๊ณผ
- ๋คํฌ๋ชจ๋ ์๋ ๋์ (modern shell ๋์์ธ ํ ํฐ ํ์ฉ)
-
๊ฐ๋ณ ๊ฒ์ ํ์ด์ง (ํด๋ ๊ธฐ๋ฐ ๋ผ์ฐํ )
/games/classic- ์ผ๋ฐ ์ฃผ์ฌ์ (1~6)/games/lucky7- ๋ญํค์ธ๋ธ (ํฉ์ด 7์ด๋ฉด ๋๋ฐ)/games/polyhedral- ๋ค๋ฉด์ฒด ์ฃผ์ฌ์ (d4, d6, d8, d10, d12, d20)/games/custom- ์ปค์คํ ์ฃผ์ฌ์ (์ฌ์ฉ์ ์ ์ ๋ฉด ๊ฐ์)/games/battle- ์ฃผ์ฌ์ ๋ฐฐํ (2์ธ ๋๊ฒฐ)/games/accumulate- ๋์ ์ ์ ๊ฒ์ (๋ชฉํ ์ ์ ๋๋ฌ)- ๊ฐ ๊ฒ์๋ณ ๊ณ ์ ํ ๋ฃฐ๊ณผ UI/UX
- ์ ๋๋ฉ์ด์ ์ผ๋ก ์ฃผ์ฌ์ ๊ตด๋ฆฌ๊ธฐ ํจ๊ณผ (CSS 3D transform)
- ๊ฒฐ๊ณผ ํ์คํ ๋ฆฌ ์ค์๊ฐ ํ์
-
๊ฒ์ ๊ธฐ๋ก ์์คํ (SQLite)
- ๊ฐ ๊ฒ์๋ณ ํ๋ ์ด ๊ธฐ๋ก ์ ์ฅ (๊ฒ์ ํ์ , ๊ฒฐ๊ณผ, ํ์์คํฌํ)
- ํ์ ์ฐ๋: ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ ์์ ์ ๊ธฐ๋ก ์ถ์ , ๋นํ์์ ์ธ์ ๊ธฐ๋ฐ ์์ ๊ธฐ๋ก
- ๊ฒ์๋ณ ์ต๊ณ ๊ธฐ๋ก, ํ๊ท ์ ์, ์ด ํ๋ ์ด ํ์ ํต๊ณ
-
์ ์ฒด ํต๊ณ ๋์๋ณด๋ (
/games/stats)- ์ ์ฒด ํ๋ ์ด์ด์ ๊ฒ์๋ณ ํต๊ณ (์ด ํ๋ ์ด ํ์, ์ธ๊ธฐ ๊ฒ์ ์์)
- ์ต๊ทผ 100๊ฐ ํ๋ ์ด ๊ธฐ๋ก ํ์๋ผ์ธ
- Chart.js๋ฅผ ํ์ฉํ ๊ฒ์๋ณ ํ๋ ์ด ๋ถํฌ ํ์ด ์ฐจํธ
- ์ฃผ์ฌ์ ๋ ๋ถํฌ ํ์คํ ๊ทธ๋จ (ํ๋ฅ ๊ฒ์ฆ)
-
๊ฐ์ธ ๊ธฐ๋ก ํ์ด์ง (
/games/myrecords)- ๋ก๊ทธ์ธ ์ฌ์ฉ์ ์ ์ฉ
- ๋ด๊ฐ ํ๋ ์ดํ ๊ฒ์๋ณ ์์ธ ๊ธฐ๋ก
- ๊ฒ์๋ณ ์น๋ฅ , ์ต๊ณ ์ ์, ํ๋ ์ด ํ์ ์ดํฉ
- ๊ธฐ๋ก ์ญ์ ๊ธฐ๋ฅ (AJAX)
-
UX ์ธํฐ๋์
- ์ฃผ์ฌ์ ๊ตด๋ฆฌ๊ธฐ ๋ฒํผ ํด๋ฆญ ์ 3D ํ์ ์ ๋๋ฉ์ด์ + ์ฌ์ด๋ ํจ๊ณผ (optional)
- ๊ฒฐ๊ณผ ๋ชจ๋ฌ ํ์ (์น๋ฆฌ/ํจ๋ฐฐ ๋ฉ์์ง)
- ๋ฌดํ ์คํฌ๋กค๋ก ๊ธฐ๋ก ํ์๋ผ์ธ ๋ก๋ฉ
- ๋๋๊ทธ ์ค ๋๋กญ์ผ๋ก ๋ค๋ฉด์ฒด ์ฃผ์ฌ์ ๊ฐ์ ์กฐ์ (์ฌ๋ผ์ด๋)
์ ํ ์ถ๊ฐ ๊ธฐ๋ฅ (๋ณด๋์ค)
- ์ฃผ์ฌ์ ์คํจ ์ปค์คํฐ๋ง์ด์ง (์์, ๋ฌด๋ฌ ๋ณ๊ฒฝ)
- ์ค์๊ฐ ์์ํ (์ค๋์ ์ต๊ณ ์ ์)
- ์น๊ตฌ ์ด๋ ๋งํฌ ์์ฑ (๋ฐฐํ ๊ฒ์ ๊ณต์ )
- ๊ฒ์ ๋ฆฌํ๋ ์ด ๊ธฐ๋ฅ (๊ณผ๊ฑฐ ๊ธฐ๋ก ์ฌ์)
- ๋์ ๊ณผ์ ์์คํ (๋ฑ์ง ํ๋)
๊ธฐ์ ์คํ
- ๋ฐฑ์๋: ๊ทธ๋๋ณด๋5 SE (PHP 8.x), SQLite (๋ชจ๋ ์ ์ฉ DB)
- ํ๋ก ํธ์๋: ์์ HTML/CSS/JS (Vanilla), modern shell ๋์์ธ ํ ํฐ
- ๋ผ์ด๋ธ๋ฌ๋ฆฌ: Chart.js (CDN, ํต๊ณ ์ฐจํธ์ฉ)
- ๋ผ์ฐํ
: ํด๋ ๊ธฐ๋ฐ ์๋ ๋ผ์ฐํ
(
app/modules/games/*/index.php)
๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง
games.sqlite (G5_DATA_PATH.'/modules/games.sqlite')
CREATE TABLE IF NOT EXISTS play_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
game_type TEXT NOT NULL, -- 'classic', 'lucky7', 'polyhedral', ...
mb_id TEXT, -- ํ์ ID (๋นํ์์ NULL)
session_id TEXT, -- ๋นํ์ ์ธ์
ID
result_data TEXT NOT NULL, -- JSON ํ์ ๊ฒ์ ๊ฒฐ๊ณผ
score INTEGER DEFAULT 0, -- ์ ์
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_game_type ON play_records(game_type);
CREATE INDEX IF NOT EXISTS idx_mb_id ON play_records(mb_id);
CREATE INDEX IF NOT EXISTS idx_created_at ON play_records(created_at DESC);
ํด๋ ๊ตฌ์กฐ
app/modules/games/
โโโ index.php (๋ฉ์ธ ๊ฒ์ ์ปฌ๋ ์
ํ์ด์ง)
โโโ classic/
โ โโโ index.php (์ผ๋ฐ ์ฃผ์ฌ์ ๊ฒ์)
โโโ lucky7/
โ โโโ index.php (๋ญํค์ธ๋ธ ๊ฒ์)
โโโ polyhedral/
โ โโโ index.php (๋ค๋ฉด์ฒด ์ฃผ์ฌ์)
โโโ custom/
โ โโโ index.php (์ปค์คํ
์ฃผ์ฌ์)
โโโ battle/
โ โโโ index.php (์ฃผ์ฌ์ ๋ฐฐํ)
โโโ accumulate/
โ โโโ index.php (๋์ ์ ์ ๊ฒ์)
โโโ stats/
โ โโโ index.php (์ ์ฒด ํต๊ณ ๋์๋ณด๋)
โโโ myrecords/
โโโ index.php (๊ฐ์ธ ๊ธฐ๋ก ํ์ด์ง)
์ด ํ๋ก์ ํธ๋ ํด๋ ๊ธฐ๋ฐ ๋ผ์ฐํ ์ ๊ฐ๋ ฅํจ์ ์ฒดํํ๊ณ , SQLite ๋ชจ๋ DB ํจํด์ ์ตํ๋ฉฐ, modern shell ๋์์ธ ํ ํฐ์ผ๋ก ์ผ๊ด๋ UX๋ฅผ ๊ตฌํํ๋ ์ค์ ์ฐ์ต์ ๋๋ค.
์ ์ฝ ์กฐ๊ฑด
- โข ๊ทธ๋๋ณด๋5 SE ์ค์น๋ณธ ์์์ ์์ (https://github.com/gnuboard/g5se)
- โข ์์ PHP + g5se ์ปจ๋ฒค์ ๋ง ์ฌ์ฉ (Laravel/Composer/Vite/React/Vue ๊ธ์ง)
- โข ํด๋ ๊ธฐ๋ฐ ์๋ ๋ผ์ฐํ ํ์ฉ (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/games.sqlite'์ ์์ฑ
- โข prepared statement๋ก SQL injection ๋ฐฉ์ง
- โข htmlspecialchars()๋ก XSS ๋ฐฉ์ง
- โข ์ธ๋ถ ๋น๋ ๋๊ตฌ(npm/webpack) ์ฌ์ฉ ๊ธ์ง
- โข Chart.js๋ CDN์์ ๋ก๋ (<script src='https://cdn.jsdelivr.net/npm/chart.js@4'></script>)
- โข ํ์ ์ฐ๋: $is_member, $member['mb_id'] ํ์ฉ
- โข ๋นํ์์ session_id()๋ก ์์ ๊ธฐ๋ก ์ถ์
- โข AJAX ์์ฒญ์ Content-Type: application/json ์๋ต
- โข ์ฃผ์ฌ์ ์ ๋๋ฉ์ด์ ์ CSS 3D transform + @keyframes ์ฌ์ฉ
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
g5se๊ฐ ์ค์น๋ ๋๋ ํฐ๋ฆฌ(๋ฃจํธ์ `index.php`ยท`app/`์ด ์๋ ๊ณณ)์์ ์์
์ ์์ํฉ๋๋ค. (g5se ์ค์น๋ https://github.com/gnuboard/g5se ์ฐธ๊ณ )
**Step 1: ํ๋ก์ ํธ ํด๋ ๊ตฌ์กฐ ์์ฑ & ๋ฉ์ธ ํ์ด์ง ๊ณจ๊ฒฉ**
์๋ ํด๋ ๊ตฌ์กฐ๋ฅผ ์์ฑํ์ธ์:
```
app/modules/games/
โโโ index.php (๋ฉ์ธ ๊ฒ์ ์ปฌ๋ ์
)
โโโ classic/
โ โโโ index.php
โโโ lucky7/
โ โโโ index.php
โโโ polyhedral/
โ โโโ index.php
โโโ custom/
โ โโโ index.php
โโโ battle/
โ โโโ index.php
โโโ accumulate/
โ โโโ index.php
โโโ stats/
โ โโโ index.php
โโโ myrecords/
โโโ index.php
```
**app/modules/games/index.php** ํ์ผ์ ์์ฑํ๊ณ ๋ค์ ๋ด์ฉ์ ์์ฑํ์ธ์:
```php
<?php
include_once('./_common.php');
// SQLite DB ์ด๊ธฐํ
@mkdir(G5_DATA_PATH.'/modules', 0707, true);
$dbPath = G5_DATA_PATH.'/modules/games.sqlite';
$pdo = new PDO('sqlite:'.$dbPath);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// ํ
์ด๋ธ ์์ฑ
$pdo->exec("
CREATE TABLE IF NOT EXISTS play_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
game_type TEXT NOT NULL,
mb_id TEXT,
session_id TEXT,
result_data TEXT NOT NULL,
score INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
");
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_game_type ON play_records(game_type)");
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_mb_id ON play_records(mb_id)");
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_created_at ON play_records(created_at DESC)");
// ์ ์ฒด ๊ฒ์ ํต๊ณ ์กฐํ
$stmt = $pdo->query("
SELECT game_type, COUNT(*) as play_count
FROM play_records
GROUP BY game_type
ORDER BY play_count DESC
");
$gameStats = $stmt->fetchAll(PDO::FETCH_ASSOC);
$statsMap = [];
foreach ($gameStats as $row) {
$statsMap[$row['game_type']] = $row['play_count'];
}
// ๊ฒ์ ๋ชฉ๋ก ์ ์
$games = [
['id' => 'classic', 'name' => 'ํด๋์ ์ฃผ์ฌ์', 'desc' => '1~6 ์ซ์๊ฐ ๋์ค๋ ๊ธฐ๋ณธ ์ฃผ์ฌ์', 'icon' => '๐ฒ'],
['id' => 'lucky7', 'name' => '๋ญํค์ธ๋ธ', 'desc' => '๋ ์ฃผ์ฌ์ ํฉ์ด 7์ด๋ฉด ๋๋ฐ!', 'icon' => '๐'],
['id' => 'polyhedral', 'name' => '๋ค๋ฉด์ฒด ์ฃผ์ฌ์', 'desc' => 'd4, d6, d8, d10, d12, d20 ์ ํ', 'icon' => '๐ท'],
['id' => 'custom', 'name' => '์ปค์คํ
์ฃผ์ฌ์', 'desc' => '์ํ๋ ๋ฉด ๊ฐ์ ์ค์ ', 'icon' => 'โ๏ธ'],
['id' => 'battle', 'name' => '์ฃผ์ฌ์ ๋ฐฐํ', 'desc' => '2์ธ ๋๊ฒฐ ๋ชจ๋', 'icon' => 'โ๏ธ'],
['id' => 'accumulate', 'name' => '๋์ ์ ์', 'desc' => '๋ชฉํ ์ ์ ๋จผ์ ๋๋ฌํ๊ธฐ', 'icon' => '๐ฏ'],
];
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;">
<h1 style="font-size: 2rem; font-weight: 700; margin-bottom: 12px; color: var(--m-text);">๐ฒ ์ฃผ์ฌ์ ๊ฒ์ ์ปฌ๋ ์
</h1>
<p style="color: var(--m-text-muted); margin-bottom: 32px;">๋ค์ํ ์ฃผ์ฌ์ ๊ฒ์์ ์ฆ๊ธฐ๊ณ ๊ธฐ๋ก์ ๋จ๊ฒจ๋ณด์ธ์!</p>
<div class="game-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px;">
<?php foreach ($games as $game): ?>
<a href="/games/<?php echo htmlspecialchars($game['id']); ?>" class="game-card" style="text-decoration: none;">
<div style="background: var(--m-surface); border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); padding: 24px; transition: transform 0.2s, box-shadow 0.2s;">
<div style="font-size: 3rem; margin-bottom: 12px;"><?php echo $game['icon']; ?></div>
<h3 style="font-size: 1.25rem; font-weight: 600; color: var(--m-text); margin-bottom: 8px;"><?php echo htmlspecialchars($game['name']); ?></h3>
<p style="color: var(--m-text-muted); font-size: 0.9rem; margin-bottom: 12px;"><?php echo htmlspecialchars($game['desc']); ?></p>
<div style="display: flex; align-items: center; gap: 8px; color: var(--m-text-muted); font-size: 0.85rem;">
<span>โถ๏ธ ํ๋ ์ด ํ์:</span>
<strong style="color: var(--m-primary);"><?php echo isset($statsMap[$game['id']]) ? number_format($statsMap[$game['id']]) : 0; ?></strong>
</div>
</div>
</a>
<?php endforeach; ?>
</div>
<div style="margin-top: 40px; text-align: center;">
<a href="/games/stats" style="display: inline-block; background: var(--m-primary); color: white; padding: 12px 24px; border-radius: var(--m-radius-lg); text-decoration: none; font-weight: 600;">๐ ์ ์ฒด ํต๊ณ ๋ณด๊ธฐ</a>
<?php if ($is_member): ?>
<a href="/games/myrecords" style="display: inline-block; background: var(--m-surface); color: var(--m-text); padding: 12px 24px; border-radius: var(--m-radius-lg); text-decoration: none; font-weight: 600; margin-left: 12px; box-shadow: var(--m-shadow);">๐ ๋ด ๊ธฐ๋ก ๋ณด๊ธฐ</a>
<?php endif; ?>
</div>
</main>
<?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<style>
.game-card:hover > div {
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
}
</style>
<?php include_once('./tail.sub.php'); ?>
```
์ด์ `/games` URL๋ก ์ ์ํ๋ฉด ๊ฒ์ ์ปฌ๋ ์
๋ฉ์ธ ํ์ด์ง๊ฐ ํ์๋ฉ๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๋ฉ์ธ ๊ฒ์ ์ปฌ๋ ์ ํ์ด์ง๊ฐ ์์ฑ๋๊ณ , 6๊ฐ ๊ฒ์ ์นด๋๊ฐ ๊ทธ๋ฆฌ๋๋ก ๋ฐฐ์น๋ฉ๋๋ค. ๊ฐ ์นด๋์๋ ์์ด์ฝ, ์ด๋ฆ, ์ค๋ช , ํ๋ ์ด ํ์๊ฐ ํ์๋๋ฉฐ, ํธ๋ฒ ์ ์นด๋๊ฐ ์ด์ง ์ฌ๋ผ๊ฐ๋ ์ ๋๋ฉ์ด์ ์ด ์ ์ฉ๋ฉ๋๋ค. SQLite DB๊ฐ ์๋์ผ๋ก ์์ฑ๋๊ณ play_records ํ ์ด๋ธ์ด ์ด๊ธฐํ๋ฉ๋๋ค. ๋คํฌ๋ชจ๋ ์ ํ ์ ์์์ด ์๋์ผ๋ก ๋์๋ฉ๋๋ค.
**Step 2: ํด๋์ ์ฃผ์ฌ์ ๊ฒ์ ํ์ด์ง ๊ตฌํ**
**app/modules/games/classic/index.php** ํ์ผ์ ์์ฑํ๊ณ ๋ค์ ๋ด์ฉ์ ์์ฑํ์ธ์:
```php
<?php
include_once('./_common.php');
// SQLite ์ฐ๊ฒฐ
$dbPath = G5_DATA_PATH.'/modules/games.sqlite';
$pdo = new PDO('sqlite:'.$dbPath);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// AJAX ์์ฒญ ์ฒ๋ฆฌ (์ฃผ์ฌ์ ๊ตด๋ฆฌ๊ธฐ)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'roll') {
header('Content-Type: application/json');
$result = rand(1, 6);
$mbId = $is_member ? $member['mb_id'] : null;
$sessionId = session_id();
$stmt = $pdo->prepare("
INSERT INTO play_records (game_type, mb_id, session_id, result_data, score)
VALUES ('classic', ?, ?, ?, ?)
");
$stmt->execute([$mbId, $sessionId, json_encode(['dice' => $result]), $result]);
echo json_encode(['success' => true, 'result' => $result, 'id' => $pdo->lastInsertId()]);
exit;
}
// ์ต๊ทผ ๊ธฐ๋ก 10๊ฐ ์กฐํ
$stmt = $pdo->prepare("
SELECT result_data, created_at
FROM play_records
WHERE game_type = 'classic'
ORDER BY created_at DESC
LIMIT 10
");
$stmt->execute();
$recentRecords = $stmt->fetchAll(PDO::FETCH_ASSOC);
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;">
<a href="/games" style="color: var(--m-text-muted); text-decoration: none; margin-bottom: 20px; display: inline-block;">โ ๊ฒ์ ๋ชฉ๋ก์ผ๋ก</a>
<h1 style="font-size: 2rem; font-weight: 700; margin-bottom: 12px; color: var(--m-text);">๐ฒ ํด๋์ ์ฃผ์ฌ์</h1>
<p style="color: var(--m-text-muted); margin-bottom: 32px;">1๋ถํฐ 6๊น์ง ๋์ค๋ ์ผ๋ฐ ์ฃผ์ฌ์์
๋๋ค.</p>
<div style="max-width: 600px; margin: 0 auto;">
<div id="diceContainer" style="text-align: center; margin-bottom: 32px;">
<div id="dice" style="width: 120px; height: 120px; margin: 0 auto 24px; background: white; border-radius: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.2); display: flex; align-items: center; justify-content: center; font-size: 4rem; font-weight: 700; color: #333;">
?
</div>
<button id="rollBtn" style="background: var(--m-primary); color: white; padding: 14px 32px; border: none; border-radius: var(--m-radius-lg); font-size: 1.1rem; font-weight: 600; cursor: pointer; transition: transform 0.1s;">
์ฃผ์ฌ์ ๊ตด๋ฆฌ๊ธฐ
</button>
</div>
<div style="background: var(--m-surface); border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); padding: 20px;">
<h3 style="font-size: 1.1rem; font-weight: 600; color: var(--m-text); margin-bottom: 16px;">๐ ์ต๊ทผ ๊ธฐ๋ก</h3>
<div id="recordList">
<?php if (empty($recentRecords)): ?>
<p style="color: var(--m-text-muted); text-align: center;">์์ง ๊ธฐ๋ก์ด ์์ต๋๋ค.</p>
<?php else: ?>
<?php foreach ($recentRecords as $record): ?>
<?php $data = json_decode($record['result_data'], true); ?>
<div style="padding: 12px; border-bottom: 1px solid rgba(0,0,0,0.1); display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 1.5rem;">๐ฒ <?php echo htmlspecialchars($data['dice']); ?></span>
<span style="color: var(--m-text-muted); font-size: 0.85rem;"><?php echo date('m/d H:i', strtotime($record['created_at'])); ?></span>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
</main>
<?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<style>
@keyframes roll {
0% { transform: rotate(0deg); }
100% { transform: rotate(720deg); }
}
.rolling {
animation: roll 0.6s ease-out;
}
#rollBtn:hover {
transform: scale(1.05);
}
#rollBtn:active {
transform: scale(0.95);
}
</style>
<script>
const dice = document.getElementById('dice');
const rollBtn = document.getElementById('rollBtn');
const recordList = document.getElementById('recordList');
rollBtn.addEventListener('click', async () => {
rollBtn.disabled = true;
dice.textContent = '?';
dice.classList.add('rolling');
const formData = new FormData();
formData.append('action', 'roll');
try {
const res = await fetch('/games/classic', {
method: 'POST',
body: formData
});
const data = await res.json();
setTimeout(() => {
dice.classList.remove('rolling');
dice.textContent = data.result;
// ๊ธฐ๋ก ์ถ๊ฐ
const now = new Date();
const timeStr = `${String(now.getMonth()+1).padStart(2,'0')}/${String(now.getDate()).padStart(2,'0')} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`;
const newRecord = `
<div style="padding: 12px; border-bottom: 1px solid rgba(0,0,0,0.1); display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 1.5rem;">๐ฒ ${data.result}</span>
<span style="color: var(--m-text-muted); font-size: 0.85rem;">${timeStr}</span>
</div>
`;
recordList.insertAdjacentHTML('afterbegin', newRecord);
// 10๊ฐ ๋์ผ๋ฉด ๋ง์ง๋ง ์ ๊ฑฐ
const records = recordList.querySelectorAll('div');
if (records.length > 10) {
records[records.length - 1].remove();
}
rollBtn.disabled = false;
}, 600);
} catch (err) {
console.error(err);
alert('์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
rollBtn.disabled = false;
dice.classList.remove('rolling');
}
});
</script>
<?php include_once('./tail.sub.php'); ?>
```
์ด์ `/games/classic` URL๋ก ์ ์ํ๋ฉด ํด๋์ ์ฃผ์ฌ์ ๊ฒ์์ ํ๋ ์ดํ ์ ์์ต๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: ํด๋์ ์ฃผ์ฌ์ ๊ฒ์ ํ์ด์ง๊ฐ ์์ฑ๋ฉ๋๋ค. ์ฃผ์ฌ์ ๊ตด๋ฆฌ๊ธฐ ๋ฒํผ์ ํด๋ฆญํ๋ฉด ์ฃผ์ฌ์๊ฐ ํ์ ์ ๋๋ฉ์ด์ ๊ณผ ํจ๊ป ๊ตด๋ฌ๊ฐ๊ณ , 0.6์ด ํ ๊ฒฐ๊ณผ๊ฐ ํ์๋ฉ๋๋ค. ๊ฒฐ๊ณผ๋ SQLite DB์ ์ ์ฅ๋๊ณ , ์ต๊ทผ 10๊ฐ ๊ธฐ๋ก์ด ์ค์๊ฐ์ผ๋ก ์ ๋ฐ์ดํธ๋ฉ๋๋ค. AJAX๋ก ํ์ด์ง ์๋ก๊ณ ์นจ ์์ด ์๋ํฉ๋๋ค.
**Step 3: ๋ญํค์ธ๋ธ ๊ฒ์ & ๋ค๋ฉด์ฒด ์ฃผ์ฌ์ ๊ฒ์ ๊ตฌํ**
**app/modules/games/lucky7/index.php** ํ์ผ์ ์์ฑํ์ธ์:
```php
<?php
include_once('./_common.php');
$dbPath = G5_DATA_PATH.'/modules/games.sqlite';
$pdo = new PDO('sqlite:'.$dbPath);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'roll') {
header('Content-Type: application/json');
$dice1 = rand(1, 6);
$dice2 = rand(1, 6);
$sum = $dice1 + $dice2;
$isLucky = ($sum === 7);
$mbId = $is_member ? $member['mb_id'] : null;
$sessionId = session_id();
$stmt = $pdo->prepare("
INSERT INTO play_records (game_type, mb_id, session_id, result_data, score)
VALUES ('lucky7', ?, ?, ?, ?)
");
$stmt->execute([$mbId, $sessionId, json_encode(['dice1' => $dice1, 'dice2' => $dice2, 'sum' => $sum, 'lucky' => $isLucky]), $isLucky ? 100 : 0]);
echo json_encode(['success' => true, 'dice1' => $dice1, 'dice2' => $dice2, 'sum' => $sum, 'lucky' => $isLucky]);
exit;
}
$stmt = $pdo->query("SELECT result_data, created_at FROM play_records WHERE game_type = 'lucky7' ORDER BY created_at DESC LIMIT 10");
$recentRecords = $stmt->fetchAll(PDO::FETCH_ASSOC);
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;">
<a href="/games" style="color: var(--m-text-muted); text-decoration: none; margin-bottom: 20px; display: inline-block;">โ ๊ฒ์ ๋ชฉ๋ก์ผ๋ก</a>
<h1 style="font-size: 2rem; font-weight: 700; margin-bottom: 12px; color: var(--m-text);">๐ ๋ญํค์ธ๋ธ</h1>
<p style="color: var(--m-text-muted); margin-bottom: 32px;">๋ ์ฃผ์ฌ์์ ํฉ์ด 7์ด๋ฉด ๋๋ฐ! ๐ฐ</p>
<div style="max-width: 600px; margin: 0 auto;">
<div style="text-align: center; margin-bottom: 32px;">
<div style="display: flex; gap: 20px; justify-content: center; margin-bottom: 24px;">
<div id="dice1" class="dice-box">?</div>
<div id="dice2" class="dice-box">?</div>
</div>
<div id="sumDisplay" style="font-size: 2rem; font-weight: 700; color: var(--m-text); margin-bottom: 12px;">ํฉ๊ณ: -</div>
<div id="resultMsg" style="font-size: 1.2rem; font-weight: 600; min-height: 30px;"></div>
<button id="rollBtn" class="roll-button">์ฃผ์ฌ์ ๊ตด๋ฆฌ๊ธฐ</button>
</div>
<div style="background: var(--m-surface); border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); padding: 20px;">
<h3 style="font-size: 1.1rem; font-weight: 600; color: var(--m-text); margin-bottom: 16px;">๐ ์ต๊ทผ ๊ธฐ๋ก</h3>
<div id="recordList">
<?php if (empty($recentRecords)): ?>
<p style="color: var(--m-text-muted); text-align: center;">์์ง ๊ธฐ๋ก์ด ์์ต๋๋ค.</p>
<?php else: ?>
<?php foreach ($recentRecords as $record): ?>
<?php $data = json_decode($record['result_data'], true); ?>
<div style="padding: 12px; border-bottom: 1px solid rgba(0,0,0,0.1); display: flex; justify-content: space-between; align-items: center;">
<span><?php echo $data['dice1']; ?> + <?php echo $data['dice2']; ?> = <?php echo $data['sum']; ?> <?php echo $data['lucky'] ? '๐ ๋ญํค!' : ''; ?></span>
<span style="color: var(--m-text-muted); font-size: 0.85rem;"><?php echo date('m/d H:i', strtotime($record['created_at'])); ?></span>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
</main>
<?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<style>
.dice-box {
width: 100px;
height: 100px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
font-weight: 700;
color: #333;
}
.roll-button {
background: var(--m-primary);
color: white;
padding: 14px 32px;
border: none;
border-radius: var(--m-radius-lg);
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.1s;
margin-top: 20px;
}
.roll-button:hover { transform: scale(1.05); }
.roll-button:active { transform: scale(0.95); }
@keyframes roll { 0% { transform: rotate(0deg); } 100% { transform: rotate(720deg); } }
.rolling { animation: roll 0.6s ease-out; }
</style>
<script>
const dice1 = document.getElementById('dice1');
const dice2 = document.getElementById('dice2');
const sumDisplay = document.getElementById('sumDisplay');
const resultMsg = document.getElementById('resultMsg');
const rollBtn = document.getElementById('rollBtn');
const recordList = document.getElementById('recordList');
rollBtn.addEventListener('click', async () => {
rollBtn.disabled = true;
dice1.textContent = '?';
dice2.textContent = '?';
sumDisplay.textContent = 'ํฉ๊ณ: -';
resultMsg.textContent = '';
dice1.classList.add('rolling');
dice2.classList.add('rolling');
const formData = new FormData();
formData.append('action', 'roll');
try {
const res = await fetch('/games/lucky7', { method: 'POST', body: formData });
const data = await res.json();
setTimeout(() => {
dice1.classList.remove('rolling');
dice2.classList.remove('rolling');
dice1.textContent = data.dice1;
dice2.textContent = data.dice2;
sumDisplay.textContent = `ํฉ๊ณ: ${data.sum}`;
if (data.lucky) {
resultMsg.innerHTML = '๐ <span style="color: #FFD700;">๋ญํค ์ธ๋ธ! ๋๋ฐ!</span>';
} else {
resultMsg.innerHTML = '<span style="color: var(--m-text-muted);">๋ค์ ๊ธฐํ์...</span>';
}
const now = new Date();
const timeStr = `${String(now.getMonth()+1).padStart(2,'0')}/${String(now.getDate()).padStart(2,'0')} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`;
const newRecord = `
<div style="padding: 12px; border-bottom: 1px solid rgba(0,0,0,0.1); display: flex; justify-content: space-between; align-items: center;">
<span>${data.dice1} + ${data.dice2} = ${data.sum} ${data.lucky ? '๐ ๋ญํค!' : ''}</span>
<span style="color: var(--m-text-muted); font-size: 0.85rem;">${timeStr}</span>
</div>
`;
recordList.insertAdjacentHTML('afterbegin', newRecord);
const records = recordList.querySelectorAll('div');
if (records.length > 10) records[records.length - 1].remove();
rollBtn.disabled = false;
}, 600);
} catch (err) {
console.error(err);
alert('์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
rollBtn.disabled = false;
}
});
</script>
<?php include_once('./tail.sub.php'); ?>
```
**app/modules/games/polyhedral/index.php** ํ์ผ๋ ์์ฑํ์ธ์ (๋ค๋ฉด์ฒด ์ฃผ์ฌ์ - d4, d6, d8, d10, d12, d20 ์ ํ ๊ฐ๋ฅ). ์ฝ๋๋ lucky7์ ์ ์ฌํ๋, ์ฃผ์ฌ์ ์ข
๋ฅ ์ ํ `<select>` ์ถ๊ฐํ๊ณ `rand(1, $sides)` ๋ฐฉ์์ผ๋ก ๊ตฌํํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๋ญํค์ธ๋ธ ๊ฒ์์ด ์์ฑ๋ฉ๋๋ค. ๋ ์ฃผ์ฌ์๊ฐ ๋์์ ๊ตด๋ฌ๊ฐ๊ณ , ํฉ์ด 7์ด๋ฉด '๋ญํค ์ธ๋ธ!' ๋ฉ์์ง๊ฐ ํฉ๊ธ์์ผ๋ก ํ์๋ฉ๋๋ค. ๋ค๋ฉด์ฒด ์ฃผ์ฌ์ ๊ฒ์๋ ์ ๋ ํธ ๋ฐ์ค๋ก ์ฃผ์ฌ์ ์ข ๋ฅ(d4~d20)๋ฅผ ์ ํํ๋ฉด ํด๋น ๋ฒ์์ ๋์๊ฐ ์์ฑ๋ฉ๋๋ค.
**Step 4: ์ปค์คํ
์ฃผ์ฌ์ & ์ฃผ์ฌ์ ๋ฐฐํ ๊ฒ์ ๊ตฌํ**
**app/modules/games/custom/index.php** ํ์ผ์ ์์ฑํ์ธ์:
์ฌ์ฉ์๊ฐ ์ฌ๋ผ์ด๋๋ก ์ฃผ์ฌ์ ๋ฉด ๊ฐ์(2~100)๋ฅผ ์ ํํ๊ณ , ๊ทธ ๋ฒ์ ๋ด์์ ๋๋ค ์ซ์๊ฐ ๋์ค๋๋ก ๊ตฌํํ์ธ์. UI๋ ์ฌ๋ผ์ด๋ + ํ์ฌ ๊ฐ ํ์ + ์ฃผ์ฌ์ ๊ตด๋ฆฌ๊ธฐ ๋ฒํผ. ๊ฒฐ๊ณผ๋ ํฐ ์ซ์๋ก ํ์ํ๊ณ ๊ธฐ๋ก์ "์ปค์คํ
d{sides}: {result}" ํํ๋ก ์ ์ฅ.
**app/modules/games/battle/index.php** ํ์ผ์ ์์ฑํ์ธ์:
2์ธ ๋๊ฒฐ ๋ชจ๋์
๋๋ค. ๊ฐ ํ๋ ์ด์ด(Player 1, Player 2)๊ฐ ์ฃผ์ฌ์๋ฅผ ๊ตด๋ฆฌ๊ณ , ๋ ํฐ ์ซ์๊ฐ ๋์จ ์ชฝ์ด ์น๋ฆฌ. UI๋ ๋ ๊ฐ์ ์ฃผ์ฌ์ + "๋ฐฐํ ์์" ๋ฒํผ. ๊ฒฐ๊ณผ๋ "Player 1: 5 vs Player 2: 3 โ Player 1 ์น๋ฆฌ!" ํํ๋ก ํ์ํ๊ณ , ์น์ ์ด๋ฆ์ ๊ฐ์กฐ(์์ ๋ณ๊ฒฝ). ๊ธฐ๋ก์๋ `result_data`์ `{"p1": 5, "p2": 3, "winner": 1}` JSON ์ ์ฅ.
๋ ํ์ด์ง ๋ชจ๋ modern shell + ๋์์ธ ํ ํฐ + AJAX ํจํด ์ ์งํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ปค์คํ ์ฃผ์ฌ์ ๊ฒ์์์ ์ฌ๋ผ์ด๋๋ฅผ ์์ง์ด๋ฉด "d{sides}" ํ์๊ฐ ์ค์๊ฐ ์ ๋ฐ์ดํธ๋๊ณ , ์ฃผ์ฌ์๋ฅผ ๊ตด๋ฆฌ๋ฉด ์ ํํ ๋ฒ์ ๋ด์ ์ซ์๊ฐ ๋์ต๋๋ค. ์ฃผ์ฌ์ ๋ฐฐํ ๊ฒ์์์๋ ๋ ์ฃผ์ฌ์๊ฐ ๋์์ ๊ตด๋ฌ๊ฐ๊ณ , ์น์๊ฐ ๊ฒฐ์ ๋๋ฉด ์น์ ์ด๋ฆ์ด ํ๋์(๋๋ ์ฃผ ์์)์ผ๋ก ๊ฐ์กฐ ํ์๋ฉ๋๋ค. ๋ ๊ฒ์ ๋ชจ๋ SQLite์ ๊ธฐ๋ก์ด ์ ์ฅ๋๊ณ ์ต๊ทผ 10๊ฐ ๊ธฐ๋ก์ด ํ์๋ฉ๋๋ค.
**Step 5: ๋์ ์ ์ ๊ฒ์ & ์ ์ฒด ํต๊ณ ๋์๋ณด๋ ๊ตฌํ**
**app/modules/games/accumulate/index.php** ํ์ผ์ ์์ฑํ์ธ์:
๋ชฉํ ์ ์(์: 50์ )๋ฅผ ๋จผ์ ๋ฌ์ฑํ๋ ๊ฒ์์
๋๋ค. ์ฃผ์ฌ์๋ฅผ ๊ตด๋ฆด ๋๋ง๋ค ๋์จ ์ซ์๋งํผ ์ ์ ๋์ . ํ์ฌ ์ ์๋ฅผ ํ๋ฉด์ ํฐ ์ซ์๋ก ํ์ํ๊ณ , ๋ชฉํ ๋๋ฌ ์ "๐ ๋ชฉํ ๋ฌ์ฑ! {roll_count}๋ฒ ๋ง์ ์ฑ๊ณต!" ๋ฉ์์ง ํ์. "๋ฆฌ์
" ๋ฒํผ์ผ๋ก ๋ค์ ์์. ๊ธฐ๋ก์๋ `{"rolls": 12, "final_score": 53}` ํํ๋ก ์ ์ฅ.
**app/modules/games/stats/index.php** ํ์ผ์ ์์ฑํ์ธ์:
์ ์ฒด ํต๊ณ ๋์๋ณด๋์
๋๋ค:
1. **๊ฒ์๋ณ ํ๋ ์ด ํ์ ํ์ด ์ฐจํธ** (Chart.js ์ฌ์ฉ)
- `SELECT game_type, COUNT(*) FROM play_records GROUP BY game_type`
- ์ฐจํธ๋ `<canvas id="gameChart"></canvas>`์ ๋ ๋๋ง
2. **์ต๊ทผ 100๊ฐ ํ๋ ์ด ํ์๋ผ์ธ**
- ๊ฒ์ ํ์
, ๊ฒฐ๊ณผ ์์ฝ, ์๊ฐ ํ์
- ๋ฌดํ ์คํฌ๋กค (์ ํ: 100๊ฐ ์ดํ ๋๋ณด๊ธฐ ๋ฒํผ)
3. **์ฃผ์ฌ์ ๋ ๋ถํฌ ํ์คํ ๊ทธ๋จ** (ํด๋์ ์ฃผ์ฌ์ ํ์ )
- `SELECT result_data FROM play_records WHERE game_type='classic'`
- JSON ํ์ฑํ์ฌ 1~6 ๊ฐ ์ซ์ ์ถํ ํ์ ์ง๊ณ
- ๋ง๋ ๊ทธ๋ํ๋ก ํ์
Chart.js๋ CDN์ผ๋ก ๋ก๋:
```html
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
```
์ฐจํธ ์์ฑ ์์:
```javascript
const ctx = document.getElementById('gameChart').getContext('2d');
new Chart(ctx, {
type: 'pie',
data: {
labels: ['ํด๋์', '๋ญํค์ธ๋ธ', '๋ค๋ฉด์ฒด', '์ปค์คํ
', '๋ฐฐํ', '๋์ '],
datasets: [{
data: [120, 85, 60, 45, 30, 20],
backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40']
}]
}
});
```
์ค์ ๋ฐ์ดํฐ๋ PHP์์ JSON์ผ๋ก ์ถ๋ ฅํ๊ณ JS์์ ํ์ฑํ์ฌ ์ฌ์ฉํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๋์ ์ ์ ๊ฒ์์์ ์ฃผ์ฌ์๋ฅผ ๊ตด๋ฆด ๋๋ง๋ค ์ ์๊ฐ ๋์ ๋๊ณ , ๋ชฉํ ์ ์ ๋๋ฌ ์ ์ถํ ๋ฉ์์ง๊ฐ ํ์๋ฉ๋๋ค. ์ ์ฒด ํต๊ณ ๋์๋ณด๋์๋ ๊ฒ์๋ณ ํ๋ ์ด ํ์ ํ์ด ์ฐจํธ๊ฐ ํ์๋๊ณ , ์ต๊ทผ 100๊ฐ ํ๋ ์ด ๊ธฐ๋ก์ด ํ์๋ผ์ธ ํํ๋ก ๋์ด๋ฉ๋๋ค. ํด๋์ ์ฃผ์ฌ์์ ๋ ๋ถํฌ ํ์คํ ๊ทธ๋จ๋ ๋ง๋ ๊ทธ๋ํ๋ก ์๊ฐํ๋ฉ๋๋ค.
**Step 6: ๊ฐ์ธ ๊ธฐ๋ก ํ์ด์ง & UX ์ธํฐ๋์
๊ฐํ**
**app/modules/games/myrecords/index.php** ํ์ผ์ ์์ฑํ์ธ์:
๋ก๊ทธ์ธํ ํ์๋ง ์ ๊ทผ ๊ฐ๋ฅ (`if (!$is_member) { alert_close('๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค.'); }`). ์ฌ์ฉ์์ ๊ฒ์๋ณ ํต๊ณ๋ฅผ ํ์:
1. **๊ฒ์๋ณ ํ๋ ์ด ํ์ & ์ต๊ณ ์ ์**
- `SELECT game_type, COUNT(*) as play_count, MAX(score) as max_score FROM play_records WHERE mb_id = ? GROUP BY game_type`
2. **์ ์ฒด ํ๋ ์ด ๊ธฐ๋ก ํ
์ด๋ธ**
- ๊ฒ์ ํ์
, ๊ฒฐ๊ณผ, ์ ์, ๋ ์ง ํ์
- ๊ฐ ํ์ "์ญ์ " ๋ฒํผ (AJAX๋ก DELETE ์์ฒญ)
3. **์ญ์ ๊ธฐ๋ฅ ๊ตฌํ**
- POST action=delete, id={record_id}
- `DELETE FROM play_records WHERE id = ? AND mb_id = ?` (๋ณธ์ธ ๊ธฐ๋ก๋ง ์ญ์ ๊ฐ๋ฅ)
- ์ญ์ ํ ํด๋น ํ fade-out ์ ๋๋ฉ์ด์
**UX ์ธํฐ๋์
๊ฐํ:**
1. **์ฃผ์ฌ์ ๊ตด๋ฆฌ๊ธฐ ์ฌ์ด๋ ํจ๊ณผ (์ ํ ์ฌํญ)**
- Web Audio API๋ก ๊ฐ๋จํ "ํ" ์๋ฆฌ ์ฌ์
- ๋๋ `<audio>` ํ๊ทธ + ๋ฌด๋ฃ ํจ๊ณผ์ ํ์ผ
2. **๊ฒฐ๊ณผ ๋ชจ๋ฌ ํ์
**
- ์ฃผ์ฌ์ ๊ฒฐ๊ณผ๊ฐ ๋์ค๋ฉด ํ๋ฉด ์ค์์ ๋ชจ๋ฌ ํ์
- "๋ค์ ํ๊ธฐ" / "๊ธฐ๋ก ๋ณด๊ธฐ" ๋ฒํผ
- ๋ชจ๋ฌ ์ธ๋ถ ํด๋ฆญ ์ ๋ซ๊ธฐ
3. **ํธ๋ฒ ์ ์นด๋ ํธํธ ํจ๊ณผ**
- ๋ฉ์ธ ํ์ด์ง ๊ฒ์ ์นด๋์ ๋ง์ฐ์ค ์ฌ๋ฆฌ๋ฉด 3D ํธํธ
- vanilla-tilt.js ๋๋ ์์ CSS transform ์ฌ์ฉ
4. **๋ฌดํ ์คํฌ๋กค (ํต๊ณ ํ์ด์ง)**
- Intersection Observer API๋ก ์คํฌ๋กค ๋ ๊ฐ์ง
- ์ถ๊ฐ ๋ฐ์ดํฐ AJAX ๋ก๋
๋ชจ๋ ํ์ด์ง์ modern shell ์ ์ฉ ์ ์งํ๊ณ , ๋คํฌ๋ชจ๋์์๋ ์ ๋ณด์ด๋๋ก ์์ ํ ํฐ ์ฌ์ฉํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๊ฐ์ธ ๊ธฐ๋ก ํ์ด์ง๊ฐ ์์ฑ๋์ด ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ ์์ ์ ๊ฒ์๋ณ ํต๊ณ์ ์ ์ฒด ํ๋ ์ด ๊ธฐ๋ก์ ํ์ธํ ์ ์์ต๋๋ค. ๊ธฐ๋ก ์ญ์ ๋ฒํผ์ ํด๋ฆญํ๋ฉด AJAX๋ก ์ญ์ ๋๊ณ ํด๋น ํ์ด fade-out๋ฉ๋๋ค. ์ฃผ์ฌ์ ๊ฒฐ๊ณผ๊ฐ ๋์ฌ ๋ ๋ชจ๋ฌ ํ์ ์ด ํ์๋๊ณ , ๋ฉ์ธ ํ์ด์ง ๊ฒ์ ์นด๋์ ๋ง์ฐ์ค๋ฅผ ์ฌ๋ฆฌ๋ฉด 3D ํธํธ ํจ๊ณผ๊ฐ ์ ์ฉ๋ฉ๋๋ค. ํต๊ณ ํ์ด์ง์์ ์คํฌ๋กค์ ๋ด๋ฆฌ๋ฉด ์ถ๊ฐ ๊ธฐ๋ก์ด ์๋์ผ๋ก ๋ก๋๋ฉ๋๋ค.
**Step 7: ์ต์ข ๋ง๋ฌด๋ฆฌ & ์ ํ ๊ธฐ๋ฅ ๊ตฌํ** **์ต์ข ์ ๊ฒ ์ฌํญ:** 1. **๋ชจ๋ ํ์ด์ง modern shell ์ ์ฉ ํ์ธ** - `_head.inc.php`, `m-shell`, `_nav.inc.php`, `m-container`, `_footer.inc.php` ํฌํจ ์ฌ๋ถ - ๋์์ธ ํ ํฐ (`var(--m-surface)` ๋ฑ) ์ฌ์ฉ ์ฌ๋ถ 2. **SQLite prepared statement ํ์ธ** - ๋ชจ๋ SQL ์ฟผ๋ฆฌ๊ฐ `->prepare()` + `->execute([])` ๋ฐฉ์ ์ฌ์ฉ - ์ฌ์ฉ์ ์ ๋ ฅ์ด ์ง์ ์ฟผ๋ฆฌ์ ๋ค์ด๊ฐ์ง ์๋๋ก 3. **XSS ๋ฐฉ์ง ํ์ธ** - ๋ชจ๋ ์ฌ์ฉ์ ๋ฐ์ดํฐ ์ถ๋ ฅ ์ `htmlspecialchars()` ์ฌ์ฉ 4. **๋คํฌ๋ชจ๋ ํ ์คํธ** - ์ฌ์ดํธ ์๋จ ๋คํฌ๋ชจ๋ ํ ๊ธ ๋ฒํผ ํด๋ฆญ ํ ๋ชจ๋ ๊ฒ์ ํ์ด์ง ํ์ธ - ํ ์คํธ/๋ฐฐ๊ฒฝ์ด ์ ๋ณด์ด๋์ง ๊ฒ์ฆ **์ ํ ๊ธฐ๋ฅ ๊ตฌํ (๋ณด๋์ค):** 1. **์ฃผ์ฌ์ ์คํจ ์ปค์คํฐ๋ง์ด์ง** - ์ค์ ํ์ด์ง (`/games/settings`) ์ถ๊ฐ - ์ฃผ์ฌ์ ์์(๋นจ๊ฐ, ํ๋, ์ด๋ก ๋ฑ) ์ ํ - localStorage์ ์ ์ฅํ๊ณ ๋ชจ๋ ๊ฒ์์ ์ ์ฉ 2. **์ค์๊ฐ ์์ํ** - `/games/leaderboard` ํ์ด์ง ์ถ๊ฐ - ์ค๋์ ์ต๊ณ ์ ์ TOP 10 (๊ฒ์๋ณ) - ์๋ ์๋ก๊ณ ์นจ (30์ด๋ง๋ค) 3. **์น๊ตฌ ์ด๋ ๋งํฌ** - ๋ฐฐํ ๊ฒ์์์ "์น๊ตฌ ์ด๋" ๋ฒํผ - ๊ณ ์ ์ธ์ ID ์์ฑํ์ฌ URL์ ํฌํจ (`/games/battle?session=abc123`) - ๊ฐ์ ์ธ์ ID๋ก ์ ์ํ ์ฌ์ฉ์๋ผ๋ฆฌ ๊ฒฐ๊ณผ ๊ณต์ 4. **๋์ ๊ณผ์ ์์คํ ** - "์ฒซ ๋ญํค์ธ๋ธ ๋ฌ์ฑ", "๋์ ๊ฒ์ 100ํ ํ๋ ์ด" ๋ฑ ๋ฑ์ง - ๋ฌ์ฑ ์ ๋ชจ๋ฌ ํ์ ์ผ๋ก ์ถํ - ๋ด ๊ธฐ๋ก ํ์ด์ง์ ๋ฑ์ง ๋ชฉ๋ก ํ์ **๋ฐฐํฌ ์ ์ฒดํฌ๋ฆฌ์คํธ:** - [ ] ๋ชจ๋ ๊ฒ์ ํ์ด์ง ์ ์ ์๋ - [ ] AJAX ์์ฒญ ์๋ฌ ํธ๋ค๋ง - [ ] ๋ชจ๋ฐ์ผ ๋ฐ์ํ ํ์ธ (๋ฏธ๋์ด ์ฟผ๋ฆฌ) - [ ] ๋ก๋ฉ ์คํผ๋ ์ถ๊ฐ (AJAX ์์ฒญ ์ค) - [ ] 404 ํ์ด์ง ์ฒ๋ฆฌ (์กด์ฌํ์ง ์๋ ๊ฒ์ ๊ฒฝ๋ก) - [ ] ์ฑ๋ฅ ์ต์ ํ (์ฟผ๋ฆฌ ์ธ๋ฑ์ค, ์ด๋ฏธ์ง ์์ถ) ์ถํํฉ๋๋ค! ๐ ๊ทธ๋๋ณด๋5 SE ์์์ ๋์ํ๋ ์์ ํ ์ฃผ์ฌ์ ๊ฒ์ ์ปฌ๋ ์ ํ๋ซํผ์ด ์์ฑ๋์์ต๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๋ชจ๋ ํ์ด์ง๊ฐ modern shell๋ก ํต์ผ๋๊ณ , ๋คํฌ๋ชจ๋๊ฐ ์ ์ ์๋ํ๋ฉฐ, SQLite ๋ณด์์ด ๊ฐํ๋์์ต๋๋ค. ์ ํ ๊ธฐ๋ฅ ์ค ์ผ๋ถ๋ฅผ ๊ตฌํํ๋ฉด ์ฌ์ฉ์ ๊ฒฝํ์ด ๋์ฑ ํ๋ถํด์ง๋๋ค. ์ต์ข ์ ์ผ๋ก 6๊ฐ ์ด์์ ์ฃผ์ฌ์ ๊ฒ์, ํต๊ณ ๋์๋ณด๋, ๊ฐ์ธ ๊ธฐ๋ก ํ์ด์ง๊ฐ ๋ชจ๋ ์ฐ๋๋์ด ์๋ํ๋ ์์ ํ ํ๋ซํผ์ด ์์ฑ๋ฉ๋๋ค.
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!