๐ŸŽฏ g5se ์˜ค๋Š˜์˜ ์Šค๋„์ฟ  ํผ์ฆ ์ƒ์„ฑ๊ธฐ - ๋‚œ์ด๋„๋ณ„ ํผ์ฆ ํ’€์ด & ํƒ€์ด๋จธ ๊ธฐ๋ก - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
๊ณ ๊ธ‰ PHP g5se AI: Claude

๐ŸŽฏ g5se ์˜ค๋Š˜์˜ ์Šค๋„์ฟ  ํผ์ฆ ์ƒ์„ฑ๊ธฐ - ๋‚œ์ด๋„๋ณ„ ํผ์ฆ ํ’€์ด & ํƒ€์ด๋จธ ๊ธฐ๋ก

๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช… g5se-sudoku-puzzle-generator
์กฐํšŒ์ˆ˜ 116 0๊ฑด ์ œ์ถœ

๋ฌธ์ œ ์„ค๋ช…

๐Ÿงฉ ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

๋งค์ผ ์ƒˆ๋กœ์šด ์Šค๋„์ฟ  ํผ์ฆ์„ ํ’€ ์ˆ˜ ์žˆ๋Š” ์›น ๊ธฐ๋ฐ˜ ํผ์ฆ ํ”Œ๋žซํผ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ๋‚œ์ด๋„(์‰ฌ์›€/๋ณดํ†ต/์–ด๋ ค์›€/์ต์ŠคํŠธ๋ฆผ)๋ฅผ ์„ ํƒํ•˜๊ณ , ๋žœ๋ค ์ƒ์„ฑ๋œ 9ร—9 ์Šค๋„์ฟ  ๋ณด๋“œ์—์„œ ๋นˆ์นธ์„ ์ฑ„์›Œ ํผ์ฆ์„ ์™„์„ฑํ•ฉ๋‹ˆ๋‹ค. ํ’€์ด ์‹œ๊ฐ„์„ ์ธก์ •ํ•˜๊ณ , ์™„๋ฃŒํ•œ ํผ์ฆ์€ ํžˆ์Šคํ† ๋ฆฌ์— ๊ธฐ๋ก๋˜์–ด ์ž์‹ ์˜ ์‹ค๋ ฅ ํ–ฅ์ƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

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

  • ๋‚œ์ด๋„๋ณ„ ํผ์ฆ ์ƒ์„ฑ: ๋ฐฑํŠธ๋ž˜ํ‚น ์•Œ๊ณ ๋ฆฌ์ฆ˜์œผ๋กœ ์œ ํšจํ•œ ์Šค๋„์ฟ  ๋ณด๋“œ๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ๋‚œ์ด๋„์— ๋”ฐ๋ผ ํžŒํŠธ ๊ฐœ์ˆ˜ ์กฐ์ ˆ (์‰ฌ์›€: 40๊ฐœ, ๋ณดํ†ต: 30๊ฐœ, ์–ด๋ ค์›€: 25๊ฐœ, ์ต์ŠคํŠธ๋ฆผ: 20๊ฐœ)
  • ์‹ค์‹œ๊ฐ„ ์ž…๋ ฅ ๊ฒ€์ฆ: ์‚ฌ์šฉ์ž๊ฐ€ ์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•  ๋•Œ ํ–‰/์—ด/3ร—3 ๋ฐ•์Šค ๊ทœ์น™ ์œ„๋ฐ˜์„ ์ฆ‰์‹œ ๊ฐ์ง€ํ•˜์—ฌ ๋นจ๊ฐ„์ƒ‰์œผ๋กœ ํ‘œ์‹œ
  • ํ’€์ด ํƒ€์ด๋จธ: ํผ์ฆ ์‹œ์ž‘ ์‹œ ํƒ€์ด๋จธ๊ฐ€ ์ž๋™์œผ๋กœ ์‹œ์ž‘๋˜๋ฉฐ, ์™„๋ฃŒ ์‹œ ์†Œ์š” ์‹œ๊ฐ„ ๊ธฐ๋ก
  • ํžŒํŠธ ์‹œ์Šคํ…œ: ๋ง‰ํž ๋•Œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ํžŒํŠธ ๋ฒ„ํŠผ (๊ฐ ํผ์ฆ๋‹น 3ํšŒ ์ œํ•œ, ํžŒํŠธ ์‚ฌ์šฉ ์‹œ ๋žœ๋ค ๋นˆ์นธ์— ์ •๋‹ต ํ‘œ์‹œ)
  • ํผ์ฆ ํžˆ์Šคํ† ๋ฆฌ: ์™„๋ฃŒํ•œ ํผ์ฆ์˜ ๋‚œ์ด๋„, ์†Œ์š” ์‹œ๊ฐ„, ํžŒํŠธ ์‚ฌ์šฉ ํšŸ์ˆ˜๋ฅผ SQLite DB์— ์ €์žฅํ•˜์—ฌ ๋งˆ์ด ํŽ˜์ด์ง€์—์„œ ํ™•์ธ
  • ๋ฉ”๋ชจ ๋ชจ๋“œ: ์…€์„ ์šฐํด๋ฆญํ•˜๋ฉด ์ž‘์€ ์ˆซ์ž๋กœ ํ›„๋ณด ์ˆซ์ž๋ฅผ ๋ฉ”๋ชจํ•  ์ˆ˜ ์žˆ๋Š” ๋ชจ๋“œ (ํ† ๊ธ€ ๋ฒ„ํŠผ์œผ๋กœ ํ™œ์„ฑํ™”)

๋ณด๋„ˆ์Šค ๊ธฐ๋Šฅ

  • ์ผ์ผ ์ฑŒ๋ฆฐ์ง€ ๋ชจ๋“œ: ๋งค์ผ ์ž์ •์— ์‹œ๋“œ๊ฐ’์ด ๋ฐ”๋€Œ์–ด ๋ชจ๋“  ์‚ฌ์šฉ์ž๊ฐ€ ๋™์ผํ•œ ํผ์ฆ์„ ํ’€๋„๋ก (์ผ์ผ ์ˆœ์œ„ํ‘œ ๊ธฐ๋Šฅ)
  • ํผ์ฆ ๊ณต์œ : ํŠน์ • ํผ์ฆ์˜ ์‹œ๋“œ๊ฐ’์„ URL๋กœ ๊ณต์œ ํ•˜์—ฌ ์นœ๊ตฌ์™€ ๋™์ผํ•œ ํผ์ฆ ๋„์ „
  • ๋‹คํฌ๋ชจ๋“œ ๋Œ€์‘: ๋ณด๋“œ ์ƒ‰์ƒ์ด ๋‹คํฌ๋ชจ๋“œ์—์„œ๋„ ๋ˆˆ์ด ํŽธํ•˜๋„๋ก ๋””์ž์ธ ํ† ํฐ ํ™œ์šฉ
  • ์ž๋™ ์ €์žฅ: ํ’€์ด ์ค‘์ธ ๋ณด๋“œ๋ฅผ localStorage์— ์ž๋™ ์ €์žฅํ•˜์—ฌ ์ƒˆ๋กœ๊ณ ์นจ ํ›„์—๋„ ์ด์–ด์„œ ํ’€ ์ˆ˜ ์žˆ๋„๋ก

๐ŸŽจ UI/UX ์š”๊ตฌ์‚ฌํ•ญ

  • 9ร—9 ๊ทธ๋ฆฌ๋“œ๋Š” 3ร—3 ๋‘๊บผ์šด ๊ฒฝ๊ณ„์„ ์œผ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ์Šค๋„์ฟ  ๋ณด๋“œ ํ˜•ํƒœ ๋ช…ํ™•ํžˆ ํ‘œํ˜„
  • ์ดˆ๊ธฐ ํžŒํŠธ ์ˆซ์ž๋Š” ํšŒ์ƒ‰ ๋ฐฐ๊ฒฝ, ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์ˆซ์ž๋Š” ํฐ์ƒ‰ ๋ฐฐ๊ฒฝ์œผ๋กœ ๊ตฌ๋ถ„
  • ๊ทœ์น™ ์œ„๋ฐ˜ ์…€์€ ๋นจ๊ฐ„์ƒ‰ ํ…Œ๋‘๋ฆฌ + ํ”๋“ค๋ฆผ ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ๊ฒฝ๊ณ 
  • ํผ์ฆ ์™„์„ฑ ์‹œ ์ถ•ํ•˜ ๋ชจ๋‹ฌ ํŒ์—… (์†Œ์š” ์‹œ๊ฐ„, ํžŒํŠธ ์‚ฌ์šฉ ํšŸ์ˆ˜ ํ‘œ์‹œ)
  • ํ„ฐ์น˜ ๊ธฐ๊ธฐ์—์„œ๋„ ํŽธํ•˜๊ฒŒ ํ”Œ๋ ˆ์ดํ•  ์ˆ˜ ์žˆ๋„๋ก ์…€ ํฌ๊ธฐ ์ถฉ๋ถ„ํžˆ ํ™•๋ณด (์ตœ์†Œ 40px)

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

  • ํ”„๋ ˆ์ž„์›Œํฌ: ๊ทธ๋ˆ„๋ณด๋“œ5 SE (g5se, PHP ๊ธฐ๋ฐ˜ CMS)
  • DB: SQLite (ํผ์ฆ ํžˆ์Šคํ† ๋ฆฌ ์ €์žฅ์šฉ, g5se ๋ฉ”์ธ DB์™€ ๊ฒฉ๋ฆฌ)
  • ํ”„๋ก ํŠธ์—”๋“œ: Vanilla JS + CSS Grid (React/Vue ์‚ฌ์šฉ ๊ธˆ์ง€)
  • ์Šคํƒ€์ผ๋ง: g5se modern shell ๋””์ž์ธ ํ† ํฐ + ์ปค์Šคํ…€ CSS

๐Ÿ“Š ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ

CREATE TABLE puzzle_history (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id TEXT,           -- ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž์˜ mb_id (๋น„ํšŒ์›์€ 'guest')
  difficulty TEXT,        -- 'easy', 'medium', 'hard', 'extreme'
  seed TEXT,              -- ํผ์ฆ ์‹œ๋“œ๊ฐ’ (์žฌํ˜„ ๊ฐ€๋Šฅ)
  solve_time INTEGER,     -- ์†Œ์š” ์‹œ๊ฐ„(์ดˆ)
  hints_used INTEGER,     -- ํžŒํŠธ ์‚ฌ์šฉ ํšŸ์ˆ˜
  completed_at TEXT       -- ์™„๋ฃŒ ์‹œ๊ฐ (ISO 8601)
);

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

  • g5se ํด๋” ๊ธฐ๋ฐ˜ ์ž๋™ ๋ผ์šฐํŒ… ์‹œ์Šคํ…œ ํ™œ์šฉ
  • ๋ณต์žกํ•œ ์•Œ๊ณ ๋ฆฌ์ฆ˜(๋ฐฑํŠธ๋ž˜ํ‚น)์„ PHP๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒฝํ—˜
  • ์‹ค์‹œ๊ฐ„ ์ž…๋ ฅ ๊ฒ€์ฆ ๋กœ์ง์„ ์ˆœ์ˆ˜ JavaScript๋กœ ๊ตฌํ˜„
  • SQLite ํŠธ๋žœ์žญ์…˜๊ณผ ์ธ๋ฑ์Šค ์ตœ์ ํ™” (๋‚œ์ด๋„๋ณ„ ํ†ต๊ณ„ ์กฐํšŒ ์‹œ ๋น ๋ฅธ ์‘๋‹ต)
  • ์บ์‹ฑ ์ „๋žต (์ผ์ผ ์ฑŒ๋ฆฐ์ง€ ํผ์ฆ์„ ์„œ๋ฒ„ ๋ฉ”๋ชจ๋ฆฌ์— ์บ์‹ฑํ•˜์—ฌ ์ค‘๋ณต ์ƒ์„ฑ ๋ฐฉ์ง€)
  • ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ํ–ฅ์ƒ์„ ์œ„ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ฐ ํ”ผ๋“œ๋ฐฑ ์ฒ˜๋ฆฌ

์ œ์•ฝ ์กฐ๊ฑด

  • โ€ข g5se ํ”„๋ ˆ์ž„์›Œํฌ ๊ธฐ๋ฐ˜ (Laravel/React/Vue ์‚ฌ์šฉ ๊ธˆ์ง€)
  • โ€ข ํด๋” ๊ธฐ๋ฐ˜ ์ž๋™ ๋ผ์šฐํŒ… ์‚ฌ์šฉ (app/modules/sudoku/index.php โ†’ /sudoku)
  • โ€ข modern shell ํ•„์ˆ˜ ์ ์šฉ (๋‹คํฌ๋ชจ๋“œ ์ž๋™ ๋Œ€์‘)
  • โ€ข SQLite DB๋Š” G5_DATA_PATH.'/modules/sudoku.sqlite'์— ์ €์žฅ
  • โ€ข ์Šค๋„์ฟ  ์ƒ์„ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ ๋ฐฑํŠธ๋ž˜ํ‚น์œผ๋กœ ๊ตฌํ˜„ (์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ธˆ์ง€)
  • โ€ข ํ”„๋ก ํŠธ์—”๋“œ๋Š” Vanilla JS๋งŒ ์‚ฌ์šฉ (jQuery/React/Vue ๊ธˆ์ง€)
  • โ€ข CDN ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ ๊ธˆ์ง€ (์ˆœ์ˆ˜ HTML/CSS/JS๋กœ ๊ตฌํ˜„)
  • โ€ข ๋ชจ๋“  ์‚ฌ์šฉ์ž ์ž…๋ ฅ์€ htmlspecialchars()๋กœ ์ด์Šค์ผ€์ดํ”„
  • โ€ข DB ์ฟผ๋ฆฌ๋Š” prepared statement๋กœ ์ธ์ ์…˜ ๋ฐฉ์ง€
  • โ€ข ํผ์ฆ ์ƒ์„ฑ์€ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ์—์„œ ์ฒ˜๋ฆฌ (ํด๋ผ์ด์–ธํŠธ์—์„œ ์ƒ์„ฑ ๊ธˆ์ง€)
  • โ€ข ์ผ์ผ ์ฑŒ๋ฆฐ์ง€ ์‹œ๋“œ๋Š” date('Y-m-d') ๊ธฐ๋ฐ˜์œผ๋กœ ๊ณ ์ •
  • โ€ข ํผ์ฆ ์™„์„ฑ ๊ฒ€์ฆ์€ ์„œ๋ฒ„์—์„œ ์ด์ค‘ ์ฒดํฌ (ํด๋ผ์ด์–ธํŠธ ์กฐ์ž‘ ๋ฐฉ์ง€)

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

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

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

**Step 1: ํ”„๋กœ์ ํŠธ ๊ณจ๊ฒฉ ์ƒ์„ฑ ๋ฐ modern shell ์ ์šฉ**

1. `app/modules/sudoku/index.php` ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  ๋‹ค์Œ ๊ณจ๊ฒฉ์„ ์ž‘์„ฑํ•˜์„ธ์š”:

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

// ํŽ˜์ด์ง€ ํƒ€์ดํ‹€
$g5['title'] = '์˜ค๋Š˜์˜ ์Šค๋„์ฟ  ํผ์ฆ';

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;">
        <div class="sudoku-header">
            <h1 style="font-size: 28px; font-weight: 700; margin-bottom: 8px;">๐Ÿงฉ ์˜ค๋Š˜์˜ ์Šค๋„์ฟ </h1>
            <p style="color: var(--m-text-muted); margin-bottom: 24px;">๋‚œ์ด๋„๋ฅผ ์„ ํƒํ•˜๊ณ  ํผ์ฆ์„ ํ’€์–ด๋ณด์„ธ์š”!</p>
        </div>

        <div class="difficulty-selector" style="margin-bottom: 24px;">
            <button class="diff-btn" data-level="easy">์‰ฌ์›€</button>
            <button class="diff-btn" data-level="medium">๋ณดํ†ต</button>
            <button class="diff-btn" data-level="hard">์–ด๋ ค์›€</button>
            <button class="diff-btn" data-level="extreme">์ต์ŠคํŠธ๋ฆผ</button>
        </div>

        <div id="sudoku-board" style="display: none;">
            <!-- ์Šค๋„์ฟ  ๋ณด๋“œ๊ฐ€ ์—ฌ๊ธฐ์— ๋™์ ์œผ๋กœ ์‚ฝ์ž…๋ฉ๋‹ˆ๋‹ค -->
        </div>

        <div id="game-controls" style="display: none; margin-top: 24px;">
            <div style="display: flex; gap: 12px; align-items: center;">
                <button id="hint-btn">๐Ÿ’ก ํžŒํŠธ (3)</button>
                <button id="reset-btn">๐Ÿ”„ ์ดˆ๊ธฐํ™”</button>
                <button id="check-btn">โœ… ์ •๋‹ต ํ™•์ธ</button>
                <div id="timer" style="margin-left: auto; font-size: 18px; font-weight: 600;">00:00</div>
            </div>
        </div>
    </main>
    <?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>

<style>
.difficulty-selector {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
}
.diff-btn {
    padding: 12px 24px;
    background: var(--m-surface);
    border: 2px solid var(--m-primary);
    border-radius: var(--m-radius-lg);
    color: var(--m-primary);
    font-weight: 600;
    cursor: pointer;
    transition: all 0.2s;
}
.diff-btn:hover {
    background: var(--m-primary);
    color: white;
}
#game-controls button {
    padding: 10px 20px;
    background: var(--m-primary);
    color: white;
    border: none;
    border-radius: var(--m-radius-lg);
    font-weight: 600;
    cursor: pointer;
}
#game-controls button:hover {
    opacity: 0.9;
}
</style>

<script>
document.querySelectorAll('.diff-btn').forEach(btn => {
    btn.addEventListener('click', function() {
        const level = this.dataset.level;
        alert(`${level} ๋‚œ์ด๋„ ์„ ํƒ๋จ! (๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ํผ์ฆ ์ƒ์„ฑ ๊ตฌํ˜„)`);
    });
});
</script>

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

2. ๋ธŒ๋ผ์šฐ์ €์—์„œ `/sudoku` ๊ฒฝ๋กœ๋กœ ์ ‘์†ํ•˜์—ฌ ํŽ˜์ด์ง€๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.
3. ๋‚œ์ด๋„ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด alert๊ฐ€ ๋œจ๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”.

**ํ™•์ธ ์‚ฌํ•ญ:**
- modern shell์ด ์ ์šฉ๋˜์–ด ๋„ค๋น„๊ฒŒ์ด์…˜๊ณผ ํ‘ธํ„ฐ๊ฐ€ ํ‘œ์‹œ๋˜๋Š”๊ฐ€?
- ๋‹คํฌ๋ชจ๋“œ ํ† ๊ธ€ ์‹œ ๋ฐฐ๊ฒฝ์ƒ‰๊ณผ ํ…์ŠคํŠธ ์ƒ‰์ƒ์ด ์ž๋™์œผ๋กœ ๋ณ€ํ•˜๋Š”๊ฐ€?
- ๋‚œ์ด๋„ ๋ฒ„ํŠผ ํ˜ธ๋ฒ„ ์‹œ ์ƒ‰์ƒ ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋ถ€๋“œ๋Ÿฌ์šด๊ฐ€?

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: `/sudoku` URL๋กœ ์ ‘์† ์‹œ modern shell์ด ์ ์šฉ๋œ ํŽ˜์ด์ง€๊ฐ€ ํ‘œ์‹œ๋˜๊ณ , 4๊ฐœ์˜ ๋‚œ์ด๋„ ๋ฒ„ํŠผ๊ณผ ๊ฒŒ์ž„ ์ปจํŠธ๋กค ์˜์—ญ์ด ๋ณด์ž…๋‹ˆ๋‹ค. ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ alert๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋ฉฐ, ๋‹คํฌ๋ชจ๋“œ ์ „ํ™˜ ์‹œ ๋””์ž์ธ ํ† ํฐ์ด ์ž๋™ ์ ์šฉ๋˜์–ด ๊ฐ€๋…์„ฑ์ด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

2
Step 2
**Step 2: SQLite DB ์ดˆ๊ธฐํ™” ๋ฐ ์Šค๋„์ฟ  ์ƒ์„ฑ ๋ฐฑ์—”๋“œ ๊ตฌํ˜„**

1. `app/modules/sudoku/index.php` ์ƒ๋‹จ(modern shell ์ธํด๋ฃจ๋“œ ์ „)์— ๋‹ค์Œ DB ์ดˆ๊ธฐํ™” ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”:

```php
// SQLite DB ์ดˆ๊ธฐํ™”
@mkdir(G5_DATA_PATH.'/modules', 0707, true);
$db_path = G5_DATA_PATH.'/modules/sudoku.sqlite';
$pdo = new PDO('sqlite:'.$db_path);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// ํ…Œ์ด๋ธ” ์ƒ์„ฑ (์ฒซ ์ ‘๊ทผ ์‹œ ์ž๋™ ์ƒ์„ฑ)
$pdo->exec("
    CREATE TABLE IF NOT EXISTS puzzle_history (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id TEXT,
        difficulty TEXT,
        seed TEXT,
        solve_time INTEGER,
        hints_used INTEGER,
        completed_at TEXT
    )
");
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_user_difficulty ON puzzle_history(user_id, difficulty)");
```

2. ๊ฐ™์€ ํŒŒ์ผ์— ์Šค๋„์ฟ  ์ƒ์„ฑ ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š” (modern shell ์ธํด๋ฃจ๋“œ ์ „):

```php
// ์Šค๋„์ฟ  ์ƒ์„ฑ ํ•จ์ˆ˜ (๋ฐฑํŠธ๋ž˜ํ‚น ์•Œ๊ณ ๋ฆฌ์ฆ˜)
function generateSudoku($seed) {
    mt_srand($seed);
    $board = array_fill(0, 9, array_fill(0, 9, 0));
    
    function isValid($board, $row, $col, $num) {
        // ํ–‰ ๊ฒ€์‚ฌ
        for ($x = 0; $x < 9; $x++) {
            if ($board[$row][$x] == $num) return false;
        }
        // ์—ด ๊ฒ€์‚ฌ
        for ($x = 0; $x < 9; $x++) {
            if ($board[$x][$col] == $num) return false;
        }
        // 3x3 ๋ฐ•์Šค ๊ฒ€์‚ฌ
        $startRow = $row - $row % 3;
        $startCol = $col - $col % 3;
        for ($i = 0; $i < 3; $i++) {
            for ($j = 0; $j < 3; $j++) {
                if ($board[$i + $startRow][$j + $startCol] == $num) return false;
            }
        }
        return true;
    }
    
    function solve(&$board) {
        for ($row = 0; $row < 9; $row++) {
            for ($col = 0; $col < 9; $col++) {
                if ($board[$row][$col] == 0) {
                    $nums = range(1, 9);
                    shuffle($nums);
                    foreach ($nums as $num) {
                        if (isValid($board, $row, $col, $num)) {
                            $board[$row][$col] = $num;
                            if (solve($board)) return true;
                            $board[$row][$col] = 0;
                        }
                    }
                    return false;
                }
            }
        }
        return true;
    }
    
    solve($board);
    return $board;
}

// ๋‚œ์ด๋„๋ณ„ ํžŒํŠธ ๊ฐœ์ˆ˜ ์„ค์ •
function removeCells($board, $difficulty) {
    $hints = ['easy' => 40, 'medium' => 30, 'hard' => 25, 'extreme' => 20];
    $cellsToRemove = 81 - $hints[$difficulty];
    
    $puzzle = $board;
    $removed = 0;
    while ($removed < $cellsToRemove) {
        $row = mt_rand(0, 8);
        $col = mt_rand(0, 8);
        if ($puzzle[$row][$col] != 0) {
            $puzzle[$row][$col] = 0;
            $removed++;
        }
    }
    return $puzzle;
}
```

3. AJAX ์š”์ฒญ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜์„ธ์š” (DB ์ดˆ๊ธฐํ™” ์ฝ”๋“œ ๋ฐ”๋กœ ์•„๋ž˜):

```php
// AJAX: ํผ์ฆ ์ƒ์„ฑ ์š”์ฒญ ์ฒ˜๋ฆฌ
if (isset($_POST['action']) && $_POST['action'] === 'generate') {
    $difficulty = $_POST['difficulty'] ?? 'medium';
    $seed = isset($_POST['daily']) ? strtotime(date('Y-m-d')) : time();
    
    $solution = generateSudoku($seed);
    $puzzle = removeCells($solution, $difficulty);
    
    header('Content-Type: application/json');
    echo json_encode([
        'puzzle' => $puzzle,
        'solution' => $solution,
        'seed' => $seed
    ]);
    exit;
}
```

4. ํ”„๋ก ํŠธ์—”๋“œ JavaScript๋ฅผ ์ˆ˜์ •ํ•˜์—ฌ AJAX๋กœ ํผ์ฆ์„ ๋ฐ›์•„์˜ค๋„๋ก ๋ณ€๊ฒฝํ•˜์„ธ์š”:

```javascript
let currentPuzzle = null;
let currentSolution = null;
let hintsLeft = 3;

document.querySelectorAll('.diff-btn').forEach(btn => {
    btn.addEventListener('click', async function() {
        const level = this.dataset.level;
        const formData = new FormData();
        formData.append('action', 'generate');
        formData.append('difficulty', level);
        
        const response = await fetch(window.location.href, {
            method: 'POST',
            body: formData
        });
        const data = await response.json();
        
        currentPuzzle = data.puzzle;
        currentSolution = data.solution;
        hintsLeft = 3;
        
        renderBoard(data.puzzle);
        document.getElementById('sudoku-board').style.display = 'block';
        document.getElementById('game-controls').style.display = 'block';
    });
});

function renderBoard(puzzle) {
    const boardHTML = puzzle.map((row, i) => 
        row.map((cell, j) => {
            const isGiven = cell !== 0;
            return `<input type="text" 
                class="sudoku-cell ${isGiven ? 'given' : ''}" 
                data-row="${i}" data-col="${j}" 
                value="${isGiven ? cell : ''}" 
                ${isGiven ? 'readonly' : ''} 
                maxlength="1">`;
        }).join('')
    ).join('');
    
    document.getElementById('sudoku-board').innerHTML = `
        <div class="sudoku-grid">${boardHTML}</div>
    `;
}
```

5. CSS์— ๋ณด๋“œ ์Šคํƒ€์ผ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”:

```css
.sudoku-grid {
    display: grid;
    grid-template-columns: repeat(9, 50px);
    gap: 0;
    border: 3px solid var(--m-text);
    width: fit-content;
    margin: 0 auto;
}
.sudoku-cell {
    width: 50px;
    height: 50px;
    text-align: center;
    font-size: 20px;
    font-weight: 600;
    border: 1px solid var(--m-text-muted);
    background: var(--m-surface);
    color: var(--m-text);
}
.sudoku-cell.given {
    background: rgba(128, 128, 128, 0.2);
    cursor: not-allowed;
}
.sudoku-cell:not(.given):focus {
    outline: 2px solid var(--m-primary);
}
.sudoku-cell:nth-child(3n) {
    border-right: 2px solid var(--m-text);
}
.sudoku-cell:nth-child(n+19):nth-child(-n+27),
.sudoku-cell:nth-child(n+46):nth-child(-n+54) {
    border-bottom: 2px solid var(--m-text);
}
```

**ํ™•์ธ ์‚ฌํ•ญ:**
- ๋‚œ์ด๋„ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ AJAX๋กœ ํผ์ฆ์ด ์ƒ์„ฑ๋˜๋Š”๊ฐ€?
- 9ร—9 ๊ทธ๋ฆฌ๋“œ๊ฐ€ 3ร—3 ๋‘๊บผ์šด ๊ฒฝ๊ณ„์„ ์œผ๋กœ ๊ตฌ๋ถ„๋˜์–ด ํ‘œ์‹œ๋˜๋Š”๊ฐ€?
- ์ดˆ๊ธฐ ํžŒํŠธ ์…€์€ ํšŒ์ƒ‰ ๋ฐฐ๊ฒฝ, ๋นˆ์นธ์€ ํฐ์ƒ‰ ๋ฐฐ๊ฒฝ์œผ๋กœ ๊ตฌ๋ถ„๋˜๋Š”๊ฐ€?
- SQLite DB ํŒŒ์ผ์ด `data/modules/sudoku.sqlite` ๊ฒฝ๋กœ์— ์ƒ์„ฑ๋˜์—ˆ๋Š”๊ฐ€?

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋‚œ์ด๋„ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์„œ๋ฒ„์—์„œ ๋ฐฑํŠธ๋ž˜ํ‚น ์•Œ๊ณ ๋ฆฌ์ฆ˜์œผ๋กœ ์ƒ์„ฑ๋œ ์Šค๋„์ฟ  ํผ์ฆ์ด 9ร—9 ๊ทธ๋ฆฌ๋“œ๋กœ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค. ์ดˆ๊ธฐ ํžŒํŠธ๋Š” ํšŒ์ƒ‰ ๋ฐฐ๊ฒฝ์œผ๋กœ ํ‘œ์‹œ๋˜๊ณ  ์ˆ˜์ • ๋ถˆ๊ฐ€๋Šฅํ•˜๋ฉฐ, ๋นˆ์นธ์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. SQLite DB๊ฐ€ ์ •์ƒ ์ƒ์„ฑ๋˜์–ด ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ๊ฐ€ ์ค€๋น„๋ฉ๋‹ˆ๋‹ค.

3
Step 3
**Step 3: ์‹ค์‹œ๊ฐ„ ์ž…๋ ฅ ๊ฒ€์ฆ ๋ฐ ํƒ€์ด๋จธ ๊ตฌํ˜„**

1. JavaScript์— ์ž…๋ ฅ ๊ฒ€์ฆ ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”:

```javascript
function validateCell(row, col, value) {
    if (value === '') return true;
    const num = parseInt(value);
    if (isNaN(num) || num < 1 || num > 9) return false;
    
    // ํ–‰ ๊ฒ€์‚ฌ
    const cells = document.querySelectorAll('.sudoku-cell');
    for (let i = 0; i < 9; i++) {
        const cell = cells[row * 9 + i];
        if (i !== col && cell.value === value) return false;
    }
    
    // ์—ด ๊ฒ€์‚ฌ
    for (let i = 0; i < 9; i++) {
        const cell = cells[i * 9 + col];
        if (i !== row && cell.value === value) return false;
    }
    
    // 3x3 ๋ฐ•์Šค ๊ฒ€์‚ฌ
    const boxRow = Math.floor(row / 3) * 3;
    const boxCol = Math.floor(col / 3) * 3;
    for (let i = 0; i < 3; i++) {
        for (let j = 0; j < 3; j++) {
            const r = boxRow + i;
            const c = boxCol + j;
            if (r !== row || c !== col) {
                const cell = cells[r * 9 + c];
                if (cell.value === value) return false;
            }
        }
    }
    return true;
}

function renderBoard(puzzle) {
    // ๊ธฐ์กด ์ฝ”๋“œ ์œ ์ง€ํ•˜๊ณ , ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€
    const boardHTML = puzzle.map((row, i) => 
        row.map((cell, j) => {
            const isGiven = cell !== 0;
            return `<input type="text" 
                class="sudoku-cell ${isGiven ? 'given' : ''}" 
                data-row="${i}" data-col="${j}" 
                value="${isGiven ? cell : ''}" 
                ${isGiven ? 'readonly' : ''} 
                maxlength="1">`;
        }).join('')
    ).join('');
    
    document.getElementById('sudoku-board').innerHTML = `
        <div class="sudoku-grid">${boardHTML}</div>
    `;
    
    // ์ž…๋ ฅ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
    document.querySelectorAll('.sudoku-cell:not(.given)').forEach(cell => {
        cell.addEventListener('input', function(e) {
            const row = parseInt(this.dataset.row);
            const col = parseInt(this.dataset.col);
            const value = this.value;
            
            if (!validateCell(row, col, value)) {
                this.classList.add('invalid');
                this.style.animation = 'shake 0.3s';
            } else {
                this.classList.remove('invalid');
            }
        });
    });
    
    startTimer();
}
```

2. ํƒ€์ด๋จธ ๊ตฌํ˜„:

```javascript
let timerInterval = null;
let elapsedSeconds = 0;

function startTimer() {
    elapsedSeconds = 0;
    clearInterval(timerInterval);
    timerInterval = setInterval(() => {
        elapsedSeconds++;
        const minutes = Math.floor(elapsedSeconds / 60);
        const seconds = elapsedSeconds % 60;
        document.getElementById('timer').textContent = 
            `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
    }, 1000);
}

function stopTimer() {
    clearInterval(timerInterval);
}
```

3. CSS์— ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ถ”๊ฐ€:

```css
.sudoku-cell.invalid {
    border: 2px solid #ef4444;
    background: rgba(239, 68, 68, 0.1);
}
@keyframes shake {
    0%, 100% { transform: translateX(0); }
    25% { transform: translateX(-5px); }
    75% { transform: translateX(5px); }
}
```

**ํ™•์ธ ์‚ฌํ•ญ:**
- ๋นˆ์นธ์— ์ˆซ์ž ์ž…๋ ฅ ์‹œ ํ–‰/์—ด/๋ฐ•์Šค ๊ทœ์น™ ์œ„๋ฐ˜์ด ์ฆ‰์‹œ ๊ฐ์ง€๋˜๋Š”๊ฐ€?
- ์œ„๋ฐ˜ ์‹œ ๋นจ๊ฐ„์ƒ‰ ํ…Œ๋‘๋ฆฌ์™€ ํ”๋“ค๋ฆผ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ํ‘œ์‹œ๋˜๋Š”๊ฐ€?
- ํƒ€์ด๋จธ๊ฐ€ ํผ์ฆ ๋กœ๋“œ ์‹œ ์ž๋™์œผ๋กœ ์‹œ์ž‘๋˜๋Š”๊ฐ€?

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์‚ฌ์šฉ์ž๊ฐ€ ์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•  ๋•Œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์Šค๋„์ฟ  ๊ทœ์น™์„ ๊ฒ€์ฆํ•˜๋ฉฐ, ์œ„๋ฐ˜ ์‹œ ๋นจ๊ฐ„์ƒ‰ ํ…Œ๋‘๋ฆฌ์™€ ํ”๋“ค๋ฆผ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ํƒ€์ด๋จธ๊ฐ€ 00:00๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜์—ฌ 1์ดˆ๋งˆ๋‹ค ์ฆ๊ฐ€ํ•˜๋ฉฐ, ์™„๋ฃŒ ์‹œ ์ •์ง€๋ฉ๋‹ˆ๋‹ค.

4
Step 4
**Step 4: ํžŒํŠธ ์‹œ์Šคํ…œ ๋ฐ ์ •๋‹ต ํ™•์ธ ๊ตฌํ˜„**

1. ํžŒํŠธ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€:

```javascript
document.getElementById('hint-btn').addEventListener('click', function() {
    if (hintsLeft <= 0) {
        alert('ํžŒํŠธ๋ฅผ ๋ชจ๋‘ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค!');
        return;
    }
    
    const emptyCells = [];
    document.querySelectorAll('.sudoku-cell:not(.given)').forEach((cell, idx) => {
        if (cell.value === '') {
            const row = parseInt(cell.dataset.row);
            const col = parseInt(cell.dataset.col);
            emptyCells.push({ cell, row, col });
        }
    });
    
    if (emptyCells.length === 0) {
        alert('๋ชจ๋“  ์นธ์ด ์ฑ„์›Œ์กŒ์Šต๋‹ˆ๋‹ค!');
        return;
    }
    
    const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
    randomCell.cell.value = currentSolution[randomCell.row][randomCell.col];
    randomCell.cell.classList.add('hint-cell');
    randomCell.cell.readOnly = true;
    
    hintsLeft--;
    this.textContent = `๐Ÿ’ก ํžŒํŠธ (${hintsLeft})`;
});
```

2. ์ •๋‹ต ํ™•์ธ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€:

```javascript
document.getElementById('check-btn').addEventListener('click', async function() {
    const cells = document.querySelectorAll('.sudoku-cell');
    let isComplete = true;
    let isCorrect = true;
    
    cells.forEach((cell, idx) => {
        const row = Math.floor(idx / 9);
        const col = idx % 9;
        const value = parseInt(cell.value);
        
        if (!value) {
            isComplete = false;
        } else if (value !== currentSolution[row][col]) {
            isCorrect = false;
        }
    });
    
    if (!isComplete) {
        alert('์•„์ง ๋นˆ์นธ์ด ๋‚จ์•„์žˆ์Šต๋‹ˆ๋‹ค!');
        return;
    }
    
    if (!isCorrect) {
        alert('ํ‹€๋ฆฐ ์นธ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ํ™•์ธํ•ด๋ณด์„ธ์š”!');
        return;
    }
    
    stopTimer();
    
    // ์„œ๋ฒ„์— ์™„๋ฃŒ ๊ธฐ๋ก ์ €์žฅ
    const formData = new FormData();
    formData.append('action', 'save_result');
    formData.append('difficulty', currentDifficulty);
    formData.append('seed', currentSeed);
    formData.append('solve_time', elapsedSeconds);
    formData.append('hints_used', 3 - hintsLeft);
    
    await fetch(window.location.href, {
        method: 'POST',
        body: formData
    });
    
    showCompletionModal();
});

function showCompletionModal() {
    const modal = document.createElement('div');
    modal.className = 'completion-modal';
    modal.innerHTML = `
        <div class="modal-content">
            <h2>๐ŸŽ‰ ์ถ•ํ•˜ํ•ฉ๋‹ˆ๋‹ค!</h2>
            <p>ํผ์ฆ์„ ์™„์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค!</p>
            <div class="stats">
                <div>โฑ๏ธ ์†Œ์š” ์‹œ๊ฐ„: ${Math.floor(elapsedSeconds / 60)}๋ถ„ ${elapsedSeconds % 60}์ดˆ</div>
                <div>๐Ÿ’ก ํžŒํŠธ ์‚ฌ์šฉ: ${3 - hintsLeft}ํšŒ</div>
            </div>
            <button onclick="location.reload()">์ƒˆ ํผ์ฆ ๋„์ „ํ•˜๊ธฐ</button>
        </div>
    `;
    document.body.appendChild(modal);
}
```

3. PHP์— ์™„๋ฃŒ ๊ธฐ๋ก ์ €์žฅ ๋กœ์ง ์ถ”๊ฐ€ (AJAX ์ฒ˜๋ฆฌ ์„น์…˜์—):

```php
// AJAX: ์™„๋ฃŒ ๊ธฐ๋ก ์ €์žฅ
if (isset($_POST['action']) && $_POST['action'] === 'save_result') {
    $user_id = $is_member ? $member['mb_id'] : 'guest';
    $difficulty = $_POST['difficulty'];
    $seed = $_POST['seed'];
    $solve_time = (int)$_POST['solve_time'];
    $hints_used = (int)$_POST['hints_used'];
    $completed_at = date('c');
    
    $stmt = $pdo->prepare("
        INSERT INTO puzzle_history (user_id, difficulty, seed, solve_time, hints_used, completed_at)
        VALUES (?, ?, ?, ?, ?, ?)
    ");
    $stmt->execute([$user_id, $difficulty, $seed, $solve_time, $hints_used, $completed_at]);
    
    echo json_encode(['success' => true]);
    exit;
}
```

4. ๋ชจ๋‹ฌ CSS ์ถ”๊ฐ€:

```css
.completion-modal {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.7);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
}
.modal-content {
    background: var(--m-surface);
    padding: 32px;
    border-radius: var(--m-radius-lg);
    text-align: center;
    box-shadow: var(--m-shadow);
    max-width: 400px;
}
.modal-content h2 {
    font-size: 32px;
    margin-bottom: 16px;
}
.stats {
    margin: 24px 0;
    font-size: 18px;
}
.stats div {
    margin: 8px 0;
}
.hint-cell {
    background: rgba(59, 130, 246, 0.2);
}
```

5. ์ดˆ๊ธฐํ™” ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€:

```javascript
document.getElementById('reset-btn').addEventListener('click', function() {
    if (confirm('ํ˜„์žฌ ์ž…๋ ฅํ•œ ๋‚ด์šฉ์ด ๋ชจ๋‘ ์ง€์›Œ์ง‘๋‹ˆ๋‹ค. ๊ณ„์†ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?')) {
        renderBoard(currentPuzzle);
        hintsLeft = 3;
        document.getElementById('hint-btn').textContent = '๐Ÿ’ก ํžŒํŠธ (3)';
    }
});
```

**ํ™•์ธ ์‚ฌํ•ญ:**
- ํžŒํŠธ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋žœ๋ค ๋นˆ์นธ์— ์ •๋‹ต์ด ํ‘œ์‹œ๋˜๋Š”๊ฐ€?
- ํžŒํŠธ ์‚ฌ์šฉ ํšŸ์ˆ˜๊ฐ€ ๊ฐ์†Œํ•˜๊ณ , 3ํšŒ ์ดˆ๊ณผ ์‹œ ๊ฒฝ๊ณ ๊ฐ€ ๋œจ๋Š”๊ฐ€?
- ์ •๋‹ต ํ™•์ธ ์‹œ ๋ชจ๋“  ์นธ์ด ๋งž์œผ๋ฉด ์ถ•ํ•˜ ๋ชจ๋‹ฌ์ด ํ‘œ์‹œ๋˜๋Š”๊ฐ€?
- ์™„๋ฃŒ ๊ธฐ๋ก์ด SQLite DB์— ์ •์ƒ ์ €์žฅ๋˜๋Š”๊ฐ€?

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ํžŒํŠธ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋žœ๋ค ๋นˆ์นธ์— ํŒŒ๋ž€์ƒ‰ ๋ฐฐ๊ฒฝ์œผ๋กœ ์ •๋‹ต์ด ํ‘œ์‹œ๋˜๋ฉฐ, ์‚ฌ์šฉ ํšŸ์ˆ˜๊ฐ€ ๊ฐ์†Œํ•ฉ๋‹ˆ๋‹ค. ์ •๋‹ต ํ™•์ธ ์‹œ ๋ชจ๋“  ์นธ์ด ๋งž์œผ๋ฉด ์ถ•ํ•˜ ๋ชจ๋‹ฌ์ด ํŒ์—…๋˜๊ณ , ์†Œ์š” ์‹œ๊ฐ„๊ณผ ํžŒํŠธ ์‚ฌ์šฉ ํšŸ์ˆ˜๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ์™„๋ฃŒ ๊ธฐ๋ก์ด DB์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.

5
Step 5
**Step 5: ํผ์ฆ ํžˆ์Šคํ† ๋ฆฌ ํŽ˜์ด์ง€ ๊ตฌํ˜„**

1. `app/modules/sudoku/history.php` ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜์„ธ์š”:

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

// SQLite DB ์—ฐ๊ฒฐ
$db_path = G5_DATA_PATH.'/modules/sudoku.sqlite';
$pdo = new PDO('sqlite:'.$db_path);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$user_id = $is_member ? $member['mb_id'] : 'guest';

// ๋‚œ์ด๋„๋ณ„ ํ†ต๊ณ„
$stats = [];
foreach (['easy', 'medium', 'hard', 'extreme'] as $diff) {
    $stmt = $pdo->prepare("
        SELECT COUNT(*) as count, AVG(solve_time) as avg_time, MIN(solve_time) as best_time
        FROM puzzle_history
        WHERE user_id = ? AND difficulty = ?
    ");
    $stmt->execute([$user_id, $diff]);
    $stats[$diff] = $stmt->fetch(PDO::FETCH_ASSOC);
}

// ์ตœ๊ทผ 10๊ฐœ ๊ธฐ๋ก
$stmt = $pdo->prepare("
    SELECT * FROM puzzle_history
    WHERE user_id = ?
    ORDER BY completed_at DESC
    LIMIT 10
");
$stmt->execute([$user_id]);
$recent = $stmt->fetchAll(PDO::FETCH_ASSOC);

$g5['title'] = '๋‚ด ํผ์ฆ ํžˆ์Šคํ† ๋ฆฌ';

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: 28px; font-weight: 700; margin-bottom: 24px;">๐Ÿ“Š ๋‚ด ํผ์ฆ ํžˆ์Šคํ† ๋ฆฌ</h1>
        
        <div class="stats-grid">
            <?php foreach ($stats as $diff => $data): ?>
                <div class="stat-card">
                    <h3><?php echo ucfirst($diff); ?></h3>
                    <div class="stat-item">์™„๋ฃŒ: <?php echo $data['count']; ?>ํšŒ</div>
                    <div class="stat-item">ํ‰๊ท : <?php echo $data['avg_time'] ? gmdate('i:s', $data['avg_time']) : '-'; ?></div>
                    <div class="stat-item">์ตœ๊ณ : <?php echo $data['best_time'] ? gmdate('i:s', $data['best_time']) : '-'; ?></div>
                </div>
            <?php endforeach; ?>
        </div>
        
        <h2 style="font-size: 24px; font-weight: 700; margin: 32px 0 16px;">์ตœ๊ทผ ๊ธฐ๋ก</h2>
        <div class="history-table">
            <?php if (empty($recent)): ?>
                <p style="color: var(--m-text-muted);">์•„์ง ์™„๋ฃŒํ•œ ํผ์ฆ์ด ์—†์Šต๋‹ˆ๋‹ค.</p>
            <?php else: ?>
                <table>
                    <thead>
                        <tr>
                            <th>๋‚œ์ด๋„</th>
                            <th>์†Œ์š” ์‹œ๊ฐ„</th>
                            <th>ํžŒํŠธ ์‚ฌ์šฉ</th>
                            <th>์™„๋ฃŒ ์ผ์‹œ</th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach ($recent as $row): ?>
                            <tr>
                                <td><?php echo htmlspecialchars($row['difficulty']); ?></td>
                                <td><?php echo gmdate('i:s', $row['solve_time']); ?></td>
                                <td><?php echo $row['hints_used']; ?>ํšŒ</td>
                                <td><?php echo date('Y-m-d H:i', strtotime($row['completed_at'])); ?></td>
                            </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            <?php endif; ?>
        </div>
        
        <div style="margin-top: 32px;">
            <a href="/sudoku" style="display: inline-block; padding: 12px 24px; background: var(--m-primary); color: white; border-radius: var(--m-radius-lg); text-decoration: none; font-weight: 600;">์ƒˆ ํผ์ฆ ํ’€๊ธฐ</a>
        </div>
    </main>
    <?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>

<style>
.stats-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 16px;
    margin-bottom: 32px;
}
.stat-card {
    background: var(--m-surface);
    padding: 20px;
    border-radius: var(--m-radius-lg);
    box-shadow: var(--m-shadow);
}
.stat-card h3 {
    font-size: 18px;
    font-weight: 700;
    margin-bottom: 12px;
    text-transform: capitalize;
}
.stat-item {
    margin: 8px 0;
    color: var(--m-text-muted);
}
.history-table table {
    width: 100%;
    border-collapse: collapse;
}
.history-table th,
.history-table td {
    padding: 12px;
    text-align: left;
    border-bottom: 1px solid var(--m-text-muted);
}
.history-table th {
    font-weight: 700;
    background: var(--m-surface);
}
</style>

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

2. `app/modules/sudoku/index.php`์˜ ๊ฒŒ์ž„ ์ปจํŠธ๋กค ์˜์—ญ์— ํžˆ์Šคํ† ๋ฆฌ ๋งํฌ ์ถ”๊ฐ€:

```html
<div id="game-controls" style="display: none; margin-top: 24px;">
    <div style="display: flex; gap: 12px; align-items: center;">
        <button id="hint-btn">๐Ÿ’ก ํžŒํŠธ (3)</button>
        <button id="reset-btn">๐Ÿ”„ ์ดˆ๊ธฐํ™”</button>
        <button id="check-btn">โœ… ์ •๋‹ต ํ™•์ธ</button>
        <a href="/sudoku/history" style="margin-left: auto; padding: 10px 20px; background: var(--m-surface); border: 2px solid var(--m-primary); border-radius: var(--m-radius-lg); color: var(--m-primary); text-decoration: none; font-weight: 600;">๐Ÿ“Š ํžˆ์Šคํ† ๋ฆฌ</a>
        <div id="timer" style="font-size: 18px; font-weight: 600;">00:00</div>
    </div>
</div>
```

**ํ™•์ธ ์‚ฌํ•ญ:**
- `/sudoku/history` ๊ฒฝ๋กœ๋กœ ์ ‘์† ์‹œ ๋‚œ์ด๋„๋ณ„ ํ†ต๊ณ„๊ฐ€ ํ‘œ์‹œ๋˜๋Š”๊ฐ€?
- ์ตœ๊ทผ 10๊ฐœ ์™„๋ฃŒ ๊ธฐ๋ก์ด ํ…Œ์ด๋ธ”๋กœ ํ‘œ์‹œ๋˜๋Š”๊ฐ€?
- ๋‹คํฌ๋ชจ๋“œ์—์„œ๋„ ํ…Œ์ด๋ธ”์ด ์ฝ๊ธฐ ์‰ฌ์šด๊ฐ€?

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: `/sudoku/history` ํŽ˜์ด์ง€์—์„œ ๋‚œ์ด๋„๋ณ„ ์™„๋ฃŒ ํšŸ์ˆ˜, ํ‰๊ท /์ตœ๊ณ  ์†Œ์š” ์‹œ๊ฐ„์ด ์นด๋“œ ํ˜•ํƒœ๋กœ ํ‘œ์‹œ๋˜๋ฉฐ, ์ตœ๊ทผ 10๊ฐœ ๊ธฐ๋ก์ด ํ…Œ์ด๋ธ”๋กœ ๋‚˜์—ด๋ฉ๋‹ˆ๋‹ค. ๋‹คํฌ๋ชจ๋“œ์—์„œ๋„ ๊ฐ€๋…์„ฑ์ด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

6
Step 6
**Step 6: ์ผ์ผ ์ฑŒ๋ฆฐ์ง€ ๋ชจ๋“œ ๋ฐ ๋ฉ”๋ชจ ๊ธฐ๋Šฅ ๊ตฌํ˜„**

1. `app/modules/sudoku/index.php`์˜ ๋‚œ์ด๋„ ์„ ํƒ ์˜์—ญ ์œ„์— ์ผ์ผ ์ฑŒ๋ฆฐ์ง€ ๋ฒ„ํŠผ ์ถ”๊ฐ€:

```html
<div class="daily-challenge" style="margin-bottom: 24px; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: var(--m-radius-lg); color: white;">
    <h2 style="font-size: 20px; font-weight: 700; margin-bottom: 8px;">๐Ÿ† ์˜ค๋Š˜์˜ ์ฑŒ๋ฆฐ์ง€</h2>
    <p style="margin-bottom: 12px; opacity: 0.9;">๋ชจ๋“  ์‚ฌ์šฉ์ž๊ฐ€ ๋™์ผํ•œ ํผ์ฆ์— ๋„์ „ํ•ฉ๋‹ˆ๋‹ค!</p>
    <button id="daily-btn" style="padding: 10px 24px; background: white; color: #667eea; border: none; border-radius: var(--m-radius-lg); font-weight: 600; cursor: pointer;">์˜ค๋Š˜์˜ ํผ์ฆ ์‹œ์ž‘ํ•˜๊ธฐ</button>
</div>
```

2. JavaScript์— ์ผ์ผ ์ฑŒ๋ฆฐ์ง€ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€:

```javascript
let currentDifficulty = 'medium';
let currentSeed = null;

document.getElementById('daily-btn').addEventListener('click', async function() {
    const formData = new FormData();
    formData.append('action', 'generate');
    formData.append('difficulty', 'medium');
    formData.append('daily', '1');
    
    const response = await fetch(window.location.href, {
        method: 'POST',
        body: formData
    });
    const data = await response.json();
    
    currentPuzzle = data.puzzle;
    currentSolution = data.solution;
    currentDifficulty = 'medium';
    currentSeed = data.seed;
    hintsLeft = 3;
    
    renderBoard(data.puzzle);
    document.getElementById('sudoku-board').style.display = 'block';
    document.getElementById('game-controls').style.display = 'block';
});

// ๊ธฐ์กด ๋‚œ์ด๋„ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ์—๋„ currentDifficulty, currentSeed ์ €์žฅ ์ถ”๊ฐ€
document.querySelectorAll('.diff-btn').forEach(btn => {
    btn.addEventListener('click', async function() {
        const level = this.dataset.level;
        currentDifficulty = level;
        // ... ๊ธฐ์กด ์ฝ”๋“œ ...
        currentSeed = data.seed;
    });
});
```

3. ๋ฉ”๋ชจ ๋ชจ๋“œ ๊ตฌํ˜„:

```javascript
let memoMode = false;

// ๊ฒŒ์ž„ ์ปจํŠธ๋กค์— ๋ฉ”๋ชจ ๋ชจ๋“œ ๋ฒ„ํŠผ ์ถ”๊ฐ€ (HTML)
<button id="memo-btn" style="background: var(--m-surface); border: 2px solid var(--m-primary); color: var(--m-primary);">๐Ÿ“ ๋ฉ”๋ชจ ๋ชจ๋“œ</button>

// JavaScript ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
document.getElementById('memo-btn').addEventListener('click', function() {
    memoMode = !memoMode;
    this.style.background = memoMode ? 'var(--m-primary)' : 'var(--m-surface)';
    this.style.color = memoMode ? 'white' : 'var(--m-primary)';
    this.textContent = memoMode ? 'โœ๏ธ ์ž…๋ ฅ ๋ชจ๋“œ' : '๐Ÿ“ ๋ฉ”๋ชจ ๋ชจ๋“œ';
});

// renderBoard ํ•จ์ˆ˜ ์ˆ˜์ • (์…€ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋ถ€๋ถ„)
cell.addEventListener('input', function(e) {
    const row = parseInt(this.dataset.row);
    const col = parseInt(this.dataset.col);
    const value = this.value;
    
    if (memoMode) {
        // ๋ฉ”๋ชจ ๋ชจ๋“œ: ์ž‘์€ ์ˆซ์ž๋กœ ํ‘œ์‹œ
        this.classList.add('memo-cell');
        this.style.fontSize = '12px';
        this.style.color = 'var(--m-text-muted)';
    } else {
        // ์ผ๋ฐ˜ ๋ชจ๋“œ: ๊ฒ€์ฆ ์ˆ˜ํ–‰
        this.classList.remove('memo-cell');
        this.style.fontSize = '20px';
        this.style.color = 'var(--m-text)';
        
        if (!validateCell(row, col, value)) {
            this.classList.add('invalid');
            this.style.animation = 'shake 0.3s';
        } else {
            this.classList.remove('invalid');
        }
    }
});
```

4. CSS์— ๋ฉ”๋ชจ ์…€ ์Šคํƒ€์ผ ์ถ”๊ฐ€:

```css
.sudoku-cell.memo-cell {
    font-size: 12px !important;
    color: var(--m-text-muted) !important;
    font-weight: 400;
}
```

5. PHP์— ์ผ์ผ ์ฑŒ๋ฆฐ์ง€ ์ˆœ์œ„ํ‘œ ์กฐํšŒ ๋กœ์ง ์ถ”๊ฐ€ (history.php์—):

```php
// ์˜ค๋Š˜์˜ ์ฑŒ๋ฆฐ์ง€ ์ˆœ์œ„ (์ƒ์œ„ 10๋ช…)
$today_seed = strtotime(date('Y-m-d'));
$stmt = $pdo->prepare("
    SELECT user_id, solve_time, hints_used, completed_at
    FROM puzzle_history
    WHERE seed = ? AND difficulty = 'medium'
    ORDER BY solve_time ASC, hints_used ASC
    LIMIT 10
");
$stmt->execute([$today_seed]);
$daily_leaderboard = $stmt->fetchAll(PDO::FETCH_ASSOC);
```

6. history.php์— ์ˆœ์œ„ํ‘œ ์„น์…˜ ์ถ”๊ฐ€:

```html
<h2 style="font-size: 24px; font-weight: 700; margin: 32px 0 16px;">๐Ÿ† ์˜ค๋Š˜์˜ ์ฑŒ๋ฆฐ์ง€ ์ˆœ์œ„</h2>
<div class="leaderboard">
    <?php if (empty($daily_leaderboard)): ?>
        <p style="color: var(--m-text-muted);">์•„์ง ์˜ค๋Š˜์˜ ์ฑŒ๋ฆฐ์ง€๋ฅผ ์™„๋ฃŒํ•œ ์‚ฌ๋žŒ์ด ์—†์Šต๋‹ˆ๋‹ค.</p>
    <?php else: ?>
        <table>
            <thead>
                <tr>
                    <th>์ˆœ์œ„</th>
                    <th>์‚ฌ์šฉ์ž</th>
                    <th>์†Œ์š” ์‹œ๊ฐ„</th>
                    <th>ํžŒํŠธ ์‚ฌ์šฉ</th>
                </tr>
            </thead>
            <tbody>
                <?php foreach ($daily_leaderboard as $idx => $row): ?>
                    <tr>
                        <td><?php echo $idx + 1; ?></td>
                        <td><?php echo htmlspecialchars($row['user_id']); ?></td>
                        <td><?php echo gmdate('i:s', $row['solve_time']); ?></td>
                        <td><?php echo $row['hints_used']; ?>ํšŒ</td>
                    </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
    <?php endif; ?>
</div>
```

**ํ™•์ธ ์‚ฌํ•ญ:**
- ์˜ค๋Š˜์˜ ์ฑŒ๋ฆฐ์ง€ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋™์ผํ•œ ์‹œ๋“œ๋กœ ํผ์ฆ์ด ์ƒ์„ฑ๋˜๋Š”๊ฐ€?
- ๋ฉ”๋ชจ ๋ชจ๋“œ ํ† ๊ธ€ ์‹œ ์ž…๋ ฅ ์ˆซ์ž๊ฐ€ ์ž‘์€ ํšŒ์ƒ‰ ๊ธ€์”จ๋กœ ํ‘œ์‹œ๋˜๋Š”๊ฐ€?
- ํžˆ์Šคํ† ๋ฆฌ ํŽ˜์ด์ง€์— ์˜ค๋Š˜์˜ ์ฑŒ๋ฆฐ์ง€ ์ˆœ์œ„ํ‘œ๊ฐ€ ํ‘œ์‹œ๋˜๋Š”๊ฐ€?

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

7
Step 7
**Step 7: ์ž๋™ ์ €์žฅ ๋ฐ ํผ์ฆ ๊ณต์œ  ๊ธฐ๋Šฅ ๊ตฌํ˜„**

1. localStorage ์ž๋™ ์ €์žฅ ๊ตฌํ˜„:

```javascript
function saveProgress() {
    const cells = document.querySelectorAll('.sudoku-cell');
    const progress = [];
    cells.forEach(cell => {
        progress.push({
            row: cell.dataset.row,
            col: cell.dataset.col,
            value: cell.value,
            isMemo: cell.classList.contains('memo-cell')
        });
    });
    
    localStorage.setItem('sudoku_progress', JSON.stringify({
        puzzle: currentPuzzle,
        solution: currentSolution,
        difficulty: currentDifficulty,
        seed: currentSeed,
        progress: progress,
        elapsedSeconds: elapsedSeconds,
        hintsLeft: hintsLeft,
        timestamp: Date.now()
    }));
}

function loadProgress() {
    const saved = localStorage.getItem('sudoku_progress');
    if (!saved) return false;
    
    const data = JSON.parse(saved);
    // 24์‹œ๊ฐ„ ์ด์ƒ ์ง€๋‚œ ๋ฐ์ดํ„ฐ๋Š” ๋ฌด์‹œ
    if (Date.now() - data.timestamp > 86400000) {
        localStorage.removeItem('sudoku_progress');
        return false;
    }
    
    currentPuzzle = data.puzzle;
    currentSolution = data.solution;
    currentDifficulty = data.difficulty;
    currentSeed = data.seed;
    elapsedSeconds = data.elapsedSeconds;
    hintsLeft = data.hintsLeft;
    
    renderBoard(data.puzzle);
    
    // ์ง„ํ–‰ ์ƒํ™ฉ ๋ณต์›
    data.progress.forEach(item => {
        const cell = document.querySelector(`[data-row="${item.row}"][data-col="${item.col}"]`);
        if (cell && !cell.classList.contains('given')) {
            cell.value = item.value;
            if (item.isMemo) {
                cell.classList.add('memo-cell');
                cell.style.fontSize = '12px';
                cell.style.color = 'var(--m-text-muted)';
            }
        }
    });
    
    document.getElementById('hint-btn').textContent = `๐Ÿ’ก ํžŒํŠธ (${hintsLeft})`;
    document.getElementById('sudoku-board').style.display = 'block';
    document.getElementById('game-controls').style.display = 'block';
    
    return true;
}

// ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ž๋™ ๋ณต์›
window.addEventListener('DOMContentLoaded', function() {
    if (loadProgress()) {
        if (confirm('์ด์ „์— ํ’€๋˜ ํผ์ฆ์„ ์ด์–ด์„œ ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?')) {
            startTimer();
        } else {
            localStorage.removeItem('sudoku_progress');
            location.reload();
        }
    }
});

// ์ž…๋ ฅ ์‹œ๋งˆ๋‹ค ์ž๋™ ์ €์žฅ
document.addEventListener('input', function(e) {
    if (e.target.classList.contains('sudoku-cell')) {
        saveProgress();
    }
});
```

2. ํผ์ฆ ๊ณต์œ  ๊ธฐ๋Šฅ ๊ตฌํ˜„:

```javascript
// ๊ฒŒ์ž„ ์ปจํŠธ๋กค์— ๊ณต์œ  ๋ฒ„ํŠผ ์ถ”๊ฐ€ (HTML)
<button id="share-btn" style="background: var(--m-surface); border: 2px solid var(--m-primary); color: var(--m-primary);">๐Ÿ”— ํผ์ฆ ๊ณต์œ </button>

// JavaScript ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
document.getElementById('share-btn').addEventListener('click', function() {
    if (!currentSeed) {
        alert('๋จผ์ € ํผ์ฆ์„ ์‹œ์ž‘ํ•ด์ฃผ์„ธ์š”!');
        return;
    }
    
    const shareUrl = `${window.location.origin}/sudoku?seed=${currentSeed}&difficulty=${currentDifficulty}`;
    
    if (navigator.clipboard) {
        navigator.clipboard.writeText(shareUrl).then(() => {
            alert('ํผ์ฆ ๋งํฌ๊ฐ€ ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!\n\n' + shareUrl);
        });
    } else {
        prompt('์ด ๋งํฌ๋ฅผ ๋ณต์‚ฌํ•˜์—ฌ ์นœ๊ตฌ์—๊ฒŒ ๊ณต์œ ํ•˜์„ธ์š”:', shareUrl);
    }
});
```

3. URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํผ์ฆ ๋กœ๋“œ ๊ธฐ๋Šฅ ์ถ”๊ฐ€:

```javascript
// ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ URL ํŒŒ๋ผ๋ฏธํ„ฐ ํ™•์ธ
window.addEventListener('DOMContentLoaded', async function() {
    const params = new URLSearchParams(window.location.search);
    const seed = params.get('seed');
    const difficulty = params.get('difficulty');
    
    if (seed && difficulty) {
        const formData = new FormData();
        formData.append('action', 'generate');
        formData.append('difficulty', difficulty);
        formData.append('seed', seed);
        
        const response = await fetch(window.location.href.split('?')[0], {
            method: 'POST',
            body: formData
        });
        const data = await response.json();
        
        currentPuzzle = data.puzzle;
        currentSolution = data.solution;
        currentDifficulty = difficulty;
        currentSeed = seed;
        hintsLeft = 3;
        
        renderBoard(data.puzzle);
        document.getElementById('sudoku-board').style.display = 'block';
        document.getElementById('game-controls').style.display = 'block';
        
        alert('๊ณต์œ ๋ฐ›์€ ํผ์ฆ์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค!');
    } else {
        // ๊ธฐ์กด ์ž๋™ ๋ณต์› ๋กœ์ง
        if (loadProgress()) {
            if (confirm('์ด์ „์— ํ’€๋˜ ํผ์ฆ์„ ์ด์–ด์„œ ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?')) {
                startTimer();
            } else {
                localStorage.removeItem('sudoku_progress');
                location.reload();
            }
        }
    }
});
```

4. PHP์—์„œ ์‹œ๋“œ ๊ธฐ๋ฐ˜ ํผ์ฆ ์ƒ์„ฑ ์ง€์› (AJAX ์ฒ˜๋ฆฌ ์„น์…˜ ์ˆ˜์ •):

```php
// AJAX: ํผ์ฆ ์ƒ์„ฑ ์š”์ฒญ ์ฒ˜๋ฆฌ
if (isset($_POST['action']) && $_POST['action'] === 'generate') {
    $difficulty = $_POST['difficulty'] ?? 'medium';
    
    if (isset($_POST['seed'])) {
        // ๊ณต์œ ๋ฐ›์€ ํผ์ฆ (์‹œ๋“œ๊ฐ’ ์ง์ ‘ ์‚ฌ์šฉ)
        $seed = (int)$_POST['seed'];
    } elseif (isset($_POST['daily'])) {
        // ์ผ์ผ ์ฑŒ๋ฆฐ์ง€ (์˜ค๋Š˜ ๋‚ ์งœ ๊ธฐ๋ฐ˜)
        $seed = strtotime(date('Y-m-d'));
    } else {
        // ์ผ๋ฐ˜ ํผ์ฆ (ํ˜„์žฌ ์‹œ๊ฐ ๊ธฐ๋ฐ˜)
        $seed = time();
    }
    
    $solution = generateSudoku($seed);
    $puzzle = removeCells($solution, $difficulty);
    
    header('Content-Type: application/json');
    echo json_encode([
        'puzzle' => $puzzle,
        'solution' => $solution,
        'seed' => $seed
    ]);
    exit;
}
```

**ํ™•์ธ ์‚ฌํ•ญ:**
- ํผ์ฆ ํ’€์ด ์ค‘ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ ์ง„ํ–‰ ์ƒํ™ฉ์ด ๋ณต์›๋˜๋Š”๊ฐ€?
- ๊ณต์œ  ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ URL์ด ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌ๋˜๋Š”๊ฐ€?
- ๊ณต์œ ๋ฐ›์€ ๋งํฌ๋กœ ์ ‘์† ์‹œ ๋™์ผํ•œ ํผ์ฆ์ด ๋กœ๋“œ๋˜๋Š”๊ฐ€?
- 24์‹œ๊ฐ„ ์ด์ƒ ์ง€๋‚œ ์ž๋™ ์ €์žฅ ๋ฐ์ดํ„ฐ๋Š” ๋ฌด์‹œ๋˜๋Š”๊ฐ€?

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

8
Step 8
**Step 8: ๋ฐ˜์‘ํ˜• ๋””์ž์ธ ๋ฐ ํ„ฐ์น˜ ์ตœ์ ํ™”**

1. CSS์— ๋ฐ˜์‘ํ˜• ๋ฏธ๋””์–ด ์ฟผ๋ฆฌ ์ถ”๊ฐ€:

```css
/* ํƒœ๋ธ”๋ฆฟ ์ดํ•˜ */
@media (max-width: 768px) {
    .sudoku-grid {
        grid-template-columns: repeat(9, 40px);
    }
    .sudoku-cell {
        width: 40px;
        height: 40px;
        font-size: 16px;
    }
    .difficulty-selector {
        flex-direction: column;
    }
    .diff-btn {
        width: 100%;
    }
    #game-controls > div {
        flex-wrap: wrap;
    }
    #game-controls button,
    #game-controls a {
        flex: 1 1 45%;
    }
    .stats-grid {
        grid-template-columns: 1fr;
    }
}

/* ๋ชจ๋ฐ”์ผ */
@media (max-width: 480px) {
    .sudoku-grid {
        grid-template-columns: repeat(9, 35px);
    }
    .sudoku-cell {
        width: 35px;
        height: 35px;
        font-size: 14px;
    }
    .sudoku-header h1 {
        font-size: 24px;
    }
    .modal-content {
        padding: 24px;
        margin: 0 16px;
    }
}
```

2. ํ„ฐ์น˜ ๋””๋ฐ”์ด์Šค ์ตœ์ ํ™”:

```javascript
// ํ„ฐ์น˜ ๋””๋ฐ”์ด์Šค ๊ฐ์ง€
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;

if (isTouchDevice) {
    // ์ˆซ์ž ์ž…๋ ฅ ํŒจ๋“œ ์ถ”๊ฐ€
    const numpad = document.createElement('div');
    numpad.className = 'numpad';
    numpad.innerHTML = `
        <div class="numpad-title">์ˆซ์ž ์„ ํƒ</div>
        <div class="numpad-grid">
            ${[1,2,3,4,5,6,7,8,9].map(n => `<button class="numpad-btn" data-num="${n}">${n}</button>`).join('')}
            <button class="numpad-btn" data-num="0" style="grid-column: span 3;">์ง€์šฐ๊ธฐ</button>
        </div>
    `;
    document.getElementById('sudoku-board').appendChild(numpad);
    
    // ์…€ ์„ ํƒ ์‹œ ํ™œ์„ฑํ™”
    let selectedCell = null;
    document.addEventListener('click', function(e) {
        if (e.target.classList.contains('sudoku-cell') && !e.target.classList.contains('given')) {
            if (selectedCell) selectedCell.classList.remove('selected');
            selectedCell = e.target;
            selectedCell.classList.add('selected');
            numpad.style.display = 'block';
        }
    });
    
    // ์ˆซ์ž ํŒจ๋“œ ํด๋ฆญ
    document.querySelectorAll('.numpad-btn').forEach(btn => {
        btn.addEventListener('click', function() {
            if (!selectedCell) return;
            
            const num = this.dataset.num;
            if (num === '0') {
                selectedCell.value = '';
            } else {
                selectedCell.value = num;
            }
            
            // ๊ฒ€์ฆ ํŠธ๋ฆฌ๊ฑฐ
            selectedCell.dispatchEvent(new Event('input'));
        });
    });
}
```

3. ์ˆซ์ž ํŒจ๋“œ CSS ์ถ”๊ฐ€:

```css
.numpad {
    display: none;
    margin-top: 24px;
    padding: 16px;
    background: var(--m-surface);
    border-radius: var(--m-radius-lg);
    box-shadow: var(--m-shadow);
}
.numpad-title {
    font-size: 16px;
    font-weight: 700;
    margin-bottom: 12px;
    text-align: center;
}
.numpad-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 8px;
}
.numpad-btn {
    padding: 16px;
    background: var(--m-primary);
    color: white;
    border: none;
    border-radius: var(--m-radius-lg);
    font-size: 18px;
    font-weight: 600;
    cursor: pointer;
}
.numpad-btn:active {
    transform: scale(0.95);
}
.sudoku-cell.selected {
    outline: 3px solid var(--m-primary);
    background: rgba(59, 130, 246, 0.1);
}
```

4. ๊ฐ€๋กœ ์Šคํฌ๋กค ๋ฐฉ์ง€:

```css
body {
    overflow-x: hidden;
}
.m-container {
    max-width: 100%;
    overflow-x: auto;
}
```

**ํ™•์ธ ์‚ฌํ•ญ:**
- ๋ชจ๋ฐ”์ผ์—์„œ ๋ณด๋“œ๊ฐ€ ํ™”๋ฉด์— ๋งž๊ฒŒ ์ถ•์†Œ๋˜๋Š”๊ฐ€?
- ํ„ฐ์น˜ ๋””๋ฐ”์ด์Šค์—์„œ ์ˆซ์ž ํŒจ๋“œ๊ฐ€ ํ‘œ์‹œ๋˜๋Š”๊ฐ€?
- ์…€ ์„ ํƒ ์‹œ ํŒŒ๋ž€์ƒ‰ ํ…Œ๋‘๋ฆฌ๊ฐ€ ํ‘œ์‹œ๋˜๋Š”๊ฐ€?
- ๊ฐ€๋กœ ์Šคํฌ๋กค์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๊ฐ€?

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

9
Step 9
**Step 9: ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐ ์บ์‹ฑ ์ „๋žต**

1. ์ผ์ผ ์ฑŒ๋ฆฐ์ง€ ํผ์ฆ ์„œ๋ฒ„ ๋ฉ”๋ชจ๋ฆฌ ์บ์‹ฑ ๊ตฌํ˜„ (index.php ์ƒ๋‹จ์—):

```php
// ์ผ์ผ ์ฑŒ๋ฆฐ์ง€ ์บ์‹ฑ (APCu ์‚ฌ์šฉ)
function getDailyChallenge($difficulty) {
    $cache_key = 'sudoku_daily_'.date('Y-m-d').'_'.$difficulty;
    
    if (function_exists('apcu_fetch')) {
        $cached = apcu_fetch($cache_key, $success);
        if ($success) return $cached;
    }
    
    // ์บ์‹œ ๋ฏธ์Šค ์‹œ ์ƒ์„ฑ
    $seed = strtotime(date('Y-m-d'));
    $solution = generateSudoku($seed);
    $puzzle = removeCells($solution, $difficulty);
    
    $data = [
        'puzzle' => $puzzle,
        'solution' => $solution,
        'seed' => $seed
    ];
    
    if (function_exists('apcu_store')) {
        apcu_store($cache_key, $data, 86400); // 24์‹œ๊ฐ„ ์บ์‹ฑ
    }
    
    return $data;
}

// AJAX ์ฒ˜๋ฆฌ ๋ถ€๋ถ„ ์ˆ˜์ •
if (isset($_POST['action']) && $_POST['action'] === 'generate') {
    $difficulty = $_POST['difficulty'] ?? 'medium';
    
    if (isset($_POST['daily'])) {
        // ์ผ์ผ ์ฑŒ๋ฆฐ์ง€๋Š” ์บ์‹œ์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ
        $data = getDailyChallenge($difficulty);
    } else {
        // ์ผ๋ฐ˜ ํผ์ฆ์€ ๋งค๋ฒˆ ์ƒˆ๋กœ ์ƒ์„ฑ
        if (isset($_POST['seed'])) {
            $seed = (int)$_POST['seed'];
        } else {
            $seed = time();
        }
        
        $solution = generateSudoku($seed);
        $puzzle = removeCells($solution, $difficulty);
        
        $data = [
            'puzzle' => $puzzle,
            'solution' => $solution,
            'seed' => $seed
        ];
    }
    
    header('Content-Type: application/json');
    echo json_encode($data);
    exit;
}
```

2. DB ์ฟผ๋ฆฌ ์ตœ์ ํ™” (์ธ๋ฑ์Šค ์ถ”๊ฐ€):

```php
// ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์‹œ ๋ณตํ•ฉ ์ธ๋ฑ์Šค ์ถ”๊ฐ€
$pdo->exec("
    CREATE INDEX IF NOT EXISTS idx_seed_difficulty 
    ON puzzle_history(seed, difficulty)
");
$pdo->exec("
    CREATE INDEX IF NOT EXISTS idx_user_completed 
    ON puzzle_history(user_id, completed_at DESC)
");
```

3. ํ”„๋ก ํŠธ์—”๋“œ ๋””๋ฐ”์šด์‹ฑ (์ž๋™ ์ €์žฅ ์ตœ์ ํ™”):

```javascript
let saveTimeout = null;

function debouncedSave() {
    clearTimeout(saveTimeout);
    saveTimeout = setTimeout(() => {
        saveProgress();
    }, 500); // 500ms ๋””๋ฐ”์šด์‹ฑ
}

// ๊ธฐ์กด ์ž๋™ ์ €์žฅ ์ด๋ฒคํŠธ ์ˆ˜์ •
document.addEventListener('input', function(e) {
    if (e.target.classList.contains('sudoku-cell')) {
        debouncedSave();
    }
});
```

4. ์ด๋ฏธ์ง€/๋ฆฌ์†Œ์Šค ์ตœ์ ํ™” (CSS์— ํ”„๋ฆฌ๋กœ๋”ฉ ํžŒํŠธ ์ถ”๊ฐ€):

```html
<!-- head.sub.php ๋˜๋Š” _head.inc.php์— ์ถ”๊ฐ€ (์ด๋ฏธ ์žˆ๋‹ค๋ฉด ์ƒ๋žต) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
```

5. ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๊ฐ•ํ™”:

```javascript
// AJAX ์š”์ฒญ์— ์—๋Ÿฌ ํ•ธ๋“ค๋ง ์ถ”๊ฐ€
async function generatePuzzle(difficulty, options = {}) {
    try {
        const formData = new FormData();
        formData.append('action', 'generate');
        formData.append('difficulty', difficulty);
        
        if (options.daily) formData.append('daily', '1');
        if (options.seed) formData.append('seed', options.seed);
        
        const response = await fetch(window.location.href.split('?')[0], {
            method: 'POST',
            body: formData
        });
        
        if (!response.ok) {
            throw new Error('์„œ๋ฒ„ ์‘๋‹ต ์˜ค๋ฅ˜');
        }
        
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('ํผ์ฆ ์ƒ์„ฑ ์‹คํŒจ:', error);
        alert('ํผ์ฆ์„ ์ƒ์„ฑํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.');
        return null;
    }
}

// ๊ธฐ์กด ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋“ค์„ ์ด ํ•จ์ˆ˜ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •
document.getElementById('daily-btn').addEventListener('click', async function() {
    const data = await generatePuzzle('medium', { daily: true });
    if (data) {
        currentPuzzle = data.puzzle;
        currentSolution = data.solution;
        currentDifficulty = 'medium';
        currentSeed = data.seed;
        hintsLeft = 3;
        
        renderBoard(data.puzzle);
        document.getElementById('sudoku-board').style.display = 'block';
        document.getElementById('game-controls').style.display = 'block';
    }
});
```

6. PHP ์—๋Ÿฌ ๋กœ๊น…:

```php
// AJAX ์ฒ˜๋ฆฌ ๋ถ€๋ถ„์— try-catch ์ถ”๊ฐ€
try {
    if (isset($_POST['action']) && $_POST['action'] === 'generate') {
        // ... ๊ธฐ์กด ์ฝ”๋“œ ...
    } elseif (isset($_POST['action']) && $_POST['action'] === 'save_result') {
        // ... ๊ธฐ์กด ์ฝ”๋“œ ...
    }
} catch (Exception $e) {
    error_log('Sudoku Error: ' . $e->getMessage());
    header('Content-Type: application/json');
    http_response_code(500);
    echo json_encode(['error' => '์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.']);
    exit;
}
```

**ํ™•์ธ ์‚ฌํ•ญ:**
- ์ผ์ผ ์ฑŒ๋ฆฐ์ง€๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์š”์ฒญํ•ด๋„ ๋™์ผํ•œ ํผ์ฆ์ด ๋ฐ˜ํ™˜๋˜๋Š”๊ฐ€? (์บ์‹ฑ ํ™•์ธ)
- ์ž๋™ ์ €์žฅ์ด ์—ฐ์† ์ž…๋ ฅ ์‹œ 500ms๋งˆ๋‹ค ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋˜๋Š”๊ฐ€? (๋””๋ฐ”์šด์‹ฑ ํ™•์ธ)
- ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ์‹œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ ์ ˆํ•œ ๋ฉ”์‹œ์ง€๊ฐ€ ํ‘œ์‹œ๋˜๋Š”๊ฐ€?
- DB ์ธ๋ฑ์Šค๊ฐ€ ์ •์ƒ ์ƒ์„ฑ๋˜์—ˆ๋Š”๊ฐ€?

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์ผ์ผ ์ฑŒ๋ฆฐ์ง€ ํผ์ฆ์ด ์„œ๋ฒ„ ๋ฉ”๋ชจ๋ฆฌ(APCu)์— ์บ์‹ฑ๋˜์–ด ์ค‘๋ณต ์ƒ์„ฑ์„ ๋ฐฉ์ง€ํ•˜๋ฉฐ, ์ž๋™ ์ €์žฅ์ด ๋””๋ฐ”์šด์‹ฑ๋˜์–ด ์„ฑ๋Šฅ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค. ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์นœ์ ˆํ•œ ๋ฉ”์‹œ์ง€๊ฐ€ ํ‘œ์‹œ๋˜๊ณ , DB ์ฟผ๋ฆฌ๊ฐ€ ์ธ๋ฑ์Šค๋กœ ์ตœ์ ํ™”๋ฉ๋‹ˆ๋‹ค.

10
Step 10
**Step 10: ์ตœ์ข… ํ…Œ์ŠคํŠธ ๋ฐ ๋ณด์•ˆ ๊ฐ•ํ™”**

1. XSS ๋ฐฉ์ง€ ๊ฐ•ํ™” (๋ชจ๋“  ์ถœ๋ ฅ์— ์ด์Šค์ผ€์ดํ”„ ์ ์šฉ ํ™•์ธ):

```php
// history.php์˜ ์‚ฌ์šฉ์ž ID ์ถœ๋ ฅ ๋ถ€๋ถ„ ์ˆ˜์ •
<td><?php echo htmlspecialchars($row['user_id'], ENT_QUOTES, 'UTF-8'); ?></td>

// index.php์˜ ๋ชจ๋“  ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์ฒ˜๋ฆฌ ๋ถ€๋ถ„์— ์ด์Šค์ผ€์ดํ”„ ์ ์šฉ
$difficulty = htmlspecialchars($_POST['difficulty'] ?? 'medium', ENT_QUOTES, 'UTF-8');
```

2. SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ ์žฌํ™•์ธ (๋ชจ๋“  ์ฟผ๋ฆฌ๊ฐ€ prepared statement ์‚ฌ์šฉ ํ™•์ธ):

```php
// ์˜ˆ์‹œ: ๋ชจ๋“  DB ์ฟผ๋ฆฌ๊ฐ€ ๋‹ค์Œ ํ˜•ํƒœ์ธ์ง€ ํ™•์ธ
$stmt = $pdo->prepare("SELECT * FROM puzzle_history WHERE user_id = ?");
$stmt->execute([$user_id]);
```

3. CSRF ๋ฐฉ์ง€ (g5se ๋‚ด์žฅ ํ† ํฐ ํ™œ์šฉ):

```php
// AJAX ์š”์ฒญ ์ฒ˜๋ฆฌ ์ „์— ํ† ํฐ ๊ฒ€์ฆ ์ถ”๊ฐ€
if (isset($_POST['action'])) {
    // g5se๋Š” ์„ธ์…˜ ๊ธฐ๋ฐ˜ ์ธ์ฆ์„ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ์ถ”๊ฐ€ CSRF ํ† ํฐ์€ ์„ ํƒ์‚ฌํ•ญ
    // ํ•„์š” ์‹œ $_SESSION['ss_token'] ๊ฒ€์ฆ ๋กœ์ง ์ถ”๊ฐ€
}
```

4. ์ž…๋ ฅ ๊ฒ€์ฆ ๊ฐ•ํ™”:

```php
// ๋‚œ์ด๋„ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ฒ€์ฆ
$allowed_difficulties = ['easy', 'medium', 'hard', 'extreme'];
$difficulty = $_POST['difficulty'] ?? 'medium';
if (!in_array($difficulty, $allowed_difficulties)) {
    $difficulty = 'medium';
}

// ์‹œ๋“œ๊ฐ’ ๋ฒ”์œ„ ๊ฒ€์ฆ
if (isset($_POST['seed'])) {
    $seed = (int)$_POST['seed'];
    if ($seed < 0 || $seed > PHP_INT_MAX) {
        $seed = time();
    }
}
```

5. ์ตœ์ข… ํ…Œ์ŠคํŠธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ์ž‘์„ฑ:

```markdown
## ํ…Œ์ŠคํŠธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

### ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ
- [ ] ๋‚œ์ด๋„๋ณ„ ํผ์ฆ ์ƒ์„ฑ (์‰ฌ์›€/๋ณดํ†ต/์–ด๋ ค์›€/์ต์ŠคํŠธ๋ฆผ)
- [ ] ์‹ค์‹œ๊ฐ„ ์ž…๋ ฅ ๊ฒ€์ฆ (ํ–‰/์—ด/๋ฐ•์Šค ๊ทœ์น™)
- [ ] ํƒ€์ด๋จธ ์‹œ์ž‘/์ •์ง€
- [ ] ํžŒํŠธ ๊ธฐ๋Šฅ (3ํšŒ ์ œํ•œ)
- [ ] ์ •๋‹ต ํ™•์ธ ๋ฐ ์™„๋ฃŒ ๋ชจ๋‹ฌ
- [ ] ์™„๋ฃŒ ๊ธฐ๋ก DB ์ €์žฅ
- [ ] ํžˆ์Šคํ† ๋ฆฌ ํŽ˜์ด์ง€ ํ†ต๊ณ„ ํ‘œ์‹œ
- [ ] ์ผ์ผ ์ฑŒ๋ฆฐ์ง€ ๋ชจ๋“œ
- [ ] ๋ฉ”๋ชจ ๋ชจ๋“œ ํ† ๊ธ€
- [ ] ์ž๋™ ์ €์žฅ/๋ณต์›
- [ ] ํผ์ฆ ๊ณต์œ  (URL ํŒŒ๋ผ๋ฏธํ„ฐ)

### ๋ฐ˜์‘ํ˜•/UX ํ…Œ์ŠคํŠธ
- [ ] ๋ฐ์Šคํฌํ†ฑ (1920x1080)
- [ ] ํƒœ๋ธ”๋ฆฟ (768x1024)
- [ ] ๋ชจ๋ฐ”์ผ (375x667)
- [ ] ํ„ฐ์น˜ ๋””๋ฐ”์ด์Šค ์ˆซ์ž ํŒจ๋“œ
- [ ] ๋‹คํฌ๋ชจ๋“œ ์ „ํ™˜

### ๋ณด์•ˆ ํ…Œ์ŠคํŠธ
- [ ] XSS ๋ฐฉ์ง€ (์‚ฌ์šฉ์ž ์ž…๋ ฅ ์ด์Šค์ผ€์ดํ”„)
- [ ] SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ (prepared statement)
- [ ] ์ž…๋ ฅ ๊ฒ€์ฆ (๋‚œ์ด๋„/์‹œ๋“œ๊ฐ’)

### ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ
- [ ] ์ผ์ผ ์ฑŒ๋ฆฐ์ง€ ์บ์‹ฑ ๋™์ž‘
- [ ] ์ž๋™ ์ €์žฅ ๋””๋ฐ”์šด์‹ฑ
- [ ] DB ์ธ๋ฑ์Šค ์ ์šฉ ํ™•์ธ
- [ ] ํŽ˜์ด์ง€ ๋กœ๋“œ ์†๋„ (3์ดˆ ์ด๋‚ด)
```

6. README.md ์ž‘์„ฑ:

```markdown
# ๐Ÿงฉ g5se ์˜ค๋Š˜์˜ ์Šค๋„์ฟ  ํผ์ฆ ์ƒ์„ฑ๊ธฐ

๋งค์ผ ์ƒˆ๋กœ์šด ์Šค๋„์ฟ  ํผ์ฆ์„ ํ’€ ์ˆ˜ ์žˆ๋Š” ์›น ๊ธฐ๋ฐ˜ ํผ์ฆ ํ”Œ๋žซํผ

## ์ฃผ์š” ๊ธฐ๋Šฅ

- ๋‚œ์ด๋„๋ณ„ ํผ์ฆ ์ƒ์„ฑ (์‰ฌ์›€/๋ณดํ†ต/์–ด๋ ค์›€/์ต์ŠคํŠธ๋ฆผ)
- ์‹ค์‹œ๊ฐ„ ์ž…๋ ฅ ๊ฒ€์ฆ ๋ฐ ์˜ค๋ฅ˜ ํ‘œ์‹œ
- ํžŒํŠธ ์‹œ์Šคํ…œ (3ํšŒ ์ œํ•œ)
- ํ’€์ด ํƒ€์ด๋จธ ๋ฐ ๊ธฐ๋ก ์ €์žฅ
- ์ผ์ผ ์ฑŒ๋ฆฐ์ง€ ๋ชจ๋“œ (์ „์ฒด ์ˆœ์œ„ํ‘œ)
- ๋ฉ”๋ชจ ๋ชจ๋“œ (ํ›„๋ณด ์ˆซ์ž ๊ธฐ๋ก)
- ์ž๋™ ์ €์žฅ/๋ณต์› (localStorage)
- ํผ์ฆ ๊ณต์œ  (URL ๋งํฌ)

## ๊ธฐ์ˆ  ์Šคํƒ

- ํ”„๋ ˆ์ž„์›Œํฌ: ๊ทธ๋ˆ„๋ณด๋“œ5 SE
- DB: SQLite
- ํ”„๋ก ํŠธ์—”๋“œ: Vanilla JavaScript + CSS Grid
- ์•Œ๊ณ ๋ฆฌ์ฆ˜: ๋ฐฑํŠธ๋ž˜ํ‚น (์Šค๋„์ฟ  ์ƒ์„ฑ)

## ์„ค์น˜ ๋ฐฉ๋ฒ•

1. g5se ์„ค์น˜ (https://github.com/gnuboard/g5se)
2. `app/modules/sudoku/` ํด๋”์— ํŒŒ์ผ ๋ณต์‚ฌ
3. `/sudoku` ๊ฒฝ๋กœ๋กœ ์ ‘์†

## ๋ผ์ด์„ผ์Šค

MIT License
```

**์ตœ์ข… ํ™•์ธ ์‚ฌํ•ญ:**
- ๋ชจ๋“  ๊ธฐ๋Šฅ์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€?
- ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ์—†๋Š”๊ฐ€?
- ๋‹ค์–‘ํ•œ ๋””๋ฐ”์ด์Šค์—์„œ ํ…Œ์ŠคํŠธํ–ˆ๋Š”๊ฐ€?
- ์—๋Ÿฌ ํ•ธ๋“ค๋ง์ด ์ ์ ˆํ•œ๊ฐ€?
- ์ฝ”๋“œ์— ์ฃผ์„์ด ์ถฉ๋ถ„ํ•œ๊ฐ€?

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋ชจ๋“  ๊ธฐ๋Šฅ์ด ์•ˆ์ •์ ์œผ๋กœ ๋™์ž‘ํ•˜๋ฉฐ, XSS/SQL ์ธ์ ์…˜ ๋“ฑ ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ์—†์Šต๋‹ˆ๋‹ค. ๋ฐ์Šคํฌํ†ฑ/ํƒœ๋ธ”๋ฆฟ/๋ชจ๋ฐ”์ผ์—์„œ ๋ฐ˜์‘ํ˜•์œผ๋กœ ์ž‘๋™ํ•˜๊ณ , ๋‹คํฌ๋ชจ๋“œ๋„ ์ •์ƒ ์ง€์›๋ฉ๋‹ˆ๋‹ค. ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ๋ฉ”์‹œ์ง€๊ฐ€ ํ‘œ์‹œ๋˜๋ฉฐ, README.md์— ํ”„๋กœ์ ํŠธ ์ •๋ณด๊ฐ€ ๋ช…ํ™•ํžˆ ๊ธฐ์ˆ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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

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

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

๋Œ“๊ธ€ (0)

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