Files
Gnuboard7/docs/backend/core-config.md
T
HeuJung 2ae1972a3b perf(core): 대용량 목록 상한 총 건수·커서 계약 신설 및 요청당 반복 실행 비용 정리
공개 이슈 gnuboard/g7 이 지목한 병목은 "같은 일을 반복 실행한다" 축이다.
응답·행의 무게를 다룬 · 과 겹치지 않는다.

공통 계약 — 총 건수 상한과 페이지 이동 범위는 별개 결정이다. 묶으면 필요 없이
기능이 깎인다. 총 건수만 상한을 받고(파생 테이블 COUNT), "다음" 이동은
per_page + 1 실측으로 끝까지 열어 둔다. 계산이 불가능해지는 것은 마지막 페이지
번호 하나뿐이며 그 사실은 last_page: null 이 알린다. 최신순처럼 실제 컬럼으로
정렬하는 목록은 커서로 전환해 깊이와 무관하게 일정 속도로 이동한다. 관련도순은
계산값 정렬이라 커서 키로 쓸 수 없어 offset 을 유지한다.

계약의 입구는 표준 paginate 와 같은 폭이어야 한다. 관계·쿼리 빌더를 받지 못하면
그 좁은 만큼이 그대로 운영 500 이 되고, 실제로 관리자 알림 목록에서 그렇게 터졌다.
응답 조립도 컬렉션마다 손으로 하면 형태가 늘어나는 순간 없는 값을 부르거나 새
필드를 흘리므로, 형태 판정을 paginationMeta 한 곳에 모았다. 표준 paginate
응답은 필드 단위로 이전과 동일하다.

요청당 반복 비용 — 훅 구독마다 남기던 로그 400줄, 요청당 스무 번 넘던 설정 파일
재읽기, 이미 캐시된 목록의 DB 재조회를 없앴다. 권한 판정은 요청 스코프 메모를 두어
화면 요소마다 나가던 조회를 한 번으로 줄였고, 크로스 요청 캐시는 두지 않아 권한
변경이 종전처럼 다음 요청에 반영된다.

검색 질의는 활성 엔진이 만든다. 저장소가 구체 엔진을 지목하면 플러그인이 등록한
엔진은 호출될 기회 자체를 잃고 오류 없이 다른 방식으로 동작한다. 해석기를 두어
활성 엔진에 위임하고, 전문검색이 없는 DBMS 의 부분일치 폴백도 드라이버명 하드코딩
대신 선언형 config 로 옮겼다.

총 건수가 잘린 목록에서 순번을 역산하면 0 과 음수가 나온다. 지어내지 않고 null 을
돌려주며, 그 원칙을 last_page 와 동일하게 적용했다.
2026-08-06 11:13:51 +09:00

12 KiB

코어 설정 (config/core.php)

목적: 코어 시스템의 권한, 역할, 메뉴, 메일 템플릿 정의 파일 구조 설명


TL;DR (5초 요약)

1. config/core.php = 코어 권한/역할/메뉴/메일템플릿의 SSoT (Single Source of Truth)
2. 구조: module(1레벨) → categories(2레벨) → permissions(3레벨) 3단계
3. 설치(Seeder) + 업데이트(CoreUpdateService) 모두 이 파일에서 읽음
4. 모듈/플러그인은 config/core.php 대신 자체 config.php에 동일 구조로 정의
5. 수정 시 Seeder/Sync 로직이 자동 반영 (수동 마이그레이션 불필요)

목차


파일 구조 개요

config/core.php는 최상위 키로 구성되며, 이 문서는 그중 권한·역할·메뉴를 다룹니다. 나머지 키는 각자의 규정 문서가 소유합니다.

return [
    // 이 문서가 다루는 범위
    'permissions' => [...],            // 코어 권한 정의
    'roles' => [...],                  // 코어 역할 정의
    'menus' => [...],                  // 코어 메뉴 정의

    // 다른 문서가 소유하는 키
    'settings_limits' => [...],        // 관리자 환경설정 입력 한계값
    'pagination' => [...],             // 목록 한계값 기본값 → pagination.md
    'search' => [...],                 // DBMS 별 부분일치 연산자 → search-system.md
    'notification_definitions' => [...], // 알림 정의 → notification-system.md
    'identity_policies' => [...],      // 본인인증 정책 → identity-policies.md
    'identity_messages' => [...],      // 본인인증 메시지 → identity-messages.md
    'identity_purposes' => [...],      // 본인인증 목적 → identity-policies.md
];

키를 새로 추가할 때는 그 키를 설명하는 규정 문서를 함께 지정하고 이 목록에 한 줄을 더합니다. 목록이 실제 구조와 어긋나면 "여기 없으니 없는 값" 으로 읽혀 중복 키가 생깁니다.

변경 이력 (7.0.0-beta.2): mail_templates 키는 알림 시스템 통합(#146)으로 제거되었습니다. 코어 메일 템플릿은 notification_definitions + notification_templates 로 이전되었으며, database/seeders/NotificationDefinitionSeeder 가 시드 데이터를 직접 보유합니다.


permissions — 권한 정의

3레벨 계층 구조:

permissions
├── module (1레벨)           → identifier, name, description, order
└── categories (2레벨 배열)
    ├── category 항목        → identifier, name, description, category, order
    └── permissions (3레벨)  → identifier, name, description, order, resource_route_key?, owner_key?

module (1레벨)

'module' => [
    'identifier' => 'core',
    'name' => ['ko' => '코어', 'en' => 'Core'],
    'description' => ['ko' => '코어 시스템 권한', 'en' => 'Core system permissions'],
    'order' => 1,
],

categories (2레벨) + permissions (3레벨)

'categories' => [
    [
        'identifier' => 'core.users',
        'name' => ['ko' => '사용자 관리', 'en' => 'User Management'],
        'description' => [...],
        'category' => 'users',     // 카테고리 슬러그
        'order' => 1,
        'type' => 'admin',         // 카테고리 타입 (선택, 미지정 시 하위 권한에서 추론)
        'permissions' => [
            [
                'identifier' => 'core.users.read',
                'name' => ['ko' => '사용자 조회', 'en' => 'View Users'],
                'description' => [...],
                'order' => 1,
                'type' => 'admin',               // 권한 타입 (선택, 미지정 시 카테고리 타입 상속)
                'resource_route_key' => 'user',  // scope 체크 시 라우트 모델 키 (선택)
                'owner_key' => 'id',             // scope 체크 시 소유자 필드 (선택)
            ],
        ],
    ],
],

권한 필드 설명

필드 필수 설명
identifier ✅ 권한 식별자 (예: core.users.read)
name ✅ 다국어 이름 배열 ['ko' => ..., 'en' => ...]
description ✅ 다국어 설명 배열
order ✅ 정렬 순서
type ❌ 권한 컨텍스트 (admin / user). 미지정 시 카테고리 타입 상속, 카테고리도 미지정이면 admin 기본값
resource_route_key ❌ scope 기반 접근 체크 시 라우트 모델 바인딩 키
owner_key ❌ scope 기반 접근 체크 시 소유자 판단 필드

type 필드 우선순위 (카테고리/권한 동기화 규칙)

CoreUpdateService::syncCoreRolesAndPermissions()는 다음 우선순위로 type을 결정합니다:

  1. 명시 우선: 카테고리/권한에 type 필드가 명시되어 있으면 그 값 사용
  2. 하위 추론: 카테고리에 type 미지정 + 모든 하위 권한이 동일 type → 그 type을 카테고리에도 적용
  3. 기본값: 그 외 → admin

권한 레벨 type이 미지정이면 카테고리 type을 상속합니다. (ModuleManager::syncPermissions()와 동일 패턴)

admin/user 권한 분리 사례

같은 도메인이지만 관리자와 사용자 컨텍스트에서 모두 사용해야 한다면 별도 식별자로 분리합니다:

// 관리자가 시스템 전체 알림을 관리 (관리자 화면)
[
    'identifier' => 'core.notifications',
    'name' => ['ko' => '알림 (관리자)', 'en' => 'Notifications (Admin)'],
    'type' => 'admin',
    'permissions' => [
        ['identifier' => 'core.notifications.read', 'type' => 'admin', ...],
        ['identifier' => 'core.notifications.update', 'type' => 'admin', ...],
        ['identifier' => 'core.notifications.delete', 'type' => 'admin', ...],
    ],
],
// 사용자가 본인의 알림을 관리 (사용자 화면)
[
    'identifier' => 'core.user-notifications',
    'name' => ['ko' => '알림 (사용자)', 'en' => 'Notifications (User)'],
    'type' => 'user',
    'permissions' => [
        ['identifier' => 'core.user-notifications.read', 'type' => 'user', ...],
        ['identifier' => 'core.user-notifications.update', 'type' => 'user', ...],
        ['identifier' => 'core.user-notifications.delete', 'type' => 'user', ...],
    ],
],
⚠️ 함정 경고: permissions.identifier에 단일 unique 제약이 있어 같은 식별자로 admin/user 두 행을 동시에 만들 수 없습니다.
✅ 사용자 컨텍스트 권한이 필요하면 반드시 별도 식별자(예: core.user-notifications.*)를 사용하세요.
✅ 라우트 미들웨어 permission:user,xxx 는 type=user 권한 행이 존재해야만 통과합니다 (식별자 + type 모두 매칭).

roles — 역할 정의

'roles' => [
    [
        'identifier' => 'admin',
        'name' => ['ko' => '관리자', 'en' => 'Administrator'],
        'description' => [...],
        'attributes' => ['is_active' => true],
        'permissions' => 'all_leaf',  // 특수 값: 모든 리프 권한 할당
    ],
    [
        'identifier' => 'manager',
        'name' => ['ko' => '매니저', 'en' => 'Manager'],
        'permissions' => ['core.users.read', 'core.users.create', ...],
        'permission_scopes' => [
            'core.users.read' => 'self',     // 본인 리소스만
            'core.users.update' => 'self',
        ],
    ],
],

역할 필드 설명

필드 필수 설명
identifier ✅ 역할 식별자 (예: admin, manager)
name ✅ 다국어 이름 배열
description ✅ 다국어 설명 배열
attributes ❌ 추가 속성 (is_active 등)
permissions ✅ 권한 배열 또는 'all_leaf' (모든 리프 권한)
permission_scopes ❌ 권한별 scope_type 매핑 ('self' / 'role' / 미지정=전체)

menus — 메뉴 정의

'menus' => [
    [
        'slug' => 'admin-dashboard',
        'name' => ['ko' => '대시보드', 'en' => 'Dashboard'],
        'url' => '/admin/dashboard',
        'icon' => 'fas fa-tachometer-alt',
        'parent_id' => null,
        'order' => 1,
        'is_active' => true,
    ],
],

사용처

사용처 시점 설명
RolePermissionSeeder 초기 설치 permissions, roles 데이터 시딩
CoreAdminMenuSeeder 초기 설치 menus 데이터 시딩
CoreUpdateService::syncCoreRolesAndPermissions() 코어 업데이트 권한/역할 동기화
CoreUpdateService::syncCoreMenus() 코어 업데이트 메뉴 동기화

핵심 원칙: 새 권한/역할/메뉴 추가 시 이 파일에 추가하면 설치/업데이트 모두 자동 반영됩니다.


완전 동기화 원칙

config/core.php 는 코어 권한·역할·메뉴의 SSoT 입니다. 업데이트 시 sync 는 단순 upsert 가 아니라 완전 동기화 를 수행합니다.

4단계 패턴

단계 설명 책임
1. Upsert config → DB (신규 생성 or user_overrides 보존 업데이트) syncMenu/syncRole/syncPermission
2. Orphan Delete config 에 없는 DB row 삭제 (user_overrides 무관) cleanupStaleMenus/cleanupStaleRoles/cleanupStalePermissions
3. Mapping Diff role_permissions 등 관계 테이블 재정렬 syncAllRoleAssignments
4. Dependent Cleanup FK cascade 또는 명시적 정리 Service 책임

중요한 의미 구분

  • row 존재 여부: config 기준으로만 결정 — user_overrides 무관 (제거된 건 삭제)
  • 필드 값 (유지 row): user_overrides 에 등록된 필드만 보존, 나머지는 갱신

즉 "사용자가 수정한 row 이니 보존" 이 아닙니다. "config 에 있어야 유지되고, 유지되는 row 중 사용자가 수정한 필드만 보존" 입니다.

적용 지점

sync 메서드 stale cleanup 호출
CoreUpdateService::syncCoreMenus 말미에 cleanupStaleMenus(Core, 'core', $currentSlugs)
CoreUpdateService::syncCoreRolesAndPermissions 말미에 cleanupStalePermissions 및 role identifier 기준 cleanupStaleRoles
CoreUpdateService::syncAllRoleAssignments core/core 소유 권한 전체 를 diff 기준으로 사용 (이관된 구 식별자도 detach)
확장 ModuleManager::updateModule / PluginManager::updatePlugin 동일 4단계 (확장별 식별자 범위)

예외 안전장치

  • cleanupStaleRoles: 삭제 대상 역할을 참조하는 user_roles 피벗 row 가 있으면 삭제 차단 + 경고 로그 (사용자 수동 재배정 유도)
  • FK cascade: NotificationDefinition 삭제 시 NotificationTemplate 자동 cascade

Helper 사용 (권장)

이 원칙은 각 helper 에 캡슐화되어 있습니다. Service/Seeder 에서 직접 Model 조작 금지. 자세한 사용법은 data-sync-helpers.md 참조.

// 올바른 패턴 (CoreUpdateService::syncCoreMenus 말미)
$currentSlugs = $menuSyncHelper->collectSlugsRecursive($coreMenus);
$menuSyncHelper->cleanupStaleMenus(ExtensionOwnerType::Core, 'core', $currentSlugs);

관련 문서