공개이슈 ··· 및 그 전수 해소분. - 설정 저장·복원·초기화가 상주 프로세스 미러를 갱신하도록 단일 소유자화 - 만료 데이터 자동 정리 예약 12종 신설, 파기는 기본키 배치로 분할 - 회원 탈퇴를 전 경로 원자화하고 관리자 상태변경 경로를 정식 탈퇴로 통일 - 일괄 처리 목록의 선택을 화면에 보이는 행으로 한정(selectionScope) - 비밀번호 정책 문구를 도메인 중립 라벨로 정정 - 정적 검사 2종·검증 스킬 설계 정합성 축 신설
1151 lines
34 KiB
Markdown
1151 lines
34 KiB
Markdown
# 컴포넌트 Props 레퍼런스
|
|
|
|
> **중요**: 레이아웃 JSON 작성 시 반드시 이 문서에서 지원되는 props를 확인하세요.
|
|
|
|
---
|
|
|
|
## 목차
|
|
|
|
1. [핵심 원칙](#핵심-원칙)
|
|
2. [Select 컴포넌트](#select-컴포넌트)
|
|
3. [Input 컴포넌트](#input-컴포넌트)
|
|
4. [Button 컴포넌트](#button-컴포넌트)
|
|
5. [DataGrid 컴포넌트](#datagrid-컴포넌트)
|
|
6. [HtmlEditor 컴포넌트](#htmleditor-컴포넌트)
|
|
7. [CodeEditor 컴포넌트](#codeeditor-컴포넌트)
|
|
8. [Modal 컴포넌트](#modal-컴포넌트)
|
|
9. [LoadingSpinner 컴포넌트](#loadingspinner-컴포넌트)
|
|
10. [관련 문서](#관련-문서)
|
|
|
|
---
|
|
|
|
## 핵심 원칙
|
|
|
|
```
|
|
필수: 레이아웃 JSON 작성 전 이 문서에서 props 확인 (문서에 없는 props 사용 금지)
|
|
필수: 작성 후 /validate-frontend 스킬로 검증
|
|
```
|
|
|
|
### props 사용 전 확인 체크리스트
|
|
|
|
레이아웃 JSON에서 컴포넌트 props 작성 시:
|
|
|
|
1. **이 문서에서 해당 컴포넌트의 지원 props 확인**
|
|
2. **props 타입과 기본값 확인**
|
|
3. **레이아웃 JSON 작성**
|
|
4. **`/validate-frontend` 스킬로 검증**
|
|
|
|
---
|
|
|
|
## Select 컴포넌트
|
|
|
|
**타입**: `basic` | **템플릿**: `sirsoft-admin_basic`, `sirsoft-basic`
|
|
|
|
**파일**: `templates/*/src/components/basic/Select.tsx`
|
|
|
|
### Props 인터페이스
|
|
|
|
```typescript
|
|
export interface SelectOption {
|
|
value: string | number; // 옵션 값 (필수)
|
|
label: string; // 표시될 라벨 (필수)
|
|
disabled?: boolean; // 옵션 비활성화 여부
|
|
}
|
|
|
|
export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
|
// 커스텀 Props
|
|
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; // 선택된 값 (제어 컴포넌트)
|
|
defaultValue?: string | number; // 기본 선택값 (비제어 컴포넌트)
|
|
name?: string; // 폼 필드명
|
|
id?: string; // 요소 ID
|
|
disabled?: boolean; // 비활성화
|
|
required?: boolean; // 필수 입력
|
|
className?: string; // CSS 클래스
|
|
multiple?: boolean; // 다중 선택 (children 모드만)
|
|
size?: number; // 표시 행 수 (children 모드만)
|
|
}
|
|
```
|
|
|
|
### 지원 Props 요약
|
|
|
|
| Props | 타입 | 필수 | 기본값 | 설명 |
|
|
|-------|------|------|--------|------|
|
|
| `options` | `SelectOption[]` \| `string[]` | ❌ | - | 드롭다운 옵션 배열 |
|
|
| `value` | `string` \| `number` | ❌ | - | 선택된 값 (제어 컴포넌트) |
|
|
| `defaultValue` | `string` \| `number` | ❌ | - | 기본 선택값 (비제어 컴포넌트) |
|
|
| `name` | `string` | ❌ | - | 폼 필드명 |
|
|
| `disabled` | `boolean` | ❌ | `false` | 비활성화 |
|
|
| `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일 때) |
|
|
|
|
### 렌더링 모드
|
|
|
|
Select 컴포넌트는 두 가지 모드로 동작합니다:
|
|
|
|
1. **Options 모드** (권장): `options` prop 제공 시 커스텀 드롭다운
|
|
2. **Children 모드**: `options` 없이 children으로 `<option>` 제공 시 기본 HTML select
|
|
|
|
### 레이아웃 JSON 사용 예시
|
|
|
|
#### 기본 사용 (제어 컴포넌트)
|
|
|
|
```json
|
|
{
|
|
"type": "basic",
|
|
"name": "Select",
|
|
"props": {
|
|
"name": "category",
|
|
"value": "{{_local.category}}",
|
|
"className": "w-full",
|
|
"options": [
|
|
{ "value": "", "label": "선택하세요" },
|
|
{ "value": "1", "label": "카테고리 1" },
|
|
{ "value": "2", "label": "카테고리 2" }
|
|
]
|
|
},
|
|
"actions": [
|
|
{
|
|
"type": "change",
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "_local",
|
|
"category": "{{$event.target.value}}"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### 기본값 설정 (defaultValue 사용)
|
|
|
|
```json
|
|
{
|
|
"type": "basic",
|
|
"name": "Select",
|
|
"props": {
|
|
"name": "status",
|
|
"defaultValue": "active",
|
|
"options": [
|
|
{ "value": "active", "label": "활성" },
|
|
{ "value": "inactive", "label": "비활성" }
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 공란 방지 패턴 (권장)
|
|
|
|
**방법 1: state 초기값 설정** (권장)
|
|
|
|
```json
|
|
{
|
|
"state": {
|
|
"salesStatus": "on_sale"
|
|
}
|
|
}
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "basic",
|
|
"name": "Select",
|
|
"props": {
|
|
"name": "salesStatus",
|
|
"value": "{{_local.salesStatus}}",
|
|
"options": [
|
|
{ "value": "on_sale", "label": "판매중" },
|
|
{ "value": "sold_out", "label": "품절" }
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**방법 2: defaultValue 사용**
|
|
|
|
```json
|
|
{
|
|
"type": "basic",
|
|
"name": "Select",
|
|
"props": {
|
|
"name": "status",
|
|
"value": "{{_local.status}}",
|
|
"defaultValue": "on_sale",
|
|
"options": [
|
|
{ "value": "on_sale", "label": "판매중" },
|
|
{ "value": "sold_out", "label": "품절" }
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### 주의사항
|
|
|
|
```
|
|
value 타입 일치: value와 option.value 타입을 맞춰야 함
|
|
공란 방지: state 초기값 또는 defaultValue 설정 필수
|
|
✅ options 배열: 첫 번째 항목이 기본 선택됨 (value가 일치할 때)
|
|
```
|
|
|
|
---
|
|
|
|
## Input 컴포넌트
|
|
|
|
**타입**: `basic` | **템플릿**: `sirsoft-admin_basic`, `sirsoft-basic`
|
|
|
|
**파일**: `templates/*/src/components/basic/Input.tsx`
|
|
|
|
### Props 인터페이스
|
|
|
|
```typescript
|
|
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
label?: string; // 레이블 텍스트
|
|
error?: string; // 에러 메시지
|
|
helperText?: string; // 도움말 텍스트
|
|
}
|
|
```
|
|
|
|
### 지원 Props 요약
|
|
|
|
| Props | 타입 | 필수 | 기본값 | 설명 |
|
|
|-------|------|------|--------|------|
|
|
| `type` | `string` | ❌ | `"text"` | input 타입 (text, email, password, number, date 등) |
|
|
| `value` | `string` \| `number` | ❌ | - | 입력값 (제어 컴포넌트) |
|
|
| `defaultValue` | `string` \| `number` | ❌ | - | 기본값 (비제어 컴포넌트) |
|
|
| `name` | `string` | ❌ | - | 폼 필드명 |
|
|
| `placeholder` | `string` | ❌ | - | 플레이스홀더 |
|
|
| `disabled` | `boolean` | ❌ | `false` | 비활성화 |
|
|
| `required` | `boolean` | ❌ | `false` | 필수 입력 |
|
|
| `readOnly` | `boolean` | ❌ | `false` | 읽기 전용 |
|
|
| `className` | `string` | ❌ | - | CSS 클래스 |
|
|
| `min` | `string` \| `number` | ❌ | - | 최소값 (number, date) |
|
|
| `max` | `string` \| `number` | ❌ | - | 최대값 (number, date) |
|
|
| `step` | `string` \| `number` | ❌ | - | 증가 단위 (number) |
|
|
| `maxLength` | `number` | ❌ | - | 최대 길이 |
|
|
| `pattern` | `string` | ❌ | - | 정규식 패턴 |
|
|
|
|
### IME 처리 (한글/CJK 입력)
|
|
|
|
Input 컴포넌트는 한글 등 IME 조합 입력을 올바르게 처리합니다:
|
|
|
|
- IME 조합 중에는 외부 `onChange`를 호출하지 않음 (내부 `localValue`로 즉시 시각적 피드백 제공)
|
|
- 조합 완료 후 최종 값으로 `onChange` 호출
|
|
- 조합 중에는 `onKeyPress` 이벤트도 발생하지 않음
|
|
|
|
**디바운스와 IME 조합 상호작용**:
|
|
|
|
```
|
|
디바운스가 설정된 Input에서 한글 입력 시:
|
|
- 조합 중에는 디바운스 타이머가 시작되지 않음
|
|
- 조합 완료 시점에 onChange 호출 → 디바운스 타이머 시작
|
|
- 빠른 타이핑 시 조합 완료 + 디바운스 지연이 합산됨
|
|
|
|
✅ localValue 패턴:
|
|
- Input은 내부 localValue 상태로 즉시 화면에 반영 (시각적 지연 없음)
|
|
- 외부 상태(_local)와 내부 상태(localValue)가 일시적으로 불일치 가능
|
|
- 디바운스 완료 시 외부 상태와 동기화됨
|
|
```
|
|
|
|
### 레이아웃 JSON 사용 예시
|
|
|
|
```json
|
|
{
|
|
"type": "basic",
|
|
"name": "Input",
|
|
"props": {
|
|
"type": "text",
|
|
"name": "searchKeyword",
|
|
"placeholder": "$t:common.search_placeholder",
|
|
"className": "input w-full",
|
|
"value": "{{_local.searchKeyword}}"
|
|
},
|
|
"actions": [
|
|
{
|
|
"type": "change",
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "_local",
|
|
"searchKeyword": "{{$event.target.value}}"
|
|
}
|
|
},
|
|
{
|
|
"type": "keypress",
|
|
"key": "Enter",
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "products"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Button 컴포넌트
|
|
|
|
**타입**: `basic` | **템플릿**: `sirsoft-admin_basic`, `sirsoft-basic`
|
|
|
|
**파일**: `templates/*/src/components/basic/Button.tsx`
|
|
|
|
### Props 인터페이스
|
|
|
|
```typescript
|
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
variant?: 'primary' | 'secondary' | 'danger' | 'success' | 'outline' | 'ghost';
|
|
size?: 'sm' | 'md' | 'lg';
|
|
loading?: boolean;
|
|
icon?: string;
|
|
}
|
|
```
|
|
|
|
### 지원 Props 요약
|
|
|
|
| Props | 타입 | 필수 | 기본값 | 설명 |
|
|
|-------|------|------|--------|------|
|
|
| `type` | `"button"` \| `"submit"` \| `"reset"` | ❌ | `"button"` | 버튼 타입 |
|
|
| `disabled` | `boolean` | ❌ | `false` | 비활성화 |
|
|
| `className` | `string` | ❌ | - | CSS 클래스 |
|
|
| `variant` | `string` | ❌ | `"primary"` | 버튼 스타일 |
|
|
| `size` | `string` | ❌ | `"md"` | 버튼 크기 |
|
|
| `loading` | `boolean` | ❌ | `false` | 로딩 상태 |
|
|
|
|
### 레이아웃 JSON 사용 예시
|
|
|
|
```json
|
|
{
|
|
"type": "basic",
|
|
"name": "Button",
|
|
"props": {
|
|
"type": "button",
|
|
"className": "btn btn-primary"
|
|
},
|
|
"text": "$t:common.search",
|
|
"actions": [
|
|
{
|
|
"type": "click",
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "products"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## DataGrid 컴포넌트
|
|
|
|
**타입**: `composite` | **템플릿**: `sirsoft-admin_basic`
|
|
|
|
**파일**: `templates/sirsoft-admin_basic/src/components/composite/DataGrid.tsx`
|
|
|
|
### Props 인터페이스
|
|
|
|
```typescript
|
|
export interface DataGridColumn {
|
|
field: string; // 필드명 (필수)
|
|
header: string; // 컬럼 헤더 텍스트 (필수)
|
|
width?: string; // 컬럼 너비 (예: "100px", "auto")
|
|
sortable?: boolean; // 정렬 가능 여부
|
|
template?: ComponentDefinition; // 셀 커스텀 템플릿
|
|
}
|
|
|
|
export interface DataGridProps {
|
|
data: any[]; // 데이터 배열 (필수)
|
|
columns: DataGridColumn[]; // 컬럼 정의 (필수)
|
|
rowKey?: string; // 행 고유 키 필드 (기본: 'id')
|
|
selectable?: boolean; // 행 선택 기능
|
|
selectedRows?: any[]; // 선택된 행 배열
|
|
selectionScope?: 'page' | 'free'; // 선택의 유효 범위 (아래 "선택의 유효 범위" 참조)
|
|
loading?: boolean; // 로딩 상태
|
|
className?: string; // CSS 클래스
|
|
|
|
// SubRow Props (engine-v1.6.0+)
|
|
subRowChildren?: ComponentDefinition[]; // 서브 행 컴포넌트 배열
|
|
subRowCondition?: string; // 서브 행 표시 조건 표현식
|
|
subRowClassName?: string; // 서브 행 CSS 클래스
|
|
|
|
// Footer Row Props
|
|
footerCells?: DataGridFooterCell[]; // 합계 행 셀 정의 (field별 매핑)
|
|
footerClassName?: string; // 합계 행 CSS 클래스
|
|
footerCardChildren?: ComponentDefinition[]; // 모바일 카드 뷰 합계 커스텀
|
|
|
|
// Responsive Props
|
|
responsiveBreakpoint?: number; // 반응형 전환점 (기본: 768)
|
|
}
|
|
```
|
|
|
|
### 지원 Props 요약
|
|
|
|
| Props | 타입 | 필수 | 기본값 | 설명 |
|
|
|-------|------|------|--------|------|
|
|
| `data` | `any[]` | ✅ | - | 테이블에 표시할 데이터 배열 |
|
|
| `columns` | `DataGridColumn[]` | ✅ | - | 컬럼 정의 배열 |
|
|
| `rowKey` | `string` | ❌ | `'id'` | 행 고유 키로 사용할 필드명 |
|
|
| `selectable` | `boolean` | ❌ | `false` | 체크박스 선택 기능 활성화 |
|
|
| `selectedRows` | `any[]` | ❌ | `[]` | 선택된 행 배열 (ID 배열) |
|
|
| `selectionScope` | `'page' \| 'free'` | ❌ | `'free'` | 선택의 유효 범위 (아래 참조) |
|
|
| `loading` | `boolean` | ❌ | `false` | 로딩 상태 표시 |
|
|
| `className` | `string` | ❌ | - | CSS 클래스 |
|
|
|
|
### 선택의 유효 범위 (selectionScope)
|
|
|
|
체크박스 선택은 컴포넌트 밖(전역/로컬 상태)에 저장된다. 그래서 검색·필터·페이지 이동으로 행이 목록에서 빠져도 그 행의 선택은 남는다. 그 상태에서 일괄 처리를 실행하면 사용자가 보고 있지도, 체크하지도 않은 행이 대상이 된다 — 확인 모달은 건수만 말하므로 실행 전에 알아챌 방법이 없고, 처리는 정상 성공하므로 오류도 남지 않는다.
|
|
|
|
`onSelectionChange` 를 배선하는 DataGrid 는 둘 중 하나를 반드시 명시한다 (기본값에 기대지 않는다).
|
|
|
|
| 값 | 동작 | 쓰는 곳 |
|
|
|----|------|--------|
|
|
| `"page"` | 지금 그려진 행에 없는 선택을 버린다. 대상은 언제나 "보이고 체크된 행" | 일괄 처리 버튼이 달린 목록 화면 |
|
|
| `"free"` | 선택을 컴포넌트가 건드리지 않는다 | 선택 자체가 저장 대상인 폼(적용 대상 고르기 등) — 여러 페이지에 걸쳐 고르는 선택기 포함 |
|
|
|
|
```json
|
|
{
|
|
"type": "composite",
|
|
"name": "DataGrid",
|
|
"props": {
|
|
"selectable": true,
|
|
"selectionScope": "page",
|
|
"selectedIds": "{{_global.selectedIds_admin_users || []}}"
|
|
},
|
|
"actions": [
|
|
{
|
|
"event": "onSelectionChange",
|
|
"type": "change",
|
|
"handler": "setState",
|
|
"params": { "target": "global", "selectedIds_admin_users": "{{$args[0]}}" }
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
값은 리터럴로 고정한다. 표현식으로 분기하면 분기마다 보존 여부가 갈려 한쪽이 조용히 대상 밖 행을 싣는다. `selectable` 이 꺼진 화면이라고 안전한 것도 아니다 — 체크박스가 없으면 남은 선택이 더 안 보이므로, 범위 판정은 `selectable` 과 무관하게 적용된다.
|
|
|
|
### SubRow Props (engine-v1.6.0+)
|
|
|
|
행 아래에 추가 정보를 표시하는 서브 행 기능입니다.
|
|
|
|
| Props | 타입 | 필수 | 기본값 | 설명 |
|
|
|-------|------|------|--------|------|
|
|
| `subRowChildren` | `ComponentDefinition[]` | ❌ | - | 서브 행에 렌더링할 컴포넌트 배열 |
|
|
| `subRowCondition` | `string` | ❌ | - | 서브 행 표시 조건 (예: `"{{row.status !== 'free'}}"`) |
|
|
| `subRowClassName` | `string` | ❌ | - | 서브 행 CSS 클래스 |
|
|
|
|
**subRowCondition 컨텍스트 변수**:
|
|
- `row`: 현재 행 데이터 객체
|
|
- `$index`: 현재 행 인덱스
|
|
|
|
### Footer Row Props
|
|
|
|
테이블 하단에 합계 행을 표시하는 기능입니다.
|
|
|
|
| Props | 타입 | 필수 | 기본값 | 설명 |
|
|
|-------|------|------|--------|------|
|
|
| `footerCells` | `DataGridFooterCell[]` | ❌ | - | 합계 행 셀 정의 (컬럼 field별 매핑) |
|
|
| `footerClassName` | `string` | ❌ | - | 합계 행 CSS 클래스 |
|
|
| `footerCardChildren` | `ComponentDefinition[]` | ❌ | - | 모바일 카드 뷰 합계 커스텀 렌더링 |
|
|
|
|
**FooterCell 구조**:
|
|
- `field`: 매핑할 컬럼 field (해당 컬럼 위치에 렌더링)
|
|
- `children`: 셀 내부 컴포넌트 정의 (`condition` 속성으로 조건부 표시 가능)
|
|
|
|
### 레이아웃 JSON 사용 예시
|
|
|
|
#### 기본 사용
|
|
|
|
```json
|
|
{
|
|
"type": "composite",
|
|
"name": "DataGrid",
|
|
"props": {
|
|
"data": "{{products?.data?.data || []}}",
|
|
"rowKey": "id",
|
|
"selectable": true,
|
|
"selectedRows": "{{_local.selectedItems || []}}",
|
|
"loading": "{{products?.loading}}",
|
|
"columns": [
|
|
{
|
|
"field": "name",
|
|
"header": "$t:product.table.name",
|
|
"sortable": true,
|
|
"width": "200px"
|
|
},
|
|
{
|
|
"field": "price",
|
|
"header": "$t:product.table.price",
|
|
"sortable": true,
|
|
"width": "100px"
|
|
}
|
|
]
|
|
},
|
|
"actions": [
|
|
{
|
|
"event": "onSelectionChange",
|
|
"type": "change",
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "_local",
|
|
"selectedItems": "{{$args[0]}}"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### SubRow 사용 (배송비 요약 표시)
|
|
|
|
```json
|
|
{
|
|
"type": "composite",
|
|
"name": "DataGrid",
|
|
"props": {
|
|
"data": "{{shipping_policies?.data?.data || []}}",
|
|
"rowKey": "id",
|
|
"subRowCondition": "{{row.charge_policy !== 'free'}}",
|
|
"subRowClassName": "px-6 py-2 bg-gray-50 dark:bg-gray-800/50 text-sm text-gray-600 dark:text-gray-400",
|
|
"columns": [...],
|
|
"subRowChildren": [
|
|
{
|
|
"type": "layout",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "flex items-center gap-2"
|
|
},
|
|
"children": [
|
|
{
|
|
"type": "basic",
|
|
"name": "Icon",
|
|
"props": {
|
|
"name": "truck",
|
|
"className": "w-4 h-4 text-gray-400"
|
|
}
|
|
},
|
|
{
|
|
"type": "basic",
|
|
"name": "Span",
|
|
"props": {
|
|
"className": "text-gray-500 dark:text-gray-400"
|
|
},
|
|
"text": "$t:module::admin/entity.fee_summary.label"
|
|
},
|
|
{
|
|
"type": "basic",
|
|
"name": "Span",
|
|
"props": {
|
|
"className": "font-medium text-gray-700 dark:text-gray-300"
|
|
},
|
|
"text": "{{row.fee_summary}}"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 컬럼 템플릿 사용
|
|
|
|
```json
|
|
{
|
|
"columns": [
|
|
{
|
|
"field": "status",
|
|
"header": "$t:common.status",
|
|
"width": "100px",
|
|
"template": {
|
|
"type": "basic",
|
|
"name": "Span",
|
|
"props": {
|
|
"className": "{{$row.status === 'active' ? 'text-green-600' : 'text-gray-400'}}"
|
|
},
|
|
"text": "{{$row.status_label}}"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**컬럼 템플릿 컨텍스트 변수**:
|
|
- `$row`: 현재 행 데이터 객체
|
|
- `$index`: 현재 행 인덱스
|
|
- `$value`: 현재 셀 값 (field에 해당하는 값)
|
|
|
|
### 주의사항
|
|
|
|
```
|
|
subRowChildren vs subRowCondition
|
|
- subRowChildren만 설정: 모든 행에 서브 행 표시
|
|
- subRowCondition 함께 설정: 조건 만족하는 행만 서브 행 표시
|
|
|
|
컨텍스트 변수 차이
|
|
- columns.template 내부: $row, $index, $value 사용
|
|
- subRowChildren 내부: row, $index 사용 ($ 없이 row)
|
|
|
|
✅ 다크 모드: subRowClassName에 dark: 클래스 쌍으로 지정
|
|
```
|
|
|
|
### cellChildren — 셀 내 커스텀 컴포넌트 (engine-v1.8.0+)
|
|
|
|
컬럼 `template` 대신 셀 단위로 커스텀 컴포넌트를 렌더링합니다. 조건부 렌더링과 다중 컴포넌트 배치를 지원합니다.
|
|
|
|
```typescript
|
|
interface DataGridCellChild {
|
|
column: string; // 대상 컬럼 field명 (필수)
|
|
condition?: string; // 표시 조건 (예: "{{row.status === 'active'}}")
|
|
children: ComponentDefinition[]; // 셀에 렌더링할 컴포넌트 배열
|
|
}
|
|
```
|
|
|
|
| Props | 타입 | 필수 | 기본값 | 설명 |
|
|
|-------|------|------|--------|------|
|
|
| `cellChildren` | `DataGridCellChild[]` | ❌ | - | 셀 내 커스텀 컴포넌트 배열 |
|
|
|
|
**cellChildren 컨텍스트 변수**:
|
|
- `row`: 현재 행 데이터 객체
|
|
- `value` / `$value`: 현재 셀 값 (column.field에 해당)
|
|
- `$index`: 현재 행 인덱스
|
|
|
|
```json
|
|
{
|
|
"cellChildren": [
|
|
{
|
|
"column": "actions",
|
|
"children": [
|
|
{
|
|
"type": "basic",
|
|
"name": "Button",
|
|
"props": {
|
|
"type": "button",
|
|
"className": "btn btn-sm btn-outline"
|
|
},
|
|
"text": "$t:common.edit",
|
|
"actions": [
|
|
{
|
|
"type": "click",
|
|
"handler": "navigate",
|
|
"params": { "path": "/admin/products/{{row.id}}/edit" }
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"column": "status",
|
|
"condition": "{{row.status === 'draft'}}",
|
|
"children": [
|
|
{
|
|
"type": "basic",
|
|
"name": "Span",
|
|
"props": { "className": "text-yellow-600 dark:text-yellow-400" },
|
|
"text": "$t:common.draft"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### expandChildren — 확장 가능한 행 (engine-v1.10.0+)
|
|
|
|
행을 클릭하여 추가 콘텐츠를 펼치는 확장 행 기능입니다.
|
|
|
|
| Props | 타입 | 필수 | 기본값 | 설명 |
|
|
|-------|------|------|--------|------|
|
|
| `expandable` | `boolean` | ❌ | `false` | 확장 기능 활성화 |
|
|
| `expandedRowIds` | `any[]` | ❌ | `[]` | 현재 펼쳐진 행 ID 배열 |
|
|
| `expandChildren` | `ComponentDefinition[]` | ❌ | - | 확장 영역에 렌더링할 컴포넌트 배열 |
|
|
| `expandContext` | `object` | ❌ | - | 확장 영역에 추가 전달할 컨텍스트 |
|
|
|
|
**expandChildren 컨텍스트 변수**:
|
|
- `__componentContext`: 확장 행에 자동 전달되는 컨텍스트 (현재 행 데이터 포함)
|
|
- `row`: 현재 행 데이터 객체
|
|
- iteration 지원: expandChildren 내에서 `iteration` 속성으로 하위 데이터 반복 렌더링 가능
|
|
|
|
```json
|
|
{
|
|
"type": "composite",
|
|
"name": "DataGrid",
|
|
"props": {
|
|
"data": "{{orders?.data?.data || []}}",
|
|
"rowKey": "id",
|
|
"expandable": true,
|
|
"expandedRowIds": "{{_local.expandedRows ?? []}}",
|
|
"columns": [...],
|
|
"expandChildren": [
|
|
{
|
|
"type": "layout",
|
|
"name": "Div",
|
|
"props": { "className": "p-4 bg-gray-50 dark:bg-gray-800" },
|
|
"children": [
|
|
{
|
|
"type": "basic",
|
|
"name": "H3",
|
|
"props": { "className": "text-sm font-medium mb-2" },
|
|
"text": "$t:order.items"
|
|
},
|
|
{
|
|
"type": "basic",
|
|
"name": "Span",
|
|
"text": "{{row.item_count}} $t:common.items"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
"actions": [
|
|
{
|
|
"event": "onExpandChange",
|
|
"type": "change",
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "_local",
|
|
"expandedRows": "{{$args[0]}}"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### disabledField — 행별 액션 비활성화 (engine-v1.14.0+)
|
|
|
|
`rowActions`의 개별 버튼을 행 데이터 기반으로 비활성화합니다. 권한(abilities) 연동에 주로 사용됩니다.
|
|
|
|
| Props | 타입 | 필수 | 기본값 | 설명 |
|
|
|-------|------|------|--------|------|
|
|
| `disabledField` | `string` | ❌ | - | 행 데이터 내 비활성화 판단 경로 (dot notation). falsy 값이면 비활성화 |
|
|
|
|
```json
|
|
{
|
|
"columns": [...],
|
|
"rowActions": [
|
|
{
|
|
"label": "$t:common.edit",
|
|
"handler": "navigate",
|
|
"params": { "path": "/admin/items/{{row.id}}/edit" },
|
|
"disabledField": "abilities.can_update"
|
|
},
|
|
{
|
|
"label": "$t:common.delete",
|
|
"handler": "apiCall",
|
|
"params": { "endpoint": "/api/admin/items/{{row.id}}", "method": "DELETE" },
|
|
"disabledField": "abilities.can_delete",
|
|
"confirm": "$t:common.confirm_delete"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
```
|
|
disabledField 동작
|
|
- disabledField 경로의 값이 falsy (false, null, undefined, 0, '') → 버튼 비활성화
|
|
- truthy → 버튼 활성화
|
|
- 경로가 존재하지 않으면 undefined → 비활성화
|
|
|
|
✅ abilities 패턴: API Resource에서 abilities 객체 반환 → disabledField로 참조
|
|
```
|
|
|
|
---
|
|
|
|
## HtmlEditor 컴포넌트
|
|
|
|
**타입**: `composite` | **템플릿**: `sirsoft-admin_basic`, `sirsoft-basic`
|
|
|
|
**파일**: `templates/sirsoft-admin_basic/src/components/composite/HtmlEditor.tsx`
|
|
|
|
### Props 인터페이스
|
|
|
|
```typescript
|
|
export interface HtmlEditorProps {
|
|
content?: string; // 콘텐츠 값
|
|
onChange?: (event: { target: { name: string; value: string } }) => void;
|
|
isHtml?: boolean; // HTML 모드 여부
|
|
onIsHtmlChange?: (event: { target: { name: string; checked: boolean } }) => void;
|
|
rows?: number; // Textarea 행 수 (기본: 15)
|
|
placeholder?: string;
|
|
label?: string;
|
|
showHtmlModeToggle?: boolean; // HTML 모드 체크박스 표시 (기본: true)
|
|
contentClassName?: string; // HtmlContent 미리보기 클래스
|
|
purifyConfig?: any; // DOMPurify 설정
|
|
className?: string;
|
|
name?: string; // 콘텐츠 필드명 (기본: 'content')
|
|
htmlFieldName?: string; // HTML 모드 필드명 (기본: 'content_mode')
|
|
readOnly?: boolean;
|
|
}
|
|
```
|
|
|
|
### 지원 Props 요약
|
|
|
|
| Props | 타입 | 필수 | 기본값 | 설명 |
|
|
|-------|------|------|--------|------|
|
|
| `content` | `string` | ❌ | `''` | 콘텐츠 값 |
|
|
| `onChange` | `function` | ❌ | - | 콘텐츠 변경 콜백 |
|
|
| `isHtml` | `boolean` | ❌ | `false` | HTML 모드 여부 |
|
|
| `onIsHtmlChange` | `function` | ❌ | - | HTML 모드 변경 콜백 |
|
|
| `rows` | `number` | ❌ | `15` | Textarea 행 수 |
|
|
| `placeholder` | `string` | ❌ | `''` | 플레이스홀더 |
|
|
| `label` | `string` | ❌ | - | 라벨 텍스트 |
|
|
| `showHtmlModeToggle` | `boolean` | ❌ | `true` | HTML 모드 체크박스 표시 |
|
|
| `name` | `string` | ❌ | `'content'` | 콘텐츠 필드명 |
|
|
| `htmlFieldName` | `string` | ❌ | `'content_mode'` | HTML 모드 필드명 |
|
|
| `readOnly` | `boolean` | ❌ | `false` | 읽기 전용 |
|
|
|
|
### 레이아웃 JSON 사용 예시
|
|
|
|
```json
|
|
{
|
|
"type": "composite",
|
|
"name": "HtmlEditor",
|
|
"props": {
|
|
"content": "{{_local.form?.content ?? ''}}",
|
|
"isHtml": "{{(_local.form?.content_mode ?? 'text') === 'html'}}",
|
|
"rows": 15,
|
|
"placeholder": "$t:board.form.content_placeholder",
|
|
"label": "$t:board.form.content",
|
|
"name": "content",
|
|
"htmlFieldName": "content_mode"
|
|
},
|
|
"actions": [
|
|
{
|
|
"event": "onChange",
|
|
"type": "change",
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "local",
|
|
"form": { "content": "{{$args[0].target.value}}" }
|
|
}
|
|
},
|
|
{
|
|
"event": "onIsHtmlChange",
|
|
"type": "change",
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "local",
|
|
"form": { "content_mode": "{{$args[0].target.checked ? 'html' : 'text'}}" }
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## CodeEditor 컴포넌트
|
|
|
|
**타입**: `composite` | **템플릿**: `sirsoft-admin_basic`
|
|
|
|
**파일**: `templates/sirsoft-admin_basic/src/components/composite/CodeEditor.tsx`
|
|
|
|
### Props 인터페이스
|
|
|
|
```typescript
|
|
export interface CodeEditorProps {
|
|
value: string; // 에디터 값 (필수)
|
|
onChange?: (value: string) => void; // 값 변경 콜백
|
|
language?: string; // 언어 (기본: 'json')
|
|
height?: string; // 높이 (기본: '100%')
|
|
readOnly?: boolean;
|
|
theme?: 'vs-dark' | 'vs-light'; // 테마 (기본: 'vs-dark')
|
|
}
|
|
```
|
|
|
|
### 지원 Props 요약
|
|
|
|
| Props | 타입 | 필수 | 기본값 | 설명 |
|
|
|-------|------|------|--------|------|
|
|
| `value` | `string` | ✅ | - | 에디터 값 |
|
|
| `onChange` | `function` | ❌ | - | 값 변경 콜백 |
|
|
| `language` | `string` | ❌ | `'json'` | 언어 (json, javascript, html, css 등) |
|
|
| `height` | `string` | ❌ | `'100%'` | 에디터 높이 |
|
|
| `readOnly` | `boolean` | ❌ | `false` | 읽기 전용 |
|
|
| `theme` | `string` | ❌ | `'vs-dark'` | 에디터 테마 |
|
|
|
|
### 레이아웃 JSON 사용 예시
|
|
|
|
```json
|
|
{
|
|
"type": "composite",
|
|
"name": "CodeEditor",
|
|
"props": {
|
|
"value": "{{_local.jsonContent ?? '{}'}}",
|
|
"language": "json",
|
|
"height": "400px",
|
|
"theme": "vs-dark"
|
|
},
|
|
"actions": [
|
|
{
|
|
"event": "onChange",
|
|
"type": "change",
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "local",
|
|
"jsonContent": "{{$args[0]}}"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### 주의사항
|
|
|
|
```
|
|
HtmlEditor vs CodeEditor onChange 차이
|
|
- HtmlEditor: { target: { name, value } } 이벤트 객체 전달
|
|
- CodeEditor: 문자열 직접 전달 ($args[0]가 문자열)
|
|
```
|
|
|
|
---
|
|
|
|
## Modal 컴포넌트
|
|
|
|
**타입**: `composite` | **템플릿**: `sirsoft-admin_basic`, `sirsoft-basic`
|
|
|
|
**파일**: `templates/*/src/components/composite/Modal.tsx`
|
|
|
|
### Props 인터페이스
|
|
|
|
```typescript
|
|
export interface ModalProps {
|
|
isOpen: boolean; // 모달 열림 상태 (템플릿 엔진이 자동 주입)
|
|
onClose: () => void; // 모달 닫기 콜백 (템플릿 엔진이 자동 주입)
|
|
title?: string; // 모달 타이틀
|
|
icon?: string; // 타이틀 옆 아이콘 (sirsoft-basic만)
|
|
iconClassName?: string; // 아이콘 색상 클래스 (sirsoft-basic만)
|
|
closeOnBackdropClick?: boolean; // 배경 클릭 시 닫기 (기본: true)
|
|
closeOnOverlayClick?: boolean; // closeOnBackdropClick 별칭 (레이아웃 JSON 호환)
|
|
showCloseButton?: boolean; // 닫기 버튼 표시 (기본: true)
|
|
closeOnEscape?: boolean; // ESC 키로 닫기 (기본: true)
|
|
width?: string; // 모달 너비 (기본: '500px')
|
|
className?: string; // CSS 클래스
|
|
style?: React.CSSProperties; // 인라인 스타일
|
|
children?: React.ReactNode; // 모달 내용
|
|
}
|
|
```
|
|
|
|
### 지원 Props 요약
|
|
|
|
| Props | 타입 | 필수 | 기본값 | 설명 |
|
|
|-------|------|------|--------|------|
|
|
| `isOpen` | `boolean` | ✅ | - | 모달 열림 상태 (템플릿 엔진 자동 주입) |
|
|
| `onClose` | `function` | ✅ | - | 모달 닫기 콜백 (템플릿 엔진 자동 주입) |
|
|
| `title` | `string` | ❌ | - | 모달 타이틀 |
|
|
| `icon` | `string` | ❌ | - | 타이틀 옆 아이콘 (sirsoft-basic) |
|
|
| `iconClassName` | `string` | ❌ | `'text-gray-500'` | 아이콘 색상 클래스 |
|
|
| `closeOnBackdropClick` | `boolean` | ❌ | `true` | 배경 클릭 시 닫기 |
|
|
| `closeOnOverlayClick` | `boolean` | ❌ | `true` | closeOnBackdropClick 별칭 |
|
|
| `showCloseButton` | `boolean` | ❌ | `true` | 헤더 닫기 버튼 표시 |
|
|
| `closeOnEscape` | `boolean` | ❌ | `true` | ESC 키로 닫기 허용 |
|
|
| `width` | `string` | ❌ | `'500px'` | 모달 너비 |
|
|
| `className` | `string` | ❌ | - | CSS 클래스 |
|
|
|
|
### 회피 불가 모달 설정
|
|
|
|
사용자가 닫기 버튼, 배경 클릭, ESC 키로 모달을 회피할 수 없도록 설정:
|
|
|
|
```json
|
|
{
|
|
"type": "composite",
|
|
"name": "Modal",
|
|
"props": {
|
|
"title": "$t:common.error",
|
|
"showCloseButton": false,
|
|
"closeOnOverlayClick": false,
|
|
"closeOnEscape": false
|
|
},
|
|
"children": [...]
|
|
}
|
|
```
|
|
|
|
### 레이아웃 JSON 사용 예시
|
|
|
|
#### modals 섹션에 정의 (권장)
|
|
|
|
```json
|
|
{
|
|
"modals": [
|
|
{
|
|
"id": "confirm_modal",
|
|
"type": "composite",
|
|
"name": "Modal",
|
|
"props": {
|
|
"title": "$t:common.confirm",
|
|
"width": "400px"
|
|
},
|
|
"children": [
|
|
{
|
|
"type": "basic",
|
|
"name": "P",
|
|
"text": "$t:common.confirm_message"
|
|
},
|
|
{
|
|
"type": "basic",
|
|
"name": "Button",
|
|
"props": { "type": "button" },
|
|
"text": "$t:common.ok",
|
|
"actions": [
|
|
{
|
|
"type": "click",
|
|
"handler": "closeModal"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### 모달 열기/닫기
|
|
|
|
```json
|
|
// 모달 열기
|
|
{
|
|
"handler": "openModal",
|
|
"target": "confirm_modal"
|
|
}
|
|
|
|
// 모달 닫기
|
|
{
|
|
"handler": "closeModal"
|
|
}
|
|
```
|
|
|
|
### 주의사항
|
|
|
|
```
|
|
isOpen/onClose는 템플릿 엔진이 자동 주입 - 직접 설정 불필요
|
|
modals 섹션은 반드시 배열 형식 (객체 형식 사용 시 렌더링 안 됨)
|
|
각 모달에 고유한 id 필수
|
|
✅ 회피 불가 모달: showCloseButton, closeOnOverlayClick, closeOnEscape 모두 false
|
|
```
|
|
|
|
---
|
|
|
|
## LoadingSpinner 컴포넌트
|
|
|
|
**타입**: `composite` | **템플릿**: `sirsoft-admin_basic`
|
|
|
|
**파일**: `templates/sirsoft-admin_basic/src/components/composite/LoadingSpinner.tsx`
|
|
|
|
### Props 인터페이스
|
|
|
|
```typescript
|
|
export type SpinnerSize = 'sm' | 'md' | 'lg' | 'xl';
|
|
|
|
export interface LoadingSpinnerProps {
|
|
id?: string; // 요소 ID (engine-v1.11.0+)
|
|
size?: SpinnerSize; // 스피너 크기 (기본: 'md')
|
|
color?: string; // 색상 클래스 (기본: 'text-blue-600')
|
|
fullscreen?: boolean; // 전체 화면 모드 (기본: false)
|
|
text?: string; // 로딩 텍스트
|
|
className?: string; // CSS 클래스
|
|
}
|
|
```
|
|
|
|
### 지원 Props 요약
|
|
|
|
| Props | 타입 | 필수 | 기본값 | 설명 |
|
|
|-------|------|------|--------|------|
|
|
| `id` | `string` | ❌ | - | 요소 ID (DOM selector 사용 시 필요) |
|
|
| `size` | `'sm'` \| `'md'` \| `'lg'` \| `'xl'` | ❌ | `'md'` | 스피너 크기 |
|
|
| `color` | `string` | ❌ | `'text-blue-600'` | Tailwind 색상 클래스 |
|
|
| `fullscreen` | `boolean` | ❌ | `false` | 전체 화면 모드 |
|
|
| `text` | `string` | ❌ | - | 로딩 텍스트 ($t: 다국어 지원) |
|
|
| `className` | `string` | ❌ | `''` | 추가 CSS 클래스 |
|
|
|
|
### size별 스피너 크기
|
|
|
|
| Size | 스피너 | 텍스트 |
|
|
|------|--------|--------|
|
|
| `sm` | `w-4 h-4 border-2` | `text-xs` |
|
|
| `md` | `w-8 h-8 border-2` | `text-sm` |
|
|
| `lg` | `w-12 h-12 border-3` | `text-base` |
|
|
| `xl` | `w-16 h-16 border-4` | `text-lg` |
|
|
|
|
### 레이아웃 JSON 사용 예시
|
|
|
|
#### 기본 사용
|
|
|
|
```json
|
|
{
|
|
"type": "composite",
|
|
"name": "LoadingSpinner",
|
|
"props": {
|
|
"size": "md",
|
|
"text": "$t:common.loading"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### id 지정 (scrollIntoView 등 DOM selector 사용 시)
|
|
|
|
```json
|
|
{
|
|
"id": "loading_indicator",
|
|
"type": "composite",
|
|
"name": "LoadingSpinner",
|
|
"if": "{{_global.infiniteScroll.isLoadingMore}}",
|
|
"props": {
|
|
"size": "sm",
|
|
"text": "$t:common.loading",
|
|
"color": "text-blue-600",
|
|
"className": "py-3"
|
|
}
|
|
}
|
|
```
|
|
|
|
렌더링 결과:
|
|
|
|
```html
|
|
<div id="loading_indicator" class="flex items-center justify-center p-4" aria-live="polite" aria-busy="true">
|
|
<div class="flex flex-col items-center justify-center gap-3 py-3">
|
|
<div class="w-4 h-4 border-2 border-gray-200 dark:border-gray-700 border-t-current rounded-full animate-spin text-blue-600" role="status"></div>
|
|
<span class="text-xs text-blue-600 font-medium">로딩 중...</span>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
#### 전체 화면 모드
|
|
|
|
```json
|
|
{
|
|
"type": "composite",
|
|
"name": "LoadingSpinner",
|
|
"if": "{{_global.isLoading}}",
|
|
"props": {
|
|
"fullscreen": true,
|
|
"size": "lg",
|
|
"text": "$t:common.loading"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 주의사항
|
|
|
|
```
|
|
id prop 사용: scrollIntoView 등 DOM selector로 접근해야 할 때 필수 지정
|
|
✅ 다크 모드: 자동 지원 (border-gray-200 dark:border-gray-700)
|
|
✅ 접근성: role="status", aria-label, aria-live="polite" 자동 설정
|
|
```
|
|
|
|
---
|
|
|
|
## 관련 문서
|
|
|
|
- [컴포넌트 개발 규칙](components.md) - basic, composite, layout 컴포넌트
|
|
- [레이아웃 JSON 스키마](layout-json.md) - 컴포넌트를 레이아웃 JSON에서 사용하는 방법
|
|
- [sirsoft-admin_basic 컴포넌트](templates/sirsoft-admin_basic/components.md) - Admin 컴포넌트 목록 (111개)
|
|
- [sirsoft-basic 컴포넌트](templates/sirsoft-basic/components.md) - User 컴포넌트 목록 (58개)
|
|
- [에디터 컴포넌트](editors.md) - HtmlEditor, CodeEditor 상세 가이드
|
|
- [데이터 바인딩](data-binding.md) - `{{}}` 표현식, `$t:` 다국어
|