Files
sam-manage/docs/DEV_PROCESS.md
hskwon 76c8a94e4f docs: MNG 프로젝트 문서 정비
- 개발 단계별 문서 추가 (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 업데이트
2025-11-30 21:04:19 +09:00

20 KiB

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

# 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>

📝 다음 단계

  1. Phase 1 시작: Laravel 프로젝트 생성 및 환경 구성
  2. 인증 구현: 로그인 API + Blade 화면
  3. 첫 기능 개발: 사용자 관리 (이 프로세스 적용)

작성일: 2025-01-20 버전: 1.0 기술 스택: Laravel 12 + MySQL 8.0 + HTMX + DaisyUI 목표: API 우선, 단순함, 수정 용이성