Files
sam-api/claudedocs/MENU_INTEGRATION_SYSTEM_DESIGN.md
hskwon aabd791336 docs: 메뉴 동기화 시스템 상세 설계 추가
- 동기화 상태 분류 (new, up_to_date, updatable, customized, deleted)
- 동기화 액션 유형 (개별, 신규 가져오기, 기존 업데이트, 선택 동기화)
- 커스터마이징 메뉴 보호 정책 (force 옵션)
- 동기화 전용 API 스펙 4개 엔드포인트
- MenuSyncService 메서드 설계
- MNG 동기화 센터 UI 와이어프레임
2025-12-02 09:38:15 +09:00

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. 다음 작업

  1. Phase 1 시작: 마이그레이션 생성
  2. Menu 모델 수정
  3. MenuBootstrapService 수정

변경 이력

날짜 작성자 내용
2025-12-01 Claude 초안 작성
2025-12-02 Claude 동기화 시스템 상세 설계 추가 (상태 분류, API 스펙, MNG UI)