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 WebEditor('#editor', {
  height: '400px'
});
</script>

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

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

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

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

2. 초기화 옵션

new WebEditor(selector, options)
옵션 타입 기본값 설명
height string | number '400px' 에디터 높이
placeholder string '' 빈 상태 안내 텍스트
defaultFont string 'Malgun Gothic' 기본 글꼴
defaultFontSize number 14 기본 글자 크기 (pt)
contentCSS string '' 편집 영역에 적용할 추가 CSS
readOnly boolean false 읽기 전용 모드
autofocus boolean false 초기화 직후 자동 포커스
enterKey 'p' | 'br' 'p' Enter 키 줄바꿈 태그
maxImageWidth number 0 삽입 이미지 최대 너비 (px, 0=무제한)
pastePlainText boolean false 붙여넣기 시 서식 제거
showTableGuide boolean false 테두리 없는 표를 점선으로 표시
logoUrl string '' About 다이얼로그에 표시할 로고 이미지 URL
license string '' 라이선스 키 (WED-XXXX-...)
licenseLang 'ko' | 'en' | null null 라이선스 모달 언어 (null=자동)
uploadHandler (file, headers?) ⇒ Promise<string> null 이미지 서버 업로드 핸들러 (2번째 인자 headers는 v2.4)
plugins string[] | Object 전체 활성 플러그인 목록. Word 가져오기는 'wordimport' 포함 필요
menubar boolean | Array false 상단 메뉴바. true=기본 7그룹, 배열=커스텀 구성
onInit Function null 초기화 완료 콜백
onKeyDown Function null 키다운 이벤트 콜백 (return false → 차단)
onKeyUp Function null 키업 이벤트 콜백
onBeforeCommand Function null 커맨드 실행 전 콜백 (return false → 차단)
onTabChange Function null 탭 변경 콜백 (return false → 차단)
onMenuCommand Function null 메뉴 명령 실행 전 콜백 (v2.4, return false → 차단)
onLicenseValid Function null 정식 라이선스 검증 성공 콜백
onLicenseInvalid Function null 라이선스 검증 실패 콜백
── v2.4.0 신규 옵션 ──
autoSave Object | null null 자동 임시 저장 { interval, storageKey, unloadWarning, unloadMessage, onSave }
inlineToolbar boolean | string[] false 텍스트 선택 시 부유 미니툴바
hyperLinkDefaultTarget '_self'|'_blank'|'_top'|'_parent' '_self' 새 하이퍼링크 기본 target
privacy Object | null null 개인정보 검출 { detect:[...], onDetect }
profanity Object | null null 금칙어 검출 { words, onDetect, maskChar }
uploadHeaders Object | null null 업로드 요청 커스텀 헤더
csrfCookie string | null null 쿠키값을 X-CSRF-Token 헤더로 자동 주입
evaluationWatermark boolean true 평가판 모드 시 본문 배경 워터마크 표시

2-1. uploadHandler 사용 예

uploadHandler를 제공하지 않으면 이미지를 base64 인라인으로 저장합니다. 서버 저장이 필요한 환경에서는 반드시 설정하세요.
new WebEditor('#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)
1 HTML 소스
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 WebEditor('#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 제어 메서드 v2.4+

메뉴바·상태바·전체화면 등 에디터 UI를 런타임에 제어합니다.

  • showMenuBar(bool) · enableMenuBar(config?) · disableMenuBar() — 상단 메뉴바 표시·활성화·제거
  • showStatusBar(bool) — 하단 상태바 표시/숨김
  • setFullscreen(bool) · isFullscreen() — 전체 화면 전환·상태 조회
  • setUI(state) · getUI() — UI 상태 일괄 적용·조회

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 또는 WebEditor에서 직접 호출합니다.

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 WebEditor('#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 WebEditor('#ed1');
const editor2 = new WebEditor('#ed2');

6-3. 키 포맷

WED-{CUSTOMER}-{DOMAIN_HASH}-{EXPIRY}-{SIGNATURE}
  • HMAC-SHA256 서명 기반

6-4. 도메인 매칭 규칙

패턴 매칭 예시
example.com example.com 정확 일치
*.example.com sub.example.com (1단계)
**.example.com a.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 WebEditor('#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 WebEditor('#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. 지원 변환 항목

OOXML HTML 비고
<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 WebEditorOptions {
  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: WebEditor) => void;
  onKeyDown?: (e: KeyboardEvent) => boolean | void;
  onKeyUp?: (e: KeyboardEvent) => void;
  onBeforeCommand?: (cmd: string) => boolean | void;
  onTabChange?: (index: number) => boolean | void;
  onMenuCommand?: (cmd: string, editor: WebEditor, 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 WebEditor {
  static version: string;
  static setLicense(key: string): void;
  static getLicenseInfo(): Promise<LicenseInfo>;
  static registerPlugin(name: string, plugin: PluginClass): void;
  static getAPIModelById(id: string): WebEditor | null;
  static getAPIModelByIndex(index: number): WebEditor | null;
  static getAllAPIModels(): WebEditor[];

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

  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: backColor try/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 WebEditor('#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 WebEditor('#editor-1', { height: '300px' });
const editor2 = new WebEditor('#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.