- 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 아키텍처)
557 lines
16 KiB
Markdown
557 lines
16 KiB
Markdown
# 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 화면만)
|
|
|
|
```php
|
|
// 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 (실제 데이터 처리)
|
|
|
|
```php
|
|
// 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)
|
|
|
|
```bash
|
|
# 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 생성
|
|
|
|
```php
|
|
// 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 생성
|
|
|
|
```php
|
|
// 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 화면만)
|
|
|
|
```php
|
|
// 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 호출)
|
|
|
|
```blade
|
|
{{-- 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 응답)
|
|
|
|
```blade
|
|
{{-- 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일
|
|
|
|
**구현 범위:**
|
|
- [x] 모델 복사 (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)
|
|
```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 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%)
|
|
|
|
### 공통 작업
|
|
- [x] Admin 모델 분석 완료
|
|
- [x] 마이그레이션 계획 수립
|
|
- [ ] HTMX 환경 구축
|
|
- [ ] DaisyUI 컴포넌트 확정
|
|
- [ ] API 응답 형식 표준화
|
|
|
|
---
|
|
|
|
## 🔗 관련 문서
|
|
|
|
- **[MNG_PROJECT_PLAN.md](../../claudedocs/mng/MNG_PROJECT_PLAN.md)** - 전체 프로젝트 계획
|
|
- **[DEV_PROCESS.md](../../claudedocs/mng/DEV_PROCESS.md)** - 개발 프로세스 (HTMX 패턴)
|
|
- **[mng/docs/INDEX.md](./INDEX.md)** - MNG 프로젝트 문서
|
|
- **[CLAUDE.md](../../CLAUDE.md)** - SAM 프로젝트 가이드
|
|
|
|
---
|
|
|
|
**최종 업데이트**: 2025-11-21
|
|
**버전**: 1.0
|
|
**다음 단계**: Phase 4-1 테넌트 관리 구현 시작 |