Files
Gnuboard7/docs/extension/template-basics.md
T
2026-07-01 10:30:32 +09:00

21 KiB

템플릿 시스템 기초

위치: docs/extension/template-basics.md 관련 문서: index.md | template-routing.md | template-security.md


TL;DR (5초 요약)

1. 타입: Admin (관리자용), User (일반사용자용)
2. 디렉토리: templates/vendor-template (예: sirsoft-admin_basic)
3. 필수: template.json, routes.json, components.json
4. 코어/템플릿 분리: 코어는 엔진, 템플릿은 컴포넌트
5. 버전 히스토리: Admin만 지원, User는 미지원

목차

  1. 개요
  2. 템플릿 타입
  3. 템플릿 타입별 버전 히스토리 규칙
  4. 템플릿 네이밍 규칙
  5. 템플릿 메타데이터 (template.json)
  6. 에러 페이지 설정 (error_config)
  7. 레이아웃 등록 스캔 범위
  8. 역호환성 (다국어 필드)

개요

G7의 템플릿 시스템은 JSON 기반 레이아웃 정의를 통해 동적으로 UI를 생성하는 프론트엔드 아키텍처입니다. 코어는 렌더링 엔진만 제공하고, 실제 컴포넌트는 템플릿별로 독립적으로 구현됩니다.

핵심 원칙:

  • 코어와 템플릿의 완전한 분리
  • JSON 기반 레이아웃 정의
  • 컴포넌트 재사용성 극대화
  • 다국어 및 권한 기반 렌더링

템플릿 타입

Admin 템플릿

용도: 관리자 페이지 UI 제공

특징:

  • 레이아웃은 읽기 전용 (DB 저장)
  • 관리자만 접근 가능
  • /admin/* 경로 전용
  • 예: sirsoft-admin_basic

User 템플릿

용도: 일반 사용자 페이지 UI 제공

특징:

  • 레이아웃 편집 가능 (사용자 커스터마이징)
  • 공개 접근 가능
  • / 루트 경로 전용
  • 예: sirsoft-user_theme1 (추후 구현 예정)

템플릿 타입별 버전 히스토리 규칙

중요: 템플릿 타입에 따라 버전 히스토리 동작이 다름
✅ 필수: Admin과 User 템플릿의 버전 관리 차이 이해

Admin 템플릿 (type: "admin")

버전 히스토리: ❌ 사용하지 않음

  • 레이아웃은 읽기 전용 (수정 불가)
  • template_layout_versions 테이블 미사용
  • 레이아웃 변경 시 직접 template_layouts.content 덮어쓰기
  • 이유: 디자인 변경만 가능, 레이아웃 구조는 템플릿 교체로만 변경

구현 예시:

// Admin 템플릿 레이아웃 업데이트
public function updateAdminLayout(int $id, array $data): TemplateLayout
{
    $layout = TemplateLayout::findOrFail($id);

    // 버전 생성 없이 직접 수정
    $layout->update([
        'content' => $data['content'],
    ]);

    return $layout;
}

User 템플릿 (type: "user")

버전 히스토리: ✅ 자동 관리

  • 레이아웃 편집 가능 (사용자 커스터마이징)
  • template_layout_versions 테이블 사용
  • 레이아웃 수정 시 자동으로 새 버전 생성
  • 버전별 저장/복원 기능 제공

구현 예시:

// User 템플릿 레이아웃 업데이트 (버전 자동 생성)
public function updateUserLayout(int $id, array $data): TemplateLayout
{
    $layout = TemplateLayout::findOrFail($id);

    // 현재 버전 번호 조회
    $latestVersion = $layout->versions()->max('version') ?? 0;
    $newVersion = $latestVersion + 1;

    // 새 버전 생성
    TemplateLayoutVersion::create([
        'layout_id' => $layout->id,
        'version' => $newVersion,
        'content' => $data['content'],
        'created_by' => auth()->id(),
    ]);

    // 레이아웃 content도 업데이트
    $layout->update([
        'content' => $data['content'],
    ]);

    return $layout;
}

버전 복원:

// 특정 버전으로 복원
public function restoreVersion(int $layoutId, int $version): TemplateLayout
{
    $layout = TemplateLayout::findOrFail($layoutId);
    $versionRecord = $layout->getVersion($version);

    if (!$versionRecord) {
        throw new Exception('버전을 찾을 수 없습니다.');
    }

    // 복원 시 새 버전으로 저장
    $latestVersion = $layout->versions()->max('version') ?? 0;

    TemplateLayoutVersion::create([
        'layout_id' => $layout->id,
        'version' => $latestVersion + 1,
        'content' => $versionRecord->content,
        'created_by' => auth()->id(),
    ]);

    $layout->update([
        'content' => $versionRecord->content,
    ]);

    return $layout;
}

비교표

항목 Admin 템플릿 User 템플릿
레이아웃 편집 ❌ 불가 ✅ 가능
버전 히스토리 ❌ 미사용 ✅ 자동 생성
template_layout_versions ❌ 미사용 ✅ 사용
레이아웃 복원 ❌ 불가 ✅ 가능
변경 방법 템플릿 교체 직접 편집

템플릿 네이밍 규칙

형식: [vendor-template] (GitHub 스타일)

  • 소문자 사용
  • 하이픈(-) 구분
  • vendor: 개발자/조직 식별자
  • template: 템플릿명

디렉토리 예시:

  • /templates/_bundled/sirsoft-admin_basic/ - sirsoft가 개발한 기본 관리자 템플릿
  • /templates/_bundled/johndoe-admin_dark/ - johndoe가 개발한 다크 모드 관리자 템플릿

식별자 검증 규칙

모듈/플러그인/템플릿 공통 식별자 검증 규칙은 extension-manager.md를 참조하세요.


템플릿 메타데이터 (template.json)

위치: /templates/_bundled/[vendor-template]/template.json

구조

{
  "identifier": "sirsoft-admin_basic",
  "vendor": "sirsoft",
  "name": "Admin Basic",
  "version": "1.0.0",
  "license": "MIT",
  "description": "Basic admin template for Gnuboard7 platform",
  "type": "admin",
  "locales": ["ko", "en"],
  "dependencies": [],
  "features": {
    "responsive": true,
    "darkMode": true,
    "rtl": false
  },
  "assets": {
    "css": ["dist/bundle.css"],
    "js": ["dist/bundle.js"]
  },
  "externals": [
    {
      "id": "fontawesome",
      "type": "style",
      "url": "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css",
      "preconnect": "https://cdnjs.cloudflare.com"
    }
  ]
}

필수 필드

필드 설명
identifier 템플릿 고유 식별자 (vendor-template 형식)
vendor 벤더명
name 템플릿 표시명
version 버전 (Semantic Versioning)
license 라이선스 유형 (예: "MIT") — API 리소스의 license 필드로 노출
type "admin" 또는 "user"
locales 지원 언어 배열 (config('app.supported_locales')와 매칭)

외부 리소스 (externals)

externals는 admin/user 템플릿에 공통 적용되는 선택 필드입니다. 최초 HTML 문서에 정적으로 필요한 외부 스타일, 웹폰트, 스크립트, 리소스 힌트를 선언합니다. external_styles는 사용하지 않습니다.

속성 타입 필수 적용 type 허용값/형식 렌더링/동작
id string 권장 all 영문/숫자/-/_ HTML id 속성
type string 필수 all style, webfont, script, preconnect, dns-prefetch, preload, modulepreload 출력 태그와 위치 결정
url string 필수 all https://... link 계열은 href, script는 src
preconnect string 선택 style, webfont, script, preload, modulepreload https://cdn.example.com 리소스보다 먼저 <link rel="preconnect"> 출력, 중복 제거
crossorigin boolean/string 선택 style, webfont, script, preconnect, preload, modulepreload true, anonymous, use-credentials true는 anonymous로 정규화
integrity string 선택 style, webfont, script, preload, modulepreload SRI hash HTML integrity
referrerpolicy string 선택 style, webfont, script, preload, modulepreload 표준 referrer policy HTML referrerpolicy
media string 선택 style, webfont CSS media query stylesheet link의 media
position string 선택 script head, before-core, before-template, body-end script 삽입 위치, 기본 before-template
async boolean 선택 script true, false async; defer와 동시 사용 불가
defer boolean 선택 script true, false defer
as string preload 필수 preload style, script, font, image, fetch 등 preload의 as
mimeType string 선택 preload, modulepreload MIME type HTML type
fetchpriority string 선택 preload, modulepreload high, low, auto HTML fetchpriority
type 출력 위치 HTML 결과
style head <link rel="stylesheet" href="...">
webfont head <link rel="stylesheet" href="...">
script position 기준 <script src="..."></script>
preconnect head 상단 <link rel="preconnect" href="...">
dns-prefetch head 상단 <link rel="dns-prefetch" href="...">
preload head <link rel="preload" href="..." as="...">
modulepreload head <link rel="modulepreload" href="...">

externals는 페이지 최초 진입에 항상 필요한 리소스만 선언합니다. 라우트별 조건부 스크립트나 액션 실행 중 동적 로딩은 layout scripts와 loadScript 책임입니다.


에러 페이지 설정 (error_config)

필수: 모든 템플릿은 에러 페이지 설정을 포함해야 함
✅ 규칙: DB에는 layout_name만 저장되므로 경로 없이 이름만 사용

template.json 설정

{
  "error_config": {
    "layouts": {
      "404": "404",
      "403": "403",
      "500": "500"
    }
  }
}

에러 레이아웃 파일 위치

에러 레이아웃은 반드시 layouts/errors/ 디렉토리에 위치해야 합니다:

templates/_bundled/[vendor-template]/
├── template.json               # 메타데이터 (이름, 버전, 라이선스 등 SSoT)
├── seo-config.json             # SEO 컴포넌트→HTML 매핑 설정 (선택)
├── LICENSE                     # 라이선스 전문 (MIT) — API 엔드포인트 `GET /api/admin/templates/{identifier}/license`로 제공
├── layouts/
│   ├── dashboard.json        # 일반 레이아웃
│   ├── _admin_base.json      # 베이스 레이아웃
│   └── errors/               # 에러 레이아웃 전용
│       ├── 404.json
│       ├── 403.json
│       └── 500.json

에러 레이아웃 구조

에러 레이아웃도 일반 레이아웃과 동일하게 extends + slots 패턴을 사용합니다:

{
  "extends": "_admin_base",
  "slots": {
    "content": [
      {
        "type": "composite",
        "name": "EmptyState",
        "props": {
          "icon": "alert-triangle",
          "title": "$t:errors.404.title",
          "description": "$t:errors.404.description"
        }
      }
    ]
  }
}

설치 시 검증

TemplateManager::installTemplate() 호출 시 다음 사항이 자동 검증됩니다:

  1. error_config 섹션 존재 여부
  2. 필수 에러 코드(404, 403, 500) 레이아웃 정의 여부
  3. 레이아웃 파일 실제 존재 여부 (layouts/errors/ 디렉토리 내)

레이아웃 등록 스캔 범위

중요: registerLayouts()는 특정 디렉토리만 스캔
✅ 포함: layouts/*.json (루트) + layouts/errors/*.json
❌ 제외: layouts/partials/ 등 기타 하위 디렉토리

스캔 대상

디렉토리 스캔 여부 용도
layouts/*.json ✅ 스캔 일반 레이아웃
layouts/errors/*.json ✅ 스캔 에러 페이지 레이아웃
layouts/partials/*.json ❌ 제외 extends로 참조되는 부분 레이아웃

이유

  • partials/ 디렉토리의 파일은 메인 레이아웃에서 extends로 참조되어 병합됨
  • 직접 라우팅되지 않으므로 DB에 별도 등록 불필요
  • 에러 레이아웃은 ErrorPageHandler에서 직접 조회하므로 DB 등록 필요

TemplateManager.php 구현

// 루트 layouts 디렉토리의 JSON 파일만 스캔
$layoutFiles = File::glob("{$layoutsPath}/*.json");

// errors/ 디렉토리만 추가 스캔 (에러 페이지 레이아웃용)
$errorsPath = "{$layoutsPath}/errors";
if (File::exists($errorsPath)) {
    $errorLayoutFiles = File::glob("{$errorsPath}/*.json");
    $layoutFiles = array_merge($layoutFiles, $errorLayoutFiles);
}

모듈 레이아웃 자동 등록

모듈 활성화 시 레이아웃은 디렉토리 위치에 따라 해당 타입의 템플릿에 등록됩니다:

  • modules/{module}/resources/layouts/admin/*.json → Admin 타입 템플릿에 등록
  • modules/{module}/resources/layouts/user/*.json → User 타입 템플릿에 등록

이 동작은 ModuleManager::registerModuleLayouts()에서 처리됩니다.


역호환성 (다국어 필드)

중요: 다국어 필드의 역호환성 지원
✅ 필수: 문자열 → 다국어 배열 자동 변환

지원 필드

  • name: 템플릿명
  • description: 템플릿 설명

ModuleInterface/PluginInterface와 동일한 패턴

// ✅ 권장: 다국어 배열 반환
public function getName(): array
{
    return [
        'ko' => '기본 관리자 템플릿',
        'en' => 'Basic Admin Template',
    ];
}

// 역호환: 문자열 반환 (자동 변환됨)
public function getName(): string
{
    return 'Basic Admin Template';  // 자동으로 ['ko' => '...', 'en' => '...']로 변환
}

TemplateManager 자동 변환

// app/Extension/TemplateManager.php

/**
 * 문자열을 다국어 배열로 자동 변환
 */
protected function convertToMultilingual($value): array
{
    // 이미 배열이면 그대로 반환
    if (is_array($value)) {
        return $value;
    }

    // 문자열이면 모든 로케일에 동일한 값 설정
    if (is_string($value)) {
        $locales = config('app.translatable_locales', ['ko', 'en']);
        $result = [];
        foreach ($locales as $locale) {
            $result[$locale] = $value;
        }
        return $result;
    }

    // 그 외 타입은 빈 배열 반환
    return [];
}

template.json 예시

{
  "identifier": "sirsoft-admin_basic",
  "vendor": "sirsoft",

  // ✅ 권장: 다국어 객체
  "name": {
    "ko": "기본 관리자 템플릿",
    "en": "Basic Admin Template"
  },
  "description": {
    "ko": "그누보드7용 기본 관리자 템플릿",
    "en": "Basic admin template for Gnuboard7 platform"
  }

  // 역호환: 문자열 (자동 변환됨)
  // "name": "Basic Admin Template",
  // "description": "Basic admin template for Gnuboard7 platform"
}

DB 저장 형식

// templates 테이블
[
    'name' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'],
    'description' => ['ko' => 'G7용...', 'en' => 'Basic admin template...'],
]

역호환성 보장 사항

  • ✅ 기존 문자열 기반 template.json 정상 작동
  • ✅ 문자열 → 다국어 배열 자동 변환
  • ✅ 변환된 값은 모든 로케일에 동일하게 적용
  • ✅ Model Accessor (getLocalizedName())에서 올바른 폴백 처리

템플릿 업데이트 레이아웃 충돌 전략

템플릿 버전 업데이트 시, 관리자가 수정한 레이아웃과 새 버전의 레이아웃이 충돌할 수 있습니다.

사전 확인

업데이트 전 수정된 레이아웃을 확인할 수 있습니다:

GET /api/admin/templates/{templateName}/check-modified-layouts

응답:

{
    "has_modified": true,
    "modified_layouts": [
        { "id": 1, "name": "dashboard", "updated_at": "2026-02-24T10:00:00Z" }
    ]
}

충돌 해결 전략

업데이트 실행 시 layout_strategy 파라미터로 전략을 선택합니다:

전략 동작
apply_new 새 버전의 레이아웃으로 덮어쓰기 (관리자 수정 사항 유실)
keep_current 기존 레이아웃 유지 (새 버전 레이아웃 미적용)
POST /api/admin/templates/{templateName}/update
Body: { "layout_strategy": "apply_new" }

상세: extension-update-system.md


번들 디렉토리 작업 규칙

필수: 템플릿 수정/개발은 _bundled 디렉토리에서만 작업 (활성 디렉토리 직접 수정 금지)
필수: _bundled 작업 완료 후 반영/검증은 업데이트 프로세스 사용

개발 워크플로우

1. templates/_bundled/{identifier}/ 에서 코드 수정
2. template.json 버전 올리기
3. php artisan template:update {identifier} 로 활성 디렉토리에 반영
4. 테스트 실행으로 검증

왜 활성 디렉토리 직접 수정이 금지되는가?

  • 활성 디렉토리는 .gitignore 대상 → Git에 변경 기록 불가
  • 다음 업데이트 시 _bundled 소스로 덮어쓰기 → 직접 수정 사항 유실
  • 업데이트 프로세스 미수행 시 레이아웃 갱신/캐시 무효화 누락

예외: 초기 개발 (아직 _bundled에 미등록)

✅ 허용: 신규 템플릿 초기 개발 시 활성 디렉토리에서 직접 작업
전환점: _bundled에 최초 반영한 이후부터는 반드시 _bundled에서만 작업

상세: extension-update-system.md "번들 디렉토리 개발 워크플로우" 참조


코드 변경 시 버전 변경 필수

필수: 템플릿 코드를 변경한 경우 버전을 올려야 합니다.
버전 변경 없이 _bundled에 반영하면, 이미 설치된 환경에서 업데이트가 감지되지 않습니다.
참고: 템플릿은 모듈/플러그인과 달리 업그레이드 스텝(upgrades/)을 사용하지 않습니다.

필수 작업

  1. template.json 버전 올리기: version 필드를 Semantic Versioning에 따라 증가
  2. _bundled 동기화: templates/_bundled/{identifier}/ 디렉토리에 변경 사항 반영

템플릿 vs 모듈/플러그인 차이

항목 템플릿 모듈/플러그인
manifest 버전 올리기 ✅ 필수 ✅ 필수
_bundled 동기화 ✅ 필수 ✅ 필수
업그레이드 스텝 (upgrades/) ❌ 해당 없음 조건부 필요
DB 마이그레이션 ❌ 해당 없음 조건부 필요
레이아웃 충돌 전략 ✅ layout_strategy 파라미터 ❌ 해당 없음

상세: extension-update-system.md "개발자 버전 업데이트 가이드" 참조


SEO 설정 (seo-config.json)

위치: templates/_bundled/[vendor-template]/seo-config.json (선택 파일)

SEO 페이지 생성기가 컴포넌트를 HTML로 변환할 때 사용하는 매핑 설정입니다. 이 파일이 없으면 모든 컴포넌트가 <div> fallback으로 렌더링됩니다.

주요 섹션

섹션 설명
text_props 텍스트 추출 우선순위 (예: ["text", "label", "value", "title"])
attr_map props→HTML 속성 매핑 (예: {"className": "class", "htmlFor": "for"})
allowed_attrs 허용 HTML 속성 목록 (목록에 없는 속성은 출력 안됨)
component_map 컴포넌트명 → HTML 태그 매핑 (기본 30개 + 커스텀)
render_modes 렌더 모드 정의 (iterate/format/raw 타입)
self_closing 셀프 클로징 태그 목록 (["img", "input", "hr", "br"])
stylesheets SEO 페이지에 포함할 외부 CSS URL

Graceful Degradation

  • text_props/attr_map/allowed_attrs 미선언 → 엔진 내장 기본값 사용 (범용 HTML/React 매핑)
  • text_props 빈 배열로 명시 → props에서 텍스트 추출 불가 (component.text는 여전히 동작)
  • attr_map 빈 객체로 명시 → 속성명 변환 없음 (className→class 미적용)
  • allowed_attrs 빈 배열로 명시 → 모든 속성 출력 차단
  • seo-config.json 미존재 → 모든 컴포넌트 <div> fallback, 기본 속성 매핑으로 최소한의 HTML 생성

검증

TemplateManager.validateSeoConfig()가 설치/업데이트 시 자동 검증합니다:

  • JSON 파싱, component_map.*.tag 필수, render → render_modes 교차 참조
  • 검증 실패 시 설치/업데이트 차단
  • 파일 미존재 시 경고만 (설치 허용)

상세 스키마: seo-system.md "템플릿 seo-config.json" 섹션 참조

관련 문서