๐ฒ ํ์ด์ ์ฃผ์ฌ์ - ๊ฒฐ์ ์ฅ์ ํด๊ฒฐ์ฌ
lucky-dice-decision-maker
๋ฌธ์ ์ค๋ช
๐ฏ ํ๋ก์ ํธ ๊ฐ์
๊ฒฐ์ ์ฅ์ ๊ฐ ์๋ ์ฌ๋๋ค์ ์ํ ๊ท์ฌ์ด ์ฃผ์ฌ์ ๊ตด๋ฆฌ๊ธฐ ํ์ด์ง๋ฅผ ๋ง๋ญ๋๋ค. ์ฌ์ฉ์๊ฐ 2~6๊ฐ์ ์ ํ์ง๋ฅผ ์ ๋ ฅํ๋ฉด ์ฃผ์ฌ์๋ฅผ ๊ตด๋ ค์ ํ๋๋ฅผ ๋๋ค์ผ๋ก ์ ํํด์ฃผ๋ ๊ฐ๋จํ ๋๊ตฌ์ ๋๋ค. ๊ทธ๋๋ณด๋5 SE์ modern shell์ ํ์ฉํ์ฌ ์ฌ์ดํธ ์ ์ฒด์ ํต์ผ๋ ๋์์ธ์ ์ ์งํ๋ฉฐ, ๋คํฌ๋ชจ๋๋ ์๋์ผ๋ก ์ง์ํฉ๋๋ค.
โจ ํ์ ๊ธฐ๋ฅ
-
์ ํ์ง ์ ๋ ฅ ํผ
- 2~6๊ฐ์ ํ ์คํธ ์ ๋ ฅ ํ๋ (๋์ ์ถ๊ฐ/์ญ์ )
- "์ ํ์ง ์ถ๊ฐ" / "์ญ์ " ๋ฒํผ
- ๋น ์ ํ์ง ์ ๋ ฅ ์ ๊ฒฝ๊ณ ๋ฉ์์ง
-
์ฃผ์ฌ์ ์ ๋๋ฉ์ด์
- "์ฃผ์ฌ์ ๊ตด๋ฆฌ๊ธฐ" ๋ฒํผ ํด๋ฆญ ์ 3D ํ์ ์ ๋๋ฉ์ด์
- ์ ๋๋ฉ์ด์ ์ข ๋ฃ ํ ๋น์ฒจ๋ ์ ํ์ง๋ฅผ ํฌ๊ฒ ํ์
- ์ฃผ์ฌ์ ๋ฉด์ 1~6 ์ซ์ ๋์ ์ ํ์ง ๋ฒํธ ํ์
-
๊ฒฐ๊ณผ ํ์
- ๋น์ฒจ๋ ์ ํ์ง๋ฅผ ๊ฐ์กฐ ํ์ (ํฐ ๊ธ์จ + ๋ฐฐ๊ฒฝ์)
- "๋ค์ ๊ตด๋ฆฌ๊ธฐ" ๋ฒํผ์ผ๋ก ์ฌ์๋ ๊ฐ๋ฅ
- ์ด์ ๊ฒฐ๊ณผ ํ์คํ ๋ฆฌ (์ต๊ทผ 5๊ฐ) ๋ฆฌ์คํธ ํ์
-
๋ฐ์ํ ๋์์ธ
- ๋ชจ๋ฐ์ผ/๋ฐ์คํฌํฑ ๋ชจ๋ ์ต์ ํ
- ๋คํฌ๋ชจ๋ ์๋ ๋์ (modern shell ํ ํฐ ์ฌ์ฉ)
๐ ๋ณด๋์ค ๊ธฐ๋ฅ (์ ํ)
- ์ฃผ์ฌ์ ๊ตด๋ฆผ ํจ๊ณผ์ ์ถ๊ฐ (Web Audio API)
- ๋น์ฒจ ํ๋ฅ ์กฐ์ ์ต์ (ํน์ ์ ํ์ง์ ๊ฐ์ค์น ๋ถ์ฌ)
- ํ์คํ ๋ฆฌ๋ฅผ localStorage์ ์ ์ฅํ์ฌ ํ์ด์ง ์๋ก๊ณ ์นจ ํ์๋ ์ ์ง
- ์ ํ์ง ํ ํ๋ฆฟ ํ๋ฆฌ์ ("์ ์ฌ ๋ฉ๋ด", "์ํ ์ฅ๋ฅด", "์ด๋ ์ข ๋ฅ" ๋ฑ)
- ์ฃผ์ฌ์ ์์/ํ ๋ง ์ปค์คํฐ๋ง์ด์ง (ํ๋์, ๋นจ๊ฐ์, ๋ฌด์ง๊ฐ ๋ฑ)
๐ ๏ธ ๊ธฐ์ ์คํ
- ๋ฐฑ์๋: PHP (๊ทธ๋๋ณด๋5 SE ๊ธฐ๋ฐ, DB ๋ถํ์)
- ํ๋ก ํธ์๋: HTML5, CSS3 (modern shell ํ ํฐ), Vanilla JavaScript
- ์ ๋๋ฉ์ด์ : CSS3 Transform & Transition
- ๋ผ์ฐํ
: g5se ํด๋ ๊ธฐ๋ฐ ์๋ ๋ผ์ฐํ
(
app/modules/lucky-dice/index.php)
๐ ๊ตฌํ ์ ์ฃผ์์ฌํญ
- DB๋ฅผ ์ฌ์ฉํ์ง ์์ผ๋ฏ๋ก ๋ชจ๋ ๋ฐ์ดํฐ๋ ํด๋ผ์ด์ธํธ ์ธก(JavaScript ๋ณ์ ๋๋ localStorage)์์ ๊ด๋ฆฌ
- modern shell์
--m-surface,--m-text,--m-primary๋ฑ CSS ๋ณ์๋ฅผ ํ์ฉํ์ฌ ๋คํฌ๋ชจ๋ ์๋ ๋์ - ์ฃผ์ฌ์ ์ ๋๋ฉ์ด์
์ CSS
@keyframes์transform: rotate3d()๋ฅผ ์ฌ์ฉ - ์ ํ์ง ๊ฐ์์ ๋ฐ๋ผ ์ฃผ์ฌ์ ๋ฉด ๊ฐ์๋ฅผ ๋์ ์ผ๋ก ์กฐ์ (2๊ฐ๋ฉด ๋์ , 3~6๊ฐ๋ฉด ํด๋น ๋ฉด ์ฃผ์ฌ์)
- XSS ๋ฐฉ์ง๋ฅผ ์ํด ์ฌ์ฉ์ ์
๋ ฅ์
htmlspecialchars()๋ก ์ด์ค์ผ์ดํ (์ถ๋ ฅ ์)
์ ์ฝ ์กฐ๊ฑด
- โข ๊ทธ๋๋ณด๋5 SE ์ค์น๋ณธ ์์ ์์ (https://github.com/gnuboard/g5se main ๋ธ๋์น)
- โข ํด๋ ๊ธฐ๋ฐ ์๋ ๋ผ์ฐํ ์ฌ์ฉ (`app/modules/lucky-dice/index.php` โ `/lucky-dice`)
- โข modern shell ํ์ ์ ์ฉ (`_head.inc.php` + `m-shell` + `_nav.inc.php` + `_footer.inc.php`)
- โข ๋์์ธ ํ ํฐ ์ฌ์ฉ (`--m-surface`, `--m-text`, `--m-primary` ๋ฑ) โ ๋คํฌ๋ชจ๋ ์๋ ๋์
- โข DB ์ฌ์ฉ ๊ธ์ง (๋ชจ๋ ์ํ๋ ํด๋ผ์ด์ธํธ ์ธก์์ ๊ด๋ฆฌ)
- โข ์ธ๋ถ ๋น๋ ๋๊ตฌ(npm/vite) ์ฌ์ฉ ๊ธ์ง, CDN ์์กด ์ต์ํ
- โข ์ฌ์ฉ์ ์ ๋ ฅ์ `htmlspecialchars()`๋ก ์ด์ค์ผ์ดํ
- โข Laravel/Composer/React/Vue ์ ๋ ์ฌ์ฉ ๊ธ์ง โ ์์ PHP + g5se๋ง ์ฌ์ฉ
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
g5se๊ฐ ์ค์น๋ ๋๋ ํฐ๋ฆฌ(๋ฃจํธ์ `index.php`ยท`app/`์ด ์๋ ๊ณณ)์์ ์์
์ ์์ํฉ๋๋ค. (g5se ์ค์น๋ https://github.com/gnuboard/g5se ์ฐธ๊ณ )
## Step 1: ํ๋ก์ ํธ ๊ธฐ๋ณธ ๊ณจ๊ฒฉ ์์ฑ
`app/modules/lucky-dice/index.php` ํ์ผ์ ์์ฑํ๊ณ ๋ค์ ๋ด์ฉ์ ์์ฑํ์ธ์.
**์๊ตฌ์ฌํญ:**
1. g5se modern shell ๊ณจ๊ฒฉ ์ ์ฉ (์๋ ๊ตฌ์กฐ ํ์)
2. ํ์ด์ง ์ ๋ชฉ: "๐ฒ ํ์ด์ ์ฃผ์ฌ์ - ๊ฒฐ์ ์ฅ์ ํด๊ฒฐ์ฌ"
3. ๊ฐ๋จํ ์ค๋ช
๋ฌธ๊ตฌ์ "์ฃผ์ฌ์ ๊ตด๋ฆฌ๊ธฐ ์์" ๋ฒํผ ๋ฐฐ์น
4. ๋คํฌ๋ชจ๋ ์๋ ๋์์ ์ํ CSS ๋ณ์ ์ฌ์ฉ (`--m-surface`, `--m-text` ๋ฑ)
**modern shell ๊ณจ๊ฒฉ (๋ฐ๋์ ์ด ๊ตฌ์กฐ๋ฅผ ๋ฐ๋ฅผ ๊ฒ):**
```php
<?php
include_once('./_common.php');
// ํ์ด์ง ๋ก์ง์ ์ฌ๊ธฐ์
require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<div class="m-shell">
<?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
<main class="m-container" style="padding: 32px 20px 48px;">
<!-- ํ์ด์ง ๋ณธ๋ฌธ -->
<h1 style="font-size: 2rem; font-weight: 700; margin-bottom: 12px;">๐ฒ ํ์ด์ ์ฃผ์ฌ์</h1>
<p style="color: var(--m-text-muted); margin-bottom: 32px;">๊ฒฐ์ ์ฅ์ ๊ฐ ์์ผ์ ๊ฐ์? ์ฃผ์ฌ์๊ฐ ๋์ ์ ํํด๋๋ฆฝ๋๋ค!</p>
<div style="background: var(--m-surface); padding: 24px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow);">
<p style="margin-bottom: 16px;">์๋์ 2~6๊ฐ์ ์ ํ์ง๋ฅผ ์
๋ ฅํ๊ณ ์ฃผ์ฌ์๋ฅผ ๊ตด๋ ค๋ณด์ธ์.</p>
<button id="startBtn" style="background: var(--m-primary); color: white; padding: 12px 24px; border: none; border-radius: 8px; font-size: 1rem; cursor: pointer;">
์์ํ๊ธฐ
</button>
</div>
</main>
<?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<?php include_once('./tail.sub.php'); ?>
๊ธฐ๋ ๊ฒฐ๊ณผ: `app/modules/lucky-dice/index.php` ํ์ผ์ด ์์ฑ๋๊ณ , `/lucky-dice` URL๋ก ์ ์ํ๋ฉด modern shell์ด ์ ์ฉ๋ ํ์ด์ง๊ฐ ํ์๋ฉ๋๋ค. ๋ค๋น๊ฒ์ด์ ๋ฐ, ํธํฐ, ๋คํฌ๋ชจ๋ ํ ๊ธ์ด ์๋์ผ๋ก ํฌํจ๋๋ฉฐ, ๊ฐ๋จํ ์ค๋ช ๋ฌธ๊ตฌ์ "์์ํ๊ธฐ" ๋ฒํผ์ด ๋ณด์ ๋๋ค.
## Step 2: ์ ํ์ง ์
๋ ฅ ํผ ๊ตฌํ
`app/modules/lucky-dice/index.php`๋ฅผ ์์ ํ์ฌ ๋ค์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ์ธ์.
**์๊ตฌ์ฌํญ:**
1. ์ ํ์ง ์
๋ ฅ ํ๋ 2๊ฐ๋ฅผ ๊ธฐ๋ณธ์ผ๋ก ํ์ (์ต๋ 6๊ฐ๊น์ง ์ถ๊ฐ ๊ฐ๋ฅ)
2. "+ ์ ํ์ง ์ถ๊ฐ" ๋ฒํผ (์ต๋ 6๊ฐ ์ ํ)
3. ๊ฐ ์
๋ ฅ ํ๋ ์์ "์ญ์ " ๋ฒํผ (์ต์ 2๊ฐ๋ ์ ์ง)
4. "์ฃผ์ฌ์ ๊ตด๋ฆฌ๊ธฐ" ๋ฒํผ (์ ํ์ง 2๊ฐ ๋ฏธ๋ง์ด๋ฉด ๋นํ์ฑํ)
5. ๋น ์ ํ์ง๊ฐ ์์ผ๋ฉด ๊ฒฝ๊ณ ๋ฉ์์ง ํ์
**JavaScript ๋ก์ง:**
- `addChoice()`: ์ ํ์ง ์
๋ ฅ ํ๋ ์ถ๊ฐ (์ต๋ 6๊ฐ)
- `removeChoice(index)`: ์ ํ์ง ์ญ์ (์ต์ 2๊ฐ ์ ์ง)
- `rollDice()`: ์ฃผ์ฌ์ ๊ตด๋ฆฌ๊ธฐ (๋น ์ ํ์ง ๊ฒ์ฆ)
**HTML ๊ตฌ์กฐ ์์:**
```html
<div id="choiceInputs">
<div class="choice-item" style="display: flex; gap: 8px; margin-bottom: 12px;">
<input type="text" class="choice-input" placeholder="์ ํ์ง 1" style="flex: 1; padding: 10px; border: 1px solid var(--m-text-muted); border-radius: 6px; background: var(--m-surface); color: var(--m-text);">
<button onclick="removeChoice(0)" style="padding: 10px 16px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer;">์ญ์ </button>
</div>
</div>
<button onclick="addChoice()" style="margin-top: 12px; padding: 10px 20px; background: var(--m-primary); color: white; border: none; border-radius: 6px; cursor: pointer;">+ ์ ํ์ง ์ถ๊ฐ</button>
<button id="rollBtn" onclick="rollDice()" style="margin-top: 20px; width: 100%; padding: 16px; background: #4CAF50; color: white; border: none; border-radius: 8px; font-size: 1.1rem; font-weight: 700; cursor: pointer;">๐ฒ ์ฃผ์ฌ์ ๊ตด๋ฆฌ๊ธฐ</button>
```
**JavaScript ํจ์ ๊ตฌํ:**
```javascript
let choiceCount = 2;
function addChoice() {
if (choiceCount >= 6) {
alert('์ต๋ 6๊ฐ๊น์ง๋ง ์ถ๊ฐํ ์ ์์ต๋๋ค.');
return;
}
choiceCount++;
const container = document.getElementById('choiceInputs');
const div = document.createElement('div');
div.className = 'choice-item';
div.style.cssText = 'display: flex; gap: 8px; margin-bottom: 12px;';
div.innerHTML = `
<input type="text" class="choice-input" placeholder="์ ํ์ง ${choiceCount}" style="flex: 1; padding: 10px; border: 1px solid var(--m-text-muted); border-radius: 6px; background: var(--m-surface); color: var(--m-text);">
<button onclick="removeChoice(${choiceCount - 1})" style="padding: 10px 16px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer;">์ญ์ </button>
`;
container.appendChild(div);
}
function removeChoice(index) {
if (choiceCount <= 2) {
alert('์ต์ 2๊ฐ์ ์ ํ์ง๋ ํ์ํฉ๋๋ค.');
return;
}
const items = document.querySelectorAll('.choice-item');
items[index].remove();
choiceCount--;
}
function rollDice() {
const inputs = document.querySelectorAll('.choice-input');
const choices = Array.from(inputs).map(input => input.value.trim()).filter(v => v !== '');
if (choices.length < 2) {
alert('์ต์ 2๊ฐ์ ์ ํ์ง๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.');
return;
}
// ๋ค์ ๋จ๊ณ์์ ์ฃผ์ฌ์ ์ ๋๋ฉ์ด์
๊ตฌํ ์์
alert('์ ํ์ง: ' + choices.join(', '));
}
```
๊ธฐ๋ ๊ฒฐ๊ณผ: ํ์ด์ง์ ์ ํ์ง ์ ๋ ฅ ํผ์ด ํ์๋ฉ๋๋ค. ์ฌ์ฉ์๋ ์ต๋ 6๊ฐ๊น์ง ์ ํ์ง๋ฅผ ์ถ๊ฐํ ์ ์๊ณ , ๊ฐ ์ ํ์ง ์์ ์ญ์ ๋ฒํผ์ด ์์ต๋๋ค. "์ฃผ์ฌ์ ๊ตด๋ฆฌ๊ธฐ" ๋ฒํผ์ ๋๋ฅด๋ฉด ์ ๋ ฅ๋ ์ ํ์ง ๋ชฉ๋ก์ด alert๋ก ํ์๋ฉ๋๋ค (์ฃผ์ฌ์ ์ ๋๋ฉ์ด์ ์ ๋ค์ ๋จ๊ณ์์ ๊ตฌํ).
## Step 3: ์ฃผ์ฌ์ ์ ๋๋ฉ์ด์
& ๊ฒฐ๊ณผ ํ์
`app/modules/lucky-dice/index.php`๋ฅผ ์์ ํ์ฌ ์ฃผ์ฌ์ ๊ตด๋ฆผ ์ ๋๋ฉ์ด์
๊ณผ ๊ฒฐ๊ณผ ํ์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ์ธ์.
**์๊ตฌ์ฌํญ:**
1. ์ฃผ์ฌ์ ๊ตด๋ฆผ ๋ฒํผ ํด๋ฆญ ์ 3D ํ์ ์ ๋๋ฉ์ด์
(2์ด ๋์)
2. ์ ๋๋ฉ์ด์
์ข
๋ฃ ํ ๋๋ค์ผ๋ก ์ ํ๋ ๊ฒฐ๊ณผ๋ฅผ ํฐ ์นด๋๋ก ํ์
3. ๊ฒฐ๊ณผ ์นด๋์ "๐ ๋น์ฒจ!" ๋ฌธ๊ตฌ์ ์ ํ๋ ํญ๋ชฉ ํ์
4. "๋ค์ ๊ตด๋ฆฌ๊ธฐ" ๋ฒํผ์ผ๋ก ์ฌ์๋ ๊ฐ๋ฅ
5. ์ต๊ทผ 5๊ฐ ๊ฒฐ๊ณผ ํ์คํ ๋ฆฌ ๋ฆฌ์คํธ ํ์
**CSS ์ ๋๋ฉ์ด์
:**
```css
@keyframes diceRoll {
0% { transform: rotate3d(1, 1, 1, 0deg); }
25% { transform: rotate3d(1, 1, 1, 180deg) scale(1.2); }
50% { transform: rotate3d(1, 1, 1, 360deg) scale(1); }
75% { transform: rotate3d(1, 1, 1, 540deg) scale(1.2); }
100% { transform: rotate3d(1, 1, 1, 720deg) scale(1); }
}
.dice-rolling {
animation: diceRoll 2s cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
```
**JavaScript ๋ก์ง ์์ :**
```javascript
let history = [];
function rollDice() {
const inputs = document.querySelectorAll('.choice-input');
const choices = Array.from(inputs).map(input => input.value.trim()).filter(v => v !== '');
if (choices.length < 2) {
alert('์ต์ 2๊ฐ์ ์ ํ์ง๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.');
return;
}
// ์ฃผ์ฌ์ ์ ๋๋ฉ์ด์
์์
const diceElement = document.getElementById('diceAnimation');
diceElement.classList.add('dice-rolling');
document.getElementById('rollBtn').disabled = true;
// 2์ด ํ ๊ฒฐ๊ณผ ํ์
setTimeout(() => {
diceElement.classList.remove('dice-rolling');
document.getElementById('rollBtn').disabled = false;
const winner = choices[Math.floor(Math.random() * choices.length)];
showResult(winner);
addToHistory(winner);
}, 2000);
}
function showResult(winner) {
const resultDiv = document.getElementById('result');
resultDiv.innerHTML = `
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 32px; border-radius: var(--m-radius-lg); text-align: center; margin-top: 24px; color: white;">
<div style="font-size: 3rem; margin-bottom: 8px;">๐</div>
<h2 style="font-size: 1.5rem; font-weight: 700; margin-bottom: 8px;">๋น์ฒจ!</h2>
<p style="font-size: 2rem; font-weight: 700;">${winner}</p>
</div>
`;
resultDiv.style.display = 'block';
}
function addToHistory(item) {
history.unshift(item);
if (history.length > 5) history.pop();
const historyDiv = document.getElementById('history');
historyDiv.innerHTML = '<h3 style="font-size: 1.2rem; font-weight: 700; margin-bottom: 12px;">์ต๊ทผ ๊ฒฐ๊ณผ</h3>' +
history.map((h, i) => `<div style="padding: 8px 12px; background: var(--m-surface); border-radius: 6px; margin-bottom: 8px;">${i + 1}. ${h}</div>`).join('');
historyDiv.style.display = 'block';
}
```
**HTML ์ถ๊ฐ:**
```html
<div id="diceAnimation" style="width: 100px; height: 100px; margin: 24px auto; background: var(--m-primary); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 3rem; color: white;">
๐ฒ
</div>
<div id="result" style="display: none;"></div>
<div id="history" style="display: none; margin-top: 24px;"></div>
```
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ฃผ์ฌ์ ๊ตด๋ฆฌ๊ธฐ ๋ฒํผ์ ๋๋ฅด๋ฉด ์ฃผ์ฌ์ ์ด๋ชจ์ง(๐ฒ)๊ฐ 2์ด ๋์ 3D ํ์ ์ ๋๋ฉ์ด์ ์ ์ํํฉ๋๋ค. ์ ๋๋ฉ์ด์ ์ด ๋๋๋ฉด ๋๋ค์ผ๋ก ์ ํ๋ ํญ๋ชฉ์ด ๋ณด๋ผ์ ๊ทธ๋ผ๋ฐ์ด์ ์นด๋์ ํฌ๊ฒ ํ์๋๊ณ , ํ๋จ์ ์ต๊ทผ 5๊ฐ ๊ฒฐ๊ณผ ํ์คํ ๋ฆฌ๊ฐ ๋ฆฌ์คํธ๋ก ๋ํ๋ฉ๋๋ค. ๋คํฌ๋ชจ๋์์๋ ์์์ด ์์ฐ์ค๋ฝ๊ฒ ์ ์ฉ๋ฉ๋๋ค.
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!