์ ์๋ขฐ ๋ด์ฉ์ ๋ฐํ์ผ๋ก ๋ฐ์ด๋ธ์ฝ๋ฉ ๋ ์ํผ๋ฅผ ๋ง๋ค์ด ๋ดค์ต๋๋คย
ย
## ํ๋ก์ ํธ ๊ฐ์
๋ฐ๋ ค๊ฒฌ ๋ณดํธ์๋ค์ ์ํ ์ปค๋ฎค๋ํฐ ์น ์ ํ๋ฆฌ์ผ์ด์
์
๋๋ค. ๊ฐ์ธ ์ ๊ฒฌ์ผ์ง๋ฅผ ๊ธฐ๋กํ๊ณ ์ ํ์ ์ผ๋ก ๊ณต๊ฐํ ์ ์์ผ๋ฉฐ, ์ปค๋ฎค๋ํฐ์์ ์์ก ๋
ธํ์ฐ๋ฅผ ๊ณต์ ํ๊ณ ๋๊ธ๋ก ์ํตํ ์ ์์ต๋๋ค. ๊ด๋ฆฌ์์ ๊ณต์ง์ฌํญ์ ์น ํธ์๋ก ํ์์๊ฒ ์ ๋ฌ๋ฉ๋๋ค.
## ํ์ ๊ธฐ๋ฅ
- ํ์๊ฐ์
/๋ก๊ทธ์ธ (์ด๋ฉ์ผ ๊ธฐ๋ฐ ์ธ์ฆ)
- ์ ๊ฒฌ์ผ์ง ์์ฑ (๋น๊ณต๊ฐ ๊ธฐ๋ณธ, ์ ํ์ ๊ณต๊ฐ)
- ์ ๊ฒฌ์ผ์ง ๊ณต๊ฐ์ฉ ๊ฐค๋ฌ๋ฆฌ (๊ณต๊ฐ๋ ์ผ์ง๋ง ๋
ธ์ถ)
- ์ปค๋ฎค๋ํฐ ๊ฒ์ํ (์นดํ
๊ณ ๋ฆฌ๋ณ ๊ธ ์์ฑ, ๋๊ธ)
- ๊ด๋ฆฌ์ ์นดํ
๊ณ ๋ฆฌ ๊ด๋ฆฌ (์ถ๊ฐ/์์ /์ญ์ )
- ๊ณต์ง์ฌํญ ์์ฑ ๋ฐ ์น ํธ์ ์๋ฆผ ๋ฐ์ก
- ๋ฐ์ํ ๋์์ธ (๋ชจ๋ฐ์ผ ์ฑ ์ ํ ๋๋น)
## ๋ณด๋์ค ๊ธฐ๋ฅ
- ์ ๊ฒฌ ํ๋กํ ๋ฑ๋ก (์ด๋ฆ, ๊ฒฌ์ข
, ๋์ด, ์ฌ์ง)
- ์ ๊ฒฌ์ผ์ง ์ฌ์ง ๋ค์ค ์
๋ก๋
- ๊ฒ์๊ธ/๋๊ธ ์ข์์ ๊ธฐ๋ฅ
- ์ธ๊ธฐ ๊ฒ์๊ธ ๋ญํน
- ๋คํฌ ๋ชจ๋ ์ง์
## ๊ธฐ์ ์คํ
- Laravel 13.x (๋ฐฑ์๋)
- Blade + Alpine.js (ํ๋ก ํธ์๋)
- Tailwind CSS (์คํ์ผ๋ง)
- SQLite (๋ฐ์ดํฐ๋ฒ ์ด์ค)
- Laravel Web Push (laravel-notification-channels/webpush)
## ์ ์ฝ ์กฐ๊ฑด
- Laravel 13.x + Blade ํ
ํ๋ฆฟ ์์ง ๊ธฐ๋ฐ MPA ๊ตฌ์กฐ
- Laravel Breeze Blade starter kit์ผ๋ก ํ๋ก์ ํธ ์์ฑ
- SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฌ์ฉ (๊ฐํธํ ๋ก์ปฌ ๊ฐ๋ฐ)
- Tailwind CSS๋ก ๋ชจ๋ ์คํ์ผ๋ง ์ฒ๋ฆฌ
- Alpine.js๋ก ํด๋ผ์ด์ธํธ ์ธก ์ธํฐ๋์
์ฒ๋ฆฌ (๋ชจ๋ฌ, ํ ๊ธ ๋ฑ)
- Laravel Eloquent ORM๊ณผ ๋ง์ด๊ทธ๋ ์ด์
ํ์ฉ
- ์ด๋ฏธ์ง ์
๋ก๋๋ Laravel Storage (public ๋์คํฌ) ์ฌ์ฉ
- ํผ ๊ฒ์ฆ์ Laravel Form Request Validation ์ฌ์ฉ
- ์น ํธ์๋ laravel-notification-channels/webpush ํจํค์ง ์ฌ์ฉ
- ํฅํ ๋ชจ๋ฐ์ผ ์ฑ ์ ํ์ ๊ณ ๋ คํ์ฌ ์ปจํธ๋กค๋ฌ ๋ก์ง๊ณผ ๋ทฐ๋ฅผ ๋ถ๋ฆฌํ๊ณ , ํ์ ์ API ๋ผ์ฐํธ ์ถ๊ฐ ๊ฐ๋ฅํ๋๋ก ์ค๊ณ
---
## ํ๋กฌํํธ ๋ ์ํผ - Step๋ณ ๊ฐ์ด๋
---
### Step 1: Laravel 13.x + Breeze Blade ํ๋ก์ ํธ ์์ฑ
**ํ๋กฌํํธ:**
mkdir dog-community && cd dog-community
์ด ํด๋์์ Laravel 13.x + Blade ํ๋ก์ ํธ๋ฅผ ์์ํฉ๋๋ค.
๋ค์ ๋ช ๋ น์ผ๋ก Laravel Breeze Blade starter kit ํ๋ก์ ํธ๋ฅผ ์์ฑํ์ธ์:
composer create-project laravel/laravel . composer require laravel/breeze --dev php artisan breeze:install blade php artisan migrate npm install npm run build
ํ๋ก์ ํธ ๊ตฌ์กฐ๋ฅผ ํ์ธํ๊ณ , .env ํ์ผ์์ DB_CONNECTION=sqlite๋ก ์ค์ ํ ํ database/database.sqlite ํ์ผ์ ์์ฑํ์ธ์.
Alpine.js๋ Breeze Blade์ ๊ธฐ๋ณธ ํฌํจ๋์ด ์์ผ๋ฏ๋ก ๋ณ๋ ์ค์น๊ฐ ํ์ ์์ต๋๋ค.
๊ธฐ๋ณธ ์ธ์ฆ ๊ธฐ๋ฅ(๋ก๊ทธ์ธ/ํ์๊ฐ์ )์ด ํฌํจ๋ Breeze ์คํํฐ ํท์ด ์ค์น๋์๋์ง ํ์ธํ๊ณ , npm run dev๋ก ํ๋ก ํธ์๋ ๋น๋๊ฐ ์ ์ ์๋ํ๋์ง ํ ์คํธํ์ธ์.
APP_NAME์ "๋ฉ๋ฉ์ปค๋ฎค๋ํฐ"๋ก ์ค์ ํ์ธ์.
**๊ธฐ๋ ๊ฒฐ๊ณผ:** "Laravel 13.x + Breeze Blade ํ๋ก์ ํธ๊ฐ ์์ฑ๋๊ณ , SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ์ค์ ๋๋ฉฐ, ๊ธฐ๋ณธ ์ธ์ฆ ํ์ด์ง(๋ก๊ทธ์ธ/ํ์๊ฐ์
)๊ฐ ์๋ํฉ๋๋ค."
---
### Step 2: ํต์ฌ ๋ชจ๋ธ ๋ฐ ๋ง์ด๊ทธ๋ ์ด์
์์ฑ
**ํ๋กฌํํธ:**
๋ค์ ๋ชจ๋ธ๋ค์ ์์ฑํ๊ณ ๋ง์ด๊ทธ๋ ์ด์ ์ ์์ฑํ์ธ์.
โ Pet (์ ๊ฒฌ ํ๋กํ) ๋ชจ๋ธ:
- id (primary key)
- user_id (foreign key, users ํ ์ด๋ธ ์ฐธ์กฐ)
- name (string, ์ ๊ฒฌ ์ด๋ฆ)
- breed (string, nullable, ๊ฒฌ์ข )
- birth_date (date, nullable, ์๋ ์์ผ)
- photo (string, nullable, ํ๋กํ ์ฌ์ง ๊ฒฝ๋ก)
- timestamps
โ Diary (์ ๊ฒฌ์ผ์ง) ๋ชจ๋ธ:
- id (primary key)
- user_id (foreign key, users ํ ์ด๋ธ ์ฐธ์กฐ)
- pet_id (foreign key, nullable, pets ํ ์ด๋ธ ์ฐธ์กฐ)
- title (string, ์ ๋ชฉ)
- content (text, ๋ด์ฉ)
- is_public (boolean, default: false, ๊ณต๊ฐ ์ฌ๋ถ)
- published_at (timestamp, nullable, ๊ณต๊ฐ ์์ )
- timestamps
โ DiaryImage (์ ๊ฒฌ์ผ์ง ์ด๋ฏธ์ง) ๋ชจ๋ธ:
- id (primary key)
- diary_id (foreign key, diaries ํ ์ด๋ธ ์ฐธ์กฐ)
- image_path (string, ์ด๋ฏธ์ง ๊ฒฝ๋ก)
- sort_order (integer, default: 0)
- timestamps
โ Category (์ปค๋ฎค๋ํฐ ์นดํ ๊ณ ๋ฆฌ) ๋ชจ๋ธ:
- id (primary key)
- name (string, ์นดํ ๊ณ ๋ฆฌ๋ช )
- slug (string, unique, URL์ฉ ์ฌ๋ฌ๊ทธ)
- sort_order (integer, default: 0)
- is_active (boolean, default: true)
- timestamps
โ Post (์ปค๋ฎค๋ํฐ ๊ฒ์๊ธ) ๋ชจ๋ธ:
- id (primary key)
- user_id (foreign key, users ํ ์ด๋ธ ์ฐธ์กฐ)
- category_id (foreign key, categories ํ ์ด๋ธ ์ฐธ์กฐ)
- title (string, ์ ๋ชฉ)
- content (text, ๋ด์ฉ)
- view_count (integer, default: 0)
- timestamps
โ Comment (๋๊ธ) ๋ชจ๋ธ:
- id (primary key)
- user_id (foreign key, users ํ ์ด๋ธ ์ฐธ์กฐ)
- post_id (foreign key, posts ํ ์ด๋ธ ์ฐธ์กฐ)
- content (text, ๋๊ธ ๋ด์ฉ)
- timestamps
โ Notice (๊ณต์ง์ฌํญ) ๋ชจ๋ธ:
- id (primary key)
- user_id (foreign key, users ํ ์ด๋ธ ์ฐธ์กฐ, ์์ฑ ๊ด๋ฆฌ์)
- title (string, ์ ๋ชฉ)
- content (text, ๋ด์ฉ)
- is_pushed (boolean, default: false, ํธ์ ๋ฐ์ก ์ฌ๋ถ)
- timestamps
๋ชจ๋ธ ๊ฐ ๊ด๊ณ ์ค์ :
- User hasMany Pet, Diary, Post, Comment
- Pet belongsTo User, hasMany Diary
- Diary belongsTo User, belongsTo Pet, hasMany DiaryImage
- DiaryImage belongsTo Diary
- Category hasMany Post
- Post belongsTo User, belongsTo Category, hasMany Comment
- Comment belongsTo User, belongsTo Post
users ํ ์ด๋ธ์ is_admin (boolean, default: false) ์ปฌ๋ผ์ ์ถ๊ฐํ๋ ๋ง์ด๊ทธ๋ ์ด์ ๋ ์์ฑํ์ธ์.
๋ง์ด๊ทธ๋ ์ด์ ์ ์คํํ๊ณ , ํ ์คํธ์ฉ ์๋๋ฅผ ์์ฑํ์ธ์:
- ์ผ๋ฐ ์ ์ 3๋ช , ๊ด๋ฆฌ์ 1๋ช
- ๊ฐ ์ ์ ์ ์ ๊ฒฌ 1~2๋ง๋ฆฌ
- ์ ๊ฒฌ์ผ์ง ๊ฐ 5๊ฐ (์ผ๋ถ๋ ๊ณต๊ฐ)
- ์นดํ ๊ณ ๋ฆฌ 4๊ฐ (๊ฑด๊ฐ/์์, ํ๋ จ/๊ต์ก, ์ฉํ ํ๊ธฐ, ์์ ๊ฒ์ํ)
- ๊ฒ์๊ธ ์นดํ ๊ณ ๋ฆฌ๋ณ 3๊ฐ์ฉ, ๋๊ธ ๊ฐ 2๊ฐ
- ๊ณต์ง์ฌํญ 2๊ฐ
php artisan make:model Pet -m php artisan make:model Diary -m php artisan make:model DiaryImage -m php artisan make:model Category -m php artisan make:model Post -m php artisan make:model Comment -m php artisan make:model Notice -m php artisan migrate:fresh --seed
**๊ธฐ๋ ๊ฒฐ๊ณผ:** "๋ชจ๋ ํ
์ด๋ธ์ด ์์ฑ๋๊ณ , ํ
์คํธ ๋ฐ์ดํฐ(์ ์ 4๋ช
, ์ ๊ฒฌ, ์ผ์ง, ์นดํ
๊ณ ๋ฆฌ 4๊ฐ, ๊ฒ์๊ธ, ๋๊ธ, ๊ณต์ง์ฌํญ)๊ฐ ์๋ฉ๋ฉ๋๋ค."
---
### Step 3: ์ ๊ฒฌ ํ๋กํ ๋ฐ ์ ๊ฒฌ์ผ์ง ๊ธฐ๋ฅ
**ํ๋กฌํํธ:**
์ ๊ฒฌ ํ๋กํ ๊ด๋ฆฌ์ ์ ๊ฒฌ์ผ์ง CRUD ๊ธฐ๋ฅ์ ๊ตฌํํ์ธ์.
โ Laravel ์ปจํธ๋กค๋ฌ (PetController):
- index(): ๋ก๊ทธ์ธ ์ ์ ์ ์ ๊ฒฌ ๋ชฉ๋ก ์กฐํ
- store(): ์ ๊ฒฌ ๋ฑ๋ก (์ด๋ฆ, ๊ฒฌ์ข , ์๋ ์์ผ, ์ฌ์ง ์ ๋ก๋)
- update(): ์ ๊ฒฌ ์ ๋ณด ์์
- destroy(): ์ ๊ฒฌ ์ญ์
โ Laravel ์ปจํธ๋กค๋ฌ (DiaryController):
- index(): ๋ก๊ทธ์ธ ์ ์ ์ ๋น๊ณต๊ฐ ์ ๊ฒฌ์ผ์ง ๋ชฉ๋ก (is_public=false ํฌํจ ์ ์ฒด)
- create(): ์ผ์ง ์์ฑ ํผ
- store(): ์ผ์ง ์ ์ฅ (์ ๋ชฉ, ๋ด์ฉ, ์ ๊ฒฌ ์ ํ, ์ด๋ฏธ์ง ๋ค์ค ์ ๋ก๋)
- show(): ์ผ์ง ์์ธ ๋ณด๊ธฐ (๋ณธ์ธ ์ผ์ง ๋๋ ๊ณต๊ฐ ์ผ์ง๋ง ์ ๊ทผ ๊ฐ๋ฅ)
- edit(): ์ผ์ง ์์ ํผ
- update(): ์ผ์ง ์์
- destroy(): ์ผ์ง ์ญ์
- publish(): ์ผ์ง ๊ณต๊ฐ ์ฒ๋ฆฌ (is_public=true, published_at=now())
- unpublish(): ์ผ์ง ๋น๊ณต๊ฐ ์ ํ (is_public=false)
โ Laravel ์ปจํธ๋กค๋ฌ (PublicDiaryController):
- index(): ๊ณต๊ฐ๋ ์ ๊ฒฌ์ผ์ง ๊ฐค๋ฌ๋ฆฌ (is_public=true์ธ ์ผ์ง๋ง, ์ต์ ์, ํ์ด์ง๋ค์ด์ )
- show(): ๊ณต๊ฐ ์ผ์ง ์์ธ ๋ณด๊ธฐ
โ Blade ๋ทฐ ๊ตฌ์ฑ:
- resources/views/pets/index.blade.php: ๋ด ์ ๊ฒฌ ๋ชฉ๋ก + ๋ฑ๋ก ํผ (Alpine.js ๋ชจ๋ฌ)
- resources/views/diaries/index.blade.php: ๋ด ์ ๊ฒฌ์ผ์ง ๋ชฉ๋ก (๊ณต๊ฐ/๋น๊ณต๊ฐ ๋ฐฐ์ง ํ์)
- resources/views/diaries/create.blade.php: ์ผ์ง ์์ฑ ํผ (์ด๋ฏธ์ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํฌํจ)
- resources/views/diaries/show.blade.php: ์ผ์ง ์์ธ (์ด๋ฏธ์ง ๊ฐค๋ฌ๋ฆฌ)
- resources/views/diaries/edit.blade.php: ์ผ์ง ์์ ํผ
- resources/views/diaries/public/index.blade.php: ๊ณต๊ฐ ์ผ์ง ๊ฐค๋ฌ๋ฆฌ (์นด๋ ๊ทธ๋ฆฌ๋)
- resources/views/diaries/public/show.blade.php: ๊ณต๊ฐ ์ผ์ง ์์ธ
โ ๋ผ์ฐํธ ์ค์ (routes/web.php): Route::middleware('auth')->group(function () { Route::resource('pets', PetController::class)->except(['show']); Route::resource('diaries', DiaryController::class); Route::patch('/diaries/{diary}/publish', [DiaryController::class, 'publish'])->name('diaries.publish'); Route::patch('/diaries/{diary}/unpublish', [DiaryController::class, 'unpublish'])->name('diaries.unpublish'); }); Route::get('/public-diaries', [PublicDiaryController::class, 'index'])->name('public-diaries.index'); Route::get('/public-diaries/{diary}', [PublicDiaryController::class, 'show'])->name('public-diaries.show');
์ด๋ฏธ์ง ์ ๋ก๋๋ Storage::disk('public')์ ์ ์ฅํ๊ณ , php artisan storage:link ๋ก ์ฌ๋ณผ๋ฆญ ๋งํฌ๋ฅผ ์์ฑํ์ธ์.
์ผ์ง ์์ฑ ์ ์ด๋ฏธ์ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ Alpine.js๋ก ๊ตฌํํ์ธ์. ๊ณต๊ฐ/๋น๊ณต๊ฐ ํ ๊ธ ๋ฒํผ๋ Alpine.js๋ก ํ์ธ ๋ค์ด์ผ๋ก๊ทธ๋ฅผ ํ์ํ ํ ๋์ํ๊ฒ ํ์ธ์.
Tailwind CSS๋ก ๊น๋ํ ์นด๋ ๋ ์ด์์์ ๊ตฌ์ฑํ์ธ์.
**๊ธฐ๋ ๊ฒฐ๊ณผ:** "์ ๊ฒฌ ํ๋กํ์ ๋ฑ๋ก/์์ /์ญ์ ํ ์ ์๊ณ , ์ ๊ฒฌ์ผ์ง๋ฅผ ์์ฑํ๋ฉด ๊ธฐ๋ณธ ๋น๊ณต๊ฐ๋ก ์ ์ฅ๋ฉ๋๋ค. ๊ณต๊ฐ ๋ฒํผ์ ๋๋ฅด๋ฉด ๊ณต๊ฐ ๊ฐค๋ฌ๋ฆฌ์ ๋
ธ์ถ๋๊ณ , ๋น๊ณต๊ฐ๋ก ๋ค์ ์ ํํ ์ ์์ต๋๋ค."
---
### Step 4: ์ปค๋ฎค๋ํฐ ๊ฒ์ํ ๋ฐ ๋๊ธ ๊ธฐ๋ฅ
**ํ๋กฌํํธ:**
์ปค๋ฎค๋ํฐ ๊ฒ์ํ๊ณผ ๋๊ธ ๊ธฐ๋ฅ์ ๊ตฌํํ์ธ์.
โ Laravel ์ปจํธ๋กค๋ฌ (PostController):
- index(): ๊ฒ์๊ธ ๋ชฉ๋ก (์นดํ ๊ณ ๋ฆฌ ํํฐ, ๊ฒ์, ํ์ด์ง๋ค์ด์ )
- create(): ๊ฒ์๊ธ ์์ฑ ํผ (์นดํ ๊ณ ๋ฆฌ ์ ํ)
- store(): ๊ฒ์๊ธ ์ ์ฅ
- show(): ๊ฒ์๊ธ ์์ธ + ๋๊ธ ๋ชฉ๋ก
- edit(): ๊ฒ์๊ธ ์์ (๋ณธ์ธ ๊ธ๋ง)
- update(): ๊ฒ์๊ธ ์์ ์ฒ๋ฆฌ
- destroy(): ๊ฒ์๊ธ ์ญ์ (๋ณธ์ธ ๊ธ ๋๋ ๊ด๋ฆฌ์)
โ Laravel ์ปจํธ๋กค๋ฌ (CommentController):
- store(): ๋๊ธ ์์ฑ (post_id, content)
- update(): ๋๊ธ ์์ (๋ณธ์ธ ๋๊ธ๋ง)
- destroy(): ๋๊ธ ์ญ์ (๋ณธ์ธ ๋๊ธ ๋๋ ๊ด๋ฆฌ์)
โ Laravel ์ปจํธ๋กค๋ฌ (Admin\CategoryController):
- index(): ์นดํ ๊ณ ๋ฆฌ ๊ด๋ฆฌ ํ์ด์ง
- store(): ์นดํ ๊ณ ๋ฆฌ ์ถ๊ฐ
- update(): ์นดํ ๊ณ ๋ฆฌ ์์ (์ด๋ฆ, ์ฌ๋ฌ๊ทธ, ์ ๋ ฌ์์, ํ์ฑํ ์ฌ๋ถ)
- destroy(): ์นดํ ๊ณ ๋ฆฌ ์ญ์ (๊ฒ์๊ธ์ด ์์ผ๋ฉด ์ญ์ ๋ถ๊ฐ, ๊ฒฝ๊ณ ํ์)
โ Blade ๋ทฐ ๊ตฌ์ฑ:
- resources/views/posts/index.blade.php:
- ์๋จ์ ์นดํ ๊ณ ๋ฆฌ ํญ (์ ์ฒด + ๊ฐ ์นดํ ๊ณ ๋ฆฌ)
- ๊ฒ์ ๋ฐ (์ ๋ชฉ+๋ด์ฉ ๊ฒ์)
- ๊ฒ์๊ธ ๋ฆฌ์คํธ (์ ๋ชฉ, ์์ฑ์, ์นดํ ๊ณ ๋ฆฌ, ์์ฑ์ผ, ์กฐํ์)
- ํ์ด์ง๋ค์ด์
- resources/views/posts/create.blade.php: ๊ฒ์๊ธ ์์ฑ ํผ
- resources/views/posts/show.blade.php:
- ๊ฒ์๊ธ ์์ธ ๋ด์ฉ
- ํ๋จ์ ๋๊ธ ๋ชฉ๋ก
- ๋๊ธ ์์ฑ ํผ
- ๋๊ธ ์์ /์ญ์ ๋ฒํผ (๋ณธ์ธ ๋๊ธ์๋ง ํ์)
- resources/views/posts/edit.blade.php: ๊ฒ์๊ธ ์์ ํผ
- resources/views/admin/categories/index.blade.php:
- ์นดํ ๊ณ ๋ฆฌ ๋ชฉ๋ก (๋๋๊ทธ ์ ๋ ฌ ๋๋ ์์ ์ ๋ ฅ)
- ์นดํ ๊ณ ๋ฆฌ ์ถ๊ฐ/์์ /์ญ์
โ ๋ผ์ฐํธ ์ค์ : Route::middleware('auth')->group(function () { Route::resource('posts', PostController::class); Route::post('/posts/{post}/comments', [CommentController::class, 'store'])->name('comments.store'); Route::put('/comments/{comment}', [CommentController::class, 'update'])->name('comments.update'); Route::delete('/comments/{comment}', [CommentController::class, 'destroy'])->name('comments.destroy'); });
// ๊ด๋ฆฌ์ ์ ์ฉ Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(function () { Route::resource('categories', Admin\CategoryController::class)->except(['show', 'create', 'edit']); });
admin ๋ฏธ๋ค์จ์ด๋ฅผ ์์ฑํ์ธ์:
- App\Http\Middleware\AdminMiddleware
- is_admin์ด true์ธ ์ ์ ๋ง ์ ๊ทผ ๊ฐ๋ฅ
- bootstrap/app.php์ 'admin' ๋ณ์นญ์ผ๋ก ๋ฑ๋ก
๊ฒ์๊ธ ์์ธ ํ์ด์ง ์ ๊ทผ ์ view_count๋ฅผ 1 ์ฆ๊ฐ์ํค์ธ์. ๋๊ธ ์์ ์ Alpine.js๋ก ์ธ๋ผ์ธ ํธ์ง ๋ชจ๋๋ฅผ ๊ตฌํํ์ธ์.
Tailwind CSS๋ก ๊ฒ์ํ ํ ์ด๋ธ ์คํ์ผ๊ณผ ์นดํ ๊ณ ๋ฆฌ ํญ UI๋ฅผ ๊ตฌ์ฑํ์ธ์.
**๊ธฐ๋ ๊ฒฐ๊ณผ:** "์ปค๋ฎค๋ํฐ ๊ฒ์ํ์์ ์นดํ
๊ณ ๋ฆฌ๋ณ๋ก ๊ฒ์๊ธ์ ์กฐํํ๊ณ , ์์ฑ/์์ /์ญ์ ํ ์ ์์ต๋๋ค. ๋๊ธ ์์ฑ๊ณผ ์ธ๋ผ์ธ ์์ /์ญ์ ๊ฐ ์๋ํฉ๋๋ค. ๊ด๋ฆฌ์๋ ์นดํ
๊ณ ๋ฆฌ๋ฅผ ์ถ๊ฐ/์์ /์ญ์ ํ ์ ์์ต๋๋ค."
---
### Step 5: ๊ณต์ง์ฌํญ ๋ฐ ์น ํธ์ ์๋ฆผ
**ํ๋กฌํํธ:**
๊ณต์ง์ฌํญ ๊ธฐ๋ฅ๊ณผ ์น ํธ์ ์๋ฆผ์ ๊ตฌํํ์ธ์.
โ ์น ํธ์ ํจํค์ง ์ค์น: composer require laravel-notification-channels/webpush php artisan vendor:publish --provider="NotificationChannels\WebPush\WebPushServiceProvider" --tag="migrations" php artisan migrate
User ๋ชจ๋ธ์ NotificationChannels\WebPush\HasPushSubscriptions ํธ๋ ์ดํธ๋ฅผ ์ถ๊ฐํ์ธ์.
VAPID ํค ์์ฑ: php artisan webpush:vapid ์์ฑ๋ ํค๋ฅผ .env์ ์ค์ ํ์ธ์.
โ Laravel ์ปจํธ๋กค๋ฌ (NoticeController):
- index(): ๊ณต์ง์ฌํญ ๋ชฉ๋ก (๋ชจ๋ ์ ์ ์ ๊ทผ ๊ฐ๋ฅ, ํ์ด์ง๋ค์ด์ )
- show(): ๊ณต์ง์ฌํญ ์์ธ
- create(): ๊ณต์ง์ฌํญ ์์ฑ ํผ (๊ด๋ฆฌ์๋ง)
- store(): ๊ณต์ง์ฌํญ ์ ์ฅ + ์น ํธ์ ๋ฐ์ก (๊ด๋ฆฌ์๋ง)
- edit(): ๊ณต์ง์ฌํญ ์์ ํผ (๊ด๋ฆฌ์๋ง)
- update(): ๊ณต์ง์ฌํญ ์์ (๊ด๋ฆฌ์๋ง)
- destroy(): ๊ณต์ง์ฌํญ ์ญ์ (๊ด๋ฆฌ์๋ง)
โ Laravel ์ปจํธ๋กค๋ฌ (PushSubscriptionController):
- store(): ํธ์ ๊ตฌ๋ ๋ฑ๋ก (๋ธ๋ผ์ฐ์ ์์ ์ ์กํ subscription ์ ๋ณด ์ ์ฅ)
- destroy(): ํธ์ ๊ตฌ๋ ํด์
โ Laravel Notification (NewNoticeNotification):
- toWebPush($notifiable, $notification) ๋ฉ์๋:
- ์ ๋ชฉ: ๊ณต์ง์ฌํญ ์ ๋ชฉ
- ๋ณธ๋ฌธ: ๊ณต์ง์ฌํญ ๋ด์ฉ ์ 100์
- ์ก์ : ๊ณต์ง์ฌํญ ์์ธ ํ์ด์ง URL
- via() ๋ฉ์๋: ['webpush'] ๋ฐํ
โ Blade ๋ทฐ ๊ตฌ์ฑ:
- resources/views/notices/index.blade.php: ๊ณต์ง์ฌํญ ๋ชฉ๋ก (์ ๋ชฉ, ์์ฑ์ผ)
- resources/views/notices/show.blade.php: ๊ณต์ง์ฌํญ ์์ธ
- resources/views/notices/create.blade.php: ๊ณต์ง์ฌํญ ์์ฑ ํผ (๊ด๋ฆฌ์)
- resources/views/notices/edit.blade.php: ๊ณต์ง์ฌํญ ์์ ํผ (๊ด๋ฆฌ์)
โ ์๋น์ค ์์ปค ๋ฐ ํธ์ ๊ตฌ๋ JavaScript:
- public/sw.js: ์๋น์ค ์์ปค (ํธ์ ์์ ๋ฐ ์๋ฆผ ํ์, ์๋ฆผ ํด๋ฆญ ์ ํด๋น URL๋ก ์ด๋)
- resources/views/layouts/app.blade.php์ ํธ์ ๊ตฌ๋
์คํฌ๋ฆฝํธ ์ถ๊ฐ:
- ๋ก๊ทธ์ธํ ์ ์ ์๊ฒ ์๋ฆผ ๊ถํ ์์ฒญ
- ํ์ฉ ์ PushManager.subscribe()๋ก ๊ตฌ๋ ์์ฑ
- ๊ตฌ๋ ์ ๋ณด๋ฅผ /push-subscriptions๋ก POST ์ ์ก
โ ๋ผ์ฐํธ ์ค์ : Route::get('/notices', [NoticeController::class, 'index'])->name('notices.index'); Route::get('/notices/{notice}', [NoticeController::class, 'show'])->name('notices.show');
Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(function () { Route::get('/notices/create', [NoticeController::class, 'create'])->name('notices.create'); Route::post('/notices', [NoticeController::class, 'store'])->name('notices.store'); Route::get('/notices/{notice}/edit', [NoticeController::class, 'edit'])->name('notices.edit'); Route::put('/notices/{notice}', [NoticeController::class, 'update'])->name('notices.update'); Route::delete('/notices/{notice}', [NoticeController::class, 'destroy'])->name('notices.destroy'); });
Route::middleware('auth')->group(function () { Route::post('/push-subscriptions', [PushSubscriptionController::class, 'store']); Route::delete('/push-subscriptions', [PushSubscriptionController::class, 'destroy']); });
๊ณต์ง์ฌํญ ์ ์ฅ ์ store() ๋ฉ์๋์์ ๋ชจ๋ ํธ์ ๊ตฌ๋ ์ ์ ์๊ฒ NewNoticeNotification์ ๋ฐ์กํ์ธ์. ๋๋ ๋ฐ์ก ์ ํ๋ฅผ ์ฌ์ฉํ๋๋ก ShouldQueue ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ์ธ์.
**๊ธฐ๋ ๊ฒฐ๊ณผ:** "๊ด๋ฆฌ์๊ฐ ๊ณต์ง์ฌํญ์ ์์ฑํ๋ฉด ์น ํธ์ ์๋ฆผ์ด ๊ตฌ๋
์ ์ ์๊ฒ ๋ฐ์ก๋ฉ๋๋ค. ์ ์ ๋ ๋ธ๋ผ์ฐ์ ์์ ์๋ฆผ ํ์ฉ ์ ์๋์ผ๋ก ๊ตฌ๋
๋๋ฉฐ, ์๋ฆผ์ ํด๋ฆญํ๋ฉด ๊ณต์ง์ฌํญ ์์ธ ํ์ด์ง๋ก ์ด๋ํฉ๋๋ค."
---
### Step 6: ๋ฉ์ธ ํ์ด์ง, ๋ค๋น๊ฒ์ด์
๋ฐ ๋ง๋ฌด๋ฆฌ
**ํ๋กฌํํธ:**
๋ฉ์ธ(ํ) ํ์ด์ง, ํตํฉ ๋ค๋น๊ฒ์ด์ , ๊ทธ๋ฆฌ๊ณ ์ ์ฒด์ ์ธ UI ๋ง๋ฌด๋ฆฌ๋ฅผ ์ํํ์ธ์.
โ Laravel ์ปจํธ๋กค๋ฌ (HomeController):
- index(): ๋ฉ์ธ ํ์ด์ง ๋ฐ์ดํฐ
- ์ต์ ๊ณต์ง์ฌํญ 3๊ฐ
- ์ต์ ๊ณต๊ฐ ์ ๊ฒฌ์ผ์ง 6๊ฐ (์ธ๋ค์ผ ํฌํจ)
- ์ธ๊ธฐ ์ปค๋ฎค๋ํฐ ๊ฒ์๊ธ 5๊ฐ (์กฐํ์ ๊ธฐ์ค)
- ์ต์ ์ปค๋ฎค๋ํฐ ๊ฒ์๊ธ 5๊ฐ
โ Blade ๋ทฐ ๊ตฌ์ฑ:
- resources/views/home.blade.php:
- ํ์ด๋ก ์น์ (์ฌ์ดํธ ์๊ฐ, ๊ฐ์์ง ์ผ๋ฌ์คํธ ๋๋ ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง)
- ์ต์ ๊ณต์ง์ฌํญ ์ฌ๋ผ์ด๋ ๋๋ ๋ฆฌ์คํธ
- ๊ณต๊ฐ ์ ๊ฒฌ์ผ์ง ์นด๋ ๊ทธ๋ฆฌ๋ (์ฌ์ง ์ธ๋ค์ผ, ์ ๋ชฉ, ์์ฑ์)
- ์ธ๊ธฐ ๊ฒ์๊ธ ๋ฆฌ์คํธ
- ์ต์ ๊ฒ์๊ธ ๋ฆฌ์คํธ
โ ๋ค๋น๊ฒ์ด์ ๋ ์ด์์ ์์ (resources/views/layouts/navigation.blade.php):
- ๋ก๊ณ : "๋ฉ๋ฉ์ปค๋ฎค๋ํฐ"
- ๋ฉ๋ด: ํ, ๊ณต๊ฐ์ผ์ง, ์ปค๋ฎค๋ํฐ, ๊ณต์ง์ฌํญ
- ๋ก๊ทธ์ธ ์ ์ : ๋ด ์ผ์ง, ๋ด ์ ๊ฒฌ ๊ด๋ฆฌ (๋๋กญ๋ค์ด)
- ๊ด๋ฆฌ์: ๊ด๋ฆฌ ๋ฉ๋ด (์นดํ ๊ณ ๋ฆฌ ๊ด๋ฆฌ, ๊ณต์ง์ฌํญ ๊ด๋ฆฌ)
- ๋ชจ๋ฐ์ผ ํ๋ฒ๊ฑฐ ๋ฉ๋ด
โ ๋ผ์ฐํธ ์ค์ : Route::get('/', [HomeController::class, 'index'])->name('home');
โ ์ถ๊ฐ ๋ง๋ฌด๋ฆฌ ์์ :
- 404, 403 ์๋ฌ ํ์ด์ง ์ปค์คํฐ๋ง์ด์ง (resources/views/errors/)
- ํ๋์ ๋ฉ์์ง ์ปดํฌ๋ํธ (์ฑ๊ณต/์๋ฌ ์๋ฆผ ํ ์คํธ, Alpine.js ์๋ ๋ซ๊ธฐ)
- ๋ฉํ ํ๊ทธ ์ค์ (SEO ๊ธฐ๋ณธ, Open Graph ํ๊ทธ)
- ํ๋กํ ํ์ด์ง์์ ๋๋ค์ ์์ ๊ธฐ๋ฅ ์ถ๊ฐ (users ํ ์ด๋ธ์ nickname ์ปฌ๋ผ ์ถ๊ฐ ๋ง์ด๊ทธ๋ ์ด์ )
- ์ ์ฒด ํ์ด์ง ๋ฐ์ํ ์ ๊ฒ (๋ชจ๋ฐ์ผ, ํ๋ธ๋ฆฟ, ๋ฐ์คํฌํ)
Tailwind CSS๋ก ๊ฐ์์ง ์ปค๋ฎค๋ํฐ์ ์ด์ธ๋ฆฌ๋ ๋ฐ๋ปํ ํค์ ์ปฌ๋ฌ ํ ๋ง๋ฅผ ์ ์ฉํ์ธ์:
- Primary: amber/orange ๊ณ์ด
- ๋ฐฐ๊ฒฝ: warm gray ๊ณ์ด
- ์นด๋: ๋ฅ๊ทผ ๋ชจ์๋ฆฌ, ๋ถ๋๋ฌ์ด ๊ทธ๋ฆผ์
tailwind.config.js์ ์ปค์คํ ์ปฌ๋ฌ๋ฅผ ์ค์ ํ์ธ์.
**๊ธฐ๋ ๊ฒฐ๊ณผ:** "๋ฉ์ธ ํ์ด์ง์ ์ต์ ์ฝํ
์ธ ๊ฐ ์ข
ํฉ์ ์ผ๋ก ํ์๋๊ณ , ๋ค๋น๊ฒ์ด์
์ ํตํด ๋ชจ๋ ๊ธฐ๋ฅ์ ์ ๊ทผํ ์ ์์ต๋๋ค. ๋ฐ์ํ ๋์์ธ์ด ๋ชจ๋ฐ์ผ์์๋ ์ ๋์ํ๋ฉฐ, ํฅํ ๋ชจ๋ฐ์ผ ์ฑ ์ ํ์ด ์ฉ์ดํ ๊ตฌ์กฐ๊ฐ ์์ฑ๋ฉ๋๋ค."
---
## ๋ชจ๋ฐ์ผ ์ฑ ์ ํ ๊ฐ์ด๋ (ํฅํ ์ฐธ๊ณ )
์น ์์ฑ ํ ๋ชจ๋ฐ์ผ ์ฑ ์ ํ ์ ๋ค์ ๋ฐฉ๋ฒ์ ๊ถ์ฅํฉ๋๋ค:
1. **API ๋ผ์ฐํธ ์ถ๊ฐ**: ๊ธฐ์กด ์ปจํธ๋กค๋ฌ ๋ก์ง์ ์ฌ์ฌ์ฉํ์ฌ `routes/api.php`์ JSON ์๋ต API๋ฅผ ์ถ๊ฐ
2. **Laravel Sanctum**์ผ๋ก ๋ชจ๋ฐ์ผ ์ฑ ํ ํฐ ์ธ์ฆ ๊ตฌํ
3. **React Native** ๋๋ **Flutter**๋ก ๋ชจ๋ฐ์ผ ํด๋ผ์ด์ธํธ ๊ฐ๋ฐ
4. ๋๋ **PWA(Progressive Web App)** ๋ฐฉ์์ผ๋ก ํ์ฌ ์น์ ์ฑ์ฒ๋ผ ์ค์น ๊ฐ๋ฅํ๊ฒ ๊ตฌ์ฑ (์๋น์ค ์์ปค ์ด๋ฏธ ๊ตฌํ๋์ด ์์ผ๋ฏ๋ก manifest.json ์ถ๊ฐ๋ง์ผ๋ก ๊ฐ๋ฅ)
์ ๋ด์ฉ์ ๊ทธ๋๋ก ๊ฒ์๋ฌผ์ ์ ๋ชฉ/๋ด์ฉ ํ๋์ ๋ถ์ฌ๋ฃ์ผ๋ฉด ๋ฉ๋๋ค. Laravel ๋ฒ์ ์ 13.x๋ก ๋ชจ๋ ์์ ํ๊ณ , ์ฐธ๊ณ ๋ ์ํผ์ ๋์ผํ ํ์(ํ๋ก์ ํธ ๊ฐ์ โ ๊ธฐ๋ฅ โ ๊ธฐ์ ์คํ โ ์ ์ฝ ์กฐ๊ฑด โ Step๋ณ ํ๋กฌํํธ + ๊ธฐ๋ ๊ฒฐ๊ณผ)์ผ๋ก ๊ตฌ์ฑํ์ต๋๋ค.
๋๊ธ 2๊ฐ
์ธ๋ค์ผ๋ฑ์ ์ฒจ๋ถํ๋ ์ฝ๊ฐ์ ์์ ์ ํด์ ๋ง๋ฌด๋ฆฌ ํ์ต๋๋ค
1์๊ฐ ์ ๋ ๊ฑธ๋ฆฐ๊ฒ ๊ฐ์ต๋๋ค
https://sir.kr/showcases/199