- 동기화 상태 분류 (new, up_to_date, updatable, customized, deleted) - 동기화 액션 유형 (개별, 신규 가져오기, 기존 업데이트, 선택 동기화) - 커스터마이징 메뉴 보호 정책 (force 옵션) - 동기화 전용 API 스펙 4개 엔드포인트 - MenuSyncService 메서드 설계 - MNG 동기화 센터 UI 와이어프레임
27 KiB
27 KiB
메뉴 통합관리 시스템 설계서
작성일: 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 마이그레이션
-- 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 동기화 상태 판단 로직
// 의사 코드
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:
{
"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:
{
"menu_ids": [10, 11, 12],
"force": false
}
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
menu_ids |
array | Y | 동기화할 글로벌 메뉴 ID 목록 |
force |
boolean | N | 커스텀 메뉴 강제 덮어쓰기 (기본: false) |
Response:
{
"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:
{
"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:
{
"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 추가 메서드
/**
* 복제 가능한 글로벌 메뉴 목록
* - 테넌트가 아직 복제하지 않은 글로벌 메뉴
*/
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 (신규)
/**
* 글로벌 메뉴 생성
*/
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 (신규)
/**
* 동기화 상태 목록 조회
*/
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. 다음 작업
- Phase 1 시작: 마이그레이션 생성
- Menu 모델 수정
- MenuBootstrapService 수정
변경 이력
| 날짜 | 작성자 | 내용 |
|---|---|---|
| 2025-12-01 | Claude | 초안 작성 |
| 2025-12-02 | Claude | 동기화 시스템 상세 설계 추가 (상태 분류, API 스펙, MNG UI) |