dist/ ├── webeditor.js // UMD 번들 (가독성 유지) ├── webeditor.min.js // UMD 번들 (terser minified) ├── webeditor.esm.js // ES Module 빌드 ├── webeditor.css // 전체 스타일 ├── webeditor.min.css // 압축 스타일 ├── webeditor.d.ts // TypeScript 타입 선언 └── plugins/ // 별도 로드 플러그인 ├── wordimport-native.js // MS Word 가져오기 (Native engine) └── wordimport-native.min.js
src/ ├── editor.js // 코어 Editor 클래스 + 옵션/이벤트 시스템 ├── editor.css // 툴바·본문·모달·상태바 등 전체 CSS ├── toolbar.js // 툴바 렌더링/디스패치 ├── menubar.js // 7개 그룹 드롭다운 메뉴바 + About 다이얼로그 ├── dom-api.js // WebEditorAPIModel (DOM 조회·수정 체이너블 API) ├── license/ // 라이선스 검증 (HMAC-SHA256, 도메인 매칭) │ ├── manager.js │ ├── validator.js │ ├── domain-matcher.js │ ├── i18n.js │ └── evaluation-modal.js └── plugins/ ├── format.js // Bold/Italic/색상/크기/정렬/목록 ├── image.js // 붙여넣기 + 드래그앤드롭 + 리사이즈 ├── table.js // 격자 피커 + 셀 병합 + 음영 + 코너 핸들 리사이즈 ├── link.js // 하이퍼링크 다이얼로그 ├── emoji.js // 8개 카테고리 이모지 팔레트 ├── video.js // YouTube/Vimeo 임베드 ├── sourceview.js // HTML 소스 보기/편집 탭 ├── insert.js // 가로줄·특수문자·페이지 나누기 ├── find.js // 찾기/바꾸기 └── wordimport/ // MS Word 가져오기 (Phase 2.3 NEW) ├── index.js // 플러그인 오케스트레이터 ├── core/ │ ├── DocxReader.js // ZIP 해제 (DecompressionStream) │ └── HtmlNormalizer.js // 변환 결과 살균 ├── engines/ │ ├── EngineRegistry.js // 엔진 등록·선택 │ └── NativeEngine.js // OOXML→HTML 변환 엔진 └── ui/ ├── ImportDialog.js // 옵션 선택 모달 └── ProgressDialog.js // 진행 인디케이터
demo/ ├── sdk-guide.html // 본 문서 — 통합 가이드 ├── board.html // 게시판 목록 ├── write.html // 게시글 작성 (Word 가져오기 통합) ├── view.html // 게시글 보기 (XSS 새니타이저 포함) ├── dom-api.html // DOM API 인터랙티브 데모 ├── menubar.html // 메뉴바·UI 컨트롤 데모 └── wordimport.html // MS Word 가져오기 데모 assets/ └── icons.svg // SVG 스프라이트 (60+ symbols) img/ ├── dee1.PNG // 밝은 배경용 로고 ├── dee2.PNG // 어두운 배경용 로고 └── dee_gray.png // 무채색 로고
build.js // 코어 + 플러그인 별도 번들 + 미니파이 make-package.js // 고객용/사내용 ZIP 생성 package.json // npm scripts: build / build:watch
Word 가져오기는
코어 번들과 분리된 외부 플러그인으로 설계되어
있습니다. 에디션(Standard / Premium) 별로 다른 변환 엔진을 선택할
수 있도록 EngineRegistry 패턴을 사용합니다. 같은
메뉴/툴바 UI에서 어떤 엔진이 등록되어 있든 자동으로 가장 우선순위
높은 엔진을 사용합니다.
src/plugins/wordimport/ ├── index.js // WordImportPlugin — 오케스트레이터 │ // · 메뉴/툴바/드래그앤드롭 진입 │ // · editor.importWordFile(file, options) API 노출 │ // · 자기 등록: WebEditor.registerPlugin('wordimport', ...) ├── core/ │ ├── DocxReader.js // ZIP 해제 (DecompressionStream 'deflate-raw') │ │ // · EOCD/Central Directory 파싱 │ │ // · 엔트리별 STORE/DEFLATE 해제 → Map<path, Uint8Array> │ └── HtmlNormalizer.js // 변환 결과 살균 (script/on*/javascript: 제거) ├── engines/ │ ├── EngineRegistry.js // 엔진 등록·선택 (priority 기반) │ │ // · register / all / best / byName │ └── NativeEngine.js // Standard 에디션 변환 엔진 (의존성 0) │ // · OOXML(w:p/w:r/w:tbl/...) → HTML │ // · 단락·서식·표(tblGrid/음영/병합)·목록·링크·이미지 │ // · EMU → px 정확 변환 (1 inch = 914400 EMU = 96 px) └── ui/ ├── ImportDialog.js // 옵션 모달 (엔진/삽입모드/이미지/로그) └── ProgressDialog.js // 변환 진행 스피너 dist/plugins/ // 빌드 산출 ├── wordimport-native.js // 49 KB — 자기등록 IIFE 번들 └── wordimport-native.min.js // 21 KB — terser minified
<!-- 1. 코어 로드 --> <script src="dist/webeditor.min.js"></script> <!-- 2. Word 가져오기 플러그인 로드 (별도 파일) --> <script src="dist/plugins/wordimport-native.min.js"></script> <!-- 3. 에디터 옵션에 'wordimport' 추가 --> <script> const editor = new WebEditor('#editor', { menubar: true, plugins: ['format', 'image', 'table', 'link', 'wordimport'], }); </script>
.docx 파일 드래그앤드롭
MammothEngine.js를 같은 폴더 구조로 추가하면
EngineRegistry가 자동으로 우선순위(priority=100)에
따라 선택합니다. UI/메뉴/사용자 코드는 변경할 필요가 없습니다.
DecompressionStream API 필수)
<script> 하나와
new WebEditor()만으로 사용합니다.
<!-- 1. CSS 로드 --> <link rel="stylesheet" href="dist/webeditor.min.css"> <!-- 2. 컨테이너 --> <div id="editor"></div> <!-- 3. JS 로드 + 초기화 --> <script src="dist/webeditor.min.js"></script> <script> const editor = new WebEditor('#editor'); </script>
const editor = new WebEditor('#editor', { height: '500px', placeholder: '게시판 내용을 입력하세요.', onChange: (html) => { document.getElementById('char-count').textContent = editor.body.innerText.replace(/\s/g, '').length + '자'; }, onFocus: () => console.log('편집 시작'), onBlur: () => console.log('편집 완료'), });
const editor = new WebEditor('#editor', { plugins: ['format', 'link'], // 서식 + 링크만 });
const editor = new WebEditor('#editor', { plugins: ['format', 'image', 'link', 'emoji', 'insert'], });
const editor = new WebEditor('#editor'); // 기본 = 전체
toolbar 옵션으로 버튼 배치를 완전히 제어합니다.
'|'는 구분선입니다.
const editor = new WebEditor('#editor', { toolbar: [ // 1행: 서식 핵심만 ['bold', 'italic', 'underline', 'strikeThrough', '|', 'fontColor', 'highlightColor', '|', 'removeFormat'], // 2행: 레이아웃 + 삽입 ['justifyLeft', 'justifyCenter', 'justifyRight', '|', 'insertImage', 'insertLink', 'insertTable', '|', 'undo', 'redo'], ], });
uploadHandler를 지정하면 이미지를 서버에 업로드한 뒤
URL로 삽입합니다. 미지정 시 base64 Data URL로 삽입됩니다.
const editor = new WebEditor('#editor', { uploadHandler: async (file) => { const form = new FormData(); form.append('image', file); const res = await fetch('/api/upload', { method: 'POST', body: form }); const { url } = await res.json(); return url; // 최종 URL 반환 필수 }, });
init, execute,
queryState)를 구현하면 됩니다.
// 글자수 제한 플러그인 예제 class CharLimitPlugin { constructor(editor, limit = 1000) { this.editor = editor; this.limit = limit; } init() { this.editor.on('change', () => { const chars = this.editor.body.innerText.replace(/\s/g, '').length; if (chars > this.limit) { alert(`글자수 초과! 현재 ${chars}자 / 최대 ${this.limit}자`); } }); } execute(command) {} // 툴바 버튼 없으면 비워도 됨 queryState() { return {}; } } // 사용 const editor = new WebEditor('#editor', { plugins: ['format'] }); const limiter = new CharLimitPlugin(editor, 500); limiter.init(); editor.plugins.push(limiter);
// 폼 제출 시 hidden input에 HTML 복사 const editor = new WebEditor('#editor'); const form = document.querySelector('#post-form'); form.addEventListener('submit', (e) => { const html = editor.getHTML(); if (!html.trim() || html === '<br>') { e.preventDefault(); alert('내용을 입력해주세요.'); editor.focus(); return; } document.getElementById('content-input').value = html; });
// ID로 요소 모델 획득 const table = editor.getAPIModelById('mytable'); // CSS 선택자로 조회 const firstP = editor.querySelector('p'); const allImgs = editor.querySelectorAll('img'); // 본문 루트 const root = editor.getRoot(); // or editor.getBody() // 옵션 — 변경 이벤트 억제 (일괄 작업용) const p = editor.querySelector('p', { skipRendering: true });
const el = editor.getAPIModelById('title'); el.getText(); // "제목 텍스트" el.getHTML(); // "<strong>제목</strong>" el.getTagName(); // "h2" el.getId(); // "title" el.getAttribute('class'); el.getStyle(); // "color:red; font-size:14pt" el.getCSS('color'); // "red" el.getRect(); // DOMRect {x, y, width, height} el.getWidth(); // offsetWidth el.getNode(); // 원시 DOM Element (escape hatch)
el.setText('새 텍스트'); el.setHTML('<b>HTML</b>'); el.setStyle('color:red; font-weight:bold'); el.setCSS('color', 'blue'); el.setCSS({ color: 'red', fontSize: '14pt' }); el.setAttribute('data-id', '123'); el.removeAttribute('data-id'); el.addClass('highlight'); el.removeClass('highlight'); el.toggleClass('active'); el.hasClass('active'); // true / false // 모든 메서드는 자기 자신을 반환 → 체이닝 가능 el.setText('Hello').addClass('note').setStyle('color:blue');
// 탐색 el.parent(); // APIModel | null el.children(); // APIModel[] el.nextSibling(); el.prevSibling(); el.find('td'); // 하위 검색 (배열) el.findOne('td'); // 하위 첫 매치 el.closest('table'); // 조상 검색 (선택자 또는 함수) el.closest(m => m.hasClass('foo')); // 삽입 — 문자열 / Node / APIModel 모두 허용 el.append('<p>끝에</p>'); el.prepend('<p>앞에</p>'); el.insertBefore('<hr>'); el.insertAfter('<hr>'); // 제거 el.empty(); // 자식 노드 모두 제거 el.remove(); // 자기 자신 제거
const tbl = editor.getAPIModelById('mytable'); tbl.insertRow(1); // 인덱스 1번 위에 새 행 삽입 tbl.insertCol(2); // 인덱스 2번 왼쪽에 새 열 삽입 tbl.deleteRow(0); // 0번 행 삭제 tbl.deleteCol(); // 마지막 열 삭제 tbl.setWidth(400); tbl.setHeight('200px');
change)는 매 작업마다 발생합니다. 다수
변경 시 한 번만 통지하려면 skipRendering: true로
작업한 뒤 editor.render()를 호출합니다.
// 100개 단락에 클래스 추가 — change는 1회만 발생 editor.querySelectorAll('p', { skipRendering: true }) .forEach(p => p.addClass('paragraph')); editor.render(); // → change 이벤트 1회 발생
// 입력: 폼 값을 에디터 표 셀에 채워넣기 function applyToEditor(form) { editor.getAPIModelById('cell-name').setText(form.name); editor.getAPIModelById('cell-phone').setText(form.phone); editor.getAPIModelById('cell-addr').setText(form.address); } // 추출: 에디터 표에서 값 읽어오기 function extractFromEditor() { return { name: editor.getAPIModelById('cell-name').getText(), phone: editor.getAPIModelById('cell-phone').getText(), address: editor.getAPIModelById('cell-addr').getText(), }; }
| 분류 | 메서드 |
|---|---|
| 식별·노드 |
getNode, isValid,
getId, setId,
getTagName, matches
|
| 속성 |
getAttribute, setAttribute,
removeAttribute, hasAttribute
|
| 클래스 |
addClass, removeClass,
hasClass, toggleClass
|
| 스타일 |
getStyle, setStyle,
setCSS(prop, val), setCSS({...}),
getCSS
|
| 내용 |
getText, setText,
getHTML, setHTML,
empty, remove
|
| 탐색 |
parent, closest,
children, childNodes,
nextSibling, prevSibling,
find, findOne
|
| 삽입 |
append, prepend,
insertBefore, insertAfter
|
| 측정 |
getRect, getWidth,
getHeight
|
| 표 전용 |
insertRow, insertCol,
deleteRow, deleteCol,
setWidth, setHeight
|
// 기본 메뉴 활성화 (파일·편집·보기·삽입·서식·도구·도움말) const editor = new WebEditor('#editor', { menubar: true, }); // 커스텀 메뉴 구성 const editor2 = new WebEditor('#editor', { menubar: [ { label: '게시글', items: [ { label: '새 게시글', cmd: 'newDocument' }, { label: '임시저장', cmd: 'mySaveDraft' }, { sep: true }, { label: '발행', cmd: 'myPublish' }, ]}, { label: '서식', items: [ { label: '굵게', cmd: 'bold', shortcut: 'Ctrl+B' }, { label: '기울임', cmd: 'italic', shortcut: 'Ctrl+I' }, ]}, ], onMenuCommand: (cmd, editor, item) => { if (cmd === 'mySaveDraft') { fetch('/api/draft', { method: 'POST', body: editor.getHTML() }); return false; // 기본 처리 차단 } }, });
| 분류 | 명령(cmd) |
|---|---|
| 파일 |
newDocument, openHTML,
save, saveAs, print,
printPreview
|
| 편집 |
undo, redo, cut,
copy, paste,
selectAll, find
|
| 보기 |
toggleToolbar, toggleMenuBar,
toggleStatusBar, toggleFullscreen,
modeEdit, modeHTML,
modePreview
|
| 삽입 |
insertImage, insertTable,
createLink, insertVideo,
insertEmoji, insertHorizontalRule
|
| 서식 |
bold, italic,
underline, strikeThrough,
clearFormat, clearCSSFormat
|
| 도구 |
toggleReadOnly, showWordCount,
about, shortcuts
|
// 개별 토글 editor.showMenuBar(true); // 메뉴바 표시/숨김 editor.showToolbar(false); // 툴바 표시/숨김 editor.showStatusBar(true); // 상태바 표시/숨김 editor.setReadOnly(true); // 읽기 전용 전환 editor.setFullscreen(true); // 전체 화면 (F11, Esc 자동 지원) editor.setHeight('500px'); editor.setWidth('100%'); // 런타임에 메뉴바 동적 추가/제거 editor.enableMenuBar(); // 기본 메뉴 추가 editor.enableMenuBar(customConfig); // 커스텀 추가 editor.disableMenuBar(); // 일괄 적용 editor.setUI({ menubar: true, toolbar: true, statusbar: false, readOnly: false, fullscreen: false, height: '600px', }); // 현재 상태 조회 const state = editor.getUI(); // → { toolbar:true, menubar:true, statusbar:true, readOnly:false, fullscreen:false, width:'100%', height:'400px' }
F11 — 전체 화면 전환Esc — 전체 화면 종료Esc — 드롭다운 닫기.docx 파일을 에디터로 가져오는 별도 플러그인입니다.
외부 라이브러리 0 — 브라우저 내장 DecompressionStream +
DOMParser만 사용. Standard 에디션에
기본 포함.
<!-- 1. 코어 + 플러그인 함께 로드 (순서 중요) --> <script src="dist/webeditor.js"></script> <script src="dist/plugins/wordimport-native.js"></script> <script> const editor = new WebEditor('#editor', { menubar: true, plugins: ['format', 'image', 'table', 'wordimport'], // ← 추가 }); // 프로그래매틱 호출 editor.importWordFile(file, { mode: 'cursor', // 'replace' | 'cursor' | 'append' silent: false, // true면 다이얼로그 생략 }); </script>
.docx 파일 드래그앤드롭editor.importWordFile(file, options) API 호출
| 지원 | 미지원 |
|---|---|
|
단락·정렬·들여쓰기 제목 스타일 (H1~H6) 굵게/기울임/밑줄/취소선 색상·폰트·크기·배경 표 + 셀 병합 (colspan/rowspan) 글머리·번호 목록 하이퍼링크 이미지 (PNG/JPG, base64) 페이지 나누기 |
SmartArt · 도형 수식 (MathML) 머리글 / 꼬리글 각주 / 미주 VBA 매크로 변경 내용 추적 |
Chrome 80+ · Edge 80+ · Firefox 113+ · Safari 16.4+ (이전 버전 미지원)
dist/webeditor.d.ts가 포함되어 있어 타입 추론이
자동으로 동작합니다.
전체 인터페이스 정의(WebEditorOptions·LicenseInfo·메서드
시그니처)는
📑 API 레퍼런스 §11 TypeScript 타입을 참조하세요.
import WebEditor, { WebEditorOptions } from '@webeditor/core'; const options: WebEditorOptions = { height: '500px', onChange: (html: string) => saveContent(html), }; const editor = new WebEditor('#editor', options); // 타입 안전한 API 사용 const html: string = editor.getHTML(); editor.setHTML('<p>안녕하세요!</p>'); editor.on('change', (html: string) => console.log(html));
dist/webeditor.d.ts 참조.
const editor = new WebEditor('#editor', { autoSave: { interval: 30000, // 저장 주기(ms) storageKey: 'my-doc', // 기본: we-autosave-{id} unloadWarning: true, // 변경 후 이탈 시 경고 onSave: (html) => {} // false 반환 시 저장 억제 } }); // 페이지 로드 시 복구 const saved = editor.getAutoSaved(); // string | null if (saved) { editor.setHTML(saved); editor.clearAutoSaved(); } editor.saveNow(); // 즉시 저장
editor.saveToPDF({ title: '문서', hideToolbar: true }); // 본문만 인쇄(PDF)
editor.saveToPDFInWindow({ title: '문서' }); // 새 창 출력 후 인쇄
editor.setZoom(1.5); // 0.25 ~ 4.0 (범위 밖 RangeError)
editor.getZoom(); // 현재 비율
editor.on('zoom', ({ ratio }) => console.log(ratio));
new WebEditor('#editor', { inlineToolbar: true, // 텍스트 선택 시 부유 서식 툴바 hyperLinkDefaultTarget: '_blank' // 새 링크 기본 새 창 });
editor.getSafeHTML(); // script/on*/javascript: 제거 editor.getSafeHTML({ allowedTags: ['p','b','i','a'], // 외 태그는 언랩(텍스트 보존) allowedAttrs: ['href'], imageRewriter: (src) => 'https://cdn/' + src });
const editor = new WebEditor('#editor', { privacy: { detect: ['ssn','phone','email','credit-card','bank-account'] }, profanity: { words: ['금칙어1','금칙어2'], maskChar: '*' } // words: 배열 또는 URL }); const r = await editor.validate(); // { profanity:[...], privacy:[...] } await editor.maskSensitiveData(); // 감지 항목 마스킹(반환: 개수)
new WebEditor('#editor', { uploadHeaders: { 'X-App': 'dee' }, // 커스텀 헤더 csrfCookie: 'XSRF-TOKEN', // 쿠키값 → X-CSRF-Token 자동 주입 uploadHandler: (file, headers) => fetch('/upload', { method:'POST', headers, body: makeForm(file) }) .then(r => r.json()).then(j => j.url) });
editor.on('imageInserted', ({ src, file, node }) => {}); // 이미지 DOM 부착 시 editor.on('imageUploaded', ({ src, file, node }) => {}); // 업로드 URL 확정 시 editor.on('error', ({ context, error }) => {}); // 내부 오류 수집 editor.isEmpty(); // 본문 비었는지 (공백·빈 단락은 빈 것으로, 이미지·표는 false)