๐ฏ g5se ์ค์๊ฐ ์คํฌ์ธ ํ ๋๋จผํธ ๋ธ๋ํท ์์ฑ๊ธฐ - ๋์งํ ์๋ ์์ฑ & ๊ฒฝ๊ธฐ ๊ฒฐ๊ณผ ์ ๋ ฅ
g5se-tournament-bracket-generator
๋ฌธ์ ์ค๋ช
๐ ํ๋ก์ ํธ ๊ฐ์
์คํฌ์ธ ๋ํธํ, e์คํฌ์ธ ๋ํ, ์ฌ๋ด ๊ฒ์ ํ ๋๋จผํธ ๋ฑ์์ ์ฌ์ฉํ ์ ์๋ ์ค์๊ฐ ํ ๋๋จผํธ ๋์งํ ์์ฑ ๋ฐ ๊ด๋ฆฌ ์์คํ ์ ๋๋ค. ์ฐธ๊ฐ์ ๋ช ๋จ์ ์ ๋ ฅํ๋ฉด ์๋์ผ๋ก ์ฑ๊ธ ์๋ฆฌ๋ฏธ๋ค์ด์ (๋จํ ํ ๋๋จผํธ) ๋๋ ๋๋ธ ์๋ฆฌ๋ฏธ๋ค์ด์ (ํจ์๋ถํ์ ) ๋์งํ๋ฅผ ์์ฑํ๊ณ , ๊ฐ ๊ฒฝ๊ธฐ ๊ฒฐ๊ณผ๋ฅผ ์ค์๊ฐ์ผ๋ก ์ ๋ ฅํ์ฌ ๋ค์ ๋ผ์ด๋ ์ง์ถ์๋ฅผ ์๋ ๋ฐฐ์ ํฉ๋๋ค. ๊ด๋ฆฌ์๋ ๋ํ๋ฅผ ์์ฑยท์์ ยท์ญ์ ํ ์ ์๊ณ , ์ผ๋ฐ ์ฌ์ฉ์๋ ๊ณต๊ฐ๋ ๋์งํ๋ฅผ ์ค์๊ฐ์ผ๋ก ์กฐํํ ์ ์์ต๋๋ค.
โ ํ์ ๊ธฐ๋ฅ
-
๋ํ ์์ฑ ํผ (๊ด๋ฆฌ์ ์ ์ฉ)
- ๋ํ๋ช , ์ข ๋ชฉ, ์ฐธ๊ฐ์ ๋ช ๋จ(ํ ์คํธ ์ ๋ ฅ ๋๋ CSV ์ ๋ก๋), ํ ๋๋จผํธ ํ์(์ฑ๊ธ/๋๋ธ ์๋ฆฌ๋ฏธ๋ค์ด์ ), ์๋ ๋ฐฐ์ ๋ฐฉ์(๋๋ค/์๋) ์ ๋ ฅ
- ์ฐธ๊ฐ์ ์๊ฐ 2์ ๊ฑฐ๋ญ์ ๊ณฑ์ด ์๋ ๊ฒฝ์ฐ ์๋์ผ๋ก ๋ถ์ ์น(Bye) ๋ฐฐ์
- ์์ฑ ์ ๊ณ ์ URL ๋ฐ๊ธ (์:
/modules/tournament/view/abc123)
-
๋์งํ ์๊ฐํ
- SVG ๊ธฐ๋ฐ ํธ๋ฆฌ ๊ตฌ์กฐ๋ก ํ ๋๋จผํธ ๋ธ๋ํท ๋ ๋๋ง (๋ผ์ด๋๋ณ ๊ฐ๋ก ๋ ์ด์ด, ๋งค์น ์ธ๋ก ์ฐ๊ฒฐ์ )
- ๊ฐ ๋งค์น ์นด๋: ์ฐธ๊ฐ์๋ช , ์ ์ ์ ๋ ฅ๋, ์น์ ํ์ด๋ผ์ดํธ
- ๋ชจ๋ฐ์ผ์์๋ ๊ฐ๋ก ์คํฌ๋กค๋ก ์ ์ฒด ๋์งํ ํ์ธ ๊ฐ๋ฅ
-
๊ฒฝ๊ธฐ ๊ฒฐ๊ณผ ์ ๋ ฅ (๊ด๋ฆฌ์ ์ ์ฉ)
- ๊ฐ ๋งค์น ์นด๋์ '๊ฒฐ๊ณผ ์ ๋ ฅ' ๋ฒํผ โ ๋ชจ๋ฌ ํ์ ์์ ์์ชฝ ์ฐธ๊ฐ์ ์ ์ ์ ๋ ฅ
- ์ ์ถ ์ AJAX๋ก ์๋ฒ ์ ์ก, ์น์ ์๋ ํ์ ๋ฐ ๋ค์ ๋ผ์ด๋ ์ง์ถ์ ๋ฐฐ์
- ์ด๋ฏธ ๊ฒฐ๊ณผ๊ฐ ์ ๋ ฅ๋ ๋งค์น๋ ์ ์ ํ์ & ์น์ ๊ตต์ ๊ธ์จ + ๋ฐฐ๊ฒฝ ํ์ด๋ผ์ดํธ
-
์ค์๊ฐ ๋์งํ ์ ๋ฐ์ดํธ
- ๊ฒฐ๊ณผ ์ ๋ ฅ ํ ์๋ก๊ณ ์นจ ์์ด ๋์งํ DOM ์ ๋ฐ์ดํธ (vanilla JS๋ก ๋ถ๋ถ ๋ ๋๋ง)
- ๋ค์ ๋ผ์ด๋ ๋งค์น๊ฐ ์๋ ์์ฑ๋๊ณ ์ง์ถ์๋ช ํ์
-
๋ํ ๋ชฉ๋ก ํ์ด์ง
- ์งํ ์ค/์๋ฃ ๋ํ ํํฐ๋ง
- ๊ฐ ๋ํ ์นด๋: ๋ํ๋ช , ์ข ๋ชฉ, ์ฐธ๊ฐ์ ์, ํ์ฌ ๋ผ์ด๋, ์ํ(์งํ ์ค/์๋ฃ), ์กฐํ ๋ฒํผ
-
๊ด๋ฆฌ์ ์ธ์ฆ
- 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)
);
๐ฏ ํต์ฌ ์๊ณ ๋ฆฌ์ฆ
-
์ฑ๊ธ ์๋ฆฌ๋ฏธ๋ค์ด์ ๋ธ๋ํท ์์ฑ:
- ์ฐธ๊ฐ์ ์
n์์2^k >= n์ธ ์ต์k๊ณ์ฐ โ ์ด ๋ผ์ด๋ ์ - 1๋ผ์ด๋ ๋งค์น ์ =
n / 2(๋ถ์ ์น ์ ์ธ), 2๋ผ์ด๋ =n / 4, ..., ๊ฒฐ์น = 1 - ๋ถ์ ์น(Bye) ๋ฐฐ์ :
2^k - n๊ฐ ํ์ โ ์๋ ์์์์๊ฒ ์ฐ์ ๋ฐฐ์
- ์ฐธ๊ฐ์ ์
-
๋ค์ ๋ผ์ด๋ ์ง์ถ์ ๋ฐฐ์ :
- ๋งค์น ๊ฒฐ๊ณผ ์
๋ ฅ ์
winner_idํ์ โnext_match_id๊ฐ ๊ฐ๋ฆฌํค๋ ๋งค์น์player1_id๋๋player2_id์ ์ฝ์ - ์: 1๋ผ์ด๋ ๋งค์น 1๋ฒ ์น์ โ 2๋ผ์ด๋ ๋งค์น 1๋ฒ์
player1_id, 1๋ผ์ด๋ ๋งค์น 2๋ฒ ์น์ โ 2๋ผ์ด๋ ๋งค์น 1๋ฒ์player2_id
- ๋งค์น ๊ฒฐ๊ณผ ์
๋ ฅ ์
-
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์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
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์ด ์ ์ฉ๋๊ณ ์ ๋ชฉ/๋ฒํผ์ด ํ์๋จ. ๋คํฌ๋ชจ๋ ์ ํ ์ ๋ฐฐ๊ฒฝ/ํ ์คํธ ์์์ด ์๋ ๋ณ๊ฒฝ๋จ.
**๋ชฉํ**: 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 ์ฐ๊ฒฐ ์๋ฌ ์์ด ์ ์ ์๋.
**๋ชฉํ**: ๋ํ ์์ฑ ํผ ํ์ด์ง๋ฅผ ๋ง๋ค๊ณ , ์ฐธ๊ฐ์ ๋ช
๋จ ์
๋ ฅยทํ ๋๋จผํธ ํ์ ์ ํ ๊ธฐ๋ฅ์ ๊ตฌํํฉ๋๋ค.
**์์
๋ด์ฉ**:
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์ ๋ํยท์ฐธ๊ฐ์ ์ฝ์ ์ฑ๊ณต. ๋ฆฌ๋ค์ด๋ ํธ ์ ์ ์๋ (๋ค์ ๋จ๊ณ์์ ๋์งํ ํ์ด์ง ๊ตฌํ).
**๋ชฉํ**: ์ฑ๊ธ ์๋ฆฌ๋ฏธ๋ค์ด์
๋ธ๋ํท ์๋ ์์ฑ ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํ๊ณ , ์ด๊ธฐ ๋งค์น ๋ฐ์ดํฐ๋ฅผ 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 ์ฐ๊ฒฐ ์ ์.
**๋ชฉํ**: ๋์งํ ์กฐํ ํ์ด์ง๋ฅผ ๋ง๋ค๊ณ , 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 ๋ธ๋ํท ๋ ๋๋ง ์ฑ๊ณต. ๋งค์น ์นด๋๊ฐ ๋ผ์ด๋๋ณ๋ก ์ ๋ ฌ๋์ด ํ์๋จ. ์น์ ํ์ด๋ผ์ดํธ ์ ์ ์๋.
**๋ชฉํ**: ๊ฒฝ๊ธฐ ๊ฒฐ๊ณผ ์
๋ ฅ ๊ธฐ๋ฅ์ ๊ตฌํํ๊ณ , 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 ์์ฒญ์ผ๋ก ๋งค์น ์ ๋ฐ์ดํธ ์ฑ๊ณต. ๋ค์ ๋ผ์ด๋ ์ง์ถ์ ์๋ ๋ฐฐ์ . ํ์ด์ง ์๋ก๊ณ ์นจ ํ ๋ณ๊ฒฝ ์ฌํญ ๋ฐ์.
**๋ชฉํ**: ๊ฒฐ๊ณผ ์
๋ ฅ ํ ํ์ด์ง ์๋ก๊ณ ์นจ ์์ด ๋์งํ๋ฅผ ์ค์๊ฐ ์
๋ฐ์ดํธํ๊ณ , ์น์ ์ ๋๋ฉ์ด์
์ ์ถ๊ฐํฉ๋๋ค.
**์์
๋ด์ฉ**:
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 ์ ๋ฐ์ดํธ ๊ตฌํ ์๋ฃ. ์น์ ์ ๋๋ฉ์ด์ ์ ์ ์๋. ๋ค์ ๋ผ์ด๋ ์ง์ถ์ ์๋ ํ์.
**๋ชฉํ**: ๋๋ธ ์๋ฆฌ๋ฏธ๋ค์ด์
(ํจ์๋ถํ์ ) ๋ธ๋ํท ์์ฑ ๋ก์ง์ ์ถ๊ฐํ๊ณ , ์น์/ํจ์ ๋ธ๋ํท์ ๋์ ๋ ๋๋งํฉ๋๋ค.
**์์
๋ด์ฉ**:
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
- ์น์ ๋ธ๋ํท ํจ์๊ฐ ํจ์ ๋ธ๋ํท์ผ๋ก ์ด๋ํ๋ ๋ก์ง์ ๋ณต์กํ๋ฏ๋ก ๊ฐ์ํ (์ค๋ฌด์์๋ ๋ ์ ๊ตํ ๋งคํ ํ์)
**์์ ๊ฒฐ๊ณผ**: ๋๋ธ ์๋ฆฌ๋ฏธ๋ค์ด์
๋ํ ์์ฑ ์ ์น์/ํจ์ ๋ธ๋ํท์ด ๋ชจ๋ ์์ฑ๋๊ณ , ์น์ ๋ธ๋ํท ํจ์๊ฐ ํจ์ ๋ธ๋ํท์ผ๋ก ์๋ ์ด๋ํฉ๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๋๋ธ ์๋ฆฌ๋ฏธ๋ค์ด์ ๋ธ๋ํท ์์ฑ ์๋ฃ. ์น์/ํจ์ ๋ธ๋ํท ๋์ ๋ ๋๋ง. ํจ์ ์ด๋ ๋ก์ง ์ ์ ์๋.
**๋ชฉํ**: ๋ํ ์ญ์ ๊ธฐ๋ฅ๊ณผ ์๋ฃ ์ํ ์๋ ๊ฐ์ง๋ฅผ ์ถ๊ฐํ๊ณ , ๋ํ ๋ชฉ๋ก ํ์ด์ง๋ฅผ ๊ฐ์ ํฉ๋๋ค.
**์์
๋ด์ฉ**:
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()`์ผ๋ก ์ญ์ ํ์ธ ๋ค์ด์ผ๋ก๊ทธ
- ๋ํ ์๋ฃ ์ฌ๋ถ๋ ๊ฒฐ์น์ ์น์ ์กด์ฌ ์ฌ๋ถ๋ก ํ๋จ
**์์ ๊ฒฐ๊ณผ**: ๊ด๋ฆฌ์๊ฐ ๋ํ๋ฅผ ์ญ์ ํ ์ ์๊ณ , ๊ฒฐ์น์ ์ข
๋ฃ ์ ๋ํ๊ฐ ์๋์ผ๋ก ์๋ฃ ์ํ๋ก ๋ณ๊ฒฝ๋ฉ๋๋ค. ๋ชฉ๋ก ํ์ด์ง์์ ํํฐ๋ง ๋ฐ ์ฐ์น์ ํ์๊ฐ ์ ์ ์๋ํฉ๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๋ํ ์ญ์ ๊ธฐ๋ฅ ๊ตฌํ ์๋ฃ. ์๋ฃ ์ํ ์๋ ๊ฐ์ง ์ ์ ์๋. ๋ํ ๋ชฉ๋ก ํํฐ๋ง ๋ฐ ์ฐ์น์ ํ์ ์์ฑ.
**๋ชฉํ**: ๋ชจ๋ฐ์ผ ์ต์ ํ, ์ ๊ทผ์ฑ ๊ฐ์ , ์ต์ข
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)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!