๐ŸŽฐ ๋žœ๋ค ์ ์‹ฌ ๋ฉ”๋‰ด ๋ฃฐ๋ › - ์˜ค๋Š˜ ๋ญ ๋จน์ง€? ๊ณ ๋ฏผ ๋! - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
์ž…๋ฌธ PHP g5se AI: Claude

๐ŸŽฐ ๋žœ๋ค ์ ์‹ฌ ๋ฉ”๋‰ด ๋ฃฐ๋ › - ์˜ค๋Š˜ ๋ญ ๋จน์ง€? ๊ณ ๋ฏผ ๋!

๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช… lunch-menu-roulette
์กฐํšŒ์ˆ˜ 239 0๊ฑด ์ œ์ถœ

๋ฌธ์ œ ์„ค๋ช…

๐Ÿฝ๏ธ ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

์ ์‹ฌ ๋ฉ”๋‰ด ์„ ํƒ ๊ณ ๋ฏผ์„ ํ•ด๊ฒฐํ•ด์ฃผ๋Š” ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ๋ฃฐ๋ › ํŽ˜์ด์ง€์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ๋ฉ”๋‰ด ํ›„๋ณด๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ๋ฃฐ๋ ›์„ ๋Œ๋ ค ๋žœ๋ค์œผ๋กœ ์˜ค๋Š˜์˜ ์ ์‹ฌ ๋ฉ”๋‰ด๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ์‚ฌ์šด๋“œ ํšจ๊ณผ๋กœ ์žฌ๋ฏธ๋ฅผ ๋”ํ–ˆ์Šต๋‹ˆ๋‹ค.

โœจ ํ•„์ˆ˜ ๊ธฐ๋Šฅ

  1. ๋ฉ”๋‰ด ์ž…๋ ฅ ํผ - ์‚ฌ์šฉ์ž๊ฐ€ ์ ์‹ฌ ๋ฉ”๋‰ด ํ›„๋ณด๋ฅผ ์ž์œ ๋กญ๊ฒŒ ์ถ”๊ฐ€ (์˜ˆ: ๊น€์น˜์ฐŒ๊ฐœ, ํŒŒ์Šคํƒ€, ํ–„๋ฒ„๊ฑฐ)
  2. ๋ฉ”๋‰ด ๋ชฉ๋ก ๊ด€๋ฆฌ - ์ถ”๊ฐ€๋œ ๋ฉ”๋‰ด๋ฅผ ๋ฆฌ์ŠคํŠธ๋กœ ํ‘œ์‹œ, ๊ฐœ๋ณ„ ์‚ญ์ œ ๊ฐ€๋Šฅ
  3. ๋ฃฐ๋ › ์• ๋‹ˆ๋ฉ”์ด์…˜ - ์›ํ˜• ๋ฃฐ๋ ›์ด ํšŒ์ „ํ•˜๋ฉฐ ๋žœ๋ค ๋ฉ”๋‰ด ์„ ํƒ
  4. ๊ฒฐ๊ณผ ํ‘œ์‹œ - ์„ ํƒ๋œ ๋ฉ”๋‰ด๋ฅผ ํฌ๊ฒŒ ๊ฐ•์กฐํ•˜์—ฌ ํ‘œ์‹œ (์ถ•ํ•˜ ์ดํŽ™ํŠธ ํฌํ•จ)
  5. ๋‹คํฌ๋ชจ๋“œ ์ž๋™ ๋Œ€์‘ - modern shell์˜ ๋””์ž์ธ ํ† ํฐ ํ™œ์šฉ
  6. ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ์ €์žฅ - ์ž…๋ ฅํ•œ ๋ฉ”๋‰ด ๋ชฉ๋ก์„ ๋ธŒ๋ผ์šฐ์ €์— ์ €์žฅํ•˜์—ฌ ์ƒˆ๋กœ๊ณ ์นจ ํ›„์—๋„ ์œ ์ง€

๐ŸŽ ์„ ํƒ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ (๋ณด๋„ˆ์Šค)

  • ๋ฃฐ๋ › ํšŒ์ „ ์‹œ ํšจ๊ณผ์Œ ์žฌ์ƒ (Web Audio API)
  • ๋ฉ”๋‰ด๋ณ„ ๊ฐ€์ค‘์น˜ ์„ค์ • (์ž์ฃผ ๋จน๊ณ  ์‹ถ์€ ๋ฉ”๋‰ด๋Š” ๋‹น์ฒจ ํ™•๋ฅ  ๋†’์ด๊ธฐ)
  • ์ตœ๊ทผ ๋‹น์ฒจ ๋ฉ”๋‰ด ํžˆ์Šคํ† ๋ฆฌ ํ‘œ์‹œ (์˜ค๋Š˜/์–ด์ œ/๊ทธ์ €๊ป˜ ๋ญ ๋จน์—ˆ๋Š”์ง€)
  • ๋ฉ”๋‰ด ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ (ํ•œ์‹/์ค‘์‹/์ผ์‹/์–‘์‹)
  • ๋ฃฐ๋ › ํ…Œ๋งˆ ๋ณ€๊ฒฝ (์ƒ‰์ƒ ์Šคํ‚ด ์„ ํƒ)

๐ŸŽฏ ํ•™์Šต ๋ชฉํ‘œ

  • g5se์˜ ํด๋” ๊ธฐ๋ฐ˜ ์ž๋™ ๋ผ์šฐํŒ… ์ฒดํ—˜
  • modern shell์„ ํ™œ์šฉํ•œ ์ผ๊ด€๋œ ์‚ฌ์ดํŠธ ๋””์ž์ธ ์ ์šฉ
  • ์ˆœ์ˆ˜ JavaScript๋กœ Canvas ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ตฌํ˜„
  • localStorage API๋ฅผ ํ™œ์šฉํ•œ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ฐ์ดํ„ฐ ์ €์žฅ
  • CSS ๋ณ€์ˆ˜(๋””์ž์ธ ํ† ํฐ)๋ฅผ ํ™œ์šฉํ•œ ๋‹คํฌ๋ชจ๋“œ ๋Œ€์‘

๐Ÿ’ก ๊ตฌํ˜„ ํŒ

  • ๋ฃฐ๋ › ๊ทธ๋ฆฌ๊ธฐ๋Š” HTML5 Canvas API ์‚ฌ์šฉ
  • ๋ฉ”๋‰ด ๋ชฉ๋ก์€ JSON ํ˜•ํƒœ๋กœ localStorage์— ์ €์žฅ
  • ํšŒ์ „ ์• ๋‹ˆ๋ฉ”์ด์…˜์€ requestAnimationFrame์œผ๋กœ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์ฒ˜๋ฆฌ
  • ๋‹น์ฒจ ํšจ๊ณผ๋Š” CSS animation + JavaScript ์กฐํ•ฉ

์ œ์•ฝ ์กฐ๊ฑด

  • โ€ข g5se ๊ธฐ๋ฐ˜ ์ˆœ์ˆ˜ PHP ํ”„๋กœ์ ํŠธ (Laravel/Composer/npm ์‚ฌ์šฉ ๊ธˆ์ง€)
  • โ€ข app/modules/lunch-menu-roulette/index.php ๋‹จ์ผ ํŽ˜์ด์ง€๋กœ ๊ตฌํ˜„
  • โ€ข modern shell (_head.inc.php + m-shell + _nav.inc.php + m-container + _footer.inc.php) ํ•„์ˆ˜ ์ ์šฉ
  • โ€ข ๋””์ž์ธ ํ† ํฐ (--m-surface, --m-text, --m-shadow ๋“ฑ) ์‚ฌ์šฉํ•˜์—ฌ ๋‹คํฌ๋ชจ๋“œ ์ž๋™ ๋Œ€์‘
  • โ€ข ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ ๊ธˆ์ง€ (localStorage๋กœ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ์ €์žฅ)
  • โ€ข ์™ธ๋ถ€ CDN ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ ๊ธˆ์ง€ (์ˆœ์ˆ˜ HTML/CSS/JS)
  • โ€ข Canvas API๋กœ ๋ฃฐ๋ › UI ์ง์ ‘ ๊ตฌํ˜„
  • โ€ข ์‚ฌ์šฉ์ž ์ž…๋ ฅ์€ htmlspecialchars()๋กœ ์ด์Šค์ผ€์ดํ”„

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

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

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

## Step 1: ๊ธฐ๋ณธ ํŽ˜์ด์ง€ ๊ตฌ์กฐ ์ƒ์„ฑ

`app/modules/lunch-menu-roulette/index.php` ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  modern shell ๊ณจ๊ฒฉ์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.

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

require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<style>
.roulette-container {
    max-width: 800px;
    margin: 0 auto;
    text-align: center;
}

.page-header {
    margin-bottom: 32px;
}

.page-header h1 {
    font-size: 2rem;
    font-weight: 700;
    color: var(--m-text);
    margin-bottom: 8px;
}

.page-header p {
    color: var(--m-text-muted);
    font-size: 1rem;
}

.menu-input-section {
    background: var(--m-surface);
    padding: 24px;
    border-radius: var(--m-radius-lg);
    box-shadow: var(--m-shadow);
    margin-bottom: 32px;
}

.menu-form {
    display: flex;
    gap: 12px;
    margin-bottom: 16px;
}

.menu-form input {
    flex: 1;
    padding: 12px 16px;
    border: 1px solid var(--m-border, #e5e7eb);
    border-radius: 8px;
    font-size: 1rem;
    background: var(--m-surface);
    color: var(--m-text);
}

.menu-form input:focus {
    outline: none;
    border-color: #3b82f6;
}

.menu-form button {
    padding: 12px 24px;
    background: #3b82f6;
    color: white;
    border: none;
    border-radius: 8px;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.2s;
}

.menu-form button:hover {
    background: #2563eb;
}

.menu-list {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    min-height: 40px;
}

.menu-item {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 8px 16px;
    background: #f3f4f6;
    border-radius: 20px;
    font-size: 0.9rem;
    color: #374151;
}

[data-theme="dark"] .menu-item {
    background: #374151;
    color: #e5e7eb;
}

.menu-item button {
    background: none;
    border: none;
    color: #ef4444;
    cursor: pointer;
    font-size: 1.1rem;
    padding: 0;
    line-height: 1;
}

.canvas-section {
    background: var(--m-surface);
    padding: 32px;
    border-radius: var(--m-radius-lg);
    box-shadow: var(--m-shadow);
    margin-bottom: 24px;
}

#rouletteCanvas {
    max-width: 100%;
    border-radius: 50%;
    box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}

.spin-button {
    margin-top: 24px;
    padding: 16px 48px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border: none;
    border-radius: 12px;
    font-size: 1.2rem;
    font-weight: 700;
    cursor: pointer;
    transition: transform 0.2s, box-shadow 0.2s;
}

.spin-button:hover:not(:disabled) {
    transform: translateY(-2px);
    box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
}

.spin-button:disabled {
    opacity: 0.6;
    cursor: not-allowed;
}

.result-section {
    background: var(--m-surface);
    padding: 24px;
    border-radius: var(--m-radius-lg);
    box-shadow: var(--m-shadow);
    min-height: 100px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.result-content {
    text-align: center;
}

.result-emoji {
    font-size: 3rem;
    margin-bottom: 12px;
}

.result-text {
    font-size: 1.5rem;
    font-weight: 700;
    color: var(--m-text);
}

.result-menu {
    font-size: 2rem;
    font-weight: 900;
    color: #667eea;
    margin-top: 8px;
}

.empty-state {
    color: var(--m-text-muted);
    font-size: 0.95rem;
}
</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="roulette-container">
            <div class="page-header">
                <h1>๐ŸŽฐ ๋žœ๋ค ์ ์‹ฌ ๋ฉ”๋‰ด ๋ฃฐ๋ ›</h1>
                <p>์˜ค๋Š˜ ๋ญ ๋จน์ง€? ๊ณ ๋ฏผ์€ ๊ทธ๋งŒ, ๋ฃฐ๋ ›์ด ๊ฒฐ์ •ํ•ด๋“œ๋ ค์š”!</p>
            </div>

            <div class="menu-input-section">
                <form class="menu-form" id="menuForm">
                    <input 
                        type="text" 
                        id="menuInput" 
                        placeholder="๋ฉ”๋‰ด ์ด๋ฆ„ ์ž…๋ ฅ (์˜ˆ: ๊น€์น˜์ฐŒ๊ฐœ, ํŒŒ์Šคํƒ€)" 
                        maxlength="20"
                        required
                    >
                    <button type="submit">์ถ”๊ฐ€</button>
                </form>
                <div class="menu-list" id="menuList">
                    <span class="empty-state">์•„์ง ์ถ”๊ฐ€๋œ ๋ฉ”๋‰ด๊ฐ€ ์—†์–ด์š”. ์œ„์—์„œ ๋ฉ”๋‰ด๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณด์„ธ์š”!</span>
                </div>
            </div>

            <div class="canvas-section">
                <canvas id="rouletteCanvas" width="400" height="400"></canvas>
                <div>
                    <button class="spin-button" id="spinButton" disabled>๋ฃฐ๋ › ๋Œ๋ฆฌ๊ธฐ ๐ŸŽฒ</button>
                </div>
            </div>

            <div class="result-section" id="resultSection" style="display: none;">
                <div class="result-content">
                    <div class="result-emoji">๐ŸŽ‰</div>
                    <div class="result-text">์˜ค๋Š˜์˜ ์ ์‹ฌ ๋ฉ”๋‰ด๋Š”</div>
                    <div class="result-menu" id="resultMenu"></div>
                </div>
            </div>
        </div>
    </main>
    <?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>

<?php include_once('./tail.sub.php'); ?>
```

์ด ๋‹จ๊ณ„์—์„œ๋Š” ํŽ˜์ด์ง€์˜ HTML ๊ตฌ์กฐ์™€ CSS ์Šคํƒ€์ผ๋งŒ ์™„์„ฑํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ ๋‹จ๊ณ„์—์„œ JavaScript ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋ธŒ๋ผ์šฐ์ €์—์„œ `/lunch-menu-roulette` ์ ‘์† ์‹œ modern shell์ด ์ ์šฉ๋œ ํŽ˜์ด์ง€๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๋ฉ”๋‰ด ์ž…๋ ฅ ํผ, ๋นˆ ์บ”๋ฒ„์Šค ์˜์—ญ, ๋น„ํ™œ์„ฑํ™”๋œ ๋ฃฐ๋ › ๋ฒ„ํŠผ์ด ๋ณด์ด๋ฉฐ, ๋‹คํฌ๋ชจ๋“œ ํ† ๊ธ€ ์‹œ ์ƒ‰์ƒ์ด ์ž๋™์œผ๋กœ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค. ์•„์ง ๊ธฐ๋Šฅ์€ ๋™์ž‘ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

2
Step 2
## Step 2: ๋ฉ”๋‰ด ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„ (JavaScript)

`app/modules/lunch-menu-roulette/index.php` ํŒŒ์ผ์˜ `<?php include_once('./tail.sub.php'); ?>` ๋ฐ”๋กœ ์œ„์— ๋‹ค์Œ JavaScript ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค:

```html
<script>
// ์ „์—ญ ๋ณ€์ˆ˜
let menuList = [];
const STORAGE_KEY = 'lunch_menu_roulette_items';

// ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ localStorage์—์„œ ๋ฉ”๋‰ด ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
window.addEventListener('DOMContentLoaded', () => {
    loadMenuFromStorage();
    renderMenuList();
    updateSpinButton();
});

// localStorage์—์„œ ๋ฉ”๋‰ด ๋กœ๋“œ
function loadMenuFromStorage() {
    const saved = localStorage.getItem(STORAGE_KEY);
    if (saved) {
        try {
            menuList = JSON.parse(saved);
        } catch (e) {
            console.error('๋ฉ”๋‰ด ๋กœ๋“œ ์‹คํŒจ:', e);
            menuList = [];
        }
    }
}

// localStorage์— ๋ฉ”๋‰ด ์ €์žฅ
function saveMenuToStorage() {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(menuList));
}

// ๋ฉ”๋‰ด ์ถ”๊ฐ€
document.getElementById('menuForm').addEventListener('submit', (e) => {
    e.preventDefault();
    
    const input = document.getElementById('menuInput');
    const menuName = input.value.trim();
    
    if (!menuName) return;
    
    // ์ค‘๋ณต ์ฒดํฌ
    if (menuList.includes(menuName)) {
        alert('์ด๋ฏธ ์ถ”๊ฐ€๋œ ๋ฉ”๋‰ด์ž…๋‹ˆ๋‹ค!');
        return;
    }
    
    menuList.push(menuName);
    saveMenuToStorage();
    renderMenuList();
    updateSpinButton();
    
    input.value = '';
    input.focus();
});

// ๋ฉ”๋‰ด ์‚ญ์ œ
function removeMenu(index) {
    menuList.splice(index, 1);
    saveMenuToStorage();
    renderMenuList();
    updateSpinButton();
}

// ๋ฉ”๋‰ด ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง
function renderMenuList() {
    const container = document.getElementById('menuList');
    
    if (menuList.length === 0) {
        container.innerHTML = '<span class="empty-state">์•„์ง ์ถ”๊ฐ€๋œ ๋ฉ”๋‰ด๊ฐ€ ์—†์–ด์š”. ์œ„์—์„œ ๋ฉ”๋‰ด๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณด์„ธ์š”!</span>';
        return;
    }
    
    container.innerHTML = menuList.map((menu, index) => `
        <div class="menu-item">
            <span>${escapeHtml(menu)}</span>
            <button onclick="removeMenu(${index})" title="์‚ญ์ œ">ร—</button>
        </div>
    `).join('');
}

// ๋ฃฐ๋ › ๋ฒ„ํŠผ ํ™œ์„ฑํ™”/๋น„ํ™œ์„ฑํ™”
function updateSpinButton() {
    const button = document.getElementById('spinButton');
    button.disabled = menuList.length < 2;
    
    if (menuList.length < 2) {
        button.textContent = '์ตœ์†Œ 2๊ฐœ ์ด์ƒ์˜ ๋ฉ”๋‰ด๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”';
    } else {
        button.textContent = '๋ฃฐ๋ › ๋Œ๋ฆฌ๊ธฐ ๐ŸŽฒ';
    }
}

// XSS ๋ฐฉ์ง€์šฉ HTML ์ด์Šค์ผ€์ดํ”„
function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}
</script>
```

์ด ๋‹จ๊ณ„์—์„œ๋Š” ๋ฉ”๋‰ด ์ถ”๊ฐ€/์‚ญ์ œ/์ €์žฅ/๋ถˆ๋Ÿฌ์˜ค๊ธฐ ๊ธฐ๋Šฅ์„ ์™„์„ฑํ•ฉ๋‹ˆ๋‹ค.

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

3
Step 3
## Step 3: ๋ฃฐ๋ › Canvas ๊ทธ๋ฆฌ๊ธฐ ๋ฐ ํšŒ์ „ ์• ๋‹ˆ๋ฉ”์ด์…˜

`app/modules/lunch-menu-roulette/index.php`์˜ `<script>` ํƒœ๊ทธ ๋‚ด๋ถ€ ๋งจ ์•„๋ž˜์— ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค:

```javascript
// Canvas ๊ด€๋ จ ๋ณ€์ˆ˜
const canvas = document.getElementById('rouletteCanvas');
const ctx = canvas.getContext('2d');
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = 180;

let currentRotation = 0;
let isSpinning = false;

// ๋ฃฐ๋ › ์ƒ‰์ƒ ํŒ”๋ ˆํŠธ
const colors = [
    '#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A',
    '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E2',
    '#F8B500', '#52B788', '#E07A5F', '#81B29A'
];

// ๋ฃฐ๋ › ๊ทธ๋ฆฌ๊ธฐ
function drawRoulette() {
    if (menuList.length === 0) {
        // ๋ฉ”๋‰ด๊ฐ€ ์—†์„ ๋•Œ ๊ธฐ๋ณธ ์›
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.beginPath();
        ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
        ctx.fillStyle = '#f3f4f6';
        ctx.fill();
        ctx.strokeStyle = '#d1d5db';
        ctx.lineWidth = 3;
        ctx.stroke();
        
        ctx.fillStyle = '#9ca3af';
        ctx.font = '16px sans-serif';
        ctx.textAlign = 'center';
        ctx.fillText('๋ฉ”๋‰ด๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”', centerX, centerY);
        return;
    }
    
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.save();
    ctx.translate(centerX, centerY);
    ctx.rotate(currentRotation);
    
    const sliceAngle = (2 * Math.PI) / menuList.length;
    
    // ๊ฐ ์กฐ๊ฐ ๊ทธ๋ฆฌ๊ธฐ
    menuList.forEach((menu, index) => {
        const startAngle = index * sliceAngle;
        const endAngle = startAngle + sliceAngle;
        
        // ์กฐ๊ฐ ์ƒ‰์น 
        ctx.beginPath();
        ctx.moveTo(0, 0);
        ctx.arc(0, 0, radius, startAngle, endAngle);
        ctx.closePath();
        ctx.fillStyle = colors[index % colors.length];
        ctx.fill();
        ctx.strokeStyle = '#fff';
        ctx.lineWidth = 3;
        ctx.stroke();
        
        // ํ…์ŠคํŠธ
        ctx.save();
        ctx.rotate(startAngle + sliceAngle / 2);
        ctx.textAlign = 'center';
        ctx.fillStyle = '#fff';
        ctx.font = 'bold 16px sans-serif';
        ctx.fillText(menu, radius * 0.6, 5);
        ctx.restore();
    });
    
    ctx.restore();
    
    // ์ค‘์•™ ์› (์žฅ์‹)
    ctx.beginPath();
    ctx.arc(centerX, centerY, 20, 0, 2 * Math.PI);
    ctx.fillStyle = '#fff';
    ctx.fill();
    ctx.strokeStyle = '#333';
    ctx.lineWidth = 2;
    ctx.stroke();
    
    // ์ƒ๋‹จ ํฌ์ธํ„ฐ (๊ณ ์ •)
    ctx.beginPath();
    ctx.moveTo(centerX, 30);
    ctx.lineTo(centerX - 15, 60);
    ctx.lineTo(centerX + 15, 60);
    ctx.closePath();
    ctx.fillStyle = '#ef4444';
    ctx.fill();
    ctx.strokeStyle = '#fff';
    ctx.lineWidth = 2;
    ctx.stroke();
}

// ๋ฃฐ๋ › ํšŒ์ „ ์• ๋‹ˆ๋ฉ”์ด์…˜
function spinRoulette() {
    if (isSpinning || menuList.length < 2) return;
    
    isSpinning = true;
    document.getElementById('spinButton').disabled = true;
    document.getElementById('resultSection').style.display = 'none';
    
    // ๋žœ๋ค ํšŒ์ „ ๊ฐ๋„ (์ตœ์†Œ 5๋ฐ”ํ€ด + ๋žœ๋ค)
    const minSpins = 5;
    const extraRotation = Math.random() * 2 * Math.PI;
    const targetRotation = currentRotation + (minSpins * 2 * Math.PI) + extraRotation;
    
    const duration = 3000; // 3์ดˆ
    const startTime = Date.now();
    const startRotation = currentRotation;
    
    function animate() {
        const elapsed = Date.now() - startTime;
        const progress = Math.min(elapsed / duration, 1);
        
        // easeOut ํšจ๊ณผ
        const easeProgress = 1 - Math.pow(1 - progress, 3);
        
        currentRotation = startRotation + (targetRotation - startRotation) * easeProgress;
        drawRoulette();
        
        if (progress < 1) {
            requestAnimationFrame(animate);
        } else {
            // ํšŒ์ „ ์™„๋ฃŒ
            currentRotation = targetRotation % (2 * Math.PI);
            isSpinning = false;
            document.getElementById('spinButton').disabled = false;
            showResult();
        }
    }
    
    animate();
}

// ๊ฒฐ๊ณผ ํ‘œ์‹œ
function showResult() {
    // ํฌ์ธํ„ฐ๊ฐ€ ๊ฐ€๋ฆฌํ‚ค๋Š” ๋ฉ”๋‰ด ๊ณ„์‚ฐ (์ƒ๋‹จ ๊ธฐ์ค€)
    const sliceAngle = (2 * Math.PI) / menuList.length;
    const normalizedRotation = (2 * Math.PI - (currentRotation % (2 * Math.PI))) % (2 * Math.PI);
    const selectedIndex = Math.floor(normalizedRotation / sliceAngle) % menuList.length;
    const selectedMenu = menuList[selectedIndex];
    
    document.getElementById('resultMenu').textContent = selectedMenu;
    document.getElementById('resultSection').style.display = 'flex';
    
    // ๊ฒฐ๊ณผ ์„น์…˜ ์Šคํฌ๋กค
    setTimeout(() => {
        document.getElementById('resultSection').scrollIntoView({ 
            behavior: 'smooth', 
            block: 'center' 
        });
    }, 100);
}

// ๋ฃฐ๋ › ๋ฒ„ํŠผ ์ด๋ฒคํŠธ
document.getElementById('spinButton').addEventListener('click', spinRoulette);

// ์ดˆ๊ธฐ ๋ฃฐ๋ › ๊ทธ๋ฆฌ๊ธฐ
drawRoulette();

// ๋ฉ”๋‰ด ๋ณ€๊ฒฝ ์‹œ ๋ฃฐ๋ › ๋‹ค์‹œ ๊ทธ๋ฆฌ๊ธฐ
const originalRenderMenuList = renderMenuList;
renderMenuList = function() {
    originalRenderMenuList();
    currentRotation = 0; // ๋ฉ”๋‰ด ๋ณ€๊ฒฝ ์‹œ ํšŒ์ „ ์ดˆ๊ธฐํ™”
    drawRoulette();
};
</script>
```

์ด ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ๋ฃฐ๋ ›์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์ด ์™„์„ฑ๋ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋ฉ”๋‰ด๋ฅผ 2๊ฐœ ์ด์ƒ ์ถ”๊ฐ€ํ•˜๊ณ  '๋ฃฐ๋ › ๋Œ๋ฆฌ๊ธฐ' ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ์บ”๋ฒ„์Šค์— ๊ทธ๋ ค์ง„ ๋ฃฐ๋ ›์ด 3์ดˆ๊ฐ„ ํšŒ์ „ํ•ฉ๋‹ˆ๋‹ค. ํšŒ์ „์ด ๋ฉˆ์ถ”๋ฉด ์ƒ๋‹จ ํฌ์ธํ„ฐ๊ฐ€ ๊ฐ€๋ฆฌํ‚ค๋Š” ๋ฉ”๋‰ด๊ฐ€ ๊ฒฐ๊ณผ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๋ฉ”๋‰ด๋ฅผ ์ถ”๊ฐ€/์‚ญ์ œํ•˜๋ฉด ๋ฃฐ๋ ›์ด ์ž๋™์œผ๋กœ ๋‹ค์‹œ ๊ทธ๋ ค์ง‘๋‹ˆ๋‹ค. ๋‹คํฌ๋ชจ๋“œ์—์„œ๋„ ์ •์ƒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

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

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

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

๋Œ“๊ธ€ (0)

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