Dee Editor 매뉴얼
Dee Editor는 외부 의존성 없이 순수 JavaScript로 구현된 WYSIWYG 웹 에디터입니다. 이 문서는 설치부터 고급 기능 연동까지 전체 API를 설명합니다.
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. 초기화 옵션
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
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 사용 예
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
에디터 본문의 HTML 문자열을 반환합니다.
const html = editor.getHTML();
// '<p>안녕하세요</p><p>반갑습니다</p>'
에디터 본문을 주어진 HTML로 설정합니다.
editor.setHTML('<p>새 내용</p>');
HTML 태그를 제거한 순수 텍스트를 반환합니다.
const text = editor.getText();
// '안녕하세요\n반갑습니다'
본문 끝에 HTML을 추가합니다.
editor.appendHTML('<p>추가된 단락</p>');
현재 커서 위치에 HTML을 삽입합니다. 선택 영역이 있으면 대체합니다.
editor.insertHTML('<strong>굵은 텍스트</strong>');
현재 선택 영역의 HTML을 반환합니다. 선택이 없으면 ''.
const selected = editor.getSelectedHTML();
현재 선택 영역의 순수 텍스트를 반환합니다.
const text = editor.getSelectedText();
에디터 본문을 완전히 비웁니다.
editor.clear();
3-2. 상태 제어
에디터를 읽기 전용으로 설정하거나 해제합니다. 읽기 전용 시
wrapper에 .we-readonly 클래스가 부여됩니다.
editor.setReadOnly(true); // 읽기 전용
editor.setReadOnly(false); // 편집 가능
현재 읽기 전용 상태를 반환합니다.
if (editor.isReadOnly()) {
console.log('읽기 전용 모드입니다');
}
마지막 setHTML() 또는
resetModified() 호출 이후 내용이 변경되었는지
반환합니다.
window.addEventListener('beforeunload', (e) => {
if (editor.isModified()) {
e.preventDefault();
}
});
변경 플래그를 초기화합니다.
editor.resetModified();
에디터 높이를 변경합니다. number(px),
'500px', '50vh' 모두 허용됩니다.
editor.setHeight(600);
editor.setHeight('80vh');
현재 에디터 높이를 픽셀 단위로 반환합니다.
const h = editor.getHeight(); // 600
에디터 너비를 변경합니다.
editor.setWidth('100%');
editor.setWidth(800);
현재 에디터 너비를 픽셀 단위로 반환합니다.
에디터 탭을 전환합니다.
| index | 탭 |
|---|---|
0 |
편집 (WYSIWYG) |
1 |
HTML 소스 |
2 |
미리보기 |
editor.setTab(1); // HTML 소스 보기
editor.setTab(0); // 편집 모드로 돌아가기
현재 활성 탭 인덱스를 반환합니다. (0 /
1 / 2)
툴바 표시 여부를 제어합니다.
editor.showToolbar(false); // 툴바 숨김
editor.showToolbar(true); // 툴바 표시
3-3. 이미지
URL로 이미지를 현재 커서 위치에 삽입합니다. base64 data URL도 지원합니다.
editor.insertImage('https://example.com/photo.jpg');
editor.insertImage('data:image/png;base64,...');
본문 내 모든 이미지 URL 목록을 배열로 반환합니다.
const urls = editor.getImages();
// ['https://...', 'data:image/png;...']
이미지 클릭 → 액션바에서 "캡션 추가" 토글.
<figure> +
<figcaption contenteditable> 구조로 감쌉니다.
<figure class="we-img-figure">
<img src="...">
<figcaption contenteditable="true">캡션 텍스트</figcaption>
</figure>
이미지 클릭 → 액션바에서 "링크 추가/편집" → 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>
보안 가드:
-
_blank시rel="noopener noreferrer"자동 부여 (탭내핑·Referer 누출 방지) -
javascript:,vbscript:, 비-이미지data:URL 자동 차단 -
화이트리스트 외
target값은_self로 강제 - 편집 영역에서 링크 이미지 클릭 시 네비게이션 차단 (저장 HTML은 정상)
3-4. 콘텐츠 정제
선택 영역의 모든 서식(태그·인라인 스타일)을 제거합니다.
editor.clearFormat();
선택 영역의 인라인 CSS 스타일만 제거합니다.
<strong>, <em> 등 태그는
유지합니다.
editor.clearCSSFormat();
선택 영역 내 특정 태그를 제거하고 내용은 유지합니다.
editor.removeTag('span'); // <span style="...">텍스트</span> → 텍스트
editor.removeTag('strong');
3-5. 런타임 설정
에디터 기본 글꼴을 런타임에 변경합니다.
editor.setDefaultFont('Arial');
editor.setDefaultFont('Noto Sans KR');
에디터 기본 글자 크기를 런타임에 변경합니다 (단위: pt).
editor.setDefaultFontSize(16);
편집 영역에 CSS 규칙을 추가합니다.
editor.setContentCSS(`
p { line-height: 1.8; }
img { max-width: 100%; }
`);
3-6. 생명주기
에디터에 포커스를 줍니다.
editor.focus();
에디터 인스턴스를 완전히 제거합니다. DOM 복원 + 이벤트 해제가 수행됩니다.
editor.destroy();
3-7. Word 가져오기
wordimport-native.min.js)이
로드된 경우에만 사용할 수 있습니다.
.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 신규 메서드
본문이 비어 있는지 반환. 공백· ·빈
단락(<p><br></p>)은 빈 것으로
간주하며, 이미지·표·미디어가 있으면 false.
XSS 위험 요소(script/on*/javascript:
등)를 제거한 HTML 반환. getHTML()과 별개이며
저장·전송용.
allowedTags·allowedAttrs·imageRewriter
옵션 지원.
const safe = editor.getSafeHTML({ allowedTags: ['p','b','a'], allowedAttrs: ['href'] });
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(); }
브라우저 인쇄 대화상자로 본문만 출력(PDF 저장 유도).
{ title, hideToolbar, css }.
saveToPDFInWindow는 새 창에 본문만 출력 후
인쇄(고품질).
편집 영역 확대/축소 (0.25 ~ 4.0, 범위 밖
RangeError). zoom 이벤트 발화. Chrome
zoom · 기타 transform: scale() 폴백.
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()— UI 상태 일괄 적용·조회
4. 이벤트 시스템
| 이벤트 | 핸들러 파라미터 | 발화 시점 |
|---|---|---|
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 (다단계) |
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 네비게이션 | 기본 동작으로 페이지 이동 |
dragover에 e.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.
Dee Editor