๐จ ํ๋ฆฌ๋์ ๊ฒฌ์ ์ ์๋ ์์ฑ๊ธฐ - ๊ณ ๊ฐ ๋ง์ถค PDF ๊ฒฌ์ ์ ๋ฐ์ก
freelancer-quote-pdf-generator
๋ฌธ์ ์ค๋ช
ํ๋ก์ ํธ ๊ฐ์
ํ๋ฆฌ๋์ ๋์์ด๋, ๊ฐ๋ฐ์, ์๊ฐ ๋ฑ์ด ๊ณ ๊ฐ์๊ฒ ์ ๋ฌธ์ ์ธ ๊ฒฌ์ ์๋ฅผ ๋น ๋ฅด๊ฒ ์์ฑํ๊ณ PDF๋ก ๋ค์ด๋ก๋ํ ์ ์๋ ํ์คํ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋๋ค. Laravel API ๋ฐฑ์๋์ React ํ๋ก ํธ์๋๋ก ๊ตฌ์ฑ๋์ด ์์ผ๋ฉฐ, ๊ฒฌ์ ํญ๋ชฉ์ ์ถ๊ฐํ๋ฉด ์ค์๊ฐ์ผ๋ก ์ด์ก์ด ๊ณ์ฐ๋๊ณ ๊น๋ํ PDF ๊ฒฌ์ ์๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.
ํ์ ๊ธฐ๋ฅ
- โ ๊ฒฌ์ ์ ๊ธฐ๋ณธ ์ ๋ณด ์ ๋ ฅ (๊ณ ๊ฐ๋ช , ํ๋ก์ ํธ๋ช , ๋ฐํ์ผ์)
- โ ๊ฒฌ์ ํญ๋ชฉ ์ถ๊ฐ/์ญ์ (์๋น์ค๋ช , ๋จ๊ฐ, ์๋)
- โ ์ค์๊ฐ ์ด์ก ๊ณ์ฐ (ํญ๋ชฉ๋ณ ์๊ณ + ์ ์ฒด ํฉ๊ณ)
- โ ์์ฑ๋ ๊ฒฌ์ ์ ๋ชฉ๋ก ์กฐํ (์นด๋ ๋ ์ด์์)
- โ PDF ๋ค์ด๋ก๋ ๋ฒํผ (๋ธ๋ผ์ฐ์ ์ธ์ ๊ธฐ๋ฅ ํ์ฉ)
- โ ๋ฐ์ํ ๋์์ธ (๋ชจ๋ฐ์ผ ์ต์ ํ)
๋ณด๋์ค ๊ธฐ๋ฅ
- ๐ ๊ฒฌ์ ์ ํ ํ๋ฆฟ ์์ ํ ๋ง ์ ํ (๋ธ๋ฃจ/๊ทธ๋ฆฐ/ํผํ)
- ๐ ๋ถ๊ฐ์ธ(VAT) ์๋ ๊ณ์ฐ ์ต์
- ๐ ๊ฒฌ์ ์ ํจ๊ธฐ๊ฐ ์ค์
- ๐ ํ์ฌ ๋ก๊ณ ์ด๋ฏธ์ง ์ ๋ก๋
- ๐ ๊ฒฌ์ ์ ์ํ ๊ด๋ฆฌ (์์ฑ์ค/๋ฐ์ก์๋ฃ/์น์ธ)
๊ธฐ์ ์คํ
- ๋ฐฑ์๋: Laravel 13.x (RESTful API, SQLite)
- ํ๋ก ํธ์๋: React 19 (Vite, Tailwind CSS)
- ๋ฐ์ดํฐ ์ฐ๋: Axios
- PDF ์์ฑ: ๋ธ๋ผ์ฐ์ window.print() API
์ ์ฝ ์กฐ๊ฑด
- โข Laravel 13.x ๋ฐฑ์๋ API์ React 19 ํ๋ก ํธ์๋๋ฅผ ๋ณ๋ ํด๋๋ก ๋ถ๋ฆฌ
- โข Laravel์ RESTful API ์๋ฒ ์ญํ (JSON ์๋ต)
- โข React๋ Vite๋ก ๋ณ๋ ํ๋ก์ ํธ ์์ฑ
- โข ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ SQLite ์ฌ์ฉ (๊ฐํธํ ๋ก์ปฌ ๊ฐ๋ฐ)
- โข Tailwind CSS๋ก ๋ชจ๋ํ UI ๊ตฌํ
- โข PDF ์์ฑ์ ๋ธ๋ผ์ฐ์ ๋ด์ฅ ์ธ์ ๊ธฐ๋ฅ ํ์ฉ (๋ณต์กํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ถํ์)
- โข CORS ์ค์ ์ผ๋ก ํ๋ก ํธ์๋-๋ฐฑ์๋ ํต์ ํ์ฉ
- โข ๊ฒฌ์ ํญ๋ชฉ์ ๋์ ์ผ๋ก ์ถ๊ฐ/์ญ์ ๊ฐ๋ฅ
- โข ์ด์ก์ ํด๋ผ์ด์ธํธ ์ฌ์ด๋์์ ์ค์๊ฐ ๊ณ์ฐ
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
mkdir freelancer-quote-pdf-generator && cd freelancer-quote-pdf-generator
์ด ํด๋์์ ํ๋ก์ ํธ๋ฅผ ์์ํฉ๋๋ค.
# Laravel 13.x API ๋ฐฑ์๋ ์์ฑ
1. `backend` ํด๋์ Laravel ํ๋ก์ ํธ๋ฅผ ์์ฑํ์ธ์:
```bash
composer create-project laravel/laravel:^13.0 backend
cd backend
```
2. `.env` ํ์ผ์ ์์ ํ์ฌ SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ค์ ํ์ธ์:
```
DB_CONNECTION=sqlite
# DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD ์ฃผ์ ์ฒ๋ฆฌ
```
3. `database/database.sqlite` ๋น ํ์ผ์ ์์ฑํ์ธ์:
```bash
touch database/database.sqlite
```
4. ๊ฒฌ์ ์(Quote) ๋ชจ๋ธ๊ณผ ๋ง์ด๊ทธ๋ ์ด์
์ ์์ฑํ์ธ์:
```bash
php artisan make:model Quote -m
```
5. `database/migrations/xxxx_create_quotes_table.php` ๋ง์ด๊ทธ๋ ์ด์
ํ์ผ์ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ์ธ์:
- ํ
์ด๋ธ๋ช
: `quotes`
- ์ปฌ๋ผ:
- `id` (bigIncrements)
- `customer_name` (string) - ๊ณ ๊ฐ๋ช
- `project_name` (string) - ํ๋ก์ ํธ๋ช
- `issue_date` (date) - ๋ฐํ์ผ์
- `items` (json) - ๊ฒฌ์ ํญ๋ชฉ ๋ฐฐ์ด (์๋น์ค๋ช
, ๋จ๊ฐ, ์๋ ํฌํจ)
- `total_amount` (decimal, 10, 2) - ์ด์ก
- `timestamps`
6. ๋ง์ด๊ทธ๋ ์ด์
์ ์คํํ์ธ์:
```bash
php artisan migrate
```
7. `app/Models/Quote.php` ๋ชจ๋ธ์ `$fillable` ์์ฑ์ ์ถ๊ฐํ์ธ์:
```php
protected $fillable = ['customer_name', 'project_name', 'issue_date', 'items', 'total_amount'];
protected $casts = ['items' => 'array', 'issue_date' => 'date'];
```
8. API ์ปจํธ๋กค๋ฌ๋ฅผ ์์ฑํ์ธ์:
```bash
php artisan make:controller Api/QuoteController --api
```
9. `app/Http/Controllers/Api/QuoteController.php`์ ๋ค์ ๋ฉ์๋๋ฅผ ๊ตฌํํ์ธ์:
- `index()`: ๋ชจ๋ ๊ฒฌ์ ์ ๋ชฉ๋ก ๋ฐํ (์ต์ ์ ์ ๋ ฌ)
- `store(Request $request)`: ์ ๊ฒฌ์ ์ ์์ฑ
- ์ ํจ์ฑ ๊ฒ์ฌ: customer_name, project_name, issue_date, items (๋ฐฐ์ด), total_amount ํ์
- `show($id)`: ํน์ ๊ฒฌ์ ์ ์์ธ ์กฐํ
10. `routes/api.php`์ API ๋ผ์ฐํธ๋ฅผ ๋ฑ๋กํ์ธ์:
```php
use App\Http\Controllers\Api\QuoteController;
Route::apiResource('quotes', QuoteController::class)->only(['index', 'store', 'show']);
```
11. CORS ์ค์ ์ ์ํด `config/cors.php`๋ฅผ ์์ ํ์ธ์:
- `'paths' => ['api/*']`
- `'allowed_origins' => ['http://localhost:5173']` (React Vite ๊ธฐ๋ณธ ํฌํธ)
- `'allowed_methods' => ['*']`
12. Laravel ๊ฐ๋ฐ ์๋ฒ๋ฅผ ์คํํ์ธ์:
```bash
php artisan serve
```
โ http://localhost:8000 ์์ API ์๋ฒ ์คํ ํ์ธ
๊ธฐ๋ ๊ฒฐ๊ณผ: Laravel ๋ฐฑ์๋ API ์๋ฒ๊ฐ ๊ตฌ์ถ๋๊ณ , Quote ๋ชจ๋ธ๊ณผ ๋ง์ด๊ทธ๋ ์ด์ ์ด ์์ฑ๋๋ฉฐ, API ์๋ํฌ์ธํธ(GET /api/quotes, POST /api/quotes, GET /api/quotes/{id})๊ฐ ์ค๋น๋ฉ๋๋ค. SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค์ quotes ํ ์ด๋ธ์ด ์์ฑ๋๊ณ , CORS ์ค์ ์ด ์๋ฃ๋์ด ํ๋ก ํธ์๋์ ํต์ ํ ์ค๋น๊ฐ ๋ฉ๋๋ค.
# React 19 ํ๋ก ํธ์๋ ์์ฑ
ํ๋ก์ ํธ ๋ฃจํธ(`freelancer-quote-pdf-generator`)๋ก ๋์์์ React ํ๋ก ํธ์๋๋ฅผ ์ค์ ํ์ธ์.
1. `frontend` ํด๋์ React + Vite ํ๋ก์ ํธ๋ฅผ ์์ฑํ์ธ์:
```bash
npm create vite@latest frontend -- --template react
cd frontend
npm install
```
2. Tailwind CSS๋ฅผ ์ค์นํ๊ณ ์ค์ ํ์ธ์:
```bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```
3. `tailwind.config.js`๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ์์ ํ์ธ์:
```js
export default {
content: ['./index.html', './src/**/*.{js,jsx}'],
theme: { extend: {} },
plugins: [],
}
```
4. `src/index.css`์ Tailwind ์ง์์ด๋ฅผ ์ถ๊ฐํ์ธ์:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
5. Axios๋ฅผ ์ค์นํ์ธ์:
```bash
npm install axios
```
6. `src/App.jsx`๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ์ธ์:
- ์ํ ๊ด๋ฆฌ:
- `quotes` (๋ฐฐ์ด): ์์ฑ๋ ๊ฒฌ์ ์ ๋ชฉ๋ก
- `formData` (๊ฐ์ฒด): ์ ๊ฒฌ์ ์ ์
๋ ฅ ํผ ๋ฐ์ดํฐ (customer_name, project_name, issue_date)
- `items` (๋ฐฐ์ด): ๊ฒฌ์ ํญ๋ชฉ ๋ชฉ๋ก (๊ฐ ํญ๋ชฉ: {service, price, quantity})
- ํจ์:
- `fetchQuotes()`: GET /api/quotes ํธ์ถํ์ฌ ๊ฒฌ์ ์ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
- `addItem()`: ์ ๊ฒฌ์ ํญ๋ชฉ ์ถ๊ฐ
- `removeItem(index)`: ํน์ ํญ๋ชฉ ์ญ์
- `calculateTotal()`: ํญ๋ชฉ๋ณ ์๊ณ ๋ฐ ์ ์ฒด ํฉ๊ณ ๊ณ์ฐ
- `handleSubmit()`: POST /api/quotes๋ก ๊ฒฌ์ ์ ์์ฑ ํ ๋ชฉ๋ก ์๋ก๊ณ ์นจ
- UI ๊ตฌ์ฑ:
- ์๋จ: ์ ๋ชฉ "ํ๋ฆฌ๋์ ๊ฒฌ์ ์ ์์ฑ๊ธฐ" (Tailwind๋ก ์คํ์ผ๋ง)
- ์ค์: ๊ฒฌ์ ์ ์์ฑ ํผ (๊ณ ๊ฐ๋ช
, ํ๋ก์ ํธ๋ช
, ๋ฐํ์ผ์ ์
๋ ฅ)
- ๊ฒฌ์ ํญ๋ชฉ ์ถ๊ฐ ์น์
(์๋น์ค๋ช
, ๋จ๊ฐ, ์๋ ์
๋ ฅ + ์ถ๊ฐ ๋ฒํผ)
- ํญ๋ชฉ ๋ชฉ๋ก ํ
์ด๋ธ (์๋น์ค๋ช
, ๋จ๊ฐ, ์๋, ์๊ณ, ์ญ์ ๋ฒํผ)
- ์ด์ก ํ์ (์ค์๊ฐ ๊ณ์ฐ)
- "๊ฒฌ์ ์ ์์ฑ" ๋ฒํผ
- ํ๋จ: ์์ฑ๋ ๊ฒฌ์ ์ ๋ชฉ๋ก (์นด๋ ๋ ์ด์์, ๊ณ ๊ฐ๋ช
/ํ๋ก์ ํธ๋ช
/์ด์ก ํ์)
7. Axios ๊ธฐ๋ณธ URL ์ค์ :
```js
import axios from 'axios';
axios.defaults.baseURL = 'http://localhost:8000/api';
```
8. React ๊ฐ๋ฐ ์๋ฒ๋ฅผ ์คํํ์ธ์:
```bash
npm run dev
```
โ http://localhost:5173 ์์ ํ๋ก ํธ์๋ ์คํ ํ์ธ
๊ธฐ๋ ๊ฒฐ๊ณผ: React ํ๋ก ํธ์๋๊ฐ ๊ตฌ์ถ๋๊ณ , Tailwind CSS๋ก ์คํ์ผ๋ง๋ ๊ฒฌ์ ์ ์์ฑ ํผ๊ณผ ํญ๋ชฉ ์ถ๊ฐ/์ญ์ ๊ธฐ๋ฅ์ด ๋์ํฉ๋๋ค. Axios๋ก Laravel API์ ํต์ ํ์ฌ ๊ฒฌ์ ์๋ฅผ ์์ฑํ๊ณ ๋ชฉ๋ก์ ์กฐํํ ์ ์์ต๋๋ค. ์ด์ก์ด ์ค์๊ฐ์ผ๋ก ๊ณ์ฐ๋์ด ํ๋ฉด์ ํ์๋ฉ๋๋ค.
# PDF ๋ค์ด๋ก๋ ๊ธฐ๋ฅ ์ถ๊ฐ ๋ฐ UI ๊ฐ์
1. `src/components/QuoteDetail.jsx` ์ปดํฌ๋ํธ๋ฅผ ์์ฑํ์ธ์:
- Props: `quote` (๊ฐ์ฒด)
- UI:
- ๊ฒฌ์ ์ ํค๋ (๊ณ ๊ฐ๋ช
, ํ๋ก์ ํธ๋ช
, ๋ฐํ์ผ์)
- ํญ๋ชฉ ํ
์ด๋ธ (์๋น์ค๋ช
, ๋จ๊ฐ, ์๋, ์๊ณ)
- ์ด์ก (๊ฐ์กฐ ํ์)
- "PDF ๋ค์ด๋ก๋" ๋ฒํผ (ํด๋ฆญ ์ `window.print()` ํธ์ถ)
- ์ธ์์ฉ CSS:
- `@media print` ์ฟผ๋ฆฌ๋ก ๋ฒํผ ์จ๊น ์ฒ๋ฆฌ
- ํ์ด์ง ์ฌ๋ฐฑ ์ต์ ํ
2. `src/App.jsx`๋ฅผ ์์ ํ์ธ์:
- ๊ฒฌ์ ์ ๋ชฉ๋ก์์ ํน์ ๊ฒฌ์ ํด๋ฆญ ์ `QuoteDetail` ์ปดํฌ๋ํธ๋ฅผ ๋ชจ๋ฌ๋ก ํ์
- ๋ชจ๋ฌ ๋ฐฐ๊ฒฝ ํด๋ฆญ ์ ๋ซ๊ธฐ
- ์ํ ์ถ๊ฐ: `selectedQuote` (์ ํ๋ ๊ฒฌ์ ์)
3. UI ๊ฐ์ :
- ๊ฒฌ์ ์ ์์ฑ ํผ์ ์นด๋ ํํ๋ก ๊ฐ์ธ๊ธฐ (shadow, rounded, padding)
- ํญ๋ชฉ ์ถ๊ฐ ๋ฒํผ์ ์์ด์ฝ ์ถ๊ฐ (+ ๊ธฐํธ)
- ์ด์ก์ ํฐ ํฐํธ์ ์์์ผ๋ก ๊ฐ์กฐ
- ๊ฒฌ์ ์ ๋ชฉ๋ก์ ๊ทธ๋ฆฌ๋ ๋ ์ด์์์ผ๋ก ๋ณ๊ฒฝ (3์ด)
- ๊ฐ ๊ฒฌ์ ์ ์นด๋์ hover ํจ๊ณผ ์ถ๊ฐ
4. ํผ ์ ํจ์ฑ ๊ฒ์ฌ ์ถ๊ฐ:
- ๊ณ ๊ฐ๋ช
, ํ๋ก์ ํธ๋ช
, ๋ฐํ์ผ์ ํ์ ์
๋ ฅ
- ์ต์ 1๊ฐ ์ด์์ ๊ฒฌ์ ํญ๋ชฉ ํ์
- ๋จ๊ฐ์ ์๋์ ์์๋ง ํ์ฉ
- ์
๋ ฅ ์ค๋ฅ ์ ์๋ฆผ ๋ฉ์์ง ํ์
5. ํ
์คํธ:
- ๊ฒฌ์ ์๋ฅผ ์์ฑํ๊ณ ๋ชฉ๋ก์์ ํด๋ฆญํ์ฌ ์์ธ ๋ณด๊ธฐ
- PDF ๋ค์ด๋ก๋ ๋ฒํผ ํด๋ฆญ ์ ๋ธ๋ผ์ฐ์ ์ธ์ ๋ํ์์ ํ์ธ
- ์ธ์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์์ ๋ฒํผ์ด ์จ๊ฒจ์ง๋์ง ํ์ธ
๊ธฐ๋ ๊ฒฐ๊ณผ: ๊ฒฌ์ ์ ์์ธ ๋ณด๊ธฐ ๋ชจ๋ฌ์ด ์ถ๊ฐ๋๊ณ , PDF ๋ค์ด๋ก๋ ๊ธฐ๋ฅ์ด ๋ธ๋ผ์ฐ์ ์ธ์ API๋ก ๊ตฌํ๋ฉ๋๋ค. UI๊ฐ Tailwind CSS๋ก ์ธ๋ จ๋๊ฒ ๊ฐ์ ๋๋ฉฐ, ํผ ์ ํจ์ฑ ๊ฒ์ฌ๋ก ์ฌ์ฉ์ ๊ฒฝํ์ด ํฅ์๋ฉ๋๋ค. ์ธ์ ์ ๊น๋ํ ๊ฒฌ์ ์ ๋ ์ด์์์ด ์ถ๋ ฅ๋ฉ๋๋ค.
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!