๐ฒ ํ๋ฅ ์๋ฎฌ๋ ์ดํฐ ํ๋ ์ด๊ทธ๋ผ์ด๋ - ์ฃผ์ฌ์/๋์ /๋ฝ๊ธฐ ํ๋ฅ ์คํ์ค
probability-simulator-playground
๋ฌธ์ ์ค๋ช
๐ฏ ํ๋ก์ ํธ ๊ฐ์
ํต๊ณํยทํ๋ฅ ๋ก ์ ๋ฐฐ์ฐ๋ ํ์, ๊ฒ์ ๊ธฐํ์, ๊ฐ์ฑ ์์คํ ์ค๊ณ์๋ฅผ ์ํ ๋ํํ ํ๋ฅ ์๋ฎฌ๋ ์ดํฐ์ ๋๋ค. ์ฌ์ฉ์๊ฐ ์ฃผ์ฌ์/๋์ /์นด๋/๋ฝ๊ธฐ ๋ฑ ๋ค์ํ ํ๋ฅ ์คํ์ ์ค์ ํ๊ณ , ์์ฒ ๋ฒ ์ํํ์ฌ ๊ฒฐ๊ณผ๋ฅผ ์ค์๊ฐ ์ฐจํธ๋ก ํ์ธํ ์ ์์ต๋๋ค.
โจ ํต์ฌ ๊ธฐ๋ฅ
-
๋ค์ค ์คํ ํ์
- ๋์ ๋์ง๊ธฐ (์๋ฉด/๋ท๋ฉด ๋น์จ)
- ์ฃผ์ฌ์ ๊ตด๋ฆฌ๊ธฐ (N๋ฉด์ฒด ์ง์ โ 6๋ฉด, 20๋ฉด ๋ฑ)
- ์นด๋ ๋ฝ๊ธฐ (52์ฅ ๋ฑ์์ ํน์ ๋ฌด๋ฌ/์ซ์ ํ๋ฅ )
- ์ปค์คํ ๊ฐ์ฑ (SSR 0.5%, SR 5%, R 94.5% ๊ฐ์ ๋ฑ๊ธ๋ณ ํ๋ฅ ์ค์ )
-
๋๋ ์๋ฎฌ๋ ์ด์
- ์ฌ์ฉ์๊ฐ ์ํ ํ์ ์ ๋ ฅ (์: 10,000ํ)
- ์๋ฒ์์ PHP ๋ฃจํ๋ก ์๋ฎฌ๋ ์ด์ ์คํ
- ๊ฒฐ๊ณผ๋ฅผ JSON์ผ๋ก ๋ฐํ โ JS๊ฐ Chart.js๋ก ๋ง๋/์ ๊ทธ๋ํ ๋ ๋๋ง
-
ํต๊ณ ๋ถ์
- ์ด๋ก ์ ํ๋ฅ vs ์ค์ ๊ฒฐ๊ณผ ๋น๊ต
- ํ์คํธ์ฐจ, ์ ๋ขฐ๊ตฌ๊ฐ ํ์
- ์ํ ํ์๊ฐ ๋ง์์ง์๋ก ์ด๋ก ๊ฐ์ ์๋ ดํ๋ ๊ณผ์ ์๊ฐํ
-
ํ์คํ ๋ฆฌ & ๋ถ๋งํฌ
- ๊ณผ๊ฑฐ ์คํ ๊ฒฐ๊ณผ๋ฅผ SQLite DB์ ์ ์ฅ
- ์ฌ์ฉ์๊ฐ ์คํ ์ค์ ์ ๋ถ๋งํฌํ์ฌ ์ฌ์คํ ๊ฐ๋ฅ
- ์คํ๋ณ ๋ฉ๋ชจ/ํ๊ทธ ๊ธฐ๋ฅ
-
๋น๊ต ๋ชจ๋
- ๋ ์คํ ๊ฒฐ๊ณผ๋ฅผ ๋๋ํ ๋น๊ต (์: 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์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
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` ํ ์ด๋ธ์ด ๋ง๋ค์ด์ง๋๋ค.
## 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์ ์๋ ์ ์ฅ๋ฉ๋๋ค.
## 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)์ด ๋นจ๊ฐ ์ ์ผ๋ก ์ค๋ฒ๋ ์ด๋์ด ์ค์ ๊ฒฐ๊ณผ์ ๋น๊ตํ ์ ์์ต๋๋ค.
## 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` ์ ์ ์ ๋ฌด๋ฌ์ ์ซ์๋ฅผ ์ ํํ๋ ํผ์ด ๋ํ๋ฉ๋๋ค. ์กฐ๊ฑด์ ์ค์ ํ๊ณ ์๋ฎฌ๋ ์ด์ ์ ์คํํ๋ฉด ์กฐ๊ฑด์ ๋ง๋ ์นด๋๊ฐ ๋์จ ํ์์ ํ๋ฅ ์ด ํ์๋๊ณ , ๋ฌด๋ฌ๋ณ/์ซ์๋ณ ๋ถํฌ๊ฐ ํ์ด ์ฐจํธ์ ๋ง๋ ๊ทธ๋ํ๋ก ๋ํ๋ฉ๋๋ค.
## 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 ๊ฐ ๋ฑ๊ธ์ ํ๋ฅ ์ ์ ๋ ฅํ๋ ํผ์ด ๋ํ๋ฉ๋๋ค. ์๋ฎฌ๋ ์ด์ ์ ์คํํ๋ฉด ๊ฐ ๋ฑ๊ธ๋ณ๋ก ๋ฝํ ํ์์ ๋น์จ์ด ํ์๋๊ณ , ๋๋ ์ฐจํธ๋ก ๋ถํฌ๊ฐ ์๊ฐํ๋ฉ๋๋ค. ์ด๋ก ์ ํ๋ฅ ๊ณผ ์ค์ ๊ฒฐ๊ณผ๋ฅผ ๋น๊ตํ ์ ์์ต๋๋ค.
## 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` ์ ์ ์ ๊ณผ๊ฑฐ ์คํ ๋ชฉ๋ก์ด ์๊ฐ ์ญ์์ผ๋ก ํ์๋ฉ๋๋ค. ๊ฐ ์คํ ํญ๋ชฉ์๋ ํ์ ์ด๋ชจ์ง, ์ด๋ฆ, ์์ฑ ์๊ฐ์ด ํ์๋๊ณ , '๊ฒฐ๊ณผ ๋ณด๊ธฐ'์ '์ญ์ ' ๋ฒํผ์ด ์์ต๋๋ค. ์ญ์ ์ ํ์ธ ์ฐฝ์ด ๋จ๊ณ , ์ญ์ ํ ํ์ด์ง๊ฐ ์๋ก๊ณ ์นจ๋ฉ๋๋ค.
## 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๊ฐ๋ฅผ ์ ํํ๊ณ '๋น๊ตํ๊ธฐ' ๋ฒํผ์ ๋๋ฅด๋ฉด ๋ ์คํ์ ๊ฒฐ๊ณผ๊ฐ ์ข์ฐ๋ก ๋๋ํ ๋ฐฐ์น๋์ด ์ฐจํธ๋ก ํ์๋ฉ๋๋ค. ๋์ /์ฃผ์ฌ์/๊ฐ์ฑ ๋ฑ ํ์ ์ ๋ฐ๋ผ ์ ์ ํ ์ฐจํธ๊ฐ ๋ ๋๋ง๋ฉ๋๋ค.
## 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)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!