- 개발 단계별 문서 추가 (00_OVERVIEW ~ 06_PHASE) - 기술 표준 문서 추가 (99_TECHNICAL_STANDARDS) - 개발 프로세스 및 패턴 문서 추가 - API_FLOW_TESTER_DESIGN, DEV_PROCESS - HTMX_API_PATTERN, LAYOUT_PATTERN - SETUP_GUIDE, MNG_PROJECT_PLAN - 프로젝트 관리 문서 추가 (project-management/) - INDEX.md, MNG_CRITICAL_RULES.md 업데이트
20 KiB
20 KiB
MNG 프로젝트 개발 프로세스
🎯 개발 철학
API 우선 → HTMX 연동 → 단순하고 수정 용이한 코드
핵심 원칙
- API First: 모든 기능은 API로 먼저 개발
- Service-First: 비즈니스 로직은 Service에만
- HTMX Driven: JS 최소화, HTML 속성으로 인터랙션
- DaisyUI Only: 커스텀 CSS 금지, DaisyUI 클래스만 사용
📐 표준 개발 프로세스 (6단계)
Phase 0: 환경 구성 (최초 1회)
참조: claudedocs/mng/SETUP_GUIDE.md
# 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: 준비 단계
# 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단계)
# 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단계)
# 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단계)
# 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단계)
# 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단계)
# 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 통과
🔄 실전 워크플로 (스킬 활용)
신규 기능 개발 시
# 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 스킬 사용
# → 분석 → 수정 → 검증 → 정리 → 커밋
버그 수정 시
# Step 1: 문제 분석
/sc:troubleshoot "사용자 목록 페이징 안됨"
# → Root Cause 분석
# Step 2: 수정
/sc:improve "UserService 페이징 로직"
# Step 3: 테스트
/sc:test
# Step 4: 커밋
code-workflow
리팩토링 시
/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)
<div hx-get="/api/admin/users"
hx-trigger="load"
hx-target="this">
<span class="loading loading-spinner"></span>
</div>
2. 검색/필터 (Submit)
<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)
<form hx-post="/api/admin/users"
hx-target="#user-list"
hx-swap="beforeend">
<!-- 폼 필드 -->
<button class="btn btn-primary">저장</button>
</form>
4. 수정 (PUT)
<form hx-put="/api/admin/users/{{ $user->id }}"
hx-target="closest tr"
hx-swap="outerHTML">
<!-- 폼 필드 -->
<button class="btn btn-primary">수정</button>
</form>
5. 삭제 (DELETE)
<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. 무한 스크롤
<div hx-get="/api/admin/users?page=2"
hx-trigger="revealed"
hx-swap="afterend">
더보기...
</div>
7. 폴링 (자동 갱신)
<div hx-get="/api/admin/stats"
hx-trigger="every 10s"
hx-target="this">
통계: {{ $stats }}
</div>
8. 디바운싱 (입력 지연)
<input hx-get="/api/admin/users/search"
hx-trigger="keyup changed delay:500ms"
hx-target="#search-results"
name="q"
class="input input-bordered" />
🔧 개발 환경 설정
필수 패키지 설치
# 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 설정
// 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 레이아웃
<!-- 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>
📝 다음 단계
- Phase 1 시작: Laravel 프로젝트 생성 및 환경 구성
- 인증 구현: 로그인 API + Blade 화면
- 첫 기능 개발: 사용자 관리 (이 프로세스 적용)
작성일: 2025-01-20 버전: 1.0 기술 스택: Laravel 12 + MySQL 8.0 + HTMX + DaisyUI 목표: API 우선, 단순함, 수정 용이성