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

// 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" }}
/>
);
}