๐ฐ g5se ์นด์ง๋ ธ ์ฌ๋กฏ๋จธ์ ์๋ฎฌ๋ ์ดํฐ - ํ์ด์ ๋ฆด ๋๋ฆฌ๊ธฐ & ์ญํ ํ์คํ ๋ฆฌ
g5se-casino-slot-machine
๋ฌธ์ ์ค๋ช
๐ฐ ํ๋ก์ ํธ ๊ฐ์
์ค์ ์นด์ง๋ ธ ์ฌ๋กฏ๋จธ์ ์ฒ๋ผ ๋์ํ๋ ๋ธ๋ผ์ฐ์ ๊ธฐ๋ฐ ๊ฒ์์ g5se ์์ ๊ตฌ์ถํฉ๋๋ค. ์ฌ์ฉ์๋ ๊ฐ์ ์ฝ์ธ์ ๋ฒ ํ ํ๊ณ ๋ฆด์ ๋๋ ค ์ฌ๋ณผ ์กฐํฉ์ ๋ฐ๋ผ ๋ฐฐ๋น์ ๋ฐ์ต๋๋ค. ์ ๋๋ฉ์ด์ , ์ฌ์ด๋ ํจ๊ณผ, ์ญํ ํ์คํ ๋ฆฌ, ์ผ์ผ ๋ณด๋์ค ๋ฑ ์ค์ ์ฌ๋กฏ๋จธ์ ์ ์ฌ๋ฏธ ์์๋ฅผ ๋ชจ๋ ๊ตฌํํฉ๋๋ค.
ํ์ ๊ธฐ๋ฅ
-
๋ฆด ์คํ ์ ๋๋ฉ์ด์
- 3๊ฐ ๋ฆด์ด ๋ ๋ฆฝ์ ์ผ๋ก ํ์ ํ๋ฉฐ ์์ฐจ์ ์ผ๋ก ๋ฉ์ถค
- CSS animation + setTimeout์ผ๋ก ๋ฆด ํ์ ์๊ฐ ํจ๊ณผ
- ๊ฐ ๋ฆด์ 7๊ฐ ์ฌ๋ณผ(๐์ฒด๋ฆฌ, ๐๋ ๋ชฌ, ๐์ค๋ ์ง, ๐์๋ฐ, โญ๋ณ, ๐๋ค์ด์, 7๏ธโฃ์ธ๋ธ) ์ค ๋๋ค ์ ํ
-
๋ฐฐ๋น ์์คํ
- 3๊ฐ ์ฌ๋ณผ ์ผ์น: ๋ฐฐ๋น ์ง๊ธ (์ฒด๋ฆฌ 2๋ฐฐ, ๋ ๋ชฌ 3๋ฐฐ, ์ค๋ ์ง 5๋ฐฐ, ์๋ฐ 10๋ฐฐ, ๋ณ 20๋ฐฐ, ๋ค์ด์ 50๋ฐฐ, ์ธ๋ธ 100๋ฐฐ)
- 2๊ฐ ์ผ์น: ๋ฒ ํ ์ก ๋ฐํ
- ๋ถ์ผ์น: ๋ฒ ํ ์ก ์ฐจ๊ฐ
- ์ญํ(์ธ๋ธ 3๊ฐ): ํน์ ์ ๋๋ฉ์ด์ + ์ฌ์ด๋ + ํ์คํ ๋ฆฌ ๊ธฐ๋ก
-
๊ฐ์ ์ฝ์ธ ๊ด๋ฆฌ
- ์ด๊ธฐ ์ง๊ธ: 1000์ฝ์ธ
- ๋ฒ ํ ๊ธ์ก ์ ํ: 10/50/100/500์ฝ์ธ
- ์ฝ์ธ ์์ง ์ ์ผ์ผ ๋ณด๋์ค๋ก ์ฌ์ถฉ์ ๊ฐ๋ฅ (1ํ/์ผ, 500์ฝ์ธ)
- ์ฌ์ฉ์๋ณ ์ฝ์ธ ์์ก SQLite DB ์ ์ฅ
-
๊ฒ์ ํ์คํ ๋ฆฌ & ํต๊ณ
- ์ต๊ทผ 20ํ ์คํ ๊ฒฐ๊ณผ ๋ก๊ทธ (์ฌ๋ณผ ์กฐํฉ, ๋ฐฐ๋น)
- ์ญํ ๋ฌ์ฑ ํ์คํ ๋ฆฌ (๋๋ค์, ํ์์คํฌํ, ๋ฐฐ๋น์ก)
- ๊ฐ์ธ ํต๊ณ: ์ด ์คํ ํ์, ์ด ๋ฒ ํ ์ก, ์ด ํ๋์ก, ์น๋ฅ
-
์ฌ์ด๋ & ์๊ฐ ํจ๊ณผ
- ๋ฆด ํ์ ์ฌ์ด๋ (Web Audio API๋ก ๊ฐ๋จํ beep ์์ฑ)
- ๋น์ฒจ ์ ์ถํ ํจ๊ณผ (confetti.js ๊ฐ์ ํํฐํด ์ ๋๋ฉ์ด์ ๋๋ CSS keyframe)
- ์ญํ ์ ์ ์ฒด ํ๋ฉด ์ถํ ๋ชจ๋ฌ + ํญ์ฃฝ ์ ๋๋ฉ์ด์
-
ํ์ ์ฐ๋
- g5se ๋ก๊ทธ์ธ ํ์๋ง ํ๋ ์ด ๊ฐ๋ฅ (
$is_member์ฒดํฌ) - ๋นํ์์ ๋ฐ๋ชจ ๋ชจ๋(์ฝ์ธ ์ง๊ธ ์์, ๊ฒฐ๊ณผ๋ง ํ์)
- ๋๋ค์์ผ๋ก ์ญํ ํ์คํ ๋ฆฌ ๊ธฐ๋ก
- g5se ๋ก๊ทธ์ธ ํ์๋ง ํ๋ ์ด ๊ฐ๋ฅ (
์ ํ ์ถ๊ฐ ๊ธฐ๋ฅ (๋ณด๋์ค)
- ์คํ ์คํ: ์ง์ ํ์๋งํผ ์๋์ผ๋ก ๋ฆด ํ์
- ๋๋ธ ์ ๊ฒ์: ๋น์ฒจ ํ ๋ฐฐ๋น์ 2๋ฐฐ๋ก ๋๋ฆด ๊ธฐํ ์ ๊ณต (๋์ ๋์ง๊ธฐ ๋ฏธ๋๊ฒ์)
- ๋ฆฌ๋๋ณด๋: ๋์ ํ๋ ์ฝ์ธ TOP 10 ๋ญํน
- ์ผ์ผ ๋ฏธ์ : ํน์ ์ฌ๋ณผ ์กฐํฉ ๋ฌ์ฑ ์ ๋ณด๋์ค ์ฝ์ธ ์ง๊ธ
- ํ ๋ง ์ ํ: ํด๋์/๋ค์จ/๋ ํธ๋ก ์ฌ๋กฏ๋จธ์ ์คํจ ์ ํ
๊ธฐ์ ์คํ
- ๋ฐฑ์๋: PHP 8.2+ (g5se ์ปจ๋ฒค์ )
- ํ๋ก ํธ์๋: ์์ HTML/CSS/JavaScript (CDN ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ต์ํ)
- DB: SQLite (
G5_DATA_PATH/modules/casino.sqlite) - ์ ๋๋ฉ์ด์
: CSS
@keyframes+transition - ์ฌ์ด๋: Web Audio API (AudioContext)
์ ์ฝ ์กฐ๊ฑด
- g5se modern shell ํ์ ์ฌ์ฉ (๋คํฌ๋ชจ๋ ๋์)
- ์ธ๋ถ npm/vite/webpack ๋น๋ ๋๊ตฌ ์ฌ์ฉ ๊ธ์ง
- ์ค์๊ฐ WebSocket ์ฌ์ฉ ๊ธ์ง (HTTP polling์ผ๋ก ํ์คํ ๋ฆฌ ๊ฐฑ์ )
- ์ค์ ๊ธ์ ๊ฑฐ๋ ๊ธฐ๋ฅ ์์ (๊ฐ์ ์ฝ์ธ๋ง)
- ์ฌ์ฉ์ ์
๋ ฅ ๋ฐ์ดํฐ ๋ชจ๋
htmlspecialchars()์ด์ค์ผ์ดํ
์ ์ฝ ์กฐ๊ฑด
- โข g5se ์ค์น๋ณธ ์์ ์์ (๊ธฐ์กด ์ค์น ํ์)
- โข ํด๋ ๊ธฐ๋ฐ ์๋ ๋ผ์ฐํ ์ฌ์ฉ (app/modules/casino/index.php โ /casino)
- โข modern shell ํ์ ์ ์ฉ (_head.inc.php + m-shell + _nav + _footer)
- โข ๋์์ธ ํ ํฐ ์ฌ์ฉ (var(--m-surface), var(--m-text) ๋ฑ)
- โข ์์ PHP + HTML/CSS/JS (์ธ๋ถ ๋น๋ ๋๊ตฌ ๊ธ์ง)
- โข SQLite DB๋ G5_DATA_PATH/modules/casino.sqlite์ ์ ์ฅ
- โข prepared statement๋ก SQL ์ธ์ ์ ๋ฐฉ์ง
- โข ํ์ ์ ์ฉ ๊ธฐ๋ฅ ($is_member ์ฒดํฌ)
- โข Web Audio API๋ก ์ฌ์ด๋ ์์ฑ (์ธ๋ถ mp3 ํ์ผ ์ฌ์ฉ ๊ธ์ง)
- โข CSS animation์ผ๋ก ๋ฆด ํ์ ๊ตฌํ
- โข ์ค์๊ฐ WebSocket ์ฌ์ฉ ๊ธ์ง (HTTP ๊ธฐ๋ฐ ํด๋ง)
- โข CDN ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ต์ํ (๊ฐ๋ฅํ๋ฉด ์์ JS๋ก)
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
g5se๊ฐ ์ค์น๋ ๋๋ ํฐ๋ฆฌ(๋ฃจํธ์ `index.php`ยท`app/`์ด ์๋ ๊ณณ)์์ ์์
์ ์์ํฉ๋๋ค. (g5se ์ค์น๋ https://github.com/gnuboard/g5se ์ฐธ๊ณ )
**Step 1: ํ๋ก์ ํธ ํด๋ ์์ฑ & modern shell ๊ณจ๊ฒฉ ๊ตฌ์ถ**
๋ค์ ์์
์ ์ํํ์ธ์:
1. `app/modules/casino/index.php` ํ์ผ ์์ฑ
2. modern shell ๊ณจ๊ฒฉ ์ ์ฉ:
- `_common.php` ์ธํด๋ฃจ๋ (g5se ์ง์
์ปจํ
์คํธ)
- `_head.inc.php` ์ธํด๋ฃจ๋ (HTML head + ๋คํฌ๋ชจ๋ ํ ํฐ)
- `m-shell` ๋ํผ๋ก ์ ์ฒด ๊ฐ์ธ๊ธฐ
- `_nav.inc.php` ์ธํด๋ฃจ๋ (์๋จ ๋ค๋น๊ฒ์ด์
)
- `<main class="m-container">` ์์ ํ์ด์ง ๋ณธ๋ฌธ
- `_footer.inc.php` ์ธํด๋ฃจ๋ (ํ๋จ ํธํฐ)
- `tail.sub.php` ์ธํด๋ฃจ๋ (์คํฌ๋ฆฝํธ ์ข
๋ฃ)
3. ํ์ด์ง ์ ๋ชฉ: "๐ฐ ์นด์ง๋
ธ ์ฌ๋กฏ๋จธ์ "
4. ๋ณธ๋ฌธ์ ์์ ํ
์คํธ: "์ฌ๋กฏ๋จธ์ ๊ฒ์ ์ค๋น ์ค..."
**์ฝ๋ ๊ณจ๊ฒฉ ์์:**
```php
<?php
include_once('./_common.php');
// ํ์ ์ ์ฉ ์ฒดํฌ
if (!$is_member) {
alert_close('ํ์๋ง ์ด์ฉํ ์ ์์ต๋๋ค.');
}
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: 24px;">๐ฐ ์นด์ง๋
ธ ์ฌ๋กฏ๋จธ์ </h1>
<p style="color: var(--m-text-muted);">์ฌ๋กฏ๋จธ์ ๊ฒ์ ์ค๋น ์ค...</p>
</main>
<?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<?php include_once('./tail.sub.php'); ?>
```
๋ธ๋ผ์ฐ์ ์์ `/casino` ์ ์ ์ ํ์ด์ง๊ฐ ๋ก๋๋๊ณ modern shell์ด ์ ์ฉ๋์๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: app/modules/casino/index.php ํ์ผ์ด ์์ฑ๋๊ณ , /casino ๊ฒฝ๋ก๋ก ์ ์ ์ modern shell์ด ์ ์ฉ๋ ๋น ํ์ด์ง๊ฐ ๋ ๋๋ง๋ฉ๋๋ค. ์๋จ ๋ค๋น๊ฒ์ด์ , ํ๋จ ํธํฐ, ๋คํฌ๋ชจ๋ ํ ๊ธ์ด ์ ์ ์๋ํ๋ฉฐ '์ฌ๋กฏ๋จธ์ ๊ฒ์ ์ค๋น ์ค...' ํ ์คํธ๊ฐ ํ์๋ฉ๋๋ค.
**Step 2: SQLite DB ์คํค๋ง ์ค๊ณ & ์ด๊ธฐํ**
์ฌ๋กฏ๋จธ์ ๊ฒ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ SQLite DB๋ฅผ ๊ตฌ์ถํฉ๋๋ค.
**DB ํ์ผ ์์น**: `G5_DATA_PATH/modules/casino.sqlite`
**ํ
์ด๋ธ ์ค๊ณ**:
1. `user_coins` ํ
์ด๋ธ:
- `mb_id` TEXT PRIMARY KEY (ํ์ ์์ด๋)
- `coins` INTEGER NOT NULL DEFAULT 1000 (๋ณด์ ์ฝ์ธ)
- `last_bonus_date` TEXT (๋ง์ง๋ง ์ผ์ผ ๋ณด๋์ค ์๋ น ๋ ์ง, YYYY-MM-DD)
- `created_at` TEXT (๊ณ์ ์์ฑ ์ผ์)
2. `spin_history` ํ
์ด๋ธ:
- `id` INTEGER PRIMARY KEY AUTOINCREMENT
- `mb_id` TEXT NOT NULL (ํ์ ์์ด๋)
- `bet_amount` INTEGER NOT NULL (๋ฒ ํ
์ก)
- `symbols` TEXT NOT NULL (๋ฆด ๊ฒฐ๊ณผ, ์: "๐,๐,๐")
- `payout` INTEGER NOT NULL (๋ฐฐ๋น์ก, 0์ด๋ฉด ๊ฝ)
- `is_jackpot` INTEGER DEFAULT 0 (์ญํ ์ฌ๋ถ, 1=์ญํ)
- `created_at` TEXT (์คํ ์ผ์)
3. `statistics` ํ
์ด๋ธ:
- `mb_id` TEXT PRIMARY KEY
- `total_spins` INTEGER DEFAULT 0 (์ด ์คํ ํ์)
- `total_bet` INTEGER DEFAULT 0 (์ด ๋ฒ ํ
์ก)
- `total_win` INTEGER DEFAULT 0 (์ด ํ๋์ก)
- `jackpot_count` INTEGER DEFAULT 0 (์ญํ ๋ฌ์ฑ ํ์)
**PHP ์ฝ๋ ์ถ๊ฐ**:
`app/modules/casino/index.php` ์๋จ ๋ก์ง ๋ถ๋ถ์ ๋ค์ ์ฝ๋๋ฅผ ์ถ๊ฐํ์ธ์:
```php
// DB ์ด๊ธฐํ
@mkdir(G5_DATA_PATH.'/modules', 0707, true);
$db = new PDO('sqlite:'.G5_DATA_PATH.'/modules/casino.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// ํ
์ด๋ธ ์์ฑ (์ฒซ ์ ๊ทผ ์)
$db->exec("
CREATE TABLE IF NOT EXISTS user_coins (
mb_id TEXT PRIMARY KEY,
coins INTEGER NOT NULL DEFAULT 1000,
last_bonus_date TEXT,
created_at TEXT DEFAULT (datetime('now'))
)
");
$db->exec("
CREATE TABLE IF NOT EXISTS spin_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mb_id TEXT NOT NULL,
bet_amount INTEGER NOT NULL,
symbols TEXT NOT NULL,
payout INTEGER NOT NULL,
is_jackpot INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
)
");
$db->exec("
CREATE TABLE IF NOT EXISTS statistics (
mb_id TEXT PRIMARY KEY,
total_spins INTEGER DEFAULT 0,
total_bet INTEGER DEFAULT 0,
total_win INTEGER DEFAULT 0,
jackpot_count INTEGER DEFAULT 0
)
");
// ํ์ฌ ์ฌ์ฉ์ ์ฝ์ธ ์กฐํ (์์ผ๋ฉด ์ด๊ธฐ 1000์ฝ์ธ ์ง๊ธ)
$stmt = $db->prepare('SELECT coins, last_bonus_date FROM user_coins WHERE mb_id = ?');
$stmt->execute([$member['mb_id']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user) {
$db->prepare('INSERT INTO user_coins (mb_id, coins) VALUES (?, 1000)')->execute([$member['mb_id']]);
$user = ['coins' => 1000, 'last_bonus_date' => null];
}
$coins = (int)$user['coins'];
$last_bonus_date = $user['last_bonus_date'];
```
**๋ณธ๋ฌธ์ ์ฝ์ธ ์์ก ํ์**:
`<main>` ํ๊ทธ ์์ ๋ค์ HTML์ ์ถ๊ฐํ์ธ์:
```html
<div style="background: var(--m-surface); padding: 20px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); margin-bottom: 24px;">
<h2 style="font-size: 1.5rem; font-weight: 700; margin-bottom: 12px;">๐ฐ ๋ณด์ ์ฝ์ธ</h2>
<p style="font-size: 2.5rem; font-weight: 900; color: #FFD700;"><?php echo number_format($coins); ?> ์ฝ์ธ</p>
</div>
```
๋ธ๋ผ์ฐ์ ์๋ก๊ณ ์นจ ํ ์ฝ์ธ ์์ก์ด ํ์๋๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: SQLite DB๊ฐ G5_DATA_PATH/modules/casino.sqlite ๊ฒฝ๋ก์ ์์ฑ๋๊ณ , user_coins/spin_history/statistics ํ ์ด๋ธ์ด ์๋์ผ๋ก ๋ง๋ค์ด์ง๋๋ค. ํ์ด์ง ์ ์ ์ ํ์ฌ ์ฌ์ฉ์์ ์ฝ์ธ ์์ก(์ด๊ธฐ 1000์ฝ์ธ)์ด ํ๋ฉด์ ํ์๋ฉ๋๋ค.
**Step 3: ์ฌ๋กฏ๋จธ์ UI & ๋ฆด ์ปดํฌ๋ํธ ๊ตฌ์ถ**
์ฌ๋กฏ๋จธ์ ์ ์๊ฐ์ ์ธํฐํ์ด์ค๋ฅผ ๋ง๋ญ๋๋ค.
**์๊ตฌ์ฌํญ**:
1. 3๊ฐ์ ๋ฆด(์ธ๋ก ์ปฌ๋ผ)์ ๊ฐ๋ก๋ก ๋ฐฐ์น
2. ๊ฐ ๋ฆด์ ์นด๋ ์คํ์ผ ๋ฐ์ค ์์ ์ฌ๋ณผ ์ด๋ชจ์ง ํ์
3. ์ฌ๋ณผ 7์ข
: ๐(์ฒด๋ฆฌ), ๐(๋ ๋ชฌ), ๐(์ค๋ ์ง), ๐(์๋ฐ), โญ(๋ณ), ๐(๋ค์ด์), 7๏ธโฃ(์ธ๋ธ)
4. ๋ฒ ํ
๊ธ์ก ์ ํ ๋ฒํผ: 10/50/100/500์ฝ์ธ
5. SPIN ๋ฒํผ (๋ฆด ํ์ ํธ๋ฆฌ๊ฑฐ)
6. ๋ฐฐ๋นํ ์๋ด (๊ฐ ์ฌ๋ณผ๋ณ ๋ฐฐ๋น๋ฅ )
**HTML ์ฝ๋ ์ถ๊ฐ** (`<main>` ํ๊ทธ ์):
```html
<!-- ์ฌ๋กฏ๋จธ์ ๋ณธ์ฒด -->
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px; border-radius: var(--m-radius-lg); box-shadow: 0 20px 60px rgba(0,0,0,0.3); margin-bottom: 32px;">
<!-- ๋ฆด ์ปจํ
์ด๋ -->
<div id="reels" style="display: flex; gap: 16px; justify-content: center; margin-bottom: 32px;">
<div class="reel" data-reel="0" style="background: #fff; width: 120px; height: 160px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 5rem; box-shadow: inset 0 4px 8px rgba(0,0,0,0.1);">
<span class="symbol">๐</span>
</div>
<div class="reel" data-reel="1" style="background: #fff; width: 120px; height: 160px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 5rem; box-shadow: inset 0 4px 8px rgba(0,0,0,0.1);">
<span class="symbol">๐</span>
</div>
<div class="reel" data-reel="2" style="background: #fff; width: 120px; height: 160px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 5rem; box-shadow: inset 0 4px 8px rgba(0,0,0,0.1);">
<span class="symbol">๐</span>
</div>
</div>
<!-- ๋ฒ ํ
๊ธ์ก ์ ํ -->
<div style="display: flex; gap: 12px; justify-content: center; margin-bottom: 20px;">
<button class="bet-btn" data-amount="10" style="background: #fff; color: #667eea; border: none; padding: 12px 24px; border-radius: 8px; font-weight: 700; cursor: pointer; transition: transform 0.2s;">10 ์ฝ์ธ</button>
<button class="bet-btn" data-amount="50" style="background: #fff; color: #667eea; border: none; padding: 12px 24px; border-radius: 8px; font-weight: 700; cursor: pointer; transition: transform 0.2s;">50 ์ฝ์ธ</button>
<button class="bet-btn active" data-amount="100" style="background: #FFD700; color: #333; border: none; padding: 12px 24px; border-radius: 8px; font-weight: 700; cursor: pointer; transition: transform 0.2s;">100 ์ฝ์ธ</button>
<button class="bet-btn" data-amount="500" style="background: #fff; color: #667eea; border: none; padding: 12px 24px; border-radius: 8px; font-weight: 700; cursor: pointer; transition: transform 0.2s;">500 ์ฝ์ธ</button>
</div>
<!-- SPIN ๋ฒํผ -->
<div style="text-align: center;">
<button id="spinBtn" style="background: #FFD700; color: #333; border: none; padding: 20px 60px; border-radius: 50px; font-size: 1.5rem; font-weight: 900; cursor: pointer; box-shadow: 0 8px 20px rgba(255,215,0,0.4); transition: all 0.3s;">๐ฐ SPIN</button>
</div>
</div>
<!-- ๋ฐฐ๋นํ -->
<div style="background: var(--m-surface); padding: 24px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); margin-bottom: 32px;">
<h3 style="font-size: 1.25rem; font-weight: 700; margin-bottom: 16px;">๐ ๋ฐฐ๋นํ</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
<div style="padding: 12px; background: rgba(255,215,0,0.1); border-radius: 8px;">7๏ธโฃ ์ธ๋ธ x3 โ <strong>100๋ฐฐ</strong></div>
<div style="padding: 12px; background: rgba(138,43,226,0.1); border-radius: 8px;">๐ ๋ค์ด์ x3 โ <strong>50๋ฐฐ</strong></div>
<div style="padding: 12px; background: rgba(255,215,0,0.1); border-radius: 8px;">โญ ๋ณ x3 โ <strong>20๋ฐฐ</strong></div>
<div style="padding: 12px; background: rgba(255,99,71,0.1); border-radius: 8px;">๐ ์๋ฐ x3 โ <strong>10๋ฐฐ</strong></div>
<div style="padding: 12px; background: rgba(255,165,0,0.1); border-radius: 8px;">๐ ์ค๋ ์ง x3 โ <strong>5๋ฐฐ</strong></div>
<div style="padding: 12px; background: rgba(255,255,0,0.1); border-radius: 8px;">๐ ๋ ๋ชฌ x3 โ <strong>3๋ฐฐ</strong></div>
<div style="padding: 12px; background: rgba(220,20,60,0.1); border-radius: 8px;">๐ ์ฒด๋ฆฌ x3 โ <strong>2๋ฐฐ</strong></div>
</div>
<p style="margin-top: 12px; color: var(--m-text-muted); font-size: 0.875rem;">* 2๊ฐ ์ผ์น ์ ๋ฒ ํ
์ก ๋ฐํ, ๋ถ์ผ์น ์ ๋ฒ ํ
์ก ์ฐจ๊ฐ</p>
</div>
```
**JavaScript ์ฝ๋ ์ถ๊ฐ** (ํ์ด์ง ํ๋จ, `</main>` ๋ค์):
```html
<script>
let selectedBet = 100; // ๊ธฐ๋ณธ ๋ฒ ํ
์ก
// ๋ฒ ํ
๋ฒํผ ํด๋ฆญ ์ด๋ฒคํธ
document.querySelectorAll('.bet-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.bet-btn').forEach(b => {
b.style.background = '#fff';
b.style.color = '#667eea';
b.classList.remove('active');
});
btn.style.background = '#FFD700';
btn.style.color = '#333';
btn.classList.add('active');
selectedBet = parseInt(btn.dataset.amount);
});
});
// SPIN ๋ฒํผ ํด๋ฆญ (์์ - ๋ค์ ๋จ๊ณ์์ ๊ตฌํ)
document.getElementById('spinBtn').addEventListener('click', () => {
alert('SPIN ๋ฒํผ ํด๋ฆญ! ๋ฒ ํ
์ก: ' + selectedBet + '์ฝ์ธ');
});
</script>
```
๋ธ๋ผ์ฐ์ ์๋ก๊ณ ์นจ ํ ์ฌ๋กฏ๋จธ์ UI๊ฐ ํ์๋๊ณ , ๋ฒ ํ
๋ฒํผ ํด๋ฆญ ์ ์คํ์ผ์ด ๋ณ๊ฒฝ๋๋ฉฐ, SPIN ๋ฒํผ ํด๋ฆญ ์ alert๊ฐ ๋จ๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ฌ๋กฏ๋จธ์ UI๊ฐ ํ๋ฉด์ ๋ ๋๋ง๋ฉ๋๋ค. 3๊ฐ์ ๋ฆด์ด ๊ฐ๋ก๋ก ๋ฐฐ์น๋๊ณ ๊ฐ ๋ฆด์ ์ฌ๋ณผ ์ด๋ชจ์ง๊ฐ ํ์๋ฉ๋๋ค. ๋ฒ ํ ๊ธ์ก ์ ํ ๋ฒํผ(10/50/100/500)์ด ํ์๋๋ฉฐ ํด๋ฆญ ์ ํ์ฑ ์คํ์ผ๋ก ์ ํ๋ฉ๋๋ค. SPIN ๋ฒํผ ํด๋ฆญ ์ alert๋ก ์ ํ๋ ๋ฒ ํ ์ก์ด ํ์๋ฉ๋๋ค. ๋ฐฐ๋นํ๊ฐ ํ๋จ์ ๊น๋ํ๊ฒ ์ ๋ฆฌ๋์ด ํ์๋ฉ๋๋ค.
**Step 4: ๋ฆด ์คํ ์ ๋๋ฉ์ด์
& ๋๋ค ์ฌ๋ณผ ์์ฑ**
๋ฆด์ด ํ์ ํ๋ ์ ๋๋ฉ์ด์
์ CSS๋ก ๊ตฌํํ๊ณ , JavaScript๋ก ๋๋ค ์ฌ๋ณผ์ ์์ฑํฉ๋๋ค.
**์๊ตฌ์ฌํญ**:
1. SPIN ๋ฒํผ ํด๋ฆญ ์ 3๊ฐ ๋ฆด์ด ์์ฐจ์ ์ผ๋ก ํ์ ์์
2. ๊ฐ ๋ฆด์ 0.5์ด ๊ฐ๊ฒฉ์ผ๋ก ๋ฉ์ถค (์ฒซ ๋ฒ์งธ โ ๋ ๋ฒ์งธ โ ์ธ ๋ฒ์งธ)
3. ํ์ ์ค ์ฌ๋ณผ์ด ๋ธ๋ฌ ํจ๊ณผ๋ก ํ๋ฆฟํ๊ฒ ๋ณด์
4. ๋ฉ์ถ ๋ ์ต์ข
์ฌ๋ณผ์ด ์ ๋ช
ํ๊ฒ ํ์
5. 7์ข
์ฌ๋ณผ ์ค ๋๋ค ์ ํ (๊ฐ์ค์น ์์ด ๊ท ๋ฑ ํ๋ฅ )
**CSS ์ ๋๋ฉ์ด์
์ถ๊ฐ** (`<style>` ํ๊ทธ๋ฅผ `<main>` ์์ ์ฝ์
):
```html
<style>
@keyframes spin {
0% { transform: translateY(0); filter: blur(0); }
50% { transform: translateY(-20px); filter: blur(8px); }
100% { transform: translateY(0); filter: blur(0); }
}
.reel.spinning .symbol {
animation: spin 0.1s linear infinite;
}
.reel {
transition: all 0.3s ease;
}
.reel.stopped .symbol {
animation: none;
filter: blur(0);
}
#spinBtn:hover {
transform: scale(1.05);
box-shadow: 0 12px 30px rgba(255,215,0,0.6);
}
#spinBtn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.bet-btn:hover {
transform: scale(1.05);
}
</style>
```
**JavaScript ์คํ ๋ก์ง ๊ตฌํ** (๊ธฐ์กด `<script>` ํ๊ทธ ๋ด์ฉ์ ๋ค์์ผ๋ก ๊ต์ฒด):
```javascript
const symbols = ['๐', '๐', '๐', '๐', 'โญ', '๐', '7๏ธโฃ'];
let selectedBet = 100;
let isSpinning = false;
// ๋ฒ ํ
๋ฒํผ ํด๋ฆญ ์ด๋ฒคํธ
document.querySelectorAll('.bet-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.bet-btn').forEach(b => {
b.style.background = '#fff';
b.style.color = '#667eea';
b.classList.remove('active');
});
btn.style.background = '#FFD700';
btn.style.color = '#333';
btn.classList.add('active');
selectedBet = parseInt(btn.dataset.amount);
});
});
// ๋๋ค ์ฌ๋ณผ ์ ํ
function getRandomSymbol() {
return symbols[Math.floor(Math.random() * symbols.length)];
}
// ๋ฆด ์คํ ์ ๋๋ฉ์ด์
function spinReel(reelIndex, finalSymbol, delay) {
return new Promise(resolve => {
const reel = document.querySelector(`.reel[data-reel="${reelIndex}"]`);
const symbolEl = reel.querySelector('.symbol');
setTimeout(() => {
reel.classList.add('spinning');
// 0.1์ด๋ง๋ค ๋๋ค ์ฌ๋ณผ ๋ณ๊ฒฝ (ํ์ ํจ๊ณผ)
const interval = setInterval(() => {
symbolEl.textContent = getRandomSymbol();
}, 100);
// 2์ด ํ ๋ฉ์ถค
setTimeout(() => {
clearInterval(interval);
symbolEl.textContent = finalSymbol;
reel.classList.remove('spinning');
reel.classList.add('stopped');
setTimeout(() => reel.classList.remove('stopped'), 300);
resolve();
}, 2000);
}, delay);
});
}
// SPIN ๋ฒํผ ํด๋ฆญ
document.getElementById('spinBtn').addEventListener('click', async () => {
if (isSpinning) return;
const currentCoins = parseInt(document.querySelector('[style*="font-size: 2.5rem"]').textContent.replace(/[^0-9]/g, ''));
if (currentCoins < selectedBet) {
alert('์ฝ์ธ์ด ๋ถ์กฑํฉ๋๋ค!');
return;
}
isSpinning = true;
document.getElementById('spinBtn').disabled = true;
// ๋๋ค ์ฌ๋ณผ 3๊ฐ ์์ฑ
const results = [getRandomSymbol(), getRandomSymbol(), getRandomSymbol()];
// ๋ฆด ์์ฐจ ํ์
await spinReel(0, results[0], 0);
await spinReel(1, results[1], 0);
await spinReel(2, results[2], 0);
// ๊ฒฐ๊ณผ ์ฒ๋ฆฌ (๋ค์ ๋จ๊ณ์์ ๊ตฌํ)
console.log('๊ฒฐ๊ณผ:', results);
alert('๊ฒฐ๊ณผ: ' + results.join(' '));
isSpinning = false;
document.getElementById('spinBtn').disabled = false;
});
```
๋ธ๋ผ์ฐ์ ์๋ก๊ณ ์นจ ํ SPIN ๋ฒํผ์ ํด๋ฆญํ์ธ์. ๋ฆด์ด ์์ฐจ์ ์ผ๋ก ํ์ ํ๋ฉฐ ๋ธ๋ฌ ํจ๊ณผ๊ฐ ์ ์ฉ๋๊ณ , 2์ด ํ ๋๋ค ์ฌ๋ณผ์ด ํ์๋๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: SPIN ๋ฒํผ ํด๋ฆญ ์ 3๊ฐ์ ๋ฆด์ด ์์ฐจ์ ์ผ๋ก ํ์ ์ ๋๋ฉ์ด์ ์ ์์ํฉ๋๋ค. ํ์ ์ค ์ฌ๋ณผ์ด ๋น ๋ฅด๊ฒ ๋ณ๊ฒฝ๋๋ฉฐ ๋ธ๋ฌ ํจ๊ณผ๊ฐ ์ ์ฉ๋ฉ๋๋ค. ๊ฐ ๋ฆด์ 2์ด ํ ๋ฉ์ถ๊ณ ์ต์ข ๋๋ค ์ฌ๋ณผ์ด ์ ๋ช ํ๊ฒ ํ์๋ฉ๋๋ค. ์คํ ์ค์๋ SPIN ๋ฒํผ์ด ๋นํ์ฑํ๋์ด ์ค๋ณต ํด๋ฆญ์ด ๋ฐฉ์ง๋ฉ๋๋ค. ์ฝ์๊ณผ alert๋ก ์ต์ข ๊ฒฐ๊ณผ๊ฐ ์ถ๋ ฅ๋ฉ๋๋ค.
**Step 5: ๋ฐฐ๋น ๊ณ์ฐ & ์ฝ์ธ ์
๋ฐ์ดํธ (์๋ฒ ๋ก์ง)**
์คํ ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ๋ฐฐ๋น์ ๊ณ์ฐํ๊ณ , ์ฌ์ฉ์ ์ฝ์ธ์ ์
๋ฐ์ดํธํ๋ ์๋ฒ ๋ก์ง์ ๊ตฌํํฉ๋๋ค.
**์๊ตฌ์ฌํญ**:
1. AJAX๋ก ์คํ ๊ฒฐ๊ณผ๋ฅผ ์๋ฒ์ ์ ์ก
2. ์๋ฒ์์ ๋ฐฐ๋น ๊ณ์ฐ:
- 3๊ฐ ์ผ์น: ์ฌ๋ณผ๋ณ ๋ฐฐ๋น ์ง๊ธ
- 2๊ฐ ์ผ์น: ๋ฒ ํ
์ก ๋ฐํ
- ๋ถ์ผ์น: ๋ฒ ํ
์ก ์ฐจ๊ฐ
3. ์ฝ์ธ ์์ก ์
๋ฐ์ดํธ (DB ์ ์ฅ)
4. ์คํ ํ์คํ ๋ฆฌ ๊ธฐ๋ก
5. ํต๊ณ ์
๋ฐ์ดํธ (์ด ์คํ, ์ด ๋ฒ ํ
, ์ด ํ๋)
**PHP ์๋ฒ ๋ก์ง ์ถ๊ฐ** (`app/modules/casino/index.php` ์๋จ, DB ์ด๊ธฐํ ๋ค์):
```php
// AJAX ์์ฒญ ์ฒ๋ฆฌ
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'spin') {
header('Content-Type: application/json');
$bet_amount = (int)$_POST['bet_amount'];
$symbols = json_decode($_POST['symbols'], true);
// ์ฝ์ธ ์์ก ํ์ธ
$stmt = $db->prepare('SELECT coins FROM user_coins WHERE mb_id = ?');
$stmt->execute([$member['mb_id']]);
$current_coins = (int)$stmt->fetchColumn();
if ($current_coins < $bet_amount) {
echo json_encode(['success' => false, 'message' => '์ฝ์ธ์ด ๋ถ์กฑํฉ๋๋ค.']);
exit;
}
// ๋ฐฐ๋น ๊ณ์ฐ
$symbol_payouts = [
'๐' => 2,
'๐' => 3,
'๐' => 5,
'๐' => 10,
'โญ' => 20,
'๐' => 50,
'7๏ธโฃ' => 100
];
$unique = array_unique($symbols);
$counts = array_count_values($symbols);
$payout = 0;
$is_jackpot = 0;
$result_message = '';
if (count($unique) === 1) {
// 3๊ฐ ์ผ์น
$symbol = $symbols[0];
$multiplier = $symbol_payouts[$symbol];
$payout = $bet_amount * $multiplier;
$result_message = "๐ {$symbol} 3๊ฐ ์ผ์น! {$multiplier}๋ฐฐ ๋น์ฒจ!";
if ($symbol === '7๏ธโฃ') {
$is_jackpot = 1;
$result_message = "๐ฐ JACKPOT! ์ธ๋ธ 3๊ฐ! {$multiplier}๋ฐฐ ๋๋ฐ!";
}
} elseif (max($counts) === 2) {
// 2๊ฐ ์ผ์น - ๋ฒ ํ
์ก ๋ฐํ
$payout = $bet_amount;
$result_message = "๐ 2๊ฐ ์ผ์น! ๋ฒ ํ
์ก ๋ฐํ";
} else {
// ๋ถ์ผ์น
$payout = 0;
$result_message = "๐ข ๊ฝ! ๋ค์ ๊ธฐํ์...";
}
// ์ฝ์ธ ์
๋ฐ์ดํธ
$new_coins = $current_coins - $bet_amount + $payout;
$db->prepare('UPDATE user_coins SET coins = ? WHERE mb_id = ?')->execute([$new_coins, $member['mb_id']]);
// ์คํ ํ์คํ ๋ฆฌ ๊ธฐ๋ก
$symbols_str = implode(',', $symbols);
$db->prepare('INSERT INTO spin_history (mb_id, bet_amount, symbols, payout, is_jackpot) VALUES (?, ?, ?, ?, ?)')->execute([
$member['mb_id'],
$bet_amount,
$symbols_str,
$payout,
$is_jackpot
]);
// ํต๊ณ ์
๋ฐ์ดํธ
$db->exec("
INSERT INTO statistics (mb_id, total_spins, total_bet, total_win, jackpot_count)
VALUES ('{$member['mb_id']}', 1, {$bet_amount}, {$payout}, {$is_jackpot})
ON CONFLICT(mb_id) DO UPDATE SET
total_spins = total_spins + 1,
total_bet = total_bet + {$bet_amount},
total_win = total_win + {$payout},
jackpot_count = jackpot_count + {$is_jackpot}
");
echo json_encode([
'success' => true,
'payout' => $payout,
'new_coins' => $new_coins,
'message' => $result_message,
'is_jackpot' => $is_jackpot
]);
exit;
}
```
**JavaScript AJAX ์์ฒญ ์ถ๊ฐ** (๊ธฐ์กด SPIN ๋ฒํผ ํด๋ฆญ ๋ก์ง ์์ ):
```javascript
// SPIN ๋ฒํผ ํด๋ฆญ (๊ธฐ์กด ์ฝ๋๋ฅผ ๋ค์์ผ๋ก ๊ต์ฒด)
document.getElementById('spinBtn').addEventListener('click', async () => {
if (isSpinning) return;
const currentCoins = parseInt(document.querySelector('[style*="font-size: 2.5rem"]').textContent.replace(/[^0-9]/g, ''));
if (currentCoins < selectedBet) {
alert('์ฝ์ธ์ด ๋ถ์กฑํฉ๋๋ค!');
return;
}
isSpinning = true;
document.getElementById('spinBtn').disabled = true;
// ๋๋ค ์ฌ๋ณผ 3๊ฐ ์์ฑ
const results = [getRandomSymbol(), getRandomSymbol(), getRandomSymbol()];
// ๋ฆด ์์ฐจ ํ์
await spinReel(0, results[0], 0);
await spinReel(1, results[1], 0);
await spinReel(2, results[2], 0);
// ์๋ฒ์ ๊ฒฐ๊ณผ ์ ์ก
const formData = new FormData();
formData.append('action', 'spin');
formData.append('bet_amount', selectedBet);
formData.append('symbols', JSON.stringify(results));
const response = await fetch(window.location.href, {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
// ์ฝ์ธ ์์ก ์
๋ฐ์ดํธ
document.querySelector('[style*="font-size: 2.5rem"]').textContent = data.new_coins.toLocaleString() + ' ์ฝ์ธ';
// ๊ฒฐ๊ณผ ๋ฉ์์ง ํ์
alert(data.message + '\n\nํ๋: ' + data.payout.toLocaleString() + ' ์ฝ์ธ\n์์ก: ' + data.new_coins.toLocaleString() + ' ์ฝ์ธ');
// ์ญํ ์ ๋๋ฉ์ด์
(๋ค์ ๋จ๊ณ์์ ๊ตฌํ)
if (data.is_jackpot) {
console.log('๐ฐ JACKPOT!');
}
} else {
alert(data.message);
}
isSpinning = false;
document.getElementById('spinBtn').disabled = false;
});
```
๋ธ๋ผ์ฐ์ ์๋ก๊ณ ์นจ ํ SPIN์ ์ฌ๋ฌ ๋ฒ ํด๋ฆญํ์ธ์. ์ฝ์ธ ์์ก์ด ์ค์๊ฐ์ผ๋ก ์
๋ฐ์ดํธ๋๊ณ , ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ๋ฐฐ๋น์ด ์ง๊ธ๋๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: SPIN ๋ฒํผ ํด๋ฆญ ์ ๋ฆด์ด ํ์ ํ๊ณ , ์๋ฒ๋ก ๊ฒฐ๊ณผ๊ฐ ์ ์ก๋ฉ๋๋ค. ์๋ฒ์์ ๋ฐฐ๋น์ ๊ณ์ฐํ์ฌ ์ฝ์ธ ์์ก์ด ์ค์๊ฐ์ผ๋ก ์ ๋ฐ์ดํธ๋ฉ๋๋ค. 3๊ฐ ์ผ์น ์ ๋ฐฐ๋น ์ง๊ธ, 2๊ฐ ์ผ์น ์ ๋ฒ ํ ์ก ๋ฐํ, ๋ถ์ผ์น ์ ์ฐจ๊ฐ์ด ์ ์ ์๋ํฉ๋๋ค. ์คํ ํ์คํ ๋ฆฌ์ ํต๊ณ๊ฐ DB์ ๊ธฐ๋ก๋ฉ๋๋ค. alert๋ก ๊ฒฐ๊ณผ ๋ฉ์์ง์ ํ๋ ์ฝ์ธ, ์์ก์ด ํ์๋ฉ๋๋ค.
**Step 6: ์ญํ ์ถํ ์ ๋๋ฉ์ด์
& ์ฌ์ด๋ ํจ๊ณผ**
์ธ๋ธ 3๊ฐ ์ผ์น(์ญํ) ์ ํน์ ํจ๊ณผ๋ฅผ ์ถ๊ฐํฉ๋๋ค.
**์๊ตฌ์ฌํญ**:
1. ์ญํ ์ ์ ์ฒด ํ๋ฉด ์ถํ ๋ชจ๋ฌ ํ์
2. CSS ํํฐํด ์ ๋๋ฉ์ด์
(๋จ์ด์ง๋ ๊ธํ ํจ๊ณผ)
3. Web Audio API๋ก ์น๋ฆฌ ์ฌ์ด๋ ์ฌ์
4. ๋ชจ๋ฌ ๋ซ๊ธฐ ๋ฒํผ
**HTML ๋ชจ๋ฌ ์ถ๊ฐ** (`<main>` ํ๊ทธ ๋ค์์ ์ฝ์
):
```html
<!-- ์ญํ ๋ชจ๋ฌ -->
<div id="jackpotModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 9999; justify-content: center; align-items: center;">
<div style="text-align: center; position: relative;">
<div id="confetti" style="position: absolute; top: 0; left: 50%; transform: translateX(-50%); width: 100%; height: 100%; pointer-events: none;"></div>
<div style="background: linear-gradient(135deg, #FFD700, #FFA500); padding: 60px; border-radius: 24px; box-shadow: 0 30px 80px rgba(255,215,0,0.6);">
<h1 style="font-size: 4rem; font-weight: 900; color: #fff; text-shadow: 0 4px 8px rgba(0,0,0,0.3); margin-bottom: 20px;">๐ฐ JACKPOT! ๐ฐ</h1>
<p style="font-size: 2rem; color: #fff; margin-bottom: 12px;">์ธ๋ธ 3๊ฐ ์ผ์น!</p>
<p id="jackpotAmount" style="font-size: 3rem; font-weight: 900; color: #fff; text-shadow: 0 4px 8px rgba(0,0,0,0.3);"></p>
<button onclick="closeJackpotModal()" style="margin-top: 32px; background: #fff; color: #FFA500; border: none; padding: 16px 48px; border-radius: 50px; font-size: 1.25rem; font-weight: 700; cursor: pointer; box-shadow: 0 8px 20px rgba(0,0,0,0.2);">ํ์ธ</button>
</div>
</div>
</div>
```
**CSS ํํฐํด ์ ๋๋ฉ์ด์
์ถ๊ฐ** (๊ธฐ์กด `<style>` ํ๊ทธ ์์ ์ถ๊ฐ):
```css
@keyframes fall {
0% { transform: translateY(-100vh) rotate(0deg); opacity: 1; }
100% { transform: translateY(100vh) rotate(360deg); opacity: 0; }
}
.coin {
position: absolute;
font-size: 2rem;
animation: fall linear infinite;
}
#jackpotModal {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
```
**JavaScript ์ญํ ๋ก์ง ์ถ๊ฐ** (๊ธฐ์กด `<script>` ํ๊ทธ ์์ ์ถ๊ฐ):
```javascript
// Web Audio API ์ฌ์ด๋ ์์ฑ
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
function playWinSound() {
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.setValueAtTime(523.25, audioContext.currentTime); // C5
oscillator.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.1); // E5
oscillator.frequency.setValueAtTime(783.99, audioContext.currentTime + 0.2); // G5
oscillator.frequency.setValueAtTime(1046.5, audioContext.currentTime + 0.3); // C6
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.5);
}
function createConfetti() {
const confettiContainer = document.getElementById('confetti');
confettiContainer.innerHTML = '';
for (let i = 0; i < 50; i++) {
const coin = document.createElement('div');
coin.className = 'coin';
coin.textContent = '๐ฐ';
coin.style.left = Math.random() * 100 + '%';
coin.style.animationDuration = (Math.random() * 2 + 2) + 's';
coin.style.animationDelay = Math.random() * 2 + 's';
confettiContainer.appendChild(coin);
}
}
function showJackpotModal(amount) {
document.getElementById('jackpotAmount').textContent = amount.toLocaleString() + ' ์ฝ์ธ ํ๋!';
document.getElementById('jackpotModal').style.display = 'flex';
createConfetti();
playWinSound();
}
function closeJackpotModal() {
document.getElementById('jackpotModal').style.display = 'none';
}
// ๊ธฐ์กด SPIN ๋ก์ง์ ์ญํ ๋ถ๋ถ ์์
// (๊ธฐ์กด ์ฝ๋์์ "if (data.is_jackpot)" ๋ถ๋ถ์ ๋ค์์ผ๋ก ๊ต์ฒด)
if (data.is_jackpot) {
setTimeout(() => showJackpotModal(data.payout), 500);
}
```
๋ธ๋ผ์ฐ์ ์๋ก๊ณ ์นจ ํ ์ธ๋ธ 3๊ฐ๊ฐ ๋์ฌ ๋๊น์ง SPIN์ ๋ฐ๋ณตํ์ธ์ (ํ๋ฅ ์ด ๋ฎ์ผ๋ฏ๋ก ํ
์คํธ์ฉ์ผ๋ก ์๋ฒ ๋ก์ง์์ ์์๋ก ์ญํ ํ๋ฅ ์ ๋์ด๊ฑฐ๋, ์ฝ์์์ `showJackpotModal(10000)` ํจ์๋ฅผ ์ง์ ํธ์ถํ์ฌ ํ
์คํธ ๊ฐ๋ฅ). ์ญํ ๋ชจ๋ฌ์ด ํ์๋๊ณ ๊ธํ ์ ๋๋ฉ์ด์
๊ณผ ์ฌ์ด๋๊ฐ ์ฌ์๋๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ธ๋ธ 3๊ฐ ์ผ์น(์ญํ) ์ ์ ์ฒด ํ๋ฉด ์ถํ ๋ชจ๋ฌ์ด ํ์๋ฉ๋๋ค. ๋ชจ๋ฌ ๋ฐฐ๊ฒฝ์ ๊ธํ ์ด๋ชจ์ง๊ฐ ์์์ ์๋๋ก ๋จ์ด์ง๋ ํํฐํด ์ ๋๋ฉ์ด์ ์ด ์ฌ์๋ฉ๋๋ค. Web Audio API๋ก ์์ฑ๋ ์น๋ฆฌ ๋ฉ๋ก๋๊ฐ ์ฌ์๋ฉ๋๋ค. ๋ชจ๋ฌ์๋ ํ๋ ์ฝ์ธ์ด ํ์๋๊ณ , ํ์ธ ๋ฒํผ์ผ๋ก ๋ซ์ ์ ์์ต๋๋ค.
**Step 7: ์คํ ํ์คํ ๋ฆฌ & ์ญํ ๋ช
์์ ์ ๋น**
์ต๊ทผ ์คํ ๊ธฐ๋ก๊ณผ ์ญํ ๋ฌ์ฑ์ ๋ชฉ๋ก์ ํ์ํฉ๋๋ค.
**์๊ตฌ์ฌํญ**:
1. ์ต๊ทผ 20ํ ์คํ ๊ฒฐ๊ณผ ํ
์ด๋ธ (์ฌ๋ณผ, ๋ฒ ํ
์ก, ๋ฐฐ๋น)
2. ์ญํ ๋ช
์์ ์ ๋น (๋๋ค์, ๋ ์ง, ๋ฐฐ๋น์ก)
3. ๊ฐ์ธ ํต๊ณ ์นด๋ (์ด ์คํ, ์ด ๋ฒ ํ
, ์ด ํ๋, ์น๋ฅ )
**PHP ๋ฐ์ดํฐ ์กฐํ ์ถ๊ฐ** (`app/modules/casino/index.php` ์๋จ, DB ์ด๊ธฐํ ๋ค์):
```php
// ์ต๊ทผ ์คํ ํ์คํ ๋ฆฌ ์กฐํ
$stmt = $db->prepare('SELECT * FROM spin_history WHERE mb_id = ? ORDER BY created_at DESC LIMIT 20');
$stmt->execute([$member['mb_id']]);
$recent_spins = $stmt->fetchAll(PDO::FETCH_ASSOC);
// ์ญํ ๋ช
์์ ์ ๋น
$stmt = $db->query('SELECT sh.*, uc.mb_id FROM spin_history sh JOIN user_coins uc ON sh.mb_id = uc.mb_id WHERE sh.is_jackpot = 1 ORDER BY sh.created_at DESC LIMIT 10');
$jackpot_hall = $stmt->fetchAll(PDO::FETCH_ASSOC);
// ๊ฐ์ธ ํต๊ณ
$stmt = $db->prepare('SELECT * FROM statistics WHERE mb_id = ?');
$stmt->execute([$member['mb_id']]);
$stats = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$stats) {
$stats = ['total_spins' => 0, 'total_bet' => 0, 'total_win' => 0, 'jackpot_count' => 0];
}
$win_rate = $stats['total_spins'] > 0 ? round(($stats['total_win'] / $stats['total_bet']) * 100, 1) : 0;
```
**HTML ํ์คํ ๋ฆฌ ์น์
์ถ๊ฐ** (๋ฐฐ๋นํ ๋ค์์ ์ฝ์
):
```html
<!-- ๊ฐ์ธ ํต๊ณ -->
<div style="background: var(--m-surface); padding: 24px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); margin-bottom: 32px;">
<h3 style="font-size: 1.25rem; font-weight: 700; margin-bottom: 16px;">๐ ๋์ ํต๊ณ</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px;">
<div style="text-align: center; padding: 16px; background: rgba(103,126,234,0.1); border-radius: 8px;">
<div style="font-size: 0.875rem; color: var(--m-text-muted); margin-bottom: 8px;">์ด ์คํ</div>
<div style="font-size: 1.5rem; font-weight: 700;"><?php echo number_format($stats['total_spins']); ?>ํ</div>
</div>
<div style="text-align: center; padding: 16px; background: rgba(255,99,71,0.1); border-radius: 8px;">
<div style="font-size: 0.875rem; color: var(--m-text-muted); margin-bottom: 8px;">์ด ๋ฒ ํ
</div>
<div style="font-size: 1.5rem; font-weight: 700;"><?php echo number_format($stats['total_bet']); ?></div>
</div>
<div style="text-align: center; padding: 16px; background: rgba(50,205,50,0.1); border-radius: 8px;">
<div style="font-size: 0.875rem; color: var(--m-text-muted); margin-bottom: 8px;">์ด ํ๋</div>
<div style="font-size: 1.5rem; font-weight: 700;"><?php echo number_format($stats['total_win']); ?></div>
</div>
<div style="text-align: center; padding: 16px; background: rgba(255,215,0,0.1); border-radius: 8px;">
<div style="font-size: 0.875rem; color: var(--m-text-muted); margin-bottom: 8px;">์์ต๋ฅ </div>
<div style="font-size: 1.5rem; font-weight: 700;"><?php echo $win_rate; ?>%</div>
</div>
</div>
</div>
<!-- ์ต๊ทผ ์คํ ํ์คํ ๋ฆฌ -->
<div style="background: var(--m-surface); padding: 24px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); margin-bottom: 32px;">
<h3 style="font-size: 1.25rem; font-weight: 700; margin-bottom: 16px;">๐ ์ต๊ทผ ์คํ ๊ธฐ๋ก</h3>
<?php if (empty($recent_spins)): ?>
<p style="color: var(--m-text-muted); text-align: center; padding: 32px;">์์ง ์คํ ๊ธฐ๋ก์ด ์์ต๋๋ค.</p>
<?php else: ?>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="border-bottom: 2px solid var(--m-text-muted);">
<th style="padding: 12px; text-align: left;">์ฌ๋ณผ</th>
<th style="padding: 12px; text-align: right;">๋ฒ ํ
</th>
<th style="padding: 12px; text-align: right;">๋ฐฐ๋น</th>
<th style="padding: 12px; text-align: center;">๊ฒฐ๊ณผ</th>
<th style="padding: 12px; text-align: left;">์๊ฐ</th>
</tr>
</thead>
<tbody>
<?php foreach ($recent_spins as $spin): ?>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.2);">
<td style="padding: 12px; font-size: 1.5rem;"><?php echo htmlspecialchars($spin['symbols']); ?></td>
<td style="padding: 12px; text-align: right;"><?php echo number_format($spin['bet_amount']); ?></td>
<td style="padding: 12px; text-align: right; font-weight: 700; color: <?php echo $spin['payout'] > 0 ? '#50C878' : '#FF6B6B'; ?>;"><?php echo number_format($spin['payout']); ?></td>
<td style="padding: 12px; text-align: center;">
<?php if ($spin['is_jackpot']): ?>
<span style="background: #FFD700; color: #333; padding: 4px 12px; border-radius: 12px; font-size: 0.75rem; font-weight: 700;">JACKPOT</span>
<?php elseif ($spin['payout'] > $spin['bet_amount']): ?>
<span style="color: #50C878;">โ
์น</span>
<?php elseif ($spin['payout'] == $spin['bet_amount']): ?>
<span style="color: var(--m-text-muted);">โ๏ธ ๋ฌด</span>
<?php else: ?>
<span style="color: #FF6B6B;">โ ํจ</span>
<?php endif; ?>
</td>
<td style="padding: 12px; font-size: 0.875rem; color: var(--m-text-muted);"><?php echo date('m/d H:i', strtotime($spin['created_at'])); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<!-- ์ญํ ๋ช
์์ ์ ๋น -->
<div style="background: linear-gradient(135deg, #FFD700, #FFA500); padding: 24px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow);">
<h3 style="font-size: 1.25rem; font-weight: 700; margin-bottom: 16px; color: #fff;">๐ ์ญํ ๋ช
์์ ์ ๋น</h3>
<?php if (empty($jackpot_hall)): ?>
<p style="color: #fff; text-align: center; padding: 32px; opacity: 0.8;">์์ง ์ญํ ๋ฌ์ฑ์๊ฐ ์์ต๋๋ค.</p>
<?php else: ?>
<div style="display: grid; gap: 12px;">
<?php foreach ($jackpot_hall as $idx => $jackpot): ?>
<div style="background: rgba(255,255,255,0.2); padding: 16px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 12px;">
<div style="background: #fff; color: #FFA500; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700;"><?php echo $idx + 1; ?></div>
<div>
<div style="color: #fff; font-weight: 700;"><?php echo htmlspecialchars($jackpot['mb_id']); ?></div>
<div style="color: #fff; opacity: 0.8; font-size: 0.875rem;"><?php echo date('Y-m-d H:i', strtotime($jackpot['created_at'])); ?></div>
</div>
</div>
<div style="color: #fff; font-size: 1.25rem; font-weight: 700;"><?php echo number_format($jackpot['payout']); ?> ์ฝ์ธ</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
```
๋ธ๋ผ์ฐ์ ์๋ก๊ณ ์นจ ํ ์คํ์ ์ฌ๋ฌ ๋ฒ ์คํํ์ธ์. ์ต๊ทผ ์คํ ํ์คํ ๋ฆฌ ํ
์ด๋ธ์ ๊ธฐ๋ก์ด ์์ด๊ณ , ๊ฐ์ธ ํต๊ณ๊ฐ ์
๋ฐ์ดํธ๋๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ํ์ด์ง ํ๋จ์ ๊ฐ์ธ ํต๊ณ ์นด๋๊ฐ ํ์๋๋ฉฐ ์ด ์คํ ํ์, ์ด ๋ฒ ํ ์ก, ์ด ํ๋์ก, ์์ต๋ฅ ์ด ์ค์๊ฐ์ผ๋ก ์ ๋ฐ์ดํธ๋ฉ๋๋ค. ์ต๊ทผ 20ํ ์คํ ๊ฒฐ๊ณผ๊ฐ ํ ์ด๋ธ๋ก ํ์๋๋ฉฐ ์ฌ๋ณผ, ๋ฒ ํ ์ก, ๋ฐฐ๋น, ๊ฒฐ๊ณผ(์น/๋ฌด/ํจ), ์๊ฐ์ด ๊ธฐ๋ก๋ฉ๋๋ค. ์ญํ ๋ช ์์ ์ ๋น์๋ ์ญํ ๋ฌ์ฑ์ ๋ชฉ๋ก์ด ์์๋ณ๋ก ํ์๋ฉ๋๋ค.
**Step 8: ์ผ์ผ ๋ณด๋์ค ์์คํ
**
์ฝ์ธ์ด ๋ถ์กฑํ ์ฌ์ฉ์๋ฅผ ์ํด ํ๋ฃจ 1ํ ๋ณด๋์ค ์ฝ์ธ์ ์ง๊ธํ๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํฉ๋๋ค.
**์๊ตฌ์ฌํญ**:
1. ์ผ์ผ ๋ณด๋์ค ๋ฒํผ (500์ฝ์ธ ์ง๊ธ)
2. ๋ง์ง๋ง ์๋ น์ผ ๊ธฐ์ค 24์๊ฐ ํ ์ฌ์๋ น ๊ฐ๋ฅ
3. ๋จ์ ์๊ฐ ์นด์ดํธ๋ค์ด ํ์
4. ์๋ น ์ DB ์
๋ฐ์ดํธ
**PHP ๋ณด๋์ค ๋ก์ง ์ถ๊ฐ** (`app/modules/casino/index.php` ์๋จ, AJAX ์์ฒญ ์ฒ๋ฆฌ ๋ถ๋ถ์ ์ถ๊ฐ):
```php
// ์ผ์ผ ๋ณด๋์ค ์๋ น
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'claim_bonus') {
header('Content-Type: application/json');
$stmt = $db->prepare('SELECT coins, last_bonus_date FROM user_coins WHERE mb_id = ?');
$stmt->execute([$member['mb_id']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
$today = date('Y-m-d');
if ($user['last_bonus_date'] === $today) {
echo json_encode(['success' => false, 'message' => '์ค๋ ์ด๋ฏธ ๋ณด๋์ค๋ฅผ ์๋ นํ์ต๋๋ค.']);
exit;
}
$new_coins = $user['coins'] + 500;
$db->prepare('UPDATE user_coins SET coins = ?, last_bonus_date = ? WHERE mb_id = ?')->execute([$new_coins, $today, $member['mb_id']]);
echo json_encode([
'success' => true,
'new_coins' => $new_coins,
'message' => '์ผ์ผ ๋ณด๋์ค 500์ฝ์ธ์ ๋ฐ์์ต๋๋ค!'
]);
exit;
}
```
**PHP ๋ณด๋์ค ์ํ ํ์ธ** (๊ธฐ์กด ์ฝ์ธ ์กฐํ ๋ค์์ ์ถ๊ฐ):
```php
// ๋ณด๋์ค ์๋ น ๊ฐ๋ฅ ์ฌ๋ถ
$today = date('Y-m-d');
$can_claim_bonus = ($last_bonus_date !== $today);
// ๋ค์ ๋ณด๋์ค๊น์ง ๋จ์ ์๊ฐ (์ด ๋จ์)
if (!$can_claim_bonus) {
$next_bonus_time = strtotime($last_bonus_date . ' +1 day');
$remaining_seconds = $next_bonus_time - time();
} else {
$remaining_seconds = 0;
}
```
**HTML ๋ณด๋์ค ๋ฒํผ ์ถ๊ฐ** (์ฝ์ธ ์์ก ์นด๋ ์์ ์ถ๊ฐ):
```html
<div style="background: var(--m-surface); padding: 20px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); margin-bottom: 24px;">
<h2 style="font-size: 1.5rem; font-weight: 700; margin-bottom: 12px;">๐ฐ ๋ณด์ ์ฝ์ธ</h2>
<p style="font-size: 2.5rem; font-weight: 900; color: #FFD700;"><?php echo number_format($coins); ?> ์ฝ์ธ</p>
<!-- ์ผ์ผ ๋ณด๋์ค ๋ฒํผ -->
<div style="margin-top: 16px;">
<?php if ($can_claim_bonus): ?>
<button id="bonusBtn" style="background: linear-gradient(135deg, #50C878, #32CD32); color: #fff; border: none; padding: 12px 32px; border-radius: 50px; font-weight: 700; cursor: pointer; box-shadow: 0 4px 12px rgba(50,205,50,0.4); transition: all 0.3s;">๐ ์ผ์ผ ๋ณด๋์ค ๋ฐ๊ธฐ (500์ฝ์ธ)</button>
<?php else: ?>
<button id="bonusBtn" disabled style="background: #ccc; color: #666; border: none; padding: 12px 32px; border-radius: 50px; font-weight: 700; cursor: not-allowed;">๐ ์ผ์ผ ๋ณด๋์ค (๋ฐ์)</button>
<p id="bonusTimer" style="margin-top: 8px; font-size: 0.875rem; color: var(--m-text-muted);">๋ค์ ๋ณด๋์ค: <span id="countdown"></span></p>
<?php endif; ?>
</div>
</div>
<script>
const canClaimBonus = <?php echo $can_claim_bonus ? 'true' : 'false'; ?>;
let remainingSeconds = <?php echo $remaining_seconds; ?>;
// ๋ณด๋์ค ์๋ น
if (canClaimBonus) {
document.getElementById('bonusBtn').addEventListener('click', async () => {
const formData = new FormData();
formData.append('action', 'claim_bonus');
const response = await fetch(window.location.href, {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
document.querySelector('[style*="font-size: 2.5rem"]').textContent = data.new_coins.toLocaleString() + ' ์ฝ์ธ';
alert(data.message);
location.reload();
} else {
alert(data.message);
}
});
}
// ์นด์ดํธ๋ค์ด ํ์ด๋จธ
if (!canClaimBonus && remainingSeconds > 0) {
const countdownEl = document.getElementById('countdown');
function updateCountdown() {
if (remainingSeconds <= 0) {
location.reload();
return;
}
const hours = Math.floor(remainingSeconds / 3600);
const minutes = Math.floor((remainingSeconds % 3600) / 60);
const seconds = remainingSeconds % 60;
countdownEl.textContent = `${hours}์๊ฐ ${minutes}๋ถ ${seconds}์ด`;
remainingSeconds--;
}
updateCountdown();
setInterval(updateCountdown, 1000);
}
</script>
```
๋ธ๋ผ์ฐ์ ์๋ก๊ณ ์นจ ํ ์ผ์ผ ๋ณด๋์ค ๋ฒํผ์ ํด๋ฆญํ์ธ์. ์ฝ์ธ์ด 500 ์ฆ๊ฐํ๊ณ , ๋ฒํผ์ด ๋นํ์ฑํ๋๋ฉฐ ์นด์ดํธ๋ค์ด์ด ์์๋๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ฝ์ธ ์์ก ์นด๋์ ์ผ์ผ ๋ณด๋์ค ๋ฒํผ์ด ํ์๋ฉ๋๋ค. ๋ฒํผ ํด๋ฆญ ์ 500์ฝ์ธ์ด ์ง๊ธ๋๊ณ DB์ ์๋ น ๋ ์ง๊ฐ ๊ธฐ๋ก๋ฉ๋๋ค. ์ด๋ฏธ ์๋ นํ ๊ฒฝ์ฐ ๋ฒํผ์ด ๋นํ์ฑํ๋๊ณ ๋ค์ ๋ณด๋์ค๊น์ง ๋จ์ ์๊ฐ์ด ์นด์ดํธ๋ค์ด์ผ๋ก ํ์๋ฉ๋๋ค. 24์๊ฐ ๊ฒฝ๊ณผ ํ ํ์ด์ง ์๋ก๊ณ ์นจ ์ ๋ฒํผ์ด ๋ค์ ํ์ฑํ๋ฉ๋๋ค.
**Step 9: ๋ฐ์ํ ๋์์ธ & ๋ชจ๋ฐ์ผ ์ต์ ํ**
๋ค์ํ ํ๋ฉด ํฌ๊ธฐ์์ ์ฌ๋กฏ๋จธ์ ์ด ์ ์ ์๋ํ๋๋ก ๋ฐ์ํ ์คํ์ผ์ ์ถ๊ฐํฉ๋๋ค.
**์๊ตฌ์ฌํญ**:
1. ๋ชจ๋ฐ์ผ(~768px): ๋ฆด ํฌ๊ธฐ ์ถ์, ๋ฒํผ ์ธ๋ก ๋ฐฐ์น
2. ํ๋ธ๋ฆฟ(768px~1024px): ์ค๊ฐ ํฌ๊ธฐ ๋ ์ด์์
3. ํฐ์น ์ด๋ฒคํธ ์ง์
4. ๊ฐ๋ก ์คํฌ๋กค ๋ฐฉ์ง
**CSS ๋ฏธ๋์ด ์ฟผ๋ฆฌ ์ถ๊ฐ** (๊ธฐ์กด `<style>` ํ๊ทธ ์์ ์ถ๊ฐ):
```css
/* ๋ชจ๋ฐ์ผ ์ต์ ํ */
@media (max-width: 768px) {
.reel {
width: 80px !important;
height: 120px !important;
font-size: 3.5rem !important;
}
#reels {
gap: 8px !important;
}
.bet-btn {
padding: 10px 16px !important;
font-size: 0.875rem !important;
}
#spinBtn {
padding: 16px 40px !important;
font-size: 1.25rem !important;
}
h1 {
font-size: 1.5rem !important;
}
.m-container {
padding: 16px 12px 32px !important;
}
/* ํ
์ด๋ธ ๊ฐ๋ก ์คํฌ๋กค */
table {
font-size: 0.875rem;
}
table th,
table td {
padding: 8px !important;
}
}
@media (max-width: 480px) {
.reel {
width: 70px !important;
height: 100px !important;
font-size: 3rem !important;
}
.bet-btn {
padding: 8px 12px !important;
font-size: 0.75rem !important;
}
#spinBtn {
padding: 14px 32px !important;
font-size: 1rem !important;
}
}
/* ํฐ์น ํผ๋๋ฐฑ */
button:active {
transform: scale(0.95);
}
/* ๊ฐ๋ก ์คํฌ๋กค ๋ฐฉ์ง */
body {
overflow-x: hidden;
}
.m-container {
max-width: 100%;
overflow-x: hidden;
}
```
**JavaScript ํฐ์น ์ด๋ฒคํธ ์ถ๊ฐ** (๊ธฐ์กด `<script>` ํ๊ทธ ์์ ์ถ๊ฐ):
```javascript
// ํฐ์น ์ด๋ฒคํธ ์ง์
if ('ontouchstart' in window) {
document.getElementById('spinBtn').addEventListener('touchstart', (e) => {
e.preventDefault();
document.getElementById('spinBtn').click();
});
document.querySelectorAll('.bet-btn').forEach(btn => {
btn.addEventListener('touchstart', (e) => {
e.preventDefault();
btn.click();
});
});
}
// ํ๋ฉด ํ์ ๊ฐ์ง
window.addEventListener('orientationchange', () => {
setTimeout(() => {
window.scrollTo(0, 0);
}, 100);
});
```
๋ธ๋ผ์ฐ์ ๊ฐ๋ฐ์ ๋๊ตฌ์์ ๋ชจ๋ฐ์ผ ๋ทฐ๋ก ์ ํํ๊ฑฐ๋ ์ค์ ๋ชจ๋ฐ์ผ ๊ธฐ๊ธฐ๋ก ์ ์ํ์ฌ ๋ ์ด์์์ด ์ ์ ์๋ํ๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๋ชจ๋ฐ์ผ ํ๋ฉด(~768px)์์ ๋ฆด ํฌ๊ธฐ๊ฐ ์ถ์๋๊ณ ๋ฒํผ์ด ์ ์ ํ ๋ฐฐ์น๋ฉ๋๋ค. ํฐ์น ์ด๋ฒคํธ๊ฐ ์ ์ ์๋ํ๋ฉฐ ๋ฒํผ ํด๋ฆญ ์ ์๊ฐ์ ํผ๋๋ฐฑ์ด ์ ๊ณต๋ฉ๋๋ค. ๊ฐ๋ก ์คํฌ๋กค์ด ๋ฐ์ํ์ง ์์ผ๋ฉฐ ๋ชจ๋ ์ฝํ ์ธ ๊ฐ ํ๋ฉด ์์ ์ ์ ํ์๋ฉ๋๋ค. ํ๋ฉด ํ์ ์ ๋ ์ด์์์ด ์๋ ์กฐ์ ๋ฉ๋๋ค.
**Step 10: ์ต์ข
๋ง๋ฌด๋ฆฌ & ๋ณด์ ๊ฐํ**
ํ๋ก์ ํธ๋ฅผ ์์ฑํ๊ณ ๋ณด์ ์ทจ์ฝ์ ์ ์ ๊ฒํฉ๋๋ค.
**์๊ตฌ์ฌํญ**:
1. XSS ๋ฐฉ์ด: ๋ชจ๋ ์ฌ์ฉ์ ์
๋ ฅ ์ด์ค์ผ์ดํ
2. CSRF ๋ฐฉ์ด: g5se ํ ํฐ ๊ฒ์ฆ
3. SQL ์ธ์ ์
๋ฐฉ์ด: prepared statement ํ์ธ
4. ์๋ฌ ํธ๋ค๋ง ๊ฐ์
5. ์ฝ๋ ์ ๋ฆฌ & ์ฃผ์ ์ถ๊ฐ
**๋ณด์ ๊ฐํ ์ฝ๋ ์ถ๊ฐ**:
**1. CSRF ํ ํฐ ์ถ๊ฐ** (PHP ์๋จ):
```php
// CSRF ํ ํฐ ์์ฑ
if (!isset($_SESSION['casino_token'])) {
$_SESSION['casino_token'] = bin2hex(random_bytes(32));
}
$csrf_token = $_SESSION['casino_token'];
// AJAX ์์ฒญ ์ ํ ํฐ ๊ฒ์ฆ
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['casino_token']) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => '์๋ชป๋ ์์ฒญ์
๋๋ค.']);
exit;
}
}
```
**2. JavaScript CSRF ํ ํฐ ์ ์ก** (๋ชจ๋ fetch ์์ฒญ์ ์ถ๊ฐ):
```javascript
const csrfToken = '<?php echo $csrf_token; ?>';
// SPIN ๋ฒํผ ํด๋ฆญ (๊ธฐ์กด fetch ๋ถ๋ถ ์์ )
const formData = new FormData();
formData.append('action', 'spin');
formData.append('bet_amount', selectedBet);
formData.append('symbols', JSON.stringify(results));
formData.append('csrf_token', csrfToken); // CSRF ํ ํฐ ์ถ๊ฐ
// ๋ณด๋์ค ๋ฒํผ ํด๋ฆญ (๊ธฐ์กด fetch ๋ถ๋ถ ์์ )
const formData = new FormData();
formData.append('action', 'claim_bonus');
formData.append('csrf_token', csrfToken); // CSRF ํ ํฐ ์ถ๊ฐ
```
**3. ์๋ฌ ํธ๋ค๋ง ๊ฐ์ ** (PHP):
```php
try {
// ๊ธฐ์กด DB ์์
์ฝ๋๋ฅผ try-catch๋ก ๊ฐ์ธ๊ธฐ
@mkdir(G5_DATA_PATH.'/modules', 0707, true);
$db = new PDO('sqlite:'.G5_DATA_PATH.'/modules/casino.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// ... ๋๋จธ์ง ์ฝ๋ ...
} catch (PDOException $e) {
error_log('Casino DB Error: ' . $e->getMessage());
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => '์ผ์์ ์ธ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.']);
exit;
}
}
```
**4. ์
๋ ฅ ๊ฒ์ฆ ๊ฐํ** (PHP AJAX ์ฒ๋ฆฌ ๋ถ๋ถ):
```php
// ์คํ ์์ฒญ ๊ฒ์ฆ
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'spin') {
header('Content-Type: application/json');
// ์
๋ ฅ ๊ฒ์ฆ
$bet_amount = filter_var($_POST['bet_amount'], FILTER_VALIDATE_INT);
if ($bet_amount === false || !in_array($bet_amount, [10, 50, 100, 500])) {
echo json_encode(['success' => false, 'message' => '์๋ชป๋ ๋ฒ ํ
์ก์
๋๋ค.']);
exit;
}
$symbols = json_decode($_POST['symbols'], true);
if (!is_array($symbols) || count($symbols) !== 3) {
echo json_encode(['success' => false, 'message' => '์๋ชป๋ ์์ฒญ์
๋๋ค.']);
exit;
}
$valid_symbols = ['๐', '๐', '๐', '๐', 'โญ', '๐', '7๏ธโฃ'];
foreach ($symbols as $symbol) {
if (!in_array($symbol, $valid_symbols)) {
echo json_encode(['success' => false, 'message' => '์๋ชป๋ ์ฌ๋ณผ์
๋๋ค.']);
exit;
}
}
// ... ๋๋จธ์ง ์ฝ๋ ...
}
```
**5. ์ฌ์ฉ์ ์๋ด ๋ฉ์์ง ์ถ๊ฐ** (HTML):
```html
<!-- ๊ฒ์ ์๋ด -->
<div style="background: rgba(103,126,234,0.1); padding: 16px; border-radius: 8px; margin-bottom: 24px; border-left: 4px solid #667eea;">
<h4 style="font-size: 1rem; font-weight: 700; margin-bottom: 8px;">โน๏ธ ๊ฒ์ ์๋ด</h4>
<ul style="margin: 0; padding-left: 20px; color: var(--m-text-muted); font-size: 0.875rem; line-height: 1.6;">
<li>๋ฒ ํ
๊ธ์ก์ ์ ํํ๊ณ SPIN ๋ฒํผ์ ๋๋ฌ ๋ฆด์ ํ์ ์ํค์ธ์.</li>
<li>3๊ฐ ์ฌ๋ณผ์ด ๋ชจ๋ ์ผ์นํ๋ฉด ๋ฐฐ๋น์ ๋ฐ์ต๋๋ค.</li>
<li>2๊ฐ ์ผ์น ์ ๋ฒ ํ
์ก์ด ๋ฐํ๋ฉ๋๋ค.</li>
<li>์ธ๋ธ 3๊ฐ ์ผ์น ์ ์ญํ ๋ฌ์ฑ! 100๋ฐฐ ๋ฐฐ๋น!</li>
<li>์ฝ์ธ์ด ๋ถ์กฑํ๋ฉด ์ผ์ผ ๋ณด๋์ค(500์ฝ์ธ)๋ฅผ ๋ฐ์ผ์ธ์.</li>
<li>์ด ๊ฒ์์ ๊ฐ์ ์ฝ์ธ์ผ๋ก ์งํ๋๋ฉฐ ์ค์ ๊ธ์ ๊ณผ ๋ฌด๊ดํฉ๋๋ค.</li>
</ul>
</div>
```
**์ต์ข
ํ
์คํธ ์ฒดํฌ๋ฆฌ์คํธ**:
1. โ
ํ์ ์ ์ฉ ํ์ด์ง (๋นํ์ ์ ๊ทผ ์ฐจ๋จ)
2. โ
๋ฆด ์คํ ์ ๋๋ฉ์ด์
์ ์ ์๋
3. โ
๋ฐฐ๋น ๊ณ์ฐ ์ ํ์ฑ
4. โ
์ฝ์ธ ์์ก ์ค์๊ฐ ์
๋ฐ์ดํธ
5. โ
์ญํ ๋ชจ๋ฌ & ์ฌ์ด๋ ์ฌ์
6. โ
์คํ ํ์คํ ๋ฆฌ ๊ธฐ๋ก
7. โ
์ผ์ผ ๋ณด๋์ค ์์คํ
8. โ
๋ฐ์ํ ๋์์ธ (๋ชจ๋ฐ์ผ/ํ๋ธ๋ฆฟ)
9. โ
CSRF ํ ํฐ ๊ฒ์ฆ
10. โ
SQL ์ธ์ ์
๋ฐฉ์ด
11. โ
XSS ๋ฐฉ์ด (htmlspecialchars)
12. โ
์๋ฌ ํธ๋ค๋ง
๋ธ๋ผ์ฐ์ ์์ ๋ชจ๋ ๊ธฐ๋ฅ์ ํ
์คํธํ๊ณ , ๊ฐ๋ฐ์ ๋๊ตฌ ์ฝ์์ ์๋ฌ๊ฐ ์๋์ง ํ์ธํ์ธ์. ๋ชจ๋ฐ์ผ ๋ทฐ์์๋ ์ ์ ์๋ํ๋์ง ํ
์คํธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ํ๋ก์ ํธ๊ฐ ์์ฑ๋๊ณ ๋ชจ๋ ๋ณด์ ์กฐ์น๊ฐ ์ ์ฉ๋ฉ๋๋ค. CSRF ํ ํฐ์ด ๋ชจ๋ POST ์์ฒญ์ ํฌํจ๋์ด ๊ฒ์ฆ๋ฉ๋๋ค. ์ ๋ ฅ๊ฐ์ด ์๋ฒ์์ ์๊ฒฉํ ๊ฒ์ฆ๋๋ฉฐ SQL ์ธ์ ์ /XSS ๊ณต๊ฒฉ์ด ์ฐจ๋จ๋ฉ๋๋ค. ์๋ฌ ๋ฐ์ ์ ์ฌ์ฉ์ ์นํ์ ์ธ ๋ฉ์์ง๊ฐ ํ์๋๊ณ ๋ก๊ทธ์ ๊ธฐ๋ก๋ฉ๋๋ค. ๊ฒ์ ์๋ด ๋ฉ์์ง๊ฐ ํ์๋์ด ์ฌ์ฉ์๊ฐ ๊ท์น์ ์ฝ๊ฒ ์ดํดํ ์ ์์ต๋๋ค. ๋ชจ๋ ๊ธฐ๋ฅ์ด ์์ ์ ์ผ๋ก ์๋ํ๋ฉฐ ๋ค์ํ ํ๋ฉด ํฌ๊ธฐ์์ ์ ์ ํ์๋ฉ๋๋ค.
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!