Files
Gnuboard7/docs/extension/template-workflow.md
T
HeuJung 3945b6f1b3 feat(core,extensions): 구동 에셋 자체 제공 · 자산 실패 폴백 · 운영자 추가 에셋(custom/)
공개 저장소 이슈 gnuboard/g7 (@bigmsg) 제보에서 출발한 작업이다.

브라우저가 화면을 그리려고 제3자 CDN 에 도달해야 하면, 그 도달 실패는 예외도 로그도
남기지 않고 화면 기능만 조용히 사라진다. 폐쇄망·방화벽·광고차단기에서 재현되는데
자체 서버 로그에는 흔적이 없어 운영자가 원인을 특정할 수 없다. 제보된 것은 편집기
하나였지만 같은 구조가 아이콘·글꼴·코드편집기·압축 라이브러리·설치 마법사·개발
대시보드에 똑같이 있었으므로, 번들 확장과 템플릿 전체를 자체 제공으로 옮겼다.
런타임에 외부로 나가는 것은 주소 검색 서비스 하나만 남았다.

자체 제공만으로는 부족하다 — 자기 서버에서 받는 파일도 실패할 수 있고, 종전에는 그
실패가 무음이었다. CSS 경로에 재시도 계층을 세우고(스크립트 경로와 동형), 서버가 HTML
에 직접 심는 externals 까지 실패를 붙잡아 안내 배너와 [다시 시도]로 표면화했다.
편집기·코드편집기는 확보 실패 시 평문 입력으로 내려앉되 저장 계약을 유지한다.

두 번째 축은 운영자가 CSS 를 덧붙일 자리가 없던 문제다(sir.kr 문의). 확장 디렉토리의
custom/ 을 운영자 소유로 정해, 확장 교체가 그 디렉토리만은 보존하게 했다. 출처에
의존하지 않는 서술자로 해석하므로 나중에 다른 출처가 붙어도 소비자는 바뀌지 않는다.
확장 자산과 같은 메커니즘으로 정적 게시되어 CSS 내부 상대 url 도 해석되고, 파일을
고치면 그 변경을 감지해 재게시까지 예약된다.

FTP 접근이 없는 운영자에게는 그 자리도 없는 것과 같으므로 레이아웃 편집기에서 직접
넣고 고칠 수 있게 했다. 모듈·플러그인·템플릿이 한 엔드포인트를 공유한다 — 타입별로
나누면 같은 검증이 세 벌로 갈리고 그중 약한 하나가 우회로가 된다. 여기서 올린
스크립트는 그 레이아웃 한 장이 아니라 사이트 전 화면에서 실행되므로 레이아웃 편집과
분리된 전용 권한으로 연다. 운영자 CSS 가 화면을 조작 불능으로 만들면 그것을 고칠
화면에도 같은 CSS 가 실려 스스로 갇히므로, 서버가 목록을 비우는 탈출구(?custom=off)를
함께 뒀다.

동봉 자산은 재생성 경로에 버전 대조 가드를 붙였다. 선언과 다른 버전을 버전 디렉토리에
써 넣는 조용한 거짓말은 배포된 뒤에는 드러나지 않는다.
2026-08-27 16:47:14 +09:00

19 KiB

템플릿 개발 워크플로우

위치: docs/extension/template-workflow.md 관련 문서: template-basics.md | template-commands.md | template-development.md


TL;DR (5초 요약)

1. 필수 파일: template.json, routes.json, _base.json, errors/{404,403,500}.json
2. 빌드 설정: package.json, vite.config.ts (IIFE 빌드, terser 대신 esbuild)
3. 검증 순서: JSON 파싱 → layout_name 필수 → error_config → 에러 레이아웃 파일 존재
4. 컴포넌트: 템플릿에 실제 존재하는 것만 import (없는 컴포넌트 import 시 빌드 실패)
5. 설치: php artisan template:build → php artisan template:install → php artisan template:activate

목차

  1. 개요
  2. 템플릿 생성 체크리스트
  3. Phase 1: 필수 메타데이터 파일
  4. Phase 2: 레이아웃 파일
  5. Phase 3: 빌드 설정
  6. Phase 4: 컴포넌트 설정
  7. Phase 5: 다국어 파일
  8. Phase 6: 빌드 및 설치
  9. 검증 로직 이해
  10. 자주 발생하는 오류와 해결
  11. 템플릿 개발 Artisan 명령어

개요

이 문서는 그누보드7 템플릿을 처음부터 만들 때 필요한 실전 워크플로우를 정리합니다. 실제 sirsoft-user_sample 템플릿 개발 과정에서 겪은 시행착오를 바탕으로 작성되었습니다.


템플릿 생성 체크리스트

새 템플릿 생성 시 아래 체크리스트를 순서대로 완료합니다:

Phase 1: 필수 메타데이터
□ template.json 생성 (error_config 포함)
□ template.json externals 점검 (icon 컴포넌트 사용 시 Font Awesome 항목 필수)
□ routes.json 생성 (layout_name 필수)

Phase 2: 레이아웃 파일
□ layouts/_user_base.json 또는 layouts/_admin_base.json 생성
□ layouts/home.json (또는 dashboard.json) 생성
□ layouts/errors/404.json 생성
□ layouts/errors/403.json 생성
□ layouts/errors/500.json 생성

Phase 3: 빌드 설정
□ package.json 생성 (build 스크립트 포함)
□ vite.config.ts 생성 (IIFE 빌드, esbuild minify)
□ tsconfig.json 생성
□ tsconfig.node.json 생성
□ tailwind.config.js 생성
□ postcss.config.js 생성

Phase 4: 컴포넌트 설정
□ src/index.ts 생성 (실제 존재하는 컴포넌트만 import)
□ src/handlers/index.ts 생성
□ src/styles/main.css 생성

Phase 5: 다국어 파일
□ lang/ko.json 생성 (errors 섹션 포함)
□ lang/en.json 생성 (errors 섹션 포함)

Phase 6: 빌드 및 설치
□ php artisan template:build [identifier]
□ php artisan template:install [identifier]
□ php artisan template:activate [identifier]

Phase 1: 필수 메타데이터 파일

g7_version / dependencies 등 버전 제약 작성 규칙은 changelog-rules.md 참조.

1.1 template.json

{
  "identifier": "vendor-template_name",
  "vendor": "vendor",
  "name": {
    "ko": "템플릿 한국어 이름",
    "en": "Template English Name"
  },
  "version": "1.0.0",
  "description": {
    "ko": "템플릿 설명 (한국어)",
    "en": "Template description (English)"
  },
  "type": "user",
  "locales": ["ko", "en"],
  "author": {
    "name": "vendor",
    "email": "contact@vendor.com"
  },
  "g7_version": ">=1.0.0",
  "dependencies": {
    "modules": {},
    "plugins": {}
  },
  "assets": {
    "css": ["assets/css/main.css"],
    "js": []
  },
  "components": {
    "basic": ["button", "div", "span", "h1", "h2", "p", "icon"],
    "composite": ["header", "footer", "card"],
    "layout": ["container", "flex", "grid"]
  },
  "error_config": {
    "layouts": {
      "404": "404",
      "403": "403",
      "500": "500"
    }
  },
  "externals": [
    {
      "id": "fontawesome",
      "type": "style",
      "asset": "vendor/font-awesome/6.4.0/css/all.inlined.css"
    }
  ]
}

필수 항목:

  • identifier: 고유 식별자 (vendor-name 형식)
  • type: "admin" 또는 "user"
  • locales: 지원 언어 배열
  • error_config.layouts: 404, 403, 500 매핑

externals (선택, 단 icon 사용 시 사실상 필수):

  • 최초 HTML 문서에 정적으로 필요한 외부 스타일·웹폰트·스크립트를 선언한다.
  • components.basic 에 icon 을 등록하면 Icon 컴포넌트가 생성하는 <i class="fas fa-..."> 를 렌더링할 Font Awesome CSS 가 반드시 필요하므로 위 fontawesome 항목을 함께 둔다. 누락 시 활성 템플릿에서 아이콘이 표시되지 않는다. icon 을 쓰지 않으면 "externals": [] 로 두거나 필드를 생략한다.
  • 속성/타입 상세: template-basics.md "외부 리소스 (externals)"

1.1.1 hidden 필드 (선택)

template.json 에 "hidden": true 를 설정하면 관리자 UI 의 템플릿 목록에서 기본 제외됩니다. 학습용 샘플 템플릿, 내부 운영 전용 템플릿을 일반 사용자에게 감출 때 사용합니다.

  • 제외 대상: 관리자 UI (GET /api/admin/templates 기본 응답)
  • 제외 대상 아님: artisan CLI (template:list, template:install, template:activate 등), 설치/제거/업데이트 감지
  • 슈퍼관리자는 "숨김 포함" 토글로 일시 조회 가능 (?include_hidden=1)
  • artisan CLI 에서도 기본 목록에서는 숨기고, php artisan template:list --hidden 으로 숨김 포함 목록을 조회할 수 있습니다
  • 사용 사례: 학습용 샘플 템플릿(예: gnuboard7-hello_admin_template, gnuboard7-hello_user_template), 내부 시연용 템플릿

1.2 routes.json

주의: layout_name 필드가 없으면 설치 실패!
{
  "version": "1.0.0",
  "layout_name": "routes",
  "meta": {
    "title": "Routes",
    "description": "Route definitions for template"
  },
  "data_sources": [],
  "components": [],
  "routes": [
    {
      "path": "/",
      "layout": "home",
      "auth": false
    }
  ]
}

필수 필드:

  • layout_name: DB 저장용 레이아웃 이름 (필수!)
  • routes: 라우트 정의 배열

Phase 2: 레이아웃 파일

2.1 디렉토리 구조

templates/_bundled/vendor-template/
└── layouts/
    ├── _user_base.json       # 베이스 레이아웃 (user 템플릿)
    ├── _admin_base.json      # 베이스 레이아웃 (admin 템플릿)
    ├── home.json             # 홈 레이아웃
    ├── routes.json           # 라우트 정의
    ├── errors/               # 필수 디렉토리
    │   ├── 404.json          # 필수
    │   ├── 403.json          # 필수
    │   └── 500.json          # 필수
    └── partials/             # partial 파일 (DB 저장 안됨)

2.2 베이스 레이아웃 (_user_base.json)

{
  "version": "1.0.0",
  "layout_name": "_user_base",
  "meta": {
    "is_base": true
  },
  "data_sources": [],
  "slots": {
    "header": [],
    "content": [],
    "footer": []
  }
}

2.3 에러 레이아웃

404.json 예시:

{
  "version": "1.0.0",
  "layout_name": "404",
  "extends": "_user_base",
  "meta": {
    "title": "$t:errors.404.title",
    "is_error_layout": true,
    "error_code": 404
  },
  "data_sources": [],
  "slots": {
    "content": [
      {
        "id": "error_container",
        "type": "basic",
        "name": "Div",
        "props": {
          "className": "flex flex-col items-center justify-center min-h-[60vh] text-center p-8"
        },
        "children": [
          {
            "id": "error_code",
            "type": "basic",
            "name": "H1",
            "props": {
              "className": "text-6xl font-bold text-gray-900 dark:text-white mb-4"
            },
            "text": "404"
          },
          {
            "id": "error_title",
            "type": "basic",
            "name": "H2",
            "props": {
              "className": "text-2xl font-semibold text-gray-700 dark:text-gray-300 mb-2"
            },
            "text": "$t:errors.404.title"
          },
          {
            "id": "error_message",
            "type": "basic",
            "name": "P",
            "props": {
              "className": "text-gray-500 dark:text-gray-400 mb-8"
            },
            "text": "$t:errors.404.message"
          },
          {
            "id": "back_button",
            "type": "basic",
            "name": "Button",
            "props": {
              "className": "px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg"
            },
            "text": "$t:errors.back_home",
            "actions": [
              {
                "type": "click",
                "handler": "navigate",
                "params": { "path": "/" }
              }
            ]
          }
        ]
      }
    ]
  }
}

403.json, 500.json도 동일한 구조로 생성 (error_code와 아이콘만 다름)


Phase 3: 빌드 설정

3.1 package.json

{
  "name": "vendor-template",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "vite build",
    "dev": "vite build --watch"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "@vitejs/plugin-react": "^4.0.0",
    "autoprefixer": "^10.4.14",
    "postcss": "^8.4.24",
    "tailwindcss": "^3.3.2",
    "typescript": "^5.0.0",
    "vite": "^5.0.0"
  }
}

3.2 vite.config.ts

주의: minify는 'esbuild' 사용 (terser는 별도 설치 필요)
필수: process.env.NODE_ENV 정의 (외부 라이브러리 호환성)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  define: {
    'process.env.NODE_ENV': JSON.stringify('production'),
  },
  build: {
    lib: {
      entry: path.resolve(__dirname, 'src/index.ts'),
      name: 'VendorTemplate',
      formats: ['iife'],
      fileName: () => 'components.iife.js',
    },
    outDir: 'dist',
    emptyDirBeforeWrite: true,
    minify: 'esbuild',  // terser 대신 esbuild 사용!
    rollupOptions: {
      external: ['react', 'react-dom'],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
        },
      },
    },
  },
});

3.3 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"]
}

3.4 tailwind.config.js

/** @type {import('tailwindcss').Config} */
export default {
  content: ['./src/**/*.{js,ts,jsx,tsx}', './layouts/**/*.json'],
  darkMode: 'class',
  theme: { extend: {} },
  plugins: [],
};

3.5 postcss.config.js

export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

Phase 4: 컴포넌트 설정

4.1 src/index.ts

필수: 실제 존재하는 컴포넌트만 import!
존재하지 않는 컴포넌트를 import하면 빌드 실패
// 실제 존재하는 컴포넌트만 import
import * as basicComponents from './components/basic';
import * as compositeComponents from './components/composite';
import * as layoutComponents from './components/layout';
import { handlerMap } from './handlers';

// 전역 등록
declare global {
  interface Window {
    G7Core: {
      registerComponents: (components: Record<string, unknown>) => void;
      registerHandlers: (handlers: Record<string, unknown>) => void;
    };
  }
}

// 컴포넌트 등록
const allComponents = {
  ...basicComponents,
  ...compositeComponents,
  ...layoutComponents,
};

if (window.G7Core) {
  window.G7Core.registerComponents(allComponents);
  window.G7Core.registerHandlers(handlerMap);
}

export { allComponents, handlerMap };

4.2 src/handlers/index.ts

export const handlerMap = {
  // 커스텀 핸들러 등록
} as const;

export type HandlerName = keyof typeof handlerMap;

4.3 src/styles/main.css

@tailwind base;
@tailwind components;
@tailwind utilities;

4.4 레이아웃 편집기 확장점 등록 (선택)

템플릿이 자기 컴포넌트의 커스텀 속성 위젯(예 아이콘 피커)·노드 에디터·캔버스 인플레이스 오버레이를 레이아웃 편집기에 추가하려면 G7Core.layoutEditor 확장점에 등록한다. 등록은 src/index.ts 최상위(module-load)에서 호출한다 — initTemplate/window.load 게이트 안에 두면 편집기 직접 진입 시 위젯이 누락되는 회귀가 발생한다(코어 stub 예약 접수함이 큐로 보존하므로 module-load 시점 등록이 안전).

import { registerMyTemplateEditorWidgets } from './layout-editor/registerEditorWidgets';

// index.ts 최상위 — initTemplate/window.load 밖
registerMyTemplateEditorWidgets();
// layout-editor/registerEditorWidgets.ts
export function registerMyTemplateEditorWidgets(): void {
  if (typeof window === 'undefined') return;
  const le = (window as any).G7Core?.layoutEditor;
  if (!le?.registerWidget) return; // stub 부재 → 큐가 보존
  le.registerWidget('icon-picker', IconPickerWidget);
  le.registerCanvasOverlay?.('tabnav', TabNavInplaceOverlay);
}

확장점 API·등록 시점·작성 규약 상세: editor-spec.md "편집기 확장점". identity launcher 등록(G7Core.identity.setLauncher)과 동일한 옵셔널 체이닝 가드 패턴이다.


Phase 5: 다국어 파일

5.1 lang/ko.json

필수: errors 섹션 필수! (에러 레이아웃에서 참조)
{
  "site": {
    "title": "사이트 제목"
  },
  "errors": {
    "404": {
      "title": "페이지를 찾을 수 없습니다",
      "message": "요청하신 페이지가 존재하지 않거나 이동되었습니다."
    },
    "403": {
      "title": "접근이 거부되었습니다",
      "message": "이 페이지에 접근할 권한이 없습니다."
    },
    "500": {
      "title": "서버 오류",
      "message": "서버에 문제가 발생했습니다. 잠시 후 다시 시도해 주세요."
    },
    "back_home": "홈으로 돌아가기"
  }
}

5.2 lang/en.json

{
  "site": {
    "title": "Site Title"
  },
  "errors": {
    "404": {
      "title": "Page Not Found",
      "message": "The page you requested does not exist or has been moved."
    },
    "403": {
      "title": "Access Denied",
      "message": "You do not have permission to access this page."
    },
    "500": {
      "title": "Server Error",
      "message": "Something went wrong on our end. Please try again later."
    },
    "back_home": "Back to Home"
  }
}

Phase 6: 빌드 및 설치

6.1 빌드

php artisan template:build vendor-template

6.2 설치

php artisan template:install vendor-template

6.3 활성화

php artisan template:activate vendor-template

검증 로직 이해

설치 시 자동 검증 순서

  1. JSON 파싱 검증: 모든 레이아웃 파일의 JSON 유효성
  2. layout_name 필수: 템플릿 레이아웃은 반드시 layout_name 필드 필요
  3. error_config 검증: template.json에 error_config.layouts 섹션 필수
  4. 에러 레이아웃 검증: 404, 403, 500 레이아웃 파일 존재 확인
  5. partial 병합 검증: partial 참조 파일 존재 및 순환 참조 확인

ValidatesLayoutFiles 트레이트

// 스캔 대상 디렉토리
layouts/*.json          // ✅ 스캔 (DB 저장)
layouts/errors/*.json   // ✅ 스캔 (DB 저장)
layouts/partials/*.json // ❌ 스캔 안 함 (extends로만 참조)

TemplateManager::validateErrorLayouts()

// 필수 에러 코드
$requiredErrorCodes = [404, 403, 500];

// 검증 항목
1. error_config.layouts 섹션 존재
2. 404, 403, 500 키 존재
3. layouts/errors/{code}.json 파일 존재

자주 발생하는 오류와 해결

오류 1: layout_name 필드 누락

❌ 오류: layout_name 필드가 누락되었습니다

원인: routes.json 또는 레이아웃 파일에 layout_name 필드 없음

해결:

{
  "layout_name": "routes",  // 추가
  // ... 나머지 내용
}

오류 2: 에러 레이아웃 파일 없음

❌ 오류: 404 에러 레이아웃 파일을 찾을 수 없습니다

원인: layouts/errors/404.json 파일 없음

해결:

  1. layouts/errors/ 디렉토리 생성
  2. 404.json, 403.json, 500.json 파일 생성

오류 3: terser not found

❌ 오류: Cannot find module 'terser'

원인: vite.config.ts에서 minify: 'terser' 설정했지만 terser 미설치

해결:

// vite.config.ts
build: {
  minify: 'esbuild',  // terser 대신 esbuild 사용
}

오류 4: 컴포넌트 import 실패

❌ 오류: Module not found: './components/basic/H5'

원인: 존재하지 않는 컴포넌트를 import

해결: 실제 존재하는 컴포넌트만 import

// ❌ 잘못된 예
import { H5, H6, Box } from './components/basic';  // H5, H6, Box가 없으면 에러

// ✅ 올바른 예
import { H1, H2, H3, Div, Span } from './components/basic';  // 실제 존재하는 것만

오류 5: error_config 누락

❌ 오류: 에러 페이지 설정(error_config)이 누락되었습니다

원인: template.json에 error_config 섹션 없음

해결:

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

템플릿 개발 Artisan 명령어

명령어 설명
php artisan template:list 템플릿 목록 조회
php artisan template:build [id] 템플릿 빌드
php artisan template:build --all 모든 템플릿 빌드
php artisan template:install [id] 템플릿 설치
php artisan template:activate [id] 템플릿 활성화
php artisan template:deactivate [id] 템플릿 비활성화
php artisan template:uninstall [id] 템플릿 삭제
php artisan template:refresh-layout [id] 레이아웃 갱신 (빌드 없이)
php artisan template:cache-clear 템플릿 캐시 초기화

관련 문서