# 컴포넌트 Props 레퍼런스 - Composite

> **관련 문서**: [컴포넌트 Props (Basic/DataGrid/Modal)](component-props.md) | [컴포넌트 개발 규칙](components.md) | [sirsoft-admin_basic 컴포넌트](templates/sirsoft-admin_basic/components.md)

---

## TL;DR (5초 요약)

```text
1. FileUploader: autoUpload, uploadTriggerEvent, imageCompression, apiEndpoints 커스터마이징
2. DynamicFieldList: columns 정의, enableDrag, errors 자동 매핑 (fields.0.name.ko → per-row)
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 연동 패턴 필수 숙지
```

---

## 목차

1. [FileUploader](#fileuploader)
2. [DynamicFieldList](#dynamicfieldlist)
3. [TagInput](#taginput)
4. [MultilingualInput](#multilingualinput)
5. [Toggle](#toggle)
6. [Checkbox](#checkbox)
7. [RadioGroup](#radiogroup)
8. [SearchBar](#searchbar)
9. [HtmlContent](#htmlcontent)

---

## FileUploader

**템플릿**: `sirsoft-admin_basic`, `sirsoft-basic`

파일/이미지 업로드 컴포넌트. 드래그앤드롭, 이미지 압축, 병렬 업로드, 대표 이미지 선택을 지원합니다.

### Props

| Prop | 타입 | 기본값 | 설명 |
| ---- | ---- | ------ | ---- |
| `uploadTriggerEvent` | string | - | 업로드 트리거 이벤트명 (emitEvent와 연동) |
| `attachmentableType` | string | - | 첨부 대상 모델 타입 (예: `'Product'`) |
| `attachmentableId` | number | - | 첨부 대상 모델 ID |
| `collection` | string | `'default'` | 컬렉션명 |
| `maxFiles` | number | `10` | 최대 첨부 개수 |
| `maxSize` | number | `10` | 파일당 최대 크기 (MB) |
| `maxConcurrentUploads` | number | `3` | 동시 업로드 수 |
| `accept` | string | - | 허용 확장자 (예: `"image/*"`, `".jpg,.png,.pdf"`) |
| `imageCompression` | object | - | 이미지 압축 옵션 `{ maxSizeMB, maxWidthOrHeight }` |
| `autoUpload` | boolean | `true` | 파일 선택 시 즉시 업로드 여부 |
| `uploadParams` | object | - | 업로드 시 추가 FormData 파라미터 |
| `initialFiles` | Attachment[] | - | 초기 첨부파일 목록 |
| `disabled` | boolean | `false` | 비활성화 (읽기 전용) |
| `roleIds` | number[] | - | 접근 가능 역할 ID 배열 |
| `enablePrimarySelection` | boolean | `false` | 대표 파일 선택 기능 활성화 |
| `primaryFileId` | number \| null | - | 현재 대표 파일 ID |
| `confirmBeforeRemove` | boolean | `false` | 삭제 전 확인 모달 표시 |
| `confirmRemoveTitle` | string | - | 삭제 확인 모달 제목 |
| `confirmRemoveMessage` | string | - | 삭제 확인 모달 메시지 |
| `apiEndpoints` | object | - | API 엔드포인트 커스터마이징 |
| `className` | string | - | 추가 CSS 클래스 |

### apiEndpoints 구조

```json
{
  "apiEndpoints": {
    "upload": "/api/admin/sirsoft-ecommerce/products/attachments",
    "delete": "/api/admin/sirsoft-ecommerce/products/attachments/:id",
    "reorder": "/api/admin/sirsoft-ecommerce/products/attachments/reorder"
  }
}
```

### imageCompression 구조

```json
{
  "imageCompression": {
    "maxSizeMB": 1,
    "maxWidthOrHeight": 1920
  }
}
```

### 콜백 이벤트

| 이벤트 | $args[0] 값 | 설명 |
| ------ | ----------- | ---- |
| `onUploadComplete` | Attachment[] | 업로드 완료 시 |
| `onUploadError` | `{ error, file }` | 업로드 에러 시 |
| `onRemove` | number (파일 ID) | 파일 삭제 시 |
| `onFilesChange` | PendingFile[] | 대기 파일 변경 시 |
| `onPrimaryChange` | number \| null | 대표 파일 변경 시 |

### 사용 예시

```json
{
  "type": "composite",
  "name": "FileUploader",
  "props": {
    "attachmentableType": "Product",
    "attachmentableId": "{{route.id}}",
    "collection": "images",
    "maxFiles": 10,
    "maxSize": 10,
    "accept": "image/*",
    "imageCompression": { "maxSizeMB": 1, "maxWidthOrHeight": 1920 },
    "initialFiles": "{{product?.data?.attachments ?? []}}",
    "enablePrimarySelection": true,
    "primaryFileId": "{{_local.featuredImageId}}"
  }
}
```

### 수동 업로드 패턴

```json
{
  "type": "composite",
  "name": "FileUploader",
  "props": {
    "autoUpload": false,
    "uploadTriggerEvent": "upload:product_images",
    "uploadParams": { "temp_key": "{{_local.ui.imageTempKey}}" }
  }
}
```

> 저장 버튼에서 `emitEvent` 핸들러로 `"upload:product_images"` 이벤트를 발행하면 업로드가 시작됩니다.

---

## DynamicFieldList

**템플릿**: `sirsoft-admin_basic`

동적 필드 목록 컴포넌트. 행 추가/삭제, 드래그 정렬, 다국어 입력, 검증 에러 매핑을 지원합니다.

### Props

| Prop | 타입 | 기본값 | 설명 |
| ---- | ---- | ------ | ---- |
| `items` | object[] | - | 데이터 항목 배열 |
| `columns` | DynamicFieldColumn[] | - | 컬럼 정의 |
| `enableDrag` | boolean | `true` | 드래그앤드롭 정렬 활성화 |
| `showIndex` | boolean | `true` | 순번 표시 |
| `minItems` | number | `0` | 최소 항목 수 (이하 삭제 불가) |
| `maxItems` | number | - | 최대 항목 수 (이상 추가 불가) |
| `addLabel` | string | `'항목 추가'` | 추가 버튼 라벨 |
| `emptyMessage` | string | `'항목이 없습니다.'` | 빈 상태 메시지 |
| `readOnly` | boolean | `false` | 읽기 전용 모드 |
| `errors` | object | - | 검증 에러 객체 |
| `itemIdKey` | string | `'_id'` | 항목별 고유 ID 키 |
| `rowActions` | RowAction[] | - | 행 액션 정의 |
| `name` | string | - | 폼 필드 이름 |
| `className` | string | - | 테이블 className |
| `headerClassName` | string | - | 헤더 className |
| `rowClassName` | string | - | 행 className |

### DynamicFieldColumn 구조

| 필드 | 타입 | 설명 |
| ---- | ---- | ---- |
| `key` | string | 데이터 필드명 |
| `label` | string | 컬럼 라벨 |
| `type` | string | `'input'` \| `'number'` \| `'select'` \| `'textarea'` \| `'multilingual'` \| `'custom'` |
| `width` | string | 컬럼 너비 (Tailwind 클래스, 예: `'w-48'`) |
| `placeholder` | string | 플레이스홀더 |
| `required` | boolean | 필수 여부 (헤더에 * 표시) |
| `options` | array | Select 옵션 (`type: 'select'`일 때) |
| `min` / `max` / `step` | number | 숫자 범위 (`type: 'number'`일 때) |
| `readOnly` | boolean | 컬럼별 읽기 전용 |
| `readOnlyCondition` | string | 조건부 읽기 전용 (필드명, `!` prefix로 부정) |

### 검증 에러 매핑

서버 검증 에러를 행별로 자동 추출합니다.

```text
서버 응답: { "fields.0.name.ko": ["필수 항목입니다"], "fields.1.content.ko": ["너무 깁니다"] }
                ↓ 자동 변환
행 0 에러: { "name.ko": ["필수 항목입니다"] }
행 1 에러: { "content.ko": ["너무 깁니다"] }
```

```json
{
  "type": "composite",
  "name": "DynamicFieldList",
  "props": {
    "errors": "{{_local.errors ?? {}}}"
  }
}
```

### 콜백 이벤트

| 이벤트 | $args[0] 값 | 설명 |
| ------ | ----------- | ---- |
| `onChange` | object[] | 데이터 변경 시 (전체 items 배열) |
| `onAddItem` | - | 항목 추가 시 |
| `onRemoveItem` | `{ index, item }` | 항목 삭제 시 |
| `onReorder` | object[] | 순서 변경 시 (정렬된 items) |

### 사용 예시

```json
{
  "type": "composite",
  "name": "DynamicFieldList",
  "props": {
    "items": "{{_local.fields ?? []}}",
    "columns": [
      { "key": "label", "label": "$t:common.label", "type": "multilingual", "required": true },
      { "key": "value", "label": "$t:common.value", "type": "input" },
      { "key": "count", "label": "$t:common.count", "type": "number", "min": 0 }
    ],
    "enableDrag": true,
    "minItems": 1,
    "maxItems": 10,
    "errors": "{{_local.errors ?? {}}}"
  }
}
```

---

## TagInput

**템플릿**: `sirsoft-admin_basic`

태그 선택/생성 컴포넌트. 자동완성, 새 태그 생성, 붙여넣기 분할, 그룹화를 지원합니다.

### Props

| Prop | 타입 | 기본값 | 설명 |
| ---- | ---- | ------ | ---- |
| `value` | array \| string \| number | - | 선택된 값 |
| `options` | TagOption[] | - | 선택 가능한 옵션 목록 |
| `creatable` | boolean | `false` | 새 항목 추가 가능 여부 |
| `isMulti` | boolean | `true` | 다중 선택 여부 |
| `isClearable` | boolean | `true` | 선택 해제 가능 (싱글 모드) |
| `isSearchable` | boolean | `true` | 검색 가능 여부 |
| `maxItems` | number | - | 최대 선택 개수 (멀티 모드) |
| `delimiters` | string[] | `[',']` | 태그 구분자 (creatable 모드) |
| `splitOnPaste` | boolean | `true` | 붙여넣기 시 자동 분리 |
| `placeholder` | string | - | 입력창 placeholder |
| `noOptionsMessage` | string | - | 검색 결과 없을 때 메시지 |
| `formatCreateLabel` | string | - | 새 항목 추가 라벨 |
| `createLabelSuffix` | string | `'추가'` | 새 항목 추가 접미사 |
| `disabled` | boolean | `false` | 비활성화 |
| `tagVariants` | object | - | 태그 값별 색상 맵핑 |
| `defaultVariant` | string | - | 기본 태그 색상 |
| `name` | string | - | 폼 필드 이름 |
| `className` | string | - | 추가 CSS 클래스 |

### TagOption 구조

| 필드 | 타입 | 설명 |
| ---- | ---- | ---- |
| `value` | string \| number | 옵션 값 |
| `label` | string | 표시 라벨 |
| `count` | number | 카운트 표시 (선택사항) |
| `isDisabled` | boolean | 비활성화 |
| `group` | string | 그룹명 (드롭다운 그룹 헤더) |
| `description` | string | 상세 설명 (옵션 아래 표시) |

### tagVariants 색상

사용 가능한 variant: `'blue'`, `'green'`, `'amber'`, `'red'`, `'purple'`, `'gray'`

```json
{
  "tagVariants": {
    "active": "green",
    "inactive": "gray",
    "pending": "amber"
  },
  "defaultVariant": "blue"
}
```

### 콜백 이벤트

| 이벤트 | $args[0] 값 | 설명 |
| ------ | ----------- | ---- |
| `onChange` | (string \| number)[] | 값 변경 시 (태그 배열) |
| `onCreateOption` | string | 새 옵션 생성 시 (입력값) |
| `onInputChange` | string | 검색 입력 변경 시 (비동기 검색용) |
| `onBeforeRemove` | TagOption | 삭제 전 확인 (true: 허용) |

### 사용 예시

```json
{
  "type": "composite",
  "name": "TagInput",
  "props": {
    "value": "{{_local.selectedTags ?? []}}",
    "options": "{{$computed.tagOptions ?? []}}",
    "creatable": true,
    "maxItems": 10,
    "delimiters": [",", ";"],
    "splitOnPaste": true,
    "placeholder": "$t:common.select_tags"
  },
  "actions": [
    {
      "event": "onChange",
      "handler": "setState",
      "params": { "target": "local", "selectedTags": "{{$args[0]}}" }
    }
  ]
}
```

---

## MultilingualInput

**템플릿**: `sirsoft-admin_basic`

다국어 입력 컴포넌트. 인라인/탭/컴팩트 레이아웃으로 로케일별 텍스트를 입력합니다.

### Props

| Prop | 타입 | 기본값 | 설명 |
| ---- | ---- | ------ | ---- |
| `value` | object | - | 다국어 값 (`{ ko: "...", en: "..." }`) |
| `inputType` | string | `'text'` | 입력 타입: `'text'` \| `'textarea'` |
| `layout` | string | `'inline'` | 레이아웃: `'inline'` \| `'tabs'` \| `'compact'` |
| `availableLocales` | LocaleOption[] | - | 사용 가능한 언어 목록 |
| `defaultLocale` | string | - | 기본 언어 코드 |
| `placeholder` | string | - | 입력창 placeholder |
| `required` | boolean | `false` | 필수 입력 여부 |
| `disabled` | boolean | `false` | 비활성화 |
| `maxLength` | number | - | 최대 길이 |
| `rows` | number | - | textarea 행 수 (`inputType: 'textarea'`일 때) |
| `error` | string | - | 에러 메시지 |
| `name` | string | - | 폼 필드 이름 |
| `showCodeOnMobile` | boolean | `false` | 모바일에서 언어 코드로 표시 |
| `className` | string | - | 추가 CSS 클래스 |

### 레이아웃 모드

| 모드 | 설명 |
| ---- | ---- |
| `inline` | 모든 언어를 수직으로 나열 (기본) |
| `tabs` | 탭으로 언어 전환 |
| `compact` | 탭과 입력 필드가 한 줄에 표시 |

### LocaleOption 구조

```json
{ "code": "ko", "name": "Korean", "nativeName": "한국어" }
```

### availableLocales 정책

`availableLocales` prop 은 활성 코어 언어팩을 따라가도록 **동적 바인딩** 합니다. 정적 배열 (`[{code:"ko", ...}, {code:"en", ...}]`) 로 작성하면 ja 등 추가 언어팩이 활성화돼도 입력 탭이 노출되지 않아 데이터 입력이 거부됩니다.

```json
// ✅ DO: _global.appConfig.supportedLocales 기반 동적 매핑
"availableLocales": "{{(_global.appConfig?.supportedLocales ?? ['ko', 'en']).map(l => ({ code: l, name: _global.appConfig?.localeNames?.[l] ?? l, nativeName: _global.appConfig?.localeNames?.[l] ?? l }))}}"

// ❌ DON'T: ko/en 정적 하드코딩 — 활성 언어팩 ja 미반영
"availableLocales": [
  { "code": "ko", "name": "한국어", "nativeName": "한국어" },
  { "code": "en", "name": "English", "nativeName": "English" }
]
```

prop 미지정 시 컴포넌트가 `G7Core.locale.supported()` 로 자동 폴백 — **prop 자체를 생략해도 무방** 합니다 (가장 권장).

### 사용 예시

```json
{
  "type": "composite",
  "name": "MultilingualInput",
  "props": {
    "value": "{{_local.form.name ?? {}}}",
    "layout": "tabs",
    "inputType": "text",
    "placeholder": "$t:common.enter_name",
    "required": true
  }
}
```

---

## Toggle

**템플릿**: `sirsoft-admin_basic`

토글 스위치 컴포넌트. 세 가지 크기와 선택적 라벨/설명을 지원합니다.

### Props

| Prop | 타입 | 기본값 | 설명 |
| ---- | ---- | ------ | ---- |
| `checked` | boolean | - | 체크 상태 |
| `value` | boolean | - | 체크 상태 (`checked`의 대체) |
| `size` | string | `'md'` | 크기: `'sm'` \| `'md'` \| `'lg'` |
| `label` | string | - | 라벨 텍스트 |
| `description` | string | - | 설명 텍스트 (라벨 아래 작은 글씨) |
| `disabled` | boolean | `false` | 비활성화 |
| `name` | string | - | 폼 필드 이름 |
| `className` | string | - | 추가 CSS 클래스 |

### 크기 비교

| 크기 | 사용 시점 |
| ---- | --------- |
| `sm` | 테이블 셀, 밀집 UI |
| `md` | 일반 폼 필드 (기본) |
| `lg` | 강조가 필요한 설정 |

### 사용 예시

```json
{
  "type": "composite",
  "name": "Toggle",
  "props": {
    "checked": "{{_local.form.is_active ?? false}}",
    "size": "md",
    "label": "$t:common.active",
    "description": "$t:admin.settings.active_description"
  },
  "actions": [
    {
      "type": "change",
      "handler": "setState",
      "params": { "target": "local", "form.is_active": "{{$event.target.checked}}" }
    }
  ]
}
```

---

## Checkbox

**템플릿**: `sirsoft-admin_basic`, `sirsoft-basic`

체크박스 컴포넌트. `indeterminate` 상태로 부분 선택을 표현합니다.

> **참고**: Checkbox는 Basic 컴포넌트이지만, `indeterminate` prop이 특수하므로 여기에 포함합니다.

### 추가 Props

| Prop | 타입 | 기본값 | 설명 |
| ---- | ---- | ------ | ---- |
| `indeterminate` | boolean | `false` | 부분 선택 상태 (HTML 속성이 아닌 DOM 프로퍼티) |
| `label` | string | - | 라벨 텍스트 |

> 표준 HTML input 속성(`checked`, `disabled`, `name`, `value`, `className` 등)을 모두 지원합니다.

### indeterminate 사용 예시

```json
{
  "type": "basic",
  "name": "Checkbox",
  "props": {
    "checked": "{{$computed.allSelected}}",
    "indeterminate": "{{$computed.someSelected && !$computed.allSelected}}"
  }
}
```

---

## RadioGroup

**템플릿**: `sirsoft-admin_basic`

라디오 버튼 그룹 컴포넌트. 수직/수평 배치와 옵션별 비활성화를 지원합니다.

### Props

| Prop | 타입 | 기본값 | 설명 |
| ---- | ---- | ------ | ---- |
| `name` | string | - | 그룹 이름 (필수) |
| `value` | string | - | 현재 선택된 값 |
| `options` | RadioOption[] | - | 라디오 옵션 배열 |
| `inline` | boolean | `false` | 인라인(가로) 배치 |
| `disabled` | boolean | `false` | 전체 비활성화 |
| `size` | string | `'md'` | 크기: `'sm'` \| `'md'` \| `'lg'` |
| `label` | string | - | 그룹 라벨 |
| `error` | string | - | 에러 메시지 |
| `className` | string | - | 추가 CSS 클래스 |

### RadioOption 구조

| 필드 | 타입 | 설명 |
| ---- | ---- | ---- |
| `value` | string | 옵션 값 |
| `label` | string | 옵션 라벨 |
| `disabled` | boolean | 개별 옵션 비활성화 |

### 사용 예시

```json
{
  "type": "composite",
  "name": "RadioGroup",
  "props": {
    "name": "shipping_type",
    "value": "{{_local.form.shipping_type ?? 'standard'}}",
    "options": [
      { "value": "standard", "label": "$t:common.standard" },
      { "value": "express", "label": "$t:common.express" },
      { "value": "pickup", "label": "$t:common.pickup", "disabled": true }
    ],
    "inline": true
  },
  "actions": [
    {
      "type": "change",
      "handler": "setState",
      "params": { "target": "local", "form.shipping_type": "{{$event.target.value}}" }
    }
  ]
}
```

---

## SearchBar

**템플릿**: `sirsoft-admin_basic`, `sirsoft-basic`

검색 바 컴포넌트. 자동완성 제안과 검색 버튼을 지원합니다.

### Props

| Prop | 타입 | 기본값 | 설명 |
| ---- | ---- | ------ | ---- |
| `value` | string | - | 검색어 |
| `placeholder` | string | - | 플레이스홀더 |
| `showButton` | boolean | `false` | 검색 버튼 표시 |
| `suggestions` | SearchSuggestion[] | - | 자동완성 제안 목록 |
| `showSuggestions` | boolean | - | 제안 목록 표시 여부 |
| `name` | string | - | 폼 필드 이름 |
| `className` | string | - | 추가 CSS 클래스 |
| `style` | object | - | 인라인 스타일 |

### SearchSuggestion 구조

```json
{ "id": 1, "text": "검색어 제안" }
```

### 콜백 이벤트

| 이벤트 | $args[0] 값 | 설명 |
| ------ | ----------- | ---- |
| `onChange` | ChangeEvent | 검색어 변경 시 (DOM 이벤트) |
| `onSubmit` | FormEvent | 검색 제출 시 |
| `onSuggestionClick` | SearchSuggestion | 제안 항목 클릭 시 |

### 사용 예시

```json
{
  "type": "composite",
  "name": "SearchBar",
  "props": {
    "value": "{{_local.searchKeyword ?? ''}}",
    "placeholder": "$t:common.search",
    "showButton": true
  },
  "actions": [
    {
      "type": "change",
      "handler": "setState",
      "params": { "target": "local", "searchKeyword": "{{$event.target.value}}" }
    }
  ]
}
```

---

## 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)
- [컴포넌트 개발 규칙](components.md)
- [컴포넌트 고급 기능](components-advanced.md)
- [sirsoft-admin_basic 컴포넌트](templates/sirsoft-admin_basic/components.md)
- [액션 핸들러 - 커스텀 콜백](actions.md#커스텀-이벤트-event-필드)
- [보안 가이드 - HTML 렌더링](security.md#htmlcontent--htmleditor-html-렌더링이-필요한-경우)
- [에디터 컴포넌트](editors.md)
