v2.4.0
← 홈으로 📘 튜토리얼

Dee Editor 매뉴얼

Dee Editor는 외부 의존성 없이 순수 JavaScript로 구현된 WYSIWYG 웹 에디터입니다. 이 문서는 설치부터 고급 기능 연동까지 전체 API를 설명합니다.

📑 이 문서는 "공식 API 레퍼런스"입니다. 모든 옵션·메서드·이벤트·타입의 정확한 단일 출처(SSOT)입니다. 실행 가능한 예제·통합 패턴은 📘 튜토리얼를 참조하세요.
최신 버전 v2.4.0 기준으로 작성되었습니다. © 2024-2026 DEXTSOLUTION Inc.

1. 빠른 시작

1-1. 파일 로드

아래 두 파일을 HTML에 포함합니다. Word 가져오기가 필요한 경우 추가 플러그인을 로드하세요.

<!-- 스타일시트 -->
<link rel="stylesheet" href="dist/webeditor.min.css">

<!-- 코어 번들 -->
<script src="dist/webeditor.min.js"></script>

<!-- Word 가져오기 기능이 필요한 경우 (선택) -->
<script src="dist/plugins/wordimport-native.min.js"></script>

1-2. 기본 초기화

<div id="editor"></div>

<script>
const editor = new DeeEditor('#editor', {
  height: '400px'
});
</script>

1-3. 데이터 읽기/쓰기

// 본문 HTML 가져오기 (폼 submit 시 사용)
const html = editor.getHTML();

// 본문 설정 (기존 데이터 불러올 때)
editor.setHTML('<p>저장된 내용</p>');

// 순수 텍스트만 가져오기
const text = editor.getText();

2. 초기화 옵션

new DeeEditor(selector, options)
옵션타입기본값설명
heightstring | number'400px'에디터 높이
placeholderstring''빈 상태 안내 텍스트
defaultFontstring'Malgun Gothic'기본 글꼴
defaultFontSizenumber14기본 글자 크기 (pt)
contentCSSstring''편집 영역에 적용할 추가 CSS
readOnlybooleanfalse읽기 전용 모드
autofocusbooleanfalse초기화 직후 자동 포커스
enterKey'p' | 'br''p'Enter 키 줄바꿈 태그
maxImageWidthnumber0삽입 이미지 최대 너비 (px, 0=무제한)
pastePlainTextbooleanfalse붙여넣기 시 서식 제거
showTableGuidebooleanfalse테두리 없는 표를 점선으로 표시
logoUrlstring''About 다이얼로그에 표시할 로고 이미지 URL
licensestring''라이선스 키 (WED-XXXX-...)
licenseLang'ko' | 'en' | nullnull라이선스 모달 언어 (null=자동)
uploadHandler(file, headers?) ⇒ Promise<string>null이미지 서버 업로드 핸들러 (2번째 인자 headers는 v2.4)
pluginsstring[] | Object전체활성 플러그인 목록. Word 가져오기는 'wordimport' 포함 필요
menubarboolean | Arrayfalse상단 메뉴바. true=기본 7그룹, 배열=커스텀 구성
onInitFunctionnull초기화 완료 콜백
onKeyDownFunctionnull키다운 이벤트 콜백 (return false → 차단)
onKeyUpFunctionnull키업 이벤트 콜백
onBeforeCommandFunctionnull커맨드 실행 전 콜백 (return false → 차단)
onTabChangeFunctionnull탭 변경 콜백 (return false → 차단)
onMenuCommandFunctionnull메뉴 명령 실행 전 콜백 (v2.4, return false → 차단)
onLicenseValidFunctionnull정식 라이선스 검증 성공 콜백
onLicenseInvalidFunctionnull라이선스 검증 실패 콜백
── v2.4.0 신규 옵션 ──
autoSaveObject | nullnull자동 임시 저장 { interval, storageKey, unloadWarning, unloadMessage, onSave }
inlineToolbarboolean | string[]false텍스트 선택 시 부유 미니툴바
hyperLinkDefaultTarget'_self'|'_blank'|'_top'|'_parent''_self'새 하이퍼링크 기본 target
privacyObject | nullnull개인정보 검출 { detect:[...], onDetect }
profanityObject | nullnull금칙어 검출 { words, onDetect, maskChar }
uploadHeadersObject | nullnull업로드 요청 커스텀 헤더
csrfCookiestring | nullnull쿠키값을 X-CSRF-Token 헤더로 자동 주입
evaluationWatermarkbooleantrue평가판 모드 시 본문 배경 워터마크 표시

2-1. uploadHandler 사용 예

uploadHandler를 제공하지 않으면 이미지를 base64 인라인으로 저장합니다. 서버 저장이 필요한 환경에서는 반드시 설정하세요.
new DeeEditor('#editor', {
  uploadHandler: async (file) => {
    const form = new FormData();
    form.append('file', file);
    const res = await fetch('/api/upload', {
      method: 'POST',
      body: form
    });
    const data = await res.json();
    return data.url; // 업로드된 이미지 URL 반환
  }
});

3. 인스턴스 메서드

대부분의 setter 메서드는 this를 반환하여 메서드 체이닝이 가능합니다.

3-1. 콘텐츠 I/O

getHTML() → string

에디터 본문의 HTML 문자열을 반환합니다.

const html = editor.getHTML();
// '<p>안녕하세요</p><p>반갑습니다</p>'
setHTML(html) → this

에디터 본문을 주어진 HTML로 설정합니다.

editor.setHTML('<p>새 내용</p>');
getText() → string

HTML 태그를 제거한 순수 텍스트를 반환합니다.

const text = editor.getText();
// '안녕하세요\n반갑습니다'
appendHTML(html) → this

본문 끝에 HTML을 추가합니다.

editor.appendHTML('<p>추가된 단락</p>');
insertHTML(html) → this

현재 커서 위치에 HTML을 삽입합니다. 선택 영역이 있으면 대체합니다.

editor.insertHTML('<strong>굵은 텍스트</strong>');
getSelectedHTML() → string

현재 선택 영역의 HTML을 반환합니다. 선택이 없으면 ''.

const selected = editor.getSelectedHTML();
getSelectedText() → string

현재 선택 영역의 순수 텍스트를 반환합니다.

const text = editor.getSelectedText();
clear() → this

에디터 본문을 완전히 비웁니다.

editor.clear();

3-2. 상태 제어

setReadOnly(bool) → this

에디터를 읽기 전용으로 설정하거나 해제합니다. 읽기 전용 시 wrapper에 .we-readonly 클래스가 부여됩니다.

editor.setReadOnly(true);   // 읽기 전용
editor.setReadOnly(false);  // 편집 가능
isReadOnly() → boolean

현재 읽기 전용 상태를 반환합니다.

if (editor.isReadOnly()) {
  console.log('읽기 전용 모드입니다');
}
isModified() → boolean

마지막 setHTML() 또는 resetModified() 호출 이후 내용이 변경되었는지 반환합니다.

window.addEventListener('beforeunload', (e) => {
  if (editor.isModified()) {
    e.preventDefault();
  }
});
resetModified() → this

변경 플래그를 초기화합니다.

editor.resetModified();
setHeight(height) → this

에디터 높이를 변경합니다. number(px), '500px', '50vh' 모두 허용됩니다.

editor.setHeight(600);
editor.setHeight('80vh');
getHeight() → number

현재 에디터 높이를 픽셀 단위로 반환합니다.

const h = editor.getHeight(); // 600
setWidth(width) → this

에디터 너비를 변경합니다.

editor.setWidth('100%');
editor.setWidth(800);
getWidth() → number

현재 에디터 너비를 픽셀 단위로 반환합니다.

setTab(index) → this

에디터 탭을 전환합니다.

index
0편집 (WYSIWYG)
1HTML 소스
2미리보기
editor.setTab(1);   // HTML 소스 보기
editor.setTab(0);   // 편집 모드로 돌아가기
getActiveTab() → number

현재 활성 탭 인덱스를 반환합니다. (0 / 1 / 2)

showToolbar(visible) → this

툴바 표시 여부를 제어합니다.

editor.showToolbar(false);  // 툴바 숨김
editor.showToolbar(true);   // 툴바 표시

3-3. 이미지

insertImage(url) → this

URL로 이미지를 현재 커서 위치에 삽입합니다. base64 data URL도 지원합니다.

editor.insertImage('https://example.com/photo.jpg');
editor.insertImage('data:image/png;base64,...');
getImages() → string[]

본문 내 모든 이미지 URL 목록을 배열로 반환합니다.

const urls = editor.getImages();
// ['https://...', 'data:image/png;...']
이미지 캡션 v2.3+

이미지 클릭 → 액션바에서 "캡션 추가" 토글. <figure> + <figcaption contenteditable> 구조로 감쌉니다.

<figure class="we-img-figure">
  <img src="...">
  <figcaption contenteditable="true">캡션 텍스트</figcaption>
</figure>
이미지 하이퍼링크 v2.3.2+

이미지 클릭 → 액션바에서 "링크 추가/편집" → URL + 열기 방식 선택.

열기 방식 (4종):

  • _self — 현재 창 (기본, target 미부착)
  • _blank — 새 창 (rel="noopener noreferrer" 자동 부여)
  • _top — 최상위 창 (iframe 탈출)
  • _parent — 부모 창

DOM 구조:

// 캡션 없이
<a href="https://example.com" target="_blank" rel="noopener noreferrer" data-we-img-link="1">
  <img src="...">
</a>

// 캡션 있을 때 — <a>는 <img>만 감쌈
<figure class="we-img-figure">
  <a href="..." data-we-img-link="1"><img src="..."></a>
  <figcaption contenteditable="true">캡션</figcaption>
</figure>

보안 가드:

  • _blankrel="noopener noreferrer" 자동 부여 (탭내핑·Referer 누출 방지)
  • javascript:, vbscript:, 비-이미지 data: URL 자동 차단
  • 화이트리스트 외 target 값은 _self로 강제
  • 편집 영역에서 링크 이미지 클릭 시 네비게이션 차단 (저장 HTML은 정상)

3-4. 콘텐츠 정제

clearFormat() → this

선택 영역의 모든 서식(태그·인라인 스타일)을 제거합니다.

editor.clearFormat();
clearCSSFormat() → this

선택 영역의 인라인 CSS 스타일만 제거합니다. <strong>, <em> 등 태그는 유지합니다.

editor.clearCSSFormat();
removeTag(tagName) → this

선택 영역 내 특정 태그를 제거하고 내용은 유지합니다.

editor.removeTag('span');    // <span style="...">텍스트</span> → 텍스트
editor.removeTag('strong');

3-5. 런타임 설정

setDefaultFont(fontName) → this

에디터 기본 글꼴을 런타임에 변경합니다.

editor.setDefaultFont('Arial');
editor.setDefaultFont('Noto Sans KR');
setDefaultFontSize(size) → this

에디터 기본 글자 크기를 런타임에 변경합니다 (단위: pt).

editor.setDefaultFontSize(16);
setContentCSS(css) → this

편집 영역에 CSS 규칙을 추가합니다.

editor.setContentCSS(`
  p { line-height: 1.8; }
  img { max-width: 100%; }
`);

3-6. 생명주기

focus() → void

에디터에 포커스를 줍니다.

editor.focus();
destroy() → void

에디터 인스턴스를 완전히 제거합니다. DOM 복원 + 이벤트 해제가 수행됩니다.

editor.destroy();

3-7. Word 가져오기

Word 가져오기 플러그인(wordimport-native.min.js)이 로드된 경우에만 사용할 수 있습니다.
importWordFile(file) → Promise<void>

.docx 파일 객체를 에디터로 가져옵니다.

// 파일 input에서
document.getElementById('fileInput').addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (file) await editor.importWordFile(file);
});

// 드롭 이벤트에서
dropZone.addEventListener('drop', async (e) => {
  e.preventDefault();
  const file = e.dataTransfer.files[0];
  await editor.importWordFile(file);
});

3-8. v2.4.0 신규 메서드

isEmpty() → boolean

본문이 비어 있는지 반환. 공백·&nbsp;·빈 단락(<p><br></p>)은 빈 것으로 간주하며, 이미지·표·미디어가 있으면 false.

getSafeHTML(options?) → string

XSS 위험 요소(script/on*/javascript: 등)를 제거한 HTML 반환. getHTML()과 별개이며 저장·전송용. allowedTags·allowedAttrs·imageRewriter 옵션 지원.

const safe = editor.getSafeHTML({ allowedTags: ['p','b','a'], allowedAttrs: ['href'] });
자동 저장 API v2.4+

autoSave 옵션 활성화 시 동작. getAutoSaved() → string|null (복구용), clearAutoSaved(), saveNow()(즉시 저장).

const editor = new DeeEditor('#editor', {
  autoSave: { interval: 30000, unloadWarning: true }
});
const saved = editor.getAutoSaved();
if (saved) { editor.setHTML(saved); editor.clearAutoSaved(); }
saveToPDF(options?) / saveToPDFInWindow(options?) → void

브라우저 인쇄 대화상자로 본문만 출력(PDF 저장 유도). { title, hideToolbar, css }. saveToPDFInWindow는 새 창에 본문만 출력 후 인쇄(고품질).

setZoom(ratio) / getZoom() → this / number

편집 영역 확대/축소 (0.25 ~ 4.0, 범위 밖 RangeError). zoom 이벤트 발화. Chrome zoom · 기타 transform: scale() 폴백.

validate() / maskSensitiveData() → Promise

privacy/profanity 옵션 기반 검사·마스킹. validate(){ profanity, privacy }, maskSensitiveData() → 마스킹 개수.

const r = await editor.validate();   // { profanity:[...], privacy:[...] }
await editor.maskSensitiveData();      // 감지 항목 마스킹
UI 제어 메서드 showMenuBar(bool)·enableMenuBar(config?)·disableMenuBar()·showStatusBar(bool)·setFullscreen(bool)·isFullscreen()·setUI(state)·getUI()도 제공됩니다(v2.4).

4. 이벤트 시스템

editor.on(eventName, handler)
이벤트핸들러 파라미터발화 시점
change(html: string)본문 변경 시
input(e)입력 즉시
ready(editor)초기화 완료 시
focus()에디터 포커스 획득 시
blur()에디터 포커스 해제 시
tabChange(index: 0|1|2)탭 전환 시
selectionchange()선택 영역 변경 시
resize({ width, height })에디터 크기 변경 시
undo / redo()실행 취소 / 다시 실행 시
paste(e)붙여넣기 시
── v2.4.0 신규 이벤트 ──
imageInserted({ src, file, node })이미지가 본문에 삽입(DOM 부착)된 시점
imageUploaded({ src, file, node })uploadHandler가 base64→서버 URL 교체 완료 시
zoom({ ratio })setZoom으로 확대/축소 시
error({ context, error })내부 오류(업로드 실패·플러그인 init 실패 등) 발생 시
// 변경 감지
editor.on('change', (html) => {
  console.log('변경됨:', html.length, '자');
});

// 탭 변경 감지
editor.on('tabChange', (index) => {
  const names = ['편집', 'HTML', '미리보기'];
  console.log('탭 전환:', names[index]);
});

// 포커스 관리
editor.on('focus', () => { document.title = '편집 중...'; });
editor.on('blur',  () => { document.title = '완료'; });

5. 정적(전역) API

전역 클래스 WebEditor 또는 DeeEditor에서 직접 호출합니다.

5-1. 라이선스

// 라이선스 키 저장 + 검증 (모든 인스턴스에 적용)
WebEditor.setLicense('WED-XXXX-XXXX-...');

// 현재 라이선스 정보 조회
const info = await WebEditor.getLicenseInfo();
// {
//   valid: true,
//   domains: ['example.com', '*.example.com'],
//   expiry: 1830297599,            // UNIX timestamp (0 = 영구)
//   type: 'perm'|'year'|'trial30'|'trial180'|'custom',
//   customer: 'ABC Corp'
// }

5-2. 플러그인 등록

// 전역 플러그인 등록 (이후 생성되는 모든 인스턴스에 적용)
WebEditor.registerPlugin('wordImport', WordImportPlugin);
WebEditor.registerPlugin('myPlugin',   MyCustomPlugin);

5-3. 인스턴스 접근

// ID로 인스턴스의 API 모델 획득
const model = WebEditor.getAPIModelById('editor-id');

// 인덱스로 획득 (생성 순서)
const model = WebEditor.getAPIModelByIndex(0);

// 모든 인스턴스 목록
const models = WebEditor.getAllAPIModels();

// 버전 확인
console.log(WebEditor.version);  // '2.4.0'

5-4. 노출 클래스

WebEditor.LicenseValidator     // 라이선스 검증 클래스
WebEditor.LicenseManager       // 라이선스 관리 클래스
WebEditor.DomainMatcher        // 도메인 매칭 유틸
WebEditor.EvaluationModal      // 평가판 안내 모달
WebEditor.LicenseI18n          // 다국어 문자열

6. 라이선스 API

6-1. 초기화 시 라이선스 적용

new DeeEditor('#editor', {
  license: 'WED-XXXX-XXXX-XXXX-XXXX',
  licenseLang: 'ko',

  onLicenseValid: (info) => {
    console.log('라이선스 유효:', info.customer, info.expiry);
  },

  onLicenseInvalid: (state) => {
    // state.reason: 'unlicensed' | 'invalid' | 'expired' | 'mismatch'
    console.warn('라이선스 오류:', state.reason);
  }
});

6-2. 전역 설정 (페이지 레벨)

스크립트 로드 직후, 에디터 초기화 전에 설정하면 이후 생성되는 모든 에디터 인스턴스에 자동 적용됩니다.
WebEditor.setLicense('WED-XXXX-XXXX-...');

const editor1 = new DeeEditor('#ed1');
const editor2 = new DeeEditor('#ed2');

6-3. 키 포맷

WED-{CUSTOMER}-{DOMAIN_HASH}-{EXPIRY}-{SIGNATURE}
  • HMAC-SHA256 서명 기반
  • 마스터 시크릿은 tools/secret.json에 보관 (.gitignore)
  • 키 발급: tools/license-generator.html (사내 전용)

6-4. 도메인 매칭 규칙

패턴매칭 예시
example.comexample.com 정확 일치
*.example.comsub.example.com (1단계)
**.example.coma.b.example.com (다단계)
자동 허용 (개발 환경): localhost, 사설 IP (10.x.x.x, 172.16-31.x.x, 192.168.x.x), *.local, file:// 프로토콜

7. DOM API (WebEditorAPIModel)

7-1. 모델 획득

// 방법 1: 인스턴스에서
const model = editor.getAPIModel();

// 방법 2: ID로
const model = WebEditor.getAPIModelById('my-editor');

// 방법 3: 인덱스로
const model = WebEditor.getAPIModelByIndex(0);

// 방법 4: 전체 목록
const models = WebEditor.getAllAPIModels();

7-2. 메서드 체이닝

대부분의 setter 메서드는 this를 반환하므로 체이닝이 가능합니다.

WebEditor.getAPIModelByIndex(0)
  .setReadOnly(false)
  .setHeight(600)
  .setDefaultFont('Noto Sans KR')
  .setHTML('<p>초기 내용</p>')
  .focus();

7-3. 전체 메서드 목록

// 콘텐츠 I/O
model.getHTML()               → string
model.setHTML(html)           → this
model.getText()               → string
model.appendHTML(html)        → this
model.insertHTML(html)        → this
model.getSelectedHTML()       → string
model.getSelectedText()       → string
model.clear()                 → this

// 상태 제어
model.setReadOnly(bool)       → this
model.isReadOnly()            → boolean
model.isModified()            → boolean
model.resetModified()         → this
model.setHeight(val)          → this
model.getHeight()             → number
model.setWidth(val)           → this
model.getWidth()              → number
model.setTab(index)           → this
model.getActiveTab()          → number
model.showToolbar(bool)       → this

// 이미지
model.insertImage(url)        → this
model.getImages()             → string[]

// 콘텐츠 정제
model.clearFormat()           → this
model.clearCSSFormat()        → this
model.removeTag(name)         → this

// 런타임 설정
model.setDefaultFont(name)    → this
model.setDefaultFontSize(pt)  → this
model.setContentCSS(css)      → this

// 이벤트
model.on(event, handler)      → this

// 생명주기
model.focus()                 → void
model.destroy()              → void

8. 플러그인 시스템

8-1. 플러그인 인터페이스

class MyPlugin {
  constructor(editor) {
    this.editor = editor;
  }

  /** 에디터 초기화 완료 후 호출. 툴바 버튼 등록, 이벤트 바인딩 등 수행. */
  init() {}

  /** 툴바/메뉴 명령 처리 */
  execute(command, value) {}

  /** 툴바 버튼 활성화 상태 반환 → { [command]: boolean } */
  queryState() { return {}; }

  /** 에디터 제거 시 호출. 이벤트 리스너 해제 등. */
  destroy() {}
}

8-2. 전역 등록 vs 인스턴스 등록

// 방법 1: 전역 등록 (모든 인스턴스에 적용)
WebEditor.registerPlugin('myPlugin', MyPlugin);

// 방법 2: 인스턴스별 등록
new DeeEditor('#editor', {
  plugins: { myPlugin: MyPlugin }
});

8-3. 플러그인에서 에디터 API 사용

class MyPlugin {
  init() {
    const html = this.editor.getHTML();
    this.editor.insertHTML('<mark>하이라이트</mark>');

    // 선택 저장/복원 (팝업 열기 전후)
    this.editor.saveSelection();
    // ... 팝업 표시 ...
    this.editor.restoreSelection();

    this.editor.on('change', (html) => {
      console.log('변경:', html);
    });
  }
}

9. MS Word 가져오기 API

dist/plugins/wordimport-native.js 로드가 필요합니다.

9-1. 기본 사용법

<script src="dist/webeditor.min.js"></script>
<script src="dist/plugins/wordimport-native.min.js"></script>
const editor = new DeeEditor('#editor');

document.getElementById('import-btn').addEventListener('click', () => {
  const input = document.createElement('input');
  input.type = 'file';
  input.accept = '.docx';
  input.onchange = async (e) => {
    const file = e.target.files[0];
    if (file) await editor.importWordFile(file);
  };
  input.click();
});

9-2. 드래그앤드롭

const dropZone = document.getElementById('drop-zone');

dropZone.addEventListener('dragover', (e) => {
  e.preventDefault();
  dropZone.classList.add('drag-over');
});

dropZone.addEventListener('drop', async (e) => {
  e.preventDefault();
  dropZone.classList.remove('drag-over');
  const file = e.dataTransfer.files[0];
  if (file && file.name.endsWith('.docx')) {
    await editor.importWordFile(file);
  }
});

9-3. EngineRegistry (고급)

const engines = EngineRegistry.all();
const engine  = EngineRegistry.best();
const native  = EngineRegistry.byName('native');
EngineRegistry.register('custom', MyEngine, { priority: 30 });

9-4. 지원 변환 항목

OOXMLHTML비고
<w:p><p>기본 단락
<w:p> + 제목 스타일<h1>~<h6>
<w:b><strong>
<w:i><em>
<w:u><u>
<w:color>color: #RRGGBB
<w:highlight>background-color
<w:tbl> + <w:tblGrid><table> + <colgroup>열 너비 정확 반영
<w:shd>background-color셀 배경색
<w:tcBorders>border-color셀 테두리색
<a:blip><img src="data:...">base64 인라인
<wp:extent>width / height표시 크기 (EMU → px)
<w:hyperlink><a href="...">

11. TypeScript 타입 정의

dist/webeditor.d.ts에서 전체 타입을 참조할 수 있습니다.

interface DeeEditorOptions {
  height?: string | number;
  placeholder?: string;
  defaultFont?: string;
  defaultFontSize?: number;
  contentCSS?: string;
  readOnly?: boolean;
  autofocus?: boolean;
  enterKey?: 'p' | 'br';
  maxImageWidth?: number;
  pastePlainText?: boolean;
  showTableGuide?: boolean;
  logoUrl?: string;
  license?: string;
  licenseLang?: 'ko' | 'en' | null;
  uploadHandler?: (file: File, headers?: Record<string, string>) => Promise<string>;
  plugins?: string[] | Record<string, PluginClass>;
  menubar?: boolean | MenuBarGroup[];
  onInit?: (editor: DeeEditor) => void;
  onKeyDown?: (e: KeyboardEvent) => boolean | void;
  onKeyUp?: (e: KeyboardEvent) => void;
  onBeforeCommand?: (cmd: string) => boolean | void;
  onTabChange?: (index: number) => boolean | void;
  onMenuCommand?: (cmd: string, editor: DeeEditor, item: any) => boolean | void;
  onLicenseValid?: (info: LicenseInfo) => void;
  onLicenseInvalid?: (state: LicenseState) => void;
  // v2.4.0
  autoSave?: { interval?: number; storageKey?: string; unloadWarning?: boolean; unloadMessage?: string; onSave?: (html: string) => boolean | void } | null;
  inlineToolbar?: boolean | string[];
  hyperLinkDefaultTarget?: '_self' | '_blank' | '_top' | '_parent';
  privacy?: { detect?: string[]; onDetect?: (m: any[]) => void } | null;
  profanity?: { words?: string[] | string; onDetect?: (m: any[]) => void; maskChar?: string } | null;
  uploadHeaders?: Record<string, string> | null;
  csrfCookie?: string | null;
  evaluationWatermark?: boolean;
}

interface LicenseInfo {
  valid: boolean;
  domains: string[];
  expiry: number;  // UNIX timestamp (0 = 영구)
  type: 'perm' | 'year' | 'trial30' | 'trial180' | 'custom';
  customer: string | null;
}

declare class DeeEditor {
  static version: string;
  static setLicense(key: string): void;
  static getLicenseInfo(): Promise<LicenseInfo>;
  static registerPlugin(name: string, plugin: PluginClass): void;
  static getAPIModelById(id: string): DeeEditor | null;
  static getAPIModelByIndex(index: number): DeeEditor | null;
  static getAllAPIModels(): DeeEditor[];

  constructor(selector: string | Element, options?: DeeEditorOptions);

  getHTML(): string;
  setHTML(html: string): this;
  getText(): string;
  appendHTML(html: string): this;
  insertHTML(html: string): this;
  getSelectedHTML(): string;
  getSelectedText(): string;
  clear(): this;
  setReadOnly(readonly: boolean): this;
  isReadOnly(): boolean;
  isModified(): boolean;
  resetModified(): this;
  setHeight(height: number | string): this;
  getHeight(): number;
  setWidth(width: number | string): this;
  getWidth(): number;
  setTab(index: 0 | 1 | 2): this;
  getActiveTab(): 0 | 1 | 2;
  showToolbar(visible: boolean): this;
  insertImage(url: string): this;
  getImages(): string[];
  clearFormat(): this;
  clearCSSFormat(): this;
  removeTag(tagName: string): this;
  setDefaultFont(name: string): this;
  setDefaultFontSize(size: number): this;
  setContentCSS(css: string): this;
  importWordFile(file: File): Promise<void>;
  // v2.4.0 신규
  isEmpty(): boolean;
  getSafeHTML(options?: { stripScript?: boolean; allowedTags?: string[]; allowedAttrs?: string[]; imageRewriter?: (src: string) => string }): string;
  getAutoSaved(): string | null;
  clearAutoSaved(): this;
  saveNow(): this;
  saveToPDF(options?: { title?: string; hideToolbar?: boolean; css?: string }): void;
  saveToPDFInWindow(options?: { title?: string; css?: string }): void;
  setZoom(ratio: number): this;
  getZoom(): number;
  validate(): Promise<{ profanity: any[]; privacy: any[] }>;
  maskSensitiveData(): Promise<number>;
  showMenuBar(bool: boolean): void;
  setFullscreen(bool: boolean): void;
  on(event: 'change', handler: (html: string) => void): this;
  on(event: 'tabChange', handler: (index: number) => void): this;
  on(event: 'focus' | 'blur' | 'selectionchange', handler: () => void): this;
  on(event: 'imageInserted' | 'imageUploaded', handler: (d: { src: string; file: File | null; node: HTMLImageElement }) => void): this;
  on(event: 'zoom', handler: (d: { ratio: number }) => void): this;
  on(event: 'error', handler: (d: { context: string; error: Error }) => void): this;
  off(event?: string, handler?: Function): this;
  focus(): void;
  destroy(): void;
}

12. 콜백 옵션 레퍼런스

onInit(editor)

에디터 초기화 완료 후 1회 호출됩니다.

onInit: (editor) => {
  editor.setHTML(savedContent);
  editor.resetModified();
}

onKeyDown(e) → boolean | void

키다운 이벤트 발생 시 호출. return false 시 기본 동작을 차단합니다.

onKeyDown: (e) => {
  if (e.key === 'Tab') {
    return false;  // Tab 키 차단
  }
}

onBeforeCommand(command) → boolean | void

에디터 내부 커맨드 실행 직전 호출. return false 시 커맨드를 차단합니다.

onBeforeCommand: (cmd) => {
  if (cmd === 'insertTable') {
    return false;  // 표 삽입 차단
  }
}

onTabChange(index) → boolean | void

탭 전환 직전 호출. return false 시 전환을 취소합니다.

onTabChange: (index) => {
  if (index === 1 && !userIsAdmin) {
    alert('HTML 편집 권한이 없습니다.');
    return false;
  }
}

uploadHandler(file) → Promise<string>

이미지 서버 업로드 처리. string URL을 반환해야 합니다.

uploadHandler: async (file) => {
  if (file.size > 10 * 1024 * 1024) {
    throw new Error('파일 크기는 10MB 이하여야 합니다.');
  }
  const form = new FormData();
  form.append('upload', file);
  const res = await fetch('/api/file/upload', {
    method: 'POST',
    headers: { 'X-CSRF-Token': getCSRFToken() },
    body: form
  });
  if (!res.ok) throw new Error('업로드 실패');
  const { url } = await res.json();
  return url;
}

13. 크로스브라우저 이슈 및 해결책

이슈원인해결책
Enter 줄바꿈 태그 불일치브라우저마다 div/p/br 상이execCommand('defaultParagraphSeparator', false, 'p') 초기화
툴바 클릭 시 선택 해제툴바 클릭이 contenteditable 포커스 뺏음툴바에 mousedown → e.preventDefault()
fontSize execCommand브라우저별 결과 불일치<span style="font-size:Xpt"> 직접 래핑
배경색 명령Chrome: hiliteColor, Firefox: backColortry/catch 분기
드롭 시 커서 위치Chrome/Safari vs Firefox API 차이caretRangeFromPoint / caretPositionFromPoint 정규화
IME 한국어 입력조합 중 selectionchange 오발화compositionstart/end 플래그로 가드
Safari 표 삽입 후 커서insertNode 후 커서 위치 초기화첫 번째 td에 명시적 커서 설정
Firefox drag-drop 네비게이션기본 동작으로 페이지 이동dragovere.stopPropagation() 추가

14. 전체 초기화 예제

게시판 글쓰기 페이지

<!DOCTYPE html>
<html lang="ko">
<head>
  <link rel="stylesheet" href="/dist/webeditor.min.css">
</head>
<body>
  <form id="post-form">
    <input type="text" name="title" placeholder="제목">
    <div id="editor"></div>
    <input type="hidden" name="content" id="content-field">
    <button type="submit">등록</button>
  </form>

  <script src="/dist/webeditor.min.js"></script>
  <script src="/dist/plugins/wordimport-native.min.js"></script>
  <script>
    const editor = new DeeEditor('#editor', {
      height: '500px',
      defaultFont: 'Malgun Gothic',
      defaultFontSize: 14,
      showTableGuide: true,
      license: 'WED-XXXX-XXXX-...',

      uploadHandler: async (file) => {
        const form = new FormData();
        form.append('file', file);
        const res = await fetch('/api/upload', { method: 'POST', body: form });
        return (await res.json()).url;
      },

      onInit: (ed) => {
        const existing = document.getElementById('existing-content');
        if (existing) { ed.setHTML(existing.innerHTML); ed.resetModified(); }
      }
    });

    document.getElementById('post-form').addEventListener('submit', (e) => {
      document.getElementById('content-field').value = editor.getHTML();
    });

    window.addEventListener('beforeunload', (e) => {
      if (editor.isModified()) e.preventDefault();
    });
  </script>
</body>
</html>

다중 인스턴스

// 라이선스는 전역으로 1회만 설정
WebEditor.setLicense('WED-XXXX-XXXX-...');

const editor1 = new DeeEditor('#editor-1', { height: '300px' });
const editor2 = new DeeEditor('#editor-2', { height: '300px', readOnly: true });

// 첫 번째 에디터 내용을 두 번째로 복사
const html = WebEditor.getAPIModelByIndex(0).getHTML();
WebEditor.getAPIModelByIndex(1).setHTML(html);

부록 — 단축키 목록

단축키기능
Ctrl+B굵게
Ctrl+I이탤릭
Ctrl+U밑줄
Ctrl+K하이퍼링크 삽입
Ctrl+Z실행 취소
Ctrl+Y다시 실행
Ctrl+Shift+Z다시 실행 (Mac 스타일)
Ctrl+A전체 선택
Ctrl+C복사
Ctrl+X잘라내기
Ctrl+V붙여넣기
Ctrl+Shift+V텍스트로 붙여넣기
Tab (표 안)다음 셀 이동
Del (셀 선택)셀 내용 삭제

부록 — 파일 구조

dist/
├── webeditor.js              UMD 개발용 번들
├── webeditor.min.js          UMD 운영용 번들 ⭐
├── webeditor.esm.js          ES Module 번들
├── webeditor.css             스타일시트
├── webeditor.min.css         스타일시트 (최소화) ⭐
├── webeditor.d.ts            TypeScript 타입 정의
└── plugins/
    ├── wordimport-native.js       Word 가져오기 플러그인
    └── wordimport-native.min.js   최소화 버전 ⭐

src/
├── editor.js                 에디터 코어 + DOM 생성
├── editor.css                전체 스타일
├── toolbar.js                툴바 렌더링
├── menubar.js                드롭다운 메뉴바
├── license/                  라이선스 시스템
│   ├── domain-matcher.js
│   ├── validator.js
│   ├── i18n.js
│   ├── evaluation-modal.js
│   └── manager.js
└── plugins/
    ├── format.js             Bold/Italic/Underline/색상/크기
    ├── image.js              이미지 삽입/업로드
    ├── table.js              표 생성/편집/리사이즈/멀티셀
    ├── link.js               하이퍼링크
    ├── video.js              YouTube Lite Embed
    └── wordimport/
        ├── index.js          WordImportPlugin 진입점
        ├── core/
        │   └── DocxReader.js ZIP 파싱
        ├── engines/
        │   ├── EngineRegistry.js
        │   └── NativeEngine.js  OOXML → HTML
        └── ui/
            ├── ImportDialog.js
            └── ProgressDialog.js

© 2024-2026 DEXTSOLUTION Inc. All Rights Reserved.