v7.0.0-beta.2 release

This commit is contained in:
HeuJung committed 2026-04-20 20:37:49 +09:00
1 parent 6595fd0eb5
commit fa8eba7a0d
1165 files changed
+107118 -762127

No files matched your search

+308 -4
View File
@@ -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) 동적으로 라우트 설정이 변경된 경우
---
+4 -2
View File
@@ -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` 사용
---
+6 -5
View File
@@ -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` |
+5 -3
View File
@@ -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)
+4
View File
@@ -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일 때) |
### 렌더링 모드
+11 -1
View File
@@ -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"`와 동일하게 동작합니다.
+42
View File
@@ -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
+19
View File
@@ -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
+56 -6
View File
@@ -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 | ❌ | 주입 컴포넌트에 전달할 액션 객체 (평가 없이 전달) |
---
+5 -2
View File
@@ -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
│
├── 모듈 관리