# 메뉴 통합관리 시스템 설계서 > 작성일: 2025-12-01 > 상태: 설계 완료, 개발 대기 > 관련 문서: SAM_ERP_인사관리전자결재_Storyboard_D0.6_251201.pdf --- ## 1. 개요 ### 1.1 배경 현재 테넌트 생성 시 글로벌 메뉴(tenant_id = NULL)를 완전히 복사하여 독립적인 테넌트 메뉴를 생성합니다. 이 방식은 원본과의 연결이 없어 관리가 어렵습니다. ### 1.2 목표 - 글로벌 메뉴와 테넌트 메뉴 간의 연결(링크) 시스템 구축 - 템플릿 복사 시에도 관계 유지 - 관리자에서 메뉴 추가/삭제/동기화 관리 용이하게 ### 1.3 현재 구조 vs 목표 구조 **현재:** ``` 글로벌 메뉴 (tenant_id = NULL) ↓ 완전 복사 (독립) 테넌트 메뉴 (tenant_id = X) ❌ 원본과의 연결 없음 ``` **목표:** ``` 글로벌 메뉴 (id=10) ↓ 복사 + 링크 유지 테넌트 메뉴 (id=100, global_menu_id=10) ✅ 원본 추적 가능 ``` --- ## 2. 정책 결정 사항 | 항목 | 결정 내용 | |------|----------| | **글로벌 메뉴 삭제 시** | 테넌트 메뉴 유지 (`global_menu_id = NULL`로 변경) | | **활성 메뉴 (is_active=1)** | 새 테넌트 생성 시 자동 복사 | | **비활성 메뉴 (is_active=0)** | 테넌트가 수동으로 복제 가능 | | **숨김 메뉴 (hidden=1)** | 복사되지만 테넌트에서 메뉴 안 보임 | | **기존 데이터** | 신규 테넌트부터 적용 (기존 데이터 변경 없음) | --- ## 3. 테넌트 메뉴 기능 매트릭스 | 기능 | 설명 | API | MNG | |------|------|-----|-----| | **복제** | 글로벌 메뉴 → 테넌트 메뉴로 복제 | ✅ | ✅ | | **추가** | 테넌트 자체 메뉴 생성 (global_menu_id = NULL) | ✅ | ✅ | | **수정** | 메뉴명, URL, 아이콘, 정렬 등 수정 | ✅ | ✅ | | **삭제** | 테넌트 메뉴 삭제 (소프트) | ✅ | ✅ | | **숨김** | hidden 토글 | ✅ | ✅ | | **복원** | 삭제된 메뉴 복원 | ✅ | ✅ | | **동기화** | 글로벌 메뉴 변경사항 반영 | ✅ | ✅ | --- ## 4. DB 스키마 변경 ### 4.1 마이그레이션 ```sql -- 1. global_menu_id 컬럼 추가 ALTER TABLE menus ADD COLUMN global_menu_id BIGINT UNSIGNED NULL COMMENT '원본 글로벌 메뉴 ID (복제된 메뉴인 경우)' AFTER parent_id; -- 2. is_customized 플래그 추가 (원본 대비 수정 여부) ALTER TABLE menus ADD COLUMN is_customized TINYINT(1) NOT NULL DEFAULT 0 COMMENT '테넌트가 커스터마이징 했는지 여부' AFTER hidden; -- 3. 인덱스 추가 ALTER TABLE menus ADD INDEX menus_global_menu_id_idx (global_menu_id); ALTER TABLE menus ADD INDEX menus_tenant_global_idx (tenant_id, global_menu_id); ``` ### 4.2 변경된 테이블 구조 ``` menus ├── id (PK) ├── tenant_id (FK, NULL = 글로벌 메뉴) ├── parent_id (FK, 상위 메뉴) ├── global_menu_id (FK, 원본 글로벌 메뉴) ← 신규 ├── name ├── url ├── icon ├── sort_order ├── is_active ├── hidden ├── is_customized ← 신규 ├── is_external ├── external_url ├── created_by ├── updated_by ├── deleted_by ├── created_at ├── updated_at └── deleted_at ``` --- ## 5. 메뉴 상태 플로우 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 글로벌 메뉴 (tenant_id = NULL) │ ├─────────────────────────────────────────────────────────────────┤ │ is_active=1, hidden=0 → 새 테넌트에 자동 복사 │ │ is_active=1, hidden=1 → 복사되지만 테넌트에서 안 보임 │ │ is_active=0 → 복사 안됨, 테넌트가 수동 복제 가능 │ └─────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ 테넌트 메뉴 (tenant_id = X) │ ├─────────────────────────────────────────────────────────────────┤ │ global_menu_id = 10 → 글로벌 메뉴 #10에서 복제됨 │ │ global_menu_id = NULL → 테넌트가 직접 생성한 메뉴 │ │ is_customized = 1 → 테넌트가 내용 수정함 │ │ is_customized = 0 → 원본 그대로 │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## 6. 동기화 시스템 설계 ### 6.1 동기화 상태 분류 테넌트 메뉴의 글로벌 메뉴 대비 상태를 5가지로 분류합니다: | 상태 | 코드 | 설명 | 아이콘 | |------|------|------|--------| | **신규** | `new` | 글로벌에 있으나 테넌트에 없음 | 🆕 | | **최신** | `up_to_date` | 글로벌과 동일한 상태 | ✅ | | **업데이트 가능** | `updatable` | 글로벌이 변경됨, 동기화 가능 | 🔄 | | **커스텀** | `customized` | 테넌트가 수정함 (보호됨) | ✏️ | | **삭제됨** | `deleted` | 글로벌에서 삭제됨 | ⚠️ | ### 6.2 동기화 액션 유형 사용자 편의를 위해 4가지 동기화 액션을 제공합니다: | 액션 | 설명 | 대상 상태 | |------|------|----------| | **개별 동기화** | 선택한 메뉴만 동기화 | 모든 상태 (force 옵션) | | **신규 가져오기** | 새로운 글로벌 메뉴만 일괄 복제 | `new` | | **기존 업데이트** | 변경된 기존 메뉴만 일괄 동기화 | `updatable` (커스텀 제외) | | **선택 동기화** | 필터/선택 후 일괄 동기화 | 선택된 메뉴 | ### 6.3 커스터마이징 메뉴 보호 정책 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 커스텀 메뉴 보호 정책 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ is_customized = 1 인 메뉴: │ │ ├── 일괄 동기화 시 자동 제외 (보호됨) │ │ ├── 개별 동기화 시 경고 메시지 표시 │ │ └── force=true 옵션으로만 강제 동기화 가능 │ │ │ │ 동기화 시 is_customized 플래그: │ │ ├── 동기화 완료 → is_customized = 0 (원본 상태로) │ │ └── 테넌트 수정 → is_customized = 1 (자동 설정) │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ### 6.4 동기화 상태 판단 로직 ```php // 의사 코드 function getSyncStatus($tenantMenu, $globalMenu) { if ($globalMenu === null) { return 'deleted'; // 글로벌 메뉴 삭제됨 } if ($tenantMenu === null) { return 'new'; // 테넌트에 없음 } if ($tenantMenu->is_customized) { return 'customized'; // 테넌트가 수정함 } // 변경 여부 비교 (name, url, icon, sort_order) $hasChanges = compareMenuFields($tenantMenu, $globalMenu); if ($hasChanges) { return 'updatable'; // 동기화 가능 } return 'up_to_date'; // 최신 상태 } ``` --- ## 7. API 엔드포인트 설계 ### 7.1 글로벌 메뉴 관리 (시스템 관리자용) | Method | Endpoint | 설명 | |--------|----------|------| | GET | `/v1/admin/global-menus` | 글로벌 메뉴 목록 | | POST | `/v1/admin/global-menus` | 글로벌 메뉴 생성 | | PUT | `/v1/admin/global-menus/{id}` | 글로벌 메뉴 수정 | | DELETE | `/v1/admin/global-menus/{id}` | 글로벌 메뉴 삭제 | | POST | `/v1/admin/global-menus/{id}/sync-to-tenants` | 특정 메뉴를 모든 테넌트에 추가 | ### 7.2 테넌트 메뉴 관리 (테넌트 관리자용) | Method | Endpoint | 설명 | |--------|----------|------| | GET | `/v1/menus` | 테넌트 메뉴 목록 (기존) | | GET | `/v1/menus/available-global` | 복제 가능한 글로벌 메뉴 목록 | | POST | `/v1/menus` | 테넌트 메뉴 생성 (기존) | | POST | `/v1/menus/clone-global/{globalMenuId}` | 글로벌 메뉴 복제 | | PUT | `/v1/menus/{id}` | 테넌트 메뉴 수정 (기존) | | DELETE | `/v1/menus/{id}` | 테넌트 메뉴 삭제 (기존) | | PATCH | `/v1/menus/{id}/toggle` | 상태 토글 (기존) | | POST | `/v1/menus/{id}/restore` | 삭제된 메뉴 복원 | | POST | `/v1/menus/{id}/sync-from-global` | 글로벌 원본과 동기화 (개별) | | POST | `/v1/menus/reorder` | 메뉴 순서 변경 (기존) | ### 7.3 동기화 전용 API (테넌트 관리자용) | Method | Endpoint | 설명 | |--------|----------|------| | GET | `/v1/menus/sync-status` | 동기화 상태 목록 조회 | | POST | `/v1/menus/sync` | 선택 동기화 (menu_ids 지정) | | POST | `/v1/menus/sync-new` | 신규 글로벌 메뉴 일괄 가져오기 | | POST | `/v1/menus/sync-updates` | 기존 메뉴 일괄 업데이트 (커스텀 제외) | ### 7.4 동기화 API 상세 스펙 #### GET `/v1/menus/sync-status` 동기화 상태 목록을 조회합니다. **Query Parameters:** | 파라미터 | 타입 | 필수 | 설명 | |----------|------|------|------| | `status` | string | N | 필터: `new`, `updatable`, `up_to_date`, `customized`, `deleted` | **Response:** ```json { "success": true, "message": "message.fetched", "data": { "summary": { "new": 2, "updatable": 3, "up_to_date": 15, "customized": 1, "deleted": 1 }, "items": [ { "global_menu_id": 10, "global_name": "고객센터", "global_url": "/support", "global_icon": "headphones", "status": "new", "tenant_menu_id": null, "tenant_name": null, "is_customized": false, "changes": [] }, { "global_menu_id": 1, "global_name": "대시보드", "global_url": "/dashboard", "global_icon": "home", "status": "updatable", "tenant_menu_id": 100, "tenant_name": "대시보드", "is_customized": false, "changes": ["icon", "url"] }, { "global_menu_id": 2, "global_name": "사용자 관리", "global_url": "/users", "global_icon": "users", "status": "customized", "tenant_menu_id": 101, "tenant_name": "직원 관리", "is_customized": true, "changes": ["name"] } ] } } ``` #### POST `/v1/menus/sync` 선택한 메뉴를 동기화합니다. **Request Body:** ```json { "menu_ids": [10, 11, 12], "force": false } ``` | 파라미터 | 타입 | 필수 | 설명 | |----------|------|------|------| | `menu_ids` | array | Y | 동기화할 글로벌 메뉴 ID 목록 | | `force` | boolean | N | 커스텀 메뉴 강제 덮어쓰기 (기본: false) | **Response:** ```json { "success": true, "message": "message.menu.synced", "data": { "synced": 2, "created": 1, "skipped": 0, "details": [ { "global_menu_id": 10, "action": "created", "tenant_menu_id": 150 }, { "global_menu_id": 11, "action": "updated", "tenant_menu_id": 102 }, { "global_menu_id": 12, "action": "updated", "tenant_menu_id": 103 } ] } } ``` #### POST `/v1/menus/sync-new` 신규 글로벌 메뉴만 일괄 가져옵니다. **Request Body:** (없음) **Response:** ```json { "success": true, "message": "message.menu.new_imported", "data": { "imported": 3, "menus": [ { "global_menu_id": 10, "name": "고객센터", "tenant_menu_id": 150 }, { "global_menu_id": 11, "name": "결제내역", "tenant_menu_id": 151 }, { "global_menu_id": 12, "name": "구독관리", "tenant_menu_id": 152 } ] } } ``` #### POST `/v1/menus/sync-updates` 변경된 기존 메뉴를 일괄 업데이트합니다 (커스텀 메뉴 자동 제외). **Request Body:** (없음) **Response:** ```json { "success": true, "message": "message.menu.updates_synced", "data": { "updated": 3, "skipped_customized": 1, "details": [ { "global_menu_id": 1, "tenant_menu_id": 100, "changes": ["icon"] }, { "global_menu_id": 3, "tenant_menu_id": 102, "changes": ["url", "name"] } ], "skipped": [ { "global_menu_id": 2, "tenant_menu_id": 101, "reason": "customized" } ] } } ``` --- ## 8. 서비스 메서드 설계 ### 8.1 MenuService.php 추가 메서드 ```php /** * 복제 가능한 글로벌 메뉴 목록 * - 테넌트가 아직 복제하지 않은 글로벌 메뉴 */ public function getAvailableGlobalMenus(int $tenantId): Collection /** * 글로벌 메뉴를 테넌트로 복제 */ public function cloneFromGlobal(int $globalMenuId, int $tenantId): Menu /** * 글로벌 원본과 동기화 (이름, URL, 아이콘 등 업데이트) */ public function syncFromGlobal(int $menuId): Menu /** * 삭제된 메뉴 복원 */ public function restore(int $menuId): Menu ``` ### 8.2 GlobalMenuService.php (신규) ```php /** * 글로벌 메뉴 생성 */ public function store(array $data): Menu /** * 글로벌 메뉴 수정 */ public function update(int $id, array $data): Menu /** * 글로벌 메뉴 삭제 (연결된 테넌트 메뉴의 global_menu_id를 NULL로) */ public function destroy(int $id): bool /** * 특정 글로벌 메뉴를 모든 테넌트에 추가 */ public function syncToAllTenants(int $globalMenuId): int ``` ### 8.3 MenuSyncService.php (신규) ```php /** * 동기화 상태 목록 조회 */ public function getSyncStatus(int $tenantId, ?string $statusFilter = null): array /** * 선택 동기화 (신규 생성 또는 기존 업데이트) */ public function syncMenus(int $tenantId, array $globalMenuIds, bool $force = false): array /** * 신규 글로벌 메뉴 일괄 가져오기 */ public function importNewMenus(int $tenantId): array /** * 기존 메뉴 일괄 업데이트 (커스텀 제외) */ public function syncUpdates(int $tenantId): array /** * 메뉴 필드 비교 (변경 사항 감지) */ private function compareMenuFields(Menu $tenantMenu, Menu $globalMenu): array ``` --- ## 9. MNG 화면 설계 ### 9.1 글로벌 메뉴 관리 (시스템 관리자) ``` /mng/global-menus ├── 목록 화면 │ ├── 트리 구조로 표시 │ ├── 활성/비활성/숨김 상태 표시 │ ├── 연결된 테넌트 수 표시 │ └── 추가/수정/삭제 버튼 │ ├── 생성/수정 모달 │ ├── 메뉴명, URL, 아이콘 │ ├── 상위 메뉴 선택 │ ├── 활성 여부, 숨김 여부 │ └── 정렬 순서 │ └── 테넌트 동기화 모달 ├── 대상 테넌트 선택 (전체/선택) └── 동기화 실행 ``` ### 9.2 테넌트 메뉴 관리 (테넌트 관리자) ``` /mng/menus ├── 목록 화면 │ ├── 트리 구조로 표시 │ ├── 글로벌 연결 여부 표시 (🔗 아이콘) │ ├── 커스터마이징 여부 표시 (✏️ 아이콘) │ └── 추가/수정/삭제/숨김 버튼 │ ├── 글로벌 메뉴 복제 모달 │ ├── 복제 가능한 글로벌 메뉴 목록 │ ├── 체크박스 선택 │ └── 일괄 복제 버튼 │ ├── 생성/수정 모달 │ ├── 메뉴명, URL, 아이콘 │ ├── 상위 메뉴 선택 │ └── 정렬 순서 │ └── 동기화 모달 ├── 원본과 비교 (diff 표시) └── 동기화 실행 버튼 ``` ### 9.3 동기화 센터 화면 (테넌트 관리자) ``` /mng/menus/sync ┌─────────────────────────────────────────────────────────────────┐ │ 📋 메뉴 동기화 센터 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ [요약 카드] │ │ ┌──────────┬──────────┬──────────┬──────────┬──────────┐ │ │ │ 🆕 신규 │ 🔄 업데이트│ ✅ 최신 │ ✏️ 커스텀│ ⚠️ 삭제됨│ │ │ │ 2개 │ 3개 │ 15개 │ 1개 │ 1개 │ │ │ └──────────┴──────────┴──────────┴──────────┴──────────┘ │ │ │ │ [빠른 액션] │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ 🆕 신규 가져오기 │ │ 🔄 업데이트 동기화│ │ │ │ (2개) │ │ (3개) │ │ │ └─────────────────┘ └─────────────────┘ │ │ │ │ [상태 필터] │ │ ○ 전체 ● 신규 ○ 업데이트 ○ 커스텀 ○ 삭제됨 │ │ │ │ [메뉴 목록 테이블] │ │ ┌────┬────────────┬────────────┬────────┬──────────┬────────┐ │ │ │ ☑ │ 글로벌 메뉴 │ 테넌트 메뉴 │ 상태 │ 변경 항목 │ 액션 │ │ │ ├────┼────────────┼────────────┼────────┼──────────┼────────┤ │ │ │ ☑ │ 고객센터 │ - │ 🆕 신규 │ - │ 가져오기│ │ │ │ ☑ │ 대시보드 │ 대시보드 │ 🔄 업뎃 │ icon,url │ 동기화 │ │ │ │ ☐ │ 사용자관리 │ 직원관리 │ ✏️ 커스텀│ name │ ⚠️ 강제│ │ │ └────┴────────────┴────────────┴────────┴──────────┴────────┘ │ │ │ │ [선택 액션] │ │ ☐ 전체 선택 │ [🔄 선택 동기화 (2개)] [❌ 취소] │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ### 9.4 동기화 확인 모달 ``` ┌─────────────────────────────────────────────────────────────────┐ │ ⚠️ 동기화 확인 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 다음 메뉴를 동기화하시겠습니까? │ │ │ │ 📝 동기화 내역: │ │ • 신규 생성: 2개 (고객센터, 결제내역) │ │ • 업데이트: 1개 (대시보드) │ │ │ │ ⚠️ 경고: │ │ • 커스텀 메뉴 1개가 포함되어 있습니다. │ │ - "직원관리" (원본: 사용자관리) │ │ ☐ 커스텀 메뉴도 강제 동기화 (원본으로 덮어쓰기) │ │ │ │ [취소] [동기화 실행] │ └─────────────────────────────────────────────────────────────────┘ ``` ### 9.5 동기화 결과 모달 ``` ┌─────────────────────────────────────────────────────────────────┐ │ ✅ 동기화 완료 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 동기화가 성공적으로 완료되었습니다. │ │ │ │ 📊 결과: │ │ • 생성됨: 2개 │ │ • 업데이트됨: 1개 │ │ • 건너뜀 (커스텀): 1개 │ │ │ │ 📋 상세: │ │ ✅ 고객센터 - 신규 생성 │ │ ✅ 결제내역 - 신규 생성 │ │ ✅ 대시보드 - icon, url 업데이트 │ │ ⏭️ 직원관리 - 커스텀 메뉴로 건너뜀 │ │ │ │ [확인] │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## 10. 변경 영향도 분석 | 파일 | 변경 내용 | 영향도 | |------|----------|--------| | `database/migrations/` | global_menu_id, is_customized 추가 | 🔴 높음 | | `app/Models/Commons/Menu.php` | fillable, 관계 메서드 추가 | 🟡 중간 | | `app/Services/MenuService.php` | 복제, 동기화, 복원 메서드 추가 | 🟡 중간 | | `app/Services/MenuBootstrapService.php` | global_menu_id 저장 로직 추가 | 🟡 중간 | | `app/Services/GlobalMenuService.php` | 신규 생성 | 🟢 낮음 | | `app/Http/Controllers/Api/V1/MenuController.php` | 엔드포인트 추가 | 🟡 중간 | | `app/Http/Controllers/Api/Admin/GlobalMenuController.php` | 신규 생성 | 🟢 낮음 | | `routes/api.php` | 라우트 추가 | 🟡 중간 | | `mng/` | 뷰, 컨트롤러 생성 | 🟢 낮음 | --- ## 11. 구현 계획 ### Phase 1: DB 및 모델 - [ ] 마이그레이션 생성 (`global_menu_id`, `is_customized`) - [ ] Menu 모델 수정 (fillable, 관계 메서드) - [ ] MenuBootstrapService 수정 (복사 시 global_menu_id 저장) ### Phase 2: API 서비스 (1-2일) - [ ] GlobalMenuService 생성 - [ ] MenuService 메서드 추가 - [ ] `getAvailableGlobalMenus()` - [ ] `cloneFromGlobal()` - [ ] `syncFromGlobal()` - [ ] `restore()` - [ ] GlobalMenuController 생성 - [ ] MenuController 엔드포인트 추가 - [ ] 라우트 등록 ### Phase 3: MNG 화면 (2-3일) - [ ] 글로벌 메뉴 관리 화면 - [ ] 목록 (트리 구조) - [ ] 생성/수정/삭제 - [ ] 테넌트 동기화 - [ ] 테넌트 메뉴 관리 화면 개선 - [ ] 글로벌 연결 표시 - [ ] 복제 기능 - [ ] 동기화 기능 ### Phase 4: 테스트 (1일) - [ ] 유닛 테스트 - [ ] 통합 테스트 --- ## 12. 관련 파일 위치 ``` API 프로젝트: /Users/hskwon/Works/@KD_SAM/SAM/api/ ├─ app/Models/Commons/ │ └─ Menu.php # Menu 모델 │ ├─ app/Models/Scopes/ │ └─ TenantScope.php # 테넌트 격리 스코프 │ ├─ app/Traits/ │ └─ BelongsToTenant.php # 테넌트 자동 격리 트레이트 │ ├─ app/Services/ │ ├─ MenuService.php # 메뉴 CRUD 서비스 │ ├─ MenuBootstrapService.php # 메뉴 복사(부트스트랩) 서비스 │ └─ RegisterService.php # 회원가입 서비스 (메뉴 복사 호출) │ ├─ app/Http/Controllers/Api/V1/ │ └─ MenuController.php # 메뉴 API 컨트롤러 │ ├─ app/Observers/ │ └─ MenuObserver.php # 메뉴 이벤트 옵저버 (권한 자동 생성) │ └─ database/migrations/ ├─ 2025_08_15_000000_create_authz_structures.php ├─ 2025_08_15_000200_drop_slug_from_menus_table.php └─ 2025_11_12_160656_add_performance_indexes_to_menus_table.php MNG 프로젝트: /Users/hskwon/Works/@KD_SAM/SAM/mng/ (구현 예정) ``` --- ## 13. 메뉴 추가 SQL (참고) PDF 문서에서 추출한 신규 메뉴 SQL은 별도 파일 참조: - `claudedocs/MENU_INSERT_QUERIES.sql` --- ## 14. 다음 작업 1. Phase 1 시작: 마이그레이션 생성 2. Menu 모델 수정 3. MenuBootstrapService 수정 --- ## 변경 이력 | 날짜 | 작성자 | 내용 | |------|--------|------| | 2025-12-01 | Claude | 초안 작성 | | 2025-12-02 | Claude | 동기화 시스템 상세 설계 추가 (상태 분류, API 스펙, MNG UI) |