768 lines
20 KiB
Markdown
768 lines
20 KiB
Markdown
|
|
# MNG 프로젝트 개발 프로세스
|
||
|
|
|
||
|
|
## 🎯 개발 철학
|
||
|
|
|
||
|
|
```
|
||
|
|
API 우선 → HTMX 연동 → 단순하고 수정 용이한 코드
|
||
|
|
```
|
||
|
|
|
||
|
|
### 핵심 원칙
|
||
|
|
1. **API First**: 모든 기능은 API로 먼저 개발
|
||
|
|
2. **Service-First**: 비즈니스 로직은 Service에만
|
||
|
|
3. **HTMX Driven**: JS 최소화, HTML 속성으로 인터랙션
|
||
|
|
4. **DaisyUI Only**: 커스텀 CSS 금지, DaisyUI 클래스만 사용
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📐 표준 개발 프로세스 (6단계)
|
||
|
|
|
||
|
|
### Phase 0: 환경 구성 (최초 1회)
|
||
|
|
**참조**: `claudedocs/mng/SETUP_GUIDE.md`
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# SETUP_GUIDE.md의 Step 1-10 참조
|
||
|
|
# 1. Laravel 프로젝트 생성
|
||
|
|
# 2. Docker 설정 파일 생성
|
||
|
|
# 3. docker-compose.yml 업데이트
|
||
|
|
# 4. nginx.conf 업데이트
|
||
|
|
# 5. Tailwind + DaisyUI + HTMX 설정
|
||
|
|
# 6. admin/ 모델 복사
|
||
|
|
# 7. Docker 빌드 및 실행
|
||
|
|
# 8. 동작 확인 (http://mng.sam.kr)
|
||
|
|
|
||
|
|
# 스킬 사용:
|
||
|
|
/sc:implement "SETUP_GUIDE.md 따라 MNG 환경 구성"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Phase 1: 준비 단계
|
||
|
|
```bash
|
||
|
|
# 1. 기능 분석 (Sequential Thinking)
|
||
|
|
/sc:analyze --think
|
||
|
|
|
||
|
|
# 2. 요구사항 정리
|
||
|
|
- 입력: 어떤 데이터를 받는가?
|
||
|
|
- 처리: 어떤 비즈니스 로직?
|
||
|
|
- 출력: 어떤 데이터를 반환?
|
||
|
|
- 화면: 어떤 UI 필요?
|
||
|
|
|
||
|
|
# 3. API 명세 작성
|
||
|
|
- 엔드포인트: GET /api/admin/users
|
||
|
|
- Request: { search: string, role_id?: number }
|
||
|
|
- Response: { success, data, message, meta }
|
||
|
|
```
|
||
|
|
|
||
|
|
### Phase 1: DB & Model (1단계)
|
||
|
|
```bash
|
||
|
|
# 1-1. 마이그레이션 확인
|
||
|
|
# 기존 테이블 사용? → 마이그레이션 불필요
|
||
|
|
# 신규 테이블? → admin_* or stat_* 접두사
|
||
|
|
|
||
|
|
# 1-2. 모델 확인/생성
|
||
|
|
# admin/app/Models에서 복사했는지 확인
|
||
|
|
# BelongsToTenant, HasAuditLog 트레잇 적용
|
||
|
|
|
||
|
|
# 예시: mng/app/Models/User.php
|
||
|
|
<?php
|
||
|
|
namespace App\Models;
|
||
|
|
|
||
|
|
use App\Traits\BelongsToTenant;
|
||
|
|
use App\Traits\HasAuditLog;
|
||
|
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||
|
|
|
||
|
|
class User extends Authenticatable
|
||
|
|
{
|
||
|
|
use BelongsToTenant, HasAuditLog;
|
||
|
|
|
||
|
|
protected $fillable = [
|
||
|
|
'tenant_id', 'email', 'password', 'name',
|
||
|
|
'role_id', 'department_id', 'is_active',
|
||
|
|
];
|
||
|
|
|
||
|
|
public function role()
|
||
|
|
{
|
||
|
|
return $this->belongsTo(Role::class);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function department()
|
||
|
|
{
|
||
|
|
return $this->belongsTo(Department::class);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Phase 2: Service Layer (2단계)
|
||
|
|
```bash
|
||
|
|
# 2-1. Service 생성 (비즈니스 로직)
|
||
|
|
# mng/app/Services/UserService.php
|
||
|
|
|
||
|
|
<?php
|
||
|
|
namespace App\Services;
|
||
|
|
|
||
|
|
use App\Models\User;
|
||
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||
|
|
use Illuminate\Support\Facades\Hash;
|
||
|
|
|
||
|
|
class UserService
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* 사용자 목록 조회 (검색, 필터, 페이징)
|
||
|
|
*/
|
||
|
|
public function getUsers(array $filters = []): LengthAwarePaginator
|
||
|
|
{
|
||
|
|
$query = User::with(['role', 'department']);
|
||
|
|
|
||
|
|
// 검색
|
||
|
|
if (!empty($filters['search'])) {
|
||
|
|
$query->where(function ($q) use ($filters) {
|
||
|
|
$q->where('name', 'like', "%{$filters['search']}%")
|
||
|
|
->orWhere('email', 'like', "%{$filters['search']}%");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 역할 필터
|
||
|
|
if (!empty($filters['role_id'])) {
|
||
|
|
$query->where('role_id', $filters['role_id']);
|
||
|
|
}
|
||
|
|
|
||
|
|
return $query->paginate(20);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 사용자 생성
|
||
|
|
*/
|
||
|
|
public function createUser(array $data): User
|
||
|
|
{
|
||
|
|
$data['password'] = Hash::make($data['password']);
|
||
|
|
$data['tenant_id'] = auth()->user()->tenant_id;
|
||
|
|
|
||
|
|
return User::create($data);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 사용자 수정
|
||
|
|
*/
|
||
|
|
public function updateUser(User $user, array $data): User
|
||
|
|
{
|
||
|
|
if (!empty($data['password'])) {
|
||
|
|
$data['password'] = Hash::make($data['password']);
|
||
|
|
} else {
|
||
|
|
unset($data['password']);
|
||
|
|
}
|
||
|
|
|
||
|
|
$user->update($data);
|
||
|
|
return $user->fresh();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 사용자 삭제 (Soft Delete)
|
||
|
|
*/
|
||
|
|
public function deleteUser(User $user): bool
|
||
|
|
{
|
||
|
|
return $user->delete();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Phase 3: API Controller (3단계)
|
||
|
|
```bash
|
||
|
|
# 3-1. FormRequest 생성
|
||
|
|
# mng/app/Http/Requests/StoreUserRequest.php
|
||
|
|
|
||
|
|
<?php
|
||
|
|
namespace App\Http\Requests;
|
||
|
|
|
||
|
|
use Illuminate\Foundation\Http\FormRequest;
|
||
|
|
|
||
|
|
class StoreUserRequest extends FormRequest
|
||
|
|
{
|
||
|
|
public function authorize(): bool
|
||
|
|
{
|
||
|
|
return true; // Policy로 권한 체크
|
||
|
|
}
|
||
|
|
|
||
|
|
public function rules(): array
|
||
|
|
{
|
||
|
|
return [
|
||
|
|
'name' => 'required|string|max:255',
|
||
|
|
'email' => 'required|email|unique:users,email',
|
||
|
|
'password' => 'required|string|min:8',
|
||
|
|
'role_id' => 'required|exists:roles,id',
|
||
|
|
'department_id' => 'required|exists:departments,id',
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
public function messages(): array
|
||
|
|
{
|
||
|
|
return [
|
||
|
|
'name.required' => 'users.validation.name_required',
|
||
|
|
'email.required' => 'users.validation.email_required',
|
||
|
|
'email.email' => 'users.validation.email_invalid',
|
||
|
|
'email.unique' => 'users.validation.email_unique',
|
||
|
|
];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# 3-2. API Controller 생성
|
||
|
|
# mng/app/Http/Controllers/Api/Admin/UserController.php
|
||
|
|
|
||
|
|
<?php
|
||
|
|
namespace App\Http\Controllers\Api\Admin;
|
||
|
|
|
||
|
|
use App\Http\Controllers\Controller;
|
||
|
|
use App\Http\Requests\StoreUserRequest;
|
||
|
|
use App\Http\Requests\UpdateUserRequest;
|
||
|
|
use App\Services\UserService;
|
||
|
|
use App\Models\User;
|
||
|
|
use Illuminate\Http\JsonResponse;
|
||
|
|
use Illuminate\Http\Request;
|
||
|
|
|
||
|
|
class UserController extends Controller
|
||
|
|
{
|
||
|
|
public function __construct(
|
||
|
|
private UserService $userService
|
||
|
|
) {}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 사용자 목록 (API)
|
||
|
|
* GET /api/admin/users
|
||
|
|
*/
|
||
|
|
public function index(Request $request): JsonResponse
|
||
|
|
{
|
||
|
|
$users = $this->userService->getUsers($request->all());
|
||
|
|
|
||
|
|
return response()->json([
|
||
|
|
'success' => true,
|
||
|
|
'data' => $users->items(),
|
||
|
|
'message' => 'users.retrieved',
|
||
|
|
'meta' => [
|
||
|
|
'current_page' => $users->currentPage(),
|
||
|
|
'last_page' => $users->lastPage(),
|
||
|
|
'per_page' => $users->perPage(),
|
||
|
|
'total' => $users->total(),
|
||
|
|
],
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 사용자 생성 (API)
|
||
|
|
* POST /api/admin/users
|
||
|
|
*/
|
||
|
|
public function store(StoreUserRequest $request): JsonResponse
|
||
|
|
{
|
||
|
|
$user = $this->userService->createUser($request->validated());
|
||
|
|
|
||
|
|
return response()->json([
|
||
|
|
'success' => true,
|
||
|
|
'data' => $user,
|
||
|
|
'message' => 'users.created',
|
||
|
|
], 201);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 사용자 수정 (API)
|
||
|
|
* PUT /api/admin/users/{user}
|
||
|
|
*/
|
||
|
|
public function update(UpdateUserRequest $request, User $user): JsonResponse
|
||
|
|
{
|
||
|
|
$user = $this->userService->updateUser($user, $request->validated());
|
||
|
|
|
||
|
|
return response()->json([
|
||
|
|
'success' => true,
|
||
|
|
'data' => $user,
|
||
|
|
'message' => 'users.updated',
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 사용자 삭제 (API)
|
||
|
|
* DELETE /api/admin/users/{user}
|
||
|
|
*/
|
||
|
|
public function destroy(User $user): JsonResponse
|
||
|
|
{
|
||
|
|
$this->userService->deleteUser($user);
|
||
|
|
|
||
|
|
return response()->json([
|
||
|
|
'success' => true,
|
||
|
|
'message' => 'users.deleted',
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# 3-3. 라우트 등록
|
||
|
|
# mng/routes/api.php
|
||
|
|
|
||
|
|
Route::middleware(['auth:sanctum', 'admin.permission'])
|
||
|
|
->prefix('admin')
|
||
|
|
->group(function () {
|
||
|
|
Route::apiResource('users', UserController::class);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Phase 4: Blade + HTMX (4단계)
|
||
|
|
```bash
|
||
|
|
# 4-1. HTML 응답용 Controller (선택)
|
||
|
|
# API + Blade 부분 HTML 반환
|
||
|
|
|
||
|
|
# mng/app/Http/Controllers/Api/Admin/UserController.php (추가)
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 사용자 목록 (HTMX용 Blade HTML)
|
||
|
|
* GET /api/admin/users?format=html
|
||
|
|
*/
|
||
|
|
public function index(Request $request)
|
||
|
|
{
|
||
|
|
$users = $this->userService->getUsers($request->all());
|
||
|
|
|
||
|
|
// HTMX 요청 시 부분 HTML 반환
|
||
|
|
if ($request->header('HX-Request')) {
|
||
|
|
return view('users.partials.table', compact('users'));
|
||
|
|
}
|
||
|
|
|
||
|
|
// 일반 요청 시 JSON 반환
|
||
|
|
return response()->json([
|
||
|
|
'success' => true,
|
||
|
|
'data' => $users->items(),
|
||
|
|
'message' => 'users.retrieved',
|
||
|
|
'meta' => [
|
||
|
|
'current_page' => $users->currentPage(),
|
||
|
|
'last_page' => $users->lastPage(),
|
||
|
|
'per_page' => $users->perPage(),
|
||
|
|
'total' => $users->total(),
|
||
|
|
],
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
# 4-2. Blade 템플릿 작성
|
||
|
|
# mng/resources/views/users/index.blade.php
|
||
|
|
|
||
|
|
@extends('layouts.app')
|
||
|
|
|
||
|
|
@section('content')
|
||
|
|
<div class="space-y-4">
|
||
|
|
{{-- 헤더 --}}
|
||
|
|
<div class="flex justify-between items-center">
|
||
|
|
<h1 class="text-2xl font-bold">사용자 관리</h1>
|
||
|
|
<a href="/users/create" class="btn btn-primary">사용자 추가</a>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{{-- 검색/필터 --}}
|
||
|
|
<div class="card bg-base-100 shadow-xl">
|
||
|
|
<div class="card-body">
|
||
|
|
<form hx-get="/api/admin/users"
|
||
|
|
hx-target="#user-table"
|
||
|
|
hx-trigger="submit">
|
||
|
|
<div class="grid grid-cols-3 gap-4">
|
||
|
|
<input type="text" name="search"
|
||
|
|
placeholder="이름 또는 이메일"
|
||
|
|
class="input input-bordered" />
|
||
|
|
|
||
|
|
<select name="role_id" class="select select-bordered">
|
||
|
|
<option value="">전체 역할</option>
|
||
|
|
@foreach($roles as $role)
|
||
|
|
<option value="{{ $role->id }}">{{ $role->name }}</option>
|
||
|
|
@endforeach
|
||
|
|
</select>
|
||
|
|
|
||
|
|
<button type="submit" class="btn btn-primary">검색</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{{-- 테이블 영역 --}}
|
||
|
|
<div id="user-table"
|
||
|
|
hx-get="/api/admin/users"
|
||
|
|
hx-trigger="load">
|
||
|
|
{{-- 초기 로드 시 서버에서 HTML 받아서 여기 삽입 --}}
|
||
|
|
<div class="flex justify-center p-8">
|
||
|
|
<span class="loading loading-spinner loading-lg"></span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
@endsection
|
||
|
|
|
||
|
|
# 4-3. 부분 템플릿 (HTMX 응답용)
|
||
|
|
# mng/resources/views/users/partials/table.blade.php
|
||
|
|
|
||
|
|
<div class="card bg-base-100 shadow-xl">
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="overflow-x-auto">
|
||
|
|
<table class="table w-full">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>ID</th>
|
||
|
|
<th>이름</th>
|
||
|
|
<th>이메일</th>
|
||
|
|
<th>역할</th>
|
||
|
|
<th>부서</th>
|
||
|
|
<th>상태</th>
|
||
|
|
<th>작업</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
@foreach($users as $user)
|
||
|
|
<tr>
|
||
|
|
<td>{{ $user->id }}</td>
|
||
|
|
<td>{{ $user->name }}</td>
|
||
|
|
<td>{{ $user->email }}</td>
|
||
|
|
<td>{{ $user->role->name }}</td>
|
||
|
|
<td>{{ $user->department->name }}</td>
|
||
|
|
<td>
|
||
|
|
<span class="badge {{ $user->is_active ? 'badge-success' : 'badge-error' }}">
|
||
|
|
{{ $user->is_active ? '활성' : '비활성' }}
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
<td>
|
||
|
|
<div class="btn-group">
|
||
|
|
<a href="/users/{{ $user->id }}/edit" class="btn btn-sm">수정</a>
|
||
|
|
<button hx-delete="/api/admin/users/{{ $user->id }}"
|
||
|
|
hx-confirm="정말 삭제하시겠습니까?"
|
||
|
|
hx-target="closest tr"
|
||
|
|
hx-swap="outerHTML swap:1s"
|
||
|
|
class="btn btn-sm btn-error">
|
||
|
|
삭제
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
@endforeach
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{{-- 페이징 (HTMX) --}}
|
||
|
|
<div class="flex justify-center mt-4">
|
||
|
|
@if($users->hasPages())
|
||
|
|
<div class="btn-group">
|
||
|
|
@foreach($users->getUrlRange(1, $users->lastPage()) as $page => $url)
|
||
|
|
<button hx-get="{{ $url }}"
|
||
|
|
hx-target="#user-table"
|
||
|
|
class="btn btn-sm {{ $page == $users->currentPage() ? 'btn-active' : '' }}">
|
||
|
|
{{ $page }}
|
||
|
|
</button>
|
||
|
|
@endforeach
|
||
|
|
</div>
|
||
|
|
@endif
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
### Phase 5: 테스트 & 검증 (5단계)
|
||
|
|
```bash
|
||
|
|
# 5-1. Feature Test 작성
|
||
|
|
# mng/tests/Feature/UserControllerTest.php
|
||
|
|
|
||
|
|
<?php
|
||
|
|
namespace Tests\Feature;
|
||
|
|
|
||
|
|
use Tests\TestCase;
|
||
|
|
use App\Models\User;
|
||
|
|
use App\Models\Role;
|
||
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
|
|
|
||
|
|
class UserControllerTest extends TestCase
|
||
|
|
{
|
||
|
|
use RefreshDatabase;
|
||
|
|
|
||
|
|
public function test_사용자_목록_조회()
|
||
|
|
{
|
||
|
|
$user = User::factory()->create();
|
||
|
|
|
||
|
|
$response = $this->actingAs($user)
|
||
|
|
->getJson('/api/admin/users');
|
||
|
|
|
||
|
|
$response->assertStatus(200)
|
||
|
|
->assertJsonStructure([
|
||
|
|
'success',
|
||
|
|
'data',
|
||
|
|
'message',
|
||
|
|
'meta',
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_사용자_생성()
|
||
|
|
{
|
||
|
|
$admin = User::factory()->create();
|
||
|
|
$role = Role::factory()->create();
|
||
|
|
|
||
|
|
$response = $this->actingAs($admin)
|
||
|
|
->postJson('/api/admin/users', [
|
||
|
|
'name' => '홍길동',
|
||
|
|
'email' => 'hong@example.com',
|
||
|
|
'password' => 'password123',
|
||
|
|
'role_id' => $role->id,
|
||
|
|
'department_id' => 1,
|
||
|
|
]);
|
||
|
|
|
||
|
|
$response->assertStatus(201)
|
||
|
|
->assertJson([
|
||
|
|
'success' => true,
|
||
|
|
'message' => 'users.created',
|
||
|
|
]);
|
||
|
|
|
||
|
|
$this->assertDatabaseHas('users', [
|
||
|
|
'email' => 'hong@example.com',
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# 5-2. 테스트 실행
|
||
|
|
php artisan test --filter=UserControllerTest
|
||
|
|
|
||
|
|
# 5-3. 코드 스타일 검증
|
||
|
|
./vendor/bin/pint
|
||
|
|
|
||
|
|
# 5-4. 품질 체크리스트
|
||
|
|
□ Service-First (비즈니스 로직 → Service)
|
||
|
|
□ FormRequest (컨트롤러 검증 금지)
|
||
|
|
□ BelongsToTenant (multi-tenant 스코프)
|
||
|
|
□ i18n 키 (하드코딩 금지)
|
||
|
|
□ Soft Delete (deleted_at)
|
||
|
|
□ 감사 로그 (HasAuditLog trait)
|
||
|
|
□ API 응답 형식 ({success, data, message, meta})
|
||
|
|
□ HTMX 속성 (hx-get, hx-target, hx-swap)
|
||
|
|
□ DaisyUI 클래스만 사용
|
||
|
|
□ Feature Test 통과
|
||
|
|
□ Pint 통과
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔄 실전 워크플로 (스킬 활용)
|
||
|
|
|
||
|
|
### 신규 기능 개발 시
|
||
|
|
```bash
|
||
|
|
# Step 1: 기능 분석 및 설계
|
||
|
|
/sc:design "사용자 관리 기능"
|
||
|
|
# → Sequential Thinking으로 요구사항 분석
|
||
|
|
# → API 명세 도출
|
||
|
|
|
||
|
|
# Step 2: 구현
|
||
|
|
/sc:implement "사용자 관리 API 구현"
|
||
|
|
# → Model, Service, Controller, FormRequest 생성
|
||
|
|
# → 자동으로 5단계 프로세스 진행
|
||
|
|
|
||
|
|
# Step 3: Blade + HTMX 구현
|
||
|
|
# 직접 작성 (단순하므로 AI 불필요)
|
||
|
|
# 또는 /sc:implement "사용자 목록 Blade 화면"
|
||
|
|
|
||
|
|
# Step 4: 테스트
|
||
|
|
/sc:test "UserController"
|
||
|
|
# → Feature Test 자동 생성 및 실행
|
||
|
|
|
||
|
|
# Step 5: 검증 및 커밋
|
||
|
|
code-workflow 스킬 사용
|
||
|
|
# → 분석 → 수정 → 검증 → 정리 → 커밋
|
||
|
|
```
|
||
|
|
|
||
|
|
### 버그 수정 시
|
||
|
|
```bash
|
||
|
|
# Step 1: 문제 분석
|
||
|
|
/sc:troubleshoot "사용자 목록 페이징 안됨"
|
||
|
|
# → Root Cause 분석
|
||
|
|
|
||
|
|
# Step 2: 수정
|
||
|
|
/sc:improve "UserService 페이징 로직"
|
||
|
|
|
||
|
|
# Step 3: 테스트
|
||
|
|
/sc:test
|
||
|
|
|
||
|
|
# Step 4: 커밋
|
||
|
|
code-workflow
|
||
|
|
```
|
||
|
|
|
||
|
|
### 리팩토링 시
|
||
|
|
```bash
|
||
|
|
/sc:improve --focus quality "UserController"
|
||
|
|
/sc:analyze --think-hard "전체 아키텍처"
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📋 체크리스트 템플릿
|
||
|
|
|
||
|
|
### 기능 개발 완료 체크리스트
|
||
|
|
```
|
||
|
|
기능명: _______________
|
||
|
|
|
||
|
|
[ ] Phase 1: DB & Model
|
||
|
|
[ ] 마이그레이션 (필요 시)
|
||
|
|
[ ] 모델 생성/복사
|
||
|
|
[ ] BelongsToTenant 적용
|
||
|
|
[ ] HasAuditLog 적용
|
||
|
|
[ ] 관계 설정 (belongsTo, hasMany)
|
||
|
|
|
||
|
|
[ ] Phase 2: Service Layer
|
||
|
|
[ ] Service 생성
|
||
|
|
[ ] 비즈니스 로직 구현
|
||
|
|
[ ] 트랜잭션 처리
|
||
|
|
[ ] 예외 처리
|
||
|
|
|
||
|
|
[ ] Phase 3: API Controller
|
||
|
|
[ ] FormRequest 생성 (Validation)
|
||
|
|
[ ] Controller 생성
|
||
|
|
[ ] API 응답 형식 준수
|
||
|
|
[ ] i18n 키 사용
|
||
|
|
[ ] 라우트 등록
|
||
|
|
|
||
|
|
[ ] Phase 4: Blade + HTMX
|
||
|
|
[ ] 메인 페이지 (index.blade.php)
|
||
|
|
[ ] 부분 템플릿 (partials/*.blade.php)
|
||
|
|
[ ] HTMX 속성 (hx-get, hx-post, hx-delete)
|
||
|
|
[ ] DaisyUI 컴포넌트만 사용
|
||
|
|
[ ] HX-Request 헤더 처리
|
||
|
|
|
||
|
|
[ ] Phase 5: 테스트 & 검증
|
||
|
|
[ ] Feature Test 작성
|
||
|
|
[ ] 테스트 통과 (php artisan test)
|
||
|
|
[ ] Pint 통과 (./vendor/bin/pint)
|
||
|
|
[ ] Swagger 문서화 (선택)
|
||
|
|
|
||
|
|
[ ] 커밋
|
||
|
|
[ ] code-workflow 스킬 사용
|
||
|
|
[ ] CURRENT_WORKS.md 업데이트
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🎨 HTMX 패턴 라이브러리
|
||
|
|
|
||
|
|
### 1. 목록 조회 (Load)
|
||
|
|
```blade
|
||
|
|
<div hx-get="/api/admin/users"
|
||
|
|
hx-trigger="load"
|
||
|
|
hx-target="this">
|
||
|
|
<span class="loading loading-spinner"></span>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. 검색/필터 (Submit)
|
||
|
|
```blade
|
||
|
|
<form hx-get="/api/admin/users"
|
||
|
|
hx-target="#results"
|
||
|
|
hx-trigger="submit">
|
||
|
|
<input name="search" class="input input-bordered" />
|
||
|
|
<button class="btn btn-primary">검색</button>
|
||
|
|
</form>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. 생성 (POST)
|
||
|
|
```blade
|
||
|
|
<form hx-post="/api/admin/users"
|
||
|
|
hx-target="#user-list"
|
||
|
|
hx-swap="beforeend">
|
||
|
|
<!-- 폼 필드 -->
|
||
|
|
<button class="btn btn-primary">저장</button>
|
||
|
|
</form>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. 수정 (PUT)
|
||
|
|
```blade
|
||
|
|
<form hx-put="/api/admin/users/{{ $user->id }}"
|
||
|
|
hx-target="closest tr"
|
||
|
|
hx-swap="outerHTML">
|
||
|
|
<!-- 폼 필드 -->
|
||
|
|
<button class="btn btn-primary">수정</button>
|
||
|
|
</form>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5. 삭제 (DELETE)
|
||
|
|
```blade
|
||
|
|
<button hx-delete="/api/admin/users/{{ $user->id }}"
|
||
|
|
hx-confirm="정말 삭제하시겠습니까?"
|
||
|
|
hx-target="closest tr"
|
||
|
|
hx-swap="outerHTML swap:1s"
|
||
|
|
class="btn btn-error">
|
||
|
|
삭제
|
||
|
|
</button>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 6. 무한 스크롤
|
||
|
|
```blade
|
||
|
|
<div hx-get="/api/admin/users?page=2"
|
||
|
|
hx-trigger="revealed"
|
||
|
|
hx-swap="afterend">
|
||
|
|
더보기...
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 7. 폴링 (자동 갱신)
|
||
|
|
```blade
|
||
|
|
<div hx-get="/api/admin/stats"
|
||
|
|
hx-trigger="every 10s"
|
||
|
|
hx-target="this">
|
||
|
|
통계: {{ $stats }}
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 8. 디바운싱 (입력 지연)
|
||
|
|
```blade
|
||
|
|
<input hx-get="/api/admin/users/search"
|
||
|
|
hx-trigger="keyup changed delay:500ms"
|
||
|
|
hx-target="#search-results"
|
||
|
|
name="q"
|
||
|
|
class="input input-bordered" />
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔧 개발 환경 설정
|
||
|
|
|
||
|
|
### 필수 패키지 설치
|
||
|
|
```bash
|
||
|
|
# Composer
|
||
|
|
composer require laravel/sanctum
|
||
|
|
composer require darkaonline/l5-swagger
|
||
|
|
composer require --dev laravel/pint
|
||
|
|
|
||
|
|
# NPM
|
||
|
|
npm install -D tailwindcss daisyui @tailwindcss/forms
|
||
|
|
npm install htmx.org
|
||
|
|
```
|
||
|
|
|
||
|
|
### HTMX 설정
|
||
|
|
```js
|
||
|
|
// resources/js/app.js
|
||
|
|
import htmx from 'htmx.org';
|
||
|
|
window.htmx = htmx;
|
||
|
|
|
||
|
|
// HTMX 전역 설정
|
||
|
|
document.addEventListener('DOMContentLoaded', () => {
|
||
|
|
// CSRF 토큰 자동 추가
|
||
|
|
document.body.addEventListener('htmx:configRequest', (event) => {
|
||
|
|
event.detail.headers['X-CSRF-TOKEN'] = document.querySelector('meta[name="csrf-token"]').content;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Blade 레이아웃
|
||
|
|
```blade
|
||
|
|
<!-- resources/views/layouts/app.blade.php -->
|
||
|
|
<!DOCTYPE html>
|
||
|
|
<html data-theme="light">
|
||
|
|
<head>
|
||
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||
|
|
<title>{{ config('app.name') }}</title>
|
||
|
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
@yield('content')
|
||
|
|
</body>
|
||
|
|
</html>
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📝 다음 단계
|
||
|
|
|
||
|
|
1. **Phase 1 시작**: Laravel 프로젝트 생성 및 환경 구성
|
||
|
|
2. **인증 구현**: 로그인 API + Blade 화면
|
||
|
|
3. **첫 기능 개발**: 사용자 관리 (이 프로세스 적용)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**작성일**: 2025-01-20
|
||
|
|
**버전**: 1.0
|
||
|
|
**기술 스택**: Laravel 12 + MySQL 8.0 + HTMX + DaisyUI
|
||
|
|
**목표**: API 우선, 단순함, 수정 용이성
|