๐ ์จ๋ผ์ธ ์ํฌ์ ๋ฑ๋ก & ์๋ฃ์ฆ ๋ฐ๊ธ ํ๋ซํผ
workshop-enrollment-certificate-platform
๋ฌธ์ ์ค๋ช
๐ ์จ๋ผ์ธ ์ํฌ์ ๋ฑ๋ก & ์๋ฃ์ฆ ๋ฐ๊ธ ํ๋ซํผ
์ค์ ๊ต์ก ๊ธฐ๊ด์ด๋ ํ๋ฆฌ๋์ ๊ฐ์ฌ๊ฐ ์ฌ์ฉํ ์ ์๋ ์ํฌ์ ๋ฑ๋ก ์์คํ ์ ๋ง๋ญ๋๋ค. ์ฐธ๊ฐ์๋ ์ํฌ์์ ๋๋ฌ๋ณด๊ณ , ๊ด์ฌ ์๋ ์ธ์ ์ ๋ฑ๋กํ๋ฉฐ, ์ถ์ ์ฒดํฌ ํ ์๋์ผ๋ก ์๋ฃ์ฆ์ ๋ฐ๊ธ๋ฐ์ต๋๋ค.
ํ์ ๊ธฐ๋ฅ
- ์ํฌ์ ๋ชฉ๋ก ํ์ด์ง (์นด๋ ๊ทธ๋ฆฌ๋ ๋ ์ด์์, ํ๊ทธ ํํฐ๋ง)
- ์ํฌ์ ์์ธ ํ์ด์ง (๊ฐ์ฌ ์ ๋ณด, ์ปค๋ฆฌํ๋ผ, ์ผ์ , ์ ์)
- ๋ฑ๋ก ํผ (์ด๋ฆ, ์ด๋ฉ์ผ, ์ ํ๋ฒํธ, ์ฐธ์ฌ ๋๊ธฐ)
- ์ค์๊ฐ ์์ฌ์ ํ์ (์ ์ ๋๋น ์ ์ฒญ์ ์)
- ๋์ ๋ฑ๋ก ๋ด์ญ ํ์ด์ง (์ ์ฒญํ ์ํฌ์ ๋ชฉ๋ก)
- ์ถ์ ์ฒดํฌ ๊ธฐ๋ฅ (๊ฐ์ฌ์ฉ ๊ฐ๋จ ์ธํฐํ์ด์ค)
- ์๋ฃ์ฆ ์๋ ๋ฐ๊ธ (์ถ์๋ฅ 80% ์ด์ ์ PDF ์์ฑ ๋๋ ์ด๋ฏธ์ง ๋ค์ด๋ก๋)
๋ณด๋์ค ๊ธฐ๋ฅ
- ์ํฌ์ ์นดํ ๊ณ ๋ฆฌ๋ณ ํํฐ (๋์์ธ, ๊ฐ๋ฐ, ๋ง์ผํ ๋ฑ)
- ๋ฑ๋ก ๋๊ธฐ์ด (์ ์ ์ด๊ณผ ์ ์๋ ๋๊ธฐ ๋ฑ๋ก)
- ์ด๋ฉ์ผ ์๋ฆผ (๋ฑ๋ก ํ์ธ, ์ํฌ์ ํ๋ฃจ ์ ๋ฆฌ๋ง์ธ๋)
- ์๋ฃ์ฆ ์ปค์คํฐ๋ง์ด์ง (์ฐธ๊ฐ์ ์ด๋ฆ, ์ํฌ์๋ช , ๋ ์ง ์๋ ์ฝ์ )
- ๊ฐ์ฌ ํ๊ฐ & ํ๊ธฐ ์์ฑ (๋ณ์ + ํ ์คํธ ๋ฆฌ๋ทฐ)
๊ธฐ์ ์คํ
- Backend: Laravel 13.x (Eloquent ORM, ๋ง์ด๊ทธ๋ ์ด์ , ์ปจํธ๋กค๋ฌ)
- Frontend: React 19 (ํจ์ํ ์ปดํฌ๋ํธ, Hooks)
- ํ์คํ ์ฐ๊ฒฐ: Inertia.js (Laravel โ React SPA)
- ์คํ์ผ๋ง: Tailwind CSS
- DB: SQLite (๊ฐ๋ฐ์ฉ) ๋๋ MySQL
- PDF ์์ฑ: Laravel Snappy ๋๋ DomPDF (์๋ฃ์ฆ ๋ฐ๊ธ์ฉ)
ํ์ต ๋ชฉํ
- Laravel Breeze(React+Inertia) starter kit์ผ๋ก ๋ชจ๋ ธ๋ฆฌ์ค ํ์คํ ํ๋ก์ ํธ ๊ตฌ์ฑ
- Inertia.js๋ฅผ ํตํ Laravel ์ปจํธ๋กค๋ฌ โ React ์ปดํฌ๋ํธ ๋ฐ์ดํฐ ์ ๋ฌ
- Eloquent ๊ด๊ณ ์ค์ (Workshop hasMany Enrollment, User hasMany Enrollment)
- ์กฐ๊ฑด๋ถ ๋ ๋๋ง (์ ์ ๋ง๊ฐ, ๋๊ธฐ ์ํ UI)
- ํผ ์ ํจ์ฑ ๊ฒ์ฌ (Laravel Request Validation + React ์๋ฌ ํ์)
- ํ์ผ/์ด๋ฏธ์ง ๋ค์ด๋ก๋ (์๋ฃ์ฆ PDF ์์ฑ)
์ด ํ๋ก์ ํธ๋ ์ค์ ๋น์ฆ๋์ค ์๋๋ฆฌ์ค์์ ์ฆ์ ํ์ฉ ๊ฐ๋ฅํ ์์ค์ ์ํฌ์ ๊ด๋ฆฌ ์์คํ ์ ๋๋ค.
์ ์ฝ ์กฐ๊ฑด
- โข Laravel 13.x + Inertia.js + React 19 ๋ชจ๋ ธ๋ฆฌ์ค ํ์คํ ๊ตฌ์กฐ (๋ณ๋ API ๋ผ์ฐํธ ์์)
- โข npx create laravel ๋๋ Laravel Breeze(React+Inertia) starter kit์ผ๋ก ํ๋ก์ ํธ ์์ฑ
- โข SQLite ๋๋ MySQL ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฌ์ฉ
- โข Tailwind CSS๋ก ๋ฐ์ํ UI ๊ตฌํ
- โข Eloquent ORM์ผ๋ก Workshop, Enrollment, User ๋ชจ๋ธ ์ค๊ณ
- โข Inertia::render()๋ก Laravel ์ปจํธ๋กค๋ฌ์์ React ์ปดํฌ๋ํธ์ ๋ฐ์ดํฐ ์ ๋ฌ
- โข ํผ ์ ์ถ ์ Laravel Validation ์ฌ์ฉ (FormRequest ํด๋์ค ๊ถ์ฅ)
- โข ์๋ฃ์ฆ ๋ฐ๊ธ ์ Laravel Snappy ๋๋ DomPDF ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํ์ฉ
- โข ์ค์๊ฐ ์์ฌ์ ๊ณ์ฐ ๋ก์ง (์ ์ - ํ์ฌ ์ ์ฒญ์ ์)
- โข ์ถ์ ์ฒดํฌ ๊ธฐ๋ฅ์ ๊ฐ๋จํ ์ฒดํฌ๋ฐ์ค ํ ๊ธ (๊ฐ์ฌ์ฉ ์ธํฐํ์ด์ค)
- โข React ์ปดํฌ๋ํธ๋ ํจ์ํ ์ปดํฌ๋ํธ + Hooks(useState, useEffect) ์ฌ์ฉ
- โข ์ฝ๋ ์ฃผ์ ํ์ (ํนํ Inertia.js ๋ฐ์ดํฐ ํ๋ฆ ๋ถ๋ถ)
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
mkdir workshop-enrollment-certificate-platform && cd workshop-enrollment-certificate-platform
์ด ํด๋์์ ํ๋ก์ ํธ๋ฅผ ์์ํฉ๋๋ค.
Laravel 13.x + Inertia.js + React 19 ํ์คํ ํ๋ก์ ํธ๋ฅผ ์์ฑํ์ธ์.
๋ค์ ๋ช
๋ น์ด๋ก Laravel Breeze(React+Inertia) starter kit์ ์ค์นํฉ๋๋ค:
```bash
composer create-project laravel/laravel .
composer require laravel/breeze --dev
php artisan breeze:install react
npm install
npm run dev
```
ํ๋ก์ ํธ ๊ตฌ์กฐ:
- Laravel ๋ฐฑ์๋ (routes/web.php, app/Http/Controllers, app/Models)
- React ํ๋ก ํธ์๋ (resources/js/Pages, resources/js/Components)
- Inertia.js๊ฐ ๋์ ์ฐ๊ฒฐ (๋ณ๋ API ๋ผ์ฐํธ ๋ถํ์)
๋ฐ์ดํฐ๋ฒ ์ด์ค๋ SQLite๋ฅผ ์ฌ์ฉํฉ๋๋ค. .env ํ์ผ์์ DB_CONNECTION=sqlite๋ก ์ค์ ํ๊ณ , database/database.sqlite ํ์ผ์ ์์ฑํ์ธ์.
๋ค์ 3๊ฐ์ ๋ชจ๋ธ๊ณผ ๋ง์ด๊ทธ๋ ์ด์
์ ์์ฑํ์ธ์:
1. Workshop ๋ชจ๋ธ (์ํฌ์ ์ ๋ณด)
- title (string, ์ํฌ์ ์ ๋ชฉ)
- description (text, ์์ธ ์ค๋ช
)
- instructor (string, ๊ฐ์ฌ๋ช
)
- category (string, ์นดํ
๊ณ ๋ฆฌ: ๋์์ธ/๊ฐ๋ฐ/๋ง์ผํ
๋ฑ)
- start_date (date, ์์์ผ)
- end_date (date, ์ข
๋ฃ์ผ)
- capacity (integer, ์ ์)
- image_url (string, nullable, ์ธ๋ค์ผ ์ด๋ฏธ์ง)
- timestamps
2. Enrollment ๋ชจ๋ธ (๋ฑ๋ก ์ ๋ณด)
- workshop_id (foreignId, workshops ํ
์ด๋ธ ์ฐธ์กฐ)
- name (string, ์ฐธ๊ฐ์ ์ด๋ฆ)
- email (string, ์ด๋ฉ์ผ)
- phone (string, ์ ํ๋ฒํธ)
- motivation (text, ์ฐธ์ฌ ๋๊ธฐ)
- status (enum: 'registered', 'attended', 'completed', 'waiting', ๊ธฐ๋ณธ๊ฐ 'registered')
- attended_sessions (integer, ๊ธฐ๋ณธ๊ฐ 0, ์ถ์ํ ์ธ์
์)
- total_sessions (integer, ๊ธฐ๋ณธ๊ฐ 4, ์ ์ฒด ์ธ์
์)
- timestamps
3. User ๋ชจ๋ธ (๊ธฐ๋ณธ Laravel Breeze ์ ๊ณต, ์ถ๊ฐ ์์ ๋ถํ์)
๋ง์ด๊ทธ๋ ์ด์
์คํ:
```bash
php artisan migrate
```
Workshop ๋ชจ๋ธ์ Enrollment์์ ๊ด๊ณ ์ค์ :
```php
public function enrollments()
{
return $this->hasMany(Enrollment::class);
}
public function availableSeats()
{
return $this->capacity - $this->enrollments()->count();
}
```
Enrollment ๋ชจ๋ธ์ Workshop๊ณผ์ ๊ด๊ณ ์ค์ :
```php
public function workshop()
{
return $this->belongsTo(Workshop::class);
}
```
์๋(Seeder)๋ฅผ ์์ฑํ์ฌ ๋๋ฏธ ์ํฌ์ ๋ฐ์ดํฐ 5๊ฐ๋ฅผ ์์ฑํ์ธ์:
```bash
php artisan make:seeder WorkshopSeeder
```
WorkshopSeeder ๋ด์ฉ:
```php
Workshop::create([
'title' => 'React 19 ์ค์ ์ํฌ์',
'description' => 'React ์ต์ ๊ธฐ๋ฅ์ ํ์ฉํ SPA ๊ฐ๋ฐ ์ค์ต',
'instructor' => '๊น๊ฐ๋ฐ',
'category' => '๊ฐ๋ฐ',
'start_date' => now()->addDays(10),
'end_date' => now()->addDays(12),
'capacity' => 20,
'image_url' => 'https://via.placeholder.com/400x300?text=React+Workshop',
]);
// ๋๋จธ์ง 4๊ฐ ์ํฌ์๋ ์ ์ฌํ๊ฒ ์์ฑ (๋์์ธ, ๋ง์ผํ
์นดํ
๊ณ ๋ฆฌ ํฌํจ)
```
์๋ ์คํ:
```bash
php artisan db:seed --class=WorkshopSeeder
```
ํ๋ก์ ํธ๊ฐ ์ ์์ ์ผ๋ก ์คํ๋๋์ง ํ์ธ:
```bash
php artisan serve
```
http://localhost:8000 ์ ์ ์ Laravel Breeze ๊ธฐ๋ณธ ํ๋ฉด์ด ๋ํ๋๋ฉด ์ฑ๊ณต์
๋๋ค.
๊ธฐ๋ ๊ฒฐ๊ณผ: Laravel 13.x + Inertia.js + React 19 ํ๋ก์ ํธ๊ฐ ์์ฑ๋๊ณ , Workshop, Enrollment ๋ชจ๋ธ๊ณผ ๋ง์ด๊ทธ๋ ์ด์ ์ด ์๋ฃ๋ฉ๋๋ค. ๋๋ฏธ ์ํฌ์ ๋ฐ์ดํฐ 5๊ฐ๊ฐ DB์ ์ ์ฅ๋๋ฉฐ, ํ๋ก์ ํธ๊ฐ ๋ก์ปฌ์์ ์ ์ ์คํ๋ฉ๋๋ค.
์ํฌ์ ๋ชฉ๋ก ํ์ด์ง๋ฅผ ๊ตฌํํ์ธ์.
Laravel ์ปจํธ๋กค๋ฌ๋ฅผ ์์ฑํฉ๋๋ค:
```bash
php artisan make:controller WorkshopController
```
WorkshopController์ index ๋ฉ์๋ ์์ฑ:
```php
use Inertia\Inertia;
use App\Models\Workshop;
public function index()
{
$workshops = Workshop::withCount('enrollments')
->orderBy('start_date', 'asc')
->get()
->map(function ($workshop) {
return [
'id' => $workshop->id,
'title' => $workshop->title,
'instructor' => $workshop->instructor,
'category' => $workshop->category,
'start_date' => $workshop->start_date->format('Y-m-d'),
'capacity' => $workshop->capacity,
'enrolled' => $workshop->enrollments_count,
'available' => $workshop->capacity - $workshop->enrollments_count,
'image_url' => $workshop->image_url,
];
});
return Inertia::render('Workshops/Index', [
'workshops' => $workshops,
]);
}
```
routes/web.php์ ๋ผ์ฐํธ ์ถ๊ฐ:
```php
use App\Http\Controllers\WorkshopController;
Route::get('/workshops', [WorkshopController::class, 'index'])->name('workshops.index');
```
React ์ปดํฌ๋ํธ ์์ฑ:
resources/js/Pages/Workshops/Index.jsx ํ์ผ์ ๋ง๋ค๊ณ ๋ค์ ๋ด์ฉ์ ์์ฑํ์ธ์:
```jsx
import React, { useState } from 'react';
import { Head, Link } from '@inertiajs/react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
export default function Index({ auth, workshops }) {
const [selectedCategory, setSelectedCategory] = useState('all');
const categories = ['all', '๊ฐ๋ฐ', '๋์์ธ', '๋ง์ผํ
'];
const filteredWorkshops = selectedCategory === 'all'
? workshops
: workshops.filter(w => w.category === selectedCategory);
return (
<AuthenticatedLayout user={auth.user}>
<Head title="์ํฌ์ ๋ชฉ๋ก" />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
<h1 className="text-3xl font-bold mb-6">์ํฌ์ ๋ชฉ๋ก</h1>
{/* ์นดํ
๊ณ ๋ฆฌ ํํฐ */}
<div className="flex gap-2 mb-6">
{categories.map(cat => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-4 py-2 rounded-lg font-medium transition ${
selectedCategory === cat
? 'bg-indigo-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
{cat === 'all' ? '์ ์ฒด' : cat}
</button>
))}
</div>
{/* ์ํฌ์ ์นด๋ ๊ทธ๋ฆฌ๋ */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredWorkshops.map(workshop => (
<div key={workshop.id} className="border rounded-lg overflow-hidden shadow hover:shadow-lg transition">
<img src={workshop.image_url} alt={workshop.title} className="w-full h-48 object-cover" />
<div className="p-4">
<span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded mb-2">
{workshop.category}
</span>
<h3 className="text-xl font-bold mb-2">{workshop.title}</h3>
<p className="text-gray-600 text-sm mb-2">๊ฐ์ฌ: {workshop.instructor}</p>
<p className="text-gray-600 text-sm mb-4">์์์ผ: {workshop.start_date}</p>
<div className="flex justify-between items-center">
<span className={`text-sm font-medium ${
workshop.available > 0 ? 'text-green-600' : 'text-red-600'
}`}>
{workshop.available > 0
? `์์ฌ์ ${workshop.available}์`
: '๋ง๊ฐ'}
</span>
<Link
href={`/workshops/${workshop.id}`}
className="bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700 transition"
>
์์ธํ ๋ณด๊ธฐ
</Link>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}
```
๋ค๋น๊ฒ์ด์
๋ฉ๋ด์ ์ํฌ์ ๋งํฌ ์ถ๊ฐ:
resources/js/Layouts/AuthenticatedLayout.jsx ํ์ผ์์ ๋ค๋น๊ฒ์ด์
๋ถ๋ถ์ ๋ค์ ๋งํฌ๋ฅผ ์ถ๊ฐํ์ธ์:
```jsx
<Link href="/workshops" className="...">
์ํฌ์
</Link>
```
๋ธ๋ผ์ฐ์ ์์ http://localhost:8000/workshops ์ ์ ์ ์ํฌ์ ์นด๋ ๊ทธ๋ฆฌ๋๊ฐ ํ์๋๊ณ , ์นดํ
๊ณ ๋ฆฌ ํํฐ๊ฐ ์๋ํ๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ํฌ์ ๋ชฉ๋ก ํ์ด์ง๊ฐ ์์ฑ๋ฉ๋๋ค. ์นด๋ ๊ทธ๋ฆฌ๋ ๋ ์ด์์์ผ๋ก ์ํฌ์์ด ํ์๋๋ฉฐ, ์นดํ ๊ณ ๋ฆฌ ํํฐ ๋ฒํผ์ ํด๋ฆญํ๋ฉด ํด๋น ์นดํ ๊ณ ๋ฆฌ์ ์ํฌ์๋ง ๋ณด์ ๋๋ค. ์์ฌ์์ด ์ค์๊ฐ์ผ๋ก ๊ณ์ฐ๋์ด ํ์๋ฉ๋๋ค.
์ํฌ์ ์์ธ ํ์ด์ง์ ๋ฑ๋ก ํผ์ ๊ตฌํํ์ธ์.
WorkshopController์ show ๋ฉ์๋ ์ถ๊ฐ:
```php
public function show($id)
{
$workshop = Workshop::withCount('enrollments')->findOrFail($id);
return Inertia::render('Workshops/Show', [
'workshop' => [
'id' => $workshop->id,
'title' => $workshop->title,
'description' => $workshop->description,
'instructor' => $workshop->instructor,
'category' => $workshop->category,
'start_date' => $workshop->start_date->format('Y-m-d'),
'end_date' => $workshop->end_date->format('Y-m-d'),
'capacity' => $workshop->capacity,
'enrolled' => $workshop->enrollments_count,
'available' => $workshop->capacity - $workshop->enrollments_count,
'image_url' => $workshop->image_url,
],
]);
}
```
routes/web.php์ ๋ผ์ฐํธ ์ถ๊ฐ:
```php
Route::get('/workshops/{id}', [WorkshopController::class, 'show'])->name('workshops.show');
```
React ์ปดํฌ๋ํธ ์์ฑ:
resources/js/Pages/Workshops/Show.jsx ํ์ผ์ ๋ง๋ค๊ณ ๋ค์ ๋ด์ฉ์ ์์ฑํ์ธ์:
```jsx
import React, { useState } from 'react';
import { Head, useForm } from '@inertiajs/react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
export default function Show({ auth, workshop }) {
const [showForm, setShowForm] = useState(false);
const { data, setData, post, processing, errors } = useForm({
name: '',
email: '',
phone: '',
motivation: '',
});
const handleSubmit = (e) => {
e.preventDefault();
post(`/workshops/${workshop.id}/enroll`, {
onSuccess: () => {
alert('๋ฑ๋ก์ด ์๋ฃ๋์์ต๋๋ค!');
setShowForm(false);
},
});
};
return (
<AuthenticatedLayout user={auth.user}>
<Head title={workshop.title} />
<div className="py-12">
<div className="max-w-4xl mx-auto sm:px-6 lg:px-8">
<div className="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
<img src={workshop.image_url} alt={workshop.title} className="w-full h-64 object-cover rounded-lg mb-6" />
<span className="inline-block bg-blue-100 text-blue-800 text-sm px-3 py-1 rounded mb-4">
{workshop.category}
</span>
<h1 className="text-4xl font-bold mb-4">{workshop.title}</h1>
<p className="text-gray-600 mb-2">๊ฐ์ฌ: <strong>{workshop.instructor}</strong></p>
<p className="text-gray-600 mb-2">๊ธฐ๊ฐ: {workshop.start_date} ~ {workshop.end_date}</p>
<p className="text-gray-600 mb-6">์ ์: {workshop.capacity}๋ช
(์์ฌ์ {workshop.available}์)</p>
<div className="prose max-w-none mb-6">
<h2 className="text-2xl font-bold mb-2">์ํฌ์ ์๊ฐ</h2>
<p>{workshop.description}</p>
</div>
{workshop.available > 0 ? (
<button
onClick={() => setShowForm(!showForm)}
className="bg-indigo-600 text-white px-6 py-3 rounded-lg hover:bg-indigo-700 transition font-medium"
>
{showForm ? '๋ฑ๋ก ํผ ๋ซ๊ธฐ' : '์ง๊ธ ๋ฑ๋กํ๊ธฐ'}
</button>
) : (
<div className="bg-red-100 text-red-800 px-4 py-3 rounded-lg">
ํ์ฌ ์ ์์ด ๋ง๊ฐ๋์์ต๋๋ค.
</div>
)}
{showForm && (
<form onSubmit={handleSubmit} className="mt-6 space-y-4 border-t pt-6">
<h3 className="text-2xl font-bold mb-4">๋ฑ๋ก ์ ๋ณด ์
๋ ฅ</h3>
<div>
<label className="block text-sm font-medium mb-1">์ด๋ฆ</label>
<input
type="text"
value={data.name}
onChange={e => setData('name', e.target.value)}
className="w-full border rounded-lg px-4 py-2"
required
/>
{errors.name && <p className="text-red-600 text-sm mt-1">{errors.name}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-1">์ด๋ฉ์ผ</label>
<input
type="email"
value={data.email}
onChange={e => setData('email', e.target.value)}
className="w-full border rounded-lg px-4 py-2"
required
/>
{errors.email && <p className="text-red-600 text-sm mt-1">{errors.email}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-1">์ ํ๋ฒํธ</label>
<input
type="tel"
value={data.phone}
onChange={e => setData('phone', e.target.value)}
className="w-full border rounded-lg px-4 py-2"
required
/>
{errors.phone && <p className="text-red-600 text-sm mt-1">{errors.phone}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-1">์ฐธ์ฌ ๋๊ธฐ</label>
<textarea
value={data.motivation}
onChange={e => setData('motivation', e.target.value)}
className="w-full border rounded-lg px-4 py-2 h-32"
required
/>
{errors.motivation && <p className="text-red-600 text-sm mt-1">{errors.motivation}</p>}
</div>
<button
type="submit"
disabled={processing}
className="bg-green-600 text-white px-6 py-3 rounded-lg hover:bg-green-700 transition font-medium disabled:opacity-50"
>
{processing ? '๋ฑ๋ก ์ค...' : '๋ฑ๋ก ์๋ฃ'}
</button>
</form>
)}
</div>
</div>
</div>
</AuthenticatedLayout>
);
}
```
WorkshopController์ enroll ๋ฉ์๋ ์ถ๊ฐ (๋ฑ๋ก ์ฒ๋ฆฌ):
```php
use App\Models\Enrollment;
use Illuminate\Http\Request;
public function enroll(Request $request, $id)
{
$workshop = Workshop::findOrFail($id);
// ์ ์ ์ฒดํฌ
if ($workshop->enrollments()->count() >= $workshop->capacity) {
return back()->withErrors(['error' => '์ ์์ด ๋ง๊ฐ๋์์ต๋๋ค.']);
}
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|max:255',
'phone' => 'required|string|max:20',
'motivation' => 'required|string',
]);
Enrollment::create([
'workshop_id' => $workshop->id,
'name' => $validated['name'],
'email' => $validated['email'],
'phone' => $validated['phone'],
'motivation' => $validated['motivation'],
'status' => 'registered',
'total_sessions' => 4, // ๊ธฐ๋ณธ 4ํ์ฐจ
]);
return redirect()->route('workshops.index')->with('success', '๋ฑ๋ก์ด ์๋ฃ๋์์ต๋๋ค!');
}
```
routes/web.php์ ๋ผ์ฐํธ ์ถ๊ฐ:
```php
Route::post('/workshops/{id}/enroll', [WorkshopController::class, 'enroll'])->name('workshops.enroll');
```
๋ธ๋ผ์ฐ์ ์์ ์ํฌ์ ์์ธ ํ์ด์ง ์ ์ ํ ๋ฑ๋ก ํผ์ ์์ฑํ๊ณ ์ ์ถํ์ธ์. DB์ Enrollment ๋ ์ฝ๋๊ฐ ์์ฑ๋๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ํฌ์ ์์ธ ํ์ด์ง๊ฐ ์์ฑ๋๊ณ , ๋ฑ๋ก ํผ์ด ์๋ํฉ๋๋ค. ํผ ์ ์ถ ์ Laravel Validation์ด ์๋ํ๋ฉฐ, ์ ์ ์ด๊ณผ ์ ์๋ฌ ๋ฉ์์ง๊ฐ ํ์๋ฉ๋๋ค. ๋ฑ๋ก ์ฑ๊ณต ์ DB์ ๋ฐ์ดํฐ๊ฐ ์ ์ฅ๋ฉ๋๋ค.
๋์ ๋ฑ๋ก ๋ด์ญ ํ์ด์ง๋ฅผ ๊ตฌํํ์ธ์.
WorkshopController์ myEnrollments ๋ฉ์๋ ์ถ๊ฐ:
```php
public function myEnrollments(Request $request)
{
// ํ์ฌ ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ์ด๋ฉ์ผ๋ก ๋ฑ๋ก ๋ด์ญ ์กฐํ (๊ฐ๋จํ ๊ตฌํ)
// ์ค๋ฌด์์๋ User ๋ชจ๋ธ๊ณผ ์ฐ๊ฒฐํ๋ ๊ฒ์ด ์ข์ง๋ง, ์ฌ๊ธฐ์๋ ์ด๋ฉ์ผ ๊ธฐ๋ฐ์ผ๋ก ์กฐํ
$email = $request->user()->email;
$enrollments = Enrollment::with('workshop')
->where('email', $email)
->orderBy('created_at', 'desc')
->get()
->map(function ($enrollment) {
return [
'id' => $enrollment->id,
'workshop_title' => $enrollment->workshop->title,
'workshop_instructor' => $enrollment->workshop->instructor,
'workshop_start_date' => $enrollment->workshop->start_date->format('Y-m-d'),
'status' => $enrollment->status,
'attended_sessions' => $enrollment->attended_sessions,
'total_sessions' => $enrollment->total_sessions,
'attendance_rate' => round(($enrollment->attended_sessions / $enrollment->total_sessions) * 100),
'can_certificate' => $enrollment->attended_sessions >= ($enrollment->total_sessions * 0.8),
];
});
return Inertia::render('Workshops/MyEnrollments', [
'enrollments' => $enrollments,
]);
}
```
routes/web.php์ ๋ผ์ฐํธ ์ถ๊ฐ:
```php
Route::get('/my-enrollments', [WorkshopController::class, 'myEnrollments'])->name('workshops.my-enrollments');
```
React ์ปดํฌ๋ํธ ์์ฑ:
resources/js/Pages/Workshops/MyEnrollments.jsx ํ์ผ์ ๋ง๋ค๊ณ ๋ค์ ๋ด์ฉ์ ์์ฑํ์ธ์:
```jsx
import React from 'react';
import { Head, Link } from '@inertiajs/react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
export default function MyEnrollments({ auth, enrollments }) {
const statusLabels = {
registered: '๋ฑ๋ก ์๋ฃ',
attended: '์ถ์ ์ค',
completed: '์๋ฃ',
waiting: '๋๊ธฐ',
};
const statusColors = {
registered: 'bg-blue-100 text-blue-800',
attended: 'bg-yellow-100 text-yellow-800',
completed: 'bg-green-100 text-green-800',
waiting: 'bg-gray-100 text-gray-800',
};
return (
<AuthenticatedLayout user={auth.user}>
<Head title="๋์ ๋ฑ๋ก ๋ด์ญ" />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
<h1 className="text-3xl font-bold mb-6">๋์ ๋ฑ๋ก ๋ด์ญ</h1>
{enrollments.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-600 mb-4">๋ฑ๋กํ ์ํฌ์์ด ์์ต๋๋ค.</p>
<Link
href="/workshops"
className="bg-indigo-600 text-white px-6 py-3 rounded-lg hover:bg-indigo-700 transition"
>
์ํฌ์ ๋๋ฌ๋ณด๊ธฐ
</Link>
</div>
) : (
<div className="space-y-4">
{enrollments.map(enrollment => (
<div key={enrollment.id} className="border rounded-lg p-6 hover:shadow-lg transition">
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-xl font-bold mb-1">{enrollment.workshop_title}</h3>
<p className="text-gray-600 text-sm">๊ฐ์ฌ: {enrollment.workshop_instructor}</p>
<p className="text-gray-600 text-sm">์์์ผ: {enrollment.workshop_start_date}</p>
</div>
<span className={`px-3 py-1 rounded text-sm font-medium ${statusColors[enrollment.status]}`}>
{statusLabels[enrollment.status]}
</span>
</div>
<div className="bg-gray-50 rounded-lg p-4 mb-4">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium">์ถ์๋ฅ </span>
<span className="text-sm font-bold">{enrollment.attendance_rate}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${
enrollment.attendance_rate >= 80 ? 'bg-green-600' : 'bg-yellow-600'
}`}
style={{ width: `${enrollment.attendance_rate}%` }}
/>
</div>
<p className="text-xs text-gray-600 mt-1">
{enrollment.attended_sessions} / {enrollment.total_sessions} ์ธ์
์ถ์
</p>
</div>
{enrollment.can_certificate ? (
<Link
href={`/certificates/${enrollment.id}`}
className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 transition inline-block"
>
์๋ฃ์ฆ ๋ฐ๊ธ๋ฐ๊ธฐ
</Link>
) : (
<p className="text-sm text-gray-600">
์ถ์๋ฅ 80% ์ด์ ๋ฌ์ฑ ์ ์๋ฃ์ฆ์ ๋ฐ๊ธ๋ฐ์ ์ ์์ต๋๋ค.
</p>
)}
</div>
))}
</div>
)}
</div>
</div>
</div>
</AuthenticatedLayout>
);
}
```
๋ค๋น๊ฒ์ด์
๋ฉ๋ด์ "๋์ ๋ฑ๋ก ๋ด์ญ" ๋งํฌ ์ถ๊ฐ:
resources/js/Layouts/AuthenticatedLayout.jsx ํ์ผ์์ ๋ค๋น๊ฒ์ด์
๋ถ๋ถ์ ๋ค์ ๋งํฌ๋ฅผ ์ถ๊ฐํ์ธ์:
```jsx
<Link href="/my-enrollments" className="...">
๋์ ๋ฑ๋ก ๋ด์ญ
</Link>
```
๋ธ๋ผ์ฐ์ ์์ http://localhost:8000/my-enrollments ์ ์ ์ ๋ฑ๋กํ ์ํฌ์ ๋ชฉ๋ก๊ณผ ์ถ์๋ฅ ์ด ํ์๋๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๋์ ๋ฑ๋ก ๋ด์ญ ํ์ด์ง๊ฐ ์์ฑ๋ฉ๋๋ค. ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ์ด๋ฉ์ผ๋ก ๋ฑ๋กํ ์ํฌ์ ๋ชฉ๋ก์ด ํ์๋๋ฉฐ, ์ถ์๋ฅ ์ด ํ๋ก๊ทธ๋ ์ค ๋ฐ๋ก ์๊ฐํ๋ฉ๋๋ค. ์ถ์๋ฅ 80% ์ด์ ์ ์๋ฃ์ฆ ๋ฐ๊ธ ๋ฒํผ์ด ํ์ฑํ๋ฉ๋๋ค.
์ถ์ ์ฒดํฌ ๊ธฐ๋ฅ(๊ฐ์ฌ์ฉ ๊ฐ๋จ ์ธํฐํ์ด์ค)์ ๊ตฌํํ์ธ์.
WorkshopController์ attendanceForm ๋ฉ์๋ ์ถ๊ฐ (์ถ์ ์ฒดํฌ ํ์ด์ง):
```php
public function attendanceForm($id)
{
$workshop = Workshop::findOrFail($id);
$enrollments = $workshop->enrollments()->get()->map(function ($enrollment) {
return [
'id' => $enrollment->id,
'name' => $enrollment->name,
'email' => $enrollment->email,
'attended_sessions' => $enrollment->attended_sessions,
'total_sessions' => $enrollment->total_sessions,
];
});
return Inertia::render('Workshops/Attendance', [
'workshop' => [
'id' => $workshop->id,
'title' => $workshop->title,
],
'enrollments' => $enrollments,
]);
}
public function markAttendance(Request $request, $id)
{
$validated = $request->validate([
'enrollment_id' => 'required|exists:enrollments,id',
]);
$enrollment = Enrollment::findOrFail($validated['enrollment_id']);
$enrollment->attended_sessions = min($enrollment->attended_sessions + 1, $enrollment->total_sessions);
// ์ถ์๋ฅ 80% ์ด์ ์ ์ํ๋ฅผ 'completed'๋ก ๋ณ๊ฒฝ
if ($enrollment->attended_sessions >= ($enrollment->total_sessions * 0.8)) {
$enrollment->status = 'completed';
} else {
$enrollment->status = 'attended';
}
$enrollment->save();
return back()->with('success', '์ถ์์ด ๊ธฐ๋ก๋์์ต๋๋ค.');
}
```
routes/web.php์ ๋ผ์ฐํธ ์ถ๊ฐ:
```php
Route::get('/workshops/{id}/attendance', [WorkshopController::class, 'attendanceForm'])->name('workshops.attendance');
Route::post('/workshops/{id}/attendance', [WorkshopController::class, 'markAttendance'])->name('workshops.mark-attendance');
```
React ์ปดํฌ๋ํธ ์์ฑ:
resources/js/Pages/Workshops/Attendance.jsx ํ์ผ์ ๋ง๋ค๊ณ ๋ค์ ๋ด์ฉ์ ์์ฑํ์ธ์:
```jsx
import React from 'react';
import { Head, useForm } from '@inertiajs/react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
export default function Attendance({ auth, workshop, enrollments }) {
const { post, processing } = useForm();
const handleAttendance = (enrollmentId) => {
if (confirm('์ถ์ ์ฒ๋ฆฌํ์๊ฒ ์ต๋๊น?')) {
post(`/workshops/${workshop.id}/attendance`, {
data: { enrollment_id: enrollmentId },
onSuccess: () => alert('์ถ์์ด ๊ธฐ๋ก๋์์ต๋๋ค!'),
});
}
};
return (
<AuthenticatedLayout user={auth.user}>
<Head title={`์ถ์ ์ฒดํฌ - ${workshop.title}`} />
<div className="py-12">
<div className="max-w-5xl mx-auto sm:px-6 lg:px-8">
<div className="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
<h1 className="text-3xl font-bold mb-2">{workshop.title}</h1>
<p className="text-gray-600 mb-6">์ถ์ ์ฒดํฌ (๊ฐ์ฌ์ฉ)</p>
{enrollments.length === 0 ? (
<p className="text-gray-600">๋ฑ๋ก๋ ์ฐธ๊ฐ์๊ฐ ์์ต๋๋ค.</p>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
์ด๋ฆ
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
์ด๋ฉ์ผ
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
์ถ์ ํํฉ
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
์ก์
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{enrollments.map(enrollment => (
<tr key={enrollment.id}>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{enrollment.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-500">{enrollment.email}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{enrollment.attended_sessions} / {enrollment.total_sessions} ์ธ์
</div>
<div className="w-32 bg-gray-200 rounded-full h-2 mt-1">
<div
className="bg-blue-600 h-2 rounded-full"
style={{
width: `${(enrollment.attended_sessions / enrollment.total_sessions) * 100}%`,
}}
/>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<button
onClick={() => handleAttendance(enrollment.id)}
disabled={processing || enrollment.attended_sessions >= enrollment.total_sessions}
className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{enrollment.attended_sessions >= enrollment.total_sessions
? '์ถ์ ์๋ฃ'
: '์ถ์ ์ฒดํฌ'}
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
</AuthenticatedLayout>
);
}
```
์ํฌ์ ์์ธ ํ์ด์ง(Show.jsx)์ ์ถ์ ์ฒดํฌ ํ์ด์ง๋ก ์ด๋ํ๋ ๋ฒํผ ์ถ๊ฐ:
```jsx
<Link
href={`/workshops/${workshop.id}/attendance`}
className="bg-purple-600 text-white px-4 py-2 rounded hover:bg-purple-700 transition ml-2"
>
์ถ์ ์ฒดํฌ (๊ฐ์ฌ์ฉ)
</Link>
```
๋ธ๋ผ์ฐ์ ์์ ์ถ์ ์ฒดํฌ ํ์ด์ง์ ์ ์ํ์ฌ ์ฐธ๊ฐ์์ ์ถ์์ ๊ธฐ๋กํ์ธ์. ์ถ์ ๋ฒํผ ํด๋ฆญ ์ DB์ attended_sessions ๊ฐ์ด ์ฆ๊ฐํ๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ถ์ ์ฒดํฌ ํ์ด์ง๊ฐ ์์ฑ๋ฉ๋๋ค. ๊ฐ์ฌ๋ ์ฐธ๊ฐ์๋ณ๋ก ์ถ์ ๋ฒํผ์ ํด๋ฆญํ์ฌ ์ถ์์ ๊ธฐ๋กํ ์ ์์ผ๋ฉฐ, ์ถ์ ํํฉ์ด ํ๋ก๊ทธ๋ ์ค ๋ฐ๋ก ์๊ฐํ๋ฉ๋๋ค. ์ถ์๋ฅ 80% ์ด์ ์ ์๋์ผ๋ก ์ํ๊ฐ 'completed'๋ก ๋ณ๊ฒฝ๋ฉ๋๋ค.
์๋ฃ์ฆ ์๋ ๋ฐ๊ธ ๊ธฐ๋ฅ์ ๊ตฌํํ์ธ์.
Laravel์์ PDF ์์ฑ์ ์ํด DomPDF ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ค์นํฉ๋๋ค:
```bash
composer require barryvdh/laravel-dompdf
```
WorkshopController์ certificate ๋ฉ์๋ ์ถ๊ฐ:
```php
use Barryvdh\DomPDF\Facade\Pdf;
public function certificate($enrollmentId)
{
$enrollment = Enrollment::with('workshop')->findOrFail($enrollmentId);
// ์ถ์๋ฅ 80% ๋ฏธ๋ง์ด๋ฉด ์ ๊ทผ ๊ฑฐ๋ถ
if ($enrollment->attended_sessions < ($enrollment->total_sessions * 0.8)) {
return back()->withErrors(['error' => '์ถ์๋ฅ 80% ์ด์ ๋ฌ์ฑ ์ ์๋ฃ์ฆ์ ๋ฐ๊ธ๋ฐ์ ์ ์์ต๋๋ค.']);
}
// ์๋ฃ์ฆ ๋ฐ์ดํฐ
$data = [
'name' => $enrollment->name,
'workshop_title' => $enrollment->workshop->title,
'instructor' => $enrollment->workshop->instructor,
'start_date' => $enrollment->workshop->start_date->format('Y๋
m์ d์ผ'),
'end_date' => $enrollment->workshop->end_date->format('Y๋
m์ d์ผ'),
'issue_date' => now()->format('Y๋
m์ d์ผ'),
'attendance_rate' => round(($enrollment->attended_sessions / $enrollment->total_sessions) * 100),
];
// PDF ์์ฑ
$pdf = Pdf::loadView('certificates.template', $data);
return $pdf->download('certificate_' . $enrollment->id . '.pdf');
}
```
routes/web.php์ ๋ผ์ฐํธ ์ถ๊ฐ:
```php
Route::get('/certificates/{enrollmentId}', [WorkshopController::class, 'certificate'])->name('certificates.show');
```
์๋ฃ์ฆ ํ
ํ๋ฆฟ Blade ํ์ผ ์์ฑ:
resources/views/certificates/template.blade.php ํ์ผ์ ๋ง๋ค๊ณ ๋ค์ ๋ด์ฉ์ ์์ฑํ์ธ์:
```blade
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>์๋ฃ์ฆ</title>
<style>
body {
font-family: 'DejaVu Sans', sans-serif;
text-align: center;
padding: 50px;
}
.certificate {
border: 10px solid #4F46E5;
padding: 40px;
margin: 0 auto;
max-width: 800px;
}
h1 {
font-size: 48px;
margin-bottom: 20px;
color: #4F46E5;
}
h2 {
font-size: 32px;
margin-bottom: 40px;
}
.content {
font-size: 18px;
line-height: 1.8;
margin-bottom: 40px;
}
.signature {
margin-top: 60px;
font-size: 16px;
}
.footer {
margin-top: 40px;
font-size: 14px;
color: #666;
}
</style>
</head>
<body>
<div class="certificate">
<h1>์๋ฃ์ฆ</h1>
<h2>Certificate of Completion</h2>
<div class="content">
<p style="font-size: 24px; margin-bottom: 10px;"><strong>{{ $name }}</strong> ๋</p>
<p>์ ๋ถ์ ์๋์ ์ํฌ์์ ์ฑ์คํ ์ด์ํ์๊ธฐ์ ์ด ์ฆ์๋ฅผ ์์ฌํฉ๋๋ค.</p>
<br>
<p><strong>์ํฌ์๋ช
:</strong> {{ $workshop_title }}</p>
<p><strong>๊ฐ์ฌ:</strong> {{ $instructor }}</p>
<p><strong>๊ต์ก ๊ธฐ๊ฐ:</strong> {{ $start_date }} ~ {{ $end_date }}</p>
<p><strong>์ถ์๋ฅ :</strong> {{ $attendance_rate }}%</p>
</div>
<div class="signature">
<p>{{ $issue_date }}</p>
<p>์ํฌ์ ์ด์์ง ์ผ๋</p>
</div>
<div class="footer">
<p>๋ณธ ์๋ฃ์ฆ์ ์จ๋ผ์ธ ์ํฌ์ ํ๋ซํผ์์ ์๋ ๋ฐ๊ธ๋์์ต๋๋ค.</p>
</div>
</div>
</body>
</html>
```
๋์ ๋ฑ๋ก ๋ด์ญ ํ์ด์ง(MyEnrollments.jsx)์์ ์๋ฃ์ฆ ๋ฐ๊ธ ๋งํฌ๊ฐ ์ ๋๋ก ์๋ํ๋์ง ํ์ธํ์ธ์:
```jsx
<Link
href={`/certificates/${enrollment.id}`}
className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 transition inline-block"
>
์๋ฃ์ฆ ๋ฐ๊ธ๋ฐ๊ธฐ
</Link>
```
๋ธ๋ผ์ฐ์ ์์ ์ถ์๋ฅ 80% ์ด์์ธ ์ํฌ์์ ์๋ฃ์ฆ ๋ฐ๊ธ ๋ฒํผ์ ํด๋ฆญํ๋ฉด PDF ํ์ผ์ด ๋ค์ด๋ก๋๋๋์ง ํ์ธํ์ธ์.
๋ง์ฝ PDF ์์ฑ ์ ํ๊ธ ํฐํธ๊ฐ ๊นจ์ง๋ค๋ฉด, resources/views/certificates/template.blade.php์ font-family๋ฅผ 'DejaVu Sans'๋ก ์ค์ ํ์ธ์ (DomPDF๊ฐ ๊ธฐ๋ณธ ์ง์ํ๋ ํฐํธ).
์ต์ข
ํ
์คํธ:
1. ์ํฌ์ ๋ชฉ๋ก ํ์ด์ง์์ ์นดํ
๊ณ ๋ฆฌ ํํฐ๊ฐ ์๋ํ๋์ง ํ์ธ
2. ์ํฌ์ ์์ธ ํ์ด์ง์์ ๋ฑ๋ก ํผ์ด ์ ์ ์๋ํ๋์ง ํ์ธ
3. ๋์ ๋ฑ๋ก ๋ด์ญ ํ์ด์ง์์ ์ถ์๋ฅ ์ด ์ ํํ ํ์๋๋์ง ํ์ธ
4. ์ถ์ ์ฒดํฌ ํ์ด์ง์์ ์ถ์ ๊ธฐ๋ก์ด ์ ์ ์๋ํ๋์ง ํ์ธ
5. ์ถ์๋ฅ 80% ์ด์ ์ ์๋ฃ์ฆ PDF๊ฐ ์ ์ ๋ค์ด๋ก๋๋๋์ง ํ์ธ
๊ธฐ๋ ๊ฒฐ๊ณผ: ์๋ฃ์ฆ ์๋ ๋ฐ๊ธ ๊ธฐ๋ฅ์ด ์์ฑ๋ฉ๋๋ค. ์ถ์๋ฅ 80% ์ด์์ธ ์ฐธ๊ฐ์๋ ์๋ฃ์ฆ PDF๋ฅผ ๋ค์ด๋ก๋ํ ์ ์์ผ๋ฉฐ, ์๋ฃ์ฆ์๋ ์ฐธ๊ฐ์ ์ด๋ฆ, ์ํฌ์๋ช , ๊ฐ์ฌ, ๊ต์ก ๊ธฐ๊ฐ, ์ถ์๋ฅ ์ด ํฌํจ๋ฉ๋๋ค. ๋ชจ๋ ๊ธฐ๋ฅ์ด ์ ์ ์๋ํ๋ ์์ฑ๋ ์ํฌ์ ๋ฑ๋ก & ์๋ฃ์ฆ ๋ฐ๊ธ ํ๋ซํผ์ด ์์ฑ๋ฉ๋๋ค.
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!