# 레이아웃 JSON 스키마 > **관련 문서**: [컴포넌트 개발](components.md) | [데이터 바인딩](data-binding.md) | [데이터 소스](data-sources.md) --- ## TL;DR (5초 요약) ```text 1. HTML 태그 직접 사용 금지 → 기본 컴포넌트 사용 (Div, Button, Span) 2. 텍스트: text 속성 또는 children 내 Span 사용 (텍스트 직접 배치 금지) 3. 다국어: "$t:key" 형식, 파라미터: "$t:key|param={{value}}" 4. 데이터 바인딩: "{{path.to.data}}" 형식 5. 다크 모드: light/dark 클래스 쌍 필수 (bg-white dark:bg-gray-800) 6. 새 속성 추가 시: UpdateLayoutContentRequest rules에도 추가 필수 7. 권한 제어: permissions 필드로 접근 권한 설정 (AND 로직, 401/403 응답) 8. globalHeaders: 패턴 기반 공통 헤더 자동 주입 (engine-v1.16.0+) ``` --- ## 분리된 문서 이 문서는 가독성을 위해 다음과 같이 분리되었습니다:dff | 문서 | 내용 | |------|------| | **layout-json.md** (현재) | 개요, 필수 필드, 컴포넌트 정의, 텍스트 렌더링 | | [layout-json-features.md](layout-json-features.md) | 에러 핸들링, 초기화 액션, 모달 시스템, 액션 시스템 | | [layout-json-components.md](layout-json-components.md) | 반복 렌더링(iteration), Blur 효과, 컴포넌트 생명주기 | | [layout-json-inheritance.md](layout-json-inheritance.md) | 레이아웃 상속, Partial, 병합 구조, 상속 체인 검증 | --- ## 목차 1. [개요](#개요) 2. [필수 필드](#필수-필드) 3. [레이아웃 권한 (permissions)](#레이아웃-권한-permissions) 4. [컴포넌트 정의](#컴포넌트-정의) 5. [격리된 상태 (isolatedState)](#격리된-상태-isolatedstate) 6. [텍스트 렌더링](#텍스트-렌더링) --- ## 개요 **레이아웃 JSON**은 화면 구성을 정의한 JSON 파일로, 데이터베이스 `template_layouts` 테이블에 저장됩니다. 그누보드7 템플릿 시스템에서 레이아웃 JSON은 다음 역할을 합니다: - 화면에 표시될 컴포넌트 구조 정의 - API 데이터 소스 연결 - 사용자 인터랙션(액션) 처리 - 다국어 및 데이터 바인딩 지원 --- ## 필수 필드 ```json { "version": "1.0.0", "layout_name": "dashboard", "meta": { "title": "$t:dashboard.title", "description": "$t:dashboard.description" }, "data_sources": [], "components": [] } ``` **필드 설명**: | 필드 | 타입 | 필수 | 설명 | |------|------|------|------| | `version` | string | ✅ | 스키마 버전 (예: "1.0.0") | | `layout_name` | string | ✅ | 레이아웃 식별자 (아래 네이밍 규칙 참조) | | `meta` | object | ❌ | 메타 정보 (title, description) | | `data_sources` | array | ✅ | API 데이터 소스 정의 (각 항목에 `label_key` `$t:` 키 — 편집기 데이터 연결 피커 친화 명칭) | | `init_actions` | array | ❌ | 초기화 액션 (레이아웃 로드 시 실행) | | `modals` | array | ❌ | 모달 컴포넌트 정의 | | `scripts` | array | ❌ | 외부 스크립트 동적 로드 (engine-v1.8.0+) | | `errorHandling` | object | ❌ | 레이아웃 레벨 에러 핸들링 설정 (engine-v1.6.0+) | | `permissions` | string[] | ❌ | 레이아웃 접근 권한 식별자 배열 (engine-v1.15.0+) | | `globalHeaders` | array | ❌ | 전역 HTTP 헤더 규칙 배열 (engine-v1.16.0+) | | `meta.seo` | object | ❌ | SEO 페이지 생성기 설정 (아래 참조) | | `components` | array | ✅ | 컴포넌트 배열 | ### 한 객체에 같은 키를 두 번 쓰지 않는다 JSON 은 중복 키를 문법 오류로 보지 않는다. 브라우저(`JSON.parse`)도 서버 등록(`json_decode`)도 **뒤에 온 값이 앞의 값을 덮는다.** 그래서 앞에 쓴 선언은 예외도 경고도 없이 사라지는데, 파일에는 그대로 남아 있으므로 코드를 읽는 사람에게는 반영된 것처럼 보인다. 가장 자주 걸리는 자리는 `props` 다 — 노드가 `children` **뒤**에 `"props": {}` 를 이미 갖고 있는데 작성자가 노드 앞머리에 `"props": { ... }` 를 새로 적는 경우다. 노드가 길면 앞머리와 꼬리가 한 화면에 들어오지 않아 눈으로는 발견되지 않고, 그 속성만 화면에 영영 나타나지 않는다. | ❌ 금지 | ✅ 올바른 사용 | |--------|---------------| | 한 노드에 `props`(또는 `comment`·`actions` 등)를 두 번 선언 | 하나로 합친다 — 값을 추가할 때는 그 노드에 **이미 있는** 키를 찾아 거기에 넣는다 | | 노드 앞머리에 키를 추가하기 전에 꼬리를 확인하지 않음 | 노드 전체에서 그 키의 존재를 먼저 확인한다 | 의도적으로 재선언해야 하는 예외는 그 객체의 `comment` 에 `audit:allow layout-json-duplicate-object-key <사유>` 를 남긴다 (JSON 은 주석을 담을 수 없으므로 표식 자리가 `comment` 값이다). 정적 검사가 차단한다. ### layout_name 네이밍 규칙 ```text ✅ 허용 문자: 영문(a-z, A-Z), 숫자(0-9), 언더스코어(_), 슬래시(/), 하이픈(-), 점(.) ✅ 슬래시 사용: 계층 구조 표현 가능 (예: "board/popular", "mypage/orders/show") ✅ 언더스코어 사용: 플랫 구조 표현 가능 (예: "admin_dashboard", "admin_user_list") 파일 경로와 일치: layout_name은 파일 경로(확장자 제외)와 동일해야 함 ``` **예시**: | 파일 경로 | layout_name | |----------|-------------| | `layouts/home.json` | `home` | | `layouts/board/popular.json` | `board/popular` | | `layouts/mypage/orders/show.json` | `mypage/orders/show` | | `layouts/admin_dashboard.json` | `admin_dashboard` | > **상세 문서**: > - `errorHandling`: [layout-json-features.md](layout-json-features.md#에러-핸들링-설정-errorhandling) > - `init_actions`: [layout-json-features.md](layout-json-features.md#초기화-액션-init_actions) > - `modals`: [layout-json-features.md](layout-json-features.md#모달-시스템-modals) > - `scripts`: [layout-json-features.md](layout-json-features.md#외부-스크립트-로드-scripts) > - `permissions`: [아래 섹션](#레이아웃-권한-permissions) > - `globalHeaders`: [아래 섹션](#전역-헤더-globalheaders) > - `meta.seo`: [아래 섹션](#metaseo-seo-페이지-생성기) ### meta.seo (SEO 페이지 생성기) | 필드 | 타입 | 필수 | 설명 | |------|------|------|------| | seo.enabled | boolean | X | SEO 렌더링 활성화 (기본: false) | | seo.data_sources | string[] | X | SEO 시 사전 로드할 data_sources ID | | seo.extensions | array | X | SEO 변수 제공 확장 목록 (아래 참조) | | seo.page_type | string | X | 확장 설정 SEO 템플릿 키 결정 (예: `"product"`) | | seo.toggle_setting | string | X | SEO 활성화 토글 설정 경로 | | seo.vars | object | X | SEO 변수 선언 — data 소스 변수의 표현식 매핑 | | seo.priority | number | X | sitemap priority (0.0~1.0) | | seo.changefreq | string | X | sitemap changefreq (daily/weekly 등) | | seo.og | object | X | Open Graph 메타태그 | | seo.structured_data | object | X | JSON-LD 구조화 데이터 | #### seo.extensions SEO 변수를 제공하는 확장(모듈/플러그인)을 선언합니다. 여기에 선언된 확장의 `seoVariables()` 메서드가 호출되어, `page_type`에 해당하는 변수가 자동 해석됩니다. ```json "extensions": [ { "type": "module", "id": "sirsoft-ecommerce" }, { "type": "plugin", "id": "sirsoft-payment" } ] ``` | 필드 | 타입 | 필수 | 설명 | |------|------|------|------| | type | `"module"` \| `"plugin"` | ✅ | 확장 타입 | | id | string | ✅ | 확장 식별자 (예: `"sirsoft-ecommerce"`) | 이 방식은 레이아웃 이름에 dot-notation `moduleIdentifier`를 포함할 필요 없이, 레이아웃이 어떤 확장의 SEO 변수를 사용하는지 명시적으로 선언합니다. Partial에 meta.seo 정의 금지 (무시됨) meta.seo 추가/변경 시 UpdateLayoutContentRequest 검증 규칙 동기화 필수 data_sources는 부모-자식 간 합집합 병합 (permissions와 동일 전략) > 상세: [seo-system.md](../backend/seo-system.md) --- ## 레이아웃 권한 (permissions) > **버전**: engine-v1.15.0+ 레이아웃 접근에 필요한 권한을 정의합니다. 권한이 없는 사용자는 레이아웃 서빙 API에서 401/403 응답을 받습니다. ### 기본 문법 ```json { "version": "1.0.0", "layout_name": "admin_dashboard", "permissions": ["core.dashboard.read"], "meta": { "title": "$t:admin.dashboard.title" }, "components": [...] } ``` ### 동작 규칙 | 조건 | 동작 | |------|------| | `permissions` 필드 없음 또는 빈 배열 | 공개 레이아웃 (권한 체크 없음) | | `permissions`에 값이 있음 | 모든 권한 필요 (AND 로직) | | 비회원 + 권한 없음 | 401 Unauthorized | | 회원 + 권한 없음 | 403 Forbidden | | Admin 역할 | 모든 권한 자동 통과 | ### 권한 식별자 형식 ```text ✅ 권한 식별자 규칙: [모듈명].[엔티티].[액션] ✅ 허용 문자: 영문(a-z), 숫자(0-9), 하이픈(-), 언더스코어(_), 점(.) ✅ 예시: core.dashboard.read, sirsoft-ecommerce.products.read ``` ### 상속 시 권한 병합 부모-자식 레이아웃 상속 시 permissions는 **합집합**으로 병합됩니다: ```text 부모: { "permissions": ["core.admin.access"] } 자식: { "permissions": ["core.users.read"] } 결과: { "permissions": ["core.admin.access", "core.users.read"] } ``` - 중복 제거 적용 (`array_unique`) - 병합 결과의 모든 권한을 만족해야 접근 가능 (AND 로직) ### 예시 #### 관리자 레이아웃 ```json { "version": "1.0.0", "layout_name": "admin_user_list", "permissions": ["core.users.read"], "meta": { "title": "$t:admin.users.title" }, "components": [...] } ``` #### 모듈 관리 레이아웃 ```json { "version": "1.0.0", "layout_name": "admin_ecommerce_product_list", "permissions": ["sirsoft-ecommerce.products.read"], "meta": { "title": "$t:admin.ecommerce.products.title" }, "components": [...] } ``` #### 공개 레이아웃 (권한 없음) ```json { "version": "1.0.0", "layout_name": "home", "meta": { "title": "$t:common.home" }, "components": [...] } ``` ### 일괄 업데이트 커맨드 기존 레이아웃에 권한을 일괄 추가하려면 Artisan 커맨드를 사용합니다: ```bash # 시뮬레이션 (실제 수정 없음) php artisan layout:add-permissions --dry-run # 실제 실행 php artisan layout:add-permissions # 특정 레이아웃만 php artisan layout:add-permissions --layout=admin_dashboard ``` ### 주의사항 ```text 권한 식별자는 DB의 permissions 테이블에 존재해야 함 빈 배열 []은 권한 체크 없음과 동일 (공개 레이아웃) 프론트엔드는 401/403 응답 시 적절한 UI 처리 필요 (로그인 페이지 리다이렉트 등) ``` --- ## 컴포넌트별 권한 (Component Permissions) > **버전**: engine-v1.17.0+ 개별 컴포넌트에 `permissions` 속성을 추가하면, **서버 사이드에서 해당 컴포넌트를 JSON에서 제거**하여 서빙합니다. 프론트엔드 `if` 조건부 렌더링과 달리, 권한 없는 컴포넌트의 구조 자체가 클라이언트에 노출되지 않습니다. ### 기본 문법 ```json { "type": "basic", "name": "Div", "id": "admin_widget", "permissions": ["core.dashboard.admin-widget"], "children": [ { "type": "basic", "name": "H2", "text": "관리자 전용 위젯" } ] } ``` ### 동작 규칙 | 조건 | 동작 | |------|------| | `permissions` 없음 또는 빈 배열 | 항상 포함 (권한 체크 없음) | | `permissions`에 값이 있음 | 모든 권한 필요 (AND 로직) | | 권한 없음 | 컴포넌트 + 하위 children 전체 제거 | | Admin 역할 | 모든 컴포넌트 자동 통과 | ### 필터링 대상 영역 | 영역 | 설명 | |------|------| | `components` | 메인 컴포넌트 트리 (재귀 children 포함) | | `modals[]` | 모달 자체에 permissions → 모달 전체 제거 | | `modals[].components` | 모달 내부 컴포넌트 트리 | | `defines` | 재사용 컴포넌트 정의 | ### 상위-하위 중복 선언 각 노드는 자기 permissions만 독립 평가합니다. 별도 병합 정책 없음: ```json { "type": "basic", "name": "Div", "permissions": ["core.users.read"], "children": [ { "type": "basic", "name": "Button", "permissions": ["core.users.delete"], "text": "삭제" } ] } ``` | 시나리오 | 상위 | 하위 | 결과 | |---------|------|------|------| | read 없음 | 전체 제거 | 평가 안 됨 | 둘 다 없음 | | read 있음 + delete 없음 | 통과 | 제거 | Div만 남음 | | read 있음 + delete 있음 | 통과 | 통과 | 둘 다 남음 | ### 레이아웃 최상위 permissions와의 차이 | 구분 | 레이아웃 최상위 | 컴포넌트별 | |------|---------------|-----------| | **범위** | 레이아웃 전체 접근 제어 | 개별 컴포넌트 표시/숨김 | | **실패 시** | 401/403 에러 응답 | 해당 컴포넌트만 JSON에서 제거 | | **캐싱** | 캐시 전 체크 | post-cache 필터링 (캐시 영향 없음) | ### 확장(모듈/플러그인) 컴포넌트 Extension Point 또는 Overlay로 주입된 컴포넌트에도 `permissions`를 선언할 수 있습니다. 필터링은 `applyExtensions()` 이후에 수행되므로 자동 처리됩니다. ### 주의사항 ```text 권한 식별자는 DB의 permissions 테이블에 존재해야 함 보안 목적: 프론트엔드 if 조건과 달리 JSON에 컴포넌트 구조가 노출되지 않음 필터링 후 permissions 속성 자체도 제거됨 (클라이언트 노출 방지) post-cache filtering — 기존 캐시 구조에 영향 없음 ``` --- ## 전역 헤더 (globalHeaders) > **버전**: engine-v1.16.0+ 레이아웃에서 정의한 모든 API 호출(data_sources, apiCall 핸들러)에 공통 헤더를 자동으로 주입합니다. ### 기본 문법 ```json { "version": "1.0.0", "layout_name": "shop_cart", "globalHeaders": [ { "pattern": "*", "headers": { "X-Request-Source": "user-template" } }, { "pattern": "/api/modules/sirsoft-ecommerce/*", "headers": { "X-Cart-Key": "{{_global.cartKey}}" } } ], "components": [...] } ``` ### globalHeaders 배열 구조 | 필드 | 타입 | 필수 | 설명 | |------|------|------|------| | `pattern` | string | ✅ | 엔드포인트 매칭 패턴 (`*`, `/api/shop/*` 등) | | `headers` | object | ✅ | 헤더 키-값 쌍 (표현식 지원) | ### 패턴 매칭 규칙 | 패턴 | 설명 | 매칭 예시 | |------|------|----------| | `*` | 모든 API | 전체 적용 | | `/api/modules/sirsoft-ecommerce/*` | ecommerce 모듈 API | `/api/modules/sirsoft-ecommerce/cart`, `/api/modules/sirsoft-ecommerce/products/123` | | `/api/shop/*` | shop prefix API | `/api/shop/products`, `/api/shop/categories` | | `/api/cart` | 정확히 일치 | `/api/cart`만 (하위 경로 제외) | ### 표현식 지원 헤더 값에서 데이터 바인딩 표현식을 사용할 수 있습니다: ```json { "globalHeaders": [ { "pattern": "/api/modules/sirsoft-ecommerce/*", "headers": { "X-Cart-Key": "{{_global.cartKey}}", "X-User-Locale": "{{_global.locale ?? 'ko'}}" } } ] } ``` **지원되는 컨텍스트**: | 컨텍스트 | 설명 | |----------|------| | `{{_global.xxx}}` | 전역 상태 값 | | `{{_local.xxx}}` | 로컬 상태 값 | | `{{route.xxx}}` | 라우트 파라미터 | | `{{query.xxx}}` | 쿼리 파라미터 | ### 헤더 우선순위 ```text 낮음 ← globalHeaders ← source.headers / params.headers → 높음 ``` 개별 API의 `headers` 속성이 `globalHeaders`의 동일한 키를 덮어씁니다: ```json { "globalHeaders": [ { "pattern": "*", "headers": { "X-Custom": "global-value" } } ], "data_sources": [ { "id": "products", "endpoint": "/api/products", "headers": { "X-Custom": "source-value" } // globalHeaders보다 우선 } ] } ``` ### 적용 범위 | 적용 대상 | 설명 | |----------|------| | `data_sources` | 레이아웃의 data_sources 배열에서 정의한 API | | `apiCall` 핸들러 | actions에서 `handler: "apiCall"`로 호출하는 API | | `refetchDataSource` 핸들러 | DataSourceManager를 통해 호출 (자동 적용) | ### 상속 시 병합 부모-자식 레이아웃 상속 시 globalHeaders는 **pattern 기준으로 병합**됩니다: ```text 부모: [{ pattern: "*", headers: { "X-Template": "basic" } }] 자식: [{ pattern: "*", headers: { "X-Page": "cart" } }] 결과: [{ pattern: "*", headers: { "X-Template": "basic", "X-Page": "cart" } }] ``` - 동일 pattern: headers가 병합됨 (자식이 동일 키 덮어씀) - 다른 pattern: 별도로 유지 > **상세 문서**: [layout-json-inheritance.md](layout-json-inheritance.md#globalheaders-병합) ### 주의사항 ```text 표현식 값이 빈 문자열/undefined/null이면 해당 헤더는 전송되지 않음 _isolated 컨텍스트는 지원하지 않음 (data_sources fetch 시점에 격리 스코프 미확정) 시스템 내부 API(레이아웃 로드, 번역 파일 등)에는 적용되지 않음 ``` --- ## 새 속성 추가 규정 ```text 주의: 레이아웃 JSON에 새로운 최상위 속성 추가 시 백엔드 작업 필수 필수: UpdateLayoutContentRequest rules()에도 새 속성 추가 프론트엔드만 수정하고 백엔드 누락 시 저장 후 데이터 사라짐 ``` ### 왜 필요한가? Laravel의 `FormRequest::validated()` 메서드는 `rules()`에 정의된 필드만 반환합니다. 따라서: 1. 레이아웃 JSON에 새 속성을 추가해도 2. `UpdateLayoutContentRequest::rules()`에 없으면 3. 저장 시 해당 속성이 **삭제**됩니다 ### 새 속성 추가 체크리스트 ``` □ 프론트엔드: 레이아웃 JSON에 새 속성 추가 □ 백엔드: UpdateLayoutContentRequest::rules()에 규칙 추가 □ 백엔드: UpdateLayoutContentRequest::messages()에 에러 메시지 추가 □ 다국어: lang/ko/validation.php, lang/en/validation.php 메시지 추가 □ 테스트: 해당 속성이 저장되는지 테스트 추가 ``` ### 파일 위치 - FormRequest: `app/Http/Requests/Layout/UpdateLayoutContentRequest.php` - 다국어: `lang/ko/validation.php`, `lang/en/validation.php` - 테스트: `tests/Feature/Api/Admin/LayoutControllerTest.php` ### 예시: 새 속성 `custom_field` 추가 ```php // UpdateLayoutContentRequest::rules() 'content.custom_field' => ['nullable', 'array'], // UpdateLayoutContentRequest::messages() 'content.custom_field.array' => __('validation.layout.custom_field.array'), ``` --- ## 컴포넌트 정의 ```json { "id": "unique_id", "type": "basic|composite|layout", "name": "ComponentName", "props": { "title": "$t:dashboard.title", "value": "{{data.field}}" }, "children": [], "actions": [ { "type": "click", "handler": "navigate", "target": "/path" } ], "data_binding": { "value": "data.field" } } ``` **필드 설명**: | 필드 | 타입 | 필수 | 설명 | |------|------|------|------| | `id` | string | ✅ | 컴포넌트 고유 ID | | `type` | string | ✅ | 컴포넌트 타입 (basic/composite/layout) | | `name` | string | ✅ | 컴포넌트 이름 (components.json에 등록된 이름) | | `comment` | string | ✅ | 시안 추적 코드 및 역할 설명 (예: "F-002: 상품명 검색 필드"). 공개 서빙 응답에서는 제거됨 (아래 참조) | | `props` | object | ❌ | 컴포넌트에 전달할 props | | `text` | string | ❌ | 텍스트 콘텐츠 (최우선 렌더링) | | `children` | array | ❌ | 자식 컴포넌트 배열 (레이아웃/집합 컴포넌트에서 사용) | | `iteration` | object | ❌ | 반복 렌더링 설정 (source, item_var, index_var) | | `actions` | array | ❌ | 액션 핸들러 정의 (클릭, 키보드, 폼 제출 등) | | `lifecycle` | object | ❌ | 생명주기 핸들러 (onMount, onUnmount) | | `data_binding` | object | ❌ | 데이터 바인딩 정의 | | `blur_until_loaded` | boolean \| string | ❌ | 데이터 로딩 중 blur 효과 적용 (boolean 또는 표현식 문자열) | | `component_layout` | object | ❌ | 컴포넌트 내부 커스텀 렌더링 레이아웃 정의 (RichSelect 등에서 사용) | | `isolatedState` | object | ❌ | 격리된 상태 초기값 정의 (engine-v1.14.0+) | | `isolatedScopeId` | string | ❌ | 격리된 스코프 식별자 (engine-v1.14.0+) | | `autoBinding` | boolean | ❌ | `false`로 설정 시 해당 필드의 폼 자동 바인딩을 명시적으로 비활성화. 커스텀 핸들러나 G7Core API로 상태 관리 시 사용 (engine-v1.17.6+) | > **상세 문서**: > - `iteration`: [layout-json-components.md](layout-json-components.md#반복-렌더링-iteration) > - `blur_until_loaded`: [layout-json-components.md](layout-json-components.md#데이터-로딩-중-blur-효과-blur_until_loaded) > - `lifecycle`: [layout-json-components.md](layout-json-components.md#컴포넌트-생명주기-lifecycle) > - `actions`: [layout-json-features.md](layout-json-features.md#액션-시스템-actions) > - `isolatedState`: [아래 섹션](#격리된-상태-isolatedstate) ### `comment` / `_comment` — 공개 서빙 응답에서 제거 `comment` / `_comment` 는 편집기·개발자용 설명 주석이며 런타임 렌더러는 참조하지 않는다. 공개 레이아웃 서빙(`GET /api/layouts/{template}/{layout}.json`) 응답에서는 전송 크기 절감을 위해 이 두 키가 노드 트리 전체에서 재귀적으로 제거된다(`LayoutService::stripDeveloperComments`). - 편집 모드 서빙(`?with_source_meta=1`, 편집 권한 필요)에서는 편집기가 `comment` 를 편집 가능한 속성으로 노출하므로 **보존**된다. 공개/편집 응답은 별도 캐시 키로 분리되어 있어 조건부 제거가 안전하다. - 따라서 `comment` 는 편집기·저장본에서는 유지되고, 일반 사용자에게 서빙되는 응답에서만 사라진다. 레이아웃 로직이 `comment` 값에 의존해서는 안 된다. --- ## 격리된 상태 (isolatedState) > **버전**: engine-v1.14.0+ 특정 컴포넌트 영역 내에서만 유효한 격리된 상태를 정의합니다. 해당 영역의 상태 변경 시 전체 레이아웃이 아닌 격리된 영역만 리렌더링되어 성능이 향상됩니다. ### 기본 문법 ```json { "type": "Div", "isolatedState": { "selectedCategories": [], "currentStep": 1, "isExpanded": false }, "isolatedScopeId": "category-selector", "children": [...] } ``` ### 속성 설명 | 속성 | 타입 | 필수 | 설명 | | ---- | ---- | ---- | ---- | | `isolatedState` | object | ✅ | 격리된 상태의 초기값을 정의하는 객체 | | `isolatedScopeId` | string | ❌ | 외부에서 접근할 때 사용하는 스코프 식별자 | ### 상태 접근 방법 격리된 상태는 `_isolated` 접두사로 접근합니다: ```json { "type": "basic", "name": "Button", "props": { "disabled": "{{_isolated.currentStep === 1}}" }, "text": "{{_isolated.selectedCategories.length}}개 선택됨" } ``` ### 상태 업데이트 `setState` 핸들러에서 `target: "isolated"` 옵션을 사용합니다: ```json { "type": "click", "handler": "setState", "params": { "target": "isolated", "currentStep": "{{_isolated.currentStep + 1}}" } } ``` ### 사용 시점 | 상황 | 권장 상태 | 이유 | | ---- | --------- | ---- | | 빈번한 사용자 인터랙션 | `_isolated` | 리렌더링 최소화 | | 독립적인 UI 영역 | `_isolated` | 다른 영역과 무관한 상태 | | 레이아웃 전체에서 공유 필요 | `_local` | 여러 컴포넌트에서 접근 | | 페이지 이동 후에도 유지 필요 | `_global` | 앱 전역 상태 | ### 라이프사이클 ```text 1. 생성: isolatedState 속성이 있는 컴포넌트 마운트 시 2. 업데이트: setState target:"isolated" 또는 G7Core.state.setIsolated() 3. 소멸: 해당 컴포넌트 언마운트 시 4. 페이지 이동: 초기화됨 (언마운트 → 재마운트) ``` ### 주의사항 ```text isolatedState 외부에서 target: "isolated" 사용 시 → _local로 폴백 + 경고 로그 isolatedScopeId 중복 시 → 마지막 등록된 스코프 우선 + 경고 로그 중첩 isolatedState 시 → 가장 가까운 스코프 사용 ``` ### 관련 문서 - [상태 관리 - _isolated](state-management.md#격리된-상태-_isolated) - [액션 핸들러 - target: isolated](actions-handlers-state.md#격리된-상태-변경-target-isolated) - [G7Core API - getIsolated/setIsolated](g7core-api.md#getisolated--setisolated-v1140) --- ## 텍스트 렌더링 ``` 중요: 컴포넌트에 텍스트를 표시할 때는 text 속성을 사용해야 합니다. 필수: text 속성 사용 (DynamicRenderer가 최우선 처리) 필수: children 배열 사용 (하위 컴포넌트 포함) 주의: props.children 사용 불가 (무시됨) ``` ### 핵심 원칙 그누보드7 템플릿 엔진의 DynamicRenderer는 다음 우선순위로 렌더링합니다: 1. **`text` 속성** (최우선) - 텍스트 콘텐츠 표시 2. **`children` 배열** - 하위 컴포넌트 렌더링 3. **`props`** - 일반 속성 전달 ### 올바른 패턴 ```json { "id": "page_title", "type": "basic", "name": "H1", "props": { "className": "text-2xl font-bold" }, "text": "대시보드" } ``` ```json { "id": "logo_text", "type": "basic", "name": "Span", "props": { "className": "text-white font-bold" }, "text": "G7" } ``` ### 잘못된 패턴 ```json { "id": "page_title", "type": "basic", "name": "H1", "props": { "className": "text-2xl font-bold", "children": "대시보드" // ❌ props.children은 무시됨 } } ``` ### 다국어 지원 `text` 속성에서도 다국어 키를 사용할 수 있습니다: ```json { "id": "page_title", "type": "basic", "name": "H1", "props": { "className": "text-2xl font-bold" }, "text": "$t:dashboard.title" } ``` ### 데이터 바인딩 지원 `text` 속성에서 데이터 바인딩도 가능합니다: ```json { "id": "user_name", "type": "basic", "name": "Span", "text": "{{user.name}}" } ``` ### children 배열과의 차이 ```json // ✅ text 속성: 단순 텍스트 표시 { "id": "title", "type": "basic", "name": "H1", "text": "제목" } // ✅ children 배열: 하위 컴포넌트 포함 { "id": "card", "type": "composite", "name": "Card", "children": [ { "id": "card_title", "type": "basic", "name": "H2", "text": "카드 제목" }, { "id": "card_content", "type": "basic", "name": "P", "text": "카드 내용" } ] } ``` ### DynamicRenderer 동작 방식 ```typescript // DynamicRenderer.tsx (코어 엔진) const renderChildren = useMemo(() => { // 1. text 속성이 있으면 최우선으로 사용 if (componentDef.text !== undefined) { return componentDef.text; } // 2. children이 없으면 null if (!componentDef.children || componentDef.children.length === 0) { return null; } // 3. children 배열 렌더링 return componentDef.children.map((childDef, index) => { // ... }); }, [componentDef.text, componentDef.children]); ``` ### 주의사항 - ❌ `props.children`은 DynamicRenderer에서 완전히 무시됩니다 - ✅ React 컴포넌트 개발 시 `children` prop은 정상 작동 (TSX 내부) - ✅ 레이아웃 JSON에서는 `text` 속성과 `children` 배열만 사용 ### 실제 사용 예시 ```json { "id": "sidebar_logo", "type": "basic", "name": "Div", "props": { "className": "flex items-center gap-3" }, "children": [ { "id": "logo_circle", "type": "basic", "name": "Div", "props": { "className": "flex items-center justify-center w-10 h-10 bg-black rounded-full" }, "children": [ { "id": "logo_text", "type": "basic", "name": "Span", "props": { "className": "text-white font-bold text-sm" }, "text": "G7" } ] }, { "id": "app_name", "type": "basic", "name": "Span", "props": { "className": "font-semibold text-lg" }, "text": "G7" } ] } ``` --- ## Extension Point 데이터 전달 (engine-v1.15.0+, callbacks: engine-v1.28.0+) `extension_point`에서 주입되는 컴포넌트에 데이터와 콜백을 전달합니다. - `props`: 데이터 전달용 — 표현식이 평가되어 전달됨 (`resolveObject` 재귀 평가) - `callbacks`: 액션 객체 전달용 — 평가 없이 그대로 전달 (ActionDispatcher가 실행 시점에 평가) ### 호스트 레이아웃 (extension_point 정의) ```json { "type": "extension_point", "name": "address_search_slot", "props": { "productId": "{{route.id}}", "readOnlyFields": ["zipcode", "address"] }, "callbacks": { "onAddressSelect": { "handler": "setState", "params": { "target": "local", "form.zipcode": "{{$event.zipcode}}" } } } } ``` ### 플러그인 extension JSON (주입되는 컴포넌트) ```json { "handler": "myPlugin.doSomething", "params": { "id": "{{extensionPointProps.productId}}", "fields": "{{extensionPointProps.readOnlyFields}}", "callbackAction": "{{extensionPointCallbacks.onAddressSelect}}" } } ``` - `{{extensionPointProps.xxx}}`: 호스트의 `props`에서 전달된 데이터 (표현식 평가됨) - `{{extensionPointCallbacks.xxx}}`: 호스트의 `callbacks`에서 전달된 액션 객체 (그대로 전달) | 필드 | 타입 | 필수 | 설명 | |------|------|------|------| | `props` | object | ❌ | 주입 컴포넌트에 전달할 데이터 (표현식 평가) | | `callbacks` | object | ❌ | 주입 컴포넌트에 전달할 액션 객체 (평가 없이 전달) | 전달된 값은 주입 컴포넌트와 그 자손 전체에서 참조할 수 있습니다. 검색 봇용 화면에서는 `extensionPointProps` 만 해석되고 `extensionPointCallbacks` 는 해석되지 않습니다. 색인되어야 할 내용은 `props` 로 전달하세요. 사용자 작성 콘텐츠를 넘길 때는 평문/HTML 판정 값(`isHtml`)을 함께 전달합니다 — 두 화면 모두 이 값에 따라 이스케이프하거나 위험 요소를 제거한 뒤 출력합니다. 봇 화면의 노드 문법 지원 범위는 [seo-system.md "SEO 렌더러 지원 노드 키"](../backend/seo-system.md), 정화 규칙은 같은 문서의 "봇 화면의 HTML 정화" 를 참조하세요. --- ## Deprecated 속성 하위 호환성을 위해 별칭이 유지되는 속성입니다. 새 코드에서는 권장 속성을 사용하세요. | Deprecated | 권장 속성 | 호환성 | 비고 | |-----------|----------|--------|------| | `init_actions` | `initActions` | 별칭 유지 (engine-v1.0.0+) | 카멜케이스 통일 | | `state` | `initLocal` | 별칭 유지 (engine-v1.11.0+) | `initLocal`이 더 명시적 | | `auth_required` | `auth_mode` | 별칭 유지 (engine-v1.0.0+) | `auth_mode`가 더 세밀한 제어 지원 | ```text 기존 레이아웃에서 deprecated 속성 사용 시 정상 동작 (별칭 유지) ✅ 신규 레이아웃에서는 권장 속성만 사용 ✅ 리팩토링 시 권장 속성으로 변경 권장 ``` --- ## 관련 문서 - [컴포넌트 개발 규칙](components.md) - basic, composite, layout 컴포넌트 - [데이터 바인딩](data-binding.md) - `{{}}` 표현식, `$t:` 다국어 - [데이터 소스](data-sources.md) - API 데이터 자동 fetch - [상태 관리](state-management.md) - `_global` 전역 상태