๐ŸŽฏ g5se ์‹ค์‹œ๊ฐ„ ์Šคํฌ์ธ  ํ† ๋„ˆ๋จผํŠธ ๋ธŒ๋ž˜ํ‚ท ์ƒ์„ฑ๊ธฐ - ๋Œ€์ง„ํ‘œ ์ž๋™ ์ƒ์„ฑ & ๊ฒฝ๊ธฐ ๊ฒฐ๊ณผ ์ž…๋ ฅ - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
๊ณ ๊ธ‰ PHP g5se AI: Claude

๐ŸŽฏ g5se ์‹ค์‹œ๊ฐ„ ์Šคํฌ์ธ  ํ† ๋„ˆ๋จผํŠธ ๋ธŒ๋ž˜ํ‚ท ์ƒ์„ฑ๊ธฐ - ๋Œ€์ง„ํ‘œ ์ž๋™ ์ƒ์„ฑ & ๊ฒฝ๊ธฐ ๊ฒฐ๊ณผ ์ž…๋ ฅ

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

๋ฌธ์ œ ์„ค๋ช…

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

์Šคํฌ์ธ  ๋™ํ˜ธํšŒ, e์Šคํฌ์ธ  ๋Œ€ํšŒ, ์‚ฌ๋‚ด ๊ฒŒ์ž„ ํ† ๋„ˆ๋จผํŠธ ๋“ฑ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์‹ค์‹œ๊ฐ„ ํ† ๋„ˆ๋จผํŠธ ๋Œ€์ง„ํ‘œ ์ƒ์„ฑ ๋ฐ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์ž…๋‹ˆ๋‹ค. ์ฐธ๊ฐ€์ž ๋ช…๋‹จ์„ ์ž…๋ ฅํ•˜๋ฉด ์ž๋™์œผ๋กœ ์‹ฑ๊ธ€ ์—˜๋ฆฌ๋ฏธ๋„ค์ด์…˜(๋‹จํŒ ํ† ๋„ˆ๋จผํŠธ) ๋˜๋Š” ๋”๋ธ” ์—˜๋ฆฌ๋ฏธ๋„ค์ด์…˜(ํŒจ์ž๋ถ€ํ™œ์ „) ๋Œ€์ง„ํ‘œ๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ๊ฐ ๊ฒฝ๊ธฐ ๊ฒฐ๊ณผ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ž…๋ ฅํ•˜์—ฌ ๋‹ค์Œ ๋ผ์šด๋“œ ์ง„์ถœ์ž๋ฅผ ์ž๋™ ๋ฐฐ์ •ํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž๋Š” ๋Œ€ํšŒ๋ฅผ ์ƒ์„ฑยท์ˆ˜์ •ยท์‚ญ์ œํ•  ์ˆ˜ ์žˆ๊ณ , ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž๋Š” ๊ณต๊ฐœ๋œ ๋Œ€์ง„ํ‘œ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

  1. ๋Œ€ํšŒ ์ƒ์„ฑ ํผ (๊ด€๋ฆฌ์ž ์ „์šฉ)

    • ๋Œ€ํšŒ๋ช…, ์ข…๋ชฉ, ์ฐธ๊ฐ€์ž ๋ช…๋‹จ(ํ…์ŠคํŠธ ์ž…๋ ฅ ๋˜๋Š” CSV ์—…๋กœ๋“œ), ํ† ๋„ˆ๋จผํŠธ ํ˜•์‹(์‹ฑ๊ธ€/๋”๋ธ” ์—˜๋ฆฌ๋ฏธ๋„ค์ด์…˜), ์‹œ๋“œ ๋ฐฐ์ • ๋ฐฉ์‹(๋žœ๋ค/์ˆ˜๋™) ์ž…๋ ฅ
    • ์ฐธ๊ฐ€์ž ์ˆ˜๊ฐ€ 2์˜ ๊ฑฐ๋“ญ์ œ๊ณฑ์ด ์•„๋‹ ๊ฒฝ์šฐ ์ž๋™์œผ๋กœ ๋ถ€์ „์Šน(Bye) ๋ฐฐ์ •
    • ์ƒ์„ฑ ์‹œ ๊ณ ์œ  URL ๋ฐœ๊ธ‰ (์˜ˆ: /modules/tournament/view/abc123)
  2. ๋Œ€์ง„ํ‘œ ์‹œ๊ฐํ™”

    • SVG ๊ธฐ๋ฐ˜ ํŠธ๋ฆฌ ๊ตฌ์กฐ๋กœ ํ† ๋„ˆ๋จผํŠธ ๋ธŒ๋ž˜ํ‚ท ๋ Œ๋”๋ง (๋ผ์šด๋“œ๋ณ„ ๊ฐ€๋กœ ๋ ˆ์ด์–ด, ๋งค์น˜ ์„ธ๋กœ ์—ฐ๊ฒฐ์„ )
    • ๊ฐ ๋งค์น˜ ์นด๋“œ: ์ฐธ๊ฐ€์ž๋ช…, ์ ์ˆ˜ ์ž…๋ ฅ๋ž€, ์Šน์ž ํ•˜์ด๋ผ์ดํŠธ
    • ๋ชจ๋ฐ”์ผ์—์„œ๋„ ๊ฐ€๋กœ ์Šคํฌ๋กค๋กœ ์ „์ฒด ๋Œ€์ง„ํ‘œ ํ™•์ธ ๊ฐ€๋Šฅ
  3. ๊ฒฝ๊ธฐ ๊ฒฐ๊ณผ ์ž…๋ ฅ (๊ด€๋ฆฌ์ž ์ „์šฉ)

    • ๊ฐ ๋งค์น˜ ์นด๋“œ์— '๊ฒฐ๊ณผ ์ž…๋ ฅ' ๋ฒ„ํŠผ โ†’ ๋ชจ๋‹ฌ ํŒ์—…์—์„œ ์–‘์ชฝ ์ฐธ๊ฐ€์ž ์ ์ˆ˜ ์ž…๋ ฅ
    • ์ œ์ถœ ์‹œ AJAX๋กœ ์„œ๋ฒ„ ์ „์†ก, ์Šน์ž ์ž๋™ ํŒ์ • ๋ฐ ๋‹ค์Œ ๋ผ์šด๋“œ ์ง„์ถœ์ž ๋ฐฐ์ •
    • ์ด๋ฏธ ๊ฒฐ๊ณผ๊ฐ€ ์ž…๋ ฅ๋œ ๋งค์น˜๋Š” ์ ์ˆ˜ ํ‘œ์‹œ & ์Šน์ž ๊ตต์€ ๊ธ€์”จ + ๋ฐฐ๊ฒฝ ํ•˜์ด๋ผ์ดํŠธ
  4. ์‹ค์‹œ๊ฐ„ ๋Œ€์ง„ํ‘œ ์—…๋ฐ์ดํŠธ

    • ๊ฒฐ๊ณผ ์ž…๋ ฅ ํ›„ ์ƒˆ๋กœ๊ณ ์นจ ์—†์ด ๋Œ€์ง„ํ‘œ DOM ์—…๋ฐ์ดํŠธ (vanilla JS๋กœ ๋ถ€๋ถ„ ๋ Œ๋”๋ง)
    • ๋‹ค์Œ ๋ผ์šด๋“œ ๋งค์น˜๊ฐ€ ์ž๋™ ์ƒ์„ฑ๋˜๊ณ  ์ง„์ถœ์ž๋ช… ํ‘œ์‹œ
  5. ๋Œ€ํšŒ ๋ชฉ๋ก ํŽ˜์ด์ง€

    • ์ง„ํ–‰ ์ค‘/์™„๋ฃŒ ๋Œ€ํšŒ ํ•„ํ„ฐ๋ง
    • ๊ฐ ๋Œ€ํšŒ ์นด๋“œ: ๋Œ€ํšŒ๋ช…, ์ข…๋ชฉ, ์ฐธ๊ฐ€์ž ์ˆ˜, ํ˜„์žฌ ๋ผ์šด๋“œ, ์ƒํƒœ(์ง„ํ–‰ ์ค‘/์™„๋ฃŒ), ์กฐํšŒ ๋ฒ„ํŠผ
  6. ๊ด€๋ฆฌ์ž ์ธ์ฆ

    • g5se ํšŒ์› ์‹œ์Šคํ…œ ์—ฐ๋™: ๋กœ๊ทธ์ธํ•œ ํšŒ์›๋งŒ ๋Œ€ํšŒ ์ƒ์„ฑยท์ˆ˜์ •ยท์‚ญ์ œ ๊ฐ€๋Šฅ
    • ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋Š” ๋Œ€์ง„ํ‘œ ์กฐํšŒ๋งŒ ๊ฐ€๋Šฅ (์ฝ๊ธฐ ์ „์šฉ)

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

  • ๋”๋ธ” ์—˜๋ฆฌ๋ฏธ๋„ค์ด์…˜ ์ง€์›: ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท ์ž๋™ ์ƒ์„ฑ, ์Šน์ž/ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท ๋™์‹œ ๋ Œ๋”๋ง
  • ์‹œ๋“œ ๋ฐฐ์ • UI: ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์œผ๋กœ ์ฐธ๊ฐ€์ž ์ˆœ์„œ ์ˆ˜๋™ ์กฐ์ •
  • ๊ฒฝ๊ธฐ ์ผ์ • ์ž…๋ ฅ: ๊ฐ ๋งค์น˜์— ๋‚ ์งœยท์‹œ๊ฐ„ ์„ค์ •, ์บ˜๋ฆฐ๋” ๋ทฐ ์ œ๊ณต
  • ์Šน๋ถ€์ฐจ๊ธฐ/์—ฐ์žฅ์ „ ๊ธฐ๋ก: ๋™์  ์‹œ ์ถ”๊ฐ€ ํ•„๋“œ ์ž…๋ ฅ
  • ๋Œ€์ง„ํ‘œ PDF ๋‚ด๋ณด๋‚ด๊ธฐ: ์„œ๋ฒ„์—์„œ SVG โ†’ PDF ๋ณ€ํ™˜ (DomPDF ๋“ฑ ํ™œ์šฉ)
  • ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ: ์ƒˆ ๋ผ์šด๋“œ ์‹œ์ž‘ ์‹œ ์ฐธ๊ฐ€์ž์—๊ฒŒ ๋ธŒ๋ผ์šฐ์ € ์•Œ๋ฆผ (Web Notification API)
  • ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ: ๊ฐ€์žฅ ๋งŽ์€ ๋“์ ์ž, ์ตœ๋‹ค ์—ฐ์Šน ๊ธฐ๋ก ๋“ฑ ์ž๋™ ์ง‘๊ณ„
  • ๊ณต๊ฐœ URL ๊ณต์œ : ๋Œ€์ง„ํ‘œ QR ์ฝ”๋“œ ์ƒ์„ฑ, SNS ๊ณต์œ  ๋ฒ„ํŠผ

๐ŸŽจ ๋””์ž์ธ ์š”๊ตฌ์‚ฌํ•ญ

  • Modern shell ์ ์šฉ (๋‹คํฌ๋ชจ๋“œ ์ž๋™ ๋Œ€์‘)
  • ๋””์ž์ธ ํ† ํฐ ์‚ฌ์šฉ: var(--m-surface), var(--m-text), var(--m-shadow) ๋“ฑ์œผ๋กœ ๋ฐฐ๊ฒฝ/ํ…์ŠคํŠธ/๊ทธ๋ฆผ์ž ์ฒ˜๋ฆฌ
  • ๋ธŒ๋ž˜ํ‚ท ์ƒ‰์ƒ: ์Šน์ž ํ•˜์ด๋ผ์ดํŠธ๋Š” #10b981(์—๋ฉ”๋ž„๋“œ), ํŒจ์ž๋Š” #ef4444(๋ ˆ๋“œ), ์ง„ํ–‰ ์ค‘ ๋งค์น˜๋Š” #3b82f6(๋ธ”๋ฃจ)
  • ๋ชจ๋ฐ”์ผ ์ตœ์ ํ™”: ๋Œ€์ง„ํ‘œ๋Š” ๊ฐ€๋กœ ์Šคํฌ๋กค, ๋งค์น˜ ์นด๋“œ๋Š” ํ„ฐ์น˜ ์นœํ™”์  ํฌ๊ธฐ(์ตœ์†Œ 44px ๋†’์ด)
  • ์• ๋‹ˆ๋ฉ”์ด์…˜: ๊ฒฐ๊ณผ ์ž…๋ ฅ ํ›„ ์Šน์ž ์นด๋“œ๊ฐ€ fade-in + scale ํšจ๊ณผ๋กœ ๊ฐ•์กฐ

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

  • ๋ฐฑ์—”๋“œ: PHP 8.2+, g5se (๊ทธ๋ˆ„๋ณด๋“œ5 SE)
  • ํ”„๋ก ํŠธ์—”๋“œ: Vanilla JavaScript (ES6+), SVG ๋ Œ๋”๋ง
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค: SQLite (data/modules/tournament.sqlite)
  • ์Šคํƒ€์ผ: CSS Variables (modern shell ํ† ํฐ), Flexbox/Grid
  • AJAX: Fetch API (๊ฒฐ๊ณผ ์ž…๋ ฅ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ)

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

-- tournaments ํ…Œ์ด๋ธ”
CREATE TABLE tournaments (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  sport TEXT NOT NULL,
  format TEXT NOT NULL, -- 'single' or 'double'
  status TEXT DEFAULT 'ongoing', -- 'ongoing' or 'completed'
  slug TEXT UNIQUE NOT NULL, -- URL์šฉ ๊ณ ์œ  ์‹๋ณ„์ž
  created_by INTEGER, -- g5se mb_no
  created_at TEXT DEFAULT CURRENT_TIMESTAMP
);

-- participants ํ…Œ์ด๋ธ”
CREATE TABLE participants (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  tournament_id INTEGER NOT NULL,
  name TEXT NOT NULL,
  seed INTEGER, -- ์‹œ๋“œ ์ˆœ์„œ
  FOREIGN KEY (tournament_id) REFERENCES tournaments(id) ON DELETE CASCADE
);

-- matches ํ…Œ์ด๋ธ”
CREATE TABLE matches (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  tournament_id INTEGER NOT NULL,
  round INTEGER NOT NULL, -- 1(๊ฒฐ์Šน), 2(์ค€๊ฒฐ์Šน), 4(8๊ฐ•)...
  bracket TEXT DEFAULT 'winner', -- 'winner' or 'loser' (๋”๋ธ” ์—˜๋ฆฌ๋ฏธ๋„ค์ด์…˜)
  position INTEGER NOT NULL, -- ๊ฐ™์€ ๋ผ์šด๋“œ ๋‚ด ๋งค์น˜ ์ˆœ์„œ
  player1_id INTEGER,
  player2_id INTEGER,
  score1 INTEGER,
  score2 INTEGER,
  winner_id INTEGER,
  next_match_id INTEGER, -- ์Šน์ž๊ฐ€ ์ง„์ถœํ•  ๋‹ค์Œ ๋งค์น˜ ID
  played_at TEXT,
  FOREIGN KEY (tournament_id) REFERENCES tournaments(id) ON DELETE CASCADE,
  FOREIGN KEY (player1_id) REFERENCES participants(id),
  FOREIGN KEY (player2_id) REFERENCES participants(id),
  FOREIGN KEY (winner_id) REFERENCES participants(id)
);

๐ŸŽฏ ํ•ต์‹ฌ ์•Œ๊ณ ๋ฆฌ์ฆ˜

  1. ์‹ฑ๊ธ€ ์—˜๋ฆฌ๋ฏธ๋„ค์ด์…˜ ๋ธŒ๋ž˜ํ‚ท ์ƒ์„ฑ:

    • ์ฐธ๊ฐ€์ž ์ˆ˜ n์—์„œ 2^k >= n์ธ ์ตœ์†Œ k ๊ณ„์‚ฐ โ†’ ์ด ๋ผ์šด๋“œ ์ˆ˜
    • 1๋ผ์šด๋“œ ๋งค์น˜ ์ˆ˜ = n / 2 (๋ถ€์ „์Šน ์ œ์™ธ), 2๋ผ์šด๋“œ = n / 4, ..., ๊ฒฐ์Šน = 1
    • ๋ถ€์ „์Šน(Bye) ๋ฐฐ์ •: 2^k - n๊ฐœ ํ•„์š” โ†’ ์‹œ๋“œ ์ƒ์œ„์ž์—๊ฒŒ ์šฐ์„  ๋ฐฐ์ •
  2. ๋‹ค์Œ ๋ผ์šด๋“œ ์ง„์ถœ์ž ๋ฐฐ์ •:

    • ๋งค์น˜ ๊ฒฐ๊ณผ ์ž…๋ ฅ ์‹œ winner_id ํŒ์ • โ†’ next_match_id๊ฐ€ ๊ฐ€๋ฆฌํ‚ค๋Š” ๋งค์น˜์˜ player1_id ๋˜๋Š” player2_id์— ์‚ฝ์ž…
    • ์˜ˆ: 1๋ผ์šด๋“œ ๋งค์น˜ 1๋ฒˆ ์Šน์ž โ†’ 2๋ผ์šด๋“œ ๋งค์น˜ 1๋ฒˆ์˜ player1_id, 1๋ผ์šด๋“œ ๋งค์น˜ 2๋ฒˆ ์Šน์ž โ†’ 2๋ผ์šด๋“œ ๋งค์น˜ 1๋ฒˆ์˜ player2_id
  3. SVG ๋ Œ๋”๋ง ๋กœ์ง:

    • ๋ผ์šด๋“œ๋ณ„๋กœ ์ˆ˜์ง ๋ ˆ์ด์–ด ๋ฐฐ์น˜ (x ์ขŒํ‘œ = ๋ผ์šด๋“œ ์ธ๋ฑ์Šค * 200px)
    • ๊ฐ ๋งค์น˜ ์นด๋“œ ์œ„์น˜ = y = (๋งค์น˜ ์ˆœ์„œ * ์นด๋“œ ๋†’์ด) + (๋ผ์šด๋“œ๋ณ„ ์˜คํ”„์…‹)
    • ๋ถ€๋ชจ-์ž์‹ ๋งค์น˜ ์—ฐ๊ฒฐ์„ : <path> ์š”์†Œ๋กœ ๋ฒ ์ง€์–ด ๊ณก์„  ๊ทธ๋ฆฌ๊ธฐ

๐Ÿš€ ํ•™์Šต ๋ชฉํ‘œ

  • g5se์˜ modern shell๊ณผ ๋””์ž์ธ ํ† ํฐ์„ ํ™œ์šฉํ•œ ์ผ๊ด€๋œ UI ๊ตฌ์ถ•
  • ๋ณต์žกํ•œ ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ(์ฐธ๊ฐ€์ž-๋งค์น˜-๋ผ์šด๋“œ)๋ฅผ SQLite๋กœ ์„ค๊ณ„ยท๊ด€๋ฆฌ
  • ์žฌ๊ท€์  ์•Œ๊ณ ๋ฆฌ์ฆ˜(ํ† ๋„ˆ๋จผํŠธ ํŠธ๋ฆฌ ์ƒ์„ฑ)์„ PHP๋กœ ๊ตฌํ˜„
  • SVG DOM ์กฐ์ž‘๊ณผ ๋™์  ๋ Œ๋”๋ง์„ vanilla JS๋กœ ์ฒ˜๋ฆฌ
  • AJAX ๊ธฐ๋ฐ˜ ๋น„๋™๊ธฐ ์—…๋ฐ์ดํŠธ๋กœ ๋ถ€๋“œ๋Ÿฌ์šด UX ์ œ๊ณต
  • ๊ด€๋ฆฌ์ž/์‚ฌ์šฉ์ž ๊ถŒํ•œ ๋ถ„๋ฆฌ (g5se ํšŒ์› ์‹œ์Šคํ…œ ์—ฐ๋™)

์ œ์•ฝ ์กฐ๊ฑด

  • โ€ข g5se(๊ทธ๋ˆ„๋ณด๋“œ5 SE) ํ”„๋ ˆ์ž„์›Œํฌ ์‚ฌ์šฉ ํ•„์ˆ˜
  • โ€ข ํด๋” ๊ธฐ๋ฐ˜ ์ž๋™ ๋ผ์šฐํŒ…: app/modules/tournament/index.php โ†’ /tournament
  • โ€ข modern shell ์ ์šฉ: _head.inc.php + m-shell + _nav.inc.php + m-container + _footer.inc.php + tail.sub.php
  • โ€ข ๋””์ž์ธ ํ† ํฐ ์‚ฌ์šฉ: var(--m-surface), var(--m-text), var(--m-shadow), var(--m-radius-lg) ๋“ฑ
  • โ€ข g5se ๋ฉ”์ธ DB์— ํ…Œ์ด๋ธ” ์ƒ์„ฑ ๊ธˆ์ง€ โ€” ๋ชจ๋“ˆ ์ „์šฉ SQLite ์‚ฌ์šฉ: G5_DATA_PATH.'/modules/tournament.sqlite'
  • โ€ข SQLite ์—ฐ๊ฒฐ ์‹œ prepared statement ํ•„์ˆ˜ (SQL ์ธ์ ์…˜ ๋ฐฉ์ง€)
  • โ€ข ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ์ถœ๋ ฅ ์‹œ htmlspecialchars() ์ด์Šค์ผ€์ดํ”„ ํ•„์ˆ˜
  • โ€ข ์™ธ๋ถ€ ๋นŒ๋“œ ๋„๊ตฌ(npm/vite) ์‚ฌ์šฉ ๊ธˆ์ง€, CDN ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ตœ์†Œํ™”
  • โ€ข AJAX๋Š” Fetch API๋กœ, jQuery ์‚ฌ์šฉ ๊ธˆ์ง€
  • โ€ข SVG ๋ Œ๋”๋ง์€ ์„œ๋ฒ„์—์„œ ์ดˆ๊ธฐ HTML๋กœ ์ƒ์„ฑ ํ›„ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋™์  ์—…๋ฐ์ดํŠธ
  • โ€ข ๊ด€๋ฆฌ์ž ๊ถŒํ•œ: g5se $member['mb_level'] >= 8 ๋˜๋Š” $is_admin == true ์ฒดํฌ
  • โ€ข ๋ชจ๋ฐ”์ผ ๋ฐ˜์‘ํ˜•: ๋Œ€์ง„ํ‘œ๋Š” ๊ฐ€๋กœ ์Šคํฌ๋กค(overflow-x: auto), ๋งค์น˜ ์นด๋“œ๋Š” ์ตœ์†Œ 44px ํ„ฐ์น˜ ์˜์—ญ
  • โ€ข ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜: Chrome/Safari/Firefox ์ตœ์‹  2๋ฒ„์ „ ์ง€์›

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

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

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

**๋ชฉํ‘œ**: ํ† ๋„ˆ๋จผํŠธ ๋Œ€ํšŒ ๋ชฉ๋ก ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค๊ณ , modern shell์„ ์ ์šฉํ•˜์—ฌ ์‚ฌ์ดํŠธ ์ „์ฒด์™€ ์‹œ๊ฐ ์ผ๊ด€์„ฑ์„ ํ™•๋ณดํ•ฉ๋‹ˆ๋‹ค.

**์ž‘์—… ๋‚ด์šฉ**:

1. `app/modules/tournament/` ํด๋” ์ƒ์„ฑ
2. `app/modules/tournament/index.php` ํŒŒ์ผ ์ƒ์„ฑ (๋Œ€ํšŒ ๋ชฉ๋ก ํŽ˜์ด์ง€ ์ง„์ž…์ )
3. Modern shell ๊ณจ๊ฒฉ ์ ์šฉ:
   - `_common.php` ์ธํด๋ฃจ๋“œ (`./` ์ƒ๋Œ€๊ฒฝ๋กœ ์‚ฌ์šฉ)
   - `G5_THEME_PATH.'/modern/_head.inc.php'` require
   - `./head.sub.php` include
   - `<div class="m-shell">` + `_nav.inc.php` + `<main class="m-container">` + `_footer.inc.php` + `</div>`
   - `./tail.sub.php` include
4. ํŽ˜์ด์ง€ ์ œ๋ชฉ "๐Ÿ† ํ† ๋„ˆ๋จผํŠธ ๋Œ€ํšŒ ๋ชฉ๋ก" ํ‘œ์‹œ
5. ์ž„์‹œ ๋ฉ”์‹œ์ง€: "๋Œ€ํšŒ ๋ชฉ๋ก์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค." (์‹ค์ œ DB ์—ฐ๋™์€ ๋‹ค์Œ ๋‹จ๊ณ„)
6. ๋””์ž์ธ ํ† ํฐ ์‚ฌ์šฉ:
   - ๋ฐฐ๊ฒฝ: `var(--m-surface)`
   - ํ…์ŠคํŠธ: `var(--m-text)`
   - ๊ทธ๋ฆผ์ž: `var(--m-shadow)`
7. ์ƒ๋‹จ์— "์ƒˆ ๋Œ€ํšŒ ๋งŒ๋“ค๊ธฐ" ๋ฒ„ํŠผ ์ถ”๊ฐ€ (๋งํฌ๋Š” `/tournament/create`๋กœ, ์•„์ง ๊ตฌํ˜„ ์•ˆ ๋จ)
   - ๋ฒ„ํŠผ ์Šคํƒ€์ผ: `background: #3b82f6; color: white; padding: 12px 24px; border-radius: var(--m-radius-lg); font-weight: 600;`
   - ๋กœ๊ทธ์ธํ•œ ํšŒ์›๋งŒ ๋ณด์ด๋„๋ก ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง (`$is_member` ์ฒดํฌ)

**์ฃผ์˜์‚ฌํ•ญ**:
- ํด๋” ๊ธฐ๋ฐ˜ ์ž๋™ ๋ผ์šฐํŒ…์ด๋ฏ€๋กœ `app/router.php`์— ๋ผ์šฐํŠธ ๋“ฑ๋ก ๋ถˆํ•„์š”
- `_common.php`/`head.sub.php`/`tail.sub.php`๋Š” `app/` ๋ฃจํŠธ์— ์žˆ์œผ๋ฏ€๋กœ `./` ์ƒ๋Œ€๊ฒฝ๋กœ๋กœ ์ธํด๋ฃจ๋“œ
- `G5_THEME_PATH`๋Š” `_common.php` ์ธํด๋ฃจ๋“œ ์‹œ์ ์— ์ •์˜๋จ
- ์ง„์ž… ๊ฐ€๋“œ `define('_GNUBOARD_', true);`๋Š” ๋ฃจํŠธ `index.php`๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ํŽ˜์ด์ง€์—์„œ ์žฌ์ •์˜ ๊ธˆ์ง€

**์˜ˆ์ƒ ๊ฒฐ๊ณผ**: `/tournament` ์ ‘์† ์‹œ modern shell์ด ์ ์šฉ๋œ ํŽ˜์ด์ง€๊ฐ€ ํ‘œ์‹œ๋˜๊ณ , ์ƒ๋‹จ ๋„ค๋น„๊ฒŒ์ด์…˜/๋‹คํฌ๋ชจ๋“œ ํ† ๊ธ€/Pretendard ํฐํŠธ๊ฐ€ ์ž๋™์œผ๋กœ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธํ•œ ํšŒ์›์€ "์ƒˆ ๋Œ€ํšŒ ๋งŒ๋“ค๊ธฐ" ๋ฒ„ํŠผ์ด ๋ณด์ž…๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ํด๋” ๊ตฌ์กฐ app/modules/tournament/index.php ์ƒ์„ฑ ์™„๋ฃŒ. ํŽ˜์ด์ง€ ์ ‘์† ์‹œ modern shell์ด ์ ์šฉ๋˜๊ณ  ์ œ๋ชฉ/๋ฒ„ํŠผ์ด ํ‘œ์‹œ๋จ. ๋‹คํฌ๋ชจ๋“œ ์ „ํ™˜ ์‹œ ๋ฐฐ๊ฒฝ/ํ…์ŠคํŠธ ์ƒ‰์ƒ์ด ์ž๋™ ๋ณ€๊ฒฝ๋จ.

2
Step 2
**๋ชฉํ‘œ**: SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ๋Œ€ํšŒยท์ฐธ๊ฐ€์žยท๋งค์น˜ ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ๋ฅผ ์ž๋™ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.

**์ž‘์—… ๋‚ด์šฉ**:

1. `app/modules/tournament/index.php` ์ƒ๋‹จ(๋กœ์ง ์˜์—ญ)์— DB ์—ฐ๊ฒฐ ์ฝ”๋“œ ์ถ”๊ฐ€:
   ```php
   // SQLite ํŒŒ์ผ ๊ฒฝ๋กœ
   $db_dir = G5_DATA_PATH.'/modules';
   @mkdir($db_dir, 0707, true); // ๋””๋ ‰ํ„ฐ๋ฆฌ ์—†์œผ๋ฉด ์ƒ์„ฑ
   $db_path = $db_dir.'/tournament.sqlite';
   
   // PDO ์—ฐ๊ฒฐ
   try {
       $pdo = new PDO('sqlite:'.$db_path);
       $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
   } catch (PDOException $e) {
       die('DB ์—ฐ๊ฒฐ ์‹คํŒจ: '.$e->getMessage());
   }
   
   // ํ…Œ์ด๋ธ” ์ž๋™ ์ƒ์„ฑ (์ฒซ ์ ‘๊ทผ ์‹œ)
   $pdo->exec("
       CREATE TABLE IF NOT EXISTS tournaments (
           id INTEGER PRIMARY KEY AUTOINCREMENT,
           title TEXT NOT NULL,
           sport TEXT NOT NULL,
           format TEXT NOT NULL,
           status TEXT DEFAULT 'ongoing',
           slug TEXT UNIQUE NOT NULL,
           created_by INTEGER,
           created_at TEXT DEFAULT CURRENT_TIMESTAMP
       )
   ");
   
   $pdo->exec("
       CREATE TABLE IF NOT EXISTS participants (
           id INTEGER PRIMARY KEY AUTOINCREMENT,
           tournament_id INTEGER NOT NULL,
           name TEXT NOT NULL,
           seed INTEGER,
           FOREIGN KEY (tournament_id) REFERENCES tournaments(id) ON DELETE CASCADE
       )
   ");
   
   $pdo->exec("
       CREATE TABLE IF NOT EXISTS matches (
           id INTEGER PRIMARY KEY AUTOINCREMENT,
           tournament_id INTEGER NOT NULL,
           round INTEGER NOT NULL,
           bracket TEXT DEFAULT 'winner',
           position INTEGER NOT NULL,
           player1_id INTEGER,
           player2_id INTEGER,
           score1 INTEGER,
           score2 INTEGER,
           winner_id INTEGER,
           next_match_id INTEGER,
           played_at TEXT,
           FOREIGN KEY (tournament_id) REFERENCES tournaments(id) ON DELETE CASCADE,
           FOREIGN KEY (player1_id) REFERENCES participants(id),
           FOREIGN KEY (player2_id) REFERENCES participants(id),
           FOREIGN KEY (winner_id) REFERENCES participants(id)
       )
   ");
   ```

2. ๋Œ€ํšŒ ๋ชฉ๋ก ์กฐํšŒ ์ฟผ๋ฆฌ ์ถ”๊ฐ€:
   ```php
   $stmt = $pdo->query("SELECT * FROM tournaments ORDER BY created_at DESC");
   $tournaments = $stmt->fetchAll(PDO::FETCH_ASSOC);
   ```

3. HTML ๋ณธ๋ฌธ์—์„œ ๋Œ€ํšŒ ๋ชฉ๋ก ๋ Œ๋”๋ง:
   - ๋Œ€ํšŒ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ: "์•„์ง ์ƒ์„ฑ๋œ ๋Œ€ํšŒ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ฒซ ๋Œ€ํšŒ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”!" ๋ฉ”์‹œ์ง€
   - ๋Œ€ํšŒ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ: ๊ฐ ๋Œ€ํšŒ๋ฅผ ์นด๋“œ ํ˜•ํƒœ๋กœ ํ‘œ์‹œ
     - ์นด๋“œ ๋‚ด์šฉ: ๋Œ€ํšŒ๋ช…, ์ข…๋ชฉ, ์ƒํƒœ(์ง„ํ–‰ ์ค‘/์™„๋ฃŒ), ์ƒ์„ฑ ๋‚ ์งœ
     - ์นด๋“œ ์Šคํƒ€์ผ: `background: var(--m-surface); padding: 20px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); margin-bottom: 16px;`
     - "๋Œ€์ง„ํ‘œ ๋ณด๊ธฐ" ๋ฒ„ํŠผ: `/tournament/view/{slug}` ๋งํฌ (๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ๊ตฌํ˜„)

4. ์ƒํƒœ ๋ฐฐ์ง€ ์Šคํƒ€์ผ:
   - ์ง„ํ–‰ ์ค‘: `background: #3b82f6; color: white; padding: 4px 12px; border-radius: 12px; font-size: 12px;`
   - ์™„๋ฃŒ: `background: #10b981; color: white;`

**์ฃผ์˜์‚ฌํ•ญ**:
- `G5_DATA_PATH`๋Š” `_common.php`์—์„œ ์ •์˜๋œ g5se ๋ฐ์ดํ„ฐ ๋””๋ ‰ํ„ฐ๋ฆฌ ์ƒ์ˆ˜
- `@mkdir()`์˜ `@`๋Š” ์ด๋ฏธ ๋””๋ ‰ํ„ฐ๋ฆฌ๊ฐ€ ์žˆ์„ ๋•Œ ๊ฒฝ๊ณ ๋ฅผ ๋ฌด์‹œ
- SQLite ํŒŒ์ผ์€ ์›น ๋ฃจํŠธ ๋ฐ–์— ์œ„์น˜ํ•˜๋ฏ€๋กœ ์ง์ ‘ ์ ‘๊ทผ ๋ถˆ๊ฐ€ (๋ณด์•ˆ)
- ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋Š” `htmlspecialchars()`๋กœ ์ด์Šค์ผ€์ดํ”„ํ•˜์—ฌ XSS ๋ฐฉ์ง€

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

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: SQLite ํŒŒ์ผ ์ƒ์„ฑ ๋ฐ ํ…Œ์ด๋ธ” ์ดˆ๊ธฐํ™” ์™„๋ฃŒ. ๋Œ€ํšŒ ๋ชฉ๋ก ํŽ˜์ด์ง€์—์„œ ๋นˆ ๋ชฉ๋ก ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ. DB ์—ฐ๊ฒฐ ์—๋Ÿฌ ์—†์ด ์ •์ƒ ์ž‘๋™.

3
Step 3
**๋ชฉํ‘œ**: ๋Œ€ํšŒ ์ƒ์„ฑ ํผ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค๊ณ , ์ฐธ๊ฐ€์ž ๋ช…๋‹จ ์ž…๋ ฅยทํ† ๋„ˆ๋จผํŠธ ํ˜•์‹ ์„ ํƒ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

**์ž‘์—… ๋‚ด์šฉ**:

1. `app/modules/tournament/create/index.php` ํŒŒ์ผ ์ƒ์„ฑ (ํด๋” ์ž๋™ ๋ผ์šฐํŒ…์œผ๋กœ `/tournament/create` URL ๋งคํ•‘)
2. Modern shell ๊ณจ๊ฒฉ ์ ์šฉ (์ด์ „ ๋‹จ๊ณ„์™€ ๋™์ผ)
3. ๋กœ๊ทธ์ธ ์ฒดํฌ:
   ```php
   if (!$is_member) {
       alert('๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.');
       goto_url(G5_BBS_URL.'/login.php?url='.urlencode('/tournament/create'));
   }
   ```

4. ๋Œ€ํšŒ ์ƒ์„ฑ ํผ ๊ตฌํ˜„:
   - ๋Œ€ํšŒ๋ช… ์ž…๋ ฅ๋ž€ (`<input type="text" name="title" required>`)
   - ์ข…๋ชฉ ์ž…๋ ฅ๋ž€ (`<input type="text" name="sport" placeholder="์˜ˆ: ์ถ•๊ตฌ, ๋ฆฌ๊ทธ ์˜ค๋ธŒ ๋ ˆ์ „๋“œ" required>`)
   - ํ† ๋„ˆ๋จผํŠธ ํ˜•์‹ ์„ ํƒ (`<select name="format">`)
     - ์˜ต์…˜: `single` (์‹ฑ๊ธ€ ์—˜๋ฆฌ๋ฏธ๋„ค์ด์…˜), `double` (๋”๋ธ” ์—˜๋ฆฌ๋ฏธ๋„ค์ด์…˜)
   - ์ฐธ๊ฐ€์ž ๋ช…๋‹จ ์ž…๋ ฅ (`<textarea name="participants" rows="10" placeholder="ํ•œ ์ค„์— ํ•œ ๋ช…์”ฉ ์ž…๋ ฅํ•˜์„ธ์š”" required>`)
   - ์ œ์ถœ ๋ฒ„ํŠผ: "๋Œ€ํšŒ ์ƒ์„ฑํ•˜๊ธฐ" (`background: #10b981; color: white; padding: 12px 32px; border: none; border-radius: var(--m-radius-lg); font-weight: 600; cursor: pointer;`)

5. ํผ ์ œ์ถœ ์ฒ˜๋ฆฌ (POST ์š”์ฒญ ์‹œ):
   ```php
   if ($_SERVER['REQUEST_METHOD'] === 'POST') {
       $title = trim($_POST['title']);
       $sport = trim($_POST['sport']);
       $format = $_POST['format'];
       $participants_raw = trim($_POST['participants']);
       
       // ์ฐธ๊ฐ€์ž ๋ช…๋‹จ ํŒŒ์‹ฑ (์ค„๋ฐ”๊ฟˆ์œผ๋กœ ๋ถ„๋ฆฌ)
       $participants = array_filter(array_map('trim', explode("\n", $participants_raw)));
       
       if (count($participants) < 2) {
           alert('์ฐธ๊ฐ€์ž๋Š” ์ตœ์†Œ 2๋ช… ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.');
           goto_url('/tournament/create');
       }
       
       // ๊ณ ์œ  slug ์ƒ์„ฑ (๋žœ๋ค 8์ž๋ฆฌ ์˜์ˆซ์ž)
       $slug = bin2hex(random_bytes(4));
       
       // ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘
       $pdo->beginTransaction();
       
       try {
           // tournaments ํ…Œ์ด๋ธ”์— ์‚ฝ์ž…
           $stmt = $pdo->prepare("INSERT INTO tournaments (title, sport, format, slug, created_by) VALUES (?, ?, ?, ?, ?)");
           $stmt->execute([$title, $sport, $format, $slug, $member['mb_no']]);
           $tournament_id = $pdo->lastInsertId();
           
           // participants ํ…Œ์ด๋ธ”์— ์‚ฝ์ž… (์‹œ๋“œ ์ˆœ์„œ๋Š” ์ž…๋ ฅ ์ˆœ์„œ)
           $stmt = $pdo->prepare("INSERT INTO participants (tournament_id, name, seed) VALUES (?, ?, ?)");
           foreach ($participants as $idx => $name) {
               $stmt->execute([$tournament_id, $name, $idx + 1]);
           }
           
           $pdo->commit();
           
           // ๋Œ€์ง„ํ‘œ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
           goto_url('/tournament/view/'.$slug);
       } catch (Exception $e) {
           $pdo->rollBack();
           alert('๋Œ€ํšŒ ์ƒ์„ฑ ์‹คํŒจ: '.$e->getMessage());
           goto_url('/tournament/create');
       }
   }
   ```

6. ํผ ์Šคํƒ€์ผ:
   - ์ž…๋ ฅ๋ž€: `width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: var(--m-radius-lg); background: var(--m-surface); color: var(--m-text);`
   - ๋ผ๋ฒจ: `display: block; margin-bottom: 8px; font-weight: 600; color: var(--m-text);`
   - ํผ ์ปจํ…Œ์ด๋„ˆ: `max-width: 600px; margin: 0 auto;`

**์ฃผ์˜์‚ฌํ•ญ**:
- `goto_url()`๊ณผ `alert()`๋Š” g5se์˜ ํ—ฌํผ ํ•จ์ˆ˜ (`_common.php`์—์„œ ๋กœ๋“œ)
- `$member['mb_no']`๋Š” ๋กœ๊ทธ์ธํ•œ ํšŒ์› ๋ฒˆํ˜ธ
- `random_bytes(4)`๋กœ 8์ž๋ฆฌ hex slug ์ƒ์„ฑ (์ถฉ๋Œ ํ™•๋ฅ  ๊ทนํžˆ ๋‚ฎ์Œ)
- ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋Œ€ํšŒยท์ฐธ๊ฐ€์ž ์‚ฝ์ž…์„ ์›์ž์ ์œผ๋กœ ์ฒ˜๋ฆฌ

**์˜ˆ์ƒ ๊ฒฐ๊ณผ**: `/tournament/create` ์ ‘์† ์‹œ ํผ์ด ํ‘œ์‹œ๋˜๊ณ , ์ œ์ถœ ์‹œ DB์— ๋Œ€ํšŒยท์ฐธ๊ฐ€์ž๊ฐ€ ์ €์žฅ๋œ ํ›„ ๋Œ€์ง„ํ‘œ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋Œ€ํšŒ ์ƒ์„ฑ ํผ ํŽ˜์ด์ง€ ์™„์„ฑ. ํผ ์ œ์ถœ ์‹œ DB์— ๋Œ€ํšŒยท์ฐธ๊ฐ€์ž ์‚ฝ์ž… ์„ฑ๊ณต. ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์ •์ƒ ์ž‘๋™ (๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ๋Œ€์ง„ํ‘œ ํŽ˜์ด์ง€ ๊ตฌํ˜„).

4
Step 4
**๋ชฉํ‘œ**: ์‹ฑ๊ธ€ ์—˜๋ฆฌ๋ฏธ๋„ค์ด์…˜ ๋ธŒ๋ž˜ํ‚ท ์ž๋™ ์ƒ์„ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๊ตฌํ˜„ํ•˜๊ณ , ์ดˆ๊ธฐ ๋งค์น˜ ๋ฐ์ดํ„ฐ๋ฅผ DB์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

**์ž‘์—… ๋‚ด์šฉ**:

1. `app/modules/tournament/create/index.php`์˜ ๋Œ€ํšŒ ์ƒ์„ฑ ๋กœ์ง ์ˆ˜์ • (์ฐธ๊ฐ€์ž ์‚ฝ์ž… ํ›„):
   ```php
   // ์‹ฑ๊ธ€ ์—˜๋ฆฌ๋ฏธ๋„ค์ด์…˜ ๋ธŒ๋ž˜ํ‚ท ์ƒ์„ฑ (๋”๋ธ”์€ ๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ํ™•์žฅ)
   if ($format === 'single') {
       // ์ฐธ๊ฐ€์ž ์ˆ˜
       $n = count($participants);
       
       // ์ด ๋ผ์šด๋“œ ์ˆ˜ ๊ณ„์‚ฐ (2^k >= n์ธ ์ตœ์†Œ k)
       $total_rounds = (int)ceil(log($n, 2));
       
       // ์ฒซ ๋ผ์šด๋“œ ๋งค์น˜ ์ˆ˜ (๋ถ€์ „์Šน ์ œ์™ธํ•œ ์‹ค์ œ ๊ฒฝ๊ธฐ ์ˆ˜)
       $first_round_matches = (int)pow(2, $total_rounds - 1);
       $byes = $first_round_matches * 2 - $n; // ๋ถ€์ „์Šน ์ˆ˜
       
       // ์ฐธ๊ฐ€์ž ID ๋ฐฐ์—ด ๊ฐ€์ ธ์˜ค๊ธฐ
       $stmt = $pdo->prepare("SELECT id FROM participants WHERE tournament_id = ? ORDER BY seed");
       $stmt->execute([$tournament_id]);
       $participant_ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
       
       // ๋ถ€์ „์Šน ๋ฐฐ์ •: ์ƒ์œ„ ์‹œ๋“œ์—๊ฒŒ ์šฐ์„  ๋ฐฐ์ • (player2_id = NULL)
       $matches_data = [];
       $match_position = 0;
       
       for ($i = 0; $i < $first_round_matches; $i++) {
           $p1_idx = $i * 2;
           $p2_idx = $i * 2 + 1;
           
           $p1_id = $participant_ids[$p1_idx] ?? null;
           $p2_id = ($p2_idx < $n) ? $participant_ids[$p2_idx] : null;
           
           // ๋ถ€์ „์Šน ์ฒ˜๋ฆฌ: p2๊ฐ€ ์—†์œผ๋ฉด p1์ด ์ž๋™ ์Šน๋ฆฌ
           if ($p2_id === null && $p1_id !== null) {
               $winner_id = $p1_id;
               $score1 = null;
               $score2 = null;
           } else {
               $winner_id = null;
               $score1 = null;
               $score2 = null;
           }
           
           $matches_data[] = [
               'round' => $total_rounds,
               'position' => $match_position++,
               'player1_id' => $p1_id,
               'player2_id' => $p2_id,
               'winner_id' => $winner_id,
               'score1' => $score1,
               'score2' => $score2,
           ];
       }
       
       // ์ƒ์œ„ ๋ผ์šด๋“œ ๋งค์น˜ ์ƒ์„ฑ (๋นˆ ์Šฌ๋กฏ)
       for ($round = $total_rounds - 1; $round >= 1; $round--) {
           $round_matches = (int)pow(2, $round - 1);
           for ($i = 0; $i < $round_matches; $i++) {
               $matches_data[] = [
                   'round' => $round,
                   'position' => $i,
                   'player1_id' => null,
                   'player2_id' => null,
                   'winner_id' => null,
                   'score1' => null,
                   'score2' => null,
               ];
           }
       }
       
       // matches ํ…Œ์ด๋ธ”์— ์‚ฝ์ž…
       $stmt = $pdo->prepare("INSERT INTO matches (tournament_id, round, bracket, position, player1_id, player2_id, winner_id, score1, score2) VALUES (?, ?, 'winner', ?, ?, ?, ?, ?, ?)");
       foreach ($matches_data as $match) {
           $stmt->execute([
               $tournament_id,
               $match['round'],
               $match['position'],
               $match['player1_id'],
               $match['player2_id'],
               $match['winner_id'],
               $match['score1'],
               $match['score2'],
           ]);
       }
       
       // next_match_id ์—ฐ๊ฒฐ (ํ•˜์œ„ ๋ผ์šด๋“œ ์Šน์ž๊ฐ€ ์ง„์ถœํ•  ์ƒ์œ„ ๋ผ์šด๋“œ ๋งค์น˜)
       // ์˜ˆ: 1๋ผ์šด๋“œ ๋งค์น˜ 0, 1 โ†’ 2๋ผ์šด๋“œ ๋งค์น˜ 0์œผ๋กœ ์ง„์ถœ
       for ($round = $total_rounds; $round >= 2; $round--) {
           $current_matches = $pdo->query("SELECT id FROM matches WHERE tournament_id = {$tournament_id} AND round = {$round} ORDER BY position")->fetchAll(PDO::FETCH_COLUMN);
           $next_matches = $pdo->query("SELECT id FROM matches WHERE tournament_id = {$tournament_id} AND round = ".($round - 1)." ORDER BY position")->fetchAll(PDO::FETCH_COLUMN);
           
           foreach ($current_matches as $idx => $match_id) {
               $next_match_id = $next_matches[(int)floor($idx / 2)] ?? null;
               if ($next_match_id) {
                   $pdo->exec("UPDATE matches SET next_match_id = {$next_match_id} WHERE id = {$match_id}");
               }
           }
       }
   }
   ```

2. ๋ถ€์ „์Šน ์ž๋™ ์ง„์ถœ ์ฒ˜๋ฆฌ:
   - `winner_id`๊ฐ€ ์ด๋ฏธ ์„ค์ •๋œ ๋งค์น˜(๋ถ€์ „์Šน)๋Š” ๋‹ค์Œ ๋ผ์šด๋“œ๋กœ ์ฆ‰์‹œ ์ง„์ถœ
   - ์ง„์ถœ ๋กœ์ง์€ ๋ณ„๋„ ํ•จ์ˆ˜๋กœ ๋ถ„๋ฆฌ (๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ์žฌ์‚ฌ์šฉ)

**์ฃผ์˜์‚ฌํ•ญ**:
- ๋ผ์šด๋“œ ๋ฒˆํ˜ธ๋Š” ์—ญ์ˆœ (๊ฒฐ์Šน=1, ์ค€๊ฒฐ์Šน=2, ...)
- `position`์€ ๊ฐ™์€ ๋ผ์šด๋“œ ๋‚ด ๋งค์น˜ ์ˆœ์„œ (0๋ถ€ํ„ฐ ์‹œ์ž‘)
- ๋ถ€์ „์Šน์€ `player2_id = NULL`๋กœ ํ‘œํ˜„
- `next_match_id` ์—ฐ๊ฒฐ์€ ํŠธ๋ฆฌ ๊ตฌ์กฐ์˜ ๋ถ€๋ชจ-์ž์‹ ๊ด€๊ณ„๋ฅผ ๋‚˜ํƒ€๋ƒ„

**์˜ˆ์ƒ ๊ฒฐ๊ณผ**: ๋Œ€ํšŒ ์ƒ์„ฑ ์‹œ ๋ชจ๋“  ๋ผ์šด๋“œ์˜ ๋งค์น˜๊ฐ€ ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋˜๊ณ , ๋ถ€์ „์Šน ์ฐธ๊ฐ€์ž๋Š” ๋‹ค์Œ ๋ผ์šด๋“œ์— ์ž๋™ ๋ฐฐ์ •๋ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์‹ฑ๊ธ€ ์—˜๋ฆฌ๋ฏธ๋„ค์ด์…˜ ๋ธŒ๋ž˜ํ‚ท ์ƒ์„ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์™„์„ฑ. DB์— ๋ชจ๋“  ๋ผ์šด๋“œ ๋งค์น˜ ์‚ฝ์ž… ์„ฑ๊ณต. next_match_id ์—ฐ๊ฒฐ ์ •์ƒ.

5
Step 5
**๋ชฉํ‘œ**: ๋Œ€์ง„ํ‘œ ์กฐํšŒ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค๊ณ , SVG ๊ธฐ๋ฐ˜ ๋ธŒ๋ž˜ํ‚ท ํŠธ๋ฆฌ๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

**์ž‘์—… ๋‚ด์šฉ**:

1. `app/modules/tournament/view/[slug]/index.php` ํŒŒ์ผ ์ƒ์„ฑ (๋™์  segment)
2. Modern shell ๊ณจ๊ฒฉ ์ ์šฉ
3. `$_GET['slug']`๋กœ ๋Œ€ํšŒ ์กฐํšŒ:
   ```php
   $slug = $_GET['slug'] ?? '';
   $stmt = $pdo->prepare("SELECT * FROM tournaments WHERE slug = ?");
   $stmt->execute([$slug]);
   $tournament = $stmt->fetch(PDO::FETCH_ASSOC);
   
   if (!$tournament) {
       alert('์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋Œ€ํšŒ์ž…๋‹ˆ๋‹ค.');
       goto_url('/tournament');
   }
   
   $tournament_id = $tournament['id'];
   ```

4. ๋งค์น˜ยท์ฐธ๊ฐ€์ž ๋ฐ์ดํ„ฐ ์กฐํšŒ:
   ```php
   // ๋งค์น˜ ๋ฐ์ดํ„ฐ (๋ผ์šด๋“œ๋ณ„ ์ •๋ ฌ)
   $stmt = $pdo->prepare("SELECT * FROM matches WHERE tournament_id = ? ORDER BY round DESC, position");
   $stmt->execute([$tournament_id]);
   $matches = $stmt->fetchAll(PDO::FETCH_ASSOC);
   
   // ์ฐธ๊ฐ€์ž ID โ†’ ์ด๋ฆ„ ๋งคํ•‘
   $stmt = $pdo->prepare("SELECT id, name FROM participants WHERE tournament_id = ?");
   $stmt->execute([$tournament_id]);
   $participants = [];
   foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $p) {
       $participants[$p['id']] = $p['name'];
   }
   ```

5. SVG ๋ธŒ๋ž˜ํ‚ท ๋ Œ๋”๋ง ํ•จ์ˆ˜ ์ž‘์„ฑ:
   ```php
   function renderBracket($matches, $participants) {
       $rounds = [];
       foreach ($matches as $m) {
           $rounds[$m['round']][] = $m;
       }
       ksort($rounds); // ๋ผ์šด๋“œ ๋ฒˆํ˜ธ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ (๊ฒฐ์Šน๋ถ€ํ„ฐ)
       
       $svg_width = count($rounds) * 250;
       $svg_height = max(array_map('count', $rounds)) * 120;
       
       $svg = '<svg width="'.$svg_width.'" height="'.$svg_height.'" style="overflow: visible;">';
       
       $x = 50;
       foreach ($rounds as $round => $round_matches) {
           $y = 50;
           foreach ($round_matches as $match) {
               $p1_name = $match['player1_id'] ? htmlspecialchars($participants[$match['player1_id']]) : 'TBD';
               $p2_name = $match['player2_id'] ? htmlspecialchars($participants[$match['player2_id']]) : 'Bye';
               
               $p1_score = $match['score1'] !== null ? $match['score1'] : '-';
               $p2_score = $match['score2'] !== null ? $match['score2'] : '-';
               
               $winner_id = $match['winner_id'];
               $p1_class = ($winner_id && $winner_id == $match['player1_id']) ? 'winner' : '';
               $p2_class = ($winner_id && $winner_id == $match['player2_id']) ? 'winner' : '';
               
               // ๋งค์น˜ ์นด๋“œ (foreignObject๋กœ HTML ์‚ฝ์ž…)
               $svg .= '<foreignObject x="'.$x.'" y="'.$y.'" width="200" height="100">';
               $svg .= '<div xmlns="http://www.w3.org/1999/xhtml" style="background: var(--m-surface); border: 2px solid #ddd; border-radius: 8px; padding: 8px; box-shadow: var(--m-shadow);">';
               $svg .= '<div style="display: flex; justify-content: space-between; margin-bottom: 4px; '.($p1_class ? 'font-weight: bold; color: #10b981;' : '').'">';
               $svg .= '<span>'.$p1_name.'</span><span>'.$p1_score.'</span>';
               $svg .= '</div>';
               $svg .= '<div style="display: flex; justify-content: space-between; '.($p2_class ? 'font-weight: bold; color: #10b981;' : '').'">';
               $svg .= '<span>'.$p2_name.'</span><span>'.$p2_score.'</span>';
               $svg .= '</div>';
               $svg .= '</div>';
               $svg .= '</foreignObject>';
               
               $y += 120;
           }
           $x += 250;
       }
       
       $svg .= '</svg>';
       return $svg;
   }
   ```

6. HTML ๋ณธ๋ฌธ์— ๋Œ€์ง„ํ‘œ ์ถœ๋ ฅ:
   ```php
   <div style="overflow-x: auto; padding: 20px;">
       <?php echo renderBracket($matches, $participants); ?>
   </div>
   ```

7. ๊ด€๋ฆฌ์ž ์ „์šฉ "๊ฒฐ๊ณผ ์ž…๋ ฅ" UI (๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ๊ตฌํ˜„):
   - ๋กœ๊ทธ์ธํ•œ ํšŒ์›์ด ๋Œ€ํšŒ ์ƒ์„ฑ์ž์ผ ๊ฒฝ์šฐ ๊ฐ ๋งค์น˜ ์นด๋“œ์— "๊ฒฐ๊ณผ ์ž…๋ ฅ" ๋ฒ„ํŠผ ์ถ”๊ฐ€

**์ฃผ์˜์‚ฌํ•ญ**:
- SVG `foreignObject`๋กœ HTML์„ ์‚ฝ์ž…ํ•˜์—ฌ ์Šคํƒ€์ผ ์ ์šฉ ๊ฐ€๋Šฅ
- ๋ชจ๋ฐ”์ผ์—์„œ ๊ฐ€๋กœ ์Šคํฌ๋กค ๊ฐ€๋Šฅํ•˜๋„๋ก `overflow-x: auto`
- ์Šน์ž๋Š” ๊ตต์€ ๊ธ€์”จ + ์ดˆ๋ก์ƒ‰์œผ๋กœ ํ•˜์ด๋ผ์ดํŠธ
- ๋ถ€์ „์Šน(Bye)์€ ํšŒ์ƒ‰์œผ๋กœ ํ‘œ์‹œ

**์˜ˆ์ƒ ๊ฒฐ๊ณผ**: `/tournament/view/{slug}` ์ ‘์† ์‹œ ๋Œ€์ง„ํ‘œ๊ฐ€ SVG๋กœ ๋ Œ๋”๋ง๋˜๊ณ , ๋งค์น˜ ์นด๋“œ๊ฐ€ ๋ผ์šด๋“œ๋ณ„๋กœ ๋ฐฐ์น˜๋ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋Œ€์ง„ํ‘œ ์กฐํšŒ ํŽ˜์ด์ง€ ์™„์„ฑ. SVG ๋ธŒ๋ž˜ํ‚ท ๋ Œ๋”๋ง ์„ฑ๊ณต. ๋งค์น˜ ์นด๋“œ๊ฐ€ ๋ผ์šด๋“œ๋ณ„๋กœ ์ •๋ ฌ๋˜์–ด ํ‘œ์‹œ๋จ. ์Šน์ž ํ•˜์ด๋ผ์ดํŠธ ์ •์ƒ ์ž‘๋™.

6
Step 6
**๋ชฉํ‘œ**: ๊ฒฝ๊ธฐ ๊ฒฐ๊ณผ ์ž…๋ ฅ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ณ , AJAX๋กœ ๋น„๋™๊ธฐ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.

**์ž‘์—… ๋‚ด์šฉ**:

1. `app/modules/tournament/view/[slug]/index.php`์— AJAX ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€ (ํŽ˜์ด์ง€ ์ƒ๋‹จ):
   ```php
   // AJAX ์š”์ฒญ ์ฒ˜๋ฆฌ
   if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'update_match') {
       header('Content-Type: application/json');
       
       // ๊ถŒํ•œ ์ฒดํฌ
       if (!$is_member || $tournament['created_by'] != $member['mb_no']) {
           echo json_encode(['success' => false, 'message' => '๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.']);
           exit;
       }
       
       $match_id = (int)$_POST['match_id'];
       $score1 = (int)$_POST['score1'];
       $score2 = (int)$_POST['score2'];
       
       // ๋งค์น˜ ์กฐํšŒ
       $stmt = $pdo->prepare("SELECT * FROM matches WHERE id = ? AND tournament_id = ?");
       $stmt->execute([$match_id, $tournament_id]);
       $match = $stmt->fetch(PDO::FETCH_ASSOC);
       
       if (!$match || !$match['player1_id'] || !$match['player2_id']) {
           echo json_encode(['success' => false, 'message' => '์œ ํšจํ•˜์ง€ ์•Š์€ ๋งค์น˜์ž…๋‹ˆ๋‹ค.']);
           exit;
       }
       
       // ์Šน์ž ํŒ์ •
       $winner_id = ($score1 > $score2) ? $match['player1_id'] : $match['player2_id'];
       
       // ๋งค์น˜ ์—…๋ฐ์ดํŠธ
       $stmt = $pdo->prepare("UPDATE matches SET score1 = ?, score2 = ?, winner_id = ?, played_at = CURRENT_TIMESTAMP WHERE id = ?");
       $stmt->execute([$score1, $score2, $winner_id, $match_id]);
       
       // ๋‹ค์Œ ๋ผ์šด๋“œ ์ง„์ถœ์ž ๋ฐฐ์ •
       if ($match['next_match_id']) {
           $stmt = $pdo->prepare("SELECT * FROM matches WHERE id = ?");
           $stmt->execute([$match['next_match_id']]);
           $next_match = $stmt->fetch(PDO::FETCH_ASSOC);
           
           // ํ˜„์žฌ ๋งค์น˜๊ฐ€ ๋‹ค์Œ ๋งค์น˜์˜ ์™ผ์ชฝ ์ž์‹์ด๋ฉด player1_id์—, ์˜ค๋ฅธ์ชฝ์ด๋ฉด player2_id์— ๋ฐฐ์ •
           // position์œผ๋กœ ํŒ๋‹จ: ์ง์ˆ˜๋ฉด ์™ผ์ชฝ, ํ™€์ˆ˜๋ฉด ์˜ค๋ฅธ์ชฝ
           if ($match['position'] % 2 === 0) {
               $pdo->prepare("UPDATE matches SET player1_id = ? WHERE id = ?")->execute([$winner_id, $match['next_match_id']]);
           } else {
               $pdo->prepare("UPDATE matches SET player2_id = ? WHERE id = ?")->execute([$winner_id, $match['next_match_id']]);
           }
       }
       
       echo json_encode(['success' => true, 'winner_id' => $winner_id]);
       exit;
   }
   ```

2. ๋งค์น˜ ์นด๋“œ์— "๊ฒฐ๊ณผ ์ž…๋ ฅ" ๋ฒ„ํŠผ ์ถ”๊ฐ€ (๊ถŒํ•œ ์žˆ๋Š” ๊ฒฝ์šฐ๋งŒ):
   ```php
   // renderBracket ํ•จ์ˆ˜ ๋‚ด ๋งค์น˜ ์นด๋“œ์— ์ถ”๊ฐ€
   if ($is_member && $tournament['created_by'] == $member['mb_no'] && !$match['winner_id'] && $match['player1_id'] && $match['player2_id']) {
       $svg .= '<button onclick="openMatchModal('.$match['id'].')" style="width: 100%; margin-top: 8px; padding: 6px; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">๊ฒฐ๊ณผ ์ž…๋ ฅ</button>';
   }
   ```

3. ๊ฒฐ๊ณผ ์ž…๋ ฅ ๋ชจ๋‹ฌ HTML ์ถ”๊ฐ€ (ํŽ˜์ด์ง€ ํ•˜๋‹จ):
   ```html
   <div id="matchModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; justify-content: center; align-items: center;">
       <div style="background: var(--m-surface); padding: 32px; border-radius: var(--m-radius-lg); box-shadow: var(--m-shadow); max-width: 400px; width: 90%;">
           <h3 style="margin-top: 0; color: var(--m-text);">๊ฒฝ๊ธฐ ๊ฒฐ๊ณผ ์ž…๋ ฅ</h3>
           <form id="matchForm">
               <input type="hidden" id="matchId" name="match_id">
               <div style="margin-bottom: 16px;">
                   <label style="display: block; margin-bottom: 8px; color: var(--m-text);">์ฐธ๊ฐ€์ž 1 ์ ์ˆ˜</label>
                   <input type="number" id="score1" name="score1" min="0" required style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; background: var(--m-surface); color: var(--m-text);">
               </div>
               <div style="margin-bottom: 16px;">
                   <label style="display: block; margin-bottom: 8px; color: var(--m-text);">์ฐธ๊ฐ€์ž 2 ์ ์ˆ˜</label>
                   <input type="number" id="score2" name="score2" min="0" required style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; background: var(--m-surface); color: var(--m-text);">
               </div>
               <div style="display: flex; gap: 12px;">
                   <button type="submit" style="flex: 1; padding: 12px; background: #10b981; color: white; border: none; border-radius: 8px; font-weight: 600; cursor: pointer;">์ œ์ถœ</button>
                   <button type="button" onclick="closeMatchModal()" style="flex: 1; padding: 12px; background: #6b7280; color: white; border: none; border-radius: 8px; font-weight: 600; cursor: pointer;">์ทจ์†Œ</button>
               </div>
           </form>
       </div>
   </div>
   ```

4. JavaScript ์ถ”๊ฐ€ (ํŽ˜์ด์ง€ ํ•˜๋‹จ):
   ```javascript
   <script>
   function openMatchModal(matchId) {
       document.getElementById('matchId').value = matchId;
       document.getElementById('matchModal').style.display = 'flex';
   }
   
   function closeMatchModal() {
       document.getElementById('matchModal').style.display = 'none';
       document.getElementById('matchForm').reset();
   }
   
   document.getElementById('matchForm').addEventListener('submit', async (e) => {
       e.preventDefault();
       
       const formData = new FormData(e.target);
       formData.append('action', 'update_match');
       
       const response = await fetch(window.location.href, {
           method: 'POST',
           body: formData
       });
       
       const result = await response.json();
       
       if (result.success) {
           alert('๊ฒฐ๊ณผ๊ฐ€ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
           location.reload(); // ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ (๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ๋กœ ๊ฐœ์„ )
       } else {
           alert('์˜ค๋ฅ˜: ' + result.message);
       }
       
       closeMatchModal();
   });
   </script>
   ```

**์ฃผ์˜์‚ฌํ•ญ**:
- AJAX ์—”๋“œํฌ์ธํŠธ๋Š” ๊ฐ™์€ ํŽ˜์ด์ง€์—์„œ POST ์š”์ฒญ์œผ๋กœ ์ฒ˜๋ฆฌ
- ๊ถŒํ•œ ์ฒดํฌ: ๋Œ€ํšŒ ์ƒ์„ฑ์ž๋งŒ ๊ฒฐ๊ณผ ์ž…๋ ฅ ๊ฐ€๋Šฅ
- ์Šน์ž ํŒ์ •์€ ๋‹จ์ˆœ ์ ์ˆ˜ ๋น„๊ต (๋™์  ์ฒ˜๋ฆฌ๋Š” ๋ณด๋„ˆ์Šค)
- ๋‹ค์Œ ๋ผ์šด๋“œ ์ง„์ถœ์ž๋Š” `position % 2`๋กœ ์™ผ์ชฝ/์˜ค๋ฅธ์ชฝ ๊ตฌ๋ถ„

**์˜ˆ์ƒ ๊ฒฐ๊ณผ**: ๊ด€๋ฆฌ์ž๊ฐ€ "๊ฒฐ๊ณผ ์ž…๋ ฅ" ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋ชจ๋‹ฌ์ด ์—ด๋ฆฌ๊ณ , ์ ์ˆ˜ ์ž…๋ ฅ ํ›„ ์ œ์ถœํ•˜๋ฉด DB ์—…๋ฐ์ดํŠธ ๋ฐ ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ์œผ๋กœ ๋Œ€์ง„ํ‘œ ๊ฐฑ์‹ ๋ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๊ฒฐ๊ณผ ์ž…๋ ฅ ๋ชจ๋‹ฌ ๊ตฌํ˜„ ์™„๋ฃŒ. AJAX ์š”์ฒญ์œผ๋กœ ๋งค์น˜ ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต. ๋‹ค์Œ ๋ผ์šด๋“œ ์ง„์ถœ์ž ์ž๋™ ๋ฐฐ์ •. ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ ํ›„ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ๋ฐ˜์˜.

7
Step 7
**๋ชฉํ‘œ**: ๊ฒฐ๊ณผ ์ž…๋ ฅ ํ›„ ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ ์—†์ด ๋Œ€์ง„ํ‘œ๋ฅผ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธํ•˜๊ณ , ์Šน์ž ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

**์ž‘์—… ๋‚ด์šฉ**:

1. AJAX ์‘๋‹ต์— ์—…๋ฐ์ดํŠธ๋œ ๋งค์น˜ ์ •๋ณด ํฌํ•จ:
   ```php
   // AJAX ์—”๋“œํฌ์ธํŠธ ์ˆ˜์ • (์ด์ „ ๋‹จ๊ณ„ ์ฝ”๋“œ์— ์ถ”๊ฐ€)
   // ์—…๋ฐ์ดํŠธ๋œ ๋งค์น˜์™€ ๋‹ค์Œ ๋งค์น˜ ์ •๋ณด ๋ฐ˜ํ™˜
   $stmt = $pdo->prepare("SELECT * FROM matches WHERE id = ?");
   $stmt->execute([$match_id]);
   $updated_match = $stmt->fetch(PDO::FETCH_ASSOC);
   
   $next_match_data = null;
   if ($match['next_match_id']) {
       $stmt->execute([$match['next_match_id']]);
       $next_match_data = $stmt->fetch(PDO::FETCH_ASSOC);
   }
   
   echo json_encode([
       'success' => true,
       'updated_match' => $updated_match,
       'next_match' => $next_match_data,
       'participants' => $participants
   ]);
   ```

2. JavaScript์—์„œ DOM ์—…๋ฐ์ดํŠธ ๋กœ์ง ์ถ”๊ฐ€:
   ```javascript
   // matchForm submit ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์ˆ˜์ •
   if (result.success) {
       const { updated_match, next_match, participants } = result;
       
       // ํ˜„์žฌ ๋งค์น˜ ์นด๋“œ ์—…๋ฐ์ดํŠธ
       updateMatchCard(updated_match, participants);
       
       // ๋‹ค์Œ ๋งค์น˜ ์นด๋“œ ์—…๋ฐ์ดํŠธ (์ง„์ถœ์ž ํ‘œ์‹œ)
       if (next_match) {
           updateMatchCard(next_match, participants);
       }
       
       // ์Šน์ž ์• ๋‹ˆ๋ฉ”์ด์…˜
       animateWinner(updated_match.id, updated_match.winner_id);
       
       closeMatchModal();
   }
   
   function updateMatchCard(match, participants) {
       const card = document.querySelector(`[data-match-id="${match.id}"]`);
       if (!card) return;
       
       const p1Name = match.player1_id ? participants[match.player1_id] : 'TBD';
       const p2Name = match.player2_id ? participants[match.player2_id] : 'Bye';
       const p1Score = match.score1 !== null ? match.score1 : '-';
       const p2Score = match.score2 !== null ? match.score2 : '-';
       
       const p1Winner = match.winner_id && match.winner_id == match.player1_id;
       const p2Winner = match.winner_id && match.winner_id == match.player2_id;
       
       card.innerHTML = `
           <div style="display: flex; justify-content: space-between; margin-bottom: 4px; ${p1Winner ? 'font-weight: bold; color: #10b981;' : ''}">
               <span>${p1Name}</span><span>${p1Score}</span>
           </div>
           <div style="display: flex; justify-content: space-between; ${p2Winner ? 'font-weight: bold; color: #10b981;' : ''}">
               <span>${p2Name}</span><span>${p2Score}</span>
           </div>
       `;
   }
   
   function animateWinner(matchId, winnerId) {
       const card = document.querySelector(`[data-match-id="${matchId}"]`);
       if (!card) return;
       
       card.style.transition = 'transform 0.3s ease, box-shadow 0.3s ease';
       card.style.transform = 'scale(1.05)';
       card.style.boxShadow = '0 8px 24px rgba(16, 185, 129, 0.3)';
       
       setTimeout(() => {
           card.style.transform = 'scale(1)';
           card.style.boxShadow = '';
       }, 500);
   }
   ```

3. SVG ๋ Œ๋”๋ง ์‹œ `data-match-id` ์†์„ฑ ์ถ”๊ฐ€:
   ```php
   // renderBracket ํ•จ์ˆ˜ ๋‚ด foreignObject์— ์ถ”๊ฐ€
   $svg .= '<foreignObject x="'.$x.'" y="'.$y.'" width="200" height="100">';
   $svg .= '<div xmlns="http://www.w3.org/1999/xhtml" data-match-id="'.$match['id'].'" style="...">';
   ```

4. ์ฐธ๊ฐ€์ž ๋ฐ์ดํ„ฐ๋ฅผ JavaScript๋กœ ์ „๋‹ฌ:
   ```php
   // ํŽ˜์ด์ง€ ํ•˜๋‹จ script ํƒœ๊ทธ ์ „์— ์ถ”๊ฐ€
   <script>
   const participantsData = <?php echo json_encode($participants); ?>;
   </script>
   ```

**์ฃผ์˜์‚ฌํ•ญ**:
- `data-match-id` ์†์„ฑ์œผ๋กœ DOM ์š”์†Œ๋ฅผ ์‹๋ณ„
- ์• ๋‹ˆ๋ฉ”์ด์…˜์€ CSS `transform`๊ณผ `box-shadow` ์‚ฌ์šฉ
- JavaScript ๊ฐ์ฒด๋กœ ์ฐธ๊ฐ€์ž ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ์—์„œ ์žฌ์‚ฌ์šฉ

**์˜ˆ์ƒ ๊ฒฐ๊ณผ**: ๊ฒฐ๊ณผ ์ž…๋ ฅ ํ›„ ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ ์—†์ด ๋งค์น˜ ์นด๋“œ๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๊ณ , ์Šน์ž ์นด๋“œ๊ฐ€ ํ™•๋Œ€ยท๊ฐ•์กฐ ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์‹ค์‹œ๊ฐ„ DOM ์—…๋ฐ์ดํŠธ ๊ตฌํ˜„ ์™„๋ฃŒ. ์Šน์ž ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ •์ƒ ์ž‘๋™. ๋‹ค์Œ ๋ผ์šด๋“œ ์ง„์ถœ์ž ์ž๋™ ํ‘œ์‹œ.

8
Step 8
**๋ชฉํ‘œ**: ๋”๋ธ” ์—˜๋ฆฌ๋ฏธ๋„ค์ด์…˜(ํŒจ์ž๋ถ€ํ™œ์ „) ๋ธŒ๋ž˜ํ‚ท ์ƒ์„ฑ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜๊ณ , ์Šน์ž/ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท์„ ๋™์‹œ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

**์ž‘์—… ๋‚ด์šฉ**:

1. `app/modules/tournament/create/index.php`์˜ ๋ธŒ๋ž˜ํ‚ท ์ƒ์„ฑ ๋กœ์ง ํ™•์žฅ:
   ```php
   // ๋”๋ธ” ์—˜๋ฆฌ๋ฏธ๋„ค์ด์…˜ ๋ธŒ๋ž˜ํ‚ท ์ƒ์„ฑ
   if ($format === 'double') {
       // ์‹ฑ๊ธ€ ์—˜๋ฆฌ๋ฏธ๋„ค์ด์…˜๊ณผ ๋™์ผํ•˜๊ฒŒ ์Šน์ž ๋ธŒ๋ž˜ํ‚ท ์ƒ์„ฑ
       // ... (์ด์ „ ๋‹จ๊ณ„ ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ)
       
       // ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท ์ƒ์„ฑ
       // ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท์€ ์Šน์ž ๋ธŒ๋ž˜ํ‚ท์˜ ๊ฐ ๋ผ์šด๋“œ ํŒจ์ž๊ฐ€ ์ง„์ถœ
       // ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท ๋ผ์šด๋“œ ์ˆ˜ = (์Šน์ž ๋ธŒ๋ž˜ํ‚ท ๋ผ์šด๋“œ ์ˆ˜ - 1) * 2
       $loser_rounds = ($total_rounds - 1) * 2;
       
       // ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท 1๋ผ์šด๋“œ: ์Šน์ž ๋ธŒ๋ž˜ํ‚ท 1๋ผ์šด๋“œ ํŒจ์ž๋“ค์ด ๊ฒฝ๊ธฐ
       $loser_match_position = 0;
       for ($i = 0; $i < $first_round_matches / 2; $i++) {
           $matches_data[] = [
               'round' => $loser_rounds,
               'bracket' => 'loser',
               'position' => $loser_match_position++,
               'player1_id' => null, // ์Šน์ž ๋ธŒ๋ž˜ํ‚ท 1๋ผ์šด๋“œ ํŒจ์ž๊ฐ€ ์ฑ„์›€
               'player2_id' => null,
               'winner_id' => null,
               'score1' => null,
               'score2' => null,
           ];
       }
       
       // ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท ์ƒ์œ„ ๋ผ์šด๋“œ ์ƒ์„ฑ (๋นˆ ์Šฌ๋กฏ)
       for ($round = $loser_rounds - 1; $round >= 1; $round--) {
           $round_matches = (int)pow(2, (int)floor($round / 2));
           for ($i = 0; $i < $round_matches; $i++) {
               $matches_data[] = [
                   'round' => $round,
                   'bracket' => 'loser',
                   'position' => $i,
                   'player1_id' => null,
                   'player2_id' => null,
                   'winner_id' => null,
                   'score1' => null,
                   'score2' => null,
               ];
           }
       }
       
       // ์Šน์ž ๋ธŒ๋ž˜ํ‚ท ํŒจ์ž๊ฐ€ ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท์œผ๋กœ ๋–จ์–ด์ง€๋Š” ์—ฐ๊ฒฐ ์„ค์ •
       // (๋ณต์žกํ•œ ๋กœ์ง์ด๋ฏ€๋กœ ๊ฐ„์†Œํ™”: ๋งค์น˜ ์—…๋ฐ์ดํŠธ ์‹œ ๋™์  ์ฒ˜๋ฆฌ)
   }
   ```

2. ๊ฒฐ๊ณผ ์ž…๋ ฅ ์‹œ ํŒจ์ž๋ฅผ ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท์œผ๋กœ ์ด๋™:
   ```php
   // AJAX ์—”๋“œํฌ์ธํŠธ ์ˆ˜์ • (๋งค์น˜ ์—…๋ฐ์ดํŠธ ํ›„)
   if ($tournament['format'] === 'double' && $match['bracket'] === 'winner') {
       // ํŒจ์ž ID
       $loser_id = ($winner_id == $match['player1_id']) ? $match['player2_id'] : $match['player1_id'];
       
       // ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท์˜ ํ•ด๋‹น ๋ผ์šด๋“œ ์ฒซ ๋นˆ ์Šฌ๋กฏ์— ๋ฐฐ์ •
       $loser_round = ($match['round'] - 1) * 2; // ์Šน์ž ๋ธŒ๋ž˜ํ‚ท ๋ผ์šด๋“œ์— ๋Œ€์‘ํ•˜๋Š” ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท ๋ผ์šด๋“œ
       $stmt = $pdo->prepare("SELECT * FROM matches WHERE tournament_id = ? AND bracket = 'loser' AND round = ? AND (player1_id IS NULL OR player2_id IS NULL) ORDER BY position LIMIT 1");
       $stmt->execute([$tournament_id, $loser_round]);
       $loser_match = $stmt->fetch(PDO::FETCH_ASSOC);
       
       if ($loser_match) {
           if ($loser_match['player1_id'] === null) {
               $pdo->prepare("UPDATE matches SET player1_id = ? WHERE id = ?")->execute([$loser_id, $loser_match['id']]);
           } else {
               $pdo->prepare("UPDATE matches SET player2_id = ? WHERE id = ?")->execute([$loser_id, $loser_match['id']]);
           }
       }
   }
   ```

3. SVG ๋ Œ๋”๋ง์—์„œ ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท ์ถ”๊ฐ€:
   ```php
   // renderBracket ํ•จ์ˆ˜ ์ˆ˜์ •
   function renderBracket($matches, $participants, $format) {
       $winner_matches = array_filter($matches, fn($m) => $m['bracket'] === 'winner');
       $loser_matches = array_filter($matches, fn($m) => $m['bracket'] === 'loser');
       
       $svg = '';
       
       // ์Šน์ž ๋ธŒ๋ž˜ํ‚ท ๋ Œ๋”๋ง
       $svg .= '<text x="10" y="30" style="font-size: 20px; font-weight: bold; fill: var(--m-text);">์Šน์ž ๋ธŒ๋ž˜ํ‚ท</text>';
       $svg .= renderBracketTree($winner_matches, $participants, 0, 50);
       
       // ๋”๋ธ” ์—˜๋ฆฌ๋ฏธ๋„ค์ด์…˜์ธ ๊ฒฝ์šฐ ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท ์ถ”๊ฐ€
       if ($format === 'double' && count($loser_matches) > 0) {
           $svg .= '<text x="10" y="'.($svg_height / 2 + 30).'" style="font-size: 20px; font-weight: bold; fill: var(--m-text);">ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท</text>';
           $svg .= renderBracketTree($loser_matches, $participants, 0, $svg_height / 2 + 50);
       }
       
       return '<svg width="'.$svg_width.'" height="'.$svg_height.'" style="overflow: visible;">'.$svg.'</svg>';
   }
   
   function renderBracketTree($matches, $participants, $offset_x, $offset_y) {
       // ๊ธฐ์กด ๋ Œ๋”๋ง ๋กœ์ง ์žฌ์‚ฌ์šฉ (offset_x, offset_y๋กœ ์œ„์น˜ ์กฐ์ •)
       // ...
   }
   ```

**์ฃผ์˜์‚ฌํ•ญ**:
- ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท์€ ์Šน์ž ๋ธŒ๋ž˜ํ‚ท ์•„๋ž˜์— ๋ณ„๋„ ๋ ˆ์ด์–ด๋กœ ๋ Œ๋”๋ง
- ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท ๋ผ์šด๋“œ ์ˆ˜๋Š” ์Šน์ž ๋ธŒ๋ž˜ํ‚ท์˜ 2๋ฐฐ - 1
- ์Šน์ž ๋ธŒ๋ž˜ํ‚ท ํŒจ์ž๊ฐ€ ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท์œผ๋กœ ์ด๋™ํ•˜๋Š” ๋กœ์ง์€ ๋ณต์žกํ•˜๋ฏ€๋กœ ๊ฐ„์†Œํ™” (์‹ค๋ฌด์—์„œ๋Š” ๋” ์ •๊ตํ•œ ๋งคํ•‘ ํ•„์š”)

**์˜ˆ์ƒ ๊ฒฐ๊ณผ**: ๋”๋ธ” ์—˜๋ฆฌ๋ฏธ๋„ค์ด์…˜ ๋Œ€ํšŒ ์ƒ์„ฑ ์‹œ ์Šน์ž/ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท์ด ๋ชจ๋‘ ์ƒ์„ฑ๋˜๊ณ , ์Šน์ž ๋ธŒ๋ž˜ํ‚ท ํŒจ์ž๊ฐ€ ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท์œผ๋กœ ์ž๋™ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋”๋ธ” ์—˜๋ฆฌ๋ฏธ๋„ค์ด์…˜ ๋ธŒ๋ž˜ํ‚ท ์ƒ์„ฑ ์™„๋ฃŒ. ์Šน์ž/ํŒจ์ž ๋ธŒ๋ž˜ํ‚ท ๋™์‹œ ๋ Œ๋”๋ง. ํŒจ์ž ์ด๋™ ๋กœ์ง ์ •์ƒ ์ž‘๋™.

9
Step 9
**๋ชฉํ‘œ**: ๋Œ€ํšŒ ์‚ญ์ œ ๊ธฐ๋Šฅ๊ณผ ์™„๋ฃŒ ์ƒํƒœ ์ž๋™ ๊ฐ์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ๋Œ€ํšŒ ๋ชฉ๋ก ํŽ˜์ด์ง€๋ฅผ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.

**์ž‘์—… ๋‚ด์šฉ**:

1. ๋Œ€ํšŒ ์‚ญ์ œ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ (`app/modules/tournament/view/[slug]/index.php`):
   ```php
   // ํŽ˜์ด์ง€ ์ƒ๋‹จ์— ์‚ญ์ œ ์ฒ˜๋ฆฌ ๋กœ์ง ์ถ”๊ฐ€
   if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'delete_tournament') {
       if (!$is_member || $tournament['created_by'] != $member['mb_no']) {
           alert('๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.');
           goto_url('/tournament');
       }
       
       // CASCADE๋กœ ์ฐธ๊ฐ€์žยท๋งค์น˜ ์ž๋™ ์‚ญ์ œ
       $stmt = $pdo->prepare("DELETE FROM tournaments WHERE id = ?");
       $stmt->execute([$tournament_id]);
       
       alert('๋Œ€ํšŒ๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
       goto_url('/tournament');
   }
   ```

2. ์‚ญ์ œ ๋ฒ„ํŠผ ์ถ”๊ฐ€ (๊ด€๋ฆฌ์ž ์ „์šฉ):
   ```php
   // HTML ๋ณธ๋ฌธ ์ƒ๋‹จ์— ์ถ”๊ฐ€
   <?php if ($is_member && $tournament['created_by'] == $member['mb_no']): ?>
       <form method="post" onsubmit="return confirm('์ •๋ง ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?');" style="margin-bottom: 20px;">
           <input type="hidden" name="action" value="delete_tournament">
           <button type="submit" style="padding: 8px 16px; background: #ef4444; color: white; border: none; border-radius: var(--m-radius-lg); cursor: pointer;">๋Œ€ํšŒ ์‚ญ์ œ</button>
       </form>
   <?php endif; ?>
   ```

3. ๋Œ€ํšŒ ์™„๋ฃŒ ์ƒํƒœ ์ž๋™ ๊ฐ์ง€:
   ```php
   // ๊ฒฐ๊ณผ ์ž…๋ ฅ ํ›„ ๋Œ€ํšŒ ์™„๋ฃŒ ์—ฌ๋ถ€ ์ฒดํฌ
   // ๊ฒฐ์Šน์ „(round = 1) ์Šน์ž๊ฐ€ ๊ฒฐ์ •๋˜๋ฉด ๋Œ€ํšŒ ์™„๋ฃŒ
   $stmt = $pdo->prepare("SELECT * FROM matches WHERE tournament_id = ? AND round = 1 AND bracket = 'winner'");
   $stmt->execute([$tournament_id]);
   $final_match = $stmt->fetch(PDO::FETCH_ASSOC);
   
   if ($final_match && $final_match['winner_id']) {
       $pdo->prepare("UPDATE tournaments SET status = 'completed' WHERE id = ?")->execute([$tournament_id]);
   }
   ```

4. ๋Œ€ํšŒ ๋ชฉ๋ก ํŽ˜์ด์ง€ ๊ฐœ์„  (`app/modules/tournament/index.php`):
   ```php
   // ํ•„ํ„ฐ๋ง UI ์ถ”๊ฐ€
   $filter = $_GET['filter'] ?? 'all'; // 'all', 'ongoing', 'completed'
   
   $query = "SELECT * FROM tournaments";
   if ($filter === 'ongoing') {
       $query .= " WHERE status = 'ongoing'";
   } elseif ($filter === 'completed') {
       $query .= " WHERE status = 'completed'";
   }
   $query .= " ORDER BY created_at DESC";
   
   $tournaments = $pdo->query($query)->fetchAll(PDO::FETCH_ASSOC);
   ```

5. ํ•„ํ„ฐ ๋ฒ„ํŠผ ์ถ”๊ฐ€:
   ```html
   <div style="margin-bottom: 20px; display: flex; gap: 12px;">
       <a href="/tournament?filter=all" style="padding: 8px 16px; background: <?php echo $filter === 'all' ? '#3b82f6' : '#6b7280'; ?>; color: white; border-radius: var(--m-radius-lg); text-decoration: none;">์ „์ฒด</a>
       <a href="/tournament?filter=ongoing" style="padding: 8px 16px; background: <?php echo $filter === 'ongoing' ? '#3b82f6' : '#6b7280'; ?>; color: white; border-radius: var(--m-radius-lg); text-decoration: none;">์ง„ํ–‰ ์ค‘</a>
       <a href="/tournament?filter=completed" style="padding: 8px 16px; background: <?php echo $filter === 'completed' ? '#3b82f6' : '#6b7280'; ?>; color: white; border-radius: var(--m-radius-lg); text-decoration: none;">์™„๋ฃŒ</a>
   </div>
   ```

6. ๋Œ€ํšŒ ์นด๋“œ์— ์šฐ์Šน์ž ํ‘œ์‹œ (์™„๋ฃŒ๋œ ๋Œ€ํšŒ):
   ```php
   if ($tournament['status'] === 'completed') {
       // ๊ฒฐ์Šน์ „ ์Šน์ž ์กฐํšŒ
       $stmt = $pdo->prepare("SELECT winner_id FROM matches WHERE tournament_id = ? AND round = 1 AND bracket = 'winner'");
       $stmt->execute([$tournament['id']]);
       $winner_id = $stmt->fetchColumn();
       
       if ($winner_id) {
           $stmt = $pdo->prepare("SELECT name FROM participants WHERE id = ?");
           $stmt->execute([$winner_id]);
           $winner_name = $stmt->fetchColumn();
           
           echo '<div style="margin-top: 12px; padding: 8px; background: #fef3c7; border-left: 4px solid #f59e0b; border-radius: 4px; color: #92400e;">๐Ÿ† ์šฐ์Šน์ž: <strong>'.htmlspecialchars($winner_name).'</strong></div>';
       }
   }
   ```

**์ฃผ์˜์‚ฌํ•ญ**:
- SQLite์˜ `ON DELETE CASCADE`๋กœ ๋Œ€ํšŒ ์‚ญ์ œ ์‹œ ๊ด€๋ จ ๋ฐ์ดํ„ฐ ์ž๋™ ์‚ญ์ œ
- JavaScript `confirm()`์œผ๋กœ ์‚ญ์ œ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ
- ๋Œ€ํšŒ ์™„๋ฃŒ ์—ฌ๋ถ€๋Š” ๊ฒฐ์Šน์ „ ์Šน์ž ์กด์žฌ ์—ฌ๋ถ€๋กœ ํŒ๋‹จ

**์˜ˆ์ƒ ๊ฒฐ๊ณผ**: ๊ด€๋ฆฌ์ž๊ฐ€ ๋Œ€ํšŒ๋ฅผ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๊ณ , ๊ฒฐ์Šน์ „ ์ข…๋ฃŒ ์‹œ ๋Œ€ํšŒ๊ฐ€ ์ž๋™์œผ๋กœ ์™„๋ฃŒ ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค. ๋ชฉ๋ก ํŽ˜์ด์ง€์—์„œ ํ•„ํ„ฐ๋ง ๋ฐ ์šฐ์Šน์ž ํ‘œ์‹œ๊ฐ€ ์ •์ƒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋Œ€ํšŒ ์‚ญ์ œ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์™„๋ฃŒ. ์™„๋ฃŒ ์ƒํƒœ ์ž๋™ ๊ฐ์ง€ ์ •์ƒ ์ž‘๋™. ๋Œ€ํšŒ ๋ชฉ๋ก ํ•„ํ„ฐ๋ง ๋ฐ ์šฐ์Šน์ž ํ‘œ์‹œ ์™„์„ฑ.

10
Step 10
**๋ชฉํ‘œ**: ๋ชจ๋ฐ”์ผ ์ตœ์ ํ™”, ์ ‘๊ทผ์„ฑ ๊ฐœ์„ , ์ตœ์ข… UX ํด๋ฆฌ์‹ฑ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

**์ž‘์—… ๋‚ด์šฉ**:

1. ๋ชจ๋ฐ”์ผ ๋ฐ˜์‘ํ˜• ์Šคํƒ€์ผ ์ถ”๊ฐ€:
   ```css
   <style>
   /* ๋Œ€์ง„ํ‘œ ๊ฐ€๋กœ ์Šคํฌ๋กค ํžŒํŠธ */
   .bracket-container {
       overflow-x: auto;
       padding: 20px;
       position: relative;
   }
   
   .bracket-container::after {
       content: 'โ† ์ขŒ์šฐ๋กœ ์Šคํฌ๋กค โ†’';
       position: absolute;
       top: 10px;
       right: 10px;
       background: rgba(59, 130, 246, 0.9);
       color: white;
       padding: 4px 12px;
       border-radius: 12px;
       font-size: 12px;
       pointer-events: none;
       opacity: 0;
       animation: fadeInOut 3s ease-in-out;
   }
   
   @keyframes fadeInOut {
       0%, 100% { opacity: 0; }
       50% { opacity: 1; }
   }
   
   /* ๋งค์น˜ ์นด๋“œ ํ„ฐ์น˜ ์˜์—ญ ํ™•๋ณด */
   @media (max-width: 768px) {
       foreignObject {
           width: 180px !important;
       }
       
       .match-card {
           min-height: 44px; /* iOS ํ„ฐ์น˜ ๊ฐ€์ด๋“œ๋ผ์ธ */
       }
       
       .m-container {
           padding: 16px 12px !important;
       }
   }
   </style>
   ```

2. ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ง€์›:
   ```html
   <!-- ๊ฒฐ๊ณผ ์ž…๋ ฅ ๋ฒ„ํŠผ์— tabindex ์ถ”๊ฐ€ -->
   <button onclick="openMatchModal(...)" tabindex="0" aria-label="๊ฒฝ๊ธฐ ๊ฒฐ๊ณผ ์ž…๋ ฅ">๊ฒฐ๊ณผ ์ž…๋ ฅ</button>
   
   <!-- ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ ESC ํ‚ค ์ง€์› -->
   <script>
   document.addEventListener('keydown', (e) => {
       if (e.key === 'Escape' && document.getElementById('matchModal').style.display === 'flex') {
           closeMatchModal();
       }
   });
   </script>
   ```

3. ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ์ถ”๊ฐ€:
   ```javascript
   // AJAX ์š”์ฒญ ์‹œ ๋กœ๋”ฉ ํ‘œ์‹œ
   function showLoading() {
       const loader = document.createElement('div');
       loader.id = 'loader';
       loader.style.cssText = 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.8); color: white; padding: 20px 40px; border-radius: 12px; z-index: 9999; font-weight: 600;';
       loader.textContent = '์ฒ˜๋ฆฌ ์ค‘...';
       document.body.appendChild(loader);
   }
   
   function hideLoading() {
       const loader = document.getElementById('loader');
       if (loader) loader.remove();
   }
   
   // matchForm submit์— ์ ์šฉ
   document.getElementById('matchForm').addEventListener('submit', async (e) => {
       e.preventDefault();
       showLoading();
       
       // ... (๊ธฐ์กด AJAX ์ฝ”๋“œ)
       
       hideLoading();
   });
   ```

4. ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๊ฐœ์„ :
   ```javascript
   // AJAX ์š”์ฒญ ์‹คํŒจ ์‹œ ์‚ฌ์šฉ์ž ์นœํ™”์  ๋ฉ”์‹œ์ง€
   try {
       const response = await fetch(window.location.href, {
           method: 'POST',
           body: formData
       });
       
       if (!response.ok) {
           throw new Error('์„œ๋ฒ„ ์‘๋‹ต ์˜ค๋ฅ˜ ('+response.status+')');
       }
       
       const result = await response.json();
       // ...
   } catch (error) {
       alert('์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ' + error.message + '\n์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.');
       console.error('AJAX error:', error);
   }
   ```

5. SEO ๋ฉ”ํƒ€ ํƒœ๊ทธ ์ถ”๊ฐ€ (modern shell์ด ์ฒ˜๋ฆฌํ•˜์ง€๋งŒ ํŽ˜์ด์ง€๋ณ„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•):
   ```php
   // _head.inc.php ์ธํด๋ฃจ๋“œ ์ „์— ์ถ”๊ฐ€
   add_stylesheet('<style>/* ํŽ˜์ด์ง€๋ณ„ ์Šคํƒ€์ผ */</style>', 0);
   $g5['title'] = $tournament['title'].' - ํ† ๋„ˆ๋จผํŠธ ๋Œ€์ง„ํ‘œ';
   ```

6. ๋Œ€์ง„ํ‘œ ์ธ์‡„ ์Šคํƒ€์ผ:
   ```css
   @media print {
       .m-nav, .m-footer, button, form {
           display: none !important;
       }
       
       .bracket-container {
           overflow: visible !important;
       }
       
       svg {
           max-width: 100% !important;
           height: auto !important;
       }
   }
   ```

7. ์™„๋ฃŒ ์ถ•ํ•˜ ๋ฉ”์‹œ์ง€:
   ```php
   // ๋Œ€ํšŒ ์™„๋ฃŒ ์‹œ confetti ํšจ๊ณผ (์„ ํƒ)
   <?php if ($tournament['status'] === 'completed'): ?>
       <div style="text-align: center; padding: 32px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: var(--m-radius-lg); margin-bottom: 20px;">
           <h2 style="margin: 0; font-size: 32px;">๐ŸŽ‰ ๋Œ€ํšŒ ์™„๋ฃŒ!</h2>
           <p style="margin-top: 8px; font-size: 18px;">๋ชจ๋“  ๊ฒฝ๊ธฐ๊ฐ€ ์ข…๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.</p>
       </div>
   <?php endif; ?>
   ```

**์ฃผ์˜์‚ฌํ•ญ**:
- ๋ชจ๋ฐ”์ผ์—์„œ ๊ฐ€๋กœ ์Šคํฌ๋กค ํžŒํŠธ๋Š” 3์ดˆ๊ฐ„ ํ‘œ์‹œ ํ›„ ์‚ฌ๋ผ์ง
- ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜์€ ์›น ์ ‘๊ทผ์„ฑ ํ•„์ˆ˜ (WCAG 2.1 ์ค€์ˆ˜)
- ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ๋Š” 0.5์ดˆ ์ด์ƒ ๊ฑธ๋ฆฌ๋Š” ์ž‘์—…์—๋งŒ ํ‘œ์‹œ ๊ถŒ์žฅ
- ์ธ์‡„ ์‹œ ๋„ค๋น„๊ฒŒ์ด์…˜ยท๋ฒ„ํŠผ ์ˆจ๊น€ ์ฒ˜๋ฆฌ

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

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋ชจ๋ฐ”์ผ ์ตœ์ ํ™” ์™„๋ฃŒ. ์ ‘๊ทผ์„ฑ ๊ฐœ์„  (ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜, ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์ง€์›). ๋กœ๋”ฉยท์—๋Ÿฌ ์ฒ˜๋ฆฌ ๊ฐœ์„ . ์ธ์‡„ ์Šคํƒ€์ผ ์ ์šฉ. ์ตœ์ข… UX ํด๋ฆฌ์‹ฑ ์™„์„ฑ.

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

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

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

๋Œ“๊ธ€ (0)

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