๐ฏ g5se ์ค๋์ ํ๋ก ์คํ๋ ๋ ํด์๊ธฐ - 3์ฅ ์นด๋ ๋ฆฌ๋ฉ & ์ด์ธ ์ ์ฅ
g5se-tarot-spread-interpreter
๋ฌธ์ ์ค๋ช
๐ ํ๋ก์ ํธ ๊ฐ์
ํ๋ก ์นด๋ 3์ฅ ์คํ๋ ๋(๊ณผ๊ฑฐ-ํ์ฌ-๋ฏธ๋)๋ฅผ ๋ฝ๊ณ , ๊ฐ ์นด๋์ ์๋ฏธ์ ์ ์ฒด ํด์์ ์ ๊ณตํ๋ ํ๋ก ๋ฆฌ๋ฉ ํ์ด์ง์ ๋๋ค. ์ฌ์ฉ์๋ ๋ฆฌ๋ฉ ๊ฒฐ๊ณผ๋ฅผ ์ ์ฅํ๊ณ ๊ณผ๊ฑฐ ๊ธฐ๋ก์ ๋๋์๋ณผ ์ ์์ต๋๋ค.
โจ ํ์ ๊ธฐ๋ฅ
1. ํ๋ก ์นด๋ ๋ฑ & ์คํ๋ ๋
- ๋ฉ์ด์ ์๋ฅด์นด๋ 22์ฅ ์นด๋ ๋ฐ์ดํฐ (์นด๋๋ช , ์ ๋ฐฉํฅ/์ญ๋ฐฉํฅ ํค์๋, ์ด๋ฏธ์ง)
- "๊ณผ๊ฑฐ-ํ์ฌ-๋ฏธ๋" 3์ฅ ์คํ๋ ๋ ๋ฌด์์ ๋ฝ๊ธฐ
- ๊ฐ ์นด๋๋ง๋ค ์ ๋ฐฉํฅ/์ญ๋ฐฉํฅ ๋๋ค ๊ฒฐ์
- ์นด๋ ๋ฝ๊ธฐ ์ ๋๋ฉ์ด์ (๋ค์งํ๋ ํจ๊ณผ)
2. ์นด๋ ํด์ ํ์
- ๊ฐ ์นด๋๋ณ ์์น(๊ณผ๊ฑฐ/ํ์ฌ/๋ฏธ๋) ๋ผ๋ฒจ๋ง
- ์นด๋ ์ด๋ฏธ์ง + ์ ์ญ๋ฐฉํฅ ํ์
- ํด๋น ์นด๋์ ํค์๋ & ๊ฐ๋จํ ์๋ฏธ ์ค๋ช
- 3์ฅ ์ ์ฒด๋ฅผ ์ข ํฉํ "์ค๋์ ์ด์ธ ํด์" ๋ฌธ๊ตฌ ์๋ ์์ฑ
3. ๋ฆฌ๋ฉ ๊ธฐ๋ก ์ ์ฅ (SQLite)
- ๋ฆฌ๋ฉ ๋ ์ง, 3์ฅ ์นด๋ ์ ๋ณด(์นด๋๋ช , ์ ์ญ๋ฐฉํฅ), ์ข ํฉ ํด์ ์ ์ฅ
- ์ ์ฅ ์ ํ์ ID ์ฐ๋ (๋นํ์์ ์ธ์ ID๋ก ์์ ์ ์ฅ)
- ๊ณผ๊ฑฐ ๋ฆฌ๋ฉ ๋ชฉ๋ก ํ์ด์ง: ๋ ์ง๋ณ ์นด๋ ์กฐํฉ & ํด์ ์์ฝ
- ์์ธ ๋ณด๊ธฐ: ์ ์ฅ๋ ๋ฆฌ๋ฉ ๋ค์ ๋ณด๊ธฐ
4. ๋ฐ์ํ UI & ๋คํฌ๋ชจ๋
- modern shell ์ ์ฉ์ผ๋ก ์ฌ์ดํธ ์ ์ฒด nav/footer/๋คํฌ๋ชจ๋ ํ ๊ธ ์๋ ์ฐ๋
- ์นด๋ ์ด๋ฏธ์ง๋ CDN ๋๋ public asset์ผ๋ก ์ ๊ณต (์: Rider-Waite ๋ฑ ์ด๋ฏธ์ง)
- ๋ชจ๋ฐ์ผ์์๋ ์นด๋ 3์ฅ์ด ์ธ๋ก ์คํ์ผ๋ก ๊น๋ํ๊ฒ ํ์
๐ ๋ณด๋์ค ๊ธฐ๋ฅ
- ์นด๋ ๋ฝ๊ธฐ ์ "์ง๋ฌธ ์ ๋ ฅ" ํผ (๋ฆฌ๋ฉ ์ฃผ์ ๊ธฐ๋ก)
- ๋ฆฌ๋ฉ ๊ฒฐ๊ณผ ๊ณต์ ๋งํฌ ์์ฑ (๊ณ ์ URL๋ก ๋ค๋ฅธ ์ฌ๋๋ ๋ณผ ์ ์๊ฒ)
- ์นด๋ ์์ธ ์ค๋ช ๋ชจ๋ฌ (๊ฐ ์นด๋ ํด๋ฆญ ์ ํ ๋์คํฌ๋ฆฝ์ ํ์ )
- ๋ฆฌ๋ฉ ํต๊ณ: ๊ฐ์ฅ ๋ง์ด ๋์จ ์นด๋ TOP 3
๐จ ๋์์ธ ๊ฐ์ด๋
- ํ๋ก ๋ถ์๊ธฐ์ ๋ง๋ ๋ฏธ์คํฑํ ์์ (๋ณด๋ผ/๋จ์ ๊ณ์ด)
- ์นด๋ ๋ท๋ฉด ๋์์ธ์ CSS๋ก ํจํด ๊ตฌํ (๋๋ placeholder ์ด๋ฏธ์ง)
- ๋ฆฌ๋ฉ ๊ฒฐ๊ณผ๋ ์นด๋๋ณ๋ก ๊ตฌ๋ถ๋ ์น์ ๋ ์ด์์
- ์ ์ฅ ๋ฒํผ์ ๋์ ๋๋ ์์น์ ๋ฐฐ์น
๐ ํ์ต ํฌ์ธํธ
- g5se ํด๋ ๊ธฐ๋ฐ ๋ผ์ฐํ
ํ์ฉ (
app/modules/tarot-spread/index.php) - ๋ชจ๋ ์ ์ฉ SQLite DB ์ค๊ณ (๋ฆฌ๋ฉ ๊ธฐ๋ก CRUD)
- ๋๋ค ๋ก์ง (์นด๋ ์ ํ, ์ ์ญ๋ฐฉํฅ ๊ฒฐ์ )
- AJAX ์์ด ํผ ์ ์ถ๋ก ๋ฆฌ๋ฉ ์ ์ฅ & ํ์ด์ง ๋ฆฌ๋ค์ด๋ ํธ
- modern shell๋ก ์ฌ์ดํธ ์ผ๊ด์ฑ ์ ์ง
์ ์ฝ ์กฐ๊ฑด
- โข g5se ํด๋ ๊ธฐ๋ฐ ๋ผ์ฐํ ์ฌ์ฉ (๋ผ์ฐํธ ๋ฑ๋ก ๋ถํ์)
- โข modern shell (_head.inc.php + m-shell + _nav.inc.php + m-container + _footer.inc.php) ํ์ ์ ์ฉ
- โข ๋์์ธ ํ ํฐ (--m-surface, --m-text ๋ฑ) ํ์ฉ์ผ๋ก ๋คํฌ๋ชจ๋ ์๋ ๋์
- โข ๋ชจ๋ ์ ์ฉ SQLite (G5_DATA_PATH/modules/tarot-spread.sqlite) ์ฌ์ฉ
- โข g5se ๋ฉ์ธ DB์ ํ ์ด๋ธ ์์ฑ ๊ธ์ง
- โข ์ธ๋ถ ๋น๋ ๋๊ตฌ(npm/webpack) ์ฌ์ฉ ๊ธ์ง
- โข CDN ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ต์ํ (์์ HTML/CSS/JS ๊ถ์ฅ)
- โข prepared statement๋ก SQL ์ธ์ ์ ๋ฐฉ์ง
- โข htmlspecialchars()๋ก XSS ๋ฐฉ์ง
- โข ํ์/๋นํ์ ๋ชจ๋ ์ด์ฉ ๊ฐ๋ฅํ๋๋ก ์ค๊ณ
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
g5se๊ฐ ์ค์น๋ ๋๋ ํฐ๋ฆฌ(๋ฃจํธ์ `index.php`ยท`app/`์ด ์๋ ๊ณณ)์์ ์์
์ ์์ํฉ๋๋ค. (g5se ์ค์น๋ https://github.com/gnuboard/g5se ์ฐธ๊ณ )
## Step 1: ํ๋ก ์คํ๋ ๋ ํ์ด์ง ๊ณจ๊ฒฉ ์์ฑ
`app/modules/tarot-spread/index.php` ํ์ผ์ ๋ง๋ค์ด์ฃผ์ธ์. ์ด ํ์ผ์ ์๋์ผ๋ก `/tarot-spread` URL์ ๋งคํ๋ฉ๋๋ค.
### ํ์ผ ๋ด์ฉ:
```php
<?php
include_once('./_common.php');
// ํ๋ก ์นด๋ ๋ฑ ๋ฐ์ดํฐ (๋ฉ์ด์ ์๋ฅด์นด๋ 22์ฅ)
$tarot_deck = [
['name' => 'The Fool', 'upright' => '์๋ก์ด ์์, ์์ํจ, ์์ ', 'reversed' => '๋ฌด๋ชจํจ, ๊ฒฝ์ํจ, ๋ฆฌ์คํฌ'],
['name' => 'The Magician', 'upright' => '์ฐฝ์กฐ๋ ฅ, ์์ง, ๊ธฐ์ ', 'reversed' => '์กฐ์, ์์์, ์ฌ๋ฅ ๋ญ๋น'],
['name' => 'The High Priestess', 'upright' => '์ง๊ด, ์ ๋น, ์ ์ฌ์์', 'reversed' => '๋น๋ฐ, ๋จ์ , ์นจ๋ฌต'],
['name' => 'The Empress', 'upright' => 'ํ์, ๋ชจ์ฑ, ์์ฐ', 'reversed' => '์์กด, ์ง์, ๊ณตํํจ'],
['name' => 'The Emperor', 'upright' => '๊ถ์, ๊ตฌ์กฐ, ํต์ ', 'reversed' => '๋
์ฌ, ๊ฒฝ์ง, ๋ฏธ์ฑ์'],
['name' => 'The Hierophant', 'upright' => '์ ํต, ๊ต์ก, ์ ๋
', 'reversed' => '๋ฐํญ, ์์ ์ฌ์, ๋น์ ํต'],
['name' => 'The Lovers', 'upright' => '์ฌ๋, ์กฐํ, ์ ํ', 'reversed' => '๋ถ๊ท ํ, ๊ฐ๋ฑ, ์๋ชป๋ ์ ํ'],
['name' => 'The Chariot', 'upright' => '์์ง, ์น๋ฆฌ, ๊ฒฐ๋จ', 'reversed' => '๋ฐฉํฅ ์์ค, ์ข์ , ๊ณต๊ฒฉ์ฑ'],
['name' => 'Strength', 'upright' => '์ฉ๊ธฐ, ์ธ๋ด, ์์ ๋ ฅ', 'reversed' => '์๊ธฐ ์์ฌ, ์ฝํจ, ์์ ๋ ฅ ๋ถ์กฑ'],
['name' => 'The Hermit', 'upright' => '๋ด๋ฉด ํ๊ตฌ, ๊ณ ๋
, ์งํ', 'reversed' => '๊ณ ๋ฆฝ, ์ธ๋ก์, ์ฒ ํ'],
['name' => 'Wheel of Fortune', 'upright' => '์ด๋ช
, ๋ณํ, ์ํ', 'reversed' => '๋ถ์ด, ์ ํญ, ์ ์ฒด'],
['name' => 'Justice', 'upright' => '๊ณต์ , ์ง์ค, ๋ฒ', 'reversed' => '๋ถ๊ณต์ , ๊ฑฐ์ง, ์ฑ
์ ํํผ'],
['name' => 'The Hanged Man', 'upright' => 'ํฌ์, ์๋ก์ด ๊ด์ , ๋๊ธฐ', 'reversed' => '์ง์ฐ, ์ ํญ, ํ๋ ํฌ์'],
['name' => 'Death', 'upright' => '์ข
๊ฒฐ, ๋ณํ, ์ฌํ์', 'reversed' => '์ ํญ, ์ ์ฒด, ๋ด๋ฉด์ ์ ํ'],
['name' => 'Temperance', 'upright' => '๊ท ํ, ์ ์ , ์น์ ', 'reversed' => '๋ถ๊ท ํ, ๊ณผ์, ๋ถ์กฐํ'],
['name' => 'The Devil', 'upright' => '์๋ฐ, ์ค๋
, ๋ฌผ์ง์ฃผ์', 'reversed' => 'ํด๋ฐฉ, ์๊ฐ, ํ์ถ'],
['name' => 'The Tower', 'upright' => 'ํ๊ดด, ํผ๋, ๊ณ์', 'reversed' => '์ฌ๋ ํํผ, ๋๋ ค์, ์ง์ฐ๋ ๋ณํ'],
['name' => 'The Star', 'upright' => 'ํฌ๋ง, ์๊ฐ, ํ์จ', 'reversed' => '์ ๋ง, ์ ๋ขฐ ์์ค, ๋จ์ '],
['name' => 'The Moon', 'upright' => 'ํ์, ๋๋ ค์, ์ ์ฌ์์', 'reversed' => 'ํผ๋ ํด์, ์ง์ค, ๋ถ์ ๊ทน๋ณต'],
['name' => 'The Sun', 'upright' => '๊ธฐ์จ, ์ฑ๊ณต, ํ๋ ฅ', 'reversed' => '๋น๊ด, ์ฐ์ธ, ์ง์ฐ๋ ํ๋ณต'],
['name' => 'Judgement', 'upright' => '๊ฐ์ฑ, ๊ฒฐ๋จ, ์ฌ์', 'reversed' => '์๊ธฐ ์์ฌ, ํํ, ํ๋จ ์ ๋ณด'],
['name' => 'The World', 'upright' => '์์ฑ, ์ฑ์ทจ, ์ฌํ', 'reversed' => '๋ฏธ์์ฑ, ์ง์ฐ, ๋ถ์กฑํจ']
];
require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<style>
.tarot-hero {
text-align: center;
padding: 48px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: var(--m-radius-lg);
margin-bottom: 32px;
}
.tarot-hero h1 {
font-size: 2rem;
font-weight: 700;
margin: 0 0 12px;
}
.tarot-hero p {
font-size: 1.1rem;
opacity: 0.95;
margin: 0;
}
.draw-section {
text-align: center;
padding: 32px 20px;
background: var(--m-surface);
border-radius: var(--m-radius-lg);
box-shadow: var(--m-shadow);
margin-bottom: 32px;
}
.draw-btn {
display: inline-block;
padding: 16px 48px;
font-size: 1.2rem;
font-weight: 600;
color: white;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: var(--m-radius-lg);
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.draw-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
}
.info-box {
background: var(--m-surface);
padding: 24px;
border-radius: var(--m-radius-lg);
box-shadow: var(--m-shadow);
margin-bottom: 32px;
}
.info-box h3 {
color: var(--m-text);
font-size: 1.3rem;
font-weight: 600;
margin: 0 0 16px;
}
.info-box ul {
color: var(--m-text-muted);
line-height: 1.8;
margin: 0;
padding-left: 20px;
}
</style>
<div class="m-shell">
<?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
<main class="m-container" style="padding: 32px 20px 48px; max-width: 800px; margin: 0 auto;">
<div class="tarot-hero">
<h1>๐ฎ ์ค๋์ ํ๋ก ์คํ๋ ๋</h1>
<p>๊ณผ๊ฑฐ-ํ์ฌ-๋ฏธ๋ 3์ฅ ์นด๋๋ก ๋น์ ์ ์ด์ธ๋ฅผ ํ์ธํ์ธ์</p>
</div>
<div class="draw-section">
<form method="post" action="">
<button type="submit" name="draw" class="draw-btn">๐ด ์นด๋ ๋ฝ๊ธฐ</button>
</form>
</div>
<div class="info-box">
<h3>๐ ํ๋ก ๋ฆฌ๋ฉ์ด๋?</h3>
<ul>
<li><strong>๊ณผ๊ฑฐ ์นด๋:</strong> ํ์ฌ ์ํฉ์ ์์ธ์ด ๋ ๊ณผ๊ฑฐ ์ฌ๊ฑด์ด๋ ์ํฅ</li>
<li><strong>ํ์ฌ ์นด๋:</strong> ์ง๊ธ ๋น์ ์ด ์ฒํ ์ํฉ๊ณผ ์๋์ง</li>
<li><strong>๋ฏธ๋ ์นด๋:</strong> ํ์ฌ ํ๋ฆ์ด ์ด์ด์ง ๋ ๋ค๊ฐ์ฌ ๊ฒฐ๊ณผ</li>
</ul>
</div>
<div class="info-box">
<h3>โจ ์ด์ฉ ๋ฐฉ๋ฒ</h3>
<ul>
<li>๋ง์์ ๊ฐ๋ผ์ํ๊ณ ์ง๋ฌธ์ด๋ ๊ณ ๋ฏผ์ ๋ ์ฌ๋ ค๋ณด์ธ์</li>
<li>"์นด๋ ๋ฝ๊ธฐ" ๋ฒํผ์ ํด๋ฆญํ์ฌ 3์ฅ์ ์นด๋๋ฅผ ๋ฐ์ต๋๋ค</li>
<li>๊ฐ ์นด๋์ ์๋ฏธ๋ฅผ ์ฝ๊ณ ์ ์ฒด ํด์์ ํ์ธํ์ธ์</li>
<li>๋ฆฌ๋ฉ ๊ฒฐ๊ณผ๋ ์ ์ฅํ์ฌ ๋์ค์ ๋ค์ ๋ณผ ์ ์์ต๋๋ค</li>
</ul>
</div>
</main>
<?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<?php include_once('./tail.sub.php'); ?>
```
### ์ฃผ์ ํฌ์ธํธ:
1. **modern shell ๊ณจ๊ฒฉ ์ ์ฉ**: `_head.inc.php` โ `m-shell` โ `_nav.inc.php` โ `m-container` โ `_footer.inc.php` โ `tail.sub.php` ์์๋ก ์ฌ์ดํธ ์ ์ฒด chrome ํต์ผ
2. **ํ๋ก ๋ฑ ๋ฐ์ดํฐ**: ๋ฉ์ด์ ์๋ฅด์นด๋ 22์ฅ ๋ฐฐ์ด (์นด๋๋ช
, ์ ๋ฐฉํฅ/์ญ๋ฐฉํฅ ํค์๋)
3. **ํ์ด๋ก ์น์
**: ๊ทธ๋ผ๋์ธํธ ๋ฐฐ๊ฒฝ์ผ๋ก ํ๋ก ๋ถ์๊ธฐ ์ฐ์ถ
4. **์นด๋ ๋ฝ๊ธฐ ๋ฒํผ**: ํผ ์ ์ถ ๋ฐฉ์ (๋ค์ ๋จ๊ณ์์ POST ์ฒ๋ฆฌ)
5. **์๋ด ๋ฐ์ค**: ํ๋ก ๋ฆฌ๋ฉ ์ค๋ช
& ์ด์ฉ ๋ฐฉ๋ฒ
ํ์ผ์ ์ ์ฅํ๊ณ ๋ธ๋ผ์ฐ์ ์์ `/tarot-spread`์ ์ ์ํ์ฌ ํ์ด์ง๊ฐ ์ ์์ ์ผ๋ก ํ์๋๋์ง ํ์ธํด์ฃผ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: `/tarot-spread` URL๋ก ์ ๊ทผ ์ ํ์ด๋ก ์น์ , ์นด๋ ๋ฝ๊ธฐ ๋ฒํผ, ์๋ด ๋ฐ์ค๊ฐ ํ์๋๋ ํ์ด์ง๊ฐ ๋ ๋๋ง๋ฉ๋๋ค. modern shell์ด ์ ์ฉ๋์ด ์๋จ nav์ ํ๋จ footer๊ฐ ์๋์ผ๋ก ํ์๋๋ฉฐ, ๋คํฌ๋ชจ๋ ํ ๊ธ๋ ์๋ํฉ๋๋ค.
## Step 2: ์นด๋ ๋ฝ๊ธฐ ๋ก์ง & ๋ฆฌ๋ฉ ๊ฒฐ๊ณผ ํ์
`app/modules/tarot-spread/index.php` ํ์ผ์ ์์ ํ์ฌ ์นด๋ ๋ฝ๊ธฐ POST ์ฒ๋ฆฌ์ ๊ฒฐ๊ณผ ํ๋ฉด์ ์ถ๊ฐํด์ฃผ์ธ์.
### ์์ ๋ด์ฉ:
1. **POST ์ฒ๋ฆฌ ๋ก์ง** (ํ์ผ ์๋จ, `require_once` ์ ์ ์ถ๊ฐ):
```php
<?php
include_once('./_common.php');
// ํ๋ก ์นด๋ ๋ฑ ๋ฐ์ดํฐ
$tarot_deck = [
['name' => 'The Fool', 'upright' => '์๋ก์ด ์์, ์์ํจ, ์์ ', 'reversed' => '๋ฌด๋ชจํจ, ๊ฒฝ์ํจ, ๋ฆฌ์คํฌ'],
['name' => 'The Magician', 'upright' => '์ฐฝ์กฐ๋ ฅ, ์์ง, ๊ธฐ์ ', 'reversed' => '์กฐ์, ์์์, ์ฌ๋ฅ ๋ญ๋น'],
['name' => 'The High Priestess', 'upright' => '์ง๊ด, ์ ๋น, ์ ์ฌ์์', 'reversed' => '๋น๋ฐ, ๋จ์ , ์นจ๋ฌต'],
['name' => 'The Empress', 'upright' => 'ํ์, ๋ชจ์ฑ, ์์ฐ', 'reversed' => '์์กด, ์ง์, ๊ณตํํจ'],
['name' => 'The Emperor', 'upright' => '๊ถ์, ๊ตฌ์กฐ, ํต์ ', 'reversed' => '๋
์ฌ, ๊ฒฝ์ง, ๋ฏธ์ฑ์'],
['name' => 'The Hierophant', 'upright' => '์ ํต, ๊ต์ก, ์ ๋
', 'reversed' => '๋ฐํญ, ์์ ์ฌ์, ๋น์ ํต'],
['name' => 'The Lovers', 'upright' => '์ฌ๋, ์กฐํ, ์ ํ', 'reversed' => '๋ถ๊ท ํ, ๊ฐ๋ฑ, ์๋ชป๋ ์ ํ'],
['name' => 'The Chariot', 'upright' => '์์ง, ์น๋ฆฌ, ๊ฒฐ๋จ', 'reversed' => '๋ฐฉํฅ ์์ค, ์ข์ , ๊ณต๊ฒฉ์ฑ'],
['name' => 'Strength', 'upright' => '์ฉ๊ธฐ, ์ธ๋ด, ์์ ๋ ฅ', 'reversed' => '์๊ธฐ ์์ฌ, ์ฝํจ, ์์ ๋ ฅ ๋ถ์กฑ'],
['name' => 'The Hermit', 'upright' => '๋ด๋ฉด ํ๊ตฌ, ๊ณ ๋
, ์งํ', 'reversed' => '๊ณ ๋ฆฝ, ์ธ๋ก์, ์ฒ ํ'],
['name' => 'Wheel of Fortune', 'upright' => '์ด๋ช
, ๋ณํ, ์ํ', 'reversed' => '๋ถ์ด, ์ ํญ, ์ ์ฒด'],
['name' => 'Justice', 'upright' => '๊ณต์ , ์ง์ค, ๋ฒ', 'reversed' => '๋ถ๊ณต์ , ๊ฑฐ์ง, ์ฑ
์ ํํผ'],
['name' => 'The Hanged Man', 'upright' => 'ํฌ์, ์๋ก์ด ๊ด์ , ๋๊ธฐ', 'reversed' => '์ง์ฐ, ์ ํญ, ํ๋ ํฌ์'],
['name' => 'Death', 'upright' => '์ข
๊ฒฐ, ๋ณํ, ์ฌํ์', 'reversed' => '์ ํญ, ์ ์ฒด, ๋ด๋ฉด์ ์ ํ'],
['name' => 'Temperance', 'upright' => '๊ท ํ, ์ ์ , ์น์ ', 'reversed' => '๋ถ๊ท ํ, ๊ณผ์, ๋ถ์กฐํ'],
['name' => 'The Devil', 'upright' => '์๋ฐ, ์ค๋
, ๋ฌผ์ง์ฃผ์', 'reversed' => 'ํด๋ฐฉ, ์๊ฐ, ํ์ถ'],
['name' => 'The Tower', 'upright' => 'ํ๊ดด, ํผ๋, ๊ณ์', 'reversed' => '์ฌ๋ ํํผ, ๋๋ ค์, ์ง์ฐ๋ ๋ณํ'],
['name' => 'The Star', 'upright' => 'ํฌ๋ง, ์๊ฐ, ํ์จ', 'reversed' => '์ ๋ง, ์ ๋ขฐ ์์ค, ๋จ์ '],
['name' => 'The Moon', 'upright' => 'ํ์, ๋๋ ค์, ์ ์ฌ์์', 'reversed' => 'ํผ๋ ํด์, ์ง์ค, ๋ถ์ ๊ทน๋ณต'],
['name' => 'The Sun', 'upright' => '๊ธฐ์จ, ์ฑ๊ณต, ํ๋ ฅ', 'reversed' => '๋น๊ด, ์ฐ์ธ, ์ง์ฐ๋ ํ๋ณต'],
['name' => 'Judgement', 'upright' => '๊ฐ์ฑ, ๊ฒฐ๋จ, ์ฌ์', 'reversed' => '์๊ธฐ ์์ฌ, ํํ, ํ๋จ ์ ๋ณด'],
['name' => 'The World', 'upright' => '์์ฑ, ์ฑ์ทจ, ์ฌํ', 'reversed' => '๋ฏธ์์ฑ, ์ง์ฐ, ๋ถ์กฑํจ']
];
$spread = null;
$interpretation = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['draw'])) {
// ์นด๋ ์
ํ & 3์ฅ ๋ฝ๊ธฐ
$shuffled = $tarot_deck;
shuffle($shuffled);
$selected = array_slice($shuffled, 0, 3);
// ๊ฐ ์นด๋๋ง๋ค ์ ๋ฐฉํฅ/์ญ๋ฐฉํฅ ๋๋ค ๊ฒฐ์
$spread = [
[
'position' => '๊ณผ๊ฑฐ',
'card' => $selected[0],
'reversed' => rand(0, 1) === 1,
'emoji' => 'โณ'
],
[
'position' => 'ํ์ฌ',
'card' => $selected[1],
'reversed' => rand(0, 1) === 1,
'emoji' => 'โจ'
],
[
'position' => '๋ฏธ๋',
'card' => $selected[2],
'reversed' => rand(0, 1) === 1,
'emoji' => '๐ฎ'
]
];
// ์ข
ํฉ ํด์ ์์ฑ (๊ฐ๋จํ ํ
ํ๋ฆฟ)
$past_msg = $spread[0]['reversed'] ? '์ด๋ ค์' : '๊ธ์ ์ ์ํฅ';
$present_msg = $spread[1]['reversed'] ? '๋์ ์ ์ธ ์ํฉ' : '์ข์ ํ๋ฆ';
$future_msg = $spread[2]['reversed'] ? '์ฃผ์๊ฐ ํ์ํ ์๊ธฐ' : '๋ฐ์ ์ ๋ง';
$interpretation = "๊ณผ๊ฑฐ์๋ {$past_msg}์ด ์์๊ณ , ํ์ฌ๋ {$present_msg}์
๋๋ค. ์ด ํ๋ฆ์ด ๊ณ์๋๋ฉด ๋ฏธ๋์๋ {$future_msg}์ด ์์๋ฉ๋๋ค. ๊ฐ ์นด๋์ ๋ฉ์์ง๋ฅผ ์ฐธ๊ณ ํ์ฌ ํ๋ช
ํ ์ ํ์ ํ์ธ์.";
}
require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
```
2. **์คํ์ผ ์ถ๊ฐ** (๊ธฐ์กด `<style>` ๋ธ๋ก ํ๋จ์ ์ถ๊ฐ):
```css
.spread-result {
margin-bottom: 32px;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 32px;
}
.card-item {
background: var(--m-surface);
border-radius: var(--m-radius-lg);
box-shadow: var(--m-shadow);
padding: 24px;
text-align: center;
transition: transform 0.3s;
}
.card-item:hover {
transform: translateY(-4px);
}
.card-position {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 1.1rem;
font-weight: 600;
color: #667eea;
margin-bottom: 12px;
}
.card-name {
font-size: 1.3rem;
font-weight: 700;
color: var(--m-text);
margin: 12px 0;
}
.card-orientation {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 12px;
}
.card-orientation.upright {
background: #d4edda;
color: #155724;
}
.card-orientation.reversed {
background: #f8d7da;
color: #721c24;
}
.card-meaning {
color: var(--m-text-muted);
line-height: 1.6;
font-size: 0.95rem;
}
.interpretation-box {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 32px;
border-radius: var(--m-radius-lg);
margin-bottom: 32px;
}
.interpretation-box h3 {
font-size: 1.5rem;
font-weight: 700;
margin: 0 0 16px;
}
.interpretation-box p {
font-size: 1.1rem;
line-height: 1.8;
margin: 0;
}
.action-buttons {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
}
.btn {
padding: 12px 32px;
font-size: 1rem;
font-weight: 600;
border-radius: var(--m-radius-lg);
border: none;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-secondary {
background: var(--m-surface);
color: var(--m-text);
box-shadow: var(--m-shadow);
}
.btn:hover {
transform: translateY(-2px);
}
```
3. **๊ฒฐ๊ณผ ํ๋ฉด HTML** (`<main>` ํ๊ทธ ์, draw-section ์๋์ ์ถ๊ฐ):
```php
<?php if ($spread): ?>
<div class="spread-result">
<div class="cards-grid">
<?php foreach ($spread as $item): ?>
<div class="card-item">
<div class="card-position">
<span><?php echo $item['emoji']; ?></span>
<span><?php echo htmlspecialchars($item['position']); ?></span>
</div>
<div class="card-name"><?php echo htmlspecialchars($item['card']['name']); ?></div>
<span class="card-orientation <?php echo $item['reversed'] ? 'reversed' : 'upright'; ?>">
<?php echo $item['reversed'] ? '์ญ๋ฐฉํฅ โ' : '์ ๋ฐฉํฅ โ'; ?>
</span>
<div class="card-meaning">
<?php
$meaning = $item['reversed'] ? $item['card']['reversed'] : $item['card']['upright'];
echo htmlspecialchars($meaning);
?>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="interpretation-box">
<h3>๐ฎ ์ข
ํฉ ํด์</h3>
<p><?php echo htmlspecialchars($interpretation); ?></p>
</div>
<div class="action-buttons">
<form method="post" action="">
<button type="submit" name="draw" class="btn btn-primary">๐ ๋ค์ ๋ฝ๊ธฐ</button>
</form>
<button type="button" class="btn btn-secondary" onclick="alert('์ ์ฅ ๊ธฐ๋ฅ์ ๋ค์ ๋จ๊ณ์์ ๊ตฌํ๋ฉ๋๋ค!')">๐พ ๊ฒฐ๊ณผ ์ ์ฅ</button>
</div>
</div>
<?php endif; ?>
```
### ์ฃผ์ ๋ณ๊ฒฝ ์ฌํญ:
1. **์นด๋ ๋ฝ๊ธฐ ๋ก์ง**: POST ์์ฒญ ์ ๋ฑ์ ์
ํํ๊ณ 3์ฅ ์ ํ, ๊ฐ ์นด๋๋ง๋ค ์ ์ญ๋ฐฉํฅ ๋๋ค ๊ฒฐ์
2. **์ข
ํฉ ํด์**: 3์ฅ ์นด๋์ ์ ์ญ๋ฐฉํฅ ์กฐํฉ์ ๋ฐ๋ผ ๊ฐ๋จํ ๋ฉ์์ง ์์ฑ
3. **๊ทธ๋ฆฌ๋ ๋ ์ด์์**: ์นด๋ 3์ฅ์ ๋ฐ์ํ ๊ทธ๋ฆฌ๋๋ก ํ์ (๋ชจ๋ฐ์ผ์์๋ ์ธ๋ก ์คํ)
4. **์นด๋๋ณ ์ ๋ณด**: ์์น(๊ณผ๊ฑฐ/ํ์ฌ/๋ฏธ๋), ์นด๋๋ช
, ์ ์ญ๋ฐฉํฅ, ํค์๋ ํ์
5. **์ก์
๋ฒํผ**: ๋ค์ ๋ฝ๊ธฐ & ์ ์ฅ ๋ฒํผ (์ ์ฅ์ ๋ค์ ๋จ๊ณ์์ ๊ตฌํ)
ํ์ผ์ ์ ์ฅํ๊ณ "์นด๋ ๋ฝ๊ธฐ" ๋ฒํผ์ ํด๋ฆญํ์ฌ ๋ฆฌ๋ฉ ๊ฒฐ๊ณผ๊ฐ ํ์๋๋์ง ํ์ธํด์ฃผ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ์นด๋ ๋ฝ๊ธฐ ๋ฒํผ ํด๋ฆญ ์ ํ์ด์ง๊ฐ ์๋ก๊ณ ์นจ๋๋ฉฐ 3์ฅ์ ํ๋ก ์นด๋๊ฐ ๊ทธ๋ฆฌ๋ ํํ๋ก ํ์๋ฉ๋๋ค. ๊ฐ ์นด๋๋ง๋ค ์์น(๊ณผ๊ฑฐ/ํ์ฌ/๋ฏธ๋), ์นด๋๋ช , ์ ์ญ๋ฐฉํฅ, ํค์๋๊ฐ ํ์๋๊ณ , ํ๋จ์ ์ข ํฉ ํด์ ๋ฐ์ค๊ฐ ๋ํ๋ฉ๋๋ค. ๋ค์ ๋ฝ๊ธฐ ๋ฒํผ์ผ๋ก ์๋ก์ด ๋ฆฌ๋ฉ์ ์์ฑํ ์ ์์ต๋๋ค.
## Step 3: SQLite DB ์ค์ & ๋ฆฌ๋ฉ ์ ์ฅ ๊ธฐ๋ฅ
`app/modules/tarot-spread/index.php` ํ์ผ์ ์์ ํ์ฌ ๋ฆฌ๋ฉ ๊ฒฐ๊ณผ๋ฅผ SQLite DB์ ์ ์ฅํ๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํด์ฃผ์ธ์.
### ์์ ๋ด์ฉ:
1. **DB ์ฐ๊ฒฐ & ํ
์ด๋ธ ์์ฑ ํจ์** (ํ์ผ ์๋จ, ํ๋ก ๋ฑ ๋ฐ์ดํฐ ์๋์ ์ถ๊ฐ):
```php
// SQLite DB ์ฐ๊ฒฐ & ํ
์ด๋ธ ์์ฑ
function getTarotDB() {
@mkdir(G5_DATA_PATH.'/modules', 0707, true);
$db_path = G5_DATA_PATH.'/modules/tarot-spread.sqlite';
$db = new PDO('sqlite:'.$db_path);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// ๋ฆฌ๋ฉ ๊ธฐ๋ก ํ
์ด๋ธ
$db->exec("
CREATE TABLE IF NOT EXISTS readings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
reading_date DATETIME DEFAULT CURRENT_TIMESTAMP,
card1_name TEXT NOT NULL,
card1_reversed INTEGER NOT NULL,
card2_name TEXT NOT NULL,
card2_reversed INTEGER NOT NULL,
card3_name TEXT NOT NULL,
card3_reversed INTEGER NOT NULL,
interpretation TEXT NOT NULL
)
");
return $db;
}
```
2. **์ ์ฅ ์ฒ๋ฆฌ ๋ก์ง** (์นด๋ ๋ฝ๊ธฐ ๋ก์ง ์๋, `require_once` ์ ์ ์ถ๊ฐ):
```php
// ๋ฆฌ๋ฉ ์ ์ฅ ์ฒ๋ฆฌ
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_reading'])) {
$db = getTarotDB();
// ํ์์ด๋ฉด ํ์ ID, ๋นํ์์ด๋ฉด ์ธ์
ID
$user_id = $is_member ? $member['mb_id'] : session_id();
$stmt = $db->prepare("
INSERT INTO readings (
user_id, card1_name, card1_reversed, card2_name, card2_reversed,
card3_name, card3_reversed, interpretation
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$user_id,
$_POST['card1_name'],
(int)$_POST['card1_reversed'],
$_POST['card2_name'],
(int)$_POST['card2_reversed'],
$_POST['card3_name'],
(int)$_POST['card3_reversed'],
$_POST['interpretation']
]);
// ์ ์ฅ ํ ๋ฆฌ๋ค์ด๋ ํธ
header('Location: /tarot-spread?saved=1');
exit;
}
// ์ ์ฅ ์ฑ๊ณต ๋ฉ์์ง
$saved_message = '';
if (isset($_GET['saved']) && $_GET['saved'] === '1') {
$saved_message = '๋ฆฌ๋ฉ ๊ฒฐ๊ณผ๊ฐ ์ ์ฅ๋์์ต๋๋ค!';
}
```
3. **์ ์ฅ ํผ ์์ ** (๊ฒฐ๊ณผ ํ๋ฉด์ ์ ์ฅ ๋ฒํผ ๋ถ๋ถ์ ๋ค์๊ณผ ๊ฐ์ด ๋ณ๊ฒฝ):
```php
<div class="action-buttons">
<form method="post" action="" style="display: inline;">
<button type="submit" name="draw" class="btn btn-primary">๐ ๋ค์ ๋ฝ๊ธฐ</button>
</form>
<form method="post" action="" style="display: inline;">
<input type="hidden" name="card1_name" value="<?php echo htmlspecialchars($spread[0]['card']['name']); ?>">
<input type="hidden" name="card1_reversed" value="<?php echo $spread[0]['reversed'] ? '1' : '0'; ?>">
<input type="hidden" name="card2_name" value="<?php echo htmlspecialchars($spread[1]['card']['name']); ?>">
<input type="hidden" name="card2_reversed" value="<?php echo $spread[1]['reversed'] ? '1' : '0'; ?>">
<input type="hidden" name="card3_name" value="<?php echo htmlspecialchars($spread[2]['card']['name']); ?>">
<input type="hidden" name="card3_reversed" value="<?php echo $spread[2]['reversed'] ? '1' : '0'; ?>">
<input type="hidden" name="interpretation" value="<?php echo htmlspecialchars($interpretation); ?>">
<button type="submit" name="save_reading" class="btn btn-secondary">๐พ ๊ฒฐ๊ณผ ์ ์ฅ</button>
</form>
</div>
```
4. **์ ์ฅ ์ฑ๊ณต ์๋ฆผ** (ํ์ด๋ก ์น์
๋ฐ๋ก ์๋์ ์ถ๊ฐ):
```php
<?php if ($saved_message): ?>
<div style="background: #d4edda; color: #155724; padding: 16px 24px; border-radius: var(--m-radius-lg); margin-bottom: 24px; text-align: center; font-weight: 600;">
โ
<?php echo htmlspecialchars($saved_message); ?>
</div>
<?php endif; ?>
```
### ์ฃผ์ ๋ณ๊ฒฝ ์ฌํญ:
1. **SQLite DB ํจ์**: `G5_DATA_PATH/modules/tarot-spread.sqlite` ํ์ผ์ DB ์์ฑ, ์ฒซ ์ ๊ทผ ์ ํ
์ด๋ธ ์๋ ์์ฑ
2. **๋ฆฌ๋ฉ ์ ์ฅ**: ์นด๋ 3์ฅ ์ ๋ณด(์ด๋ฆ, ์ ์ญ๋ฐฉํฅ)์ ์ข
ํฉ ํด์์ DB์ INSERT
3. **์ฌ์ฉ์ ์๋ณ**: ํ์์ `mb_id`, ๋นํ์์ `session_id()`๋ก ๊ตฌ๋ถ
4. **์ ์ฅ ํ ๋ฆฌ๋ค์ด๋ ํธ**: PRG ํจํด์ผ๋ก ์ค๋ณต ์ ์ถ ๋ฐฉ์ง
5. **์ฑ๊ณต ๋ฉ์์ง**: URL ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ก ์ ์ฅ ์ฑ๊ณต ์๋ฆผ ํ์
ํ์ผ์ ์ ์ฅํ๊ณ ์นด๋๋ฅผ ๋ฝ์ ๋ค "๊ฒฐ๊ณผ ์ ์ฅ" ๋ฒํผ์ ํด๋ฆญํ์ฌ DB์ ์ ์ฅ๋๋์ง ํ์ธํด์ฃผ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๋ฆฌ๋ฉ ๊ฒฐ๊ณผ ํ๋ฉด์์ "๊ฒฐ๊ณผ ์ ์ฅ" ๋ฒํผ ํด๋ฆญ ์ SQLite DB์ ๋ฐ์ดํฐ๊ฐ ์ ์ฅ๋๊ณ , ํ์ด์ง๊ฐ ์๋ก๊ณ ์นจ๋๋ฉฐ ์๋จ์ "๋ฆฌ๋ฉ ๊ฒฐ๊ณผ๊ฐ ์ ์ฅ๋์์ต๋๋ค!" ์ฑ๊ณต ๋ฉ์์ง๊ฐ ํ์๋ฉ๋๋ค. `data/modules/tarot-spread.sqlite` ํ์ผ์ด ์์ฑ๋๊ณ readings ํ ์ด๋ธ์ ๋ ์ฝ๋๊ฐ ์ถ๊ฐ๋ฉ๋๋ค.
## Step 4: ๊ณผ๊ฑฐ ๋ฆฌ๋ฉ ๋ชฉ๋ก ํ์ด์ง
`app/modules/tarot-spread/history/index.php` ํ์ผ์ ์๋ก ๋ง๋ค์ด ์ ์ฅ๋ ๋ฆฌ๋ฉ ๊ธฐ๋ก์ ๋ชฉ๋ก์ผ๋ก ๋ณด์ฌ์ฃผ๋ ํ์ด์ง๋ฅผ ๊ตฌํํด์ฃผ์ธ์.
### ํ์ผ ์์ฑ:
`app/modules/tarot-spread/history/index.php`:
```php
<?php
include_once('./_common.php');
// SQLite DB ์ฐ๊ฒฐ ํจ์ (๋ฉ์ธ ํ์ด์ง์ ๋์ผ)
function getTarotDB() {
@mkdir(G5_DATA_PATH.'/modules', 0707, true);
$db_path = G5_DATA_PATH.'/modules/tarot-spread.sqlite';
$db = new PDO('sqlite:'.$db_path);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec("
CREATE TABLE IF NOT EXISTS readings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
reading_date DATETIME DEFAULT CURRENT_TIMESTAMP,
card1_name TEXT NOT NULL,
card1_reversed INTEGER NOT NULL,
card2_name TEXT NOT NULL,
card2_reversed INTEGER NOT NULL,
card3_name TEXT NOT NULL,
card3_reversed INTEGER NOT NULL,
interpretation TEXT NOT NULL
)
");
return $db;
}
// ํ์ฌ ์ฌ์ฉ์ ID
$user_id = $is_member ? $member['mb_id'] : session_id();
// ๋ฆฌ๋ฉ ๊ธฐ๋ก ์กฐํ (์ต์ ์)
$db = getTarotDB();
$stmt = $db->prepare("SELECT * FROM readings WHERE user_id = ? ORDER BY reading_date DESC");
$stmt->execute([$user_id]);
$readings = $stmt->fetchAll(PDO::FETCH_ASSOC);
require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<style>
.history-hero {
text-align: center;
padding: 48px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: var(--m-radius-lg);
margin-bottom: 32px;
}
.history-hero h1 {
font-size: 2rem;
font-weight: 700;
margin: 0 0 12px;
}
.history-hero p {
font-size: 1.1rem;
opacity: 0.95;
margin: 0 0 16px;
}
.back-btn {
display: inline-block;
padding: 10px 24px;
background: rgba(255, 255, 255, 0.2);
color: white;
border-radius: var(--m-radius-lg);
text-decoration: none;
font-weight: 600;
transition: background 0.2s;
}
.back-btn:hover {
background: rgba(255, 255, 255, 0.3);
}
.readings-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.reading-card {
background: var(--m-surface);
border-radius: var(--m-radius-lg);
box-shadow: var(--m-shadow);
padding: 24px;
transition: transform 0.2s;
}
.reading-card:hover {
transform: translateY(-2px);
}
.reading-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 12px;
}
.reading-date {
font-size: 0.95rem;
color: var(--m-text-muted);
}
.reading-id {
font-size: 0.85rem;
color: var(--m-text-muted);
background: rgba(102, 126, 234, 0.1);
padding: 4px 12px;
border-radius: 12px;
}
.cards-summary {
display: flex;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.card-badge {
background: rgba(102, 126, 234, 0.1);
padding: 8px 16px;
border-radius: var(--m-radius-lg);
font-size: 0.9rem;
}
.card-badge strong {
color: #667eea;
font-weight: 600;
}
.card-badge .orientation {
font-size: 0.8rem;
color: var(--m-text-muted);
margin-left: 4px;
}
.interpretation-preview {
color: var(--m-text-muted);
line-height: 1.6;
font-size: 0.95rem;
margin-bottom: 16px;
}
.view-detail-btn {
display: inline-block;
padding: 8px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: var(--m-radius-lg);
text-decoration: none;
font-weight: 600;
font-size: 0.9rem;
transition: transform 0.2s;
}
.view-detail-btn:hover {
transform: translateY(-2px);
}
.empty-state {
text-align: center;
padding: 64px 20px;
background: var(--m-surface);
border-radius: var(--m-radius-lg);
box-shadow: var(--m-shadow);
}
.empty-state .emoji {
font-size: 4rem;
margin-bottom: 16px;
}
.empty-state h3 {
font-size: 1.5rem;
color: var(--m-text);
margin: 0 0 12px;
}
.empty-state p {
color: var(--m-text-muted);
margin: 0 0 24px;
}
.empty-state a {
display: inline-block;
padding: 12px 32px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: var(--m-radius-lg);
text-decoration: none;
font-weight: 600;
}
</style>
<div class="m-shell">
<?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
<main class="m-container" style="padding: 32px 20px 48px; max-width: 900px; margin: 0 auto;">
<div class="history-hero">
<h1>๐ ๋์ ํ๋ก ๋ฆฌ๋ฉ ๊ธฐ๋ก</h1>
<p>์ ์ฅ๋ ๋ฆฌ๋ฉ ๊ฒฐ๊ณผ๋ฅผ ๋ค์ ํ์ธํ์ธ์</p>
<a href="/tarot-spread" class="back-btn">โ ์ ๋ฆฌ๋ฉ ํ๋ฌ ๊ฐ๊ธฐ</a>
</div>
<?php if (count($readings) > 0): ?>
<div class="readings-list">
<?php foreach ($readings as $reading): ?>
<div class="reading-card">
<div class="reading-header">
<div class="reading-date">
๐๏ธ <?php echo date('Y๋
m์ d์ผ H:i', strtotime($reading['reading_date'])); ?>
</div>
<div class="reading-id">#<?php echo $reading['id']; ?></div>
</div>
<div class="cards-summary">
<div class="card-badge">
<strong>โณ ๊ณผ๊ฑฐ:</strong> <?php echo htmlspecialchars($reading['card1_name']); ?>
<span class="orientation"><?php echo $reading['card1_reversed'] ? '(์ญ๋ฐฉํฅ)' : '(์ ๋ฐฉํฅ)'; ?></span>
</div>
<div class="card-badge">
<strong>โจ ํ์ฌ:</strong> <?php echo htmlspecialchars($reading['card2_name']); ?>
<span class="orientation"><?php echo $reading['card2_reversed'] ? '(์ญ๋ฐฉํฅ)' : '(์ ๋ฐฉํฅ)'; ?></span>
</div>
<div class="card-badge">
<strong>๐ฎ ๋ฏธ๋:</strong> <?php echo htmlspecialchars($reading['card3_name']); ?>
<span class="orientation"><?php echo $reading['card3_reversed'] ? '(์ญ๋ฐฉํฅ)' : '(์ ๋ฐฉํฅ)'; ?></span>
</div>
</div>
<div class="interpretation-preview">
<?php
$preview = mb_substr($reading['interpretation'], 0, 80, 'UTF-8');
if (mb_strlen($reading['interpretation'], 'UTF-8') > 80) {
$preview .= '...';
}
echo htmlspecialchars($preview);
?>
</div>
<a href="/tarot-spread/view?id=<?php echo $reading['id']; ?>" class="view-detail-btn">์์ธํ ๋ณด๊ธฐ โ</a>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-state">
<div class="emoji">๐ฎ</div>
<h3>์์ง ์ ์ฅ๋ ๋ฆฌ๋ฉ์ด ์์ด์</h3>
<p>์ฒซ ํ๋ก ๋ฆฌ๋ฉ์ ํด๋ณด์๊ฒ ์ด์?</p>
<a href="/tarot-spread">์นด๋ ๋ฝ์ผ๋ฌ ๊ฐ๊ธฐ โ</a>
</div>
<?php endif; ?>
</main>
<?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<?php include_once('./tail.sub.php'); ?>
```
### ์ฃผ์ ๊ธฐ๋ฅ:
1. **๋ฆฌ๋ฉ ๋ชฉ๋ก ์กฐํ**: ํ์ฌ ์ฌ์ฉ์(ํ์/๋นํ์)์ ๋ฆฌ๋ฉ ๊ธฐ๋ก์ ์ต์ ์์ผ๋ก ๊ฐ์ ธ์ด
2. **์นด๋ ์์ฝ**: ๊ฐ ๋ฆฌ๋ฉ๋ง๋ค 3์ฅ ์นด๋์ ์ด๋ฆ๊ณผ ์ ์ญ๋ฐฉํฅ์ ๋ฐฐ์ง ํํ๋ก ํ์
3. **ํด์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ**: ์ข
ํฉ ํด์์ ์ฒซ 80์๋ง ํ์ (๋ง์ค์ํ)
4. **์์ธ ๋ณด๊ธฐ ๋งํฌ**: ๊ฐ ๋ฆฌ๋ฉ์ ์์ธ ํ์ด์ง๋ก ์ด๋ํ๋ ๋ฒํผ (๋ค์ ๋จ๊ณ์์ ๊ตฌํ)
5. **๋น ์ํ**: ์ ์ฅ๋ ๋ฆฌ๋ฉ์ด ์์ ๋ ์๋ด ๋ฉ์์ง์ ์ ๋ฆฌ๋ฉ ๋งํฌ
ํ์ผ์ ์ ์ฅํ๊ณ `/tarot-spread/history`์ ์ ์ํ์ฌ ๋ชฉ๋ก์ด ํ์๋๋์ง ํ์ธํด์ฃผ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: `/tarot-spread/history` URL๋ก ์ ๊ทผ ์ ์ ์ฅ๋ ๋ฆฌ๋ฉ ๊ธฐ๋ก์ด ์นด๋ ํํ๋ก ๋ชฉ๋ก ํ์๋ฉ๋๋ค. ๊ฐ ์นด๋์๋ ๋ฆฌ๋ฉ ๋ ์ง, 3์ฅ ์นด๋ ์์ฝ, ํด์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ, ์์ธํ ๋ณด๊ธฐ ๋ฒํผ์ด ํฌํจ๋ฉ๋๋ค. ์ ์ฅ๋ ๊ธฐ๋ก์ด ์์ผ๋ฉด ๋น ์ํ ์๋ด ํ๋ฉด์ด ๋ํ๋ฉ๋๋ค.
## Step 5: ๋ฆฌ๋ฉ ์์ธ ๋ณด๊ธฐ ํ์ด์ง & ๋ฉ์ธ ํ์ด์ง ๋งํฌ ์ถ๊ฐ
### 1. ์์ธ ๋ณด๊ธฐ ํ์ด์ง ์์ฑ
`app/modules/tarot-spread/view/index.php` ํ์ผ์ ๋ง๋ค์ด์ฃผ์ธ์:
```php
<?php
include_once('./_common.php');
// SQLite DB ์ฐ๊ฒฐ ํจ์
function getTarotDB() {
@mkdir(G5_DATA_PATH.'/modules', 0707, true);
$db_path = G5_DATA_PATH.'/modules/tarot-spread.sqlite';
$db = new PDO('sqlite:'.$db_path);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec("
CREATE TABLE IF NOT EXISTS readings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
reading_date DATETIME DEFAULT CURRENT_TIMESTAMP,
card1_name TEXT NOT NULL,
card1_reversed INTEGER NOT NULL,
card2_name TEXT NOT NULL,
card2_reversed INTEGER NOT NULL,
card3_name TEXT NOT NULL,
card3_reversed INTEGER NOT NULL,
interpretation TEXT NOT NULL
)
");
return $db;
}
// ํ๋ก ์นด๋ ๋ฑ ๋ฐ์ดํฐ (ํค์๋ ์กฐํ์ฉ)
$tarot_deck = [
'The Fool' => ['upright' => '์๋ก์ด ์์, ์์ํจ, ์์ ', 'reversed' => '๋ฌด๋ชจํจ, ๊ฒฝ์ํจ, ๋ฆฌ์คํฌ'],
'The Magician' => ['upright' => '์ฐฝ์กฐ๋ ฅ, ์์ง, ๊ธฐ์ ', 'reversed' => '์กฐ์, ์์์, ์ฌ๋ฅ ๋ญ๋น'],
'The High Priestess' => ['upright' => '์ง๊ด, ์ ๋น, ์ ์ฌ์์', 'reversed' => '๋น๋ฐ, ๋จ์ , ์นจ๋ฌต'],
'The Empress' => ['upright' => 'ํ์, ๋ชจ์ฑ, ์์ฐ', 'reversed' => '์์กด, ์ง์, ๊ณตํํจ'],
'The Emperor' => ['upright' => '๊ถ์, ๊ตฌ์กฐ, ํต์ ', 'reversed' => '๋
์ฌ, ๊ฒฝ์ง, ๋ฏธ์ฑ์'],
'The Hierophant' => ['upright' => '์ ํต, ๊ต์ก, ์ ๋
', 'reversed' => '๋ฐํญ, ์์ ์ฌ์, ๋น์ ํต'],
'The Lovers' => ['upright' => '์ฌ๋, ์กฐํ, ์ ํ', 'reversed' => '๋ถ๊ท ํ, ๊ฐ๋ฑ, ์๋ชป๋ ์ ํ'],
'The Chariot' => ['upright' => '์์ง, ์น๋ฆฌ, ๊ฒฐ๋จ', 'reversed' => '๋ฐฉํฅ ์์ค, ์ข์ , ๊ณต๊ฒฉ์ฑ'],
'Strength' => ['upright' => '์ฉ๊ธฐ, ์ธ๋ด, ์์ ๋ ฅ', 'reversed' => '์๊ธฐ ์์ฌ, ์ฝํจ, ์์ ๋ ฅ ๋ถ์กฑ'],
'The Hermit' => ['upright' => '๋ด๋ฉด ํ๊ตฌ, ๊ณ ๋
, ์งํ', 'reversed' => '๊ณ ๋ฆฝ, ์ธ๋ก์, ์ฒ ํ'],
'Wheel of Fortune' => ['upright' => '์ด๋ช
, ๋ณํ, ์ํ', 'reversed' => '๋ถ์ด, ์ ํญ, ์ ์ฒด'],
'Justice' => ['upright' => '๊ณต์ , ์ง์ค, ๋ฒ', 'reversed' => '๋ถ๊ณต์ , ๊ฑฐ์ง, ์ฑ
์ ํํผ'],
'The Hanged Man' => ['upright' => 'ํฌ์, ์๋ก์ด ๊ด์ , ๋๊ธฐ', 'reversed' => '์ง์ฐ, ์ ํญ, ํ๋ ํฌ์'],
'Death' => ['upright' => '์ข
๊ฒฐ, ๋ณํ, ์ฌํ์', 'reversed' => '์ ํญ, ์ ์ฒด, ๋ด๋ฉด์ ์ ํ'],
'Temperance' => ['upright' => '๊ท ํ, ์ ์ , ์น์ ', 'reversed' => '๋ถ๊ท ํ, ๊ณผ์, ๋ถ์กฐํ'],
'The Devil' => ['upright' => '์๋ฐ, ์ค๋
, ๋ฌผ์ง์ฃผ์', 'reversed' => 'ํด๋ฐฉ, ์๊ฐ, ํ์ถ'],
'The Tower' => ['upright' => 'ํ๊ดด, ํผ๋, ๊ณ์', 'reversed' => '์ฌ๋ ํํผ, ๋๋ ค์, ์ง์ฐ๋ ๋ณํ'],
'The Star' => ['upright' => 'ํฌ๋ง, ์๊ฐ, ํ์จ', 'reversed' => '์ ๋ง, ์ ๋ขฐ ์์ค, ๋จ์ '],
'The Moon' => ['upright' => 'ํ์, ๋๋ ค์, ์ ์ฌ์์', 'reversed' => 'ํผ๋ ํด์, ์ง์ค, ๋ถ์ ๊ทน๋ณต'],
'The Sun' => ['upright' => '๊ธฐ์จ, ์ฑ๊ณต, ํ๋ ฅ', 'reversed' => '๋น๊ด, ์ฐ์ธ, ์ง์ฐ๋ ํ๋ณต'],
'Judgement' => ['upright' => '๊ฐ์ฑ, ๊ฒฐ๋จ, ์ฌ์', 'reversed' => '์๊ธฐ ์์ฌ, ํํ, ํ๋จ ์ ๋ณด'],
'The World' => ['upright' => '์์ฑ, ์ฑ์ทจ, ์ฌํ', 'reversed' => '๋ฏธ์์ฑ, ์ง์ฐ, ๋ถ์กฑํจ']
];
// ํ์ฌ ์ฌ์ฉ์ ID
$user_id = $is_member ? $member['mb_id'] : session_id();
// ๋ฆฌ๋ฉ ID ๊ฐ์ ธ์ค๊ธฐ
$reading_id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($reading_id === 0) {
die('์๋ชป๋ ์ ๊ทผ์
๋๋ค.');
}
// ๋ฆฌ๋ฉ ์กฐํ (๋ณธ์ธ ๊ฒ๋ง)
$db = getTarotDB();
$stmt = $db->prepare("SELECT * FROM readings WHERE id = ? AND user_id = ?");
$stmt->execute([$reading_id, $user_id]);
$reading = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$reading) {
die('๋ฆฌ๋ฉ์ ์ฐพ์ ์ ์์ต๋๋ค.');
}
// ์นด๋ ์ ๋ณด ์ฌ๊ตฌ์ฑ
$cards = [
[
'position' => '๊ณผ๊ฑฐ',
'emoji' => 'โณ',
'name' => $reading['card1_name'],
'reversed' => (bool)$reading['card1_reversed'],
'meaning' => isset($tarot_deck[$reading['card1_name']])
? ($reading['card1_reversed'] ? $tarot_deck[$reading['card1_name']]['reversed'] : $tarot_deck[$reading['card1_name']]['upright'])
: ''
],
[
'position' => 'ํ์ฌ',
'emoji' => 'โจ',
'name' => $reading['card2_name'],
'reversed' => (bool)$reading['card2_reversed'],
'meaning' => isset($tarot_deck[$reading['card2_name']])
? ($reading['card2_reversed'] ? $tarot_deck[$reading['card2_name']]['reversed'] : $tarot_deck[$reading['card2_name']]['upright'])
: ''
],
[
'position' => '๋ฏธ๋',
'emoji' => '๐ฎ',
'name' => $reading['card3_name'],
'reversed' => (bool)$reading['card3_reversed'],
'meaning' => isset($tarot_deck[$reading['card3_name']])
? ($reading['card3_reversed'] ? $tarot_deck[$reading['card3_name']]['reversed'] : $tarot_deck[$reading['card3_name']]['upright'])
: ''
]
];
require_once(G5_THEME_PATH.'/modern/_head.inc.php');
include_once('./head.sub.php');
?>
<style>
.detail-hero {
text-align: center;
padding: 48px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: var(--m-radius-lg);
margin-bottom: 32px;
}
.detail-hero h1 {
font-size: 2rem;
font-weight: 700;
margin: 0 0 12px;
}
.detail-hero .date {
font-size: 1.1rem;
opacity: 0.95;
margin: 0 0 16px;
}
.back-link {
display: inline-block;
padding: 10px 24px;
background: rgba(255, 255, 255, 0.2);
color: white;
border-radius: var(--m-radius-lg);
text-decoration: none;
font-weight: 600;
transition: background 0.2s;
}
.back-link:hover {
background: rgba(255, 255, 255, 0.3);
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 32px;
}
.card-item {
background: var(--m-surface);
border-radius: var(--m-radius-lg);
box-shadow: var(--m-shadow);
padding: 24px;
text-align: center;
}
.card-position {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 1.1rem;
font-weight: 600;
color: #667eea;
margin-bottom: 12px;
}
.card-name {
font-size: 1.3rem;
font-weight: 700;
color: var(--m-text);
margin: 12px 0;
}
.card-orientation {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 12px;
}
.card-orientation.upright {
background: #d4edda;
color: #155724;
}
.card-orientation.reversed {
background: #f8d7da;
color: #721c24;
}
.card-meaning {
color: var(--m-text-muted);
line-height: 1.6;
font-size: 0.95rem;
}
.interpretation-box {
background: var(--m-surface);
border-radius: var(--m-radius-lg);
box-shadow: var(--m-shadow);
padding: 32px;
margin-bottom: 32px;
}
.interpretation-box h3 {
font-size: 1.5rem;
font-weight: 700;
color: var(--m-text);
margin: 0 0 16px;
}
.interpretation-box p {
font-size: 1.1rem;
line-height: 1.8;
color: var(--m-text);
margin: 0;
}
</style>
<div class="m-shell">
<?php require G5_THEME_PATH.'/modern/_nav.inc.php'; ?>
<main class="m-container" style="padding: 32px 20px 48px; max-width: 900px; margin: 0 auto;">
<div class="detail-hero">
<h1>๐ฎ ํ๋ก ๋ฆฌ๋ฉ ์์ธ๋ณด๊ธฐ</h1>
<div class="date">
๐๏ธ <?php echo date('Y๋
m์ d์ผ H:i', strtotime($reading['reading_date'])); ?>
</div>
<a href="/tarot-spread/history" class="back-link">โ ๋ชฉ๋ก์ผ๋ก ๋์๊ฐ๊ธฐ</a>
</div>
<div class="cards-grid">
<?php foreach ($cards as $card): ?>
<div class="card-item">
<div class="card-position">
<span><?php echo $card['emoji']; ?></span>
<span><?php echo htmlspecialchars($card['position']); ?></span>
</div>
<div class="card-name"><?php echo htmlspecialchars($card['name']); ?></div>
<span class="card-orientation <?php echo $card['reversed'] ? 'reversed' : 'upright'; ?>">
<?php echo $card['reversed'] ? '์ญ๋ฐฉํฅ โ' : '์ ๋ฐฉํฅ โ'; ?>
</span>
<div class="card-meaning">
<?php echo htmlspecialchars($card['meaning']); ?>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="interpretation-box">
<h3>๐ฎ ์ข
ํฉ ํด์</h3>
<p><?php echo nl2br(htmlspecialchars($reading['interpretation'])); ?></p>
</div>
</main>
<?php require G5_THEME_PATH.'/modern/_footer.inc.php'; ?>
</div>
<?php include_once('./tail.sub.php'); ?>
```
### 2. ๋ฉ์ธ ํ์ด์ง์ ๊ธฐ๋ก ๋ณด๊ธฐ ๋งํฌ ์ถ๊ฐ
`app/modules/tarot-spread/index.php`์ ํ์ด๋ก ์น์
์๋์ ๋งํฌ๋ฅผ ์ถ๊ฐํด์ฃผ์ธ์:
```php
<div class="tarot-hero">
<h1>๐ฎ ์ค๋์ ํ๋ก ์คํ๋ ๋</h1>
<p>๊ณผ๊ฑฐ-ํ์ฌ-๋ฏธ๋ 3์ฅ ์นด๋๋ก ๋น์ ์ ์ด์ธ๋ฅผ ํ์ธํ์ธ์</p>
</div>
<!-- ์ด ๋ถ๋ถ ์ถ๊ฐ -->
<div style="text-align: center; margin-bottom: 24px;">
<a href="/tarot-spread/history" style="display: inline-block; padding: 12px 32px; background: var(--m-surface); color: var(--m-text); border-radius: var(--m-radius-lg); text-decoration: none; font-weight: 600; box-shadow: var(--m-shadow); transition: transform 0.2s;" onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'">
๐ ๋์ ๋ฆฌ๋ฉ ๊ธฐ๋ก ๋ณด๊ธฐ
</a>
</div>
```
### ์ฃผ์ ๊ธฐ๋ฅ:
1. **์์ธ ๋ณด๊ธฐ ํ์ด์ง**: URL ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ก ๋ฆฌ๋ฉ ID๋ฅผ ๋ฐ์ ํด๋น ๋ฆฌ๋ฉ์ ์ ์ฒด ์ ๋ณด ํ์
2. **๊ถํ ํ์ธ**: ๋ณธ์ธ์ ๋ฆฌ๋ฉ๋ง ๋ณผ ์ ์๋๋ก user_id ์กฐ๊ฑด ์ถ๊ฐ
3. **์นด๋ ์ ๋ณด ์ฌ๊ตฌ์ฑ**: DB์์ ๊ฐ์ ธ์จ ์นด๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐฐ์ด๋ก ์ฌ๊ตฌ์ฑํ์ฌ ํ์
4. **์ ์ฒด ํด์ ํ์**: ์ข
ํฉ ํด์์ ์ค๋ฐ๊ฟ ํฌํจํ์ฌ ์ ์ฒด ํ์
5. **๋ด๋น๊ฒ์ด์
**: ๋ฉ์ธ ํ์ด์ง โ ๋ชฉ๋ก โ ์์ธ ํ์ด์ง ๊ฐ ์ด๋ ๋งํฌ
ํ์ผ์ ์ ์ฅํ๊ณ ๋ฆฌ๋ฉ ๋ชฉ๋ก์์ "์์ธํ ๋ณด๊ธฐ" ๋ฒํผ์ ํด๋ฆญํ์ฌ ์์ธ ํ์ด์ง๊ฐ ํ์๋๋์ง ํ์ธํด์ฃผ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๋ฆฌ๋ฉ ๋ชฉ๋ก์์ "์์ธํ ๋ณด๊ธฐ" ํด๋ฆญ ์ `/tarot-spread/view?id={๋ฆฌ๋ฉID}` URL๋ก ์ด๋ํ๋ฉฐ, ํด๋น ๋ฆฌ๋ฉ์ 3์ฅ ์นด๋ ์ ์ฒด ์ ๋ณด์ ์ข ํฉ ํด์์ด ํ์๋ฉ๋๋ค. ๋ฉ์ธ ํ์ด์ง์์ "๋์ ๋ฆฌ๋ฉ ๊ธฐ๋ก ๋ณด๊ธฐ" ๋งํฌ๋ฅผ ํตํด ๋ชฉ๋ก ํ์ด์ง๋ก ์ด๋ํ ์ ์์ต๋๋ค. ๋ชจ๋ ํ์ด์ง๊ฐ modern shell๋ก ํต์ผ๋ ๋์์ธ์ ์ ์งํฉ๋๋ค.
## Step 6: ์นด๋ ๋ค์ง๊ธฐ ์ ๋๋ฉ์ด์
& ์ต์ข
๋ค๋ฌ๊ธฐ
`app/modules/tarot-spread/index.php` ํ์ผ์ ์์ ํ์ฌ ์นด๋ ๋ฝ๊ธฐ ์ ๋ค์งํ๋ ์ ๋๋ฉ์ด์
์ ์ถ๊ฐํ๊ณ , ์ ์ฒด์ ์ธ UX๋ฅผ ๊ฐ์ ํด์ฃผ์ธ์.
### ์์ ๋ด์ฉ:
1. **์คํ์ผ ๋ธ๋ก์ ์ ๋๋ฉ์ด์
์ถ๊ฐ** (๊ธฐ์กด `<style>` ๋ธ๋ก ํ๋จ์ ์ถ๊ฐ):
```css
/* ์นด๋ ๋ค์ง๊ธฐ ์ ๋๋ฉ์ด์
*/
@keyframes flipCard {
0% {
transform: perspective(1000px) rotateY(0deg);
opacity: 0;
}
50% {
transform: perspective(1000px) rotateY(90deg);
opacity: 0.5;
}
100% {
transform: perspective(1000px) rotateY(0deg);
opacity: 1;
}
}
.card-item.animate {
animation: flipCard 0.8s ease-out;
}
.card-item:nth-child(1).animate {
animation-delay: 0s;
}
.card-item:nth-child(2).animate {
animation-delay: 0.2s;
}
.card-item:nth-child(3).animate {
animation-delay: 0.4s;
}
/* ์นด๋ ๋ท๋ฉด ํจ๊ณผ (์ ํ์ ) */
.card-back {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: var(--m-radius-lg);
padding: 80px 20px;
text-align: center;
color: white;
font-size: 3rem;
box-shadow: var(--m-shadow);
}
/* ๋ก๋ฉ ์ํ */
.loading-spinner {
display: inline-block;
width: 40px;
height: 40px;
border: 4px solid rgba(102, 126, 234, 0.2);
border-radius: 50%;
border-top-color: #667eea;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ํต๊ณ ๋ฐ์ค (๋ณด๋์ค) */
.stats-box {
background: var(--m-surface);
border-radius: var(--m-radius-lg);
box-shadow: var(--m-shadow);
padding: 24px;
margin-top: 32px;
}
.stats-box h3 {
color: var(--m-text);
font-size: 1.3rem;
font-weight: 600;
margin: 0 0 16px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
}
.stat-item {
text-align: center;
padding: 16px;
background: rgba(102, 126, 234, 0.05);
border-radius: var(--m-radius-lg);
}
.stat-number {
font-size: 2rem;
font-weight: 700;
color: #667eea;
margin-bottom: 4px;
}
.stat-label {
font-size: 0.9rem;
color: var(--m-text-muted);
}
```
2. **์นด๋ ๊ทธ๋ฆฌ๋์ ์ ๋๋ฉ์ด์
ํด๋์ค ์ถ๊ฐ** (๊ฒฐ๊ณผ ํ๋ฉด HTML ์์ ):
```php
<div class="cards-grid">
<?php foreach ($spread as $index => $item): ?>
<div class="card-item animate">
<!-- ๊ธฐ์กด ์นด๋ ๋ด์ฉ ๋์ผ -->
</div>
<?php endforeach; ?>
</div>
```
3. **ํต๊ณ ์น์
์ถ๊ฐ** (info-box ์๋์ ์ถ๊ฐ, ๋ณด๋์ค):
```php
<?php
// ๊ฐ๋จํ ํต๊ณ ์กฐํ
$db = getTarotDB();
$user_id = $is_member ? $member['mb_id'] : session_id();
$total_readings = $db->query("SELECT COUNT(*) FROM readings WHERE user_id = '{$user_id}'")->fetchColumn();
?>
<?php if ($total_readings > 0): ?>
<div class="stats-box">
<h3>๐ ๋์ ํ๋ก ํต๊ณ</h3>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-number"><?php echo $total_readings; ?></div>
<div class="stat-label">์ด ๋ฆฌ๋ฉ ํ์</div>
</div>
<div class="stat-item">
<div class="stat-number"><?php echo date('Y.m.d'); ?></div>
<div class="stat-label">์ค๋ ๋ ์ง</div>
</div>
<div class="stat-item">
<div class="stat-number">22</div>
<div class="stat-label">๋ฉ์ด์ ์๋ฅด์นด๋</div>
</div>
</div>
</div>
<?php endif; ?>
```
4. **์นด๋ ๋ฝ๊ธฐ ๋ฒํผ์ ๋ก๋ฉ ์ํ ์ถ๊ฐ** (์ ํ์ , JS ํ์ ์):
```php
<div class="draw-section">
<form method="post" action="" id="drawForm">
<button type="submit" name="draw" class="draw-btn" id="drawBtn">
๐ด ์นด๋ ๋ฝ๊ธฐ
</button>
</form>
</div>
<script>
document.getElementById('drawForm')?.addEventListener('submit', function() {
const btn = document.getElementById('drawBtn');
btn.innerHTML = '<span class="loading-spinner"></span>';
btn.disabled = true;
});
</script>
```
5. **README ์์ฑ** (ํ๋ก์ ํธ ๋ฃจํธ์ `README.md` ์์ฑ):
```markdown
# ๐ฎ g5se ์ค๋์ ํ๋ก ์คํ๋ ๋ ํด์๊ธฐ
๊ณผ๊ฑฐ-ํ์ฌ-๋ฏธ๋ 3์ฅ ์นด๋ ๋ฆฌ๋ฉ & ์ด์ธ ์ ์ฅ ํ๋ซํผ
## โจ ์ฃผ์ ๊ธฐ๋ฅ
- ๋ฉ์ด์ ์๋ฅด์นด๋ 22์ฅ ํ๋ก ๋ฑ
- 3์ฅ ์คํ๋ ๋ (๊ณผ๊ฑฐ-ํ์ฌ-๋ฏธ๋) ์๋ ์์ฑ
- ์ ๋ฐฉํฅ/์ญ๋ฐฉํฅ ๋๋ค ๊ฒฐ์
- ์นด๋๋ณ ํค์๋ & ์ข
ํฉ ํด์ ์ ๊ณต
- ๋ฆฌ๋ฉ ๊ฒฐ๊ณผ SQLite DB ์ ์ฅ
- ๊ณผ๊ฑฐ ๋ฆฌ๋ฉ ๊ธฐ๋ก ๋ชฉ๋ก & ์์ธ ๋ณด๊ธฐ
- ํ์/๋นํ์ ๋ชจ๋ ์ด์ฉ ๊ฐ๋ฅ
- ๋คํฌ๋ชจ๋ ์๋ ๋์
- ๋ฐ์ํ ๋์์ธ
## ๐ ์ค์น ๋ฐฉ๋ฒ
1. g5se ์ค์น (https://github.com/gnuboard/g5se)
2. `app/modules/tarot-spread/` ํด๋์ ํ์ผ ๋ณต์ฌ
3. `/tarot-spread` ์ ์ํ์ฌ ์นด๋ ๋ฝ๊ธฐ
## ๐ ํ์ผ ๊ตฌ์กฐ
```
app/modules/tarot-spread/
โโโ index.php # ๋ฉ์ธ ํ์ด์ง (์นด๋ ๋ฝ๊ธฐ)
โโโ history/
โ โโโ index.php # ๋ฆฌ๋ฉ ๊ธฐ๋ก ๋ชฉ๋ก
โโโ view/
โโโ index.php # ๋ฆฌ๋ฉ ์์ธ ๋ณด๊ธฐ
data/modules/
โโโ tarot-spread.sqlite # ๋ฆฌ๋ฉ ๊ธฐ๋ก DB (์๋ ์์ฑ)
```
## ๐จ ์ปค์คํฐ๋ง์ด์ง
- ํ๋ก ๋ฑ ๋ฐ์ดํฐ๋ `$tarot_deck` ๋ฐฐ์ด์์ ์์ ๊ฐ๋ฅ
- ์ข
ํฉ ํด์ ๋ก์ง์ ์นด๋ ๋ฝ๊ธฐ ์น์
์์ ์ปค์คํฐ๋ง์ด์ง
- ์์/๋ ์ด์์์ ์ธ๋ผ์ธ ์คํ์ผ ๋๋ ๋ณ๋ CSS ํ์ผ๋ก ๋ถ๋ฆฌ ๊ฐ๋ฅ
## ๐ ๋ผ์ด์ ์ค
MIT License
```
### ์ฃผ์ ๊ฐ์ ์ฌํญ:
1. **์นด๋ ๋ค์ง๊ธฐ ์ ๋๋ฉ์ด์
**: CSS `@keyframes`๋ก 3์ฅ ์นด๋๊ฐ ์์ฐจ์ ์ผ๋ก ๋ค์งํ๋ ํจ๊ณผ
2. **๋ก๋ฉ ์คํผ๋**: ์นด๋ ๋ฝ๊ธฐ ๋ฒํผ ํด๋ฆญ ์ ๋ก๋ฉ ํ์ (์ ํ์ )
3. **ํต๊ณ ์น์
**: ์ด ๋ฆฌ๋ฉ ํ์ ๋ฑ ๊ฐ๋จํ ํต๊ณ ํ์ (๋ณด๋์ค)
4. **README ๋ฌธ์**: ํ๋ก์ ํธ ์ค๋ช
& ์ค์น ๋ฐฉ๋ฒ ์ ๋ฆฌ
ํ์ผ์ ์ ์ฅํ๊ณ ์ ์ฒด ํ๋ก์ฐ๋ฅผ ํ
์คํธํด์ฃผ์ธ์:
1. ๋ฉ์ธ ํ์ด์ง์์ ์นด๋ ๋ฝ๊ธฐ
2. ๊ฒฐ๊ณผ ์ ์ฅ
3. ๋ฆฌ๋ฉ ๊ธฐ๋ก ๋ชฉ๋ก ํ์ธ
4. ์์ธ ๋ณด๊ธฐ ํ์ด์ง ํ์ธ
5. ๋คํฌ๋ชจ๋ ํ ๊ธ ํ
์คํธ
๋ชจ๋ ๊ธฐ๋ฅ์ด ์ ์ ์๋ํ๋ฉด ํ๋ก์ ํธ ์์ฑ์
๋๋ค! ๐
๊ธฐ๋ ๊ฒฐ๊ณผ: ์นด๋ ๋ฝ๊ธฐ ์ 3์ฅ์ ์นด๋๊ฐ ์์ฐจ์ ์ผ๋ก ๋ค์งํ๋ ์ ๋๋ฉ์ด์ ์ด ์ฌ์๋ฉ๋๋ค. ์นด๋ ๋ฝ๊ธฐ ๋ฒํผ ํด๋ฆญ ์ ๋ก๋ฉ ์คํผ๋๊ฐ ํ์๋๊ณ , ๋ฉ์ธ ํ์ด์ง ํ๋จ์ ๊ฐ๋จํ ํต๊ณ ์น์ ์ด ๋ํ๋ฉ๋๋ค. ์ ์ฒด ํ๋ก์ฐ(์นด๋ ๋ฝ๊ธฐ โ ์ ์ฅ โ ๋ชฉ๋ก โ ์์ธ๋ณด๊ธฐ)๊ฐ ๋ถ๋๋ฝ๊ฒ ๋์ํ๋ฉฐ, ๋คํฌ๋ชจ๋ ์ ํ ์ ๋ชจ๋ ์์๊ฐ ์์ฐ์ค๋ฝ๊ฒ ์์์ด ๋ณ๊ฒฝ๋ฉ๋๋ค. ํ๋ก์ ํธ๊ฐ ์์ฑ๋์ด ์ค์ ์ฌ์ฉ ๊ฐ๋ฅํ ์ํ๊ฐ ๋ฉ๋๋ค.
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!