취업/자바스크립트

CKeditor 이미지 파일 외 업로드시 해당 알림창 출력하기.

카슈밀 2025. 11. 24. 16:36
반응형

심플업로드 아니고 커스텀이니 알아서 알아들어요.

extraPlugins는 적용안됩니다. 해당 내용을 적용하려면 업로드 시작에서 강탈해야하는데, 안그러면 에디터 자체에서 폐기해버려
아무 반응없음.

실행순서가 input -> click-> editor -> plugin 이라 너무 늦음.

해당 이벤트는 CKeditor onReady 자체에 적용해야합니다.
그래야 이벤트를 강탈 가능.

해당 라이브러리는 @ckeditor/ckeditor5-react를 사용함

// utils/ckeditor.ts

// MIME → 확장자 변환 (있으면 유지)
export function mimesToExts(mimes: string[]) {
  const mapping: Record<string, string> = {
    'image/jpeg': 'jpeg',
    'image/jpg': 'jpg',
    'image/png': 'png',
    'image/gif': 'gif',
    'image/webp': 'webp',
    'image/avif': 'avif',
    'image/bmp': 'bmp',
  };
  const exts = new Set<string>();
  mimes.forEach((m) => {
    const ext = mapping[m.toLowerCase()];
    if (ext) exts.add(ext);
  });
  if (exts.has('jpeg')) exts.add('jpg');
  return Array.from(exts);
}

/**
 * CKEditor 내부에서 생성되는 input[type="file"]을 가로채서
 * 이미지 외 파일 업로드를 막는 강제 검증 함수
 */
export function setupFileInputValidation(
  editor: any,
  allowedTypes: string[],
  onError?: (msg: string) => void
) {
  const editorRoot =
    editor.ui?.view?.element ||
    editor.sourceElement?.parentElement ||
    document.querySelector('.ck-editor');

  if (!editorRoot) {
    console.warn('⚠ 에디터 root 를 찾을 수 없음');
    return;
  }

  // file input에 검증 붙이는 함수
  const attachValidation = (input: HTMLInputElement) => {
    if (input.dataset.validationAttached === 'true') return;
    input.dataset.validationAttached = 'true';

    // accept 초기화 (CKEditor가 제한적으로 input을 생성하는 것을 방지)
    input.removeAttribute('accept');

    input.addEventListener(
      'change',
      (e) => {
        const files = (e.target as HTMLInputElement).files;
        if (!files || files.length === 0) return;

        const file = files[0];

        // 파일 MIME 체크
        if (!allowedTypes.includes(file.type)) {
          const allowedExts = mimesToExts(allowedTypes)
            .map((ext) => ext.toUpperCase())
            .join(', ');

          onError?.(
            `허용되지 않은 파일 형식입니다.\n파일: ${file.name}\n허용 형식: ${allowedExts}`
          );

          (e.target as HTMLInputElement).value = '';
          e.preventDefault();
          e.stopImmediatePropagation();
          return false;
        }
      },
      { capture: true }
    );
  };

  // MutationObserver: 동적으로 생성되는 input 캐치
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      mutation.addedNodes.forEach((node) => {
        if (node.nodeType !== Node.ELEMENT_NODE) return;

        const element = node as HTMLElement;
        const fileInputs: HTMLInputElement[] = [];

        if (element.matches('input[type="file"]')) {
          fileInputs.push(element as HTMLInputElement);
        }

        fileInputs.push(
          ...Array.from(element.querySelectorAll('input[type="file"]'))
        );

        fileInputs.forEach(attachValidation);
      });
    });
  });

  // 이미 존재하는 input에도 검증 추가
  const existingInputs = editorRoot.querySelectorAll('input[type="file"]');
  existingInputs.forEach(attachValidation);

  // DOM 변경 감지 시작
  observer.observe(editorRoot, {
    childList: true,
    subtree: true,
  });

  // editor destroy 시 정리
  editor.on('destroy', () => observer.disconnect());
}
728x90