v7.0.0-beta.2 release
This commit is contained in:
1 parent
6595fd0eb5
commit
fa8eba7a0d
1165 files changed
+107118
-762127
No files matched your search
@@ -9,8 +9,9 @@
|
||||
1. [navigate](#navigate)
|
||||
2. [navigateBack](#navigateback)
|
||||
3. [openWindow](#openwindow)
|
||||
4. [reloadRoutes](#reloadroutes)
|
||||
5. [refresh](#refresh)
|
||||
4. [reloadExtensions](#reloadextensions) ⭐ NEW (engine-v1.38.0+)
|
||||
5. [reloadRoutes](#reloadroutes) (deprecated)
|
||||
6. [refresh](#refresh)
|
||||
|
||||
---
|
||||
|
||||
@@ -36,6 +37,120 @@
|
||||
| `query` | object | - | 쿼리 파라미터 객체 |
|
||||
| `mergeQuery` | boolean | false | 기존 쿼리 파라미터와 병합 |
|
||||
| `replace` | boolean | false | URL만 변경 (페이지 리로드 없음) |
|
||||
| `transition_overlay_target` | string | - | `replace: true` 호출 시 `transition_overlay.target` 을 동적으로 override (engine-v1.36.0+) |
|
||||
| `scroll` | string \| number \| object | `"top"` | 이동 후 스크롤 위치 (engine-v1.37.0+). `"top"` / `"preserve"` / 숫자 / `{x,y}` / `"#selector"` |
|
||||
| `scrollBehavior` | string | `"instant"` | 스크롤 애니메이션 (engine-v1.37.0+). `"instant"` (즉시) / `"smooth"` (부드럽게). 기본값 `"instant"`는 템플릿 CSS의 `scroll-behavior: smooth` 전역 설정을 무시하고 페이지 전환 시 즉시 이동 |
|
||||
| `fallback` | `false \| string \| object` | `"openWindow"` | 대상 경로가 현재 템플릿의 `routes.json`에 없을 때 실행할 fallback 핸들러 (engine-v1.40.0+). 기본값은 `openWindow`(새 창). `false` 시 비활성화(기존 동작), 문자열은 핸들러명만 지정, 객체는 `{handler, params}` 상세 지정. 관리자 ↔ 사용자 템플릿 교차 경로에 사용 |
|
||||
|
||||
### transition_overlay_target 옵션 (engine-v1.36.0+)
|
||||
|
||||
`replace: true` 로 탭 전환/페이지네이션 시 `updateQueryParams` 경로에 진입할 때, 레이아웃의 `transition_overlay.target` 대신 **이 호출에 한해** 다른 영역에만 spinner 를 표시하도록 한다.
|
||||
|
||||
**주 용도**: 탭 속 서브 탭(환경설정 > 알림 탭 > 채널 탭 등) 클릭 시 서브 탭 콘텐츠 영역에만 spinner 가 표시되어야 할 때.
|
||||
|
||||
```json
|
||||
{
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/settings",
|
||||
"replace": true,
|
||||
"mergeQuery": true,
|
||||
"query": { "channel": "{{ch.id}}" },
|
||||
"transition_overlay_target": "notif_channel_content"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**동작**:
|
||||
|
||||
- 미지정 시: 레이아웃의 `transition_overlay.target` 사용 (탭 콘텐츠 wrapper 등)
|
||||
- 지정 시: 해당 ID 의 DOM 요소에만 spinner mount. 요소 미발견 시 `transition_overlay.fallback_target` → `#app` 순으로 3단계 폴백
|
||||
- `replace: true` 아닌 일반 navigate(다른 path)는 `handleRouteChange` 경로로 가며 이 옵션은 효과 없음
|
||||
|
||||
**DataGrid body 영역 한정 spinner 컨벤션**:
|
||||
|
||||
목록 페이지 페이지네이션 시 **pagination 영역은 제외하고 그리드 본문만** spinner 를 표시하려면 `transition_overlay_target` 에 DataGrid 의 `${id}__body` suffix 를 사용한다. DataGrid composite 컴포넌트는 root Div 에 `id` 를, 테이블/카드 목록을 감싸는 내부 wrapper Div 에 `${id}__body` 를 자동으로 부여한다 (pagination 은 body wrapper 밖의 형제).
|
||||
|
||||
```json
|
||||
// DataGrid 컴포넌트 정의
|
||||
{ "type": "composite", "name": "DataGrid", "id": "users_data_grid", "props": {...} }
|
||||
|
||||
// 페이지네이션 navigate
|
||||
{
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/users",
|
||||
"replace": true,
|
||||
"mergeQuery": true,
|
||||
"query": { "page": "{{$args[0]}}" },
|
||||
"transition_overlay_target": "users_data_grid__body"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
이 컨벤션으로 pagination 버튼은 spinner 위에 표시되어 사용자가 연속 클릭 가능하다.
|
||||
|
||||
> 상세: [layout-json-components-loading.md#wait_for — 데이터소스 가드](../frontend/layout-json-components-loading.md#wait_for--데이터소스-가드-engine-v1340)
|
||||
|
||||
### fallback 옵션 (engine-v1.40.0+)
|
||||
|
||||
대상 경로가 현재 템플릿의 `routes.json`에 등록되어 있지 않을 때(예: 관리자 화면에서 사용자 페이지 URL로 navigate 시도), 404 라우트가 아닌 다른 핸들러로 분기시킨다. **기본값은 `openWindow`** — 미지정 시 자동으로 새 창에서 열림.
|
||||
|
||||
**주 용도**: 알림센터에서 알림 클릭 시 관리자 ↔ 사용자 템플릿 교차 경로 처리.
|
||||
|
||||
**판정 흐름**:
|
||||
|
||||
1. `params.fallback === false` → fallback 비활성화 (SPA navigate 강행, 기존 동작)
|
||||
2. `params.replace === true` → 쿼리 갱신 전용이므로 fallback 미적용
|
||||
3. Router 미초기화 또는 routes 미로드 → fallback 미적용 (정상 navigate)
|
||||
4. `Router.match(pathname)` 성공 → fallback 미적용 (정상 navigate)
|
||||
5. 매칭 실패 → fallback 핸들러로 dispatch
|
||||
|
||||
**파라미터 형태**:
|
||||
|
||||
```jsonc
|
||||
// 1. 미지정 (기본값: openWindow)
|
||||
{ "handler": "navigate", "params": { "path": "/shop/products" } }
|
||||
|
||||
// 2. 비활성화 (기존 동작 — 미등록 경로 그대로 SPA navigate)
|
||||
{ "handler": "navigate", "params": { "path": "/admin/x", "fallback": false } }
|
||||
|
||||
// 3. 핸들러명만 지정 (string)
|
||||
{ "handler": "navigate", "params": { "path": "/shop/products", "fallback": "openWindow" } }
|
||||
|
||||
// 4. 상세 지정 (object) — params는 finalPath 위에 merge
|
||||
{
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/shop/products",
|
||||
"fallback": { "handler": "openWindow", "params": { "target": "_blank" } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**알림센터 클릭 패턴 (parallel + fallback)**:
|
||||
|
||||
알림센터에서 알림 클릭 시 mark-as-read API 호출과 navigate를 **`parallel`로 묶어** 동시 실행하면, openWindow fallback이 즉시 새 창을 열고 mark-as-read는 백그라운드에서 진행된다. `sequence`로 묶으면 mark-as-read 완료(1~2초)를 기다린 후에야 새 창이 열려 사용자 체감이 나빠진다.
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "onNotificationClick",
|
||||
"handler": "parallel",
|
||||
"params": {
|
||||
"actions": [
|
||||
{
|
||||
"handler": "navigate",
|
||||
"params": { "path": "{{$args[0].url ?? '/admin/notification-logs'}}" }
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"target": "/api/admin/notifications/{{$args[0].id}}/read",
|
||||
"params": { "method": "PATCH" }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### mergeQuery 옵션
|
||||
|
||||
@@ -231,6 +346,124 @@ replace: true는 같은 페이지 내에서만 사용
|
||||
}
|
||||
```
|
||||
|
||||
### scroll 옵션
|
||||
|
||||
> **버전**: engine-v1.37.0+
|
||||
|
||||
페이지 이동 후 스크롤 위치를 제어합니다. **기본값은 `"top"`** 으로, 일반적인 하이퍼링크 이동 UX와 동일하게 새 페이지가 최상단에서 시작됩니다.
|
||||
|
||||
#### 단축 문법
|
||||
|
||||
| 값 | 동작 |
|
||||
|----|------|
|
||||
| `"top"` (기본) | `#app` 내부의 모든 스크롤 컨테이너 + `window`를 상단으로 리셋 |
|
||||
| `"preserve"` | 이전 스크롤 위치 유지 (엔진이 건드리지 않음) |
|
||||
| `number` | `window`를 해당 Y 좌표로 이동 |
|
||||
| `{ x, y }` | `window`를 특정 좌표로 이동 |
|
||||
| `"#selector"` / `".class"` | 해당 엘리먼트로 `scrollIntoView({ block: 'start' })` |
|
||||
|
||||
#### 확장 객체 문법
|
||||
|
||||
특정 스크롤 컨테이너를 지정하거나, sticky 헤더 오프셋, `scrollIntoView`의 `block` 위치 등을 세밀하게 제어할 때 사용합니다.
|
||||
|
||||
```ts
|
||||
{
|
||||
container?: string; // 스크롤 컨테이너 선택자 (생략 시 window)
|
||||
to?: string | number | { x?, y? } | "top"; // 이동 대상 (생략 시 "top")
|
||||
block?: "start" | "center" | "end" | "nearest"; // scrollIntoView block (기본 "start")
|
||||
offset?: number; // sticky 헤더 보정 (px, 양수 = 위쪽 여유)
|
||||
}
|
||||
```
|
||||
|
||||
| 필드 | 타입 | 기본값 | 설명 |
|
||||
|------|------|--------|------|
|
||||
| `container` | string | `window` | 스크롤 컨테이너 CSS 선택자. 관리자 템플릿처럼 내부 div가 실제 스크롤 영역인 경우 지정 (예: `"#right_content_area"`) |
|
||||
| `to` | string / number / object / `"top"` | `"top"` | 이동 대상. 엘리먼트 선택자 / Y 좌표 / `{x, y}` 좌표 / `"top"` |
|
||||
| `block` | string | `"start"` | `to`가 엘리먼트 선택자일 때 스크롤 위치 지정. 네이티브 `scrollIntoView`와 동일한 의미 |
|
||||
| `offset` | number | `0` | 최종 스크롤 위치에서 차감할 픽셀 수. sticky 헤더가 있는 경우 헤더 높이만큼 지정하면 대상이 헤더 아래로 가려지지 않음 |
|
||||
|
||||
**동작 방식**:
|
||||
|
||||
- 스크롤은 `requestAnimationFrame`을 통해 다음 tick에 적용되어 새 레이아웃이 DOM에 반영된 뒤 정확한 위치로 이동합니다
|
||||
- `replace: true` 분기 (`updateQueryParams`)에도 동일하게 기본 `"top"`이 적용됩니다. 검색/필터/페이지네이션에서 스크롤 유지를 원하면 `"preserve"` 명시
|
||||
- `scrollBehavior: "smooth"`와 조합하면 부드러운 스크롤 애니메이션이 적용됩니다
|
||||
- 관리자 템플릿(`sirsoft-admin_basic`)처럼 `html/body`가 `overflow: hidden`이고 내부 div(`#right_content_area`)가 실제 스크롤 컨테이너인 구조에서는 `"top"` 단축 문법이 자동으로 모든 스크롤 컨테이너를 리셋합니다. 숫자/좌표/선택자 단축 문법은 `window`만 스크롤하므로, 내부 컨테이너 제어가 필요하면 **확장 객체 문법에서 `container` 지정**
|
||||
|
||||
#### 예시
|
||||
|
||||
**검색 필터에서 스크롤 유지**:
|
||||
|
||||
```json
|
||||
{
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/products",
|
||||
"replace": true,
|
||||
"mergeQuery": true,
|
||||
"query": { "search_keyword": "{{_local.keyword}}" },
|
||||
"scroll": "preserve"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**특정 엘리먼트로 이동 (단축)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/docs",
|
||||
"scroll": "#section-api"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**관리자 템플릿 내부 컨테이너의 Y 좌표로 이동 (확장)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/users",
|
||||
"scroll": {
|
||||
"container": "#right_content_area",
|
||||
"to": 500
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**sticky 헤더가 있는 페이지에서 섹션으로 이동 (확장)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/docs",
|
||||
"scroll": {
|
||||
"to": "#section-api",
|
||||
"offset": 80
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**특정 엘리먼트를 화면 중앙에 위치 (확장)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/report",
|
||||
"scroll": {
|
||||
"container": "#right_content_area",
|
||||
"to": "#chart-revenue",
|
||||
"block": "center"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## openWindow
|
||||
@@ -338,8 +571,79 @@ replace: true는 같은 페이지 내에서만 사용
|
||||
|
||||
---
|
||||
|
||||
## reloadExtensions
|
||||
|
||||
> **버전**: engine-v1.38.0+
|
||||
|
||||
확장(모듈/플러그인/템플릿) install/activate/deactivate/uninstall 직후 onSuccess 체인에서 호출하여 **페이지 전체 새로고침 없이** 확장 상태를 원자적으로 재동기화합니다.
|
||||
|
||||
내부적으로 다음을 순차 수행합니다:
|
||||
|
||||
1. `/api/templates/{id}/config.json` 에서 최신 `cache_version` 획득
|
||||
2. localStorage 의 확장 캐시 버전 갱신
|
||||
3. `Router.loadRoutes(newVersion)` — 신 버전 쿼리로 routes.json 재fetch
|
||||
4. LayoutLoader 캐시 버전 갱신 및 클리어
|
||||
5. TranslationEngine 재로드 — 활성 사전은 원자 교체되어 `$t:` 해석 경합 없음
|
||||
|
||||
```json
|
||||
{
|
||||
"handler": "reloadExtensions"
|
||||
}
|
||||
```
|
||||
|
||||
### 선택 파라미터 (모듈/플러그인 에셋 동적 로드/제거)
|
||||
|
||||
모듈 또는 플러그인의 JS/CSS 에셋을 동시에 로드/제거하려면 `moduleInfo` 또는 `pluginInfo` 와 `action` 을 전달합니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"handler": "reloadExtensions",
|
||||
"params": {
|
||||
"moduleInfo": "{{result}}",
|
||||
"action": "add"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 파라미터 | 타입 | 설명 |
|
||||
|---------|------|------|
|
||||
| `moduleInfo` | object | 모듈 activate/deactivate API 응답 (`{{result}}`) — `identifier` 및 `assets` 포함 |
|
||||
| `pluginInfo` | object | 플러그인 activate/deactivate API 응답 |
|
||||
| `action` | 문자열 | `"add"` 또는 `"remove"` — 에셋 로드 또는 제거 |
|
||||
|
||||
### 사용 사례
|
||||
|
||||
- 모듈/플러그인/템플릿 install/activate/deactivate/uninstall onSuccess
|
||||
- 관리자가 같은 세션 안에서 확장을 활성화한 뒤 새 라우트로 즉시 이동하는 UX
|
||||
- 전체 페이지 새로고침(`refresh`) 을 SPA 친화적으로 대체
|
||||
|
||||
### 올바른 사용
|
||||
|
||||
```json
|
||||
"onSuccess": [
|
||||
{ "handler": "toast", "params": { "type": "success", "message": "$t:admin.modules.activate_success" } },
|
||||
{ "handler": "refetchDataSource", "params": { "dataSourceId": "modules" } },
|
||||
{
|
||||
"handler": "reloadExtensions",
|
||||
"params": {
|
||||
"moduleInfo": "{{result}}",
|
||||
"action": "add"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 주의
|
||||
|
||||
- 기존에 3~4건을 병렬 호출하던 `reloadRoutes` + `reloadTranslations` + `reloadModuleHandlers` / `reloadPluginHandlers` 패턴을 이 단일 핸들러로 대체하세요.
|
||||
- 템플릿 activate/force_activate 에서 `refresh`(전체 새로고침)로 해결하던 케이스도 `reloadExtensions` 로 교체 가능.
|
||||
|
||||
---
|
||||
|
||||
## reloadRoutes
|
||||
|
||||
> **Deprecated** (engine-v1.38.0+): `reloadExtensions` 사용을 권장합니다. 하위 호환을 위해 유지되며 내부적으로 `reloadExtensions` 와 동일한 `TemplateApp.reloadExtensionState()` 로 위임됩니다.
|
||||
|
||||
라우트(routes.json)를 다시 로드합니다.
|
||||
|
||||
```json
|
||||
@@ -350,8 +654,8 @@ replace: true는 같은 페이지 내에서만 사용
|
||||
|
||||
### 사용 사례
|
||||
|
||||
- 모듈/플러그인 설치 후 새 라우트 적용
|
||||
- 동적으로 라우트 설정이 변경된 경우
|
||||
- (deprecated) 모듈/플러그인 설치 후 새 라우트 적용 → `reloadExtensions` 사용
|
||||
- (deprecated) 동적으로 라우트 설정이 변경된 경우
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -612,6 +612,8 @@ sequence 내에서 `target: "isolated"` setState가 실행되면, 이후 스텝
|
||||
|
||||
## reloadTranslations
|
||||
|
||||
> **Deprecated** (engine-v1.38.0+): 모듈/플러그인/템플릿 라이프사이클 onSuccess 에서는 `reloadExtensions` 사용을 권장합니다. 하위 호환을 위해 유지되며 내부적으로 `TemplateApp.reloadExtensionState()` 로 위임되어 routes/translations/layouts 가 함께 재동기화됩니다. 자세한 내용은 [`reloadExtensions`](actions-handlers-navigation.md#reloadextensions) 참조.
|
||||
|
||||
다국어 파일을 다시 로드합니다.
|
||||
|
||||
```json
|
||||
@@ -622,8 +624,8 @@ sequence 내에서 `target: "isolated"` setState가 실행되면, 이후 스텝
|
||||
|
||||
### 사용 사례
|
||||
|
||||
- 언어 설정 변경 후 번역 적용
|
||||
- 모듈/플러그인 설치 후 새 번역 적용
|
||||
- 언어 설정 변경 후 번역 적용 (단독 사용)
|
||||
- (deprecated) 모듈/플러그인 설치 후 새 번역 적용 → `reloadExtensions` 사용
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
| 하위 문서 | 주요 핸들러 | 설명 |
|
||||
|----------|------------|------|
|
||||
| [actions-handlers-navigation.md](actions-handlers-navigation.md) | navigate, navigateBack, openWindow, replaceUrl, reloadRoutes, refresh | 페이지 이동 및 라우트 관리 |
|
||||
| [actions-handlers-navigation.md](actions-handlers-navigation.md) | navigate, navigateBack, openWindow, replaceUrl, **reloadExtensions**, reloadRoutes, refresh | 페이지 이동 및 라우트 관리 |
|
||||
| [actions-handlers-state.md](actions-handlers-state.md) | apiCall, setState, setError, refetchDataSource, remount | API 호출 및 상태 관리 |
|
||||
| [actions-handlers-ui.md](actions-handlers-ui.md) | login/logout, openModal/closeModal, toast, switch, sequence/parallel, loadScript, callExternal | UI 인터랙션 및 외부 스크립트 |
|
||||
|
||||
@@ -37,8 +37,9 @@
|
||||
2. [navigateBack](actions-handlers-navigation.md#navigateback) - 뒤로 가기
|
||||
3. [openWindow](actions-handlers-navigation.md#openwindow) - 새 창/탭에서 열기
|
||||
4. [replaceUrl](actions-handlers-navigation.md#replaceurl) - URL만 변경 (refetch 없음)
|
||||
5. [reloadRoutes](actions-handlers-navigation.md#reloadroutes) - 라우트 재로드
|
||||
6. [refresh](actions-handlers-navigation.md#refresh) - 페이지 새로고침
|
||||
5. [reloadExtensions](actions-handlers-navigation.md#reloadextensions) ⭐ NEW (engine-v1.38.0+) - 확장 상태 원자 재동기화
|
||||
6. [reloadRoutes](actions-handlers-navigation.md#reloadroutes) (deprecated) - 라우트 재로드
|
||||
7. [refresh](actions-handlers-navigation.md#refresh) - 페이지 새로고침
|
||||
|
||||
### 상태 관리 핸들러 → [상세 문서](actions-handlers-state.md)
|
||||
|
||||
@@ -60,7 +61,7 @@
|
||||
17. [confirm (액션 속성)](actions-handlers-ui.md#confirm-액션-속성) - 실행 전 확인 대화상자
|
||||
18. [switch](actions-handlers-ui.md#switch) - 조건부 액션
|
||||
19. [sequence / parallel](actions-handlers-ui.md#sequence--parallel) - 액션 조합
|
||||
20. [reloadTranslations](actions-handlers-ui.md#reloadtranslations) - 다국어 재로드
|
||||
20. [reloadTranslations](actions-handlers-ui.md#reloadtranslations) (deprecated) - 다국어 재로드 (extension 라이프사이클은 `reloadExtensions` 사용)
|
||||
21. [showErrorPage](actions-handlers-ui.md#showerrorpage) - 에러 페이지
|
||||
22. [loadScript](actions-handlers-ui.md#loadscript) ⭐ NEW - 외부 스크립트 로드
|
||||
23. [callExternal](actions-handlers-ui.md#callexternal) ⭐ NEW - 외부 라이브러리 호출
|
||||
@@ -89,7 +90,7 @@
|
||||
|
||||
| 핸들러 | 필수 속성 | 선택 속성 |
|
||||
|--------|----------|----------|
|
||||
| `navigate` | `params.path` | `params.query`, `params.mergeQuery`, `params.replace` |
|
||||
| `navigate` | `params.path` | `params.query`, `params.mergeQuery`, `params.replace`, `params.fallback` (engine-v1.40.0+, 미등록 경로 fallback, 기본 `openWindow`) |
|
||||
| `openWindow` | `params.path` | - |
|
||||
| `replaceUrl` | - | `params.path`, `params.query`, `params.mergeQuery` |
|
||||
| `apiCall` | `target` | `params.method`, `params.body`, `params.contentType`, `auth_required`, `onSuccess`, `onError` |
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
| `cases` | object | ❌ | switch 핸들러용 케이스별 액션 정의 |
|
||||
| `if` | string | ❌ | 조건부 실행 (`{{expression}}` 형식, truthy일 때만 실행) |
|
||||
| `debounce` | number \| object | ❌ | 연속 호출 제어 (300 또는 { delay, leading, trailing }) |
|
||||
| `render` | boolean | ❌ | `false`이면 상태 값만 저장하고 React 리렌더를 건너뜀 (engine-v1.42.0+). 기본: `true` |
|
||||
| `resultTo` | object | ❌ | 핸들러 실행 결과를 상태에 저장 (engine-v1.11.0+). `{ target, key, merge }` |
|
||||
| `actionRef` | string | ❌ | `named_actions`에 정의된 액션을 참조 (engine-v1.19.0+). handler/params 대신 사용 |
|
||||
|
||||
@@ -373,9 +374,10 @@ init_actions에서 _local 저장 시 globalStateUpdater 경유 (componentContext
|
||||
| `parallel` | 병렬 액션 실행 | - | actions (액션 배열) |
|
||||
| `refetchDataSource` | 데이터 소스 다시 fetch | - | dataSourceId |
|
||||
| `remount` | 컴포넌트 강제 리마운트 | - | componentId |
|
||||
| `reloadRoutes` | 라우트 다시 로드 | - | - |
|
||||
| `reloadTranslations` | 다국어 파일 다시 로드 | - | - |
|
||||
| `refresh` | 현재 페이지 새로고침 | - | - |
|
||||
| `reloadExtensions` ⭐ NEW | 확장 상태 원자 재동기화 (routes+translations+layouts) | - | moduleInfo, pluginInfo, action |
|
||||
| `reloadRoutes` (deprecated) | 라우트 다시 로드 — `reloadExtensions` 위임 | - | - |
|
||||
| `reloadTranslations` (deprecated) | 다국어 파일 다시 로드 — `reloadExtensions` 위임 | - | - |
|
||||
| `refresh` | 현재 페이지 전체 새로고침 | - | - |
|
||||
| `stopPropagation` | 이벤트 전파 중지 | - | - |
|
||||
| `preventDefault` | 기본 동작 방지 | - | - |
|
||||
| `showErrorPage` | 에러 페이지 렌더링 | - | errorCode, target, containerId, layout |
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
3. TagInput: creatable, delimiters, splitOnPaste, isMulti, tagVariants 색상 맵핑
|
||||
4. MultilingualInput: layout('inline'|'tabs'|'compact'), availableLocales, inputType
|
||||
5. Toggle: size('sm'|'md'|'lg'), description 텍스트 | RadioGroup: inline 가로 배치
|
||||
6. HtmlContent: isHtml prop으로 HTML/텍스트 분기 — DB *_mode 필드 → isHtml 연동 패턴 필수 숙지
|
||||
```
|
||||
|
||||
---
|
||||
@@ -26,6 +27,7 @@
|
||||
6. [Checkbox](#checkbox)
|
||||
7. [RadioGroup](#radiogroup)
|
||||
8. [SearchBar](#searchbar)
|
||||
9. [HtmlContent](#htmlcontent)
|
||||
|
||||
---
|
||||
|
||||
@@ -571,6 +573,82 @@
|
||||
|
||||
---
|
||||
|
||||
## HtmlContent
|
||||
|
||||
HTML 콘텐츠와 일반 텍스트를 안전하게 렌더링하는 컴포넌트입니다.
|
||||
|
||||
### Props
|
||||
|
||||
| Prop | 타입 | 필수 | 기본값 | 설명 |
|
||||
|------|------|------|--------|------|
|
||||
| `content` | `string` | ❌ | `''` | 렌더링할 콘텐츠 (HTML 또는 텍스트) |
|
||||
| `text` | `string` | ❌ | - | 레이아웃 JSON용 콘텐츠 (`content`보다 우선) |
|
||||
| `isHtml` | `boolean` | ❌ | `true` | HTML 렌더링 여부 |
|
||||
| `className` | `string` | ❌ | `''` | 컨테이너 CSS 클래스 |
|
||||
| `purifyConfig` | `object` | ❌ | - | DOMPurify 커스텀 설정 (isHtml=true 시만) |
|
||||
|
||||
### isHtml에 따른 렌더링 분기
|
||||
|
||||
| `isHtml` | 렌더링 방식 | 스타일 |
|
||||
|----------|-----------|--------|
|
||||
| `true` | `dangerouslySetInnerHTML` + DOMPurify 정화 | `prose dark:prose-invert` (Tailwind Typography) |
|
||||
| `false` | React 텍스트 노드 (이스케이프됨) | `whitespace-pre-wrap` (줄바꿈 보존) |
|
||||
|
||||
### DB `*_mode` 필드 → `isHtml` 연동 패턴 (CRITICAL)
|
||||
|
||||
G7에서는 콘텐츠의 렌더링 모드를 DB의 `*_mode` 컬럼(`'text'` / `'html'`)으로 관리합니다.
|
||||
이 값이 API를 거쳐 프론트엔드 `isHtml` prop으로 변환되는 전체 흐름을 이해해야 합니다.
|
||||
|
||||
#### 데이터 흐름
|
||||
|
||||
```text
|
||||
1. DB 컬럼 → description_mode: 'text' | 'html'
|
||||
2. API Resource → 'description_mode' => $this->description_mode
|
||||
3. 레이아웃 JSON → "isHtml": "{{(product.data?.description_mode ?? 'text') === 'html'}}"
|
||||
4. HtmlContent → isHtml=true → HTML 렌더링 / isHtml=false → 텍스트 렌더링
|
||||
```
|
||||
|
||||
#### 적용 사례
|
||||
|
||||
| 기능 | DB 컬럼 | API Resource 필드 | 레이아웃 표현식 |
|
||||
|------|---------|-------------------|----------------|
|
||||
| 상품 설명 | `products.description_mode` | `description_mode` | `{{(product.data?.description_mode ?? 'text') === 'html'}}` |
|
||||
| 게시글 본문 | `posts.content_mode` | `content_mode` | `{{(post?.data?.content_mode ?? 'text') === 'html'}}` |
|
||||
| 페이지 콘텐츠 | `pages.content_mode` | `content_mode` | `{{(page?.data?.content_mode ?? 'html') === 'html'}}` |
|
||||
| 공통정보 | `common_infos.content_mode` | `common_info.content_mode` | `{{product.data?.common_info?.content_mode === 'html'}}` |
|
||||
|
||||
#### 레이아웃 JSON 사용 예시
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "HtmlContent",
|
||||
"props": {
|
||||
"content": "{{product.data?.description_localized ?? ''}}",
|
||||
"isHtml": "{{(product.data?.description_mode ?? 'text') === 'html'}}",
|
||||
"className": "prose dark:prose-invert max-w-none"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 주의사항
|
||||
|
||||
```text
|
||||
1. API Resource에 *_mode 필드 누락 시 → 프론트엔드 기본값('text')으로 fallback → HTML 미렌더링
|
||||
2. Admin Resource와 Public Resource 양쪽 모두에 *_mode 필드 포함 필수
|
||||
3. 기본값은 기능별로 다름: 상품/게시글은 'text', 페이지는 'html'
|
||||
4. isHtml=true 시 DOMPurify 자동 적용 — 별도 sanitize 불필요
|
||||
5. 백엔드에서도 description_mode='html'일 때 HTMLPurifier로 서버 사이드 정화
|
||||
```
|
||||
|
||||
### 보안
|
||||
|
||||
- `isHtml=true`: DOMPurify FORBID 방식 — `script`, `iframe`, `form` 등 위험 태그 차단, 나머지 허용
|
||||
- 외부 링크(`http://`, `https://`)에 `rel="noopener noreferrer"` 자동 추가
|
||||
- `purifyConfig`로 커스터마이징 가능하나, 보안 기본값(`FORBID_TAGS`, `FORBID_ATTR`)은 항상 유지
|
||||
|
||||
---
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [컴포넌트 Props (Basic/DataGrid/Modal)](component-props.md)
|
||||
@@ -578,3 +656,5 @@
|
||||
- [컴포넌트 고급 기능](components-advanced.md)
|
||||
- [sirsoft-admin_basic 컴포넌트](templates/sirsoft-admin_basic/components.md)
|
||||
- [액션 핸들러 - 커스텀 콜백](actions.md#커스텀-이벤트-event-필드)
|
||||
- [보안 가이드 - HTML 렌더링](security.md#htmlcontent--htmleditor-html-렌더링이-필요한-경우)
|
||||
- [에디터 컴포넌트](editors.md)
|
||||
@@ -57,6 +57,8 @@ export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElemen
|
||||
options?: SelectOption[] | string[]; // 옵션 배열
|
||||
label?: string; // 레이블 텍스트 (상위 컴포넌트에서 처리)
|
||||
error?: string; // 에러 메시지 (상위 컴포넌트에서 처리)
|
||||
searchable?: boolean; // 드롭다운 내 검색 input 활성화 (engine-v1.40.0+)
|
||||
searchPlaceholder?: string; // 검색 input placeholder (searchable=true일 때)
|
||||
|
||||
// HTML Select 표준 Props (모두 지원)
|
||||
value?: string | number; // 선택된 값 (제어 컴포넌트)
|
||||
@@ -83,6 +85,8 @@ export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElemen
|
||||
| `required` | `boolean` | ❌ | `false` | 필수 입력 |
|
||||
| `className` | `string` | ❌ | - | CSS 클래스 |
|
||||
| `placeholder` | `string` | ❌ | - | 플레이스홀더 (옵션 배열 첫 항목) |
|
||||
| `searchable` | `boolean` | ❌ | `false` | 드롭다운 내 검색 input 활성화 (label/value 부분일치 필터링, engine-v1.40.0+) |
|
||||
| `searchPlaceholder` | `string` | ❌ | `'Search...'` | 검색 input placeholder (searchable=true일 때) |
|
||||
|
||||
### 렌더링 모드
|
||||
|
||||
|
||||
@@ -301,7 +301,7 @@
|
||||
| 모드 | 토큰 전송 | 토큰 없을 때 | 사용 사례 |
|
||||
|------|----------|-------------|----------|
|
||||
| `none` (기본) | ❌ 안 보냄 | 정상 요청 | 공개 API (상품 목록, 게시판 등) |
|
||||
| `required` | ✅ 필수 | 요청 실패 | 마이페이지, 관리자 API |
|
||||
| `required` | ✅ 필수 | **요청 스킵 → fallback 사용** | 마이페이지, 관리자 API |
|
||||
| `optional` | ✅ 있으면 전송 | 비회원으로 요청 | **장바구니, 위시리스트** (비회원/회원 공용) |
|
||||
|
||||
### 사용 예시
|
||||
@@ -335,6 +335,12 @@
|
||||
### 동작 원리
|
||||
|
||||
```text
|
||||
auth_mode: "required" 동작 흐름:
|
||||
|
||||
1. 토큰 유무 확인 (AuthManager.isAuthenticated())
|
||||
2. 토큰 있음 → ApiClient 사용 (Authorization: Bearer xxx)
|
||||
3. 토큰 없음 → 요청 스킵, fallback 데이터 사용 (API 호출하지 않음)
|
||||
|
||||
auth_mode: "optional" 동작 흐름:
|
||||
|
||||
1. 토큰 유무 확인 (AuthManager.isAuthenticated())
|
||||
@@ -342,6 +348,10 @@ auth_mode: "optional" 동작 흐름:
|
||||
3. 토큰 없음 → 일반 fetch 사용 (headers에 X-Cart-Key 등 포함 가능)
|
||||
```
|
||||
|
||||
> **중요**: `auth_mode: "required"`에서 토큰이 없으면 API 요청 자체를 하지 않습니다.
|
||||
> `fallback`이 정의되어 있으면 fallback 데이터가 렌더링에 사용되고, 없으면 데이터 없이 처리됩니다.
|
||||
> 이를 통해 비로그인 상태에서 인증 필수 API 호출로 인한 401 → 로그인 리다이렉트 문제를 방지합니다.
|
||||
|
||||
### 하위 호환성
|
||||
|
||||
`auth_required: true`는 `auth_mode: "required"`와 동일하게 동작합니다.
|
||||
|
||||
@@ -618,6 +618,48 @@ const unsubscribe = G7Core.TransitionManager.subscribe((isPending) => {
|
||||
const { isPending } = G7Core.useTransitionState();
|
||||
```
|
||||
|
||||
### G7Core.updateQueryParams
|
||||
|
||||
같은 페이지 내에서 URL 쿼리 파라미터만 변경하면서 컴포넌트 리마운트 없이 데이터소스를 refetch 한다. 일반적으로 `navigate` 핸들러의 `replace: true` 옵션 사용 시 엔진이 자동 호출하므로 직접 호출은 드물다.
|
||||
|
||||
```typescript
|
||||
G7Core.updateQueryParams(
|
||||
newPath: string,
|
||||
options?: { transitionOverlayTarget?: string }
|
||||
): Promise<void>
|
||||
```
|
||||
|
||||
| 파라미터 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| `newPath` | `string` | 새 경로 (쿼리스트링 포함). 예: `/admin/products?status=active&page=2` |
|
||||
| `options.transitionOverlayTarget` | `string` | `transition_overlay.target` 동적 override (engine-v1.36.0+). 탭 속 서브 탭 등 부분 영역만 spinner 표시 |
|
||||
|
||||
**동작**:
|
||||
|
||||
1. `window.history.replaceState` 로 URL 갱신 (페이지 리로드 없음)
|
||||
2. `auto_fetch: true` 인 데이터소스 refetch (websocket 제외)
|
||||
3. `transition_overlay` 가 정의되어 있고 `blocking` 또는 `wait_for` 일치 progressive 데이터소스가 1개 이상이면 오버레이 자동 표시 (engine-v1.35.0+)
|
||||
4. fetch 완료 시 `updateTemplateData` 호출 + `hideTransitionOverlay`
|
||||
5. `computed` 재계산
|
||||
|
||||
**사용 예시**:
|
||||
|
||||
```typescript
|
||||
// 일반: 쿼리 파라미터 변경 + 데이터 refetch
|
||||
await G7Core.updateQueryParams('/admin/products?status=active&page=2');
|
||||
|
||||
// 서브 탭 spinner target 동적 override
|
||||
await G7Core.updateQueryParams(
|
||||
'/admin/settings?tab=notification&channel=email',
|
||||
{ transitionOverlayTarget: 'notif_channel_content' }
|
||||
);
|
||||
```
|
||||
|
||||
**관련**:
|
||||
|
||||
- `navigate` 핸들러 (`replace: true`) → [actions-handlers-navigation.md](actions-handlers-navigation.md)
|
||||
- `transition_overlay.wait_for` / `overlay_target` → [layout-json-components-loading.md](layout-json-components-loading.md)
|
||||
|
||||
---
|
||||
|
||||
## React Hooks
|
||||
|
||||
@@ -112,6 +112,9 @@ const toast = (window as any).G7Core?.toast;
|
||||
setLocal(updates: Record<string, any>, options?: {
|
||||
scope?: 'current' | 'parent' | 'root';
|
||||
merge?: 'deep' | 'shallow' | 'replace'; // engine-v1.18.0+ (기본: 'deep')
|
||||
debounce?: number; // engine-v1.41.0+ — 디바운스 지연 시간 (ms)
|
||||
debounceKey?: string; // engine-v1.41.0+ — 디바운스 고유 키
|
||||
render?: boolean; // engine-v1.42.0+ (기본: true) — false이면 React 리렌더 건너뜀
|
||||
}): void
|
||||
getLocal(): Record<string, any>
|
||||
```
|
||||
@@ -121,6 +124,22 @@ getLocal(): Record<string, any>
|
||||
- 액션 실행 중: 컴포넌트의 `dynamicState`를 직접 업데이트하여 즉시 UI에 반영
|
||||
- 액션 외부: 전역 `_local` 업데이트 (fallback)
|
||||
|
||||
**render: false** (engine-v1.42.0+): 값은 `globalState._local`에 저장하되 React 리렌더를 건너뜁니다.
|
||||
외부 라이브러리(CKEditor 등)가 자체 DOM을 관리하는 경우, React 트리 리렌더가 불필요하므로 성능을 대폭 개선합니다.
|
||||
`flushPendingDebounceTimers` 실행 시(저장 직전)에는 항상 render: true로 강제되어 데이터 정합성이 보장됩니다.
|
||||
|
||||
```typescript
|
||||
// 외부 라이브러리에서 자체 DOM을 관리하는 경우 — 리렌더 없이 값만 저장
|
||||
G7Core.state.setLocal({
|
||||
[`form.${name}.${locale}`]: html,
|
||||
hasChanges: true,
|
||||
}, {
|
||||
debounce: 300,
|
||||
debounceKey: `editor-sync-${name}`,
|
||||
render: false,
|
||||
});
|
||||
```
|
||||
|
||||
**dot notation 지원** (engine-v1.2.0+): 중첩된 객체 경로를 dot notation으로 표현할 수 있습니다.
|
||||
|
||||
```typescript
|
||||
|
||||
@@ -318,6 +318,78 @@
|
||||
| `enabled` | `boolean` | `false` | 전환 오버레이 활성화 |
|
||||
| `style` | `"opaque" \| "blur" \| "fade" \| "skeleton" \| "spinner"` | `"opaque"` | 오버레이 스타일 |
|
||||
| `target` | `string` | - | 오버레이 대상 요소 ID. 미지정 시 전체 화면 |
|
||||
| `wait_for` | `string[]` | - | 명시된 데이터소스 fetch 완료까지 오버레이 유지 (engine-v1.34.0+) |
|
||||
|
||||
### wait_for — 데이터소스 가드 (engine-v1.34.0+)
|
||||
|
||||
`blocking` 데이터소스가 없는 페이지에서도 명시된 progressive 데이터소스가 fetch 완료될 때까지 오버레이가 표시되도록 한다. 베이스 레이아웃에 spinner 설정을 두고, 자식 레이아웃이 `wait_for` 만 추가하는 패턴이 권장된다.
|
||||
|
||||
```json
|
||||
// 베이스 레이아웃 (_admin_base.json)
|
||||
{
|
||||
"transition_overlay": {
|
||||
"enabled": true,
|
||||
"style": "spinner",
|
||||
"target": "main_content",
|
||||
"spinner": { "component": "PageLoading" }
|
||||
}
|
||||
}
|
||||
|
||||
// 자식 레이아웃 (admin_settings.json) — wait_for 만 명시
|
||||
{
|
||||
"extends": "_admin_base",
|
||||
"transition_overlay": {
|
||||
"wait_for": ["settings"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`LayoutService` 가 `transition_overlay` 를 shallow merge 하므로 자식의 `wait_for` 만 추가되고 베이스의 `enabled/style/target/spinner` 설정이 보존된다.
|
||||
|
||||
#### 호환되는 loading_strategy
|
||||
|
||||
| loading_strategy | wait_for 동작 | 비고 |
|
||||
| ---------------- | ------------- | ---- |
|
||||
| `blocking` | step 2.5 에서 spinner 자동 트리거. wait_for 명시는 중복이지만 무해 | 명시 불필요 |
|
||||
| `progressive` (default) | wait_for 의 핵심 사용처 — fetch 완료까지 오버레이 유지 | 권장 |
|
||||
| `background` | **검증 단계에서 차단됨** — "사용자 차단 없음" 의도와 충돌 | UpdateLayoutContentRequest 422 |
|
||||
| `websocket` | **검증 단계에서 차단됨** — fetch 완료 이벤트 없음 | UpdateLayoutContentRequest 422 |
|
||||
|
||||
#### 트리거/해제 시점
|
||||
|
||||
- **트리거 1 — 페이지 진입**: handleRouteChange step 2.5 에서 `wait_for` 에 명시된 ID 중 progressive/blocking 데이터소스가 1개 이상 존재하면 오버레이 표시
|
||||
- **트리거 2 — 탭 전환/페이지네이션** (engine-v1.35.0+): `navigate replace:true` 로 `updateQueryParams` 경로에 진입할 때도 동일 조건으로 오버레이 표시. handleRouteChange 와 동일 정책으로 `wait_for` 검사 + 오버레이 호출
|
||||
- **해제**: 모든 progressive 데이터소스 fetch 완료 시 (hideTransitionOverlay 호출)
|
||||
- **존재하지 않는 ID**: 엔진이 자동 무시 (백엔드 검증과 별개로 런타임 안전 가드)
|
||||
|
||||
#### 동적 target override (engine-v1.36.0+)
|
||||
|
||||
`replace: true` navigate 의 `params.transition_overlay_target` 으로 호출별 target 을 override 할 수 있다. 탭 속 서브 탭(환경설정 > 알림 탭 > 채널 탭 등)에서 서브 탭 콘텐츠 영역에만 spinner 를 표시하고 싶을 때 사용.
|
||||
|
||||
```json
|
||||
// 상위 탭 전환: 베이스 transition_overlay.target 사용 (settings_tab_content)
|
||||
{ "handler": "navigate", "params": { "path": "/admin/settings", "replace": true, "query": { "tab": "notification_definitions" } } }
|
||||
|
||||
// 채널 서브 탭 전환: 채널 콘텐츠 영역에만 spinner
|
||||
{
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/settings",
|
||||
"replace": true,
|
||||
"mergeQuery": true,
|
||||
"query": { "channel": "{{ch.id}}" },
|
||||
"transition_overlay_target": "notif_channel_content"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`transition_overlay_target` 미존재 시 레이아웃의 `transition_overlay.target` 사용. 해당 ID DOM 요소 미발견 시 `fallback_target` → `#app` 3단계 폴백.
|
||||
|
||||
> 상세: [actions-handlers-navigation.md#transition_overlay_target-옵션-engine-v1360](actions-handlers-navigation.md#transition_overlay_target-옵션-engine-v1360)
|
||||
|
||||
#### refetchDataSource 와의 관계
|
||||
|
||||
`refetchDataSource` 핸들러로 단독 데이터를 다시 부르는 경우는 `wait_for` 트리거 대상이 아니다. 이 경우 컴포넌트 단위 `blur_until_loaded` 의 객체 형태(`{ enabled: true, data_sources: ["xxx"] }`)로 처리한다 — 페이지 단위 vs 컴포넌트 단위의 역할 분리를 유지한다.
|
||||
|
||||
`target` 지정 시 CSS `<style>` 태그를 `<head>`에 주입하여 타겟 요소에 `::after` 의사 요소로 오버레이를 생성합니다. 타겟 요소에 `position:relative; z-index:0`을 설정하여 명시적 stacking context를 생성하므로, `::after`가 타겟 내부에 갇히고 헤더/네비게이션 등 형제 요소를 절대 가리지 않습니다. `target` 미지정 시 `document.body`에 `position:fixed` div를 삽입하는 폴백 방식을 사용합니다.
|
||||
|
||||
@@ -624,6 +696,45 @@ lifecycle 핸들러에서는 다음 데이터에 접근할 수 있습니다:
|
||||
|
||||
---
|
||||
|
||||
## _seo 컨텍스트 네임스페이스
|
||||
|
||||
> **버전**: SeoRenderer에서 주입
|
||||
|
||||
SEO 변수 시스템이 활성화된 레이아웃에서, SeoRenderer가 확장 설정 SEO 템플릿을 해석한 결과를 `_seo` 네임스페이스에 주입합니다.
|
||||
|
||||
### 구조
|
||||
|
||||
```text
|
||||
_seo.{page_type}.title — 해석된 SEO 제목
|
||||
_seo.{page_type}.description — 해석된 SEO 설명
|
||||
```
|
||||
|
||||
### 사용법
|
||||
|
||||
`_seo` 컨텍스트는 `og`, `structured_data`, 그리고 모든 표현식 영역에서 참조할 수 있습니다.
|
||||
|
||||
```json
|
||||
"og": {
|
||||
"title": "{{_seo.product.title ?? product.data.name ?? ''}}",
|
||||
"description": "{{_seo.product.description ?? product.data.short_description ?? ''}}"
|
||||
},
|
||||
"structured_data": {
|
||||
"@type": "Product",
|
||||
"name": "{{_seo.product.title ?? product.data.name ?? ''}}"
|
||||
}
|
||||
```
|
||||
|
||||
### 동작 조건
|
||||
|
||||
- `meta.seo.extensions`에 확장이 선언되어 있어야 함
|
||||
- 해당 확장의 `seoVariables()`에서 변수가 정의되어 있어야 함
|
||||
- 확장 설정에 `seo.meta_{page_type}_title` / `seo.meta_{page_type}_description` 템플릿이 존재해야 함
|
||||
- 조건 미충족 시 `_seo.{page_type}` 값이 `undefined` → fallback 표현식(예: `?? product.data.name ?? ''`) 사용 권장
|
||||
|
||||
> 상세: [seo-system.md](../backend/seo-system.md) "SEO 변수 시스템" 참조
|
||||
|
||||
---
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [레이아웃 JSON 컴포넌트 인덱스](layout-json-components.md)
|
||||
|
||||
@@ -678,6 +678,7 @@ onSortEnd에서 setState로 소스 배열을 업데이트해야 정렬 결과가
|
||||
| `handle` | string | ❌ | - | 드래그 핸들 CSS 선택자 |
|
||||
| `itemVar` | string | ❌ | `"$item"` | 아이템 컨텍스트 변수명 |
|
||||
| `indexVar` | string | ❌ | `"$index"` | 인덱스 컨텍스트 변수명 |
|
||||
| `wrapperElement` | string | ❌ | `"div"` | 래퍼 HTML 요소. Table 내부에서 `"tr"` 지정 시 itemTemplate의 children을 `<tr>` 안에 직접 렌더링 (engine-v1.30.0+) |
|
||||
|
||||
### itemTemplate
|
||||
|
||||
|
||||
@@ -120,11 +120,33 @@
|
||||
|------|------|------|------|
|
||||
| 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와 동일 전략)
|
||||
@@ -839,24 +861,52 @@ const renderChildren = useMemo(() => {
|
||||
|
||||
---
|
||||
|
||||
## extensionPointProps (engine-v1.15.0+)
|
||||
## Extension Point 데이터 전달 (engine-v1.15.0+, callbacks: engine-v1.28.0+)
|
||||
|
||||
`extension_point` 타입 컴포넌트에서 주입되는 컴포넌트에 추가 props를 전달합니다.
|
||||
`extension_point`에서 주입되는 컴포넌트에 데이터와 콜백을 전달합니다.
|
||||
|
||||
- `props`: 데이터 전달용 — 표현식이 평가되어 전달됨 (`resolveObject` 재귀 평가)
|
||||
- `callbacks`: 액션 객체 전달용 — 평가 없이 그대로 전달 (ActionDispatcher가 실행 시점에 평가)
|
||||
|
||||
### 호스트 레이아웃 (extension_point 정의)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "extension_point",
|
||||
"name": "product_detail_sidebar",
|
||||
"extensionPointProps": {
|
||||
"name": "address_search_slot",
|
||||
"props": {
|
||||
"productId": "{{route.id}}",
|
||||
"currency": "{{_global.currency ?? 'KRW'}}"
|
||||
"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`에서 전달된 액션 객체 (그대로 전달)
|
||||
|
||||
| 필드 | 타입 | 필수 | 설명 |
|
||||
|------|------|------|------|
|
||||
| `extensionPointProps` | object | ❌ | extension_point에 주입된 컴포넌트에 전달할 props (표현식 지원) |
|
||||
| `props` | object | ❌ | 주입 컴포넌트에 전달할 데이터 (표현식 평가) |
|
||||
| `callbacks` | object | ❌ | 주입 컴포넌트에 전달할 액션 객체 (평가 없이 전달) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -97,13 +97,16 @@ G7이 자동으로 주입하는 `_global` 속성입니다. 레이아웃에서
|
||||
|
||||
```json
|
||||
{
|
||||
"comment": "appConfig 활용 예시 - 동적 Select 옵션",
|
||||
"comment": "appConfig 활용 예시 - 동적 Select 옵션 (타임존)",
|
||||
"props": {
|
||||
"options": "{{_global.appConfig?.supportedTimezones?.map(tz => ({ value: tz, label: tz })) ?? []}}"
|
||||
"options": "{{_global.appConfig?.supportedTimezones ?? []}}",
|
||||
"searchable": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> `_global.appConfig.supportedTimezones`는 `[{value: 'Asia/Seoul', label: '(UTC+09:00) Asia/Seoul'}, ...]` 형식의 `{value, label}` 객체 배열입니다(오프셋 오름차순 정렬). 백엔드 `SettingsService::buildTimezoneOptions()`가 매 요청 시점에 UTC 오프셋을 계산하여 라벨을 생성하므로 DST 전환이 자동 반영됩니다. 별도 `.map()` 변환 없이 Select `options`에 직접 바인딩합니다.
|
||||
|
||||
### 기본 사용법
|
||||
|
||||
```json
|
||||
|
||||
@@ -61,8 +61,8 @@ _admin_base.json (베이스 레이아웃)
|
||||
│ └── admin_settings.json (탭 네비게이션)
|
||||
│ └── partials/admin_settings/
|
||||
│ ├── _tab_general.json (일반)
|
||||
│ ├── _tab_mail.json (메일 발송)
|
||||
│ ├── _tab_mail_templates.json (메일 템플릿)
|
||||
│ ├── _tab_mail.json (메일 발송 SMTP 설정)
|
||||
│ ├── _tab_notification_definitions.json (알림 정의)
|
||||
│ ├── _tab_security.json (보안)
|
||||
│ ├── _tab_upload.json (업로드)
|
||||
│ ├── _tab_drivers.json (드라이버)
|
||||
@@ -73,8 +73,8 @@ _admin_base.json (베이스 레이아웃)
|
||||
│ ├── _modal_core_changelog.json
|
||||
│ ├── _modal_core_update_guide.json
|
||||
│ ├── _modal_core_update_result.json
|
||||
│ ├── _modal_mail_template_form.json
|
||||
│ ├── _modal_mail_template_preview.json
|
||||
│ ├── _modal_notification_template_form.json
|
||||
│ ├── _modal_notification_template_preview.json
|
||||
│ └── _modal_password_confirm.json
|
||||
│
|
||||
├── 모듈 관리
|
||||
|
||||
Reference in new issue
Block a user