You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
77 lines
2.8 KiB
TypeScript
77 lines
2.8 KiB
TypeScript
// frontend/components/mail/MailHtmlBody.tsx — 원본 HTML 메일을 샌드박스 iframe 으로 렌더.
|
|
// sandbox 에 allow-scripts 를 주지 않아 메일 속 JS 가 실행되지 않음(XSS 차단).
|
|
// allow-same-origin 은 본문 높이 측정(scrollHeight)에 필요, allow-popups 는 링크 새 탭 열기용.
|
|
"use client";
|
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
function wrapDocument(html: string): string {
|
|
return `<!DOCTYPE html><html><head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<base target="_blank">
|
|
<style>
|
|
html, body { margin: 0; padding: 0; background: #fff; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
"Apple SD Gothic Neo", "Noto Sans KR", sans-serif;
|
|
font-size: 14px; line-height: 1.6; color: #1a1a1a;
|
|
padding: 6px 4px; word-break: break-word; overflow-wrap: anywhere;
|
|
}
|
|
img { max-width: 100%; height: auto; }
|
|
a { color: #2563eb; }
|
|
table { max-width: 100%; }
|
|
blockquote { margin: 0 0 0 12px; padding-left: 12px; border-left: 3px solid #e5e7eb; color: #555; }
|
|
</style></head><body>${html}</body></html>`;
|
|
}
|
|
|
|
export function MailHtmlBody({ html }: { html: string }) {
|
|
const ref = useRef<HTMLIFrameElement>(null);
|
|
const [height, setHeight] = useState(160);
|
|
const srcDoc = useMemo(() => wrapDocument(html), [html]);
|
|
|
|
useEffect(() => {
|
|
const iframe = ref.current;
|
|
if (!iframe) return;
|
|
let ro: ResizeObserver | null = null;
|
|
const timers: ReturnType<typeof setTimeout>[] = [];
|
|
const measure = () => {
|
|
const doc = iframe.contentDocument;
|
|
if (doc?.body) {
|
|
const h = Math.max(doc.body.scrollHeight, doc.documentElement?.scrollHeight || 0);
|
|
if (h > 0) setHeight(h + 8);
|
|
}
|
|
};
|
|
const onLoad = () => {
|
|
measure();
|
|
const doc = iframe.contentDocument;
|
|
if (doc?.body) {
|
|
ro = new ResizeObserver(measure);
|
|
ro.observe(doc.body);
|
|
// 이미지가 늦게 로드되면 높이가 늘어나므로 로드 시 재측정.
|
|
Array.from(doc.images || []).forEach((img) => {
|
|
if (!img.complete) img.addEventListener("load", measure);
|
|
});
|
|
}
|
|
// 폰트·원격 이미지 등 후속 레이아웃 변화 대비 몇 차례 재측정.
|
|
[150, 500, 1200].forEach((t) => timers.push(setTimeout(measure, t)));
|
|
};
|
|
iframe.addEventListener("load", onLoad);
|
|
return () => {
|
|
iframe.removeEventListener("load", onLoad);
|
|
ro?.disconnect();
|
|
timers.forEach(clearTimeout);
|
|
};
|
|
}, [srcDoc]);
|
|
|
|
return (
|
|
<iframe
|
|
ref={ref}
|
|
className="rd-html"
|
|
title="메일 본문"
|
|
srcDoc={srcDoc}
|
|
sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"
|
|
style={{ width: "100%", height, border: 0, display: "block" }}
|
|
/>
|
|
);
|
|
}
|