Files
sam-manage/docs/MIGRATION_PLAN.md
hskwon 575e9df431 feat: Phase 4-1 테넌트 관리 백엔드 구현
- TenantService 생성 (CRUD, 통계, 복원/영구삭제)
- API Controller 구현 (HTMX 요청 감지, HTML/JSON 이중 응답)
- FormRequest 검증 (StoreTenantRequest, UpdateTenantRequest)
- Tenant 모델 확장 (17개 필드, 관계 설정, accessor)
- Department, Menu, Role 모델 복사 (admin → mng)
- Web Controller 수정 (index/create/edit 화면)
- MIGRATION_PLAN.md 작성 (HTMX + API 아키텍처)
2025-11-21 14:46:13 +09:00

16 KiB

Admin → MNG 마이그레이션 계획

📌 Admin 시스템 관리 메뉴 11개를 MNG로 마이그레이션

작성일: 2025-11-21 상태: Phase 4 준비 중


📋 마이그레이션 개요

목표

Admin(Filament v4)의 시스템 관리 메뉴 11개를 MNG(Plain Laravel)로 이식하여 수정 용이한 관리자 패널 구축

핵심 전략

┌─────────────────────────────────────────┐
│  Blade 화면 (Web Routes)               │
│  - 화면만 담당, 데이터 처리 없음         │
│  ↓ HTMX 호출 (hx-get, hx-post 등)      │
│  ↓                                      │
│  API Routes (/api/admin/*)             │
│  - 실제 데이터 CRUD 처리                │
│  - HTMX 요청 시 HTML 반환              │
│  - 일반 요청 시 JSON 반환              │
│  ↓                                      │
│  API Controller → Service → Model      │
│  ↓                                      │
│  MySQL (admin/api와 DB 공유)            │
└─────────────────────────────────────────┘

기술 스택

  • 프론트엔드: Blade + HTMX + DaisyUI + Tailwind CSS
  • 백엔드: Laravel 12 + PHP 8.4 + Sanctum
  • 인터랙션: HTMX (Alpine.js 제거)

🎯 마이그레이션 대상 (11개 메뉴)

Admin 시스템 관리 메뉴 분석

# 메뉴명 Resource 파일 모델 복잡도 우선순위
1 테넌트 TenantResource.php Tenant 1
2 사용자 UserResource.php User 2
3 메뉴 MenuResource.php Menu 3
4 역할 RoleResource.php Role 4
5 부서 DepartmentResource.php Department 5
6 권한 PermissionResource.php Permission 6
7 역할 권한 관리 RolePermissionsResource.php - 7
8 부서 권한 관리 DepartmentPermissionsResource.php - 8
9 개인 권한 관리 UserPermissionsResource.php - 9
10 권한 분석 PermissionAnalysisResource.php - 10
11 삭제된 데이터 백업 ArchivedRecordResource.php ArchivedRecord 11

복잡도:

  • 단순 CRUD
  • CRUD + 관계
  • CRUD + 복잡한 관계 + 커스텀 UI
  • 읽기 전용 + 복잡한 쿼리 + 매트릭스 UI

🏗️ 라우트 구조

Web Routes (Blade 화면만)

// routes/web.php
Route::middleware(['auth:sanctum'])->group(function () {
    // 테넌트 관리
    Route::get('/tenants', [TenantController::class, 'index'])->name('tenants.index');
    Route::get('/tenants/create', [TenantController::class, 'create'])->name('tenants.create');
    Route::get('/tenants/{tenant}/edit', [TenantController::class, 'edit'])->name('tenants.edit');

    // 사용자 관리
    Route::get('/users', [UserController::class, 'index'])->name('users.index');
    Route::get('/users/create', [UserController::class, 'create'])->name('users.create');
    Route::get('/users/{user}/edit', [UserController::class, 'edit'])->name('users.edit');

    // ... 나머지 메뉴
});

API Routes (실제 데이터 처리)

// routes/api.php
Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () {
    // 테넌트 API
    Route::apiResource('tenants', Api\Admin\TenantController::class);

    // 사용자 API
    Route::apiResource('users', Api\Admin\UserController::class);

    // 권한 관리 API (Custom)
    Route::prefix('permissions')->group(function () {
        Route::get('/role/{role}', [Api\Admin\RolePermissionController::class, 'show']);
        Route::post('/role/{role}', [Api\Admin\RolePermissionController::class, 'update']);

        Route::get('/department/{department}', [Api\Admin\DepartmentPermissionController::class, 'show']);
        Route::post('/department/{department}', [Api\Admin\DepartmentPermissionController::class, 'update']);

        Route::get('/user/{user}', [Api\Admin\UserPermissionController::class, 'show']);
        Route::post('/user/{user}', [Api\Admin\UserPermissionController::class, 'update']);

        Route::get('/analysis', [Api\Admin\PermissionAnalysisController::class, 'index']);
    });

    // ... 나머지 메뉴
});

📐 표준 개발 프로세스

1. 모델 복사 (Admin → MNG)

# 1. 모델 복사
cp admin/app/Models/Tenants/Tenant.php mng/app/Models/Tenant.php
cp admin/app/Models/Members/User.php mng/app/Models/User.php
# ...

# 2. Filament 코드 제거
# - form(), table(), getNavigationLabel() 등 제거
# - 순수 Eloquent 관계만 유지

# 3. Traits 복사
cp admin/app/Traits/BelongsToTenant.php mng/app/Traits/
cp admin/app/Traits/ModelTrait.php mng/app/Traits/

2. Service Layer 생성

// mng/app/Services/TenantService.php
namespace App\Services;

use App\Models\Tenant;
use Illuminate\Pagination\LengthAwarePaginator;

class TenantService
{
    /**
     * 테넌트 목록 조회 (검색, 필터, 페이징)
     */
    public function getTenants(array $filters = []): LengthAwarePaginator
    {
        $query = Tenant::query();

        // 검색
        if (!empty($filters['search'])) {
            $query->where('company_name', 'like', "%{$filters['search']}%");
        }

        // 상태 필터
        if (!empty($filters['status'])) {
            $query->where('tenant_st_code', $filters['status']);
        }

        return $query->paginate(20);
    }

    /**
     * 테넌트 생성
     */
    public function createTenant(array $data): Tenant
    {
        return Tenant::create($data);
    }

    // update(), delete() ...
}

3. API Controller 생성

// mng/app/Http/Controllers/Api/Admin/TenantController.php
namespace App\Http\Controllers\Api\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\Tenant\StoreTenantRequest;
use App\Services\TenantService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class TenantController extends Controller
{
    public function __construct(
        private TenantService $tenantService
    ) {}

    /**
     * 테넌트 목록 (API)
     * GET /api/admin/tenants
     */
    public function index(Request $request): JsonResponse
    {
        $tenants = $this->tenantService->getTenants($request->all());

        // HTMX 요청 시 HTML 반환
        if ($request->header('HX-Request')) {
            return response()->view('tenants.partials.table', compact('tenants'));
        }

        // 일반 요청 시 JSON 반환
        return response()->json([
            'success' => true,
            'data' => $tenants->items(),
            'meta' => [
                'current_page' => $tenants->currentPage(),
                'total' => $tenants->total(),
            ],
        ]);
    }

    /**
     * 테넌트 생성 (API)
     * POST /api/admin/tenants
     */
    public function store(StoreTenantRequest $request): JsonResponse
    {
        $tenant = $this->tenantService->createTenant($request->validated());

        return response()->json([
            'success' => true,
            'data' => $tenant,
            'message' => 'tenants.created',
        ], 201);
    }

    // update(), destroy() ...
}

4. Web Controller 생성 (Blade 화면만)

// mng/app/Http/Controllers/TenantController.php
namespace App\Http\Controllers;

class TenantController extends Controller
{
    /**
     * 테넌트 목록 화면
     * GET /tenants
     */
    public function index()
    {
        return view('tenants.index');
    }

    /**
     * 테넌트 생성 화면
     * GET /tenants/create
     */
    public function create()
    {
        return view('tenants.create');
    }

    /**
     * 테넌트 수정 화면
     * GET /tenants/{tenant}/edit
     */
    public function edit($id)
    {
        return view('tenants.edit', compact('id'));
    }
}

5. Blade 뷰 생성 (HTMX 호출)

{{-- resources/views/tenants/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="{{ route('tenants.create') }}" class="btn btn-primary">테넌트 추가</a>
    </div>

    {{-- 검색/필터 --}}
    <div class="card bg-white shadow-xl">
        <div class="card-body">
            <form hx-get="/api/admin/tenants"
                  hx-target="#tenant-table"
                  hx-trigger="submit">
                <div class="grid grid-cols-3 gap-4">
                    <input type="text" name="search"
                           placeholder="회사명 검색"
                           class="input input-bordered" />
                    <select name="status" class="select select-bordered">
                        <option value="">전체 상태</option>
                        <option value="trial">트라이얼</option>
                        <option value="active">활성</option>
                    </select>
                    <button type="submit" class="btn btn-primary">검색</button>
                </div>
            </form>
        </div>
    </div>

    {{-- 테이블 영역 (HTMX로 로드) --}}
    <div id="tenant-table"
         hx-get="/api/admin/tenants"
         hx-trigger="load">
        <div class="flex justify-center p-8">
            <span class="loading loading-spinner loading-lg"></span>
        </div>
    </div>
</div>
@endsection

6. 부분 템플릿 생성 (HTMX 응답)

{{-- resources/views/tenants/partials/table.blade.php --}}
<div class="card bg-white 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>
                    </tr>
                </thead>
                <tbody>
                    @foreach($tenants as $tenant)
                    <tr>
                        <td>{{ $tenant->id }}</td>
                        <td>{{ $tenant->company_name }}</td>
                        <td>{{ $tenant->email }}</td>
                        <td>
                            <span class="badge badge-success">
                                {{ $tenant->tenant_st_code }}
                            </span>
                        </td>
                        <td>
                            <a href="{{ route('tenants.edit', $tenant->id) }}"
                               class="btn btn-sm">수정</a>
                            <button hx-delete="/api/admin/tenants/{{ $tenant->id }}"
                                    hx-confirm="정말 삭제하시겠습니까?"
                                    hx-target="closest tr"
                                    hx-swap="outerHTML swap:1s"
                                    class="btn btn-sm btn-error">
                                삭제
                            </button>
                        </td>
                    </tr>
                    @endforeach
                </tbody>
            </table>
        </div>

        {{-- 페이징 (HTMX) --}}
        <div class="flex justify-center mt-4">
            @if($tenants->hasPages())
                <div class="btn-group">
                    @for($page = 1; $page <= $tenants->lastPage(); $page++)
                        <button hx-get="/api/admin/tenants?page={{ $page }}"
                                hx-target="#tenant-table"
                                class="btn btn-sm {{ $page == $tenants->currentPage() ? 'btn-active' : '' }}">
                            {{ $page }}
                        </button>
                    @endfor
                </div>
            @endif
        </div>
    </div>
</div>

📋 Phase 4 세부 계획

Phase 4-1: 테넌트 관리 (우선순위 1)

예상 기간: 2-3일

구현 범위:

  • 모델 복사 (Tenant.php)
  • TenantService 생성
  • API Controller (Api\Admin\TenantController)
  • Web Controller (TenantController)
  • FormRequest (StoreTenantRequest, UpdateTenantRequest)
  • Blade 뷰 (index, create, edit)
  • 부분 템플릿 (partials/table, partials/form)
  • Feature Test

Phase 4-2: 사용자 관리 (우선순위 2)

예상 기간: 3-4일

구현 범위:

  • 모델 복사 (User.php)
  • UserService 생성
  • API Controller
  • Web Controller
  • FormRequest
  • Blade 뷰
  • 탭 UI (HTMX로 구현)
    • Tenants 탭
    • Departments 탭
    • Roles 탭
    • Permissions 탭
  • Feature Test

Phase 4-3: 메뉴/역할/부서 관리 (우선순위 3-5)

예상 기간: 5-6일

구현 범위:

  • Menu CRUD + 트리 구조 UI
  • Role CRUD
  • Department CRUD + 트리 구조 UI

Phase 4-4: 권한 관리 (우선순위 6-9)

예상 기간: 7-8일

구현 범위:

  • Permission CRUD
  • 역할 권한 관리 (체크박스 매트릭스)
  • 부서 권한 관리 (체크박스 매트릭스)
  • 개인 권한 관리 (체크박스 매트릭스)

Phase 4-5: 권한 분석/백업 (우선순위 10-11)

예상 기간: 3-4일

구현 범위:

  • 권한 분석 (읽기 전용 매트릭스)
  • 삭제 데이터 백업 조회/복원

🔧 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 class="tabs tabs-boxed">
    <button class="tab tab-active"
            hx-get="/api/admin/users/{{ $user->id }}/tenants"
            hx-target="#tab-content">
        테넌트
    </button>
    <button class="tab"
            hx-get="/api/admin/users/{{ $user->id }}/roles"
            hx-target="#tab-content">
        역할
    </button>
</div>

<div id="tab-content" class="mt-4">
    <!-- HTMX로 로드된 탭 내용 -->
</div>

📊 진행 상황 체크리스트

전체 진행률

  • Phase 4-1: 테넌트 관리 (0%)
  • Phase 4-2: 사용자 관리 (0%)
  • Phase 4-3: 메뉴/역할/부서 (0%)
  • Phase 4-4: 권한 관리 (0%)
  • Phase 4-5: 권한 분석/백업 (0%)

공통 작업

  • Admin 모델 분석 완료
  • 마이그레이션 계획 수립
  • HTMX 환경 구축
  • DaisyUI 컴포넌트 확정
  • API 응답 형식 표준화

🔗 관련 문서


최종 업데이트: 2025-11-21 버전: 1.0 다음 단계: Phase 4-1 테넌트 관리 구현 시작