diff --git a/plans/mng-menu-system-plan.md b/plans/mng-menu-system-plan.md index 3ea6f86..6b696aa 100644 --- a/plans/mng-menu-system-plan.md +++ b/plans/mng-menu-system-plan.md @@ -1,7 +1,7 @@ # MNG 메뉴 시스템: DB 기반 동적 메뉴 전환 계획 > 작성일: 2025-12-16 -> 수정일: 2025-12-16 (Laravel 12 미들웨어 등록 방식, 확장 테이블 설계 반영) +> 수정일: 2025-12-16 (Laravel 12 미들웨어, JSON options 컬럼 방식으로 변경) > 목적: mng 사이드바를 DB 기반으로 전환하여 직원별 권한에 따라 메뉴 동적 표시 > 선택: **Option A - DB 메뉴 기반** @@ -31,7 +31,7 @@ ┌─────────────────────────────────────────────────────────────────┐ │ 목표 mng 메뉴 시스템 │ ├─────────────────────────────────────────────────────────────────┤ -│ DB (menus + menu_mng_extensions 테이블, tenant_id=1) │ +│ DB (menus 테이블 + options JSON 컬럼, tenant_id=1) │ │ ├── 일반 메뉴 (역할/부서/개인 권한으로 제어) │ │ ├── 개발도구 메뉴 (슈퍼관리자 전용) │ │ └── R&D Labs 메뉴 (슈퍼관리자 전용) │ @@ -41,40 +41,54 @@ └─────────────────────────────────────────────────────────────────┘ ``` -### 1.3 DB 테이블 구조 +### 1.3 DB 설계 방식 비교 -**menus 테이블** (기존 - 수정하지 않음) +| 방식 | 장점 | 단점 | +|------|------|------| +| **별도 테이블** | menus 완전 무수정 | JOIN 필요, 테이블 관리 | +| **JSON 컬럼** ✅ | JOIN 불필요, 유연한 확장, 범용 | menus 수정 (안전) | + +**선택: JSON 컬럼 방식** +- nullable JSON 컬럼 추가는 기존 코드에 영향 없음 +- Laravel의 JSON 캐스팅으로 편리한 사용 +- 나중에 API, React에서도 활용 가능 + +### 1.4 DB 테이블 구조 + +**menus 테이블** (기존 + options 컬럼 추가) ``` id, tenant_id, parent_id, global_menu_id name, url, icon, sort_order is_active, hidden, is_customized, is_external, external_url +options (JSON, nullable) ← 신규 추가 created_by, updated_by, deleted_by, created_at, updated_at, deleted_at ``` -> ⚠️ **중요**: menus 테이블은 API, React 등 28개 이상 파일에서 사용 중이므로 직접 수정하지 않음 - -**menu_mng_extensions 테이블** (신규 - mng 전용 확장) -```sql -CREATE TABLE menu_mng_extensions ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - menu_id BIGINT UNSIGNED NOT NULL, -- menus 테이블 FK - menu_type ENUM('normal', 'dev_tool', 'lab') DEFAULT 'normal', - route_name VARCHAR(100) NULL, -- Laravel 라우트 이름 - section ENUM('main', 'dev_tools', 'labs') DEFAULT 'main', - lab_tab CHAR(1) NULL, -- R&D Labs용: 's', 'a', 'm' - blade_component VARCHAR(100) NULL, -- 커스텀 Blade 컴포넌트 - css_class VARCHAR(100) NULL, -- 추가 CSS 클래스 - requires_super_admin BOOLEAN DEFAULT FALSE, -- 슈퍼관리자 전용 - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - FOREIGN KEY (menu_id) REFERENCES menus(id) ON DELETE CASCADE, - INDEX idx_menu_id (menu_id), - INDEX idx_section (section), - INDEX idx_menu_type (menu_type) -); +**options JSON 구조** (범용 설계) +```json +{ + "route_name": "dashboard", + "section": "main", + "menu_type": "normal", + "requires_role": null, + "blade_component": null, + "css_class": null, + "meta": {} +} ``` +| 필드 | 타입 | 설명 | 예시 | +|------|------|------|------| +| `route_name` | string | Laravel 라우트 이름 | `"pm.projects.index"` | +| `section` | string | 메뉴 섹션 위치 | `"main"`, `"tools"`, `"labs"` | +| `menu_type` | string | 메뉴 유형 | `"normal"`, `"tool"`, `"lab"` | +| `requires_role` | string | 필요 역할 | `"super_admin"`, `null` | +| `blade_component` | string | 커스텀 컴포넌트 | `"menus.custom-item"` | +| `css_class` | string | 추가 CSS 클래스 | `"text-red-500"` | +| `meta` | object | 앱별 추가 데이터 | `{"tab": "s"}` | + +> **범용 설계 원칙**: mng 고유 필드는 `meta`에 저장, 공통 필드만 최상위에 배치 + **permissions 테이블** (Spatie) ``` id, tenant_id, name, guard_name, created_at, updated_at @@ -109,8 +123,8 @@ menu:{menu_id}.delete # 삭제 권한 | 메뉴 유형 | 권한 처리 | |-----------|----------| | 일반 메뉴 | 역할/부서/개인 권한으로 제어 | -| 개발도구 | `is_super_admin` 체크 또는 특별 권한 | -| R&D Labs | `is_super_admin` 체크 또는 특별 권한 | +| 개발도구 | `options.requires_role = "super_admin"` | +| R&D Labs | `options.requires_role = "super_admin"` | --- @@ -118,10 +132,10 @@ menu:{menu_id}.delete # 삭제 권한 ### Phase 1: DB 스키마 및 시딩 (1-2일) -#### 3.1.1 확장 테이블 마이그레이션 +#### 3.1.1 options 컬럼 마이그레이션 ```php -// database/migrations/xxxx_create_menu_mng_extensions_table.php +// database/migrations/xxxx_add_options_to_menus_table.php id(); - $table->foreignId('menu_id') - ->constrained('menus') - ->onDelete('cascade'); - $table->enum('menu_type', ['normal', 'dev_tool', 'lab'])->default('normal'); - $table->string('route_name', 100)->nullable() - ->comment('Laravel 라우트 이름'); - $table->enum('section', ['main', 'dev_tools', 'labs'])->default('main'); - $table->char('lab_tab', 1)->nullable() - ->comment('R&D Labs용: s, a, m'); - $table->string('blade_component', 100)->nullable() - ->comment('커스텀 Blade 컴포넌트'); - $table->string('css_class', 100)->nullable() - ->comment('추가 CSS 클래스'); - $table->boolean('requires_super_admin')->default(false) - ->comment('슈퍼관리자 전용 여부'); - $table->timestamps(); - - $table->index('menu_id'); - $table->index('section'); - $table->index('menu_type'); + Schema::table('menus', function (Blueprint $table) { + $table->json('options')->nullable() + ->after('external_url') + ->comment('확장 옵션 (JSON): route_name, section, menu_type, requires_role, meta 등'); }); } public function down(): void { - Schema::dropIfExists('menu_mng_extensions'); + Schema::table('menus', function (Blueprint $table) { + $table->dropColumn('options'); + }); } }; ``` -#### 3.1.2 확장 테이블 모델 +#### 3.1.2 Menu 모델 수정 (API) ```php -// app/Models/MenuMngExtension.php +// api/app/Models/Commons/Menu.php 'boolean', + 'is_active' => 'boolean', + 'hidden' => 'boolean', + 'is_customized' => 'boolean', + 'is_external' => 'boolean', + 'options' => 'array', // 추가 ]; - public function menu(): BelongsTo + // 헬퍼 메서드 (선택적) + public function getOption(string $key, mixed $default = null): mixed { - return $this->belongsTo(Menu::class); + return data_get($this->options, $key, $default); + } + + public function getRouteName(): ?string + { + return $this->getOption('route_name'); + } + + public function getSection(): string + { + return $this->getOption('section', 'main'); + } + + public function getMenuType(): string + { + return $this->getOption('menu_type', 'normal'); + } + + public function requiresRole(): ?string + { + return $this->getOption('requires_role'); } } ``` @@ -202,13 +220,12 @@ class MenuMngExtension extends Model #### 3.1.3 mng 메뉴 시더 생성 ```php -// database/seeders/MngMenuSeeder.php +// mng/database/seeders/MngMenuSeeder.php '대시보드', 'url' => '/dashboard', 'icon' => 'home', - 'extension' => [ + 'options' => [ 'route_name' => 'dashboard', - 'menu_type' => 'normal', 'section' => 'main', + 'menu_type' => 'normal', ], ], @@ -235,22 +252,25 @@ class MngMenuSeeder extends Seeder 'name' => '프로젝트 관리', 'url' => null, 'icon' => 'folder', - 'extension' => ['menu_type' => 'normal', 'section' => 'main'], + 'options' => [ + 'section' => 'main', + 'menu_type' => 'normal', + ], 'children' => [ [ 'name' => '프로젝트 대시보드', 'url' => '/project-management', - 'extension' => ['route_name' => 'pm.index'], + 'options' => ['route_name' => 'pm.index'], ], [ 'name' => '프로젝트', 'url' => '/project-management/projects', - 'extension' => ['route_name' => 'pm.projects.index'], + 'options' => ['route_name' => 'pm.projects.index'], ], [ 'name' => '일일 스크럼', 'url' => '/daily-logs', - 'extension' => ['route_name' => 'daily-logs.index'], + 'options' => ['route_name' => 'daily-logs.index'], ], ], ], @@ -262,26 +282,26 @@ class MngMenuSeeder extends Seeder 'name' => '개발 도구', 'url' => null, 'icon' => 'cog', - 'extension' => [ - 'menu_type' => 'dev_tool', - 'section' => 'dev_tools', - 'requires_super_admin' => true, + 'options' => [ + 'section' => 'tools', + 'menu_type' => 'tool', + 'requires_role' => 'super_admin', ], 'children' => [ [ 'name' => 'API 플로우 테스터', 'url' => '/dev-tools/flow-tester', - 'extension' => [ + 'options' => [ 'route_name' => 'dev-tools.flow-tester.index', - 'requires_super_admin' => true, + 'requires_role' => 'super_admin', ], ], [ 'name' => 'API 요청 로그', 'url' => '/dev-tools/api-logs', - 'extension' => [ + 'options' => [ 'route_name' => 'dev-tools.api-logs.index', - 'requires_super_admin' => true, + 'requires_role' => 'super_admin', ], ], ], @@ -292,13 +312,22 @@ class MngMenuSeeder extends Seeder 'name' => 'R&D Labs', 'url' => null, 'icon' => 'beaker', - 'extension' => [ - 'menu_type' => 'lab', + 'options' => [ 'section' => 'labs', - 'requires_super_admin' => true, + 'menu_type' => 'lab', + 'requires_role' => 'super_admin', ], 'children' => [ - // S, A, M 탭 구조 + // 하위 메뉴들 (meta에 앱별 데이터 저장 가능) + [ + 'name' => 'S Lab', + 'url' => '/labs/s', + 'options' => [ + 'route_name' => 'labs.s.index', + 'requires_role' => 'super_admin', + 'meta' => ['tab' => 's'], + ], + ], ], ], ]; @@ -309,9 +338,8 @@ class MngMenuSeeder extends Seeder private function seedMenus(int $tenantId, array $menus, ?int $parentId = null): void { foreach ($menus as $index => $menuData) { - $extension = $menuData['extension'] ?? []; $children = $menuData['children'] ?? []; - unset($menuData['extension'], $menuData['children']); + unset($menuData['children']); // 메뉴 생성 $menu = Menu::create([ @@ -322,16 +350,9 @@ class MngMenuSeeder extends Seeder 'icon' => $menuData['icon'] ?? null, 'sort_order' => $index, 'is_active' => true, + 'options' => $menuData['options'] ?? null, ]); - // 확장 데이터 생성 - if (!empty($extension)) { - MenuMngExtension::create([ - 'menu_id' => $menu->id, - ...$extension, - ]); - } - // 자식 메뉴 재귀 처리 if (!empty($children)) { $this->seedMenus($tenantId, $children, $menu->id); @@ -346,7 +367,7 @@ class MngMenuSeeder extends Seeder #### 3.2.1 SidebarMenuService 생성 ```php -// app/Services/SidebarMenuService.php +// mng/app/Services/SidebarMenuService.php user(); $tenantId = session('selected_tenant_id', 1); - // 1. 테넌트의 모든 활성 메뉴 + 확장 데이터 조회 (LEFT JOIN) - $allMenus = Menu::where('menus.tenant_id', $tenantId) - ->where('menus.is_active', true) - ->where('menus.hidden', false) - ->leftJoin('menu_mng_extensions', 'menus.id', '=', 'menu_mng_extensions.menu_id') - ->select( - 'menus.*', - 'menu_mng_extensions.menu_type', - 'menu_mng_extensions.route_name', - 'menu_mng_extensions.section', - 'menu_mng_extensions.lab_tab', - 'menu_mng_extensions.blade_component', - 'menu_mng_extensions.css_class', - 'menu_mng_extensions.requires_super_admin' - ) - ->orderBy('menus.sort_order') + // 1. 테넌트의 모든 활성 메뉴 조회 + $allMenus = Menu::where('tenant_id', $tenantId) + ->where('is_active', true) + ->where('hidden', false) + ->orderBy('sort_order') ->get(); // 2. 슈퍼관리자는 모든 메뉴 표시 @@ -392,16 +402,15 @@ class SidebarMenuService // 3. 일반 사용자: 권한 기반 필터링 $permittedMenuIds = $this->getPermittedMenuIds($user, $tenantId); - // 4. 개발도구/Labs 및 슈퍼관리자 전용 메뉴 제외 - $filteredMenus = $allMenus->filter(function ($menu) use ($permittedMenuIds) { - // 슈퍼관리자 전용 메뉴 제외 - if ($menu->requires_super_admin) { - return false; - } - // 개발도구/Labs는 일반 사용자에게 표시 안함 - if (in_array($menu->menu_type, ['dev_tool', 'lab'])) { + // 4. 역할 필요 메뉴 및 특수 메뉴 제외 + $filteredMenus = $allMenus->filter(function ($menu) use ($permittedMenuIds, $user) { + // requires_role 체크 + $requiredRole = $menu->getOption('requires_role'); + if ($requiredRole && !$this->hasRole($user, $requiredRole)) { return false; } + + // 권한 체크 return in_array($menu->id, $permittedMenuIds); }); @@ -409,19 +418,30 @@ class SidebarMenuService } /** - * 섹션별 메뉴 조회 (main, dev_tools, labs) + * 섹션별 메뉴 조회 (main, tools, labs) */ public function getMenusBySection(?User $user = null): array { $menuTree = $this->getUserMenuTree($user); return [ - 'main' => $menuTree->filter(fn($m) => ($m->section ?? 'main') === 'main'), - 'dev_tools' => $menuTree->filter(fn($m) => ($m->section ?? 'main') === 'dev_tools'), - 'labs' => $menuTree->filter(fn($m) => ($m->section ?? 'main') === 'labs'), + 'main' => $menuTree->filter(fn($m) => $m->getSection() === 'main')->values(), + 'tools' => $menuTree->filter(fn($m) => $m->getSection() === 'tools')->values(), + 'labs' => $menuTree->filter(fn($m) => $m->getSection() === 'labs')->values(), ]; } + /** + * 역할 확인 + */ + private function hasRole(User $user, string $role): bool + { + return match ($role) { + 'super_admin' => $user->is_super_admin, + default => $user->hasRole($role), + }; + } + /** * 사용자가 접근 가능한 메뉴 ID 목록 조회 * 우선순위: 개인 DENY > 개인 ALLOW > 부서 DENY > 부서 ALLOW > 역할 @@ -444,18 +464,19 @@ class SidebarMenuService private function getRoleMenuPermissions(User $user, int $tenantId): array { // menu:*.view 형식의 권한에서 메뉴 ID 추출 - $permissions = $user->getPermissionsViaRoles() + return $user->getPermissionsViaRoles() ->filter(fn($p) => str_starts_with($p->name, 'menu:') && str_ends_with($p->name, '.view')) ->pluck('name') ->map(fn($name) => (int) explode('.', explode(':', $name)[1])[0]) ->toArray(); - - return $permissions; } private function getDepartmentMenuPermissions(User $user, int $tenantId): array { - // 부서 권한 조회 로직 (department_permissions 테이블) + if (!$user->department_id) { + return []; + } + return DB::table('department_permissions') ->where('department_id', $user->department_id) ->where('permission_id', 'LIKE', 'menu:%') @@ -468,7 +489,6 @@ class SidebarMenuService private function getUserMenuOverrides(User $user, int $tenantId): array { - // 개인 권한 오버라이드 조회 (user_permission_overrides 테이블) return DB::table('user_permission_overrides') ->where('user_id', $user->id) ->where('permission_id', 'LIKE', 'menu:%') @@ -495,14 +515,14 @@ class SidebarMenuService if ($user[$menuId]) { $permitted[] = $menuId; } - continue; // 개인 설정이 있으면 다른 건 무시 + continue; } if (isset($dept[$menuId])) { if ($dept[$menuId]) { $permitted[] = $menuId; } - continue; // 부서 설정이 있으면 역할은 무시 + continue; } if (in_array($menuId, $role)) { @@ -560,7 +580,7 @@ resources/views/ #### 3.3.3 ViewServiceProvider에서 메뉴 공유 ```php -// app/Providers/ViewServiceProvider.php +// mng/app/Providers/ViewServiceProvider.php with([ 'mainMenus' => $menusBySection['main'], - 'devToolsMenus' => $menusBySection['dev_tools'], + 'toolsMenus' => $menusBySection['tools'], 'labsMenus' => $menusBySection['labs'], ]); }); @@ -592,13 +612,12 @@ class ViewServiceProvider extends ServiceProvider #### 3.4.1 메뉴 권한 체크 미들웨어 ```php -// app/Http/Middleware/CheckMenuPermission.php +// mng/app/Http/Middleware/CheckMenuPermission.php route()->getName(); - $extension = MenuMngExtension::where('route_name', $routeName)->first(); + $menu = Menu::where('tenant_id', session('selected_tenant_id', 1)) + ->whereJsonContains('options->route_name', $routeName) + ->first(); - if (!$extension) { + if (!$menu) { return $next($request); // 메뉴 등록 안 된 라우트는 패스 } - // 슈퍼관리자 전용 메뉴 체크 - if ($extension->requires_super_admin) { - abort(403, '슈퍼관리자만 접근 가능합니다.'); + // requires_role 체크 + $requiredRole = $menu->getOption('requires_role'); + if ($requiredRole) { + if ($requiredRole === 'super_admin' && !$user->is_super_admin) { + abort(403, '슈퍼관리자만 접근 가능합니다.'); + } + if ($requiredRole !== 'super_admin' && !$user->hasRole($requiredRole)) { + abort(403, '접근 권한이 없습니다.'); + } } - $permissionName = $permission ?? "menu:{$extension->menu_id}.view"; + $permissionName = $permission ?? "menu:{$menu->id}.view"; - if (!$this->hasMenuPermission($user, $extension->menu_id, $permissionName)) { + if (!$user->can($permissionName)) { abort(403, '접근 권한이 없습니다.'); } return $next($request); } - - private function hasMenuPermission($user, int $menuId, string $permissionName): bool - { - // SidebarMenuService와 동일한 권한 체크 로직 사용 - // 또는 Spatie Permission 직접 체크 - return $user->can($permissionName); - } } ``` #### 3.4.2 미들웨어 등록 (Laravel 12 방식) ```php -// bootstrap/app.php +// mng/bootstrap/app.php withMiddleware(function (Middleware $middleware): void { - // 기존 미들웨어 $middleware->alias([ 'hq.member' => \App\Http\Middleware\EnsureHQMember::class, 'super.admin' => \App\Http\Middleware\EnsureSuperAdmin::class, @@ -692,7 +711,7 @@ Route::middleware(['auth', 'hq.member', 'menu.permission'])->group(function () { ``` Phase 1: 준비 (하드코딩 + DB 병행) -├── DB에 menu_mng_extensions 테이블 생성 +├── menus 테이블에 options 컬럼 추가 ├── mng 메뉴 시딩 ├── SidebarMenuService 개발 └── 기존 sidebar.blade.php 유지 @@ -732,24 +751,24 @@ Phase 4: 안정화 | 파일 | 설명 | |------|------| -| `database/migrations/xxxx_create_menu_mng_extensions_table.php` | mng 확장 테이블 생성 | -| `app/Models/MenuMngExtension.php` | 확장 테이블 모델 | -| `database/seeders/MngMenuSeeder.php` | mng 메뉴 시더 | -| `database/seeders/MngMenuPermissionSeeder.php` | mng 메뉴 권한 시더 | -| `app/Services/SidebarMenuService.php` | 사용자별 메뉴 조회 | -| `app/Http/Middleware/CheckMenuPermission.php` | 메뉴 권한 미들웨어 | -| `app/Providers/ViewServiceProvider.php` | 뷰 컴포저 | -| `resources/views/partials/sidebar-dynamic.blade.php` | 동적 사이드바 | -| `resources/views/components/sidebar/*.blade.php` | 사이드바 컴포넌트들 | +| `api/database/migrations/xxxx_add_options_to_menus.php` | options 컬럼 추가 | +| `mng/database/seeders/MngMenuSeeder.php` | mng 메뉴 시더 | +| `mng/database/seeders/MngMenuPermissionSeeder.php` | mng 메뉴 권한 시더 | +| `mng/app/Services/SidebarMenuService.php` | 사용자별 메뉴 조회 | +| `mng/app/Http/Middleware/CheckMenuPermission.php` | 메뉴 권한 미들웨어 | +| `mng/app/Providers/ViewServiceProvider.php` | 뷰 컴포저 | +| `mng/resources/views/partials/sidebar-dynamic.blade.php` | 동적 사이드바 | +| `mng/resources/views/components/sidebar/*.blade.php` | 사이드바 컴포넌트들 | ### 수정 | 파일 | 변경 내용 | |------|----------| -| `bootstrap/app.php` | CheckMenuPermission 미들웨어 등록 | -| `config/app.php` | mng_dynamic_sidebar 설정 추가 | -| `routes/web.php` | 미들웨어 적용 | -| `resources/views/partials/sidebar.blade.php` | 조건부 렌더링 | +| `api/app/Models/Commons/Menu.php` | options 캐스팅 + 헬퍼 메서드 | +| `mng/bootstrap/app.php` | CheckMenuPermission 미들웨어 등록 | +| `mng/config/app.php` | mng_dynamic_sidebar 설정 추가 | +| `mng/routes/web.php` | 미들웨어 적용 | +| `mng/resources/views/partials/sidebar.blade.php` | 조건부 렌더링 | --- @@ -776,9 +795,10 @@ Phase 4: 안정화 ### Phase 1 완료 조건 -- [ ] 마이그레이션 실행 성공 (menu_mng_extensions 테이블 생성) +- [ ] 마이그레이션 실행 성공 (options 컬럼 추가) - [ ] mng 메뉴 시더 실행 성공 - [ ] DB에 모든 mng 메뉴 존재 확인 +- [ ] 기존 API/React 영향 없음 확인 ### Phase 2 완료 조건 @@ -848,10 +868,11 @@ AuditLog::create([ 이 계획을 승인하시면 다음 순서로 진행합니다: 1. **현재 mng 메뉴 전체 목록 정리** (그룹/항목/라우트/아이콘) -2. **마이그레이션 및 시더 작성** (menu_mng_extensions 테이블) -3. **SidebarMenuService 개발** -4. **동적 사이드바 컴포넌트 개발** -5. **권한 미들웨어 적용** -6. **테스트 및 전환** +2. **마이그레이션 작성** (menus.options 컬럼) +3. **Menu 모델 수정** (options 캐스팅) +4. **SidebarMenuService 개발** +5. **동적 사이드바 컴포넌트 개발** +6. **권한 미들웨어 적용** +7. **테스트 및 전환** 진행하시겠습니까? diff --git a/projects/migration-5130-mng/MIGRATION_TRACKER.md b/projects/migration-5130-mng/MIGRATION_TRACKER.md index bb878c4..919d36e 100644 --- a/projects/migration-5130-mng/MIGRATION_TRACKER.md +++ b/projects/migration-5130-mng/MIGRATION_TRACKER.md @@ -1,7 +1,7 @@ # 5130 → MNG 마이그레이션 작업 추적 > **시작일**: 2025-12-16 -> **현재 상태**: ✅ Phase 1 완료 +> **현재 상태**: ✅ Phase 2 레이아웃 변환 완료 > **마지막 업데이트**: 2025-12-16 --- @@ -11,7 +11,7 @@ | Phase | 상태 | 완료 | 전체 | 진행률 | |-------|------|------|------|--------| | Phase 1: 레이아웃 변환 | ✅ 완료 | 13 | 13 | 100% | -| Phase 2: AI 기능 구현 | ⏳ 대기 | 0 | 10 | 0% | +| Phase 2: AI 기능 구현 | 🔄 레이아웃 완료 | 0 | 10 | 0% | | Phase 3: Management 구현 | ⏳ 대기 | 0 | 11 | 0% | | Phase 4: Strategy placeholder | ⏳ 대기 | 0 | 3 | 0% | | **전체** | | **14** | **38** | **36.8%** | @@ -58,21 +58,21 @@ ## 📋 Phase 2: AI 기능 구현 (10개) > **작업 유형**: placeholder → 전체 구현 -> **예상 시간**: 각 2~8시간 +> **레이아웃 변환**: ✅ 완료 (2025-12-16) > **우선순위**: 🟡 중간 -| # | 파일명 | 메뉴명 | 상태 | 작업일 | 비고 | -|---|--------|--------|:----:|--------|------| -| 1 | `web-recording.blade.php` | 웹 녹음 AI 요약 | ⬜ | - | | -| 2 | `meeting-summary.blade.php` | 회의록 AI 요약 | ⬜ | - | | -| 3 | `work-memo-summary.blade.php` | 업무협의록 AI 요약 | ⬜ | - | | -| 4 | `operator-chatbot.blade.php` | 운영자용 챗봇 | ⬜ | - | | -| 5 | `vertex-rag.blade.php` | Vertex RAG 챗봇 | ⬜ | - | | -| 6 | `tenant-knowledge.blade.php` | 테넌트 지식 업로드 | ⬜ | - | | -| 7 | `tenant-chatbot.blade.php` | 테넌트 챗봇 | ⬜ | - | | -| 8 | `sam-ai-alarm.blade.php` | SAM AI 알람음 제작 | ⬜ | - | | -| 9 | `gps-attendance.blade.php` | GPS 출퇴근 관리 | ⬜ | - | | -| 10 | `company-overview.blade.php` | 기업개황 조회 | ⬜ | - | | +| # | 파일명 | 메뉴명 | 레이아웃 | 기능구현 | 비고 | +|---|--------|--------|:----:|:----:|------| +| 1 | `web-recording.blade.php` | 웹 녹음 AI 요약 | ✅ | ⬜ | | +| 2 | `meeting-summary.blade.php` | 회의록 AI 요약 | ✅ | ⬜ | | +| 3 | `work-memo-summary.blade.php` | 업무협의록 AI 요약 | ✅ | ⬜ | | +| 4 | `operator-chatbot.blade.php` | 운영자용 챗봇 | ✅ | ⬜ | | +| 5 | `vertex-rag.blade.php` | Vertex RAG 챗봇 | ✅ | ⬜ | | +| 6 | `tenant-knowledge.blade.php` | 테넌트 지식 업로드 | ✅ | ⬜ | | +| 7 | `tenant-chatbot.blade.php` | 테넌트 챗봇 | ✅ | ⬜ | | +| 8 | `sam-ai-alarm.blade.php` | SAM AI 알람음 제작 | ✅ | ⬜ | | +| 9 | `gps-attendance.blade.php` | GPS 출퇴근 관리 | ✅ | ⬜ | | +| 10 | `company-overview.blade.php` | 기업개황 조회 | ✅ | ⬜ | | --- @@ -159,6 +159,7 @@ | 날짜 | 내용 | 커밋 | |------|------|------| +| 2025-12-16 | Phase 2 레이아웃 변환 - 10개 AI 페이지 | mng: (pending) | | 2025-12-16 | Phase 1 완료 - 13개 파일 레이아웃 변환 | mng: `27052af` | | 2025-12-16 | 작업 추적 문서 생성 | docs: `5e6508c` |