Files
Gnuboard7/docs/frontend/component-props.md
T
HeuJung 4e1ec6e001 fix(core,admin_basic,board,ecommerce,page,gdpr,verification): 설정 미러·자동 정리·탈퇴 원자성·일괄 선택 범위 정비
공개이슈 ··· 및 그 전수 해소분.

- 설정 저장·복원·초기화가 상주 프로세스 미러를 갱신하도록 단일 소유자화
- 만료 데이터 자동 정리 예약 12종 신설, 파기는 기본키 배치로 분할
- 회원 탈퇴를 전 경로 원자화하고 관리자 상태변경 경로를 정식 탈퇴로 통일
- 일괄 처리 목록의 선택을 화면에 보이는 행으로 한정(selectionScope)
- 비밀번호 정책 문구를 도메인 중립 라벨로 정정
- 정적 검사 2종·검증 스킬 설계 정합성 축 신설
2026-08-14 11:06:25 +09:00

34 KiB

컴포넌트 Props 레퍼런스

중요: 레이아웃 JSON 작성 시 반드시 이 문서에서 지원되는 props를 확인하세요.


목차

  1. 핵심 원칙
  2. Select 컴포넌트
  3. Input 컴포넌트
  4. Button 컴포넌트
  5. DataGrid 컴포넌트
  6. HtmlEditor 컴포넌트
  7. CodeEditor 컴포넌트
  8. Modal 컴포넌트
  9. 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 인터페이스

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 사용 예시

기본 사용 (제어 컴포넌트)

{
  "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 사용)

{
  "type": "basic",
  "name": "Select",
  "props": {
    "name": "status",
    "defaultValue": "active",
    "options": [
      { "value": "active", "label": "활성" },
      { "value": "inactive", "label": "비활성" }
    ]
  }
}

공란 방지 패턴 (권장)

방법 1: state 초기값 설정 (권장)

{
  "state": {
    "salesStatus": "on_sale"
  }
}
{
  "type": "basic",
  "name": "Select",
  "props": {
    "name": "salesStatus",
    "value": "{{_local.salesStatus}}",
    "options": [
      { "value": "on_sale", "label": "판매중" },
      { "value": "sold_out", "label": "품절" }
    ]
  }
}

방법 2: defaultValue 사용

{
  "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 인터페이스

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 사용 예시

{
  "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 인터페이스

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 사용 예시

{
  "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 인터페이스

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" 선택을 컴포넌트가 건드리지 않는다 선택 자체가 저장 대상인 폼(적용 대상 고르기 등) — 여러 페이지에 걸쳐 고르는 선택기 포함
{
  "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: 현재 행 인덱스

테이블 하단에 합계 행을 표시하는 기능입니다.

Props 타입 필수 기본값 설명
footerCells DataGridFooterCell[] ❌ - 합계 행 셀 정의 (컬럼 field별 매핑)
footerClassName string ❌ - 합계 행 CSS 클래스
footerCardChildren ComponentDefinition[] ❌ - 모바일 카드 뷰 합계 커스텀 렌더링

FooterCell 구조:

  • field: 매핑할 컬럼 field (해당 컬럼 위치에 렌더링)
  • children: 셀 내부 컴포넌트 정의 (condition 속성으로 조건부 표시 가능)

레이아웃 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 사용 (배송비 요약 표시)

{
  "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}}"
          }
        ]
      }
    ]
  }
}

컬럼 템플릿 사용

{
  "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 대신 셀 단위로 커스텀 컴포넌트를 렌더링합니다. 조건부 렌더링과 다중 컴포넌트 배치를 지원합니다.

interface DataGridCellChild {
  column: string;                    // 대상 컬럼 field명 (필수)
  condition?: string;                // 표시 조건 (예: "{{row.status === 'active'}}")
  children: ComponentDefinition[];   // 셀에 렌더링할 컴포넌트 배열
}
Props 타입 필수 기본값 설명
cellChildren DataGridCellChild[] ❌ - 셀 내 커스텀 컴포넌트 배열

cellChildren 컨텍스트 변수:

  • row: 현재 행 데이터 객체
  • value / $value: 현재 셀 값 (column.field에 해당)
  • $index: 현재 행 인덱스
{
  "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 속성으로 하위 데이터 반복 렌더링 가능
{
  "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 값이면 비활성화
{
  "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 인터페이스

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 사용 예시

{
  "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 인터페이스

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 사용 예시

{
  "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 인터페이스

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 키로 모달을 회피할 수 없도록 설정:

{
  "type": "composite",
  "name": "Modal",
  "props": {
    "title": "$t:common.error",
    "showCloseButton": false,
    "closeOnOverlayClick": false,
    "closeOnEscape": false
  },
  "children": [...]
}

레이아웃 JSON 사용 예시

modals 섹션에 정의 (권장)

{
  "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"
            }
          ]
        }
      ]
    }
  ]
}

모달 열기/닫기

// 모달 열기
{
  "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 인터페이스

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 사용 예시

기본 사용

{
  "type": "composite",
  "name": "LoadingSpinner",
  "props": {
    "size": "md",
    "text": "$t:common.loading"
  }
}

id 지정 (scrollIntoView 등 DOM selector 사용 시)

{
  "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"
  }
}

렌더링 결과:

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

전체 화면 모드

{
  "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" 자동 설정

관련 문서