๐ŸŽฒ ํ™•๋ฅ  ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ํ”Œ๋ ˆ์ด๊ทธ๋ผ์šด๋“œ - ์ฃผ์‚ฌ์œ„/๋™์ „/๋ฝ‘๊ธฐ ํ™•๋ฅ  ์‹คํ—˜์‹ค - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
๊ณ ๊ธ‰ PHP g5se AI: Claude

๐ŸŽฒ ํ™•๋ฅ  ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ํ”Œ๋ ˆ์ด๊ทธ๋ผ์šด๋“œ - ์ฃผ์‚ฌ์œ„/๋™์ „/๋ฝ‘๊ธฐ ํ™•๋ฅ  ์‹คํ—˜์‹ค

๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช… probability-simulator-playground
์กฐํšŒ์ˆ˜ 95 0๊ฑด ์ œ์ถœ

๋ฌธ์ œ ์„ค๋ช…

๐ŸŽฏ ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

ํ†ต๊ณ„ํ•™ยทํ™•๋ฅ ๋ก ์„ ๋ฐฐ์šฐ๋Š” ํ•™์ƒ, ๊ฒŒ์ž„ ๊ธฐํš์ž, ๊ฐ€์ฑ  ์‹œ์Šคํ…œ ์„ค๊ณ„์ž๋ฅผ ์œ„ํ•œ ๋Œ€ํ™”ํ˜• ํ™•๋ฅ  ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์ฃผ์‚ฌ์œ„/๋™์ „/์นด๋“œ/๋ฝ‘๊ธฐ ๋“ฑ ๋‹ค์–‘ํ•œ ํ™•๋ฅ  ์‹คํ—˜์„ ์„ค์ •ํ•˜๊ณ , ์ˆ˜์ฒœ ๋ฒˆ ์‹œํ–‰ํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ ์‹ค์‹œ๊ฐ„ ์ฐจํŠธ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โœจ ํ•ต์‹ฌ ๊ธฐ๋Šฅ

  1. ๋‹ค์ค‘ ์‹คํ—˜ ํƒ€์ž…

    • ๋™์ „ ๋˜์ง€๊ธฐ (์•ž๋ฉด/๋’ท๋ฉด ๋น„์œจ)
    • ์ฃผ์‚ฌ์œ„ ๊ตด๋ฆฌ๊ธฐ (N๋ฉด์ฒด ์ง€์› โ€” 6๋ฉด, 20๋ฉด ๋“ฑ)
    • ์นด๋“œ ๋ฝ‘๊ธฐ (52์žฅ ๋ฑ์—์„œ ํŠน์ • ๋ฌด๋Šฌ/์ˆซ์ž ํ™•๋ฅ )
    • ์ปค์Šคํ…€ ๊ฐ€์ฑ  (SSR 0.5%, SR 5%, R 94.5% ๊ฐ™์€ ๋“ฑ๊ธ‰๋ณ„ ํ™•๋ฅ  ์„ค์ •)
  2. ๋Œ€๋Ÿ‰ ์‹œ๋ฎฌ๋ ˆ์ด์…˜

    • ์‚ฌ์šฉ์ž๊ฐ€ ์‹œํ–‰ ํšŸ์ˆ˜ ์ž…๋ ฅ (์˜ˆ: 10,000ํšŒ)
    • ์„œ๋ฒ„์—์„œ PHP ๋ฃจํ”„๋กœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹คํ–‰
    • ๊ฒฐ๊ณผ๋ฅผ JSON์œผ๋กœ ๋ฐ˜ํ™˜ โ†’ JS๊ฐ€ Chart.js๋กœ ๋ง‰๋Œ€/์› ๊ทธ๋ž˜ํ”„ ๋ Œ๋”๋ง
  3. ํ†ต๊ณ„ ๋ถ„์„

    • ์ด๋ก ์  ํ™•๋ฅ  vs ์‹ค์ œ ๊ฒฐ๊ณผ ๋น„๊ต
    • ํ‘œ์ค€ํŽธ์ฐจ, ์‹ ๋ขฐ๊ตฌ๊ฐ„ ํ‘œ์‹œ
    • ์‹œํ–‰ ํšŸ์ˆ˜๊ฐ€ ๋งŽ์•„์งˆ์ˆ˜๋ก ์ด๋ก ๊ฐ’์— ์ˆ˜๋ ดํ•˜๋Š” ๊ณผ์ • ์‹œ๊ฐํ™”
  4. ํžˆ์Šคํ† ๋ฆฌ & ๋ถ๋งˆํฌ

    • ๊ณผ๊ฑฐ ์‹คํ—˜ ๊ฒฐ๊ณผ๋ฅผ SQLite DB์— ์ €์žฅ
    • ์‚ฌ์šฉ์ž๊ฐ€ ์‹คํ—˜ ์„ค์ •์„ ๋ถ๋งˆํฌํ•˜์—ฌ ์žฌ์‹คํ–‰ ๊ฐ€๋Šฅ
    • ์‹คํ—˜๋ณ„ ๋ฉ”๋ชจ/ํƒœ๊ทธ ๊ธฐ๋Šฅ
  5. ๋น„๊ต ๋ชจ๋“œ

    • ๋‘ ์‹คํ—˜ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜๋ž€ํžˆ ๋น„๊ต (์˜ˆ: 6๋ฉด ์ฃผ์‚ฌ์œ„ vs 20๋ฉด ์ฃผ์‚ฌ์œ„)
    • ์˜ค๋ฒ„๋ ˆ์ด ์ฐจํŠธ๋กœ ๋ถ„ํฌ ์ฐจ์ด ํ™•์ธ

๐ŸŽจ UX ํŠน์ง•

  • ์‹ค์‹œ๊ฐ„ ์ง„ํ–‰ ๋ฐ”: ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ง„ํ–‰๋ฅ ์„ ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ”๋กœ ํ‘œ์‹œ
  • ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฐจํŠธ: Chart.js ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ๊ฒฐ๊ณผ๊ฐ€ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋‚˜ํƒ€๋‚จ
  • ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ: ๋ถ๋งˆํฌํ•œ ์‹คํ—˜์„ ๋น„๊ต ์Šฌ๋กฏ์— ๋“œ๋ž˜๊ทธ
  • ๋‹คํฌ๋ชจ๋“œ: g5se modern shell ๋””์ž์ธ ํ† ํฐ ํ™œ์šฉ

๐Ÿ”ง ๊ธฐ์ˆ  ์Šคํƒ

  • ๋ฐฑ์—”๋“œ: PHP 8.x + SQLite (์‹คํ—˜ ํžˆ์Šคํ† ๋ฆฌ ์ €์žฅ)
  • ํ”„๋ก ํŠธ: Vanilla JS + Chart.js (CDN)
  • ์Šคํƒ€์ผ: g5se modern shell CSS ๋ณ€์ˆ˜ + ์ปค์Šคํ…€ ๊ทธ๋ž˜๋””์–ธํŠธ

๐Ÿ“ฆ ํ•„์ˆ˜ ๊ตฌํ˜„ ํ•ญ๋ชฉ

  • ๋™์ „/์ฃผ์‚ฌ์œ„/์นด๋“œ/๊ฐ€์ฑ  4๊ฐ€์ง€ ์‹คํ—˜ ํƒ€์ž…
  • ์‹œํ–‰ ํšŸ์ˆ˜ ์ž…๋ ฅ ํผ (์ตœ์†Œ 10ํšŒ ~ ์ตœ๋Œ€ 100,000ํšŒ)
  • ์„œ๋ฒ„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์—”๋“œํฌ์ธํŠธ (AJAX ํ˜ธ์ถœ)
  • Chart.js ๋ง‰๋Œ€/์› ๊ทธ๋ž˜ํ”„ ๋ Œ๋”๋ง
  • ์ด๋ก  ํ™•๋ฅ  vs ์‹ค์ œ ๊ฒฐ๊ณผ ๋น„๊ต ํ…Œ์ด๋ธ”
  • SQLite DB์— ์‹คํ—˜ ํžˆ์Šคํ† ๋ฆฌ ์ €์žฅ (์‹คํ—˜๋ช…, ํƒ€์ž…, ์„ค์ •, ๊ฒฐ๊ณผ, ์ƒ์„ฑ์ผ์‹œ)
  • ๋ถ๋งˆํฌ ๋ชฉ๋ก ํŽ˜์ด์ง€ (๊ณผ๊ฑฐ ์‹คํ—˜ ์žฌ์‹คํ–‰)
  • ๋น„๊ต ๋ชจ๋“œ (2๊ฐœ ์‹คํ—˜ ๊ฒฐ๊ณผ ๋‚˜๋ž€ํžˆ ํ‘œ์‹œ)

๐ŸŽ ๋ณด๋„ˆ์Šค ๊ธฐ๋Šฅ

  • ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ ๋ชจ๋“œ: ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ง„ํ–‰ ์ค‘ ์ค‘๊ฐ„ ๊ฒฐ๊ณผ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ฐจํŠธ ์—…๋ฐ์ดํŠธ (Server-Sent Events)
  • ๋ชฌํ…Œ์นด๋ฅผ๋กœ ํŒŒ์ด ๊ณ„์‚ฐ๊ธฐ: ์› ์•ˆ์— ๋ฌด์ž‘์œ„ ์ ์„ ์ฐ์–ด ฯ€ ๊ฐ’ ์ถ”์ •
  • ์ƒ์ผ ๋ฌธ์ œ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ: N๋ช… ์ค‘ ์ƒ์ผ์ด ๊ฐ™์€ ์‚ฌ๋žŒ์ด ์žˆ์„ ํ™•๋ฅ 
  • CSV ๋‚ด๋ณด๋‚ด๊ธฐ: ์‹คํ—˜ ๊ฒฐ๊ณผ๋ฅผ CSV ํŒŒ์ผ๋กœ ๋‹ค์šด๋กœ๋“œ
  • ๊ณต์œ  ๋งํฌ: ์‹คํ—˜ ์„ค์ •์„ URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ธ์ฝ”๋”ฉํ•˜์—ฌ ๊ณต์œ 

์ œ์•ฝ ์กฐ๊ฑด

  • โ€ข g5se ์„ค์น˜๋ณธ ์œ„์—์„œ ์ž‘๋™ (app/modules/probability/ ํด๋” ์‚ฌ์šฉ)
  • โ€ข ํด๋” ๊ธฐ๋ฐ˜ ์ž๋™ ๋ผ์šฐํŒ… ํ™œ์šฉ โ€” /probability URL๋กœ ์ ‘๊ทผ
  • โ€ข modern shell (_head.inc.php + m-shell + _nav.inc.php + m-container + _footer.inc.php) ์ ์šฉ ํ•„์ˆ˜
  • โ€ข ๋””์ž์ธ ํ† ํฐ (var(--m-surface), var(--m-text) ๋“ฑ) ์‚ฌ์šฉํ•˜์—ฌ ๋‹คํฌ๋ชจ๋“œ ๋Œ€์‘
  • โ€ข SQLite DB๋Š” G5_DATA_PATH.'/modules/probability.sqlite'์— ์ƒ์„ฑ
  • โ€ข Chart.js๋Š” CDN์œผ๋กœ ๋กœ๋“œ (https://cdn.jsdelivr.net/npm/chart.js)
  • โ€ข ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋กœ์ง์€ PHP ์„œ๋ฒ„์—์„œ ์ฒ˜๋ฆฌ (JS๋Š” ๊ฒฐ๊ณผ๋งŒ ๋ Œ๋”๋ง)
  • โ€ข AJAX ์š”์ฒญ์€ fetch API ์‚ฌ์šฉ, JSON ์‘๋‹ต ๋ฐ˜ํ™˜
  • โ€ข ๋ชจ๋“  ์‚ฌ์šฉ์ž ์ž…๋ ฅ์€ htmlspecialchars()๋กœ ์ด์Šค์ผ€์ดํ”„
  • โ€ข prepared statement๋กœ SQL ์ธ์ ์…˜ ๋ฐฉ์ง€
  • โ€ข ์‹œํ–‰ ํšŸ์ˆ˜ 100,000ํšŒ ์ด์ƒ ์š”์ฒญ ์‹œ ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
  • โ€ข ์‹คํ—˜ ํžˆ์Šคํ† ๋ฆฌ๋Š” ์ตœ๊ทผ 50๊ฐœ๊นŒ์ง€๋งŒ ํ‘œ์‹œ (ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋ฏธ๊ตฌํ˜„)

ํ”„๋กฌํ”„ํŠธ ๋ ˆ์‹œํ”ผ

์•„๋ž˜ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ˆœ์„œ๋Œ€๋กœ AI์—๊ฒŒ ๋ณด๋‚ด๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๋ณต์‚ฌ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ๋ฐ”๋กœ ์‚ฌ์šฉํ•˜์„ธ์š”.

1
Step 1
g5se๊ฐ€ ์„ค์น˜๋œ ๋””๋ ‰ํ„ฐ๋ฆฌ(๋ฃจํŠธ์— `index.php`ยท`app/`์ด ์žˆ๋Š” ๊ณณ)์—์„œ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. (g5se ์„ค์น˜๋Š” https://github.com/gnuboard/g5se ์ฐธ๊ณ )

## Step 1: ํ”„๋กœ์ ํŠธ ๊ณจ๊ฒฉ ์ƒ์„ฑ ๋ฐ DB ์ดˆ๊ธฐํ™”

### ํŒŒ์ผ ์ƒ์„ฑ: `app/modules/probability/index.php`

```php
<?php
include_once('./_common.php');

// SQLite DB ์ดˆ๊ธฐํ™”
$dbDir = G5_DATA_PATH.'/modules';
if (!is_dir($dbDir)) {
    @mkdir($dbDir, 0707, true);
}
$dbPath = $dbDir.'/probability.sqlite';
$db = new PDO('sqlite:'.$dbPath);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// ํ…Œ์ด๋ธ” ์ƒ์„ฑ (์ฒซ ์ ‘๊ทผ ์‹œ)
$db->exec("
    CREATE TABLE IF NOT EXISTS experiments (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        type TEXT NOT NULL,
        config TEXT NOT NULL,
        result TEXT NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
");

require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<style>
.prob-hero {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    padding: 48px 24px;
    border-radius: var(--m-radius-lg);
    text-align: center;
    margin-bottom: 32px;
}
.prob-hero h1 {
    font-size: 2.5rem;
    font-weight: 800;
    margin-bottom: 12px;
}
.prob-hero p {
    font-size: 1.1rem;
    opacity: 0.9;
}
.experiment-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 20px;
    margin-bottom: 32px;
}
.exp-card {
    background: var(--m-surface);
    border-radius: var(--m-radius-lg);
    padding: 24px;
    box-shadow: var(--m-shadow);
    cursor: pointer;
    transition: transform 0.2s, box-shadow 0.2s;
}
.exp-card:hover {
    transform: translateY(-4px);
    box-shadow: 0 8px 24px rgba(0,0,0,0.15);
}
.exp-card h3 {
    font-size: 1.4rem;
    margin-bottom: 8px;
    color: var(--m-text);
}
.exp-card p {
    color: var(--m-text-muted);
    font-size: 0.95rem;
}
.exp-icon {
    font-size: 3rem;
    margin-bottom: 12px;
}
</style>
<div class="m-shell">
    <?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
    <main class="m-container" style="padding: 32px 20px 48px;">
        <div class="prob-hero">
            <h1>๐ŸŽฒ ํ™•๋ฅ  ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ</h1>
            <p>์ฃผ์‚ฌ์œ„, ๋™์ „, ์นด๋“œ, ๊ฐ€์ฑ  โ€” ํ™•๋ฅ ์˜ ์„ธ๊ณ„๋ฅผ ์‹คํ—˜ํ•˜์„ธ์š”</p>
        </div>

        <div class="experiment-grid">
            <div class="exp-card" onclick="location.href='/probability/coin'">
                <div class="exp-icon">๐Ÿช™</div>
                <h3>๋™์ „ ๋˜์ง€๊ธฐ</h3>
                <p>์•ž๋ฉด๊ณผ ๋’ท๋ฉด์˜ ํ™•๋ฅ  ๋ถ„ํฌ๋ฅผ ํ™•์ธํ•˜์„ธ์š”</p>
            </div>
            <div class="exp-card" onclick="location.href='/probability/dice'">
                <div class="exp-icon">๐ŸŽฒ</div>
                <h3>์ฃผ์‚ฌ์œ„ ๊ตด๋ฆฌ๊ธฐ</h3>
                <p>6๋ฉด์ฒด๋ถ€ํ„ฐ 20๋ฉด์ฒด๊นŒ์ง€ ๋‹ค์–‘ํ•œ ์ฃผ์‚ฌ์œ„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜</p>
            </div>
            <div class="exp-card" onclick="location.href='/probability/card'">
                <div class="exp-icon">๐Ÿƒ</div>
                <h3>์นด๋“œ ๋ฝ‘๊ธฐ</h3>
                <p>52์žฅ ๋ฑ์—์„œ ํŠน์ • ์นด๋“œ๊ฐ€ ๋‚˜์˜ฌ ํ™•๋ฅ  ์‹คํ—˜</p>
            </div>
            <div class="exp-card" onclick="location.href='/probability/gacha'">
                <div class="exp-icon">๐ŸŽ</div>
                <h3>๊ฐ€์ฑ  ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ</h3>
                <p>SSR/SR/R ๋“ฑ๊ธ‰๋ณ„ ํ™•๋ฅ  ์„ค์ • ๋ฐ ๋ฝ‘๊ธฐ ์‹œ๋ฎฌ๋ ˆ์ด์…˜</p>
            </div>
        </div>

        <div style="background: var(--m-surface); padding: 24px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow);">
            <h2 style="margin-bottom: 16px; color: var(--m-text);">๐Ÿ“Š ์ตœ๊ทผ ์‹คํ—˜ ๊ธฐ๋ก</h2>
            <?php
            $stmt = $db->query("SELECT * FROM experiments ORDER BY created_at DESC LIMIT 10");
            $experiments = $stmt->fetchAll(PDO::FETCH_ASSOC);
            if (empty($experiments)) {
                echo '<p style="color: var(--m-text-muted);">์•„์ง ์‹คํ—˜ ๊ธฐ๋ก์ด ์—†์Šต๋‹ˆ๋‹ค. ์œ„์—์„œ ์‹คํ—˜์„ ์‹œ์ž‘ํ•˜์„ธ์š”!</p>';
            } else {
                echo '<ul style="list-style: none; padding: 0;">';
                foreach ($experiments as $exp) {
                    $typeEmoji = [
                        'coin' => '๐Ÿช™',
                        'dice' => '๐ŸŽฒ',
                        'card' => '๐Ÿƒ',
                        'gacha' => '๐ŸŽ'
                    ];
                    $emoji = $typeEmoji[$exp['type']] ?? '๐Ÿ”ฌ';
                    echo '<li style="padding: 12px; border-bottom: 1px solid rgba(0,0,0,0.1); color: var(--m-text);">';
                    echo $emoji.' '.htmlspecialchars($exp['name']).' โ€” '.date('Y-m-d H:i', strtotime($exp['created_at']));
                    echo '</li>';
                }
                echo '</ul>';
            }
            ?>
        </div>
    </main>
    <?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<?php include_once('./tail.sub.php'); ?>
```

์ด ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๋ฉด `/probability` URL๋กœ ์ ‘๊ทผ ์‹œ ๋ฉ”์ธ ํŽ˜์ด์ง€๊ฐ€ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค. 4๊ฐ€์ง€ ์‹คํ—˜ ํƒ€์ž… ์นด๋“œ๊ฐ€ ๊ทธ๋ฆฌ๋“œ๋กœ ๋ฐฐ์น˜๋˜๊ณ , ํ•˜๋‹จ์— ์ตœ๊ทผ ์‹คํ—˜ ๊ธฐ๋ก์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋ธŒ๋ผ์šฐ์ €์—์„œ `/probability` ์ ‘์† ์‹œ ๋ณด๋ผ์ƒ‰ ๊ทธ๋ž˜๋””์–ธํŠธ ํžˆ์–ด๋กœ ์„น์…˜๊ณผ 4๊ฐœ์˜ ์‹คํ—˜ ์นด๋“œ(๋™์ „/์ฃผ์‚ฌ์œ„/์นด๋“œ/๊ฐ€์ฑ )๊ฐ€ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค. ํ•˜๋‹จ์—๋Š” '์•„์ง ์‹คํ—˜ ๊ธฐ๋ก์ด ์—†์Šต๋‹ˆ๋‹ค' ๋ฉ”์‹œ์ง€๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. SQLite DB ํŒŒ์ผ์ด `data/modules/probability.sqlite`์— ์ž๋™ ์ƒ์„ฑ๋˜๊ณ , `experiments` ํ…Œ์ด๋ธ”์ด ๋งŒ๋“ค์–ด์ง‘๋‹ˆ๋‹ค.

2
Step 2
## Step 2: ๋™์ „ ๋˜์ง€๊ธฐ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๊ตฌํ˜„

### ํŒŒ์ผ ์ƒ์„ฑ: `app/modules/probability/coin/index.php`

```php
<?php
include_once('./_common.php');

$dbDir = G5_DATA_PATH.'/modules';
if (!is_dir($dbDir)) {
    @mkdir($dbDir, 0707, true);
}
$dbPath = $dbDir.'/probability.sqlite';
$db = new PDO('sqlite:'.$dbPath);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// AJAX ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์š”์ฒญ ์ฒ˜๋ฆฌ
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'simulate') {
    header('Content-Type: application/json');
    $trials = (int)$_POST['trials'];
    if ($trials < 10 || $trials > 100000) {
        echo json_encode(['error' => '์‹œํ–‰ ํšŸ์ˆ˜๋Š” 10~100,000 ์‚ฌ์ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.']);
        exit;
    }

    $heads = 0;
    $tails = 0;
    for ($i = 0; $i < $trials; $i++) {
        if (rand(0, 1) === 0) {
            $heads++;
        } else {
            $tails++;
        }
    }

    $result = [
        'heads' => $heads,
        'tails' => $tails,
        'headsPercent' => round(($heads / $trials) * 100, 2),
        'tailsPercent' => round(($tails / $trials) * 100, 2),
        'trials' => $trials
    ];

    // DB์— ์ €์žฅ
    $name = '๋™์ „ ๋˜์ง€๊ธฐ '.$trials.'ํšŒ';
    $config = json_encode(['trials' => $trials]);
    $resultJson = json_encode($result);
    $stmt = $db->prepare("INSERT INTO experiments (name, type, config, result) VALUES (?, ?, ?, ?)");
    $stmt->execute([$name, 'coin', $config, $resultJson]);

    echo json_encode($result);
    exit;
}

require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<style>
.coin-container {
    background: var(--m-surface);
    padding: 32px;
    border-radius: var(--m-radius-lg);
    box-shadow: var(--m-shadow);
    margin-bottom: 24px;
}
.coin-form {
    display: flex;
    gap: 12px;
    margin-bottom: 24px;
    align-items: center;
}
.coin-form input {
    flex: 1;
    padding: 12px;
    border: 1px solid rgba(0,0,0,0.2);
    border-radius: 8px;
    font-size: 1rem;
}
.coin-form button {
    padding: 12px 32px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border: none;
    border-radius: 8px;
    font-size: 1rem;
    font-weight: 600;
    cursor: pointer;
    transition: transform 0.2s;
}
.coin-form button:hover {
    transform: scale(1.05);
}
.coin-form button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}
#result-section {
    display: none;
}
.stats-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 16px;
    margin-bottom: 24px;
}
.stat-card {
    background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
    color: white;
    padding: 20px;
    border-radius: 12px;
    text-align: center;
}
.stat-card h3 {
    font-size: 2rem;
    margin-bottom: 8px;
}
.stat-card p {
    font-size: 0.9rem;
    opacity: 0.9;
}
</style>
<div class="m-shell">
    <?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
    <main class="m-container" style="padding: 32px 20px 48px;">
        <h1 style="margin-bottom: 24px; color: var(--m-text);">๐Ÿช™ ๋™์ „ ๋˜์ง€๊ธฐ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ</h1>

        <div class="coin-container">
            <form class="coin-form" id="coin-form">
                <input type="number" id="trials" name="trials" placeholder="์‹œํ–‰ ํšŸ์ˆ˜ (10~100,000)" min="10" max="100000" value="1000" required>
                <button type="submit" id="simulate-btn">์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹œ์ž‘</button>
            </form>
            <p style="color: var(--m-text-muted); font-size: 0.9rem;">๐Ÿ’ก ์‹œํ–‰ ํšŸ์ˆ˜๊ฐ€ ๋งŽ์„์ˆ˜๋ก ์ด๋ก ์  ํ™•๋ฅ (50%/50%)์— ๊ฐ€๊นŒ์›Œ์ง‘๋‹ˆ๋‹ค.</p>
        </div>

        <div id="result-section">
            <div class="stats-grid">
                <div class="stat-card">
                    <h3 id="heads-count">0</h3>
                    <p>์•ž๋ฉด (<span id="heads-percent">0</span>%)</p>
                </div>
                <div class="stat-card" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
                    <h3 id="tails-count">0</h3>
                    <p>๋’ท๋ฉด (<span id="tails-percent">0</span>%)</p>
                </div>
            </div>

            <div style="background: var(--m-surface); padding: 24px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow);">
                <canvas id="coin-chart"></canvas>
            </div>
        </div>
    </main>
    <?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const form = document.getElementById('coin-form');
const btn = document.getElementById('simulate-btn');
const resultSection = document.getElementById('result-section');
let chart = null;

form.addEventListener('submit', async (e) => {
    e.preventDefault();
    const trials = document.getElementById('trials').value;
    if (trials < 10 || trials > 100000) {
        alert('์‹œํ–‰ ํšŸ์ˆ˜๋Š” 10~100,000 ์‚ฌ์ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.');
        return;
    }

    btn.disabled = true;
    btn.textContent = '์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ค‘...';

    const formData = new FormData();
    formData.append('action', 'simulate');
    formData.append('trials', trials);

    const res = await fetch('/probability/coin', {
        method: 'POST',
        body: formData
    });
    const data = await res.json();

    if (data.error) {
        alert(data.error);
        btn.disabled = false;
        btn.textContent = '์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹œ์ž‘';
        return;
    }

    // ๊ฒฐ๊ณผ ํ‘œ์‹œ
    document.getElementById('heads-count').textContent = data.heads.toLocaleString();
    document.getElementById('tails-count').textContent = data.tails.toLocaleString();
    document.getElementById('heads-percent').textContent = data.headsPercent;
    document.getElementById('tails-percent').textContent = data.tailsPercent;

    // ์ฐจํŠธ ๋ Œ๋”๋ง
    if (chart) chart.destroy();
    const ctx = document.getElementById('coin-chart').getContext('2d');
    chart = new Chart(ctx, {
        type: 'bar',
        data: {
            labels: ['์•ž๋ฉด', '๋’ท๋ฉด'],
            datasets: [{
                label: '๊ฒฐ๊ณผ',
                data: [data.heads, data.tails],
                backgroundColor: ['#f093fb', '#4facfe']
            }, {
                label: '์ด๋ก ๊ฐ’ (50%)',
                data: [data.trials / 2, data.trials / 2],
                backgroundColor: ['rgba(0,0,0,0.2)', 'rgba(0,0,0,0.2)'],
                borderColor: ['#000', '#000'],
                borderWidth: 2,
                type: 'line'
            }]
        },
        options: {
            responsive: true,
            plugins: {
                legend: { display: true }
            }
        }
    });

    resultSection.style.display = 'block';
    btn.disabled = false;
    btn.textContent = '์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹œ์ž‘';
});
</script>
<?php include_once('./tail.sub.php'); ?>
```

์ด ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๋ฉด `/probability/coin` URL๋กœ ๋™์ „ ๋˜์ง€๊ธฐ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: `/probability/coin` ์ ‘์† ์‹œ ์‹œํ–‰ ํšŸ์ˆ˜ ์ž…๋ ฅ ํผ์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค. ์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•˜๊ณ  '์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹œ์ž‘' ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์„œ๋ฒ„์—์„œ ๋™์ „ ๋˜์ง€๊ธฐ๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๊ณ , ๊ฒฐ๊ณผ๊ฐ€ ๋ง‰๋Œ€ ๊ทธ๋ž˜ํ”„์™€ ํ†ต๊ณ„ ์นด๋“œ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ์ด๋ก ๊ฐ’(50%/50%) ์„ ์ด ์ฐจํŠธ์— ์˜ค๋ฒ„๋ ˆ์ด๋˜์–ด ๋น„๊ตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹คํ—˜ ๊ฒฐ๊ณผ๊ฐ€ DB์— ์ž๋™ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.

3
Step 3
## Step 3: ์ฃผ์‚ฌ์œ„ ๊ตด๋ฆฌ๊ธฐ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๊ตฌํ˜„

### ํŒŒ์ผ ์ƒ์„ฑ: `app/modules/probability/dice/index.php`

```php
<?php
include_once('./_common.php');

$dbDir = G5_DATA_PATH.'/modules';
if (!is_dir($dbDir)) {
    @mkdir($dbDir, 0707, true);
}
$dbPath = $dbDir.'/probability.sqlite';
$db = new PDO('sqlite:'.$dbPath);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'simulate') {
    header('Content-Type: application/json');
    $trials = (int)$_POST['trials'];
    $sides = (int)$_POST['sides'];
    if ($trials < 10 || $trials > 100000) {
        echo json_encode(['error' => '์‹œํ–‰ ํšŸ์ˆ˜๋Š” 10~100,000 ์‚ฌ์ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.']);
        exit;
    }
    if (!in_array($sides, [4, 6, 8, 10, 12, 20])) {
        echo json_encode(['error' => '์ง€์›ํ•˜์ง€ ์•Š๋Š” ์ฃผ์‚ฌ์œ„ ๋ฉด ์ˆ˜์ž…๋‹ˆ๋‹ค.']);
        exit;
    }

    $counts = array_fill(1, $sides, 0);
    for ($i = 0; $i < $trials; $i++) {
        $roll = rand(1, $sides);
        $counts[$roll]++;
    }

    $percents = [];
    foreach ($counts as $face => $count) {
        $percents[$face] = round(($count / $trials) * 100, 2);
    }

    $result = [
        'counts' => $counts,
        'percents' => $percents,
        'trials' => $trials,
        'sides' => $sides,
        'theoreticalPercent' => round(100 / $sides, 2)
    ];

    $name = $sides.'๋ฉด ์ฃผ์‚ฌ์œ„ '.$trials.'ํšŒ';
    $config = json_encode(['trials' => $trials, 'sides' => $sides]);
    $resultJson = json_encode($result);
    $stmt = $db->prepare("INSERT INTO experiments (name, type, config, result) VALUES (?, ?, ?, ?)");
    $stmt->execute([$name, 'dice', $config, $resultJson]);

    echo json_encode($result);
    exit;
}

require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<style>
.dice-container {
    background: var(--m-surface);
    padding: 32px;
    border-radius: var(--m-radius-lg);
    box-shadow: var(--m-shadow);
    margin-bottom: 24px;
}
.dice-form {
    display: flex;
    gap: 12px;
    margin-bottom: 24px;
    align-items: center;
}
.dice-form select,
.dice-form input {
    flex: 1;
    padding: 12px;
    border: 1px solid rgba(0,0,0,0.2);
    border-radius: 8px;
    font-size: 1rem;
}
.dice-form button {
    padding: 12px 32px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border: none;
    border-radius: 8px;
    font-size: 1rem;
    font-weight: 600;
    cursor: pointer;
}
#result-section {
    display: none;
}
.theory-note {
    background: rgba(102, 126, 234, 0.1);
    padding: 16px;
    border-radius: 8px;
    margin-top: 16px;
    color: var(--m-text);
}
</style>
<div class="m-shell">
    <?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
    <main class="m-container" style="padding: 32px 20px 48px;">
        <h1 style="margin-bottom: 24px; color: var(--m-text);">๐ŸŽฒ ์ฃผ์‚ฌ์œ„ ๊ตด๋ฆฌ๊ธฐ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ</h1>

        <div class="dice-container">
            <form class="dice-form" id="dice-form">
                <select id="sides" name="sides" required>
                    <option value="4">4๋ฉด์ฒด (D4)</option>
                    <option value="6" selected>6๋ฉด์ฒด (D6)</option>
                    <option value="8">8๋ฉด์ฒด (D8)</option>
                    <option value="10">10๋ฉด์ฒด (D10)</option>
                    <option value="12">12๋ฉด์ฒด (D12)</option>
                    <option value="20">20๋ฉด์ฒด (D20)</option>
                </select>
                <input type="number" id="trials" name="trials" placeholder="์‹œํ–‰ ํšŸ์ˆ˜" min="10" max="100000" value="6000" required>
                <button type="submit" id="simulate-btn">๊ตด๋ฆฌ๊ธฐ</button>
            </form>
        </div>

        <div id="result-section">
            <div style="background: var(--m-surface); padding: 24px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); margin-bottom: 24px;">
                <canvas id="dice-chart"></canvas>
            </div>
            <div class="theory-note">
                <strong>๐Ÿ“Š ์ด๋ก ์  ํ™•๋ฅ :</strong> ๊ฐ ๋ฉด์ด ๋‚˜์˜ฌ ํ™•๋ฅ ์€ <span id="theory-percent"></span>% ์ž…๋‹ˆ๋‹ค.
            </div>
        </div>
    </main>
    <?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const form = document.getElementById('dice-form');
const btn = document.getElementById('simulate-btn');
const resultSection = document.getElementById('result-section');
let chart = null;

form.addEventListener('submit', async (e) => {
    e.preventDefault();
    const trials = document.getElementById('trials').value;
    const sides = document.getElementById('sides').value;

    btn.disabled = true;
    btn.textContent = '์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ค‘...';

    const formData = new FormData();
    formData.append('action', 'simulate');
    formData.append('trials', trials);
    formData.append('sides', sides);

    const res = await fetch('/probability/dice', {
        method: 'POST',
        body: formData
    });
    const data = await res.json();

    if (data.error) {
        alert(data.error);
        btn.disabled = false;
        btn.textContent = '๊ตด๋ฆฌ๊ธฐ';
        return;
    }

    // ์ฐจํŠธ ๋ Œ๋”๋ง
    const labels = Object.keys(data.counts);
    const counts = Object.values(data.counts);
    const theoreticalLine = Array(labels.length).fill(data.trials / data.sides);

    if (chart) chart.destroy();
    const ctx = document.getElementById('dice-chart').getContext('2d');
    chart = new Chart(ctx, {
        type: 'bar',
        data: {
            labels: labels.map(l => `๋ฉด ${l}`),
            datasets: [{
                label: '์‹ค์ œ ๊ฒฐ๊ณผ',
                data: counts,
                backgroundColor: 'rgba(102, 126, 234, 0.7)'
            }, {
                label: '์ด๋ก ๊ฐ’',
                data: theoreticalLine,
                type: 'line',
                borderColor: '#f5576c',
                borderWidth: 2,
                fill: false
            }]
        },
        options: {
            responsive: true,
            plugins: {
                legend: { display: true }
            }
        }
    });

    document.getElementById('theory-percent').textContent = data.theoreticalPercent;
    resultSection.style.display = 'block';
    btn.disabled = false;
    btn.textContent = '๊ตด๋ฆฌ๊ธฐ';
});
</script>
<?php include_once('./tail.sub.php'); ?>
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: `/probability/dice` ์ ‘์† ์‹œ ์ฃผ์‚ฌ์œ„ ๋ฉด ์ˆ˜๋ฅผ ์„ ํƒํ•˜๋Š” ๋“œ๋กญ๋‹ค์šด๊ณผ ์‹œํ–‰ ํšŸ์ˆ˜ ์ž…๋ ฅ ํผ์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค. '๊ตด๋ฆฌ๊ธฐ' ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์„œ๋ฒ„์—์„œ ์ฃผ์‚ฌ์œ„๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๊ณ , ๊ฐ ๋ฉด์ด ๋‚˜์˜จ ํšŸ์ˆ˜๊ฐ€ ๋ง‰๋Œ€ ๊ทธ๋ž˜ํ”„๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ์ด๋ก ๊ฐ’(1/N)์ด ๋นจ๊ฐ„ ์„ ์œผ๋กœ ์˜ค๋ฒ„๋ ˆ์ด๋˜์–ด ์‹ค์ œ ๊ฒฐ๊ณผ์™€ ๋น„๊ตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

4
Step 4
## Step 4: ์นด๋“œ ๋ฝ‘๊ธฐ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๊ตฌํ˜„

### ํŒŒ์ผ ์ƒ์„ฑ: `app/modules/probability/card/index.php`

```php
<?php
include_once('./_common.php');

$dbDir = G5_DATA_PATH.'/modules';
if (!is_dir($dbDir)) {
    @mkdir($dbDir, 0707, true);
}
$dbPath = $dbDir.'/probability.sqlite';
$db = new PDO('sqlite:'.$dbPath);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'simulate') {
    header('Content-Type: application/json');
    $trials = (int)$_POST['trials'];
    $targetSuit = $_POST['suit'] ?? 'any';
    $targetRank = $_POST['rank'] ?? 'any';

    if ($trials < 10 || $trials > 100000) {
        echo json_encode(['error' => '์‹œํ–‰ ํšŸ์ˆ˜๋Š” 10~100,000 ์‚ฌ์ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.']);
        exit;
    }

    $suits = ['โ™ ', 'โ™ฅ', 'โ™ฆ', 'โ™ฃ'];
    $ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'];
    $deck = [];
    foreach ($suits as $suit) {
        foreach ($ranks as $rank) {
            $deck[] = $suit.$rank;
        }
    }

    $matchCount = 0;
    $suitCounts = array_fill_keys($suits, 0);
    $rankCounts = array_fill_keys($ranks, 0);

    for ($i = 0; $i < $trials; $i++) {
        $card = $deck[array_rand($deck)];
        $suit = mb_substr($card, 0, 1);
        $rank = mb_substr($card, 1);

        $suitCounts[$suit]++;
        $rankCounts[$rank]++;

        $suitMatch = ($targetSuit === 'any' || $suit === $targetSuit);
        $rankMatch = ($targetRank === 'any' || $rank === $targetRank);
        if ($suitMatch && $rankMatch) {
            $matchCount++;
        }
    }

    $theoreticalProb = 1;
    if ($targetSuit !== 'any') $theoreticalProb *= 0.25;
    if ($targetRank !== 'any') $theoreticalProb *= (1/13);

    $result = [
        'matchCount' => $matchCount,
        'matchPercent' => round(($matchCount / $trials) * 100, 2),
        'theoreticalPercent' => round($theoreticalProb * 100, 2),
        'suitCounts' => $suitCounts,
        'rankCounts' => $rankCounts,
        'trials' => $trials
    ];

    $name = '์นด๋“œ ๋ฝ‘๊ธฐ '.$trials.'ํšŒ';
    $config = json_encode(['trials' => $trials, 'suit' => $targetSuit, 'rank' => $targetRank]);
    $resultJson = json_encode($result);
    $stmt = $db->prepare("INSERT INTO experiments (name, type, config, result) VALUES (?, ?, ?, ?)");
    $stmt->execute([$name, 'card', $config, $resultJson]);

    echo json_encode($result);
    exit;
}

require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<style>
.card-container {
    background: var(--m-surface);
    padding: 32px;
    border-radius: var(--m-radius-lg);
    box-shadow: var(--m-shadow);
    margin-bottom: 24px;
}
.card-form {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 12px;
    margin-bottom: 24px;
}
.card-form select,
.card-form input {
    padding: 12px;
    border: 1px solid rgba(0,0,0,0.2);
    border-radius: 8px;
    font-size: 1rem;
}
.card-form button {
    padding: 12px 32px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border: none;
    border-radius: 8px;
    font-size: 1rem;
    font-weight: 600;
    cursor: pointer;
    grid-column: span 2;
}
#result-section {
    display: none;
}
.match-stat {
    background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
    color: white;
    padding: 32px;
    border-radius: var(--m-radius-lg);
    text-align: center;
    margin-bottom: 24px;
}
.match-stat h2 {
    font-size: 3rem;
    margin-bottom: 8px;
}
</style>
<div class="m-shell">
    <?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
    <main class="m-container" style="padding: 32px 20px 48px;">
        <h1 style="margin-bottom: 24px; color: var(--m-text);">๐Ÿƒ ์นด๋“œ ๋ฝ‘๊ธฐ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ</h1>

        <div class="card-container">
            <form class="card-form" id="card-form">
                <select id="suit" name="suit">
                    <option value="any">๋ชจ๋“  ๋ฌด๋Šฌ</option>
                    <option value="โ™ ">โ™  ์ŠคํŽ˜์ด๋“œ</option>
                    <option value="โ™ฅ">โ™ฅ ํ•˜ํŠธ</option>
                    <option value="โ™ฆ">โ™ฆ ๋‹ค์ด์•„๋ชฌ๋“œ</option>
                    <option value="โ™ฃ">โ™ฃ ํด๋กœ๋ฒ„</option>
                </select>
                <select id="rank" name="rank">
                    <option value="any">๋ชจ๋“  ์ˆซ์ž</option>
                    <option value="A">A</option>
                    <option value="2">2</option>
                    <option value="3">3</option>
                    <option value="4">4</option>
                    <option value="5">5</option>
                    <option value="6">6</option>
                    <option value="7">7</option>
                    <option value="8">8</option>
                    <option value="9">9</option>
                    <option value="10">10</option>
                    <option value="J">J</option>
                    <option value="Q">Q</option>
                    <option value="K">K</option>
                </select>
                <input type="number" id="trials" name="trials" placeholder="์‹œํ–‰ ํšŸ์ˆ˜" min="10" max="100000" value="5200" required>
                <button type="submit" id="simulate-btn">๋ฝ‘๊ธฐ ์‹œ์ž‘</button>
            </form>
            <p style="color: var(--m-text-muted); font-size: 0.9rem;">๐Ÿ’ก ํŠน์ • ๋ฌด๋Šฌ๋‚˜ ์ˆซ์ž๋ฅผ ์„ ํƒํ•˜๋ฉด ํ•ด๋‹น ์กฐ๊ฑด์— ๋งž๋Š” ์นด๋“œ๊ฐ€ ๋‚˜์˜ฌ ํ™•๋ฅ ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.</p>
        </div>

        <div id="result-section">
            <div class="match-stat">
                <h2 id="match-count">0</h2>
                <p>์กฐ๊ฑด ์ผ์น˜ (<span id="match-percent">0</span>%)</p>
                <small>์ด๋ก ์  ํ™•๋ฅ : <span id="theory-percent">0</span>%</small>
            </div>

            <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px;">
                <div style="background: var(--m-surface); padding: 24px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow);">
                    <h3 style="margin-bottom: 16px; color: var(--m-text);">๋ฌด๋Šฌ๋ณ„ ๋ถ„ํฌ</h3>
                    <canvas id="suit-chart"></canvas>
                </div>
                <div style="background: var(--m-surface); padding: 24px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow);">
                    <h3 style="margin-bottom: 16px; color: var(--m-text);">์ˆซ์ž๋ณ„ ๋ถ„ํฌ</h3>
                    <canvas id="rank-chart"></canvas>
                </div>
            </div>
        </div>
    </main>
    <?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const form = document.getElementById('card-form');
const btn = document.getElementById('simulate-btn');
const resultSection = document.getElementById('result-section');
let suitChart = null;
let rankChart = null;

form.addEventListener('submit', async (e) => {
    e.preventDefault();
    btn.disabled = true;
    btn.textContent = '์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ค‘...';

    const formData = new FormData(form);
    formData.append('action', 'simulate');

    const res = await fetch('/probability/card', {
        method: 'POST',
        body: formData
    });
    const data = await res.json();

    if (data.error) {
        alert(data.error);
        btn.disabled = false;
        btn.textContent = '๋ฝ‘๊ธฐ ์‹œ์ž‘';
        return;
    }

    document.getElementById('match-count').textContent = data.matchCount.toLocaleString();
    document.getElementById('match-percent').textContent = data.matchPercent;
    document.getElementById('theory-percent').textContent = data.theoreticalPercent;

    // ๋ฌด๋Šฌ ์ฐจํŠธ
    if (suitChart) suitChart.destroy();
    const suitCtx = document.getElementById('suit-chart').getContext('2d');
    suitChart = new Chart(suitCtx, {
        type: 'pie',
        data: {
            labels: Object.keys(data.suitCounts),
            datasets: [{
                data: Object.values(data.suitCounts),
                backgroundColor: ['#000', '#f5576c', '#f093fb', '#4facfe']
            }]
        },
        options: { responsive: true }
    });

    // ์ˆซ์ž ์ฐจํŠธ
    if (rankChart) rankChart.destroy();
    const rankCtx = document.getElementById('rank-chart').getContext('2d');
    rankChart = new Chart(rankCtx, {
        type: 'bar',
        data: {
            labels: Object.keys(data.rankCounts),
            datasets: [{
                label: '๋นˆ๋„',
                data: Object.values(data.rankCounts),
                backgroundColor: 'rgba(102, 126, 234, 0.7)'
            }]
        },
        options: { responsive: true }
    });

    resultSection.style.display = 'block';
    btn.disabled = false;
    btn.textContent = '๋ฝ‘๊ธฐ ์‹œ์ž‘';
});
</script>
<?php include_once('./tail.sub.php'); ?>
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: `/probability/card` ์ ‘์† ์‹œ ๋ฌด๋Šฌ์™€ ์ˆซ์ž๋ฅผ ์„ ํƒํ•˜๋Š” ํผ์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค. ์กฐ๊ฑด์„ ์„ค์ •ํ•˜๊ณ  ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ์‹คํ–‰ํ•˜๋ฉด ์กฐ๊ฑด์— ๋งž๋Š” ์นด๋“œ๊ฐ€ ๋‚˜์˜จ ํšŸ์ˆ˜์™€ ํ™•๋ฅ ์ด ํ‘œ์‹œ๋˜๊ณ , ๋ฌด๋Šฌ๋ณ„/์ˆซ์ž๋ณ„ ๋ถ„ํฌ๊ฐ€ ํŒŒ์ด ์ฐจํŠธ์™€ ๋ง‰๋Œ€ ๊ทธ๋ž˜ํ”„๋กœ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.

5
Step 5
## Step 5: ๊ฐ€์ฑ  ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๊ตฌํ˜„

### ํŒŒ์ผ ์ƒ์„ฑ: `app/modules/probability/gacha/index.php`

```php
<?php
include_once('./_common.php');

$dbDir = G5_DATA_PATH.'/modules';
if (!is_dir($dbDir)) {
    @mkdir($dbDir, 0707, true);
}
$dbPath = $dbDir.'/probability.sqlite';
$db = new PDO('sqlite:'.$dbPath);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'simulate') {
    header('Content-Type: application/json');
    $trials = (int)$_POST['trials'];
    $ssrRate = (float)$_POST['ssr_rate'];
    $srRate = (float)$_POST['sr_rate'];
    $rRate = (float)$_POST['r_rate'];

    if ($trials < 10 || $trials > 100000) {
        echo json_encode(['error' => '์‹œํ–‰ ํšŸ์ˆ˜๋Š” 10~100,000 ์‚ฌ์ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.']);
        exit;
    }
    if (abs(($ssrRate + $srRate + $rRate) - 100) > 0.01) {
        echo json_encode(['error' => 'ํ™•๋ฅ  ํ•ฉ๊ณ„๋Š” 100%์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.']);
        exit;
    }

    $ssrCount = 0;
    $srCount = 0;
    $rCount = 0;

    for ($i = 0; $i < $trials; $i++) {
        $rand = mt_rand() / mt_getrandmax() * 100;
        if ($rand < $ssrRate) {
            $ssrCount++;
        } elseif ($rand < $ssrRate + $srRate) {
            $srCount++;
        } else {
            $rCount++;
        }
    }

    $result = [
        'ssr' => $ssrCount,
        'sr' => $srCount,
        'r' => $rCount,
        'ssrPercent' => round(($ssrCount / $trials) * 100, 2),
        'srPercent' => round(($srCount / $trials) * 100, 2),
        'rPercent' => round(($rCount / $trials) * 100, 2),
        'trials' => $trials,
        'rates' => ['ssr' => $ssrRate, 'sr' => $srRate, 'r' => $rRate]
    ];

    $name = '๊ฐ€์ฑ  '.$trials.'ํšŒ';
    $config = json_encode(['trials' => $trials, 'ssr_rate' => $ssrRate, 'sr_rate' => $srRate, 'r_rate' => $rRate]);
    $resultJson = json_encode($result);
    $stmt = $db->prepare("INSERT INTO experiments (name, type, config, result) VALUES (?, ?, ?, ?)");
    $stmt->execute([$name, 'gacha', $config, $resultJson]);

    echo json_encode($result);
    exit;
}

require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<style>
.gacha-container {
    background: var(--m-surface);
    padding: 32px;
    border-radius: var(--m-radius-lg);
    box-shadow: var(--m-shadow);
    margin-bottom: 24px;
}
.gacha-form {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 12px;
    margin-bottom: 24px;
}
.gacha-form input {
    padding: 12px;
    border: 1px solid rgba(0,0,0,0.2);
    border-radius: 8px;
    font-size: 1rem;
}
.gacha-form button {
    padding: 12px 32px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border: none;
    border-radius: 8px;
    font-size: 1rem;
    font-weight: 600;
    cursor: pointer;
    grid-column: span 2;
}
#result-section {
    display: none;
}
.rarity-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 16px;
    margin-bottom: 24px;
}
.rarity-card {
    padding: 24px;
    border-radius: var(--m-radius-lg);
    color: white;
    text-align: center;
}
.rarity-card.ssr {
    background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.rarity-card.sr {
    background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.rarity-card.r {
    background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
.rarity-card h3 {
    font-size: 2rem;
    margin-bottom: 8px;
}
</style>
<div class="m-shell">
    <?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
    <main class="m-container" style="padding: 32px 20px 48px;">
        <h1 style="margin-bottom: 24px; color: var(--m-text);">๐ŸŽ ๊ฐ€์ฑ  ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ</h1>

        <div class="gacha-container">
            <form class="gacha-form" id="gacha-form">
                <input type="number" id="ssr_rate" name="ssr_rate" placeholder="SSR ํ™•๋ฅ  (%)" step="0.01" value="0.5" required>
                <input type="number" id="sr_rate" name="sr_rate" placeholder="SR ํ™•๋ฅ  (%)" step="0.01" value="5" required>
                <input type="number" id="r_rate" name="r_rate" placeholder="R ํ™•๋ฅ  (%)" step="0.01" value="94.5" required>
                <input type="number" id="trials" name="trials" placeholder="์‹œํ–‰ ํšŸ์ˆ˜" min="10" max="100000" value="10000" required>
                <button type="submit" id="simulate-btn">๊ฐ€์ฑ  ๋Œ๋ฆฌ๊ธฐ</button>
            </form>
            <p style="color: var(--m-text-muted); font-size: 0.9rem;">๐Ÿ’ก ํ™•๋ฅ  ํ•ฉ๊ณ„๋Š” 100%์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ: SSR 0.5% + SR 5% + R 94.5% = 100%</p>
        </div>

        <div id="result-section">
            <div class="rarity-grid">
                <div class="rarity-card ssr">
                    <h3 id="ssr-count">0</h3>
                    <p>SSR (<span id="ssr-percent">0</span>%)</p>
                    <small>์ด๋ก : <span id="ssr-theory">0</span>%</small>
                </div>
                <div class="rarity-card sr">
                    <h3 id="sr-count">0</h3>
                    <p>SR (<span id="sr-percent">0</span>%)</p>
                    <small>์ด๋ก : <span id="sr-theory">0</span>%</small>
                </div>
                <div class="rarity-card r">
                    <h3 id="r-count">0</h3>
                    <p>R (<span id="r-percent">0</span>%)</p>
                    <small>์ด๋ก : <span id="r-theory">0</span>%</small>
                </div>
            </div>

            <div style="background: var(--m-surface); padding: 24px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow);">
                <h3 style="margin-bottom: 16px; color: var(--m-text);">๋“ฑ๊ธ‰๋ณ„ ๋ถ„ํฌ</h3>
                <canvas id="gacha-chart"></canvas>
            </div>
        </div>
    </main>
    <?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const form = document.getElementById('gacha-form');
const btn = document.getElementById('simulate-btn');
const resultSection = document.getElementById('result-section');
let chart = null;

form.addEventListener('submit', async (e) => {
    e.preventDefault();
    btn.disabled = true;
    btn.textContent = '์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ค‘...';

    const formData = new FormData(form);
    formData.append('action', 'simulate');

    const res = await fetch('/probability/gacha', {
        method: 'POST',
        body: formData
    });
    const data = await res.json();

    if (data.error) {
        alert(data.error);
        btn.disabled = false;
        btn.textContent = '๊ฐ€์ฑ  ๋Œ๋ฆฌ๊ธฐ';
        return;
    }

    document.getElementById('ssr-count').textContent = data.ssr.toLocaleString();
    document.getElementById('sr-count').textContent = data.sr.toLocaleString();
    document.getElementById('r-count').textContent = data.r.toLocaleString();
    document.getElementById('ssr-percent').textContent = data.ssrPercent;
    document.getElementById('sr-percent').textContent = data.srPercent;
    document.getElementById('r-percent').textContent = data.rPercent;
    document.getElementById('ssr-theory').textContent = data.rates.ssr;
    document.getElementById('sr-theory').textContent = data.rates.sr;
    document.getElementById('r-theory').textContent = data.rates.r;

    if (chart) chart.destroy();
    const ctx = document.getElementById('gacha-chart').getContext('2d');
    chart = new Chart(ctx, {
        type: 'doughnut',
        data: {
            labels: ['SSR', 'SR', 'R'],
            datasets: [{
                data: [data.ssr, data.sr, data.r],
                backgroundColor: ['#f093fb', '#4facfe', '#43e97b']
            }]
        },
        options: { responsive: true }
    });

    resultSection.style.display = 'block';
    btn.disabled = false;
    btn.textContent = '๊ฐ€์ฑ  ๋Œ๋ฆฌ๊ธฐ';
});
</script>
<?php include_once('./tail.sub.php'); ?>
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: `/probability/gacha` ์ ‘์† ์‹œ SSR/SR/R ๊ฐ ๋“ฑ๊ธ‰์˜ ํ™•๋ฅ ์„ ์ž…๋ ฅํ•˜๋Š” ํผ์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค. ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ์‹คํ–‰ํ•˜๋ฉด ๊ฐ ๋“ฑ๊ธ‰๋ณ„๋กœ ๋ฝ‘ํžŒ ํšŸ์ˆ˜์™€ ๋น„์œจ์ด ํ‘œ์‹œ๋˜๊ณ , ๋„๋„› ์ฐจํŠธ๋กœ ๋ถ„ํฌ๊ฐ€ ์‹œ๊ฐํ™”๋ฉ๋‹ˆ๋‹ค. ์ด๋ก ์  ํ™•๋ฅ ๊ณผ ์‹ค์ œ ๊ฒฐ๊ณผ๋ฅผ ๋น„๊ตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

6
Step 6
## Step 6: ์‹คํ—˜ ํžˆ์Šคํ† ๋ฆฌ ๋ฐ ๋ถ๋งˆํฌ ํŽ˜์ด์ง€ ๊ตฌํ˜„

### ํŒŒ์ผ ์ƒ์„ฑ: `app/modules/probability/history/index.php`

```php
<?php
include_once('./_common.php');

$dbDir = G5_DATA_PATH.'/modules';
if (!is_dir($dbDir)) {
    @mkdir($dbDir, 0707, true);
}
$dbPath = $dbDir.'/probability.sqlite';
$db = new PDO('sqlite:'.$dbPath);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// ์‹คํ—˜ ์‚ญ์ œ ์ฒ˜๋ฆฌ
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'delete') {
    $id = (int)$_POST['id'];
    $stmt = $db->prepare("DELETE FROM experiments WHERE id = ?");
    $stmt->execute([$id]);
    header('Location: /probability/history');
    exit;
}

// ์‹คํ—˜ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
$stmt = $db->query("SELECT * FROM experiments ORDER BY created_at DESC LIMIT 50");
$experiments = $stmt->fetchAll(PDO::FETCH_ASSOC);

require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<style>
.history-container {
    background: var(--m-surface);
    padding: 32px;
    border-radius: var(--m-radius-lg);
    box-shadow: var(--m-shadow);
}
.experiment-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 20px;
    border-bottom: 1px solid rgba(0,0,0,0.1);
}
.experiment-item:last-child {
    border-bottom: none;
}
.exp-info h3 {
    margin-bottom: 8px;
    color: var(--m-text);
}
.exp-info p {
    color: var(--m-text-muted);
    font-size: 0.9rem;
}
.exp-actions {
    display: flex;
    gap: 8px;
}
.btn-view {
    padding: 8px 16px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    text-decoration: none;
    font-size: 0.9rem;
}
.btn-delete {
    padding: 8px 16px;
    background: #f5576c;
    color: white;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    font-size: 0.9rem;
}
.empty-state {
    text-align: center;
    padding: 64px 20px;
    color: var(--m-text-muted);
}
.empty-state h2 {
    font-size: 2rem;
    margin-bottom: 16px;
}
</style>
<div class="m-shell">
    <?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
    <main class="m-container" style="padding: 32px 20px 48px;">
        <h1 style="margin-bottom: 24px; color: var(--m-text);">๐Ÿ“Š ์‹คํ—˜ ํžˆ์Šคํ† ๋ฆฌ</h1>

        <div class="history-container">
            <?php if (empty($experiments)): ?>
                <div class="empty-state">
                    <h2>๐Ÿ”ฌ</h2>
                    <p>์•„์ง ์‹คํ—˜ ๊ธฐ๋ก์ด ์—†์Šต๋‹ˆ๋‹ค.</p>
                    <p><a href="/probability" style="color: #667eea;">๋ฉ”์ธ ํŽ˜์ด์ง€์—์„œ ์‹คํ—˜์„ ์‹œ์ž‘ํ•˜์„ธ์š”</a></p>
                </div>
            <?php else: ?>
                <?php foreach ($experiments as $exp): ?>
                    <?php
                    $typeEmoji = [
                        'coin' => '๐Ÿช™',
                        'dice' => '๐ŸŽฒ',
                        'card' => '๐Ÿƒ',
                        'gacha' => '๐ŸŽ'
                    ];
                    $emoji = $typeEmoji[$exp['type']] ?? '๐Ÿ”ฌ';
                    $result = json_decode($exp['result'], true);
                    ?>
                    <div class="experiment-item">
                        <div class="exp-info">
                            <h3><?php echo $emoji.' '.htmlspecialchars($exp['name']); ?></h3>
                            <p><?php echo date('Y-m-d H:i', strtotime($exp['created_at'])); ?></p>
                        </div>
                        <div class="exp-actions">
                            <button class="btn-view" onclick="viewResult(<?php echo $exp['id']; ?>)">๊ฒฐ๊ณผ ๋ณด๊ธฐ</button>
                            <form method="post" style="display: inline;" onsubmit="return confirm('์ •๋ง ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?');">
                                <input type="hidden" name="action" value="delete">
                                <input type="hidden" name="id" value="<?php echo $exp['id']; ?>">
                                <button type="submit" class="btn-delete">์‚ญ์ œ</button>
                            </form>
                        </div>
                    </div>
                <?php endforeach; ?>
            <?php endif; ?>
        </div>
    </main>
    <?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<script>
function viewResult(id) {
    // ๊ฒฐ๊ณผ ์ƒ์„ธ ๋ชจ๋‹ฌ ๊ตฌํ˜„ (๋ณด๋„ˆ์Šค)
    alert('์‹คํ—˜ ID: ' + id + ' ๊ฒฐ๊ณผ ๋ณด๊ธฐ ๊ธฐ๋Šฅ์€ ๋ณด๋„ˆ์Šค๋กœ ๊ตฌํ˜„ ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.');
}
</script>
<?php include_once('./tail.sub.php'); ?>
```

์ด ํŽ˜์ด์ง€๋Š” ๊ณผ๊ฑฐ ์‹คํ—˜ ๊ธฐ๋ก์„ ์ตœ๊ทผ 50๊ฐœ๊นŒ์ง€ ํ‘œ์‹œํ•˜๊ณ , ๊ฐ ์‹คํ—˜์„ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: `/probability/history` ์ ‘์† ์‹œ ๊ณผ๊ฑฐ ์‹คํ—˜ ๋ชฉ๋ก์ด ์‹œ๊ฐ„ ์—ญ์ˆœ์œผ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๊ฐ ์‹คํ—˜ ํ•ญ๋ชฉ์—๋Š” ํƒ€์ž… ์ด๋ชจ์ง€, ์ด๋ฆ„, ์ƒ์„ฑ ์‹œ๊ฐ„์ด ํ‘œ์‹œ๋˜๊ณ , '๊ฒฐ๊ณผ ๋ณด๊ธฐ'์™€ '์‚ญ์ œ' ๋ฒ„ํŠผ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์‚ญ์ œ ์‹œ ํ™•์ธ ์ฐฝ์ด ๋œจ๊ณ , ์‚ญ์ œ ํ›„ ํŽ˜์ด์ง€๊ฐ€ ์ƒˆ๋กœ๊ณ ์นจ๋ฉ๋‹ˆ๋‹ค.

7
Step 7
## Step 7: ๋น„๊ต ๋ชจ๋“œ ๊ตฌํ˜„ (2๊ฐœ ์‹คํ—˜ ๊ฒฐ๊ณผ ๋‚˜๋ž€ํžˆ ๋น„๊ต)

### ํŒŒ์ผ ์ƒ์„ฑ: `app/modules/probability/compare/index.php`

```php
<?php
include_once('./_common.php');

$dbDir = G5_DATA_PATH.'/modules';
if (!is_dir($dbDir)) {
    @mkdir($dbDir, 0707, true);
}
$dbPath = $dbDir.'/probability.sqlite';
$db = new PDO('sqlite:'.$dbPath);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// ์‹คํ—˜ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
$stmt = $db->query("SELECT id, name, type, result FROM experiments ORDER BY created_at DESC");
$experiments = $stmt->fetchAll(PDO::FETCH_ASSOC);

$exp1 = null;
$exp2 = null;

if (isset($_GET['id1']) && isset($_GET['id2'])) {
    $id1 = (int)$_GET['id1'];
    $id2 = (int)$_GET['id2'];
    $stmt = $db->prepare("SELECT * FROM experiments WHERE id = ?");
    $stmt->execute([$id1]);
    $exp1 = $stmt->fetch(PDO::FETCH_ASSOC);
    $stmt->execute([$id2]);
    $exp2 = $stmt->fetch(PDO::FETCH_ASSOC);
}

require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<style>
.compare-selector {
    background: var(--m-surface);
    padding: 32px;
    border-radius: var(--m-radius-lg);
    box-shadow: var(--m-shadow);
    margin-bottom: 24px;
}
.compare-form {
    display: grid;
    grid-template-columns: 1fr 1fr auto;
    gap: 12px;
    align-items: center;
}
.compare-form select {
    padding: 12px;
    border: 1px solid rgba(0,0,0,0.2);
    border-radius: 8px;
    font-size: 1rem;
}
.compare-form button {
    padding: 12px 32px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border: none;
    border-radius: 8px;
    font-size: 1rem;
    font-weight: 600;
    cursor: pointer;
}
.compare-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 24px;
}
.compare-card {
    background: var(--m-surface);
    padding: 24px;
    border-radius: var(--m-radius-lg);
    box-shadow: var(--m-shadow);
}
.compare-card h2 {
    margin-bottom: 16px;
    color: var(--m-text);
}
</style>
<div class="m-shell">
    <?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
    <main class="m-container" style="padding: 32px 20px 48px;">
        <h1 style="margin-bottom: 24px; color: var(--m-text);">๐Ÿ”€ ์‹คํ—˜ ๋น„๊ต ๋ชจ๋“œ</h1>

        <div class="compare-selector">
            <form class="compare-form" method="get">
                <select name="id1" required>
                    <option value="">์‹คํ—˜ 1 ์„ ํƒ</option>
                    <?php foreach ($experiments as $exp): ?>
                        <option value="<?php echo $exp['id']; ?>" <?php echo (isset($_GET['id1']) && $_GET['id1'] == $exp['id']) ? 'selected' : ''; ?>>
                            <?php echo htmlspecialchars($exp['name']); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
                <select name="id2" required>
                    <option value="">์‹คํ—˜ 2 ์„ ํƒ</option>
                    <?php foreach ($experiments as $exp): ?>
                        <option value="<?php echo $exp['id']; ?>" <?php echo (isset($_GET['id2']) && $_GET['id2'] == $exp['id']) ? 'selected' : ''; ?>>
                            <?php echo htmlspecialchars($exp['name']); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
                <button type="submit">๋น„๊ตํ•˜๊ธฐ</button>
            </form>
        </div>

        <?php if ($exp1 && $exp2): ?>
            <div class="compare-grid">
                <div class="compare-card">
                    <h2><?php echo htmlspecialchars($exp1['name']); ?></h2>
                    <canvas id="chart1"></canvas>
                </div>
                <div class="compare-card">
                    <h2><?php echo htmlspecialchars($exp2['name']); ?></h2>
                    <canvas id="chart2"></canvas>
                </div>
            </div>
            <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
            <script>
            const result1 = <?php echo $exp1['result']; ?>;
            const result2 = <?php echo $exp2['result']; ?>;
            const type1 = '<?php echo $exp1['type']; ?>';
            const type2 = '<?php echo $exp2['type']; ?>';

            function renderChart(canvasId, result, type) {
                const ctx = document.getElementById(canvasId).getContext('2d');
                if (type === 'coin') {
                    new Chart(ctx, {
                        type: 'bar',
                        data: {
                            labels: ['์•ž๋ฉด', '๋’ท๋ฉด'],
                            datasets: [{
                                label: '๊ฒฐ๊ณผ',
                                data: [result.heads, result.tails],
                                backgroundColor: ['#f093fb', '#4facfe']
                            }]
                        },
                        options: { responsive: true }
                    });
                } else if (type === 'dice') {
                    const labels = Object.keys(result.counts).map(l => `๋ฉด ${l}`);
                    const counts = Object.values(result.counts);
                    new Chart(ctx, {
                        type: 'bar',
                        data: {
                            labels: labels,
                            datasets: [{
                                label: '๊ฒฐ๊ณผ',
                                data: counts,
                                backgroundColor: 'rgba(102, 126, 234, 0.7)'
                            }]
                        },
                        options: { responsive: true }
                    });
                } else if (type === 'gacha') {
                    new Chart(ctx, {
                        type: 'doughnut',
                        data: {
                            labels: ['SSR', 'SR', 'R'],
                            datasets: [{
                                data: [result.ssr, result.sr, result.r],
                                backgroundColor: ['#f093fb', '#4facfe', '#43e97b']
                            }]
                        },
                        options: { responsive: true }
                    });
                }
            }

            renderChart('chart1', result1, type1);
            renderChart('chart2', result2, type2);
            </script>
        <?php elseif (isset($_GET['id1']) || isset($_GET['id2'])): ?>
            <p style="text-align: center; color: var(--m-text-muted); padding: 48px;">๋‘ ์‹คํ—˜์„ ๋ชจ๋‘ ์„ ํƒํ•ด์ฃผ์„ธ์š”.</p>
        <?php else: ?>
            <p style="text-align: center; color: var(--m-text-muted); padding: 48px;">์œ„์—์„œ ๋น„๊ตํ•  ์‹คํ—˜ 2๊ฐœ๋ฅผ ์„ ํƒํ•˜์„ธ์š”.</p>
        <?php endif; ?>
    </main>
    <?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<?php include_once('./tail.sub.php'); ?>
```

์ด ํŽ˜์ด์ง€๋Š” ํžˆ์Šคํ† ๋ฆฌ์— ์ €์žฅ๋œ ์‹คํ—˜ ์ค‘ 2๊ฐœ๋ฅผ ์„ ํƒํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜๋ž€ํžˆ ๋น„๊ตํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: `/probability/compare` ์ ‘์† ์‹œ ์‹คํ—˜ ๋ชฉ๋ก์ด ๋“œ๋กญ๋‹ค์šด์œผ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. 2๊ฐœ๋ฅผ ์„ ํƒํ•˜๊ณ  '๋น„๊ตํ•˜๊ธฐ' ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ๋‘ ์‹คํ—˜์˜ ๊ฒฐ๊ณผ๊ฐ€ ์ขŒ์šฐ๋กœ ๋‚˜๋ž€ํžˆ ๋ฐฐ์น˜๋˜์–ด ์ฐจํŠธ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๋™์ „/์ฃผ์‚ฌ์œ„/๊ฐ€์ฑ  ๋“ฑ ํƒ€์ž…์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ์ฐจํŠธ๊ฐ€ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค.

8
Step 8
## Step 8: ๋ฉ”์ธ ํŽ˜์ด์ง€ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋งํฌ ์ถ”๊ฐ€ ๋ฐ ์ตœ์ข… ๋งˆ๋ฌด๋ฆฌ

### ํŒŒ์ผ ์ˆ˜์ •: `app/modules/probability/index.php`

๋ฉ”์ธ ํŽ˜์ด์ง€ ํ•˜๋‹จ์— '์‹คํ—˜ ํžˆ์Šคํ† ๋ฆฌ'์™€ '๋น„๊ต ๋ชจ๋“œ' ๋งํฌ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด `<main>` ํƒœ๊ทธ ์•ˆ์—์„œ ์ตœ๊ทผ ์‹คํ—˜ ๊ธฐ๋ก ์„น์…˜ ์•„๋ž˜์— ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”:

```php
<!-- ๊ธฐ์กด ์ตœ๊ทผ ์‹คํ—˜ ๊ธฐ๋ก ์„น์…˜ ์•„๋ž˜์— ์ถ”๊ฐ€ -->
<div style="display: flex; gap: 16px; margin-top: 24px;">
    <a href="/probability/history" style="flex: 1; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; text-align: center; border-radius: var(--m-radius-lg); text-decoration: none; font-weight: 600; box-shadow: var(--m-shadow);">
        ๐Ÿ“Š ์ „์ฒด ํžˆ์Šคํ† ๋ฆฌ ๋ณด๊ธฐ
    </a>
    <a href="/probability/compare" style="flex: 1; padding: 20px; background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white; text-align: center; border-radius: var(--m-radius-lg); text-decoration: none; font-weight: 600; box-shadow: var(--m-shadow);">
        ๐Ÿ”€ ์‹คํ—˜ ๋น„๊ตํ•˜๊ธฐ
    </a>
</div>
```

๋˜ํ•œ, ๊ฐ ์‹คํ—˜ ํŽ˜์ด์ง€(`coin`, `dice`, `card`, `gacha`)์˜ `<main>` ํƒœ๊ทธ ์‹œ์ž‘ ์งํ›„์— 'โ† ๋ฉ”์ธ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ' ๋งํฌ๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”:

```php
<a href="/probability" style="display: inline-block; margin-bottom: 16px; color: #667eea; text-decoration: none; font-weight: 600;">โ† ๋ฉ”์ธ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ</a>
```

### ์ตœ์ข… ํ…Œ์ŠคํŠธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

1. `/probability` ๋ฉ”์ธ ํŽ˜์ด์ง€์—์„œ 4๊ฐ€์ง€ ์‹คํ—˜ ์นด๋“œ๊ฐ€ ์ž˜ ๋ณด์ด๋Š”์ง€ ํ™•์ธ
2. ๊ฐ ์‹คํ—˜ ํŽ˜์ด์ง€์—์„œ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์ด ์ •์ƒ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธ
3. ์‹คํ—˜ ๊ฒฐ๊ณผ๊ฐ€ DB์— ์ €์žฅ๋˜๊ณ  ๋ฉ”์ธ ํŽ˜์ด์ง€์— ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธ
4. `/probability/history`์—์„œ ๊ณผ๊ฑฐ ์‹คํ—˜ ๋ชฉ๋ก์ด ๋ณด์ด๊ณ  ์‚ญ์ œ๊ฐ€ ๋˜๋Š”์ง€ ํ™•์ธ
5. `/probability/compare`์—์„œ 2๊ฐœ ์‹คํ—˜์„ ์„ ํƒํ•˜์—ฌ ๋น„๊ต ์ฐจํŠธ๊ฐ€ ๋‚˜ํƒ€๋‚˜๋Š”์ง€ ํ™•์ธ
6. ๋‹คํฌ๋ชจ๋“œ ์ „ํ™˜ ์‹œ ์ƒ‰์ƒ์ด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ฐ”๋€Œ๋Š”์ง€ ํ™•์ธ (modern shell ํ† ํฐ ๋•๋ถ„์— ์ž๋™ ๋Œ€์‘)
7. ์‹œํ–‰ ํšŸ์ˆ˜๋ฅผ 10~100,000 ๋ฒ”์œ„ ๋ฐ–์œผ๋กœ ์ž…๋ ฅ ์‹œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ๋œจ๋Š”์ง€ ํ™•์ธ

๋ชจ๋“  ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•˜๋ฉด ํ”„๋กœ์ ํŠธ ์™„์„ฑ! ๐ŸŽ‰

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋ฉ”์ธ ํŽ˜์ด์ง€์— '์ „์ฒด ํžˆ์Šคํ† ๋ฆฌ ๋ณด๊ธฐ'์™€ '์‹คํ—˜ ๋น„๊ตํ•˜๊ธฐ' ๋ฒ„ํŠผ์ด ์ถ”๊ฐ€๋˜์–ด ๊ฐ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ ์‹คํ—˜ ํŽ˜์ด์ง€์—์„œ ๋ฉ”์ธ์œผ๋กœ ๋Œ์•„๊ฐ€๋Š” ๋งํฌ๊ฐ€ ์ถ”๊ฐ€๋˜์–ด ๋„ค๋น„๊ฒŒ์ด์…˜์ด ํŽธ๋ฆฌํ•ด์ง‘๋‹ˆ๋‹ค. ๋ชจ๋“  ๊ธฐ๋Šฅ์ด ์ •์ƒ ์ž‘๋™ํ•˜๊ณ , ๋‹คํฌ๋ชจ๋“œ์—์„œ๋„ ์‹œ๊ฐ์ ์œผ๋กœ ์ผ๊ด€์„ฑ์ด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

์ œ์ถœ๋œ ์ž‘ํ’ˆ (0)

๋กœ๊ทธ์ธ ํ›„ ์ œ์ถœ

์•„์ง ์ œ์ถœ๋œ ์ž‘ํ’ˆ์ด ์—†์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ๋„์ „์ž๊ฐ€ ๋˜์–ด๋ณด์„ธ์š”!

๋Œ“๊ธ€ (0)

๋กœ๊ทธ์ธ ํ›„ ๋Œ“๊ธ€์„ ๋‚จ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ๋Œ“๊ธ€์„ ๋‚จ๊ฒจ๋ณด์„ธ์š”!