๐ฎ g5se ๋ฏธ๋ ๊ฒ์ ์ผํฐ - ๋ธ๋ผ์ฐ์ ์์ผ์ด๋ ๋ชจ์
g5se-browser-arcade
๋ฌธ์ ์ค๋ช
๐ฏ ํ๋ก์ ํธ ๊ฐ์
g5se ์์์ ๋์ํ๋ ๊ฐ๋ฒผ์ด ๋ธ๋ผ์ฐ์ ๊ฒ์ ๋ชจ์ ํ์ด์ง๋ฅผ ๋ง๋ญ๋๋ค. ์ฌ๋ฌ ๋ฏธ๋ ๊ฒ์์ ํ ๊ณณ์ ๋ชจ์๋๊ณ , ๊ฐ ๊ฒ์์ ์ต๊ณ ๊ธฐ๋ก์ SQLite DB์ ์ ์ฅํฉ๋๋ค. ์ฌ์ฉ์๋ ๊ฒ์ ๋ชฉ๋ก์ ๋ณด๊ณ ์ํ๋ ๊ฒ์์ ์ ํํด์ ํ๋ ์ดํ ์ ์์ต๋๋ค.
๐ ํ์ ๊ธฐ๋ฅ
1. ๊ฒ์ ๋ชฉ๋ก ๋ฉ์ธ ํ์ด์ง
- ์นด๋ ๊ทธ๋ฆฌ๋ ๋ ์ด์์์ผ๋ก ๊ฒ์ ๋ชฉ๋ก ํ์
- ๊ฐ ์นด๋: ๊ฒ์ ์ ๋ชฉ, ๊ฐ๋จํ ์ค๋ช , ์ธ๋ค์ผ ์์ด์ฝ(์ด๋ชจ์ง), ์ต๊ณ ๊ธฐ๋ก
- ์นด๋ ํด๋ฆญ ์ ํด๋น ๊ฒ์ ํ์ด์ง๋ก ์ด๋
- modern shell ์ ์ฉ์ผ๋ก ์ฌ์ดํธ nav/footer ํต์ผ
2. ๊ฒ์ 1: ์ซ์ ๋ง์ถ๊ธฐ (Number Guess)
- 1~100 ์ฌ์ด ๋๋ค ์ซ์๋ฅผ ๋ง์ถ๋ ๊ฒ์
- ์ ๋ ฅ ํ "Up" ๋๋ "Down" ํํธ ์ ๊ณต
- ์๋ ํ์ ์นด์ดํธ
- ์ ๋ต ์ ์๋ ํ์๋ฅผ DB์ ์ ์ฅ (์ต์ ์๋ ํ์๊ฐ ์ต๊ณ ๊ธฐ๋ก)
- "๋ค์ ํ๊ธฐ" ๋ฒํผ์ผ๋ก ๊ฒ์ ๋ฆฌ์
3. ๊ฒ์ 2: ์๊น ๊ธฐ์ตํ๊ธฐ (Color Memory)
- 4๊ฐ์ ์๊น ๋ฒํผ(๋นจ๊ฐ, ํ๋, ๋ ธ๋, ์ด๋ก)
- ๋๋ค ์์๋ก ์๊น์ด ๊น๋นก์ด๋ฉฐ ํจํด ํ์
- ์ฌ์ฉ์๊ฐ ๊ฐ์ ์์๋ก ํด๋ฆญํด์ผ ํจ
- ๋ผ์ด๋๋ง๋ค ํจํด ๊ธธ์ด +1
- ์คํจ ์ ๋ผ์ด๋ ์๋ฅผ DB์ ์ ์ฅ (์ต๋ ๋ผ์ด๋๊ฐ ์ต๊ณ ๊ธฐ๋ก)
4. ๊ฒ์ 3: ๋ฐ์์๋ ํ ์คํธ (Reaction Speed)
- "์ค๋น" ํ๋ฉด โ ๋๋ค ๋๋ ์ด ํ "ํด๋ฆญ!" ํ๋ฉด์ผ๋ก ์ ํ
- ํ๋ฉด์ด ๋ฐ๋๋ ์๊ฐ๋ถํฐ ํด๋ฆญ๊น์ง ๊ฑธ๋ฆฐ ์๊ฐ(ms) ์ธก์
- 3ํ ์ธก์ ํ ํ๊ท ๋ฐ์์๋๋ฅผ DB์ ์ ์ฅ (์ต์ ์๊ฐ์ด ์ต๊ณ ๊ธฐ๋ก)
5. ์ต๊ณ ๊ธฐ๋ก ๊ด๋ฆฌ
- ๊ฐ ๊ฒ์๋ณ๋ก ์ต๊ณ ๊ธฐ๋ก 1๊ฐ๋ง ์ ์ง (๊ฐฑ์ ์ UPDATE)
- ๋ฉ์ธ ํ์ด์ง ์นด๋์ ์ต๊ณ ๊ธฐ๋ก ํ์
- ๊ฒ์ ํ์ด์ง์๋ ํ์ฌ ์ต๊ณ ๊ธฐ๋ก ํ์
๐จ ๋์์ธ ์๊ตฌ์ฌํญ
- modern shell์ CSS ๋ณ์(
--m-surface,--m-text,--m-shadow๋ฑ) ํ์ฉ์ผ๋ก ๋คํฌ๋ชจ๋ ์๋ ๋์ - ๊ฒ์ ์นด๋๋ hover ์ ์ด์ง ๋ ์ค๋ฅด๋ ํจ๊ณผ
- ๊ฒ์ ํ๋ ์ด ์์ญ์ ์ค์ ์ ๋ ฌ, ์ต๋ ๋๋น 600px
- ๋ฒํผ์ ํผ์งํ๊ณ ํฐ์นํ๊ธฐ ํธํ ํฌ๊ธฐ(์ต์ 48px ๋์ด)
- ๊ฒ์๋ณ ์๊ฐ์ ์ ์ฒด์ฑ: ์ซ์ ๋ง์ถ๊ธฐ(๋ณด๋ผ), ์๊น ๊ธฐ์ต(๋ฌด์ง๊ฐ), ๋ฐ์์๋(์ฃผํฉ) ๋ฑ ๊ฐ์กฐ ์์ ์ฌ์ฉ ๊ฐ๋ฅ
๐๏ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง
CREATE TABLE IF NOT EXISTS game_records (
game_id TEXT PRIMARY KEY, -- 'number_guess', 'color_memory', 'reaction_speed'
best_score INTEGER NOT NULL,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
๐ ๋ณด๋์ค ๊ธฐ๋ฅ (์ ํ)
- ๊ฒ์ 4 ์ถ๊ฐ: ํ์ดํ ์๋ ํ ์คํธ (์ฃผ์ด์ง ๋ฌธ์ฅ์ ์ผ๋ง๋ ๋นจ๋ฆฌ ์ ํํ ํ์ดํํ๋์ง)
- ์ ๋๋ฉ์ด์ ํจ๊ณผ (์ ๋ต ์ confetti, ์ค๋ต ์ shake)
- ์ฌ์ด๋ ํจ๊ณผ (๋ฒํผ ํด๋ฆญ, ์ ๋ต/์ค๋ต ์๋ฆฌ)
- ๋ญํน ์์คํ (ํ์ ์ฐ๋ ์ ์ฌ์ฉ์๋ณ ๊ธฐ๋ก ์ ์ฅ)
- ๊ณต์ ๊ธฐ๋ฅ ("๋ด ๊ธฐ๋ก: X์ด!" SNS ๊ณต์ ํ ์คํธ ์์ฑ)
๐ฆ ๊ธฐ์ ์คํ
- g5se (๊ทธ๋๋ณด๋5 SE) - PHP ๊ธฐ๋ฐ CMS
- SQLite - ๊ฒ์ ๊ธฐ๋ก ์ ์ฅ (๋ชจ๋ ์ ์ฉ DB ํ์ผ)
- ์์ JavaScript - ๊ฒ์ ๋ก์ง & ์ธํฐ๋์
- CSS3 - ์ ๋๋ฉ์ด์ & ๋ ์ด์์
- modern shell - ์ฌ์ดํธ ํต์ผ UI
์ ์ฝ ์กฐ๊ฑด
- โข g5se ์ค์น๋ณธ ์์ ๋ชจ๋๋ก ์ถ๊ฐ (์ ํ๋ก์ ํธ ์์ฑ ์๋)
- โข ํด๋ ๊ธฐ๋ฐ ์๋ ๋ผ์ฐํ ์ฌ์ฉ: app/modules/arcade/ ํ์์ ํ์ด์ง ๊ตฌ์ฑ
- โข ๋ผ์ฐํธ ์๋ ๋ฑ๋ก ๊ธ์ง (ํด๋ ๊ตฌ์กฐ๊ฐ ๊ณง URL)
- โข modern shell ํ์ ์ ์ฉ (_head.inc.php + m-shell + _nav.inc.php + m-container + _footer.inc.php + tail.sub.php)
- โข CSS ๋ณ์(--m-surface, --m-text ๋ฑ) ํ์ฉ์ผ๋ก ๋คํฌ๋ชจ๋ ์๋ ๋์
- โข g5se ๋ฉ์ธ DB ์ ๋ ๊ฑด๋๋ฆฌ์ง ๋ง ๊ฒ - ๋ชจ๋ ์ ์ฉ SQLite ํ์ผ ์ฌ์ฉ (G5_DATA_PATH/modules/arcade.sqlite)
- โข ์ธ๋ถ ๋น๋ ๋๊ตฌ(npm/webpack) ์ฌ์ฉ ๊ธ์ง
- โข CDN ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ ์ต์ํ (์์ JS/CSS ๊ถ์ฅ)
- โข prepared statement๋ก SQL ์ธ์ ์ ๋ฐฉ์ง
- โข ์ฌ์ฉ์ ์ ๋ ฅ์ htmlspecialchars()๋ก ์ด์ค์ผ์ดํ
- โข ๋ชจ๋ ๊ฒฝ๋ก๋ ์๋๊ฒฝ๋ก(./) ์ฌ์ฉ
- โข ์ง์ ๊ฐ๋ ์ฌ์ ์ ๊ธ์ง (๋ฃจํธ index.php๊ฐ ์ด๋ฏธ ์ฒ๋ฆฌ)
- โข require/include๋ ๋ฐ๋์ ๊ธ๋ก๋ฒ ์ค์ฝํ์์๋ง (ํจ์/๋ฉ์๋ ์ ๊ธ์ง)
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
g5se๊ฐ ์ค์น๋ ๋๋ ํฐ๋ฆฌ(๋ฃจํธ์ `index.php`ยท`app/`์ด ์๋ ๊ณณ)์์ ์์
์ ์์ํฉ๋๋ค. (g5se ์ค์น๋ https://github.com/gnuboard/g5se ์ฐธ๊ณ )
์ด์ ๊ฒ์ ์ผํฐ ๋ชจ๋์ ๋ฉ์ธ ํ์ด์ง๋ฅผ ๋ง๋ค๊ฒ ์ต๋๋ค.
**1๋จ๊ณ: ํด๋ ๊ตฌ์กฐ ์์ฑ ๋ฐ ๋ฉ์ธ ํ์ด์ง ํ์ผ ์์ฑ**
```bash
mkdir -p app/modules/arcade
```
**2๋จ๊ณ: app/modules/arcade/index.php ํ์ผ ์์ฑ**
์๋ ๋ด์ฉ์ผ๋ก `app/modules/arcade/index.php` ํ์ผ์ ๋ง๋ค์ด์ฃผ์ธ์:
```php
<?php
include_once('./_common.php');
// ๊ฒ์ ๋ชฉ๋ก ์ ์
$games = [
[
'id' => 'number_guess',
'title' => '๐ข ์ซ์ ๋ง์ถ๊ธฐ',
'description' => '1~100 ์ฌ์ด ์ซ์๋ฅผ ์ต์ ์๋๋ก ๋ง์ถฐ๋ณด์ธ์!',
'icon' => '๐ฏ',
'url' => '/arcade/number-guess'
],
[
'id' => 'color_memory',
'title' => '๐ ์๊น ๊ธฐ์ตํ๊ธฐ',
'description' => '๊น๋นก์ด๋ ์๊น ํจํด์ ๊ธฐ์ตํ๊ณ ๋ฐ๋ผํด๋ณด์ธ์!',
'icon' => '๐ง ',
'url' => '/arcade/color-memory'
],
[
'id' => 'reaction_speed',
'title' => 'โก ๋ฐ์์๋ ํ
์คํธ',
'description' => 'ํ๋ฉด์ด ๋ฐ๋๋ ์๊ฐ ์ผ๋ง๋ ๋นจ๋ฆฌ ํด๋ฆญํ ์ ์๋์?',
'icon' => 'โฑ๏ธ',
'url' => '/arcade/reaction-speed'
]
];
// DB ์ฐ๊ฒฐ ๋ฐ ์ต๊ณ ๊ธฐ๋ก ์กฐํ
@mkdir(G5_DATA_PATH.'/modules', 0707, true);
$db = new PDO('sqlite:'.G5_DATA_PATH.'/modules/arcade.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// ํ
์ด๋ธ ์์ฑ
$db->exec("CREATE TABLE IF NOT EXISTS game_records (
game_id TEXT PRIMARY KEY,
best_score INTEGER NOT NULL,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
)");
// ๊ฐ ๊ฒ์์ ์ต๊ณ ๊ธฐ๋ก ์กฐํ
$stmt = $db->prepare("SELECT game_id, best_score FROM game_records");
$stmt->execute();
$records = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$records[$row['game_id']] = $row['best_score'];
}
require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<style>
.arcade-header {
text-align: center;
margin-bottom: 48px;
}
.arcade-header h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 12px;
color: var(--m-text);
}
.arcade-header p {
font-size: 1.125rem;
color: var(--m-text-muted);
}
.game-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
max-width: 1000px;
margin: 0 auto;
}
.game-card {
background: var(--m-surface);
border-radius: var(--m-radius-lg);
padding: 32px 24px;
text-align: center;
box-shadow: var(--m-shadow);
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
text-decoration: none;
color: var(--m-text);
display: block;
}
.game-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}
.game-icon {
font-size: 4rem;
margin-bottom: 16px;
}
.game-title {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 8px;
}
.game-description {
font-size: 0.875rem;
color: var(--m-text-muted);
margin-bottom: 16px;
line-height: 1.5;
}
.game-record {
display: inline-block;
background: rgba(99, 102, 241, 0.1);
color: #6366f1;
padding: 6px 12px;
border-radius: 12px;
font-size: 0.875rem;
font-weight: 600;
}
</style>
<div class="m-shell">
<?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
<main class="m-container" style="padding: 48px 20px;">
<div class="arcade-header">
<h1>๐ฎ ๋ฏธ๋ ๊ฒ์ ์ผํฐ</h1>
<p>๋ธ๋ผ์ฐ์ ์์ ์ฆ๊ธฐ๋ ๊ฐ๋จํ ์์ผ์ด๋ ๊ฒ์ ๋ชจ์</p>
</div>
<div class="game-grid">
<?php foreach ($games as $game): ?>
<a href="<?php echo htmlspecialchars($game['url']); ?>" class="game-card">
<div class="game-icon"><?php echo $game['icon']; ?></div>
<div class="game-title"><?php echo htmlspecialchars($game['title']); ?></div>
<div class="game-description"><?php echo htmlspecialchars($game['description']); ?></div>
<?php if (isset($records[$game['id']])): ?>
<div class="game-record">
<?php
if ($game['id'] === 'number_guess') {
echo '์ต์ ์๋: '.$records[$game['id']].'ํ';
} elseif ($game['id'] === 'color_memory') {
echo '์ต๊ณ ๋ผ์ด๋: '.$records[$game['id']];
} elseif ($game['id'] === 'reaction_speed') {
echo '์ต๊ณ ๊ธฐ๋ก: '.$records[$game['id']].'ms';
}
?>
</div>
<?php else: ?>
<div class="game-record" style="background: rgba(156, 163, 175, 0.1); color: var(--m-text-muted);">
๊ธฐ๋ก ์์
</div>
<?php endif; ?>
</a>
<?php endforeach; ?>
</div>
</main>
<?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<?php include_once('./tail.sub.php'); ?>
```
์ด ํ์ผ์ ์์ฑํ๊ณ ๋ธ๋ผ์ฐ์ ์์ `/arcade` ๊ฒฝ๋ก๋ก ์ ์ํ๋ฉด ๊ฒ์ ๋ชฉ๋ก ํ์ด์ง๊ฐ ํ์๋ฉ๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: - app/modules/arcade/index.php ํ์ผ ์์ฑ ์๋ฃ - ๋ธ๋ผ์ฐ์ ์์ /arcade ์ ์ ์ ๊ฒ์ ์ผํฐ ๋ฉ์ธ ํ์ด์ง ํ์ - 3๊ฐ์ ๊ฒ์ ์นด๋๊ฐ ๊ทธ๋ฆฌ๋๋ก ๋ฐฐ์น๋จ - ๊ฐ ์นด๋์ ๊ฒ์ ์์ด์ฝ, ์ ๋ชฉ, ์ค๋ช ํ์ - SQLite DB ์ฐ๊ฒฐ ๋ฐ game_records ํ ์ด๋ธ ์๋ ์์ฑ - ์ต๊ณ ๊ธฐ๋ก์ด ์์ผ๋ฉด ์นด๋์ ํ์, ์์ผ๋ฉด '๊ธฐ๋ก ์์' ํ์ - modern shell ์ ์ฉ์ผ๋ก ์ฌ์ดํธ nav/footer ์ผ๊ด์ฑ ํ๋ณด - ๋คํฌ๋ชจ๋ ์ ํ ์ ์์ ์๋ ๋์
์ด์ ์ฒซ ๋ฒ์งธ ๊ฒ์์ธ "์ซ์ ๋ง์ถ๊ธฐ"๋ฅผ ๋ง๋ค๊ฒ ์ต๋๋ค.
**1๋จ๊ณ: ํด๋ ์์ฑ**
```bash
mkdir -p app/modules/arcade/number-guess
```
**2๋จ๊ณ: app/modules/arcade/number-guess/index.php ํ์ผ ์์ฑ**
์๋ ๋ด์ฉ์ผ๋ก ํ์ผ์ ๋ง๋ค์ด์ฃผ์ธ์:
```php
<?php
include_once('./_common.php');
// AJAX ์์ฒญ ์ฒ๋ฆฌ
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
if (isset($input['action']) && $input['action'] === 'save_record') {
$attempts = (int)$input['attempts'];
@mkdir(G5_DATA_PATH.'/modules', 0707, true);
$db = new PDO('sqlite:'.G5_DATA_PATH.'/modules/arcade.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// ๊ธฐ์กด ๊ธฐ๋ก ์กฐํ
$stmt = $db->prepare("SELECT best_score FROM game_records WHERE game_id = ?");
$stmt->execute(['number_guess']);
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
$is_new_record = false;
if (!$existing || $attempts < $existing['best_score']) {
$stmt = $db->prepare(
"INSERT OR REPLACE INTO game_records (game_id, best_score, updated_at)
VALUES (?, ?, datetime('now'))"
);
$stmt->execute(['number_guess', $attempts]);
$is_new_record = true;
}
echo json_encode(['success' => true, 'is_new_record' => $is_new_record]);
exit;
}
}
// ์ต๊ณ ๊ธฐ๋ก ์กฐํ
@mkdir(G5_DATA_PATH.'/modules', 0707, true);
$db = new PDO('sqlite:'.G5_DATA_PATH.'/modules/arcade.sqlite');
$stmt = $db->prepare("SELECT best_score FROM game_records WHERE game_id = ?");
$stmt->execute(['number_guess']);
$record = $stmt->fetch(PDO::FETCH_ASSOC);
$best_score = $record ? $record['best_score'] : null;
require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<style>
.game-container {
max-width: 600px;
margin: 0 auto;
text-align: center;
}
.game-header {
margin-bottom: 32px;
}
.game-header h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 8px;
color: var(--m-text);
}
.game-header .subtitle {
color: var(--m-text-muted);
font-size: 1rem;
}
.record-badge {
display: inline-block;
background: rgba(139, 92, 246, 0.1);
color: #8b5cf6;
padding: 8px 16px;
border-radius: 12px;
font-weight: 600;
margin-top: 12px;
}
.game-area {
background: var(--m-surface);
border-radius: var(--m-radius-lg);
padding: 48px 32px;
box-shadow: var(--m-shadow);
}
.game-status {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 24px;
color: var(--m-text);
min-height: 32px;
}
.hint {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 24px;
min-height: 40px;
}
.hint.up { color: #ef4444; }
.hint.down { color: #3b82f6; }
.hint.correct { color: #10b981; }
.input-group {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 24px;
}
.input-group input {
width: 120px;
padding: 12px 16px;
font-size: 1.25rem;
text-align: center;
border: 2px solid rgba(139, 92, 246, 0.3);
border-radius: 8px;
background: var(--m-surface);
color: var(--m-text);
}
.input-group input:focus {
outline: none;
border-color: #8b5cf6;
}
.btn {
padding: 12px 32px;
font-size: 1rem;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: transform 0.2s;
}
.btn:hover { transform: scale(1.05); }
.btn-primary {
background: #8b5cf6;
color: white;
}
.btn-secondary {
background: rgba(156, 163, 175, 0.2);
color: var(--m-text);
}
.stats {
display: flex;
gap: 24px;
justify-content: center;
margin-top: 24px;
font-size: 0.875rem;
color: var(--m-text-muted);
}
.back-link {
display: inline-block;
margin-top: 24px;
color: var(--m-text-muted);
text-decoration: none;
}
.back-link:hover { text-decoration: underline; }
</style>
<div class="m-shell">
<?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
<main class="m-container" style="padding: 48px 20px;">
<div class="game-container">
<div class="game-header">
<h1>๐ข ์ซ์ ๋ง์ถ๊ธฐ</h1>
<div class="subtitle">1~100 ์ฌ์ด์ ์ซ์๋ฅผ ๋ง์ถฐ๋ณด์ธ์!</div>
<?php if ($best_score): ?>
<div class="record-badge">์ต์ ์๋ ๊ธฐ๋ก: <?php echo $best_score; ?>ํ</div>
<?php endif; ?>
</div>
<div class="game-area">
<div class="game-status" id="status">๊ฒ์์ ์์ํ๋ ค๋ฉด ์๋ ๋ฒํผ์ ํด๋ฆญํ์ธ์</div>
<div class="hint" id="hint"></div>
<div id="game-controls" style="display: none;">
<div class="input-group">
<input type="number" id="guess-input" min="1" max="100" placeholder="1-100">
<button class="btn btn-primary" onclick="makeGuess()">ํ์ธ</button>
</div>
<div class="stats">
<div>์๋ ํ์: <strong id="attempts">0</strong></div>
<div>๋ฒ์: <strong id="range">1 ~ 100</strong></div>
</div>
</div>
<button class="btn btn-primary" id="start-btn" onclick="startGame()">๊ฒ์ ์์</button>
<button class="btn btn-secondary" id="reset-btn" style="display: none;" onclick="resetGame()">๋ค์ ํ๊ธฐ</button>
</div>
<a href="/arcade" class="back-link">โ ๊ฒ์ ์ผํฐ๋ก ๋์๊ฐ๊ธฐ</a>
</div>
</main>
<?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<script>
let targetNumber = 0;
let attempts = 0;
let minRange = 1;
let maxRange = 100;
function startGame() {
targetNumber = Math.floor(Math.random() * 100) + 1;
attempts = 0;
minRange = 1;
maxRange = 100;
document.getElementById('status').textContent = '์ซ์๋ฅผ ์
๋ ฅํ๊ณ ํ์ธ ๋ฒํผ์ ๋๋ฅด์ธ์';
document.getElementById('hint').textContent = '';
document.getElementById('attempts').textContent = '0';
document.getElementById('range').textContent = '1 ~ 100';
document.getElementById('guess-input').value = '';
document.getElementById('start-btn').style.display = 'none';
document.getElementById('game-controls').style.display = 'block';
document.getElementById('reset-btn').style.display = 'none';
document.getElementById('guess-input').focus();
}
function makeGuess() {
const input = document.getElementById('guess-input');
const guess = parseInt(input.value);
if (!guess || guess < 1 || guess > 100) {
alert('1์์ 100 ์ฌ์ด์ ์ซ์๋ฅผ ์
๋ ฅํ์ธ์!');
return;
}
attempts++;
document.getElementById('attempts').textContent = attempts;
const hint = document.getElementById('hint');
if (guess === targetNumber) {
hint.textContent = '๐ ์ ๋ต์
๋๋ค!';
hint.className = 'hint correct';
document.getElementById('status').textContent = `${attempts}๋ฒ ๋ง์ ๋ง์ถ์
จ์ต๋๋ค!`;
document.getElementById('game-controls').style.display = 'none';
document.getElementById('reset-btn').style.display = 'inline-block';
// ๊ธฐ๋ก ์ ์ฅ
fetch('/arcade/number-guess', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'save_record', attempts })
})
.then(res => res.json())
.then(data => {
if (data.is_new_record) {
setTimeout(() => {
alert('๐ ์ ๊ธฐ๋ก ๋ฌ์ฑ!');
}, 500);
}
});
} else if (guess > targetNumber) {
hint.textContent = 'โฌ๏ธ DOWN';
hint.className = 'hint down';
maxRange = Math.min(maxRange, guess - 1);
} else {
hint.textContent = 'โฌ๏ธ UP';
hint.className = 'hint up';
minRange = Math.max(minRange, guess + 1);
}
document.getElementById('range').textContent = `${minRange} ~ ${maxRange}`;
input.value = '';
input.focus();
}
function resetGame() {
startGame();
}
// Enter ํค๋ก ์ ์ถ
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('guess-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') makeGuess();
});
});
</script>
<?php include_once('./tail.sub.php'); ?>
```
์ด ํ์ผ์ ์์ฑํ๊ณ ๋ธ๋ผ์ฐ์ ์์ `/arcade/number-guess` ๊ฒฝ๋ก๋ก ์ ์ํ๋ฉด ์ซ์ ๋ง์ถ๊ธฐ ๊ฒ์์ ํ๋ ์ดํ ์ ์์ต๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: - app/modules/arcade/number-guess/index.php ํ์ผ ์์ฑ ์๋ฃ - /arcade/number-guess ์ ์ ์ ๊ฒ์ ํ์ด์ง ํ์ - "๊ฒ์ ์์" ๋ฒํผ ํด๋ฆญ ์ 1~100 ๋๋ค ์ซ์ ์์ฑ - ์ฌ์ฉ์ ์ ๋ ฅ ํ UP/DOWN ํํธ ์ ๊ณต - ์ ๋ต ์ ์๋ ํ์ ํ์ ๋ฐ DB ์ ์ฅ - ์ ๊ธฐ๋ก ๋ฌ์ฑ ์ ์๋ฆผ ํ์ - "๋ค์ ํ๊ธฐ" ๋ฒํผ์ผ๋ก ๊ฒ์ ๋ฆฌ์ - ์ต๊ณ ๊ธฐ๋ก์ด ํ์ด์ง ์๋จ์ ํ์๋จ - ๋ฒ์๊ฐ ์ ์ ์ขํ์ง๋ฉฐ ํ์๋จ - Enter ํค๋ก ์ ์ถ ๊ฐ๋ฅ
์ด์ ๋ ๋ฒ์งธ ๊ฒ์์ธ "์๊น ๊ธฐ์ตํ๊ธฐ"๋ฅผ ๋ง๋ค๊ฒ ์ต๋๋ค.
**1๋จ๊ณ: ํด๋ ์์ฑ**
```bash
mkdir -p app/modules/arcade/color-memory
```
**2๋จ๊ณ: app/modules/arcade/color-memory/index.php ํ์ผ ์์ฑ**
์๋ ๋ด์ฉ์ผ๋ก ํ์ผ์ ๋ง๋ค์ด์ฃผ์ธ์:
```php
<?php
include_once('./_common.php');
// AJAX ์์ฒญ ์ฒ๋ฆฌ
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
if (isset($input['action']) && $input['action'] === 'save_record') {
$round = (int)$input['round'];
@mkdir(G5_DATA_PATH.'/modules', 0707, true);
$db = new PDO('sqlite:'.G5_DATA_PATH.'/modules/arcade.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $db->prepare("SELECT best_score FROM game_records WHERE game_id = ?");
$stmt->execute(['color_memory']);
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
$is_new_record = false;
if (!$existing || $round > $existing['best_score']) {
$stmt = $db->prepare(
"INSERT OR REPLACE INTO game_records (game_id, best_score, updated_at)
VALUES (?, ?, datetime('now'))"
);
$stmt->execute(['color_memory', $round]);
$is_new_record = true;
}
echo json_encode(['success' => true, 'is_new_record' => $is_new_record]);
exit;
}
}
// ์ต๊ณ ๊ธฐ๋ก ์กฐํ
@mkdir(G5_DATA_PATH.'/modules', 0707, true);
$db = new PDO('sqlite:'.G5_DATA_PATH.'/modules/arcade.sqlite');
$stmt = $db->prepare("SELECT best_score FROM game_records WHERE game_id = ?");
$stmt->execute(['color_memory']);
$record = $stmt->fetch(PDO::FETCH_ASSOC);
$best_score = $record ? $record['best_score'] : null;
require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<style>
.game-container {
max-width: 600px;
margin: 0 auto;
text-align: center;
}
.game-header {
margin-bottom: 32px;
}
.game-header h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 8px;
color: var(--m-text);
}
.game-header .subtitle {
color: var(--m-text-muted);
font-size: 1rem;
}
.record-badge {
display: inline-block;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
padding: 8px 16px;
border-radius: 12px;
font-weight: 600;
margin-top: 12px;
}
.game-area {
background: var(--m-surface);
border-radius: var(--m-radius-lg);
padding: 48px 32px;
box-shadow: var(--m-shadow);
}
.game-status {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 24px;
color: var(--m-text);
min-height: 32px;
}
.color-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
max-width: 400px;
margin: 0 auto 32px;
}
.color-btn {
aspect-ratio: 1;
border: none;
border-radius: 16px;
cursor: pointer;
transition: transform 0.1s, opacity 0.3s;
font-size: 2rem;
}
.color-btn:hover:not(:disabled) {
transform: scale(1.05);
}
.color-btn:disabled {
cursor: not-allowed;
}
.color-btn.flash {
opacity: 1 !important;
transform: scale(1.1);
}
.btn {
padding: 12px 32px;
font-size: 1rem;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: transform 0.2s;
}
.btn:hover { transform: scale(1.05); }
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-secondary {
background: rgba(156, 163, 175, 0.2);
color: var(--m-text);
}
.stats {
display: flex;
gap: 24px;
justify-content: center;
margin-top: 24px;
font-size: 0.875rem;
color: var(--m-text-muted);
}
.back-link {
display: inline-block;
margin-top: 24px;
color: var(--m-text-muted);
text-decoration: none;
}
.back-link:hover { text-decoration: underline; }
</style>
<div class="m-shell">
<?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
<main class="m-container" style="padding: 48px 20px;">
<div class="game-container">
<div class="game-header">
<h1>๐ ์๊น ๊ธฐ์ตํ๊ธฐ</h1>
<div class="subtitle">๊น๋นก์ด๋ ์๊น ์์๋ฅผ ๊ธฐ์ตํ๊ณ ๋ฐ๋ผํด๋ณด์ธ์!</div>
<?php if ($best_score): ?>
<div class="record-badge">์ต๊ณ ๋ผ์ด๋: <?php echo $best_score; ?></div>
<?php endif; ?>
</div>
<div class="game-area">
<div class="game-status" id="status">๊ฒ์์ ์์ํ๋ ค๋ฉด ์๋ ๋ฒํผ์ ํด๋ฆญํ์ธ์</div>
<div class="color-buttons" id="color-buttons" style="opacity: 0.5;">
<button class="color-btn" data-color="red" style="background: #ef4444;" disabled>๐ด</button>
<button class="color-btn" data-color="blue" style="background: #3b82f6;" disabled>๐ต</button>
<button class="color-btn" data-color="yellow" style="background: #fbbf24;" disabled>๐ก</button>
<button class="color-btn" data-color="green" style="background: #10b981;" disabled>๐ข</button>
</div>
<div class="stats">
<div>๋ผ์ด๋: <strong id="round">0</strong></div>
<div>ํจํด ๊ธธ์ด: <strong id="pattern-length">0</strong></div>
</div>
<button class="btn btn-primary" id="start-btn" onclick="startGame()">๊ฒ์ ์์</button>
<button class="btn btn-secondary" id="reset-btn" style="display: none;" onclick="resetGame()">๋ค์ ํ๊ธฐ</button>
</div>
<a href="/arcade" class="back-link">โ ๊ฒ์ ์ผํฐ๋ก ๋์๊ฐ๊ธฐ</a>
</div>
</main>
<?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<script>
const colors = ['red', 'blue', 'yellow', 'green'];
let pattern = [];
let userPattern = [];
let round = 0;
let isPlaying = false;
function startGame() {
pattern = [];
round = 0;
document.getElementById('start-btn').style.display = 'none';
document.getElementById('reset-btn').style.display = 'none';
nextRound();
}
function nextRound() {
round++;
userPattern = [];
// ๋๋ค ์๊น ์ถ๊ฐ
const randomColor = colors[Math.floor(Math.random() * colors.length)];
pattern.push(randomColor);
document.getElementById('round').textContent = round;
document.getElementById('pattern-length').textContent = pattern.length;
document.getElementById('status').textContent = 'ํจํด์ ๊ธฐ์ตํ์ธ์...';
// ๋ฒํผ ๋นํ์ฑํ
setButtonsEnabled(false);
// ํจํด ์ฌ์
playPattern();
}
function playPattern() {
let index = 0;
const interval = setInterval(() => {
if (index >= pattern.length) {
clearInterval(interval);
document.getElementById('status').textContent = '์ด์ ๋ฐ๋ผํด๋ณด์ธ์!';
setButtonsEnabled(true);
return;
}
flashButton(pattern[index]);
index++;
}, 800);
}
function flashButton(color) {
const btn = document.querySelector(`[data-color="${color}"]`);
btn.classList.add('flash');
setTimeout(() => btn.classList.remove('flash'), 400);
}
function setButtonsEnabled(enabled) {
const buttons = document.querySelectorAll('.color-btn');
const container = document.getElementById('color-buttons');
buttons.forEach(btn => btn.disabled = !enabled);
container.style.opacity = enabled ? '1' : '0.5';
}
function handleColorClick(color) {
if (!isPlaying) return;
flashButton(color);
userPattern.push(color);
// ํ์ฌ๊น์ง ์
๋ ฅ์ด ์ฌ๋ฐ๋ฅธ์ง ํ์ธ
const currentIndex = userPattern.length - 1;
if (userPattern[currentIndex] !== pattern[currentIndex]) {
// ์คํจ
gameOver();
return;
}
// ํจํด์ ๋ชจ๋ ๋ง์ท๋์ง ํ์ธ
if (userPattern.length === pattern.length) {
// ์ฑ๊ณต - ๋ค์ ๋ผ์ด๋
document.getElementById('status').textContent = 'โ
์ ๋ต! ๋ค์ ๋ผ์ด๋...';
setButtonsEnabled(false);
setTimeout(() => nextRound(), 1500);
}
}
function gameOver() {
isPlaying = false;
setButtonsEnabled(false);
document.getElementById('status').textContent = `๊ฒ์ ์ค๋ฒ! ${round}๋ผ์ด๋๊น์ง ์ฑ๊ณต`;
document.getElementById('reset-btn').style.display = 'inline-block';
// ๊ธฐ๋ก ์ ์ฅ
fetch('/arcade/color-memory', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'save_record', round })
})
.then(res => res.json())
.then(data => {
if (data.is_new_record) {
setTimeout(() => {
alert('๐ ์ ๊ธฐ๋ก ๋ฌ์ฑ!');
}, 500);
}
});
}
function resetGame() {
startGame();
}
// ๋ฒํผ ํด๋ฆญ ์ด๋ฒคํธ ๋ฑ๋ก
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.color-btn').forEach(btn => {
btn.addEventListener('click', () => {
if (!btn.disabled) {
handleColorClick(btn.dataset.color);
}
});
});
});
// ๊ฒ์ ์์ ์ ํ๋๊ทธ ์ค์
function startGame() {
isPlaying = true;
pattern = [];
round = 0;
document.getElementById('start-btn').style.display = 'none';
document.getElementById('reset-btn').style.display = 'none';
nextRound();
}
</script>
<?php include_once('./tail.sub.php'); ?>
```
์ด ํ์ผ์ ์์ฑํ๊ณ ๋ธ๋ผ์ฐ์ ์์ `/arcade/color-memory` ๊ฒฝ๋ก๋ก ์ ์ํ๋ฉด ์๊น ๊ธฐ์ตํ๊ธฐ ๊ฒ์์ ํ๋ ์ดํ ์ ์์ต๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: - app/modules/arcade/color-memory/index.php ํ์ผ ์์ฑ ์๋ฃ - /arcade/color-memory ์ ์ ์ ๊ฒ์ ํ์ด์ง ํ์ - 4๊ฐ์ ์๊น ๋ฒํผ(๋นจ๊ฐ, ํ๋, ๋ ธ๋, ์ด๋ก) ๊ทธ๋ฆฌ๋ ๋ฐฐ์น - ๊ฒ์ ์์ ์ ๋๋ค ์๊น ํจํด์ด ์์ฐจ์ ์ผ๋ก ๊น๋นก์ - ์ฌ์ฉ์๊ฐ ๊ฐ์ ์์๋ก ํด๋ฆญํด์ผ ํจ - ์ ๋ต ์ ๋ค์ ๋ผ์ด๋๋ก ์งํ (ํจํด ๊ธธ์ด +1) - ์ค๋ต ์ ๊ฒ์ ์ค๋ฒ ๋ฐ ๊ธฐ๋ก ์ ์ฅ - ์ ๊ธฐ๋ก ๋ฌ์ฑ ์ ์๋ฆผ ํ์ - ํ์ฌ ๋ผ์ด๋์ ํจํด ๊ธธ์ด ์ค์๊ฐ ํ์ - ์ต๊ณ ๋ผ์ด๋ ๊ธฐ๋ก์ด ํ์ด์ง ์๋จ์ ํ์๋จ
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!