# 컴포넌트 Props 레퍼런스 - Composite > **관련 문서**: [컴포넌트 Props (Basic/DataGrid/Modal)](component-props.md) | [컴포넌트 개발 규칙](components.md) | [sirsoft-admin_basic 컴포넌트](../../templates/_bundled/sirsoft-admin_basic/docs/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/_bundled/sirsoft-admin_basic/docs/components.md) - [액션 핸들러 - 커스텀 콜백](actions.md#커스텀-이벤트-event-필드) - [보안 가이드 - HTML 렌더링](security.md#htmlcontent--htmleditor-html-렌더링이-필요한-경우) - [에디터 컴포넌트](editors.md)