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>
<!-- 한글 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. 초기화 옵션
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
height | string | number | '400px' | 에디터 높이 |
placeholder | string | '' | 빈 상태 안내 텍스트 |
defaultFont | string | 'Malgun Gothic' | 기본 글꼴 (툴바 드롭다운 반영) |
defaultFontSize | number | 14 | 기본 글자 크기 (pt, 툴바 드롭다운 반영) |
defaultLineHeight | string | number | '1' | 기본 줄간격 배수 (툴바 드롭다운 반영) |
undoGranularity | 'char' | 'group' | 'char' | 실행취소 단위: 한 글자(한글은 완성 음절) / 묶음 |
documentBackground | object | {mode:'auto',inlineThreshold:10240,store:'auto'} | 문서 배경 이미지 처리·보존 전략 (소형<10KB=base64, 대형=업로드) |
uiTemplate | 'classic' | 'ribbon' | 'classic' | 메뉴 UI 템플릿: 클래식(메뉴바+툴바) / 리본(탭+큰 아이콘+폰트 퀵바) |
iconTheme | string | 'default' | 아이콘 테마 (default·gray·blue·colorful·vivid·natural·emoji 또는 폴더명) |
iconThemePath | string | 'assets/icon-themes' | 아이콘 테마 폴더 베이스 경로 |
tableBorderColor | string | '#000000' | 표 셀 기본 테두리 색. 런타임 변경 setTableBorderColor() |
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=자동) |
trustedHosts | string[] | [] | 폐쇄망 신뢰 호스트(와일드카드 지원). ⚠️ v2.5.0.0: 키가 제공된 경우에만 동작 — 키 없으면 평가판 |
offlineMode | boolean | false | Web Crypto 불가(HTTP) 환경에서 ECDSA 검증 생략. ⚠️ v2.5.0.0: 키가 제공된 경우에만 동작 |
uploadHandler | (file, headers?) ⇒ Promise<string> | null | 이미지 서버 업로드 핸들러 (2번째 인자 headers는 v2.4) |
plugins | string[] | Object | 전체 | 활성 플러그인 목록. Word 가져오기는 'wordimport' 포함 필요 |
toolbar | Array | null | null | 커스텀 툴바 레이아웃 — 행(단)별 명령 배열 [[…],[…]], '|'=구분선 (2-2 참고) |
menubar | boolean | Array | false | 상단 메뉴바. true=기본 8그룹(파일~도움말), 배열=커스텀 구성 (10-3 참고) |
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 반환
}
});
2-2. UI 설정(config) — 설정 페이지로 만들기·적용
툴바·메뉴바·너비 등 UI 구성을 코드로 직접 쓰는 대신, 설정 페이지에서 시각적으로 구성해 config 파일로 저장할 수 있습니다. 설정 허브: demo/settings.html
| 설정 페이지 | 구성 대상 | 산출 config |
|---|---|---|
| ui-config.html | 툴바 아이콘 표시·순서·1·2·3단 배치·그룹 구분선 + 메뉴바 on/off·상태표시줄·에디터 너비 | webeditor-ui-config.js → window.DEEDITOR_UI_CONFIG |
| ui-config-apply.html | 저장한 UI config를 읽어 에디터를 구성하는 적용 예시 | |
| menubar-config.html | 드롭다운 메뉴바의 메뉴·항목·순서·구분선 | webeditor-menubar-config.js → window.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_가이드.md2-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.js로 templates/manifest.json 목록을 생성하세요. 상세: 문서 기본문서템플릿_툴바구성.md3. 인스턴스 메서드
대부분의 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();
클립보드 내용을 현재 커서 위치에 붙여넣습니다(메뉴·리본·버튼 클릭용, 비동기 Clipboard API). Ctrl+V와 동일 파이프라인(plain·Word/외부 HTML·엑셀 표선택·표 셀·이미지)으로 처리합니다. 보안 컨텍스트(HTTPS·localhost)+사용자 제스처+권한이 필요하며, 미지원·거부 시 Ctrl+V 안내 후 false를 반환합니다. (메뉴/리본의 "붙여넣기" 아이콘이 이 메서드를 호출)
btn.addEventListener('click', () => editor.pasteFromClipboard());
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);
현재 에디터 너비를 픽셀 단위로 반환합니다.
에디터 탭을 전환합니다. 숫자 인덱스 또는 이름('edit'/'html'/'preview', 대소문자·별칭 허용)을 받습니다.
| index | name | 탭 |
|---|---|---|
0 | 'edit' | 편집 (WYSIWYG) |
1 | 'html' | HTML 소스 |
2 | 'preview' | 미리보기 |
editor.setTab(1); // HTML 소스 보기
editor.setTab('preview'); // 이름으로도 전환
현재 활성 탭을 반환합니다. 기본은 인덱스(0/1/2), getActiveTab(true)는 이름('edit'/'html'/'preview')을 반환합니다.
editor.getActiveTab(); // 0
editor.getActiveTab(true); // 'edit'
툴바·메뉴바·상태바 표시 여부를 제어합니다. (메뉴바는 menubar 옵션 사용 시)
editor.showToolbar(false); // 툴바 숨김
editor.showMenuBar(false); // 메뉴바 숨김
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);
기본 줄간격 변경, 기본 글꼴·크기·줄간격 일괄 설정/조회. 변경 시 툴바 글꼴·크기·줄간격 드롭다운에 즉시 반영됩니다. (드롭다운은 커서 위치의 실제 값/기본값을 표시하고 선택 시 선택값을 표시)
editor.setDefaultLineHeight('1.6');
editor.setEditorDefaults({ font: 'Georgia', fontSize: 16, lineHeight: '2' });
editor.getEditorDefaults(); // { font:'Georgia', fontSize:16, lineHeight:'2' }
편집 영역에 CSS 규칙을 추가합니다.
editor.setContentCSS(`
p { line-height: 1.8; }
img { max-width: 100%; }
`);
3-6. 생명주기
에디터에 포커스를 줍니다.
editor.focus();
에디터 인스턴스를 완전히 제거합니다. DOM 복원 + 이벤트 해제가 수행됩니다.
editor.destroy();
3-7. Word · HWPX 가져오기
wordimport-native.min.js / hwpximport-native.min.js)을 코어와 함께 로드하고 plugins에 'wordimport' / 'hwpximport'를 지정한 경우에만 사용할 수 있습니다(opt-in)..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);
});
한글 .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 신규 메서드
본문이 비어 있는지 반환. 공백· ·빈 단락(<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() → 본문 전체 마스킹 개수.
reviewSensitiveData() → 검사 결과 팝업 표시: 구분(종류)·내용 목록(페이지 8건, 이전/다음), 항목별/전체 체크박스 선택 → 선택 항목만 마스킹. 겹친 종류는 자동 정리. 툴바 🛡 버튼 / 상단 메뉴 도구 › 금칙어·개인정보 검사로도 실행.
const r = await editor.validate(); // { profanity:[...], privacy:[...] }
await editor.maskSensitiveData(); // 본문 전체 마스킹
await editor.reviewSensitiveData(); // 결과 팝업(선택 마스킹)
편집 영역에 배경 이미지/색/반복/덧붙임을 설정·제거합니다. 진입: 툴바 문서 배경 이미지 버튼 또는 상단 메뉴 삽입 › 문서 배경 이미지…. 옵션 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()로 자동 보존됩니다.
메뉴 UI 템플릿을 런타임 전환합니다. 'classic'(메뉴바+툴바) ↔ 'ribbon'(탭 리본 + 큰 라벨 아이콘 + 폰트 퀵바). 초기값은 옵션 uiTemplate로 지정합니다.
editor.setUITemplate('ribbon');
editor.getUITemplate(); // 'ribbon'
아이콘 테마를 런타임 전환합니다. 내장 default·gray·blue·colorful·vivid·natural·emoji 또는 iconThemePath 하위 폴더명. 클래식 툴바와 리본 탭/카드에 함께 적용됩니다.
editor.setIconTheme('vivid');
editor.getIconTheme(); // 'vivid'
툴바·메뉴바 표시를 토글합니다. 단축키 Ctrl+[(툴바) / Ctrl+](메뉴바).
editor.toggleToolbar(); // 툴바 표시/숨김 토글 (Ctrl+[)
editor.toggleMenuBar(); // 메뉴바 표시/숨김 토글 (Ctrl+])
이후 생성·삽입되는 표 셀의 기본 테두리 색과 테두리 색상 팔레트 기본값을 설정/조회합니다. 미설정 시 #000000. 옵션 tableBorderColor로 초기값 지정 가능.
editor.setTableBorderColor('#888888');
editor.getTableBorderColor(); // '#888888'
showMenuBar(bool)·enableMenuBar(config?)·disableMenuBar()·showStatusBar(bool)·setFullscreen(bool)·isFullscreen()·setUI(state)·getUI()도 제공됩니다(v2.4).인쇄 명령은 별도 미리보기 창 없이 화면 밖 숨김 iframe으로 인쇄 대화상자만 띄웁니다. saveToPDF()는 취소 후 미리보기가 다시 뜨지 않도록 afterprint/matchMedia('print') 기준으로 정리하며, 리본 모드에서도 본문만 출력됩니다.4. 이벤트 시스템
| 이벤트 | 핸들러 파라미터 | 발화 시점 |
|---|---|---|
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.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="..."> |
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'는 본문 전체 교체 |
maxTableWidth | number | — | 표를 이 픽셀 너비까지 비율 확대(에디터 창 폭에 맞춤) |
silent | boolean | false | true면 변환 다이얼로그 없이 즉시 삽입 |
showLog | boolean | false | 변환 경고·통계 로그 표시 |
maxTableWidth는 에디터 본문 폭에 맞춰 740~780px. 데모 페이지(demo/hwpximport.html)는 740px 기준으로 동작합니다.9-A-4. 지원 변환 항목 (OWPML → HTML)
| OWPML | HTML | 비고 |
|---|---|---|
<hp:p> | <p> | 단락 · 정렬(text-align) 반영 |
<hh:bold/> | <strong> | charPr 존재 여부로 판정 |
<hh:italic/> | <em> | |
| underline (type≠NONE) | <u> | |
charPr textColor / height | color / font-size | height = pt×100 |
<hp:tbl> + cellSz | <table> + 셀 너비/높이 | HWPUNIT → px(÷75) |
cellSpan colSpan/rowSpan | colspan / rowspan | 셀 병합 |
borderFill · winBrush@faceColor | border / background-color | 테두리·셀 음영(단색) |
borderFill · hc:gradation | background-image: linear/radial-gradient | 셀 그라디언트 배경 v2.6 |
paraPr · lineSpacing(PERCENT/FIXED) | line-height | 단락 줄간격 반영(hp:switch/default 중첩 포함) v2.6 |
subList@vertAlign | vertical-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' | 삽입 위치(가져오기/설정 시) |
baseUrl | string | '' | 상대 링크·이미지 경로의 기준 URL |
9-B-5. 지원 문법 (GFM)
| 마크다운 | HTML |
|---|---|
# ~ ###### | <h1>~<h6> |
**굵게** · *기울임* · ~~취소선~~ | <strong> · <em> · <del> |
`인라인 코드` · ```펜스``` | <code> · <pre><code> |
[텍스트](url) ·  | <a> · <img> |
- / * / 1. 목록 · - [x] 체크박스 | <ul>/<ol>/<li> · 체크박스 |
GFM 표(| … |, :--: 정렬) | <table> |
> 인용 · --- 구분선 | <blockquote> · <hr> |
9-B-6. 보안
<script> 삽입과 javascript:·data:text/html 링크는 자동 차단됩니다. viewMarkdown()·setMarkdown() 모두 동일하게 적용됩니다.9-B-7. 독립 뷰어 데모
마크다운_확장팩_API.md11. 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: 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>
게시판 파일 첨부 · 문서 미리보기 (데모 패턴)
게시글 작성 화면(에디터 하단)에 파일 첨부(드래그앤드롭·다중)를 붙이고, 첨부한 .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.