๐ธ ์ธ๋ ๋ฎค์ง์ EP ์จ๋ฒ ํ๋ฆฌ์ค๋ ํ์ด์ง - ํ์ ํ ์๋ฐ ์์ฝ ํ๋งค
indie-album-preorder-page
๋ฌธ์ ์ค๋ช
ํ๋ก์ ํธ ๊ฐ์
์ธ๋ ๋ฎค์ง์ ์ด๋ ์๊ท๋ชจ ๋ ์ด๋ธ์ด ์ ๊ท ์จ๋ฒ์ ์ถ์ํ๊ธฐ ์ , ํฌ๋ค์๊ฒ ์ ์ฃผ๋ฌธ์ ๋ฐ์ ์ ์๋ ํ๋ฆฌ์ค๋ ํ์ด์ง๋ฅผ ๋ง๋ญ๋๋ค. ์ํฐ์คํธ ์๊ฐ, ์จ๋ฒ ํธ๋๋ฆฌ์คํธ, ํ์ ํ ํจํค์ง ์ต์ , ์์ฝ ๊ตฌ๋งค ํผ์ด ํฌํจ๋ ๋งค๋ ฅ์ ์ธ ๋๋ฉํ์ด์ง์ ๋๋ค.
ํต์ฌ ๊ธฐ๋ฅ
ํ์ ๊ธฐ๋ฅ
- ์จ๋ฒ ์๊ฐ ์น์ : ์จ๋ฒ ์ปค๋ฒ ์ด๋ฏธ์ง, ์ํฐ์คํธ ํ๋กํ, ๋ฐ๋งค ์์ ์ผ ํ์
- ํธ๋๋ฆฌ์คํธ ํ ์ด๋ธ: ๊ณก ์ ๋ชฉ, ์ฌ์ ์๊ฐ, ๊ฐ๋จํ ์ค๋ช
- ํจํค์ง ์ต์ ์นด๋: ์ผ๋ฐํ/ํ์ ํ/๋๋ญ์ค ์๋์ ๋ฑ ์ ํ์ง (๊ฐ๊ฒฉ, ๊ตฌ์ฑํ ์ค๋ช )
- ํ๋ฆฌ์ค๋ ํผ: ๊ตฌ๋งค์ ์ด๋ฆ, ์ด๋ฉ์ผ, ์ ํํ ํจํค์ง, ์๋ ์ ๋ ฅ
- ์์ฝ ํํฉ ํ์: ํ์ฌ๊น์ง ์์ฝ๋ ์จ๋ฒ ์๋ ์ค์๊ฐ ์ง๊ณ ("๐ฅ ํ์ฌ 327์ฅ ์์ฝ ์๋ฃ!")
- ๊ด๋ฆฌ์ ๋ทฐ: ์์ฝ ๋ชฉ๋ก ํ์ธ (์ด๋ฆ, ์ด๋ฉ์ผ, ํจํค์ง, ์๋, ์์ฝ์ผ์)
๋ณด๋์ค ๊ธฐ๋ฅ
- ์ผ๋ฆฌ๋ฒ๋ ํ ์ธ ํ์ด๋จธ ("โฐ 24์๊ฐ ๋จ์! 20% ํ ์ธ")
- ์์ ๊ณต์ ๋ฒํผ (Twitter, Instagram)
- ์จ๋ฒ ํฐ์ ์์ ์๋ฒ ๋ (YouTube)
- ํจํค์ง๋ณ ์ฌ๊ณ ์๋ ์ ํ ์ค์
๊ธฐ์ ์คํ
- ๋ฐฑ์๋: Laravel 13.x (API ์๋ฒ, SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค)
- ํ๋ก ํธ์๋: React 19 (Vite)
- ์คํ์ผ๋ง: Tailwind CSS
- ์์ด์ฝ: Heroicons
ํ์ต ๋ชฉํ
- Laravel API ๋ผ์ฐํธ์ ์ปจํธ๋กค๋ฌ ๊ธฐ๋ณธ ๊ตฌ์กฐ ์ดํด
- React์์ fetch/axios๋ก API ํธ์ถํ์ฌ ๋ฐ์ดํฐ ์ฐ๋
- ๊ฐ๋จํ DB ๋ง์ด๊ทธ๋ ์ด์ ๊ณผ ๋ชจ๋ธ ์์ฑ
- ํผ ์ ์ถ๊ณผ ์ ํจ์ฑ ๊ฒ์ฌ ๊ธฐ์ด
- useState๋ฅผ ํ์ฉํ ์ํ ๊ด๋ฆฌ
- Tailwind CSS๋ก ๋ฐ์ํ ๋ ์ด์์ ๊ตฌ์ฑ
์ ์ฝ ์กฐ๊ฑด
- โข Laravel 13.x ์ต์ ๋ฒ์ ์ฌ์ฉ (๊ตฌ๋ฒ์ ๊ธ์ง)
- โข React 19 ์ต์ ๋ฒ์ ์ฌ์ฉ
- โข SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฌ์ฉ (์ค์ ๊ฐ๋จ)
- โข ๋ณ๋์ API ์๋ฒ(Laravel)์ ํ๋ก ํธ์๋(React+Vite) ํ๋ก์ ํธ๋ก ๋ถ๋ฆฌ
- โข CORS ์ค์ ํ์ (Laravel์์ React ๊ฐ๋ฐ ์๋ฒ ์์ฒญ ํ์ฉ)
- โข ๋ณต์กํ ์ํ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ ๊ธ์ง (useState๋ง ์ฌ์ฉ)
- โข ์ค์๊ฐ ์ฑํ ๊ธฐ๋ฅ ๊ธ์ง (HTTP ๊ธฐ๋ฐ ํผ ์ ์ถ๋ง ์ฌ์ฉ)
- โข ๊ด๋ฆฌ์ ์ธ์ฆ์ ๊ฐ๋จํ๊ฒ ์ฒ๋ฆฌ (Laravel Sanctum ๋๋ ๊ธฐ๋ณธ ์ธ์ฆ)
- โข ๋ฐ์ํ ๋์์ธ ํ์ (๋ชจ๋ฐ์ผ/ํ๋ธ๋ฆฟ/๋ฐ์คํฌํฑ ๋์)
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
mkdir indie-album-preorder-page && cd indie-album-preorder-page
์ด ํด๋์์ Laravel 13.x API ๋ฐฑ์๋์ React 19 ํ๋ก ํธ์๋๋ก ๊ตฌ์ฑ๋ ์ธ๋ ์จ๋ฒ ํ๋ฆฌ์ค๋ ํ์ด์ง ํ๋ก์ ํธ๋ฅผ ์์ํฉ๋๋ค.
## 1๋จ๊ณ: Laravel API ๋ฐฑ์๋ ์ค์
### Laravel ํ๋ก์ ํธ ์์ฑ
```bash
composer create-project laravel/laravel:^13.0 backend
cd backend
```
### ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์ (SQLite)
`.env` ํ์ผ ์์ :
```
DB_CONNECTION=sqlite
# DB_HOST, DB_PORT, DB_DATABASE ๋ฑ ์ฃผ์ ์ฒ๋ฆฌ
```
SQLite ํ์ผ ์์ฑ:
```bash
touch database/database.sqlite
```
### ๋ง์ด๊ทธ๋ ์ด์
์์ฑ
`preorders` ํ
์ด๋ธ์ ์ํ ๋ง์ด๊ทธ๋ ์ด์
ํ์ผ ์์ฑ:
```bash
php artisan make:migration create_preorders_table
```
`database/migrations/xxxx_create_preorders_table.php` ๋ด์ฉ:
```php
public function up()
{
Schema::create('preorders', function (Blueprint $table) {
$table->id();
$table->string('customer_name');
$table->string('email');
$table->enum('package_type', ['standard', 'limited', 'deluxe']);
$table->integer('quantity')->default(1);
$table->timestamps();
});
}
```
๋ง์ด๊ทธ๋ ์ด์
์คํ:
```bash
php artisan migrate
```
### ๋ชจ๋ธ ์์ฑ
```bash
php artisan make:model Preorder
```
`app/Models/Preorder.php`:
```php
protected $fillable = ['customer_name', 'email', 'package_type', 'quantity'];
```
### CORS ์ค์
`config/cors.php`์์ `'paths' => ['api/*']` ํ์ธ
`'allowed_origins' => ['http://localhost:5173']` ์ถ๊ฐ (React ๊ฐ๋ฐ ์๋ฒ)
### API ๋ผ์ฐํธ ์์ฑ
`routes/api.php`:
```php
use App\Models\Preorder;
use Illuminate\Http\Request;
Route::get('/preorders/count', function () {
return response()->json(['count' => Preorder::sum('quantity')]);
});
Route::post('/preorders', function (Request $request) {
$validated = $request->validate([
'customer_name' => 'required|string|max:255',
'email' => 'required|email',
'package_type' => 'required|in:standard,limited,deluxe',
'quantity' => 'required|integer|min:1|max:10'
]);
$preorder = Preorder::create($validated);
return response()->json($preorder, 201);
});
Route::get('/preorders', function () {
return response()->json(Preorder::latest()->get());
});
```
### Laravel ์๋ฒ ์คํ
```bash
php artisan serve
```
API๊ฐ `http://localhost:8000`์์ ์คํ๋ฉ๋๋ค.
์ด์ ๋ฐฑ์๋ API ์๋ฒ๊ฐ ์ค๋น๋์์ต๋๋ค. ๋ค์ ๋จ๊ณ์์ React ํ๋ก ํธ์๋๋ฅผ ๊ตฌ์ถํ๊ฒ ์ต๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: Laravel 13.x ๋ฐฑ์๋ API ์๋ฒ๊ฐ http://localhost:8000์์ ์คํ๋๋ฉฐ, SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค์ preorders ํ ์ด๋ธ์ด ์์ฑ๋๊ณ , API ์๋ํฌ์ธํธ(/api/preorders/count, /api/preorders POST, /api/preorders GET)๊ฐ ์ ์ ์๋ํฉ๋๋ค. Postman์ด๋ curl๋ก API ํ ์คํธ ๊ฐ๋ฅ.
## 2๋จ๊ณ: React 19 ํ๋ก ํธ์๋ ํ๋ก์ ํธ ์์ฑ
### ํ๋ก์ ํธ ๋ฃจํธ๋ก ์ด๋
```bash
cd ..
```
### React + Vite ํ๋ก์ ํธ ์์ฑ
```bash
npm create vite@latest frontend -- --template react
cd frontend
npm install
```
### Tailwind CSS ์ค์น
```bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```
`tailwind.config.js` ์์ :
```js
export default {
content: [
"./index.html",
"./src/**/*.{js,jsx}",
],
theme: {
extend: {},
},
plugins: [],
}
```
`src/index.css`:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
### ๊ธฐ๋ณธ App.jsx ๊ตฌ์กฐ
`src/App.jsx`:
```jsx
import { useState, useEffect } from 'react';
function App() {
const [preorderCount, setPreorderCount] = useState(0);
useEffect(() => {
fetch('http://localhost:8000/api/preorders/count')
.then(res => res.json())
.then(data => setPreorderCount(data.count))
.catch(err => console.error(err));
}, []);
return (
<div className="min-h-screen bg-gradient-to-br from-purple-900 via-indigo-800 to-blue-900">
<div className="container mx-auto px-4 py-12">
<h1 className="text-5xl font-bold text-white text-center mb-4">
๐ธ Midnight Echo - New EP "Neon Dreams"
</h1>
<p className="text-xl text-purple-200 text-center mb-8">
Pre-order Now | Release Date: 2025.03.15
</p>
<div className="text-center mb-12">
<span className="inline-block bg-yellow-400 text-purple-900 font-bold px-6 py-2 rounded-full text-lg">
๐ฅ {preorderCount} albums pre-ordered!
</span>
</div>
</div>
</div>
);
}
export default App;
```
### ๊ฐ๋ฐ ์๋ฒ ์คํ
```bash
npm run dev
```
React ์ฑ์ด `http://localhost:5173`์์ ์คํ๋ฉ๋๋ค.
๋ธ๋ผ์ฐ์ ์์ ํ์ด์ง๋ฅผ ์ด๋ฉด ์จ๋ฒ ์ ๋ชฉ๊ณผ ํ์ฌ ์์ฝ ์๋์ด ํ์๋ฉ๋๋ค. Laravel API์์ ๋ฐ์ดํฐ๋ฅผ ์ฑ๊ณต์ ์ผ๋ก ๊ฐ์ ธ์ค๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: React 19 + Vite ํ๋ก ํธ์๋๊ฐ http://localhost:5173์์ ์คํ๋๋ฉฐ, Tailwind CSS๊ฐ ์ ์ฉ๋ ๊ทธ๋ผ๋ฐ์ด์ ๋ฐฐ๊ฒฝ์ ์จ๋ฒ ์๊ฐ ํ์ด์ง๊ฐ ํ์๋ฉ๋๋ค. Laravel API์์ ์์ฝ ์๋์ fetchํ์ฌ ํ๋ฉด์ ํ์๋ฉ๋๋ค.
## 3๋จ๊ณ: ์จ๋ฒ ์๊ฐ & ํธ๋๋ฆฌ์คํธ ์น์
์ถ๊ฐ
`src/App.jsx`๋ฅผ ํ์ฅํ์ฌ ์จ๋ฒ ์ปค๋ฒ ์ด๋ฏธ์ง, ํธ๋๋ฆฌ์คํธ, ํจํค์ง ์ต์
์ ์ถ๊ฐํฉ๋๋ค.
```jsx
import { useState, useEffect } from 'react';
function App() {
const [preorderCount, setPreorderCount] = useState(0);
useEffect(() => {
fetch('http://localhost:8000/api/preorders/count')
.then(res => res.json())
.then(data => setPreorderCount(data.count))
.catch(err => console.error(err));
}, []);
const tracks = [
{ no: 1, title: 'Neon Lights', duration: '3:45' },
{ no: 2, title: 'Midnight Drive', duration: '4:12' },
{ no: 3, title: 'Echo in the Dark', duration: '3:58' },
{ no: 4, title: 'Digital Dreams', duration: '5:20' },
{ no: 5, title: 'Last Call', duration: '4:05' },
];
const packages = [
{
type: 'standard',
name: 'Standard Edition',
price: 15000,
items: ['CD', 'Digital Download Code', 'Photocard (Random 1 of 5)']
},
{
type: 'limited',
name: 'Limited Edition',
price: 25000,
items: ['CD', 'Digital Download Code', 'Photocard Set (5 pcs)', 'Poster', 'Sticker Pack'],
badge: 'POPULAR'
},
{
type: 'deluxe',
name: 'Deluxe Box Set',
price: 45000,
items: ['CD', 'Vinyl LP', 'Digital Download Code', 'Photocard Set', 'Poster', 'Sticker Pack', 'Art Book (40 pages)', 'Signed Postcard']
}
];
return (
<div className="min-h-screen bg-gradient-to-br from-purple-900 via-indigo-800 to-blue-900">
<div className="container mx-auto px-4 py-12">
{/* Header */}
<h1 className="text-5xl font-bold text-white text-center mb-4">
๐ธ Midnight Echo - New EP "Neon Dreams"
</h1>
<p className="text-xl text-purple-200 text-center mb-8">
Pre-order Now | Release Date: 2025.03.15
</p>
<div className="text-center mb-12">
<span className="inline-block bg-yellow-400 text-purple-900 font-bold px-6 py-2 rounded-full text-lg">
๐ฅ {preorderCount} albums pre-ordered!
</span>
</div>
{/* Album Cover & Artist Info */}
<div className="max-w-4xl mx-auto mb-16">
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-8 shadow-2xl">
<div className="grid md:grid-cols-2 gap-8 items-center">
<div>
<div className="aspect-square bg-gradient-to-br from-pink-500 to-purple-600 rounded-xl shadow-lg flex items-center justify-center">
<span className="text-white text-6xl font-bold">NE</span>
</div>
</div>
<div>
<h2 className="text-3xl font-bold text-white mb-4">About the Album</h2>
<p className="text-purple-100 mb-4">
"Neon Dreams" is Midnight Echo's sophomore EP, featuring 5 tracks of synth-driven indie rock. Recorded in Seoul, this album captures the energy of late-night city drives and neon-lit streets.
</p>
<p className="text-purple-200 text-sm">
Producer: Kim Jae-hyun | Label: Echo Records | Genre: Indie Rock, Synth Pop
</p>
</div>
</div>
</div>
</div>
{/* Tracklist */}
<div className="max-w-4xl mx-auto mb-16">
<h2 className="text-3xl font-bold text-white mb-6 text-center">๐ Tracklist</h2>
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-6 shadow-2xl">
<table className="w-full text-white">
<thead>
<tr className="border-b border-purple-400/30">
<th className="text-left py-3 px-4">No.</th>
<th className="text-left py-3 px-4">Title</th>
<th className="text-right py-3 px-4">Duration</th>
</tr>
</thead>
<tbody>
{tracks.map(track => (
<tr key={track.no} className="border-b border-purple-400/10 hover:bg-white/5">
<td className="py-3 px-4">{track.no}</td>
<td className="py-3 px-4 font-medium">{track.title}</td>
<td className="py-3 px-4 text-right text-purple-300">{track.duration}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Package Options */}
<div className="max-w-6xl mx-auto mb-16">
<h2 className="text-3xl font-bold text-white mb-6 text-center">๐ Package Options</h2>
<div className="grid md:grid-cols-3 gap-6">
{packages.map(pkg => (
<div key={pkg.type} className="bg-white/10 backdrop-blur-md rounded-2xl p-6 shadow-2xl hover:scale-105 transition relative">
{pkg.badge && (
<span className="absolute top-4 right-4 bg-yellow-400 text-purple-900 text-xs font-bold px-3 py-1 rounded-full">
{pkg.badge}
</span>
)}
<h3 className="text-2xl font-bold text-white mb-2">{pkg.name}</h3>
<p className="text-3xl font-bold text-yellow-400 mb-4">โฉ{pkg.price.toLocaleString()}</p>
<ul className="text-purple-100 space-y-2">
{pkg.items.map((item, i) => (
<li key={i} className="flex items-start">
<span className="mr-2">โ</span>
<span>{item}</span>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</div>
</div>
);
}
export default App;
```
๋ธ๋ผ์ฐ์ ๋ฅผ ์๋ก๊ณ ์นจํ๋ฉด ์จ๋ฒ ์ปค๋ฒ(์์ ๊ทธ๋ผ๋ฐ์ด์
), ํธ๋๋ฆฌ์คํธ ํ
์ด๋ธ, 3๊ฐ์ง ํจํค์ง ์ต์
์นด๋๊ฐ ํ์๋ฉ๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: ์จ๋ฒ ์๊ฐ ์น์ (์ปค๋ฒ ์ด๋ฏธ์ง, ์ํฐ์คํธ ์ค๋ช ), ํธ๋๋ฆฌ์คํธ ํ ์ด๋ธ(5๊ณก), 3๊ฐ์ง ํจํค์ง ์ต์ ์นด๋(Standard/Limited/Deluxe)๊ฐ Tailwind CSS๋ก ์คํ์ผ๋ง๋์ด ํ์๋ฉ๋๋ค. ํธ๋ฒ ํจ๊ณผ์ ๋ฑ์ง(POPULAR)๊ฐ ์๋ํฉ๋๋ค.
## 4๋จ๊ณ: ํ๋ฆฌ์ค๋ ํผ ์ถ๊ฐ ๋ฐ API ์ฐ๋
์ฌ์ฉ์๊ฐ ํจํค์ง๋ฅผ ์ ํํ๊ณ ์ ๋ณด๋ฅผ ์
๋ ฅํ ํ ํ๋ฆฌ์ค๋๋ฅผ ์ ์ถํ ์ ์๋ ํผ์ ์ถ๊ฐํฉ๋๋ค.
`src/App.jsx`์ ํผ ์ํ์ ์ ์ถ ๋ก์ง์ ์ถ๊ฐ:
```jsx
import { useState, useEffect } from 'react';
function App() {
const [preorderCount, setPreorderCount] = useState(0);
const [formData, setFormData] = useState({
customer_name: '',
email: '',
package_type: 'limited',
quantity: 1
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitMessage, setSubmitMessage] = useState('');
useEffect(() => {
fetchPreorderCount();
}, []);
const fetchPreorderCount = () => {
fetch('http://localhost:8000/api/preorders/count')
.then(res => res.json())
.then(data => setPreorderCount(data.count))
.catch(err => console.error(err));
};
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setSubmitMessage('');
try {
const response = await fetch('http://localhost:8000/api/preorders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
if (response.ok) {
setSubmitMessage('โ
Pre-order confirmed! Check your email for details.');
setFormData({
customer_name: '',
email: '',
package_type: 'limited',
quantity: 1
});
fetchPreorderCount(); // ์์ฝ ์๋ ์
๋ฐ์ดํธ
} else {
const error = await response.json();
setSubmitMessage('โ Error: ' + (error.message || 'Please try again.'));
}
} catch (err) {
setSubmitMessage('โ Network error. Please try again.');
} finally {
setIsSubmitting(false);
}
};
const tracks = [
{ no: 1, title: 'Neon Lights', duration: '3:45' },
{ no: 2, title: 'Midnight Drive', duration: '4:12' },
{ no: 3, title: 'Echo in the Dark', duration: '3:58' },
{ no: 4, title: 'Digital Dreams', duration: '5:20' },
{ no: 5, title: 'Last Call', duration: '4:05' },
];
const packages = [
{
type: 'standard',
name: 'Standard Edition',
price: 15000,
items: ['CD', 'Digital Download Code', 'Photocard (Random 1 of 5)']
},
{
type: 'limited',
name: 'Limited Edition',
price: 25000,
items: ['CD', 'Digital Download Code', 'Photocard Set (5 pcs)', 'Poster', 'Sticker Pack'],
badge: 'POPULAR'
},
{
type: 'deluxe',
name: 'Deluxe Box Set',
price: 45000,
items: ['CD', 'Vinyl LP', 'Digital Download Code', 'Photocard Set', 'Poster', 'Sticker Pack', 'Art Book (40 pages)', 'Signed Postcard']
}
];
return (
<div className="min-h-screen bg-gradient-to-br from-purple-900 via-indigo-800 to-blue-900">
<div className="container mx-auto px-4 py-12">
{/* Header */}
<h1 className="text-5xl font-bold text-white text-center mb-4">
๐ธ Midnight Echo - New EP "Neon Dreams"
</h1>
<p className="text-xl text-purple-200 text-center mb-8">
Pre-order Now | Release Date: 2025.03.15
</p>
<div className="text-center mb-12">
<span className="inline-block bg-yellow-400 text-purple-900 font-bold px-6 py-2 rounded-full text-lg">
๐ฅ {preorderCount} albums pre-ordered!
</span>
</div>
{/* Album Cover & Artist Info */}
<div className="max-w-4xl mx-auto mb-16">
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-8 shadow-2xl">
<div className="grid md:grid-cols-2 gap-8 items-center">
<div>
<div className="aspect-square bg-gradient-to-br from-pink-500 to-purple-600 rounded-xl shadow-lg flex items-center justify-center">
<span className="text-white text-6xl font-bold">NE</span>
</div>
</div>
<div>
<h2 className="text-3xl font-bold text-white mb-4">About the Album</h2>
<p className="text-purple-100 mb-4">
"Neon Dreams" is Midnight Echo's sophomore EP, featuring 5 tracks of synth-driven indie rock. Recorded in Seoul, this album captures the energy of late-night city drives and neon-lit streets.
</p>
<p className="text-purple-200 text-sm">
Producer: Kim Jae-hyun | Label: Echo Records | Genre: Indie Rock, Synth Pop
</p>
</div>
</div>
</div>
</div>
{/* Tracklist */}
<div className="max-w-4xl mx-auto mb-16">
<h2 className="text-3xl font-bold text-white mb-6 text-center">๐ Tracklist</h2>
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-6 shadow-2xl">
<table className="w-full text-white">
<thead>
<tr className="border-b border-purple-400/30">
<th className="text-left py-3 px-4">No.</th>
<th className="text-left py-3 px-4">Title</th>
<th className="text-right py-3 px-4">Duration</th>
</tr>
</thead>
<tbody>
{tracks.map(track => (
<tr key={track.no} className="border-b border-purple-400/10 hover:bg-white/5">
<td className="py-3 px-4">{track.no}</td>
<td className="py-3 px-4 font-medium">{track.title}</td>
<td className="py-3 px-4 text-right text-purple-300">{track.duration}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Package Options */}
<div className="max-w-6xl mx-auto mb-16">
<h2 className="text-3xl font-bold text-white mb-6 text-center">๐ Package Options</h2>
<div className="grid md:grid-cols-3 gap-6">
{packages.map(pkg => (
<div key={pkg.type} className="bg-white/10 backdrop-blur-md rounded-2xl p-6 shadow-2xl hover:scale-105 transition relative">
{pkg.badge && (
<span className="absolute top-4 right-4 bg-yellow-400 text-purple-900 text-xs font-bold px-3 py-1 rounded-full">
{pkg.badge}
</span>
)}
<h3 className="text-2xl font-bold text-white mb-2">{pkg.name}</h3>
<p className="text-3xl font-bold text-yellow-400 mb-4">โฉ{pkg.price.toLocaleString()}</p>
<ul className="text-purple-100 space-y-2">
{pkg.items.map((item, i) => (
<li key={i} className="flex items-start">
<span className="mr-2">โ</span>
<span>{item}</span>
</li>
))}
</ul>
</div>
))}
</div>
</div>
{/* Pre-order Form */}
<div className="max-w-2xl mx-auto mb-16">
<h2 className="text-3xl font-bold text-white mb-6 text-center">๐ Pre-order Form</h2>
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-8 shadow-2xl">
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-white font-medium mb-2">Your Name</label>
<input
type="text"
required
value={formData.customer_name}
onChange={(e) => setFormData({...formData, customer_name: e.target.value})}
className="w-full px-4 py-3 rounded-lg bg-white/20 text-white placeholder-purple-300 focus:outline-none focus:ring-2 focus:ring-yellow-400"
placeholder="Kim Min-soo"
/>
</div>
<div>
<label className="block text-white font-medium mb-2">Email</label>
<input
type="email"
required
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
className="w-full px-4 py-3 rounded-lg bg-white/20 text-white placeholder-purple-300 focus:outline-none focus:ring-2 focus:ring-yellow-400"
placeholder="your@email.com"
/>
</div>
<div>
<label className="block text-white font-medium mb-2">Package</label>
<select
value={formData.package_type}
onChange={(e) => setFormData({...formData, package_type: e.target.value})}
className="w-full px-4 py-3 rounded-lg bg-white/20 text-white focus:outline-none focus:ring-2 focus:ring-yellow-400"
>
<option value="standard" className="bg-purple-900">Standard Edition - โฉ15,000</option>
<option value="limited" className="bg-purple-900">Limited Edition - โฉ25,000</option>
<option value="deluxe" className="bg-purple-900">Deluxe Box Set - โฉ45,000</option>
</select>
</div>
<div>
<label className="block text-white font-medium mb-2">Quantity</label>
<input
type="number"
min="1"
max="10"
required
value={formData.quantity}
onChange={(e) => setFormData({...formData, quantity: parseInt(e.target.value)})}
className="w-full px-4 py-3 rounded-lg bg-white/20 text-white focus:outline-none focus:ring-2 focus:ring-yellow-400"
/>
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-yellow-400 text-purple-900 font-bold py-4 rounded-lg hover:bg-yellow-300 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? 'Processing...' : '๐ Confirm Pre-order'}
</button>
{submitMessage && (
<p className={`text-center font-medium ${submitMessage.startsWith('โ
') ? 'text-green-300' : 'text-red-300'}`}>
{submitMessage}
</p>
)}
</form>
</div>
</div>
</div>
</div>
);
}
export default App;
```
ํผ์ ์ ์ถํ๋ฉด Laravel API๋ก POST ์์ฒญ์ด ์ ์ก๋๊ณ , ์ฑ๊ณต ์ ์์ฝ ์๋์ด ์๋์ผ๋ก ์
๋ฐ์ดํธ๋ฉ๋๋ค. ๋ธ๋ผ์ฐ์ ์์ ํ
์คํธํด๋ณด์ธ์!
๊ธฐ๋ ๊ฒฐ๊ณผ: ํ๋ฆฌ์ค๋ ํผ์ด ์ถ๊ฐ๋์ด ์ฌ์ฉ์๊ฐ ์ด๋ฆ, ์ด๋ฉ์ผ, ํจํค์ง ํ์ , ์๋์ ์ ๋ ฅํ๊ณ ์ ์ถํ ์ ์์ต๋๋ค. ์ ์ถ ์ Laravel API๋ก POST ์์ฒญ์ด ์ ์ก๋๊ณ , ์ฑ๊ณต ์ 'โ Pre-order confirmed!' ๋ฉ์์ง๊ฐ ํ์๋๋ฉฐ ํ์ด์ง ์๋จ์ ์์ฝ ์๋์ด ์ค์๊ฐ์ผ๋ก ์ ๋ฐ์ดํธ๋ฉ๋๋ค. ์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ ์ฒ๋ฆฌ๋ ์๋ํฉ๋๋ค.
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!