# 템플릿 개발 워크플로우

> **위치**: `docs/extension/template-workflow.md`
> **관련 문서**: [template-basics.md](./template-basics.md) | [template-commands.md](./template-commands.md) | [template-development.md](../frontend/template-development.md)

---

## TL;DR (5초 요약)

```text
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: 필수 메타데이터 파일](#phase-1-필수-메타데이터-파일)
4. [Phase 2: 레이아웃 파일](#phase-2-레이아웃-파일)
5. [Phase 3: 빌드 설정](#phase-3-빌드-설정)
6. [Phase 4: 컴포넌트 설정](#phase-4-컴포넌트-설정)
7. [Phase 5: 다국어 파일](#phase-5-다국어-파일)
8. [Phase 6: 빌드 및 설치](#phase-6-빌드-및-설치)
9. [검증 로직 이해](#검증-로직-이해)
10. [자주 발생하는 오류와 해결](#자주-발생하는-오류와-해결)
11. [템플릿 개발 Artisan 명령어](#템플릿-개발-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](changelog-rules.md#8-코어-버전-제약-정책) 참조.

### 1.1 template.json

```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",
      "url": "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css",
      "preconnect": "https://cdnjs.cloudflare.com"
    }
  ]
}
```

**필수 항목**:
- `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)"](template-basics.md)

### 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 필드가 없으면 설치 실패!
```

```json
{
  "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)

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

### 2.3 에러 레이아웃

**404.json 예시**:
```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

```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 정의 (외부 라이브러리 호환성)
```

```typescript
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

```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

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

### 3.5 postcss.config.js

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

---

## Phase 4: 컴포넌트 설정

### 4.1 src/index.ts

```
필수: 실제 존재하는 컴포넌트만 import!
존재하지 않는 컴포넌트를 import하면 빌드 실패
```

```typescript
// 실제 존재하는 컴포넌트만 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

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

export type HandlerName = keyof typeof handlerMap;
```

### 4.3 src/styles/main.css

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

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

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

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

// index.ts 최상위 — initTemplate/window.load 밖
registerMyTemplateEditorWidgets();
```

```typescript
// 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 "편집기 확장점"](editor-spec.md). identity launcher 등록(`G7Core.identity.setLauncher`)과 동일한 옵셔널 체이닝 가드 패턴이다.

---

## Phase 5: 다국어 파일

### 5.1 lang/ko.json

```
필수: errors 섹션 필수! (에러 레이아웃에서 참조)
```

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

### 5.2 lang/en.json

```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 빌드

```bash
php artisan template:build vendor-template
```

### 6.2 설치

```bash
php artisan template:install vendor-template
```

### 6.3 활성화

```bash
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 트레이트

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

### TemplateManager::validateErrorLayouts()

```php
// 필수 에러 코드
$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` 필드 없음

**해결**:
```json
{
  "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 미설치

**해결**:
```typescript
// vite.config.ts
build: {
  minify: 'esbuild',  // terser 대신 esbuild 사용
}
```

### 오류 4: 컴포넌트 import 실패

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

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

**해결**: 실제 존재하는 컴포넌트만 import
```typescript
// ❌ 잘못된 예
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` 섹션 없음

**해결**:
```json
{
  "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` | 템플릿 캐시 초기화 |

---

## 관련 문서

- [템플릿 기초](./template-basics.md) - 템플릿 타입, 메타데이터
- [템플릿 커맨드](./template-commands.md) - Artisan 커맨드 상세
- [템플릿 개발 가이드](../frontend/template-development.md) - 컴포넌트 개발
- [레이아웃 JSON 스키마](../frontend/layout-json.md) - 레이아웃 작성법
- [컴포넌트 개발 규칙](../frontend/components.md) - 컴포넌트 작성법
