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

Dee Editor 매뉴얼

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

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

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>

<!-- 한글 HWPX 가져오기 기능이 필요한 경우 (선택) -->
<script src="dist/plugins/hwpximport-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)
옵션타입기본값설명
heightstring | number'400px'에디터 높이
placeholderstring''빈 상태 안내 텍스트
defaultFontstring'Malgun Gothic'기본 글꼴 (툴바 드롭다운 반영)
defaultFontSizenumber14기본 글자 크기 (pt, 툴바 드롭다운 반영)
defaultLineHeightstring | number'1'기본 줄간격 배수 (툴바 드롭다운 반영)
undoGranularity'char' | 'group''char'실행취소 단위: 한 글자(한글은 완성 음절) / 묶음
documentBackgroundobject{mode:'auto',inlineThreshold:10240,store:'auto'}문서 배경 이미지 처리·보존 전략 (소형<10KB=base64, 대형=업로드)
uiTemplate'classic' | 'ribbon''classic'메뉴 UI 템플릿: 클래식(메뉴바+툴바) / 리본(탭+큰 아이콘+폰트 퀵바)
iconThemestring'default'아이콘 테마 (default·gray·blue·colorful·vivid·natural·emoji 또는 폴더명)
iconThemePathstring'assets/icon-themes'아이콘 테마 폴더 베이스 경로
tableBorderColorstring'#000000'표 셀 기본 테두리 색. 런타임 변경 setTableBorderColor()
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=자동)
trustedHostsstring[][]폐쇄망 신뢰 호스트(와일드카드 지원). ⚠️ v2.5.0.0: 키가 제공된 경우에만 동작 — 키 없으면 평가판
offlineModebooleanfalseWeb Crypto 불가(HTTP) 환경에서 ECDSA 검증 생략. ⚠️ v2.5.0.0: 키가 제공된 경우에만 동작
uploadHandler(file, headers?) ⇒ Promise<string>null이미지 서버 업로드 핸들러 (2번째 인자 headers는 v2.4)
pluginsstring[] | Object전체활성 플러그인 목록. Word 가져오기는 'wordimport' 포함 필요
toolbarArray | nullnull커스텀 툴바 레이아웃 — 행(단)별 명령 배열 [[…],[…]], '|'=구분선 (2-2 참고)
menubarboolean | Arrayfalse상단 메뉴바. true=기본 8그룹(파일~도움말), 배열=커스텀 구성 (10-3 참고)
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 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 반환
  }
});

2-2. UI 설정(config) — 설정 페이지로 만들기·적용

툴바·메뉴바·너비 등 UI 구성을 코드로 직접 쓰는 대신, 설정 페이지에서 시각적으로 구성해 config 파일로 저장할 수 있습니다. 설정 허브: demo/settings.html

설정 페이지구성 대상산출 config
ui-config.html툴바 아이콘 표시·순서·1·2·3단 배치·그룹 구분선 + 메뉴바 on/off·상태표시줄·에디터 너비webeditor-ui-config.jswindow.DEEDITOR_UI_CONFIG
ui-config-apply.html저장한 UI config를 읽어 에디터를 구성하는 적용 예시
menubar-config.html드롭다운 메뉴바의 메뉴·항목·순서·구분선webeditor-menubar-config.jswindow.DEEDITOR_MENUBAR_CONFIG
menu-ui.html클래식 ↔ 리본 메뉴 UI 템플릿 비교·전환 (uiTemplate)

UI config 형식 — 초기화 옵션과 동일한 구조입니다.

window.DEEDITOR_UI_CONFIG = {
  menubar: true,          // 메뉴바 표시 여부
  statusbar: true,        // 상태표시줄
  width: "100%",          // "100%" 또는 숫자(px)
  toolbar: [               // 행(단)별 명령 배열, '|' = 그룹 구분선
    ["newDocument", "save", "|", "undo", "redo" /* …1단… */],
    ["paragraphStyle", "fontFamily", "|", "bold" /* …2단… */]
  ]
};

적용 — config 파일을 포함하고 옵션으로 전달합니다.

<script src="webeditor-ui-config.js"></script>
<script>
var cfg = window.DEEDITOR_UI_CONFIG || {};
var editor = new WebEditor('#editor', {
  menubar: cfg.menubar, toolbar: cfg.toolbar, plugins: [/* 전체 */]
});
if (cfg.statusbar === false) editor.showStatusBar(false);
if (cfg.width) editor.setWidth(cfg.width);
</script>
저장 버튼은 localStorage(deeEditor.uiConfig / deeEditor.menubarConfig)에도 기록합니다. 아이콘이 모두 나오려면 해당 플러그인 번들을 모두 로드하세요(docbg·wordimport·hwpximport 포함). 상세: 문서 UI설정_config_가이드.md

2-3. 문서 템플릿 · 표 레이아웃 (DeeDocTools) v2.6.0.0

공용 모듈 doc-tools.js를 로드하고 DeeDocTools.attach(editor) 한 줄이면 파일 ▸ 템플릿(기본 문서 양식)과 파일 ▸ 레이아웃(표 구조)이 메뉴에 추가됩니다. 코어와 무관한 opt-in 모듈이라 미로드 시 영향이 없습니다.

<script src="dist/webeditor.min.js"></script>
<script src="doc-tools.js"></script>

const editor = new WebEditor('#editor', { menubar: true });
DeeDocTools.attach(editor);   // 파일 ▸ 템플릿 / 레이아웃 메뉴 추가
템플릿은 templates/ 폴더에 HTML을 넣으면 자동 인식됩니다. 정적 호스팅(디렉터리 목록이 막힌 환경)에서는 make-templates-manifest.jstemplates/manifest.json 목록을 생성하세요. 상세: 문서 기본문서템플릿_툴바구성.md

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();
pasteFromClipboard() → Promise<boolean>

클립보드 내용을 현재 커서 위치에 붙여넣습니다(메뉴·리본·버튼 클릭용, 비동기 Clipboard API). Ctrl+V동일 파이프라인(plain·Word/외부 HTML·엑셀 표선택·표 셀·이미지)으로 처리합니다. 보안 컨텍스트(HTTPS·localhost)+사용자 제스처+권한이 필요하며, 미지원·거부 시 Ctrl+V 안내 후 false를 반환합니다. (메뉴/리본의 "붙여넣기" 아이콘이 이 메서드를 호출)

btn.addEventListener('click', () => editor.pasteFromClipboard());

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|name) → this

에디터 탭을 전환합니다. 숫자 인덱스 또는 이름('edit'/'html'/'preview', 대소문자·별칭 허용)을 받습니다.

indexname
0'edit'편집 (WYSIWYG)
1'html'HTML 소스
2'preview'미리보기
editor.setTab(1);          // HTML 소스 보기
editor.setTab('preview');  // 이름으로도 전환
getActiveTab(asName?) → number | string

현재 활성 탭을 반환합니다. 기본은 인덱스(0/1/2), getActiveTab(true)는 이름('edit'/'html'/'preview')을 반환합니다.

editor.getActiveTab();      // 0
editor.getActiveTab(true);  // 'edit'
showToolbar(visible) · showMenuBar(visible) · showStatusBar(visible) → this

툴바·메뉴바·상태바 표시 여부를 제어합니다. (메뉴바는 menubar 옵션 사용 시)

editor.showToolbar(false);   // 툴바 숨김
editor.showMenuBar(false);   // 메뉴바 숨김

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);
setDefaultLineHeight(lh) · setEditorDefaults(opts) · getEditorDefaults() → this / object

기본 줄간격 변경, 기본 글꼴·크기·줄간격 일괄 설정/조회. 변경 시 툴바 글꼴·크기·줄간격 드롭다운에 즉시 반영됩니다. (드롭다운은 커서 위치의 실제 값/기본값을 표시하고 선택 시 선택값을 표시)

editor.setDefaultLineHeight('1.6');
editor.setEditorDefaults({ font: 'Georgia', fontSize: 16, lineHeight: '2' });
editor.getEditorDefaults();  // { font:'Georgia', fontSize:16, lineHeight:'2' }
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 · HWPX 가져오기

각 가져오기 플러그인(wordimport-native.min.js / hwpximport-native.min.js)을 코어와 함께 로드하고 plugins'wordimport' / 'hwpximport'를 지정한 경우에만 사용할 수 있습니다(opt-in).
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);
});
importHwpxFile(file, options?) → Promise<void>

한글 .hwpx 파일을 에디터로 가져옵니다(plugins:['hwpximport'] opt-in). 표(셀 병합·테두리·단색/그라디언트 배경·너비/높이·세로정렬)·텍스트(정렬·줄간격·굵게/기울임/밑줄·색·크기)·이미지·벡터 도형(SVG)·심볼폰트(PUA)·목차 점선 리더·페이지 나눔까지 변환합니다(v2.6.0.0 보강, 상세 §9-A). 수식·머리글/꼬리글·각주 본문 등은 미지원이며 텍스트 보존 + 경고로 처리합니다. 구 바이너리 .hwp는 대상이 아닙니다.

옵션설명
mode'cursor'(기본)/'append'/'replace' 삽입 방식
silent다이얼로그 없이 즉시 변환·삽입
maxTableWidth최상위 표를 이 너비(px)로 스케일(좁으면 늘리고 넓으면 줄임). 0/미지정=원본. 중첩 표 미적용
showLog변환 경고 표시
const editor = new WebEditor('#editor', { plugins: ['format', 'table', 'hwpximport'] });
await editor.importHwpxFile(file, { mode: 'replace', maxTableWidth: 740 });

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

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() / reviewSensitiveData() → Promise

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

reviewSensitiveData() → 검사 결과 팝업 표시: 구분(종류)·내용 목록(페이지 8건, 이전/다음), 항목별/전체 체크박스 선택 → 선택 항목만 마스킹. 겹친 종류는 자동 정리. 툴바 🛡 버튼 / 상단 메뉴 도구 › 금칙어·개인정보 검사로도 실행.

const r = await editor.validate();   // { profanity:[...], privacy:[...] }
await editor.maskSensitiveData();      // 본문 전체 마스킹
await editor.reviewSensitiveData();    // 결과 팝업(선택 마스킹)
setDocumentBackground(opts) / removeDocumentBackground() / getDocumentBackground() → Promise / this / object

편집 영역에 배경 이미지/색/반복/덧붙임을 설정·제거합니다. 진입: 툴바 문서 배경 이미지 버튼 또는 상단 메뉴 삽입 › 문서 배경 이미지…. 옵션 documentBackground로 이미지 처리(mode: auto/base64/upload)와 보존(store: auto/wrapper/meta) 전략을 지정합니다 — 기본 auto소형(<10KB)=base64 임베드, 대형=업로드 URL. 미리보기 탭·인쇄 미리보기에도 배경이 반영됩니다.

await editor.setDocumentBackground({ image: file, color: '#fff7e6', repeat: 'no-repeat', attachment: 'scroll' });
editor.getDocumentBackground();   // { image, color, repeat, attachment, mode, store }
editor.removeDocumentBackground();

⚠️ 유의: store='meta'(업로드 모드 기본)는 getHTML()에 배경이 포함되지 않습니다. 게시판처럼 본문만 저장하는 경우 getDocumentBackground() 결과를 별도 저장하고 로드 시 setDocumentBackground()로 복원하세요. store='wrapper'(base64 기본)는 getHTML()/setHTML()로 자동 보존됩니다.

setUITemplate(name) / getUITemplate() v2.4.2 · → this / string

메뉴 UI 템플릿을 런타임 전환합니다. 'classic'(메뉴바+툴바) ↔ 'ribbon'(탭 리본 + 큰 라벨 아이콘 + 폰트 퀵바). 초기값은 옵션 uiTemplate로 지정합니다.

editor.setUITemplate('ribbon');
editor.getUITemplate();   // 'ribbon'
setIconTheme(name) / getIconTheme() v2.4.2 · → this / string

아이콘 테마를 런타임 전환합니다. 내장 default·gray·blue·colorful·vivid·natural·emoji 또는 iconThemePath 하위 폴더명. 클래식 툴바와 리본 탭/카드에 함께 적용됩니다.

editor.setIconTheme('vivid');
editor.getIconTheme();   // 'vivid'
toggleToolbar() / toggleMenuBar() v2.4.2 · → this

툴바·메뉴바 표시를 토글합니다. 단축키 Ctrl+[(툴바) / Ctrl+](메뉴바).

editor.toggleToolbar();   // 툴바 표시/숨김 토글 (Ctrl+[)
editor.toggleMenuBar();   // 메뉴바 표시/숨김 토글 (Ctrl+])
setTableBorderColor(color) / getTableBorderColor() v2.4.2 · → this / string

이후 생성·삽입되는 표 셀의 기본 테두리 색과 테두리 색상 팔레트 기본값을 설정/조회합니다. 미설정 시 #000000. 옵션 tableBorderColor로 초기값 지정 가능.

editor.setTableBorderColor('#888888');
editor.getTableBorderColor();   // '#888888'
UI 제어 메서드 showMenuBar(bool)·enableMenuBar(config?)·disableMenuBar()·showStatusBar(bool)·setFullscreen(bool)·isFullscreen()·setUI(state)·getUI()도 제공됩니다(v2.4).
🖨 인쇄·PDF (v2.5.0.0 동작 개선): 인쇄 명령은 별도 미리보기 창 없이 화면 밖 숨김 iframe으로 인쇄 대화상자만 띄웁니다. saveToPDF()는 취소 후 미리보기가 다시 뜨지 않도록 afterprint/matchMedia('print') 기준으로 정리하며, 리본 모드에서도 본문만 출력됩니다.

4. 이벤트 시스템

editor.on(eventName, handler)
이벤트핸들러 파라미터발화 시점
change(html: string)본문 변경 시
focus()에디터 포커스 획득 시
blur()에디터 포커스 해제 시
tabChange(index: 0|1|2)편집/HTML/미리보기 탭 전환 시(sourceview)
selectionchange()선택 영역 변경 시
resize({ width, height })에디터 크기 변경 시
── v2.4.0 신규 이벤트 ──
imageInserted({ src, file, node })이미지가 본문에 삽입(DOM 부착)된 시점
imageUploaded({ src, file, node })uploadHandler가 base64→서버 URL 교체 완료 시
zoom({ ratio })setZoom으로 확대/축소 시
── v2.4.2 UI 이벤트 ──
fullscreen(on: boolean)setFullscreen 전체화면 전환 시
uiTemplateChange(name)setUITemplate 클래식↔리본 전환 시
iconThemeChange(name)setIconTheme 아이콘 테마 변경 시
초기화 완료 시점은 on('ready') 대신 onInit(editor) 옵션 콜백으로 처리합니다. (ready·input·undo·redo·paste·error 는 이벤트로 발생하지 않습니다.)
// 변경 감지
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. 라이선스

라이선스는 초기화 옵션 license(코드 입력)로만 적용합니다. 상태는 onLicenseValid/onLicenseInvalid 콜백으로 확인합니다(§6 참조).

new WebEditor('#editor', {
  license: 'WED2-XXXX-XXXX-...',
  onLicenseValid: (info) => { console.log(info); }
});
// localStorage 저장 방식·전역 setLicense API·팝업 키 적용은 제공하지 않습니다.

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.6.0.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. 키 포맷

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 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. 지원 변환 항목

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="...">

9-A. 한글 HWPX 가져오기 API v2.6.0.0 보강

dist/plugins/hwpximport-native.min.js 로드 + plugins'hwpximport' 지정이 필요합니다(opt-in). 한글의 표준 XML 포맷인 .hwpx(OWPML)만 대상이며, 구 바이너리 .hwp는 지원하지 않습니다.

9-A-1. 기본 사용법

<script src="dist/webeditor.min.js"></script>
<script src="dist/plugins/hwpximport-native.min.js"></script>
const editor = new WebEditor('#editor', {
  plugins: ['format', 'table', 'link', 'hwpximport']
});

document.getElementById('import-btn').addEventListener('click', () => {
  const input = document.createElement('input');
  input.type = 'file';
  input.accept = '.hwpx';
  input.onchange = async (e) => {
    const file = e.target.files[0];
    if (file) await editor.importHwpxFile(file, { mode: 'replace', maxTableWidth: 740 });
  };
  input.click();
});

9-A-2. 드래그앤드롭

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

dropZone.addEventListener('drop', async (e) => {
  e.preventDefault();
  const file = e.dataTransfer.files[0];
  if (file && file.name.endsWith('.hwpx')) {
    await editor.importHwpxFile(file);
  }
});

9-A-3. importHwpxFile 옵션

옵션타입기본값설명
mode'cursor' | 'append' | 'replace''cursor'삽입 위치. 'replace'는 본문 전체 교체
maxTableWidthnumber표를 이 픽셀 너비까지 비율 확대(에디터 창 폭에 맞춤)
silentbooleanfalsetrue면 변환 다이얼로그 없이 즉시 삽입
showLogbooleanfalse변환 경고·통계 로그 표시
권장 maxTableWidth는 에디터 본문 폭에 맞춰 740~780px. 데모 페이지(demo/hwpximport.html)는 740px 기준으로 동작합니다.

9-A-4. 지원 변환 항목 (OWPML → HTML)

OWPMLHTML비고
<hp:p><p>단락 · 정렬(text-align) 반영
<hh:bold/><strong>charPr 존재 여부로 판정
<hh:italic/><em>
underline (type≠NONE)<u>
charPr textColor / heightcolor / font-sizeheight = pt×100
<hp:tbl> + cellSz<table> + 셀 너비/높이HWPUNIT → px(÷75)
cellSpan colSpan/rowSpancolspan / rowspan셀 병합
borderFill · winBrush@faceColorborder / background-color테두리·셀 음영(단색)
borderFill · hc:gradationbackground-image: linear/radial-gradient셀 그라디언트 배경 v2.6
paraPr · lineSpacing(PERCENT/FIXED)line-height단락 줄간격 반영(hp:switch/default 중첩 포함) v2.6
subList@vertAlignvertical-align셀 세로 정렬
<hp:pic>(BinData)<img src="data:…">이미지 base64 인라인, 표시 크기 hp:sz v2.6
벡터 도형 polygon/rect/ellipse/curve인라인 <svg>텍스트 없는 그리기 개체 보존(회색 기본 채움) v2.6
심볼폰트 특수문자(PUA, U+E000~F8FF)표준 유니코드빈 글리프로 누락되던 기호 치환(예: ·) v2.6
<hp:tab leader>점선/파선 리더선(border-bottom)목차 “제목 … 페이지” 점선 보존 v2.6
hp:p@pageBreak · 단독 ‘목차/차례’.we-page-break인쇄 시 페이지 나눔(표지→목차 분리) v2.6
도형 <hp:drawText> 내 텍스트단락 텍스트사각형·컨테이너 등 도형 안 글자 추출

9-A-5. 제한 사항

  • 미지원: 수식, SmartArt·복합 그리기 그룹의 정밀 레이아웃, 머리글/꼬리글, 각주·미주 본문, 텍스트 감싸기(도형 주변 흐름), 도형 회전.
  • 글머리·번호 목록하이퍼링크는 텍스트로 보존됩니다(리스트/링크 구조는 미반영).
  • 벡터 도형은 정점 기반 근사이며, 채움/선이 없으면 기본 회색으로 가시화합니다(원본 색과 다를 수 있음). 곡선(curve)은 제어점 없이 정점만 연결.
  • 구 바이너리 .hwp 파일은 지원하지 않습니다(.hwpx(OWPML) 표준 포맷만 대상). 미지원 요소는 텍스트 보존 + 경고로 처리됩니다.
변환 엔진 상세는 개발 문서 docs/HWPX_가져오기_개발문서.md(아키텍처·보완 이력·좌표 변환)를 참고하세요.

9-B. 마크다운 문서 뷰어/편집 API

dist/plugins/markdown-native.min.js 로드 + plugins'markdown' 지정이 필요합니다(opt-in). GFM(GitHub Flavored Markdown) 기준으로 변환하며, 가져오기·내보내기(왕복)·안전 렌더를 제공합니다.

9-B-1. 기본 사용법

<script src="dist/webeditor.min.js"></script>
<script src="dist/plugins/markdown-native.min.js"></script>
const editor = new WebEditor('#editor', {
  plugins: ['format', 'image', 'table', 'link', 'markdown']
});

9-B-2. 공개 API

메서드반환설명
editor.importMarkdownFile(file, options?)void(비동기).md 파일 → 변환 후 본문에 삽입
editor.setMarkdown(md, options?){html, warnings}마크다운 문자열 → 본문 설정
editor.getMarkdown(options?)string현재 본문(HTML) → 마크다운 직렬화(왕복)
editor.viewMarkdown(md, options?){html, warnings}마크다운 → 안전 HTML 문자열(삽입 없이 렌더용)

9-B-3. 예제

// 마크다운 → 본문
editor.setMarkdown('# 제목\n\n**굵게** 와 [링크](https://example.com)\n\n- 항목');
editor.setMarkdown(md, { mode: 'append', baseUrl: '/docs/' });

// 본문 → 마크다운 (왕복)
const md = editor.getMarkdown();

// 삽입 없이 안전 HTML만 렌더 (뷰어용)
const { html, warnings } = editor.viewMarkdown(mdText);
previewEl.innerHTML = html;

// .md 파일 가져오기
input.onchange = (e) => editor.importMarkdownFile(e.target.files[0], { mode: 'cursor' });
툴바에 마크다운 가져오기 버튼이 추가되고, 편집 영역에 .md(.markdown) 파일을 드래그앤드롭해도 가져올 수 있습니다.

9-B-4. 옵션

옵션타입기본값설명
mode'cursor' | 'append' | 'replace''replace'삽입 위치(가져오기/설정 시)
baseUrlstring''상대 링크·이미지 경로의 기준 URL

9-B-5. 지원 문법 (GFM)

마크다운HTML
# ~ ######<h1>~<h6>
**굵게** · *기울임* · ~~취소선~~<strong> · <em> · <del>
`인라인 코드` · ```펜스```<code> · <pre><code>
[텍스트](url) · ![alt](src)<a> · <img>
- / * / 1. 목록 · - [x] 체크박스<ul>/<ol>/<li> · 체크박스
GFM 표(| … |, :--: 정렬)<table>
> 인용 · --- 구분선<blockquote> · <hr>

9-B-6. 보안

모든 변환은 escape-first 정책으로 처리하며, <script> 삽입과 javascript:·data:text/html 링크는 자동 차단됩니다. viewMarkdown()·setMarkdown() 모두 동일하게 적용됩니다.

9-B-7. 독립 뷰어 데모

에디터 없이 마크다운만 보여주는 위젯 데모도 제공합니다 — demo/markdown.html(편집 모드 ↔ 전체보기 탭, 크기 지정 API), demo/markdown-editor.html(에디터 통합 왕복). 상세: 문서 마크다운_확장팩_API.md

11. TypeScript 타입 정의

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

interface WebEditorOptions {
  height?: string | number;
  placeholder?: string;
  defaultFont?: string;
  defaultFontSize?: number;
  defaultLineHeight?: string | number;
  contentCSS?: string;
  readOnly?: boolean;
  autofocus?: boolean;
  enterKey?: 'p' | 'br';
  undoGranularity?: 'char' | 'group';
  documentBackground?: { mode?: 'auto' | 'base64' | 'upload'; inlineThreshold?: number; store?: 'auto' | 'wrapper' | 'meta'; onTooLargeWithoutUploader?: 'fallback' | 'block' };
  uiTemplate?: 'classic' | 'ribbon';
  iconTheme?: string;
  iconThemePath?: string;
  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;
}

interface DocumentBackground {
  image: string | null;   // data URL 또는 업로드 URL
  color: string;          // ''=색 없음
  repeat: 'no-repeat' | 'repeat-x' | 'repeat-y' | 'repeat';
  attachment: 'scroll' | 'fixed';
  mode: 'base64' | 'upload' | null;  // 실제 적용된 이미지 처리
  store: 'wrapper' | 'meta';      // 배경 보존 위치
}

declare class WebEditor {
  static version: string;
  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(n: 0 | 1 | 2 | 'edit' | 'html' | 'preview'): this;
  getActiveTab(asName?: boolean): 0 | 1 | 2 | 'edit' | 'html' | 'preview';
  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;
  setDefaultLineHeight(lh: string | number): this;
  setEditorDefaults(opts: { font?: string; fontSize?: number; lineHeight?: string | number }): this;
  getEditorDefaults(): { font: string; fontSize: number | null; lineHeight: string };
  setDocumentBackground(opts: { image?: File | Blob | string | null; color?: string; repeat?: 'no-repeat' | 'repeat-x' | 'repeat-y' | 'repeat'; attachment?: 'scroll' | 'fixed' }): Promise<DocumentBackground>;
  removeDocumentBackground(): this;
  getDocumentBackground(): DocumentBackground | null;
  showMenuBar(bool: boolean): this;
  toggleToolbar(): this;   // Ctrl+[
  toggleMenuBar(): this;   // Ctrl+]
  setUITemplate(name: 'classic' | 'ribbon'): this;
  getUITemplate(): 'classic' | 'ribbon';
  setIconTheme(name: string): this;
  getIconTheme(): string;
  setTableBorderColor(color: string): this;
  getTableBorderColor(): string;
  setContentCSS(css: string): this;
  importWordFile(file: File): Promise<void>;
  importHwpxFile(file: File, options?: { mode?: 'cursor'|'append'|'replace'; silent?: boolean; maxTableWidth?: number; showLog?: boolean }): Promise<void>;
  pasteFromClipboard(): Promise<boolean>;
  // 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>;
  reviewSensitiveData(): 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 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>

게시판 파일 첨부 · 문서 미리보기 (데모 패턴)

게시글 작성 화면(에디터 하단)에 파일 첨부(드래그앤드롭·다중)를 붙이고, 첨부한 .docx·.hwpx·.md메뉴바·아이콘바 없는 에디터 새창으로 열어 미리보는 패턴입니다. 데모(demo/write.html·demo/view.html·demo/doc-preview.html)에서 사용합니다.

// 1) 첨부파일: 파일을 data URL(base64)로 읽어 게시글과 함께 저장
const reader = new FileReader();
reader.onload = e => attachments.push({ name: file.name, size: file.size, type: file.type, data: e.target.result });
reader.readAsDataURL(file);
// 저장: localStorage.setItem(KEY, JSON.stringify({ ...post, attachments }))

// 2) 미리보기 창(doc-preview.html) — 메뉴바·아이콘바 없는 에디터
const editor = new WebEditor('#pv-editor', {
  menubar: false,                                  // 드롭다운 메뉴바 없음
  plugins: ['format','image','table','link','wordimport','hwpximport','markdown'],
});
editor.showToolbar(false);                          // 아이콘(툴바) 숨김
editor.setHeight(window.innerHeight - 46);          // 고정 높이 + 내부 스크롤 (필수)

// 3) data URL → File 복원 후 형식별 import → 완료 후 읽기 전용으로 잠금
const blob = await (await fetch(dataUrl)).blob();
const file = new File([blob], name, { type: mime });
if (/\.hwpx$/i.test(name))          await editor.importHwpxFile(file, { silent: true, mode: 'replace' });
else if (/\.(md|markdown)$/i.test(name)) await editor.importMarkdownFile(file, { mode: 'replace' });
else                              await editor.importWordFile(file, { silent: true, mode: 'replace' });

editor.setReadOnly(true);   // 편집·상단 메뉴바·표/이미지 컨텍스트 메뉴·리사이즈 모두 차단 (v2.6.0.0)

// 4) 부모 → 미리보기 창 데이터 전달: 동일 출처 postMessage 핸드셰이크
// 미리보기는 기본 너비 1000px 팝업(새 창)으로 연다
const w = window.open('doc-preview.html', '_blank', 'width=1000,height=860,resizable=yes,scrollbars=yes');
window.addEventListener('message', e => {
  if (e.source === w && e.data.type === 'docPreviewReady')
    w.postMessage({ type: 'docPreviewFile', name, mime, dataUrl }, location.origin);
});
미리보기 스크롤: height 옵션은 본문에 min-height(자동 확장)만 적용하므로, 창을 채우는 고정 높이 + 내부 스크롤이 필요하면 생성 후 setHeight(px) 를 호출해야 합니다(생략 시 본문이 창을 넘어 잘려 스크롤 불가). importWordFile()/importHwpxFile()/importMarkdownFile() 는 각각 wordimport·hwpximport·markdown 플러그인 번들을 로드해야 사용할 수 있습니다. 첨부·미리보기 데이터는 데모에서 localStorage동일 출처 postMessage 로만 다룹니다(실서비스는 파일 업로드 API로 교체).

미리보기 창(doc-preview.html) 내부 구현

미리보기 창은 부모 창에 준비(ready) 신호를 보내고, 부모가 전달한 파일 데이터를 받아 위 2·3단계(에디터 생성 → import → setReadOnly(true))를 수행합니다. 편집이 목적이 아니므로 하단 '편집/HTML/미리보기' 탭바를 만드는 sourceview 플러그인은 제외합니다.

// doc-preview.html — 미리보기 전용 창(자식)
// (1) 로드되면 부모(opener)에 준비 신호 전송
if (window.opener)
  window.opener.postMessage({ type: 'docPreviewReady' }, location.origin);

// (2) 부모가 보낸 파일 데이터 수신 — 동일 출처 + opener 검증
window.addEventListener('message', async e => {
  if (e.origin !== location.origin) return;              // 동일 출처만 허용
  if (window.opener && e.source !== window.opener) return;
  if (!e.data || e.data.type !== 'docPreviewFile') return;

  const { name, mime, dataUrl } = e.data;
  // 뷰어 에디터 생성 — 메뉴바·툴바 없음, sourceview(탭바) 제외
  const ed = new WebEditor('#pv-editor', {
    menubar: false,
    plugins: ['format','image','table','link','wordimport','hwpximport','markdown'],
  });
  ed.showToolbar(false);
  ed.setHeight(window.innerHeight - 46);

  const blob = await (await fetch(dataUrl)).blob();
  const file = new File([blob], name, { type: mime });
  if (/\.hwpx$/i.test(name))          await ed.importHwpxFile(file, { silent: true, mode: 'replace', maxTableWidth: 960 });
  else if (/\.(md|markdown)$/i.test(name)) await ed.importMarkdownFile(file, { mode: 'replace' });
  else                              await ed.importWordFile(file, { silent: true, mode: 'replace' });

  ed.setReadOnly(true);   // 편집 완전 차단(뷰어)
});
미리보기 창 열기(부모): window.open('doc-preview.html', '_blank', 'width=1000,height=860,resizable=yes,scrollbars=yes') — 크기(width/height)를 지정하면 탭이 아닌 독립 팝업 창으로 열립니다(기본 너비 1000px). 읽기 전용(setReadOnly(true))은 v2.6.0.0부터 본문 편집뿐 아니라 상단 메뉴바·표/이미지 컨텍스트 메뉴·리사이즈까지 차단하므로 참고용 뷰어에 적합합니다.

다중 인스턴스

// 라이선스는 각 인스턴스 초기화 옵션 license로 적용
const editor1 = new WebEditor('#editor-1', { height: '300px', license: 'WED2-...' });
const editor2 = new WebEditor('#editor-2', { height: '300px', readOnly: true, license: 'WED2-...' });

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

부록 — 단축키 목록

단축키기능
Ctrl+B굵게
Ctrl+I이탤릭
Ctrl+U밑줄
Ctrl+K하이퍼링크 삽입
Ctrl+F찾기/바꾸기
Ctrl+Z실행 취소
Ctrl+Y다시 실행
Ctrl+Shift+Z다시 실행 (Mac 스타일)
Ctrl+A전체 선택
Ctrl+C복사
Ctrl+X잘라내기
Ctrl+V붙여넣기
Ctrl+Shift+V텍스트로 붙여넣기
Ctrl+[툴바 표시/숨김 토글
Ctrl+]메뉴바 표시/숨김 토글
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   최소화 버전 ⭐
    ├── hwpximport-native.js       한글 HWPX 가져오기 플러그인
    └── hwpximport-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
    └── hwpximport/           한글 HWPX 가져오기
        ├── index.js          HwpxImportPlugin 진입점
        ├── core/
        │   └── ZipReader.js  HWPX(ZIP) 파싱
        └── engines/
            └── OwpmlEngine.js  OWPML → HTML

© 2024-2026 DEXTSOLUTION Inc. All Rights Reserved.