๐ฎ ๊ฒ์ ์บ๋ฆญํฐ ์ปค์คํฐ๋ง์ด์ง ํ๋ฆฌ๋ทฐ - ์ค์๊ฐ ์๋ฐํ ๋น๋
game-avatar-customizer
๋ฌธ์ ์ค๋ช
๐ฏ ํ๋ก์ ํธ ๊ฐ์
๊ฒ์ ์บ๋ฆญํฐ๋ฅผ ์ปค์คํฐ๋ง์ด์งํ๊ณ ์ค์๊ฐ์ผ๋ก ๋ฏธ๋ฆฌ๋ณด๊ธฐํ ์ ์๋ ๊ฐ๋จํ ํ์คํ ์๋ฐํ ๋น๋๋ฅผ ๋ง๋ญ๋๋ค. ์ฌ์ฉ์๋ ํค์ด์คํ์ผ, ํผ๋ถ์, ์์, ์ก์ธ์๋ฆฌ ๋ฑ์ ์ ํํ์ฌ ์์ ๋ง์ ์บ๋ฆญํฐ๋ฅผ ๋ง๋ค๊ณ ์ ์ฅํ ์ ์์ต๋๋ค.
๐ ํ์ ๊ธฐ๋ฅ
- Laravel 13.x REST API ๋ฐฑ์๋ (SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค)
- React 19 ํ๋ก ํธ์๋ (Vite ๊ธฐ๋ฐ)
- ์บ๋ฆญํฐ ์ปค์คํฐ๋ง์ด์ง ์ต์
์ ํ UI
- ํค์ด์คํ์ผ (5๊ฐ์ง ์ด์)
- ํผ๋ถ์ (5๊ฐ์ง ์ด์)
- ์์/ํ์ (๊ฐ 3๊ฐ์ง ์ด์)
- ์ก์ธ์๋ฆฌ (๋ชจ์, ์๊ฒฝ ๋ฑ 3๊ฐ์ง ์ด์)
- ์ค์๊ฐ ์บ๋ฆญํฐ ํ๋ฆฌ๋ทฐ (์ ํํ ์ต์ ์ด ์ฆ์ ๋ฐ์)
- ๋ด ์บ๋ฆญํฐ ์ ์ฅ ๊ธฐ๋ฅ (๋๋ค์๊ณผ ํจ๊ป)
- ์ ์ฅ๋ ์บ๋ฆญํฐ ๊ฐค๋ฌ๋ฆฌ (๋ค๋ฅธ ์ฌ์ฉ์๋ค์ ์บ๋ฆญํฐ ๊ตฌ๊ฒฝํ๊ธฐ)
- ์บ๋ฆญํฐ๋ณ ์ข์์ ๊ธฐ๋ฅ
๐ ์ ํ ์ถ๊ฐ ๊ธฐ๋ฅ (๋ณด๋์ค)
- ๋๋ค ์บ๋ฆญํฐ ์์ฑ ๋ฒํผ (๋ชจ๋ ์ต์ ๋๋ค ์ ํ)
- ์บ๋ฆญํฐ ๊ณต์ URL ์์ฑ (ํน์ ์บ๋ฆญํฐ ๋งํฌ ๋ณต์ฌ)
- ์ธ๊ธฐ ์บ๋ฆญํฐ TOP 10 (์ข์์ ์)
- ์์ ํ๋ ํธ ์ปค์คํฐ๋ง์ด์ง (์์ ์์ ์ง์ ์ ํ)
- ์ ๋๋ฉ์ด์ ํจ๊ณผ (์บ๋ฆญํฐ๊ฐ ์ ํ๋ค๊ธฐ, ์ ํํ๊ธฐ ๋ฑ)
๐ ๊ธฐ์ ์คํ
- Backend: Laravel 13.x (API ๋ชจ๋)
- Frontend: React 19 + Vite
- Database: SQLite
- Styling: Tailwind CSS
- HTTP Client: Axios
๐ก ํ์ต ํฌ์ธํธ
- Laravel API ์๋ํฌ์ธํธ ์ค๊ณ ๋ฐ CORS ์ค์
- React ์ํ ๊ด๋ฆฌ (useState)๋ก ์ค์๊ฐ UI ์ ๋ฐ์ดํธ
- RESTful API์ React ํ๋ก ํธ์๋ ํต์
- ์ด๋ฏธ์ง/์์ด์ฝ ๊ธฐ๋ฐ UI ์ปดํฌ๋ํธ ๊ตฌ์ฑ
- ๊ฐ๋จํ ์ข์์ ๊ธฐ๋ฅ ๊ตฌํ
์ ์ฝ ์กฐ๊ฑด
- โข Laravel 13.x์ React 19 ์ต์ ๋ฒ์ ํ์ ์ฌ์ฉ
- โข SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฌ์ฉ (๋ณ๋ DB ์๋ฒ ๋ถํ์)
- โข Laravel์ ์์ API ์๋ฒ๋ก๋ง ๋์ (Blade ํ ํ๋ฆฟ ์ฌ์ฉ ๊ธ์ง)
- โข React๋ ๋ณ๋ Vite ํ๋ก์ ํธ๋ก ๊ตฌ์ฑ (ํฌํธ 5173)
- โข Laravel API๋ ํฌํธ 8000์์ ์คํ
- โข ์ค์๊ฐ ์ฑํ /WebSocket ๊ธฐ๋ฅ ์ฌ์ฉ ๊ธ์ง
- โข ๋ณต์กํ ์ํ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ(Redux, Zustand ๋ฑ) ์ฌ์ฉ ๊ธ์ง, useState๋ง ์ฌ์ฉ
- โข ์บ๋ฆญํฐ ์ด๋ฏธ์ง๋ ์ด๋ชจ์ง ๋๋ SVG ์์ด์ฝ์ผ๋ก ๊ฐ๋จํ๊ฒ ํํ
- โข ํ๋ก ํธ์๋๋ ๋ฐ์ํ ๋์์ธ (๋ชจ๋ฐ์ผ/๋ฐ์คํฌํฑ ๋์)
- โข API ์๋ต์ JSON ํ์
- โข CORS ์ค์ ํ์ (Laravel์์ React ๋๋ฉ์ธ ํ์ฉ)
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
mkdir game-avatar-customizer && cd game-avatar-customizer
์ด ํด๋์์ ํ๋ก์ ํธ๋ฅผ ์์ํฉ๋๋ค.
# Laravel 13.x API ๋ฐฑ์๋ ์ด๊ธฐ ์ค์
1. Laravel 13.x ํ๋ก์ ํธ๋ฅผ `backend` ํด๋์ ์์ฑํ์ธ์:
```bash
composer create-project laravel/laravel:^13.0 backend
cd backend
```
2. SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์ :
- `.env` ํ์ผ์์ DB ์ค์ ๋ณ๊ฒฝ:
```
DB_CONNECTION=sqlite
# DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD ์ฃผ์ ์ฒ๋ฆฌ
```
- `database/database.sqlite` ํ์ผ ์์ฑ:
```bash
touch database/database.sqlite
```
3. CORS ์ค์ :
- `config/cors.php`์์ `'allowed_origins'`๋ฅผ `['http://localhost:5173']`๋ก ์ค์
- `bootstrap/app.php`์์ CORS ๋ฏธ๋ค์จ์ด ํ์ฑํ ํ์ธ
4. Characters ํ
์ด๋ธ ๋ง์ด๊ทธ๋ ์ด์
์์ฑ:
```bash
php artisan make:migration create_characters_table
```
- ๋ง์ด๊ทธ๋ ์ด์
ํ์ผ ๋ด์ฉ:
```php
Schema::create('characters', function (Blueprint $table) {
$table->id();
$table->string('nickname');
$table->string('hairstyle');
$table->string('skin_color');
$table->string('top');
$table->string('bottom');
$table->string('accessory')->nullable();
$table->integer('likes')->default(0);
$table->timestamps();
});
```
5. ๋ง์ด๊ทธ๋ ์ด์
์คํ:
```bash
php artisan migrate
```
6. Character ๋ชจ๋ธ ์์ฑ:
```bash
php artisan make:model Character
```
- `app/Models/Character.php`์์ fillable ์์ฑ ์ถ๊ฐ:
```php
protected $fillable = ['nickname', 'hairstyle', 'skin_color', 'top', 'bottom', 'accessory', 'likes'];
```
7. API ๋ผ์ฐํธ ์ค์ (`routes/api.php`):
```php
use App\Http\Controllers\CharacterController;
Route::get('/characters', [CharacterController::class, 'index']);
Route::post('/characters', [CharacterController::class, 'store']);
Route::post('/characters/{id}/like', [CharacterController::class, 'like']);
```
8. CharacterController ์์ฑ:
```bash
php artisan make:controller CharacterController
```
- ๊ธฐ๋ณธ CRUD ๋ฉ์๋ ๊ตฌํ (index, store, like)
9. ์๋ฒ ์คํ ํ
์คํธ:
```bash
php artisan serve
```
- `http://localhost:8000/api/characters` ์ ์ํ์ฌ ๋น ๋ฐฐ์ด `[]` ๋ฐํ ํ์ธ
์๋ฃ๋๋ฉด Laravel API ์๋ฒ๊ฐ ํฌํธ 8000์์ ์คํ๋๊ณ , SQLite DB์ Character ๋ชจ๋ธ์ด ์ค๋น๋ ์ํ์
๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: Laravel 13.x ๋ฐฑ์๋ ํ๋ก์ ํธ๊ฐ `backend` ํด๋์ ์์ฑ๋๊ณ , SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ, Characters ํ ์ด๋ธ ๋ง์ด๊ทธ๋ ์ด์ , Character ๋ชจ๋ธ, API ๋ผ์ฐํธ, CharacterController๊ฐ ์ค๋น๋ฉ๋๋ค. `php artisan serve` ์คํ ์ ํฌํธ 8000์์ ์๋ฒ๊ฐ ๋์ํ๊ณ , `/api/characters` ์๋ํฌ์ธํธ์ ์ ์ํ๋ฉด ๋น ๋ฐฐ์ด์ด ๋ฐํ๋ฉ๋๋ค.
# Laravel API ์๋ํฌ์ธํธ ๊ตฌํ
CharacterController์ ์บ๋ฆญํฐ CRUD ๋ก์ง์ ๊ตฌํํ์ธ์.
`app/Http/Controllers/CharacterController.php` ํ์ผ ๋ด์ฉ:
```php
namespace App\Http\Controllers;
use App\Models\Character;
use Illuminate\Http\Request;
class CharacterController extends Controller
{
// ๋ชจ๋ ์บ๋ฆญํฐ ์กฐํ (์ข์์ ๋ง์ ์)
public function index()
{
$characters = Character::orderBy('likes', 'desc')
->orderBy('created_at', 'desc')
->get();
return response()->json($characters);
}
// ์ ์บ๋ฆญํฐ ์ ์ฅ
public function store(Request $request)
{
$validated = $request->validate([
'nickname' => 'required|string|max:50',
'hairstyle' => 'required|string',
'skin_color' => 'required|string',
'top' => 'required|string',
'bottom' => 'required|string',
'accessory' => 'nullable|string',
]);
$character = Character::create($validated);
return response()->json($character, 201);
}
// ์บ๋ฆญํฐ ์ข์์
public function like($id)
{
$character = Character::findOrFail($id);
$character->increment('likes');
return response()->json($character);
}
}
```
**ํ
์คํธ ๋ฐฉ๋ฒ:**
1. Postman ๋๋ curl๋ก ์บ๋ฆญํฐ ์์ฑ ํ
์คํธ:
```bash
curl -X POST http://localhost:8000/api/characters \
-H "Content-Type: application/json" \
-d '{
"nickname": "TestHero",
"hairstyle": "short",
"skin_color": "light",
"top": "tshirt",
"bottom": "jeans",
"accessory": "cap"
}'
```
2. ์บ๋ฆญํฐ ๋ชฉ๋ก ์กฐํ:
```bash
curl http://localhost:8000/api/characters
```
3. ์ข์์ ์ถ๊ฐ:
```bash
curl -X POST http://localhost:8000/api/characters/1/like
```
๋ชจ๋ ์๋ํฌ์ธํธ๊ฐ ์ ์ ์๋ํ๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: CharacterController์ index, store, like ๋ฉ์๋๊ฐ ๊ตฌํ๋์ด ์บ๋ฆญํฐ ์์ฑ, ์กฐํ, ์ข์์ ๊ธฐ๋ฅ์ด ๋์ํฉ๋๋ค. API ํ ์คํธ ์ JSON ์๋ต์ด ์ ์์ ์ผ๋ก ๋ฐํ๋๊ณ , SQLite DB์ ๋ฐ์ดํฐ๊ฐ ์ ์ฅ๋ฉ๋๋ค.
# React 19 ํ๋ก ํธ์๋ ์ด๊ธฐ ์ค์
ํ๋ก์ ํธ ๋ฃจํธ(`game-avatar-customizer`)๋ก ๋์์์ React ํ๋ก ํธ์๋๋ฅผ ์์ฑํ์ธ์.
1. React + Vite ํ๋ก์ ํธ ์์ฑ:
```bash
cd ..
npm create vite@latest frontend -- --template react
cd frontend
npm install
```
2. ํ์ํ ํจํค์ง ์ค์น:
```bash
npm install axios
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```
3. Tailwind CSS ์ค์ :
- `tailwind.config.js` ํ์ผ:
```js
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
```
- `src/index.css` ํ์ผ ๋งจ ์์ ์ถ๊ฐ:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
4. Axios ๊ธฐ๋ณธ ์ค์ ํ์ผ ์์ฑ (`src/api/axios.js`):
```js
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:8000/api',
headers: {
'Content-Type': 'application/json',
},
});
export default api;
```
5. ๊ฐ๋ฐ ์๋ฒ ์คํ ํ
์คํธ:
```bash
npm run dev
```
- ๋ธ๋ผ์ฐ์ ์์ `http://localhost:5173` ์ ์ํ์ฌ Vite ๊ธฐ๋ณธ ํ๋ฉด ํ์ธ
6. `src/App.jsx`๋ฅผ ๊ฐ๋จํ ํฌ๋ก์๋๋ก ์์ ํ์ฌ ํ
์คํธ:
```jsx
function App() {
return (
<div className="min-h-screen bg-gray-100 p-8">
<h1 className="text-3xl font-bold text-center">๊ฒ์ ์บ๋ฆญํฐ ์ปค์คํฐ๋ง์ด์ง</h1>
</div>
)
}
export default App
```
๋ธ๋ผ์ฐ์ ์ "๊ฒ์ ์บ๋ฆญํฐ ์ปค์คํฐ๋ง์ด์ง" ์ ๋ชฉ์ด ํ์๋๋ฉด ์ฑ๊ณต์
๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: React 19 + Vite ํ๋ก์ ํธ๊ฐ `frontend` ํด๋์ ์์ฑ๋๊ณ , Tailwind CSS์ Axios๊ฐ ์ค์น๋ฉ๋๋ค. `npm run dev` ์คํ ์ ํฌํธ 5173์์ ๊ฐ๋ฐ ์๋ฒ๊ฐ ๋์ํ๊ณ , ๋ธ๋ผ์ฐ์ ์ ๊ฐ๋จํ ์ ๋ชฉ์ด ํ์๋ฉ๋๋ค.
# React ์บ๋ฆญํฐ ์ปค์คํฐ๋ง์ด์ง UI ๊ตฌํ
์บ๋ฆญํฐ ์ปค์คํฐ๋ง์ด์ง ์ต์
์ ์ ํํ๊ณ ์ค์๊ฐ์ผ๋ก ํ๋ฆฌ๋ทฐ๋ฅผ ๋ณผ ์ ์๋ UI๋ฅผ ๋ง๋์ธ์.
`src/App.jsx` ํ์ผ์ ๋ค์๊ณผ ๊ฐ์ด ์์ฑ:
```jsx
import { useState } from 'react'
import api from './api/axios'
function App() {
// ์บ๋ฆญํฐ ์ปค์คํฐ๋ง์ด์ง ์ํ
const [character, setCharacter] = useState({
nickname: '',
hairstyle: 'short',
skin_color: 'light',
top: 'tshirt',
bottom: 'jeans',
accessory: 'none'
})
const [savedCharacters, setSavedCharacters] = useState([])
const [loading, setLoading] = useState(false)
// ์ต์
๋ฐ์ดํฐ
const options = {
hairstyle: ['short', 'long', 'curly', 'bald', 'ponytail'],
skin_color: ['light', 'medium', 'tan', 'dark', 'pale'],
top: ['tshirt', 'hoodie', 'jacket', 'tank'],
bottom: ['jeans', 'shorts', 'skirt', 'pants'],
accessory: ['none', 'cap', 'glasses', 'headband', 'earrings']
}
// ์ต์
๋ณ๊ฒฝ ํธ๋ค๋ฌ
const handleChange = (category, value) => {
setCharacter(prev => ({ ...prev, [category]: value }))
}
// ์บ๋ฆญํฐ ์ ์ฅ
const handleSave = async () => {
if (!character.nickname.trim()) {
alert('๋๋ค์์ ์
๋ ฅํ์ธ์!')
return
}
setLoading(true)
try {
const response = await api.post('/characters', character)
alert('์บ๋ฆญํฐ๊ฐ ์ ์ฅ๋์์ต๋๋ค!')
setCharacter({ ...character, nickname: '' })
fetchCharacters()
} catch (error) {
alert('์ ์ฅ ์คํจ: ' + error.message)
} finally {
setLoading(false)
}
}
// ์ ์ฅ๋ ์บ๋ฆญํฐ ๋ถ๋ฌ์ค๊ธฐ
const fetchCharacters = async () => {
try {
const response = await api.get('/characters')
setSavedCharacters(response.data)
} catch (error) {
console.error('์บ๋ฆญํฐ ๋ก๋ฉ ์คํจ:', error)
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-purple-400 to-pink-300 p-8">
<div className="max-w-6xl mx-auto">
<h1 className="text-4xl font-bold text-white text-center mb-8">๐ฎ ๊ฒ์ ์บ๋ฆญํฐ ๋น๋</h1>
<div className="grid md:grid-cols-2 gap-8">
{/* ์บ๋ฆญํฐ ํ๋ฆฌ๋ทฐ */}
<div className="bg-white rounded-lg shadow-xl p-6">
<h2 className="text-2xl font-bold mb-4">์บ๋ฆญํฐ ํ๋ฆฌ๋ทฐ</h2>
<div className="bg-gray-100 rounded-lg p-8 min-h-[300px] flex items-center justify-center">
<div className="text-center">
<div className="text-6xl mb-4">
{character.hairstyle === 'short' && '๐ฆ'}
{character.hairstyle === 'long' && '๐ฉ'}
{character.hairstyle === 'curly' && '๐จโ๐ฆฑ'}
{character.hairstyle === 'bald' && '๐จโ๐ฆฒ'}
{character.hairstyle === 'ponytail' && '๐ฑโโ๏ธ'}
</div>
<div className="text-sm text-gray-600 space-y-1">
<p>ํค์ด: {character.hairstyle}</p>
<p>ํผ๋ถ: {character.skin_color}</p>
<p>์์: {character.top}</p>
<p>ํ์: {character.bottom}</p>
<p>์
์ธ์๋ฆฌ: {character.accessory}</p>
</div>
</div>
</div>
</div>
{/* ์ปค์คํฐ๋ง์ด์ง ์ต์
*/}
<div className="bg-white rounded-lg shadow-xl p-6">
<h2 className="text-2xl font-bold mb-4">์ปค์คํฐ๋ง์ด์ง</h2>
<div className="space-y-4">
{/* ๋๋ค์ ์
๋ ฅ */}
<div>
<label className="block text-sm font-medium mb-2">๋๋ค์</label>
<input
type="text"
value={character.nickname}
onChange={(e) => setCharacter(prev => ({ ...prev, nickname: e.target.value }))}
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500"
placeholder="๋ฉ์ง ๋๋ค์์ ์
๋ ฅํ์ธ์"
/>
</div>
{/* ๊ฐ ์ต์
์ ํ */}
{Object.entries(options).map(([category, values]) => (
<div key={category}>
<label className="block text-sm font-medium mb-2 capitalize">
{category.replace('_', ' ')}
</label>
<div className="flex flex-wrap gap-2">
{values.map(value => (
<button
key={value}
onClick={() => handleChange(category, value)}
className={`px-4 py-2 rounded-lg border-2 transition ${
character[category] === value
? 'bg-purple-500 text-white border-purple-600'
: 'bg-white text-gray-700 border-gray-300 hover:border-purple-400'
}`}
>
{value}
</button>
))}
</div>
</div>
))}
{/* ์ ์ฅ ๋ฒํผ */}
<button
onClick={handleSave}
disabled={loading}
className="w-full bg-purple-600 text-white py-3 rounded-lg font-bold hover:bg-purple-700 transition disabled:opacity-50"
>
{loading ? '์ ์ฅ ์ค...' : '๐พ ์บ๋ฆญํฐ ์ ์ฅ'}
</button>
</div>
</div>
</div>
</div>
</div>
)
}
export default App
```
๋ธ๋ผ์ฐ์ ์์ ์ต์
๋ฒํผ์ ํด๋ฆญํ๋ฉด ์บ๋ฆญํฐ ์ ๋ณด๊ฐ ์ค์๊ฐ์ผ๋ก ์
๋ฐ์ดํธ๋๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: React ์ฑ์ ์บ๋ฆญํฐ ์ปค์คํฐ๋ง์ด์ง UI๊ฐ ํ์๋ฉ๋๋ค. ํค์ด์คํ์ผ, ํผ๋ถ์, ์์, ์ก์ธ์๋ฆฌ ์ต์ ์ ์ ํํ๋ฉด ํ๋ฆฌ๋ทฐ ์์ญ์ ์ ํํ ๊ฐ์ด ์ค์๊ฐ์ผ๋ก ํ์๋ฉ๋๋ค. ๋๋ค์ ์ ๋ ฅ ํ๋์ ์ ์ฅ ๋ฒํผ์ด ๋์ํฉ๋๋ค.
# ์ ์ฅ๋ ์บ๋ฆญํฐ ๊ฐค๋ฌ๋ฆฌ ๋ฐ ์ข์์ ๊ธฐ๋ฅ ๊ตฌํ
`src/App.jsx` ํ์ผ์ ์บ๋ฆญํฐ ๊ฐค๋ฌ๋ฆฌ ์น์
์ ์ถ๊ฐํ์ธ์.
๊ธฐ์กด `App` ์ปดํฌ๋ํธ์ ๋ค์ ๊ธฐ๋ฅ์ ์ถ๊ฐ:
1. ์ปดํฌ๋ํธ ๋ง์ดํธ ์ ์บ๋ฆญํฐ ๋ชฉ๋ก ๋ถ๋ฌ์ค๊ธฐ:
```jsx
import { useState, useEffect } from 'react'
// ... ๊ธฐ์กด ์ฝ๋ ...
useEffect(() => {
fetchCharacters()
}, [])
```
2. ์ข์์ ํธ๋ค๋ฌ ์ถ๊ฐ:
```jsx
const handleLike = async (id) => {
try {
await api.post(`/characters/${id}/like`)
fetchCharacters() // ๋ชฉ๋ก ์๋ก๊ณ ์นจ
} catch (error) {
console.error('์ข์์ ์คํจ:', error)
}
}
```
3. ๊ฐค๋ฌ๋ฆฌ UI๋ฅผ `return` ๋ฌธ ๋ง์ง๋ง์ ์ถ๊ฐ:
```jsx
return (
<div className="min-h-screen bg-gradient-to-br from-purple-400 to-pink-300 p-8">
{/* ... ๊ธฐ์กด ์ปค์คํฐ๋ง์ด์ง UI ... */}
{/* ์บ๋ฆญํฐ ๊ฐค๋ฌ๋ฆฌ */}
<div className="max-w-6xl mx-auto mt-12">
<h2 className="text-3xl font-bold text-white text-center mb-6">๐ฅ ์บ๋ฆญํฐ ๊ฐค๋ฌ๋ฆฌ</h2>
{savedCharacters.length === 0 ? (
<div className="bg-white rounded-lg shadow-xl p-8 text-center text-gray-500">
์์ง ์ ์ฅ๋ ์บ๋ฆญํฐ๊ฐ ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ์บ๋ฆญํฐ๋ฅผ ๋ง๋ค์ด๋ณด์ธ์! ๐จ
</div>
) : (
<div className="grid md:grid-cols-3 gap-6">
{savedCharacters.map(char => (
<div key={char.id} className="bg-white rounded-lg shadow-lg p-6 hover:shadow-xl transition">
<div className="text-center mb-4">
<div className="text-5xl mb-2">
{char.hairstyle === 'short' && '๐ฆ'}
{char.hairstyle === 'long' && '๐ฉ'}
{char.hairstyle === 'curly' && '๐จโ๐ฆฑ'}
{char.hairstyle === 'bald' && '๐จโ๐ฆฒ'}
{char.hairstyle === 'ponytail' && '๐ฑโโ๏ธ'}
</div>
<h3 className="text-xl font-bold text-gray-800">{char.nickname}</h3>
</div>
<div className="text-sm text-gray-600 space-y-1 mb-4">
<p>๐จ {char.hairstyle} / {char.skin_color}</p>
<p>๐ {char.top} + {char.bottom}</p>
<p>โจ {char.accessory}</p>
</div>
<button
onClick={() => handleLike(char.id)}
className="w-full bg-pink-500 text-white py-2 rounded-lg font-bold hover:bg-pink-600 transition flex items-center justify-center gap-2"
>
โค๏ธ ์ข์์ {char.likes}
</button>
</div>
))}
</div>
)}
</div>
</div>
)
```
**ํ
์คํธ ์๋๋ฆฌ์ค:**
1. ์บ๋ฆญํฐ๋ฅผ ์ฌ๋ฌ ๊ฐ ๋ง๋ค์ด ์ ์ฅ
2. ๊ฐค๋ฌ๋ฆฌ์ ์ ์ฅ๋ ์บ๋ฆญํฐ๋ค์ด ํ์๋๋์ง ํ์ธ
3. ์ข์์ ๋ฒํผ ํด๋ฆญ ์ ์ซ์๊ฐ ์ฆ๊ฐํ๋์ง ํ์ธ
4. ์ข์์๊ฐ ๋ง์ ์บ๋ฆญํฐ๊ฐ ์๋จ์ ์ ๋ ฌ๋๋์ง ํ์ธ
๋ชจ๋ ๊ธฐ๋ฅ์ด ์ ์ ์๋ํ๋ฉด ์์ฑ์
๋๋ค! ๐
๊ธฐ๋ ๊ฒฐ๊ณผ: React ์ฑ์ ์บ๋ฆญํฐ ๊ฐค๋ฌ๋ฆฌ ์น์ ์ด ์ถ๊ฐ๋ฉ๋๋ค. ์ ์ฅ๋ ์บ๋ฆญํฐ๋ค์ด ์นด๋ ํํ๋ก ํ์๋๊ณ , ๊ฐ ์บ๋ฆญํฐ๋ง๋ค ์ข์์ ๋ฒํผ์ด ๋์ํฉ๋๋ค. ์ข์์๋ฅผ ๋๋ฅด๋ฉด ์ฆ์ ์ซ์๊ฐ ์ฆ๊ฐํ๊ณ , ๊ฐค๋ฌ๋ฆฌ๊ฐ ์ข์์ ์์ผ๋ก ์ ๋ ฌ๋ฉ๋๋ค. ์บ๋ฆญํฐ๊ฐ ์์ ๋๋ ์๋ด ๋ฉ์์ง๊ฐ ํ์๋ฉ๋๋ค.
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!