기술지원 & FAQ

언제든지 도와드립니다

도입 전 기술 검토부터 운영 중 발생하는 이슈까지,
DEXTSOLUTION 기술팀이 빠르게 응답합니다.

문의 채널

아래 채널로 문의해 주시면 영업일 기준 1일 이내에 답변드립니다.

📄

라이선스 문의

도입 견적·라이선스 유형·볼륨 할인 등 구매 관련 문의

대표문의 02-6719-6200 · 02-6719-6219 · 02-6719-6202

🛠️

기술 지원

설치·연동·버그 리포트·API 사용법 등 기술적 문의

대표문의 02-6719-6200 · 02-6719-6219 · 02-6719-6202

📋

릴리즈 노트

버전별 변경사항·신규 기능·버그 수정 내역 확인

릴리즈 노트 보기

개발자 FAQ

설치부터 고급 기능까지 — 자주 묻는 기술 질문을 카테고리별로 정리했습니다.

설치 & 기본 설정

최소 필수 파일 2개<head><body> 닫는 태그 앞에 각각 로드합니다.

<!-- ① CSS (head 안에) -->
<link rel="stylesheet" href="dist/webeditor.min.css">

<!-- ② JS (body 닫기 전) -->
<script src="dist/webeditor.min.js"></script>

<!-- Word 가져오기가 필요한 경우만 추가 -->
<script src="dist/plugins/wordimport-native.min.js"></script>
jQuery, React, Vue 등 외부 라이브러리는 불필요합니다. 순수 JavaScript로 동작합니다.

HTML에 컨테이너 요소를 추가하고, JS에서 new WebEditor()를 호출합니다.

<!-- HTML -->
<div id="editor"></div>

<!-- JavaScript -->
<script>
const editor = new WebEditor('#editor', {
  height: 400,
  placeholder: '내용을 입력하세요...'
});
</script>

컨테이너는 div 이외에도 CSS 선택자 형태 '.my-editor' 또는 DOM 요소를 직접 전달할 수 있습니다.

번들러 환경(Webpack, Vite 등)에서는 ESM 번들을 사용합니다.

// ESM (권장)
import WebEditor from './dist/webeditor.esm.js';

// CommonJS
const WebEditor = require('./dist/webeditor.cjs.js');
TypeScript 자동완성을 위해 dist/webeditor.d.ts를 함께 배치하세요. tsconfig의 typeRoots 또는 /// <reference>로 참조합니다.
  • webeditor.min.css가 정상 로드되었는지 개발자 도구 → Network 탭에서 확인
  • 컨테이너 요소(#editor)가 DOM에 존재하는 시점 이후에 초기화했는지 확인 (DOMContentLoaded 이후 권장)
  • height 옵션이 숫자(px) 또는 문자열('400px')로 전달되었는지 확인
  • 부모 요소에 display:none이 적용된 경우 초기화 후 표시해도 높이가 0이 될 수 있습니다 → 표시 후 editor.setHeight(400) 재호출

destroy()를 호출하면 DOM을 원래 상태로 복원하고 모든 이벤트 리스너를 해제합니다.

editor.destroy();
// 이후 editor 변수를 null로 해제하는 것을 권장
editor = null;

SPA(Single Page Application) 환경에서 컴포넌트 언마운트 시 반드시 호출해 메모리 누수를 방지하세요.

가능합니다. 각 컨테이너마다 독립된 인스턴스를 생성합니다.

const editor1 = new WebEditor('#editor-1', { height: 300 });
const editor2 = new WebEditor('#editor-2', { height: 200, readOnly: true });

// 정적 API로 인스턴스 접근
const model0 = WebEditor.getAPIModelByIndex(0); // 첫 번째 인스턴스
const html = model0.getHTML();
라이선스 키는 인스턴스 옵션 license로 적용하거나, 전역 WebEditor.defaultLicense·window.WebEditorLicense로 한 번에 지정할 수 있습니다. (라이선스 탭 참고)
초기화 옵션

menubar: true로 기본 메뉴바를 활성화하거나, 배열로 표시할 메뉴 항목을 직접 지정합니다.

// 메뉴바 전체 표시
new WebEditor('#editor', { menubar: true });

// 특정 메뉴만 표시
new WebEditor('#editor', {
  menubar: ['file', 'edit', 'view', 'insert', 'format', 'table', 'tools']
});

툴바는 showToolbar(false) 메서드로 런타임에 숨기거나 표시할 수 있습니다.

기본적으로 줄바꿈하지 않고 한 줄을 유지하며, 넘치는 아이콘 그룹은 툴바 오른쪽 끝의 » 버튼 팝업에 담깁니다. 창을 다시 넓히면 자동으로 원래 자리로 복원됩니다. 접히는 단위는 구분선으로 나뉜 아이콘 그룹이라 관련 기능이 흩어지지 않습니다.

// 기본값 — 넘치는 아이콘은 » 팝업으로 접기
new WebEditor('#editor', { toolbarOverflow: true });

// 종전처럼 다음 줄로 줄바꿈시키려면
new WebEditor('#editor', { toolbarOverflow: false });

좁은 화면에서 아예 다른 아이콘 배열을 쓰고 싶다면 toolbarResponsive로 너비 구간별 레이아웃을 지정할 수 있으며, 두 옵션은 함께 사용할 수 있습니다.

plugins 옵션에 사용할 플러그인 이름 배열을 전달합니다.

// 내장 플러그인 선택 (9종)
new WebEditor('#editor', {
  plugins: ['format', 'image', 'table', 'link', 'emoji',
            'video', 'sourceview', 'insert', 'find']
});

// Word 가져오기 추가 (외부 번들 필요)
new WebEditor('#editor', {
  plugins: ['format', 'image', 'table', 'link',
            'emoji', 'video', 'sourceview', 'insert',
            'find', 'wordimport']
});

기본값은 <p> 단락 삽입입니다. <br>로 변경하려면:

new WebEditor('#editor', {
  enterKey: 'br'   // 기본값: 'p'
});
게시판·공지사항 환경에서는 'p' 모드를 권장합니다. 'br' 모드는 단순 메모 입력 등 단락 구조가 불필요한 경우에 적합합니다.
new WebEditor('#editor', {
  pastePlainText: true   // 모든 붙여넣기를 plain text로 처리
});

일회성 plain-paste가 필요한 경우 Ctrl+Shift+V 단축키를 사용할 수도 있습니다.

// 초기화 옵션으로 설정
new WebEditor('#editor', {
  defaultFont: 'Malgun Gothic',   // 기본: 'Malgun Gothic'
  defaultFontSize: 14             // 기본: 14 (pt 단위)
});

// 런타임에 변경
editor.setDefaultFont('나눔고딕');
editor.setDefaultFontSize(16);
new WebEditor('#editor', {
  hyperLinkDefaultTarget: '_blank'  // '_self' | '_blank' | '_top' | '_parent'
});

사용자가 링크 삽입 다이얼로그에서 target을 별도로 변경하지 않으면 이 값이 기본으로 적용됩니다.

콘텐츠 I/O & 메서드
document.getElementById('submit-btn').addEventListener('click', () => {
  const html = editor.getHTML();   // 본문 HTML 문자열

  // hidden input에 담아 전송
  document.getElementById('content-field').value = html;
  document.getElementById('my-form').submit();
});

// 순수 텍스트만 필요한 경우
const text = editor.getText();
// 초기화 후 setHTML 호출
const editor = new WebEditor('#editor', { height: 400 });

editor.on('ready', () => {
  editor.setHTML(savedHtmlFromServer);
  editor.resetModified();  // 수정 플래그 초기화 (변경 감지 오작동 방지)
});
resetModified()를 호출하지 않으면 setHTML 직후부터 isModified()가 true를 반환합니다.
if (editor.isEmpty()) {
  alert('내용을 입력해 주세요.');
  return;
}

// 또는 직접 체크
const html = editor.getHTML();
const text = editor.getText().trim();
if (!text) { /* 빈 상태 */ }

isEmpty()는 공백·줄바꿈·&nbsp; 등을 모두 무시하고 실질적인 콘텐츠가 있는지 판단합니다.

// 기본 사용 (script, on* 이벤트 제거)
const safeHtml = editor.getSafeHTML();

// 세부 옵션 지정
const safeHtml = editor.getSafeHTML({
  stripScript: true,
  allowedTags: ['p','br','strong','em','u','a','img','table','tr','td'],
  allowedAttrs: ['href','src','alt','width','height'],
  imageRewriter: (src) => {
    // base64 이미지 → CDN 경유 URL
    if (src.startsWith('data:')) return '/img/placeholder.png';
    return src;
  }
});
서버에서 DB 저장 전 또는 화면 렌더링 전에 반드시 서버 측 정제도 함께 적용하는 것을 권장합니다.
// 초기화 시 설정
const viewer = new WebEditor('#viewer', {
  height: 300,
  readOnly: true    // 키보드·마우스 입력 완전 차단
});
viewer.setHTML(savedContent);

// 런타임 전환
editor.setReadOnly(true);   // 잠금
editor.setReadOnly(false);  // 편집 허용
console.log(editor.isReadOnly()); // true/false

setter 계열 메서드는 모두 this를 반환하므로 체이닝이 가능합니다.

editor
  .setHTML('<p>안녕하세요.</p>')
  .setReadOnly(false)
  .setHeight(500)
  .setDefaultFont('나눔고딕')
  .focus();
// 탭 전환 — 숫자 인덱스와 이름 모두 지원
editor.setTab(1);          // 0=WYSIWYG, 1=HTML소스, 2=미리보기
editor.setTab('html');     // 'edit' | 'html' | 'preview' (대소문자·별칭 허용)

// 현재 탭 확인
const tabIndex = editor.getActiveTab();      // 0 | 1 | 2
const tabName  = editor.getActiveTab(true);  // 'edit' | 'html' | 'preview'

// 탭 변경 이벤트 수신 (체이닝 가능: setTab은 this 반환)
editor.on('tabChange', (index) => {
  console.log('탭 전환:', ['WYSIWYG', 'HTML', '미리보기'][index]);
});
이미지 & 파일 업로드

imageResize를 설정하면 지정한 너비를 넘는 이미지만 실제 픽셀을 줄여 재인코딩합니다(비율 유지, 확대는 안 함). 축소본이 본문 base64와 uploadHandler에 모두 반영되어 용량이 줄어듭니다.

new WebEditor('#editor', {
  imageResize: {
    maxWidth: 600,        // 600px 이하는 원본 그대로
    quality: 0.85,        // JPEG·WebP 품질
    formats: ['image/jpeg','image/png','image/webp'],
    onLargerResult: 'original'   // 결과가 더 크면 원본 사용
  }
});

editor.on('imageResized', e => console.log(e.originalWidth, '→', e.width));
애니메이션 GIF는 재인코딩 시 첫 프레임만 남으므로 자동으로 원본이 유지됩니다. 원본 형식을 유지해 PNG 투명도가 보존되고 사진의 EXIF 회전도 반영됩니다. 파일 선택·붙여넣기·드래그앤드롭 전 경로에 적용됩니다.
maxImageWidth와 다릅니다 — 그쪽은 화면에 보이는 크기(CSS)만 제한하고 업로드 파일은 원본 그대로입니다. 용량을 줄이려면 imageResize를 쓰세요.

uploadHandler 옵션에 비동기 함수를 등록합니다. 함수가 URL을 반환하면 base64 이미지가 해당 URL로 자동 교체됩니다.

new WebEditor('#editor', {
  uploadHandler: async (file, headers) => {
    const formData = new FormData();
    formData.append('file', file);

    const resp = await fetch('/api/upload', {
      method: 'POST',
      body: formData,
      headers: headers || {}  // CSRF 헤더 자동 포함
    });

    if (!resp.ok) throw new Error('업로드 실패');

    const json = await resp.json();
    return json.url;  // 반환값이 서버 URL로 교체됨
  }
});
new WebEditor('#editor', {
  // 방법 1: 쿠키에서 자동 추출
  csrfCookie: 'csrftoken',   // 쿠키명 지정

  // 방법 2: 헤더 직접 지정
  uploadHeaders: {
    'X-CSRF-Token': document.querySelector('meta[name=csrf-token]').content,
    'X-Custom-Header': 'value'
  },

  uploadHandler: async (file, headers) => {
    // headers 매개변수에 위 헤더가 자동 포함됨
    const resp = await fetch('/api/upload', {
      method: 'POST',
      body: new FormData()...,
      headers
    });
    return (await resp.json()).url;
  }
});
// 이미지가 에디터 DOM에 삽입된 직후 (base64 상태)
editor.on('imageInserted', ({ src, file, node }) => {
  console.log('삽입된 이미지:', src.substring(0, 30));
});

// uploadHandler 완료 후 URL 교체 직후
editor.on('imageUploaded', ({ src, file, node }) => {
  console.log('업로드 완료 URL:', src);  // 서버 URL
});
new WebEditor('#editor', {
  maxImageWidth: 800   // px 단위, 0이면 무제한 (기본값)
});

이미지 삽입 시 원본이 800px을 초과하면 자동으로 width="800"이 적용됩니다. height는 비율에 맞게 자동 계산됩니다.

이미지를 더블클릭하거나 선택 후 우클릭 메뉴에서 이미지 속성 다이얼로그를 열면 설정 가능합니다.

  • 캡션: <figure><img><figcaption> 구조로 생성
  • 하이퍼링크: <a href="..."><img></a> 래핑, target 속성 지원
// 코드로 이미지 삽입
editor.insertImage('https://example.com/image.jpg');
이벤트 시스템
// change 이벤트: 내용이 변경될 때마다 발생
editor.on('change', (html) => {
  // 디바운스 적용 권장 (예: 1초 지연)
  clearTimeout(window._saveTimer);
  window._saveTimer = setTimeout(() => {
    localStorage.setItem('draft', html);
  }, 1000);
});

// 또는 내장 AutoSave 사용
new WebEditor('#editor', {
  autoSave: {
    interval: 30000,       // 30초마다
    storageKey: 'draft',
    unloadWarning: true,   // 페이지 이탈 시 경고
    onSave: (html) => console.log('자동 저장됨')
  }
});
// 리스너 등록
const handler = (html) => console.log(html);
editor.on('change', handler);

// 특정 리스너 해제
editor.off('change', handler);

// 해당 이벤트의 모든 리스너 해제
editor.off('change');
  • ready — 에디터 초기화 완료
  • change(html) — 콘텐츠 변경
  • focus / blur — 포커스 획득/해제
  • input(e) — 키 입력 직후
  • paste(e) — 붙여넣기
  • selectionchange — 선택 영역 변경
  • tabChange(index) — 탭 전환 (0/1/2)
  • resize({width, height}) — 에디터 크기 변경
  • undo / redo — 실행 취소/다시 실행
  • imageInserted({src, file, node}) — 이미지 DOM 삽입 완료
  • imageUploaded({src, file, node}) — 이미지 업로드 URL 교체 완료
  • zoom({ratio}) — 확대/축소 변경
  • error({context, error}) — 내부 오류 (업로드 실패 등)
editor.on('error', ({ context, error }) => {
  console.error(`[${context}] 오류 발생:`, error.message);

  if (context === 'upload') {
    alert('이미지 업로드에 실패했습니다. 네트워크를 확인해 주세요.');
  }
});

context 값: 'upload', 'plugin', 'license', 'autosave'

라이선스

인스턴스 옵션 license(코드 입력)로 적용합니다. 상태는 콜백으로 확인합니다.

new WebEditor('#editor', {
  license: 'WED2-XXXX-XXXX-...',
  onLicenseValid:   (info)  => console.log('유효:', info.info.customer),
  onLicenseInvalid: (state) => console.warn('오류:', state.reason)
});

전역 라이선스: 페이지마다 license를 넘기지 않고, 한 곳에서 전역 키를 지정할 수 있습니다.

// 방법 1) 정적 프로퍼티
WebEditor.defaultLicense = 'WED2-XXXX-...';

// 방법 2) 전역 변수 — 설정 파일(webeditor-configuration.js)에서 1회 선언, 번들보다 먼저 로드
window.WebEditorLicense = 'WED2-XXXX-...';
우선순위: options.license > WebEditor.defaultLicense > window.WebEditorLicense. 키의 ECDSA 서명·도메인 검증은 어느 방식이든 동일하게 적용됩니다(보안 약화 없음).
  • 'unlicensed' — 키가 없거나 미설정
  • 'invalid' — 키 형식 오류 또는 서명 불일치
  • 'expired' — 라이선스 만료일 초과
  • 'mismatch' — 현재 도메인이 라이선스 허용 도메인에 포함되지 않음
  • 'domainKeyRequired' — 로컬 전용 키 'dextsolution'을 실도메인에서 사용 (해당 도메인 키 필요)
키가 없으면 localhost도 평가판입니다. 이전처럼 개발 호스트가 키 없이 자동 통과하지 않습니다. 또한 사설 IP(192.168.x 등)·내부 TLD(.local·.lan 등)는 개발 호스트에서 제외되어 키가 필요합니다. (개발 호스트 = localhost·127.0.0.1·::1·0.0.0.0·file: 만 해당)

로컬 개발(워터마크 없이): 개발 호스트에서는 정식 키 대신 로컬 전용 예약 키 'dextsolution'을 입력하면 됩니다. (실도메인·사설 IP에선 무효)

// 개발 호스트(localhost 등) 전용
new WebEditor('#editor', { license: 'dextsolution' });
// 또는 전역으로
window.WebEditorLicense = 'dextsolution';
  • 키 없음 → 개발 호스트 포함 어디서나 평가판(워터마크). 'dextsolution' 또는 정식 키를 입력하세요.
  • 정식 키는 localhost에서도 서명·만료는 검증하고 도메인 매칭만 면제 → 로컬에서 키 적용을 그대로 확인할 수 있습니다.
  • HTTP(비 HTTPS): ECDSA 서명 검증은 보안 컨텍스트(HTTPS·localhost)에서만 완전 동작합니다. 일반 HTTP에선 서명만 생략하고 도메인·만료는 그대로 검증하는 폴백으로 동작합니다(운영은 HTTPS 권장).

onLicenseValid 콜백의 info.info 또는 인스턴스의 editor._licenseState로 확인합니다.

new WebEditor('#editor', {
  license: 'WED2-...',
  onLicenseValid: (info) => {
    console.log(info.info);
    // { valid:true, customer:'ACME-001', domains:['example.com','*.example.com'],
    //   type:'perm', expiry:0, mode:'ecdsa' }
  }
});

// 인스턴스별 상태
const state = editor._licenseState;
// 정식 키(로컬):   { evaluation:false, devHost:true }
// 'dextsolution': { evaluation:false, localLicense:true }
// 키 없음:         { evaluation:true,  reason:'unlicensed' }

정식 라이선스 키를 발급받아 적용하면 자동으로 사라집니다. 개발/테스트 단계에서 임시로 비활성화하려면:

new WebEditor('#editor', {
  evaluationWatermark: false  // 워터마크 표시 비활성화 (기본값: true)
});
운영 환경에서 evaluationWatermark: false를 사용하는 것은 라이선스 계약 위반입니다. 반드시 정식 키를 구매 후 적용하세요.
고급 기능
const editor = new WebEditor('#editor', {
  autoSave: {
    interval: 30000,             // 저장 주기 (ms), 기본 60000
    storageKey: 'draft-post-1',  // localStorage 키
    unloadWarning: true,         // 페이지 이탈 시 경고 다이얼로그
    unloadMessage: '저장하지 않은 내용이 있습니다.',
    onSave: (html) => {
      console.log('자동 저장:', new Date().toLocaleTimeString());
    }
  }
});

// 임시 저장본 복구
const draft = editor.getAutoSaved();
if (draft && confirm('임시 저장된 내용을 복구하시겠습니까?')) {
  editor.setHTML(draft);
  editor.clearAutoSaved(); // 복구 후 초기화
}

// 수동 즉시 저장
editor.saveNow();

검출 결과를 본문 위 배너로 알리고 등록 자체를 막는 흐름은 guardBeforeSubmit() 한 번으로 처리합니다 — 아래 등록 직전 검사 항목을 보세요.

const editor = new WebEditor('#editor', {
  // 금칙어 설정
  profanity: {
    words: ['욕설1', '욕설2'],
    maskChar: '*',              // 마스킹 문자 (기본 '*')
    onDetect: (matches) => {
      console.log('금칙어 발견:', matches);
    }
  },

  // 개인정보 검출 패턴
  privacy: {
    detect: ['ssn', 'phone', 'email', 'credit-card', 'bank-account'],
    onDetect: (matches) => {
      console.warn('개인정보 검출:', matches);
    }
  }
});

// 저장 전 일괄 검증
document.getElementById('submit-btn').onclick = async () => {
  const result = await editor.validate();

  if (result.profanity.length > 0) {
    alert(`금칙어 ${result.profanity.length}건이 발견되었습니다.`);
    return;
  }
  if (result.privacy.length > 0) {
    if (!confirm('개인정보가 포함되어 있습니다. 계속하시겠습니까?')) return;
  }

  // 마스킹 처리 후 저장
  const maskedHtml = editor.maskSensitiveData();
  submitContent(maskedHtml);
};

guardBeforeSubmit() 은 검사 → 본문 위 배너 표시 → 등록 차단까지를 한 번에 처리합니다. 반환값의 ok:false 는 ‘호스트가 등록을 멈춰야 한다’는 뜻입니다.

const editor = new WebEditor('#editor', {
  privacy:   { detect: ['ssn', 'phone', 'email'] },
  profanity: { words: ['금칙어1'] },
  guard: {
    show:   'banner',   // 'banner' | 'notice'
    block:  true,       // 검출되면 ok:false 로 등록을 막는다
    review: 'confirm',  // 배너에 [검토·마스킹] 제공
  },
});

submitBtn.onclick = async () => {
  const r = await editor.guardBeforeSubmit();
  if (!r.ok) return;              // 배너는 에디터가 이미 띄운 상태
  submitContent(editor.getSafeHTML());
};
사용자가 ‘이건 개인정보가 아니다’라고 판단한 값은 allowGuardItems(items)이번 편집 중에만 검사에서 제외합니다 (getGuardAllowList() · clearGuardAllowList()). 판정은 종류가 아니라 기준이며 새로고침하면 사라집니다.
클라이언트 검사는 사용자 편의입니다. 브라우저에서 우회할 수 있으므로 서버에서 반드시 다시 검증하세요.

배경 보존 방식이 두 가지이기 때문입니다. 소형(10KB 미만)은 base64 로 본문에 함께 저장되지만, 업로드 방식(store:'meta' — 업로드 모드의 기본값)은 getHTML() 에 포함되지 않습니다.

// 저장 — 본문과 배경을 따로 보관한다
const html = editor.getHTML();
const bg   = editor.getDocumentBackground();   // { image, color, repeat, attachment, … } | null
save({ html, bg });

// 불러오기 — 본문을 넣고 배경을 다시 입힌다
editor.setHTML(saved.html);
if (saved.bg) await editor.setDocumentBackground(saved.bg);
store:'wrapper'(base64 기본)로 두면 getHTML()/setHTML() 만으로 자동 보존됩니다. 용량과 보존 편의 중 무엇을 택할지에 따라 documentBackground.store 를 지정하세요.
// 기본 미니툴바 (Bold, Italic, Underline, Link, 색상)
new WebEditor('#editor', {
  inlineToolbar: true
});

텍스트를 드래그로 선택하면 선택 영역 위에 미니툴바가 자동으로 표시됩니다. 모바일 터치 환경도 지원합니다.

// 확대/축소 설정 (0.25 ~ 4.0 범위)
editor.setZoom(1.5);   // 150%
editor.setZoom(0.75);  // 75%
editor.setZoom(1.0);   // 100% (원래 크기)

// 현재 배율 조회
const ratio = editor.getZoom(); // 예: 1.5

// 줌 변경 이벤트
editor.on('zoom', ({ ratio }) => {
  console.log('현재 배율:', Math.round(ratio * 100) + '%');
});
// 에디터 본문만 브라우저 인쇄 다이얼로그로 PDF 저장
editor.saveToPDF();
editor.saveToPDF({ title: '문서 제목', hideToolbar: true }); // 옵션

// 새 창에서 인쇄 (인쇄 후 창 자동 닫힘)
editor.saveToPDFInWindow();
툴바의 🖨 인쇄빨간 PDF 저장 버튼으로도 실행됩니다. 에디터가 컨테이너 안에 중첩 배치돼도 본문이 정상 출력되며, 인쇄 시 별도 미리보기 창 없이 본문만 출력됩니다(툴바·메뉴바·탭 바 제외). 서버 사이드 PDF가 필요하면 getHTML()로 추출해 Puppeteer, wkhtmltopdf 등을 활용하세요.
MS Word 가져오기

외부 플러그인 번들을 추가로 로드하고, plugins 옵션에 'wordimport'를 포함시킵니다.

<!-- 외부 번들 추가 로드 -->
<script src="dist/plugins/wordimport-native.min.js"></script>

<script>
new WebEditor('#editor', {
  plugins: ['format', 'image', 'table', 'link', 'emoji',
            'video', 'sourceview', 'insert', 'find', 'wordimport']
});
</script>

활성화 후 메뉴바의 파일 → Word 가져오기 또는 에디터 영역에 .docx 파일을 드래그하면 자동 변환됩니다.

// File 객체를 직접 전달
document.getElementById('file-input').addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file || !file.name.endsWith('.docx')) return;

  try {
    await editor.importWordFile(file);
    console.log('Word 가져오기 완료');
  } catch (err) {
    console.error('가져오기 실패:', err.message);
  }
});

변환 지원 항목:

  • 제목(H1~H6), 단락, 줄바꿈
  • 굵게·기울임·밑줄·취소선·글자색·형광펜
  • 표 (셀 배경색, 테두리색, 열 너비 포함)
  • 인라인 이미지 (EMU → px 변환, base64 삽입)
  • 하이퍼링크

미지원 항목:

  • SmartArt, 차트, OLE 개체
  • 복잡한 단 나누기(Multi-column) 레이아웃
  • 매크로, VBA 스크립트
.doc(구 포맷)은 지원하지 않습니다. 반드시 .docx 형식으로 저장 후 가져오세요.

메뉴바·툴바 없는 에디터를 새창에 만들고 importWordFile()로 변환한 뒤 setReadOnly(true)로 잠그면 편집 불가 미리보기가 됩니다. 게시판 .docx 첨부 미리보기에 쓰는 패턴입니다.

const ed = new WebEditor('#preview', {
  menubar: false,
  plugins: ['format','image','table','link','wordimport']
});
ed.showToolbar(false);                                   // 아이콘(툴바) 숨김
await ed.importWordFile(file, { silent: true, mode: 'replace' });
ed.setReadOnly(true);   // 편집·상단 메뉴바·컨텍스트 메뉴·리사이즈 모두 차단
읽기 전용(setReadOnly(true))은 상단 메뉴바·표/이미지 컨텍스트 메뉴·리사이즈까지 차단합니다. 통합 구현은 "확장·신규" 탭의 게시판 파일 첨부·문서 미리보기 참고.
한글 HWPX 가져오기

별도 외부 플러그인 번들을 로드하고, plugins 옵션에 'hwpximport'를 포함시킵니다(opt-in — 미지정 시 영향 0).

<!-- 외부 번들 추가 로드 -->
<script src="dist/plugins/hwpximport-native.min.js"></script>

<script>
new WebEditor('#editor', {
  plugins: ['format', 'image', 'table', 'link', 'hwpximport']
});
</script>

활성화 후 메뉴바의 파일 → 한글 HWPX 가져오기, 툴바의 HWPX 아이콘(MS Word 옆), 또는 에디터 영역에 .hwpx 파일을 드래그하면 변환됩니다.

const file = e.target.files[0];
if (!file || !file.name.endsWith('.hwpx')) return;

await editor.importHwpxFile(file, {
  mode: 'replace',      // 'cursor' | 'append' | 'replace'
  maxTableWidth: 740,   // 최상위 표를 지정 px 너비로 비율 스케일(권장 740~780)
  silent: false         // true면 변환 다이얼로그 생략
});

변환 지원 항목:

  • 단락·정렬·줄간격(lineSpacing), 굵게·기울임·밑줄, 글자색·크기·글꼴
  • 표 (셀 병합·테두리·단색/그라디언트 배경·셀 너비/높이·세로정렬)
  • 이미지(BinData → base64 인라인)
  • 벡터 도형(폴리곤·사각형 등) → 인라인 SVG
  • 심볼폰트 특수문자(PUA) 표준 유니코드 매핑
  • 목차 점선 리더(탭 leader)·페이지 나눔(pageBreak 및 목차 앞 자동 분리)
  • 도형 안 텍스트(hp:drawText) 추출

미지원 항목:

  • 수식, SmartArt·복합 그리기 그룹의 정밀 레이아웃
  • 머리글/꼬리글, 각주·미주 본문, 텍스트 감싸기(도형 주변 흐름), 도형 회전
한글 표준 XML 포맷 .hwpx(OWPML)만 대상입니다. 구 바이너리 .hwp는 지원하지 않습니다. 미지원 요소는 텍스트 보존 + 경고로 처리됩니다. 상세는 개발 문서 docs/HWPX_가져오기_개발문서.md 참고.

Word와 동일하게, 메뉴바·툴바 없는 에디터에 importHwpxFile()로 변환한 뒤 setReadOnly(true)로 잠급니다.

const ed = new WebEditor('#preview', {
  menubar: false,
  plugins: ['format','image','table','link','hwpximport']
});
ed.showToolbar(false);
await ed.importHwpxFile(file, { silent: true, mode: 'replace', maxTableWidth: 740 });
ed.setReadOnly(true);   // 편집·메뉴바·컨텍스트 메뉴 차단
읽기 전용 미리보기에서도 목차 점선 리더·페이지 나눔·표 그라디언트가 그대로 렌더됩니다. 통합 구현은 "확장·신규" 탭의 게시판 파일 첨부·문서 미리보기 참고.
메뉴 UI · 붙여넣기

메뉴 UI를 클래식 ↔ 탭 리본으로 전환하고, 아이콘 테마를 툴바·리본 공통으로 적용합니다.

new WebEditor('#editor', {
  uiTemplate: 'ribbon',          // 'classic' | 'ribbon'
  iconTheme:  'blue',            // default·gray·blue·colorful·vivid·natural·emoji 또는 폴더명
  iconThemePath: 'assets/icon-themes'
});

// 런타임 전환
editor.setUITemplate('classic');
editor.setIconTheme('vivid');    // 툴바 + 리본 탭 함께 갱신
폴더 이미지 테마(iconTheme:'blue' 등)는 아이콘을 외부 SVG로 로드하므로 assets/icon-themes/<테마>/ 폴더를 함께 배포해야 합니다(기본 default는 번들 내장).

메뉴·리본의 붙여넣기 버튼은 비동기 Clipboard API(pasteFromClipboard())를 사용합니다. 보안 컨텍스트(HTTPS 또는 localhost) + 사용자 제스처 + 클립보드 권한이 필요합니다.

const ok = await editor.pasteFromClipboard();
if (!ok) console.warn('클립보드 접근 불가 — Ctrl+V를 사용하세요');
HTTP(비보안) 환경이거나 권한이 거부되면 false를 반환하고 Ctrl+V 안내로 폴백합니다. 브라우저 정책상 코드로 강제 붙여넣기는 불가합니다.
확장·신규 기능

마크다운 확장팩(opt-in)을 로드하고 plugins'markdown'을 추가하면 마크다운 ↔ 본문 왕복 변환이 가능합니다.

<script src="dist/plugins/markdown-native.min.js"></script>

const editor = new WebEditor('#editor', {
  plugins: ['format','image','table','link','markdown']
});

await editor.importMarkdownFile(file, { mode: 'replace' }); // .md 파일 → 본문
editor.setMarkdown('# 제목\n\n**굵게**', { mode: 'replace' }); // 문자열 → 본문
const md = editor.getMarkdown();            // 본문 → 마크다운
const { html } = editor.viewMarkdown(md);   // 마크다운 → 안전 HTML
툴바 버튼·.md 드래그앤드롭으로도 가져올 수 있으며, <script>·javascript: 링크는 자동 차단됩니다. 확장팩 미지정 시 코어에 영향이 없습니다.

툴바 이미지 버튼이 파일 업로드 + URL 입력 다이얼로그로 개편되었습니다. 코드로는 insertImage()를 사용합니다.

editor.insertImage('https://example.com/photo.png', '대체 텍스트');
보안: javascript:·vbscript:·비이미지 data: 스킴은 자동 차단됩니다.

설정 허브에서 툴바 아이콘(표시·순서·1·2·3단 배치·구분선)·메뉴바 구조·너비·상태표시줄을 시각적으로 구성하고 config 파일로 저장할 수 있습니다. 저장한 config는 에디터 옵션과 동일한 형식입니다.

new WebEditor('#editor', {
  menubar:   savedConfig.menubar,   // 메뉴바 구성
  toolbar:   savedConfig.toolbar,   // 툴바 아이콘 배열
  statusbar: savedConfig.statusbar,
  width:     savedConfig.width
});
설정 페이지: 설정 허브 · 아이콘·툴바 UI 설정 · UI 설정 적용 데모 · 메뉴바 설정 · 메뉴 UI(클래식·리본). 자세한 형식은 docs/UI설정_config_가이드.md 참고.

공용 모듈 doc-tools.js를 로드하고 DeeDocTools.attach(editor) 한 줄이면 파일 ▸ 템플릿(기본 양식 11종)·파일 ▸ 레이아웃(표 구조 16종)이 메뉴에 추가됩니다.

<script src="doc-tools.js"></script>
DeeDocTools.attach(editor);
템플릿 목록은 templates/ 폴더에 HTML을 넣으면 자동 인식되며, 정적 호스팅은 make-templates-manifest.js로 목록을 갱신합니다.

데모 게시판은 첨부파일을 localStorage에 data URL로 저장하고, 첨부한 .docx·.hwpx·.md메뉴바·아이콘바 없는 에디터 새창에서 미리보기로 import합니다.

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

// data URL → File 복원 후 형식별 import
if (/\.hwpx$/i.test(name))                ed.importHwpxFile(file, { silent:true, mode:'replace' });
else if (/\.(md|markdown)$/i.test(name))  ed.importMarkdownFile(file, { mode:'replace' });
else                                      ed.importWordFile(file, { silent:true, mode:'replace' });
실제 서비스는 localStorage 대신 파일 업로드 API로 교체하세요. 구현 패턴은 개발 매뉴얼 §14 "게시판 파일 첨부·문서 미리보기" 참고.

모바일 데모(demo/mobile.html)는 핵심 아이콘 1줄 + 더보기(⋯) 바텀시트 구성을 제공하며, 공통 상단 메뉴는 좁은 화면(≤900px)에서 햄버거 토글 드롭다운으로 전환됩니다.

툴바는 toolbarResponsive 옵션으로 너비 구간별 배열을 지정할 수 있습니다.
문서 가져오기 통합 · UI

확장팩(opt-in) 번들을 로드하고 plugins'xlsximport'·'pptimport'를 추가합니다. 엑셀은 시트 → 표(table)로, 파워포인트는 슬라이드 → 본문으로 순차 변환됩니다.

<script src="dist/plugins/xlsximport-native.min.js"></script>
<script src="dist/plugins/pptimport-native.min.js"></script>

const editor = new WebEditor('#editor', {
  plugins: ['format','image','table','link','xlsximport','pptimport']
});

editor.importXlsxFile(file, { mode:'replace', maxTableWidth: 740 });
editor.importPptxFile(file, { mode:'replace' });
엑셀은 병합 셀·셀 배경·테두리·정렬·열 너비 비율·다중 시트를, 파워포인트는 텍스트박스·글꼴·색상·표·이미지를 반영합니다. 외부 라이브러리 없이 브라우저 내장 DecompressionStream으로 처리합니다.

importMode 옵션으로 형식별 버튼(기본)통합 버튼 1개 중에서 선택합니다. 통합 버튼은 파일 확장자를 보고 알맞은 엔진으로 자동 분기합니다.

new WebEditor('#editor', {
  importMode: 'unified',   // 'individual'(기본) | 'unified'
  plugins: ['format','image','table','link',
            'wordimport','hwpximport','xlsximport','pptimport']
});

// 코드로 직접 호출해도 확장자에 따라 자동 분기
editor.importDocumentFile(file, { mode: 'replace' });
'individual'은 W·한·X·P 아이콘이 각각 표시되고, 'unified'문서 가져오기 아이콘 1개만 표시됩니다. 툴바 배열(toolbar)에 'importDocument'를 직접 넣으면 importMode와 무관하게 통합 버튼을 배치할 수 있습니다.

fixedHeight를 켜면 본문이 길어져도 에디터 창 높이가 늘어나지 않고 편집영역에 세로 스크롤바가 생깁니다. 페이지 레이아웃을 고정해야 하는 화면에 사용합니다.

new WebEditor('#editor', {
  height: 500,
  fixedHeight: true        // 기본 false(내용에 따라 창이 늘어남)
});

editor.setFixedHeight(false);   // 런타임 전환
fixedHeight:false(기본)는 기존 동작과 동일하므로 업그레이드 시 영향이 없습니다.

toolbarOverflow(기본 true)가 켜져 있으면 툴바 폭을 넘는 아이콘이 다음 줄로 내려가지 않고 우측 »(더 보기) 팝업으로 접힙니다. false로 두면 기존처럼 줄바꿈됩니다.

new WebEditor('#editor', {
  toolbarOverflow: true    // true(기본)=» 접힘 / false=줄바꿈
});
구분선('|') 없이 긴 명령 배열을 지정해도 들어가는 만큼은 표시되고 넘치는 만큼만 접힙니다. 너비 구간별로 아예 다른 툴바 배열을 쓰려면 toolbarResponsive를 함께 사용하세요. 라이브 데모: 아이콘·툴바 UI 설정 페이지 하단의 좁은 폭 미리보기.

됩니다. 이미지를 클릭해 선택한 뒤 Ctrl+C(복사)·Ctrl+X(잘라내기) 후 원하는 위치에 Ctrl+V로 붙여넣습니다. 이때 조절한 이미지 크기(가로·세로)가 그대로 유지됩니다.

이미지 플러그인('image')이 로드된 경우에 동작합니다. 표 셀 안에 붙여넣은 이미지는 셀 폭을 넘지 않도록 자동으로 맞춰집니다.

개선되었습니다. 붙여넣기 경로가 execCommand('insertHTML')(HTML 문자열을 다시 직렬화·재파싱)에서 파싱된 노드를 커서 위치에 직접 삽입하는 방식으로 바뀌어, 대용량 본문에서 체감 속도가 크게 향상되었습니다.

별도 설정 없이 적용됩니다. 이미지·표가 포함된 붙여넣기에서 항목이 두 번 삽입되던 문제도 함께 수정되었습니다.

도형 확장팩(opt-in)을 plugins에 추가하면 툴바에 도형 삽입 아이콘이 표시됩니다.

new WebEditor('#editor', {
  plugins: ['format','image','table','link','shape']
});
삽입한 도형은 선택 후 크기 조절·이동이 가능하며, 본문 HTML에 함께 저장됩니다.

두 증상 모두 수정되었습니다. dist를 최신으로 교체한 뒤 강력 새로고침(Ctrl+F5)하세요.

  • 표 테두리 겹침 — 편집 모드의 표 안내선(showTableGuide)이 테두리가 있는 셀에도 겹쳐 그려지던 문제. 이제 테두리 없는 셀에만 안내선이 표시됩니다.
  • 셀 이미지 넘침 — 표 셀에 삽입한 이미지가 셀 폭을 초과하던 문제. 셀 안 이미지는 셀 폭 기준으로 맞춰집니다.
코드 변경 없이 dist/webeditor.js(또는 .min.jsdist/webeditor.css 교체만으로 반영됩니다.
다국어(i18n)

language 옵션 하나면 됩니다. 한국어(ko)·영어(en)가 코어에 내장되어 있어 별도 파일을 추가로 로드하지 않습니다.

const editor = new WebEditor('#editor', { language: 'en' });

적용 범위는 UI 전체입니다 — 메뉴바 · 리본 · 툴바 툴팁 · 모든 플러그인 다이얼로그(표·도형·이미지·링크·찾기·동영상·이모지·문서배경·금칙어 검사) · 가져오기 다이얼로그(Word · HWPX · XLSX · PPTX · 마크다운).

본문에 입력하거나 가져온 문서 내용은 번역 대상이 아닙니다. UI 문구만 바뀝니다.

영향 없습니다. language를 지정하지 않으면 'ko'로 고정되며 렌더 결과는 이전 버전과 완전히 동일합니다.

브라우저·OS 언어를 따라가게 하려면 'auto'명시적으로 지정해야 합니다.

new WebEditor('#editor');                      // ko (기존과 동일)
new WebEditor('#editor', { language: 'auto' }); // 감지: <html lang> → navigator.language
자동 감지를 기본값으로 두지 않은 이유 — 그렇게 하면 영어 OS를 쓰는 국내 사용자의 화면이 업그레이드만으로 갑자기 영어로 바뀝니다. 자동 감지는 opt-in입니다.

setLanguage(lang)로 즉시 전환합니다. 본문 내용 · 커서 위치 · 변경 플래그 · 읽기 전용 상태는 그대로 보존되므로 작성 중에 눌러도 안전합니다.

editor.setLanguage('en');
editor.getLanguage();   // 'en'

// 전환 완료 시점 감지
editor.on('languagechange', (lang) => {
  console.log('전환 완료:', lang);
});
전환은 인스턴스 단위입니다. 한 페이지에 에디터가 여러 개면 각각 호출하세요. 열려 있던 팝업·다이얼로그는 닫힙니다.

WebEditor.registerLocale()코어 수정 없이 추가합니다. 언어팩은 평탄한 '네임스페이스.키' → 문자열 맵입니다.

// 팩이 없는 언어를 새로 만들 때 (예: 프랑스어)
WebEditor.registerLocale('fr', {
  'menu.file': 'Fichier',
  'menu.edit': 'Édition',
  'core.placeholder': 'Saisissez votre contenu.',
});
new WebEditor('#editor', { language: 'fr' });

// 일본어는 만들 필요가 없습니다 — 636키를 채운 팩이 동봉돼 있습니다
<script src="dist/webeditor.min.js"></script>
<script src="dist/locales/ja.min.js"></script>
new WebEditor('#editor', { language: 'ja' });
부분 팩도 안전합니다 — 누락된 키는 자동으로 한국어로 폴백하므로, 자주 보이는 문구부터 점진적으로 번역해 나갈 수 있습니다. 단, 에디터를 만들기 전에 호출해야 합니다.

merge: true로 기존 언어팩에 필요한 키만 덮어씁니다.

WebEditor.registerLocale('ko', {
  'menu.file': '문서',
  'core.placeholder': '공지 내용을 입력하세요.',
}, { merge: true });
merge: true빠뜨리면 언어팩이 통째로 교체되어, 지정하지 않은 문구가 전부 폴백 처리됩니다. 일부만 바꿀 때는 반드시 켜세요.

키 이름은 src/i18n.js(코어)와 src/plugins/_shared/ImportLocale.js(가져오기 확장팩)에서 확인합니다. 현재 ko·en·ja 각 636개입니다.

가져오기 확장팩의 문구(imp.*)는 코어 사전이 아니라 확장팩 번들이 자체 보유하다가, 스크립트가 로드되는 시점에 코어 사전으로 병합됩니다. 확장팩을 쓰지 않는 프로젝트가 그 용량을 지지 않게 하기 위한 구조입니다.

따라서 imp.* 문구를 덮어쓰려면 확장팩 스크립트를 로드한 뒤 호출해야 합니다.

<script src="dist/webeditor.min.js"></script>
<script src="dist/plugins/wordimport-native.min.js"></script>
<script>
  // ↑ 확장팩 로드 이후여야 병합이 유지된다
  WebEditor.registerLocale('ko', { 'imp.ok': '불러오기' }, { merge: true });
</script>

dist/webeditor.d.ts에 모두 포함되어 있습니다.

import WebEditor, { WebEditorLanguage, WebEditorLocalePack } from './dist/webeditor';

const ed = new WebEditor('#editor', { language: 'en' });
ed.setLanguage('ko');                       // → this (체이닝 가능)
ed.on('languagechange', (lang) => { /* lang: string */ });

const pack: WebEditorLocalePack = { 'menu.file': 'Fichier' };
WebEditor.registerLocale('fr', pack);       // → boolean

UI는 전부 번역됩니다. 아래는 설계상 범위 밖입니다.

  • 본문 콘텐츠 — 사용자가 입력했거나 가져온 문서의 내용
  • 날짜/시간 삽입 형식 — 형식 목록이 현재 한국어 표기 기준입니다(언어별 기본 포맷 미지원)
  • 평가판 모달 — 별도 옵션 licenseLang을 따릅니다
  • 데모·매뉴얼 페이지 — 문서 페이지 자체는 한국어입니다
화면에 [some.key] 형태의 대괄호 문자열이 보인다면 해당 키가 언어팩에 없다는 뜻입니다. registerLocale()로 그 키를 채우면 해결됩니다.
엑셀(XLSX) 그리기 개체

가져옵니다. 이전에는 시트 위에 떠 있는 개체가 안내 없이 모두 사라졌지만, 이제 본문으로 변환됩니다. 별도 설정 없이 기본 동작입니다.

  • 그림 — 표시 크기 그대로 base64 <img> 로 삽입됩니다. maxTableWidth 를 넘으면 비율을 유지한 채 줄입니다.
  • 도형 — 도형 안의 글자를 문단으로 보존합니다.
  • SmartArt — 배치가 끝난 결과를 도형·좌표·회전·테마 색까지 살려 SVG 근사 렌더합니다.
  • 차트 — 원·도넛·막대·꺾은선·영역형을 SVG 로 그리며, 차트 제목·축 제목·값 축 눈금·가로 격자선을 함께 그립니다.
그리기 개체는 특정 셀에 속하지 않고 시트 위에 떠 있어, 표 아래에 순서대로 배치됩니다.

조용히 버리지 않고 종류별로 세어 경고로 알려줍니다. 무엇이 빠졌는지 가져온 직후에 확인할 수 있습니다.

미지원 범위는 다음과 같습니다.

  • 분산형·버블·방사형·주식·혼합(combo)·3D 차트
  • 보조축, 로그 스케일
  • 양식 컨트롤(목록 상자·체크박스 등)

영역형 차트는 꺾은선으로 근사해 그립니다.

images: false 로 그리기 개체 변환을 통째로 끕니다.

editor.importXlsxFile(file, { silent: true, images: false });

의도된 동작입니다. 이전에는 시트가 하나면 팝업을 건너뛰고 바로 삽입했는데, 그러면 삽입 방식을 고를 기회가 없어 작성 중이던 본문이 의도치 않게 바뀝니다. 이제 시트 수와 관계없이 팝업이 뜹니다.

삽입 방식 기본값도 replace(전체 교체) → cursor(커서 위치에 삽입) 로 바뀌었습니다. Word·PPTX·HWPX 가 이미 cursor 였고 XLSX 만 달랐습니다.

// 팝업 없이 바로 가져오려면 silent
editor.importXlsxFile(file, { silent: true, mode: 'cursor' });

// 팝업은 띄되 다른 방식을 미리 선택해 두려면
editor.importXlsxFile(file, { defaultMode: 'replace' });
우클릭 메뉴

있습니다. 우클릭 메뉴에 하이퍼링크 항목이 추가되었습니다. 글자 범위를 선택했을 때만 활성화되며, 선택한 문자열이 링크 텍스트가 됩니다.

link 플러그인을 로드하지 않은 구성에서는 항목 자체가 표시되지 않습니다.

해결되었습니다. 예전에는 우클릭 메뉴 구현이 표 플러그인 안에 들어 있어, plugins 에서 table 을 빼면 잘라내기·복사·붙여넣기까지 통째로 없어졌습니다.

이제 기본 메뉴는 코어 모듈(editor.contextMenu)이 가지고 있고, 표 전용 메뉴는 표 플러그인이 상황별로 덮어씁니다. 표 메뉴의 항목과 동작은 이전과 동일합니다.

editor.contextMenu.addProvider() 로 등록합니다. 상황을 보고 항목 배열을 돌려주면 그 메뉴가 뜨고, null 을 돌려주면 다음 제공자에게 넘깁니다. 나중에 등록된 제공자가 먼저 불립니다.

const off = editor.contextMenu.addProvider((ctx) => {
  // ctx: { event, target, editor, hasTextSel }
  if (!ctx.target.closest('img')) return null;   // 이 상황이 아니면 기본 메뉴로
  return [
    { label: '대체 텍스트 편집', action: () => openAltDialog(ctx.target) },
    { sep: true },
    { label: '이미지 교체', action: () => replaceImage(ctx.target) },
  ];
});

off();   // 등록 해제
항목 형식: { label, icon, disabled, action } · 구분선은 { sep: true }. 반환값을 { items, className, onAfterAction } 형태로 주면 메뉴에 별도 CSS 클래스를 입힐 수 있습니다.
기타 개선·연동

수정되었습니다. 이전 구현은 선택 영역을 평문으로 뽑아 통째로 덮어써 안에 있던 기울임·굵게·하이퍼링크·글자색이 모두 초기화됐습니다.

이제 텍스트 노드의 글자만 바꾸므로 서식과 요소 구조가 그대로 유지되며, 선택 영역도 보존되고 실행 취소도 정상 동작합니다.

모바일 브라우저의 제약입니다. 안드로이드 크롬은 <iframe>·<object> 로 넣은 PDF 를 렌더하지 않고, iOS 사파리는 인라인 PDF 를 첫 페이지만 그립니다. 브라우저 자체 동작이라 외부 PDF 라이브러리 없이는 우회할 수 없습니다.

데모 게시판은 빈 화면을 보여주는 대신, 이유를 설명하고 다운로드해서 보기를 주 버튼으로 안내합니다. 두 플랫폼 모두 내려받은 PDF 는 뷰어에서 전체 페이지가 정상 표시됩니다.

iPadOS 13 이상은 기본이 데스크톱 UA 라 navigator.maxTouchPoints 를 함께 봐야 태블릿으로 판정됩니다.

데모 게시판(작성·보기)에서 수정되었습니다. 파일명이 길면 확장자는 남기고 앞부분만 줄임표로 축약하며, 버튼은 줄지 않도록 고정했습니다.

근본 원인은 flex 아이템의 min-width: auto 기본값입니다 — 내용보다 작게 줄지 않아 컨테이너가 통째로 늘어납니다. 직접 만드는 화면에서도 컨테이너에 min-width: 0 을 주어야 합니다.

가능합니다. WebBrowser 컴포넌트에 호스트 HTML 을 올리고, 양방향 통신은 다음으로 합니다.

  • 넥사크로 → 에디터: WebBrowser.callMethod('함수명', 인자…) — 동기적으로 반환값을 받습니다(동일 출처 필수).
  • 에디터 → 넥사크로: window.NEXACROWEBBROWSER.postUserNotify(...)onusernotify 이벤트의 info.userdata.
준비 시점NEXACROWEBBROWSER 는 iframe 의 load 시점에 주입되므로, 문서가 이미 준비된 뒤에 들어올 수 있습니다. 준비 신호를 한 번만 쏘고 버리는 방식으로 구현하면 놓칩니다 — 플래그를 유지하거나 폴링하세요. 또한 V24 에는 NEXACROHTML 이 존재하지 않습니다.
상세 연동 가이드와 동작하는 호스트 페이지 예제를 별도로 제공합니다. 기술지원팀으로 요청하세요.

업무 보고서 · 기안문 · 공지사항 · 공문/안내문 · 회의록 · 출장신청서 · 휴가신청서 · 개인경비 청구서 · 지출결의서 · 구매 및 지출품의서 · 차량배차신청서 · 이력서 — 총 12종입니다.

사내 게시판 공지용 공지사항 양식이 추가되었습니다(대상·기간·장소·방법 표 · 주요 사항 · 유의사항 · 문의처). 대외 발송용인 기존 공문/안내문 과는 별개 양식입니다.

양식 추가는 demo/templates/ 폴더에 HTML 문서를 넣기만 하면 됩니다. 폴더 목록을 막아둔 정적 호스팅에서는 node make-templates-manifest.jstemplates.json 을 갱신하세요.

해결되지 않는 문제는 기술 지원팀에 직접 문의해 주세요.

📋 이메일 주소 복사 개발 매뉴얼 보기

대표문의 02-6719-6200 · 02-6719-6219 · 02-6719-6202