클립보드 이미지 paste 구현

1rock·2025년 1월 30일

클립보드 붙여넣기 관리 Hook 구현기

웹 에디터에서 클립보드로부터 텍스트, 이미지, HWP 파일, Word 파일 등을 붙여넣을 때, 각각의 데이터 형식에 맞게 적절히 처리하는 Hook을 구현했습니다. 이번 글에서는 그 과정과 코드를 정리해보겠습니다.


목차

  1. 개요
  2. 주요 기능
  3. 코드 설명
  4. 사용 예시
  5. 결론

1. 개요

웹 에디터에서 사용자가 클립보드로부터 텍스트, 이미지, HWP 파일, Word 파일 등을 붙여넣을 때, 각각의 데이터 형식에 맞게 적절히 처리해야 합니다. 이를 위해 클립보드 데이터를 분석하고, 해당 데이터를 웹 에디터에 적절히 삽입하는 Hook을 구현했습니다.


2. 주요 기능

  • 텍스트 붙여넣기: 일반 텍스트를 붙여넣을 때, 불필요한 <br> 태그를 제거하고 텍스트를 삽입합니다.
  • 이미지 붙여넣기: 클립보드에 이미지가 있을 경우, 이미지를 Base64로 변환하여 웹 에디터에 삽입합니다.
  • HWP 파일 붙여넣기: HWP 파일에서 복사한 데이터를 파싱하여 텍스트와 이미지를 분리하여 삽입합니다.
  • Word 파일 붙여넣기: Word 파일에서 복사한 데이터를 텍스트로 변환하여 삽입합니다.

3. 코드 설명

3.1. paste 함수

paste 함수는 클립보드 데이터를 처리하는 주요 함수입니다. 이 함수는 클립보드의 items와 현재 선택 영역(selObj, range)을 인자로 받아 데이터를 처리합니다.

const paste = (items, selObj: Selection, range: Range) => {
    // ...
};

3.2. brRemover 함수

이 함수는 현재 커서 위치에서 불필요한 <br> 태그를 제거합니다.

const brRemover = () => {
    const ancestorContainer = range.commonAncestorContainer as HTMLElement;
    const checkBRTag = [...ancestorContainer.childNodes].find((e) => {
        const tag = e as HTMLElement;
        return tag.outerHTML === '<br>' || tag.outerHTML === '<br />';
    });
    if (checkBRTag) {
        ancestorContainer.querySelectorAll('BR').forEach((e) => e.remove());
    }
};

3.3. fnReadBlobAsync 함수

이 함수는 이미지 파일을 Base64로 변환합니다.

const fnReadBlobAsync = async (blob): Promise<ArrayBuffer | string | null> => {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => resolve(reader.result);
        reader.onerror = reject;
        reader.readAsDataURL(blob);
    });
};

3.4. fnAddImage 함수

이 함수는 이미지를 웹 에디터에 삽입합니다.

async function fnAddImage([filename, data]) {
    return new Promise<void>((resolve) => {
        const imgSelObj = document.getSelection() || new Selection();
        const imgRange = selObj.getRangeAt(0);
        const nameTag = document.createElement('div');
        nameTag.textContent = filename;

        const i = new Image();
        i.onload = async () => {
            const span = document.createElement('span');
            span.classList.add('input-tag');
            span.contentEditable = 'false';
            const imgSpan = document.createElement('span');
            imgSpan.classList.add('input-image');
            const imgTag = document.createElement('img');

            imgTag.src = data;
            imgTag.style.width = `${i.width}px`;
            imgTag.style.height = `${i.height}px`;
            imgSpan.append(imgTag);
            span.append(imgSpan);

            brRemover();
            await imgRange.insertNode(span);
            imgSelObj.collapseToEnd();
            resolve();
        };
        i.src = data;
    });
}

3.5. getHwpCommentsFromHTML 함수

이 함수는 HWP 파일에서 복사한 데이터를 파싱합니다.

const getHwpCommentsFromHTML = (htmlString: string) => {
    const commentPattern = /<!--\[data-hwpjson\]([\s\S]*?)-->/g;
    const matches = htmlString.match(commentPattern);
    if (matches) {
        const comments = matches.map((match) => match.replace('<!--[data-hwpjson]', '').replace('-->', '').trim());
        return comments;
    }
    return [];
};

3.6. addHwpData 함수

이 함수는 HWP 파일에서 복사한 데이터를 재귀적으로 처리하여 텍스트와 이미지를 분리합니다.

const addHwpData = (hwpArr: hwpDataType[], tag: HTMLElement, html: Document, hwpComments: string[]) => {
    if (tag.childNodes.length > 0) {
        for (let i = 0; i < tag.childNodes.length; i += 1) {
            const tagItem = tag.childNodes.item(i) as HTMLElement;
            const hwpLine = [...html.querySelectorAll('body > p')];
            hwpLine.shift();
            const checkingLineNumber = hwpLine.includes(tagItem);
            if (checkingLineNumber) {
                const val = {
                    type: 'br',
                    value: 'BR',
                };
                hwpArr.push(val);
            }
            if (tagItem.nodeName === 'SPAN') {
                const val = {
                    type: 'text',
                    value: tagItem.innerText,
                };
                hwpArr.push(val);
            }
            if (tagItem.nodeName === 'IMG') {
                html.querySelectorAll('img').forEach((imgTag, imgIdx) => {
                    if (imgTag === tagItem) {
                        const { bidt } = JSON.parse(hwpComments.join());
                        const images = Object.entries(bidt);
                        const imgUrl = images[imgIdx][1] as string;
                        const val = {
                            type: 'image',
                            value: imgUrl,
                        };
                        hwpArr.push(val);
                    }
                });
            }
            addHwpData(hwpArr, tagItem, html, hwpComments);
        }
    }
};

3.7. fnAddText 함수

이 함수는 텍스트를 웹 에디터에 삽입합니다.

const fnAddText = (textValue: string) => {
    const textSelObj = document.getSelection() || new Selection();
    const textRange = selObj.getRangeAt(0);

    const txt = document.createTextNode(textValue);

    brRemover();

    textRange.insertNode(txt);
    textSelObj.collapseToEnd();
};

3.8. run 함수

이 함수는 클립보드 데이터를 분석하여 적절한 처리를 수행합니다.

const run = async () => {
    let blob;
    for (let i = 0; i < items.length; i += 1) {
        if (items.length === 1 && items[i].type.startsWith('image')) {
            blob = items[i].getAsFile() as File;
            const data: ArrayBuffer | string | null = await fnReadBlobAsync(blob);
            const name = blob !== null ? blob.name : '';
            fnAddImage([name, data]);
        } else if (items[i].type.startsWith('text')) {
            const cb = ((type, length) => {
                return async (str: string) => {
                    if (type === 'text/html') {
                        try {
                            const html = new DOMParser().parseFromString(str, 'text/html');
                            const hwpComments = getHwpCommentsFromHTML(str);
                            const wordComments = getWordCommentsFromHTML(str);

                            if (hwpComments.length) {
                                const hwpArr: hwpDataType[] = [];
                                addHwpData(hwpArr, html.body, html, hwpComments);

                                for (const hwpData of hwpArr) {
                                    if (hwpData.type === 'text') {
                                        fnAddText(hwpData.value);
                                    } else if (hwpData.type === 'image') {
                                        const mimeType = 'image/png';
                                        let data;
                                        if (!hwpData.value.startsWith('data:'))
                                            data = `data:${mimeType};base64, ${hwpData.value}`;
                                        await fnAddImage(['fromPolaris', data]);
                                    } else if (hwpData.type === 'br') {
                                        const div = document.createElement('DIV');
                                        div.innerHTML = '<br />';
                                        range.commonAncestorContainer.parentElement?.append(div);
                                        selObj.collapse(div);
                                    }
                                }
                            } else if (wordComments) {
                                fnAddText(html.body.textContent || '');
                            } else {
                                const cbImg = html.querySelector('img') as HTMLImageElement;
                                if (cbImg) {
                                    await fnAddImage(['web', cbImg.src]);
                                } else {
                                    const styleTags = html.body.querySelectorAll('style');
                                    styleTags.forEach((e) => e.remove());
                                    fnAddText(html.body.textContent || '');
                                }
                            }
                        } catch (ex) {
                            console.error('Err', ex);
                        }
                    } else if (length === 1 && type === 'text/plain') {
                        fnAddText(str);
                    }
                };
            })(items[i].type, items.length);
            await items[i].getAsString(cb);
        }
    }
};

4. 사용 예시

이 Hook은 웹 에디터에서 클립보드 붙여넣기 이벤트를 처리할 때 사용할 수 있습니다. 예를 들어, 다음과 같이 사용할 수 있습니다.

document.addEventListener('paste', async (event) => {
    const items = event.clipboardData?.items || [];
    const selObj = document.getSelection();
    const range = selObj?.getRangeAt(0);

    if (selObj && range) {
        const { run } = paste(items, selObj, range);
        await run();
    }
});
profile
FRONT_END_DEVELOMENT

0개의 댓글