From 78438f195e6950fed23a2300be5275814b043681 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sat, 21 Feb 2026 08:04:30 +0000
Subject: [PATCH] fix(ssr): resolve DOMPurify.sanitize crash in Next.js
Replaces direct DOMPurify usage with a safe wrapper that handles server-side execution and potential factory function exports. This prevents TypeError during rendering.
Co-authored-by: egoing <303822+egoing@users.noreply.github.com>
---
.../Chat/Root/Conversation/index.tsx | 8 ++++----
.../EditorLoadingErrorFallback.tsx | 4 ++--
src/components/common/ConfirmDialog/index.tsx | 6 +++---
.../common/ContentListMessage/index.tsx | 4 ++--
.../layout/bottom/agreement/docDialog.tsx | 4 ++--
src/utils/sanitize.ts | 18 ++++++++++++++++++
6 files changed, 31 insertions(+), 13 deletions(-)
create mode 100644 src/utils/sanitize.ts
diff --git a/src/components/Chat/Root/Conversation/index.tsx b/src/components/Chat/Root/Conversation/index.tsx
index a60cbbb..8d2770d 100644
--- a/src/components/Chat/Root/Conversation/index.tsx
+++ b/src/components/Chat/Root/Conversation/index.tsx
@@ -17,7 +17,7 @@ import CopyAfterIcon from '@/public/icon/copyAfterIcon';
import CopyBeforeIcon from '@/public/icon/copyBeforeIcon';
import { useLingui } from '@lingui/react/macro';
import { marked } from 'marked';
-import DOMPurify from 'dompurify';
+import { safeSanitize } from '@/utils/sanitize';
import { CreatePageBtn } from '@/components/Chat/CreatePageBtn';
export function Conversation({ onLeaveBottom }: { onLeaveBottom: (isLeave: boolean) => void }) {
@@ -150,7 +150,7 @@ function NoticeMessage({ id, name, children }: { id: string; name: string; child
return (
-
+
);
}
@@ -186,7 +186,7 @@ function SimilarityStartMessage({
return (
-
+
);
}
@@ -251,7 +251,7 @@ function LLMResponseMessage({
if (!content) return '';
// marked로 마크다운을 HTML로 변환하고 DOMPurify로 안전하게 정화
const rawHtml = marked(content, markedOptions) as string;
- return DOMPurify.sanitize(rawHtml);
+ return safeSanitize(rawHtml);
};
return (
diff --git a/src/components/common/BlockNoteEditor/EditorLoadingErrorFallback.tsx b/src/components/common/BlockNoteEditor/EditorLoadingErrorFallback.tsx
index 629c43d..04ab176 100644
--- a/src/components/common/BlockNoteEditor/EditorLoadingErrorFallback.tsx
+++ b/src/components/common/BlockNoteEditor/EditorLoadingErrorFallback.tsx
@@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import s from './EditorLoadingErrorFallback.module.css';
-import DOMPurify from 'dompurify';
+import { safeSanitize } from '@/utils/sanitize';
import { useLingui } from '@lingui/react/macro';
interface EditorLoadingErrorFallbackProps {
@@ -11,7 +11,7 @@ export function EditorLoadingErrorFallback({ html }: EditorLoadingErrorFallbackP
const { t } = useLingui();
const sanitizedHtml = useMemo(() => {
- return DOMPurify.sanitize(html);
+ return safeSanitize(html);
}, [html]);
return (
diff --git a/src/components/common/ConfirmDialog/index.tsx b/src/components/common/ConfirmDialog/index.tsx
index 480b1d6..fe83e27 100644
--- a/src/components/common/ConfirmDialog/index.tsx
+++ b/src/components/common/ConfirmDialog/index.tsx
@@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { useAtom } from 'jotai';
import { useLingui } from '@lingui/react/macro';
-import DOMPurify from 'dompurify';
+import { safeSanitize } from '@/utils/sanitize';
import { confirmState, closeConfirmState } from '@/lib/jotai'; // 경로는 프로젝트 설정에 맞게 조정하세요.
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
@@ -64,7 +64,7 @@ export default function ConfirmDialog() {
// 내용물의 실제 너비를 계산하기 위해 텍스트를 복제하여 임시로 측정
const tempSpan = document.createElement('span');
- tempSpan.innerHTML = DOMPurify.sanitize(confirm.message);
+ tempSpan.innerHTML = safeSanitize(confirm.message);
tempSpan.style.position = 'absolute';
tempSpan.style.visibility = 'hidden';
tempSpan.style.whiteSpace = 'nowrap';
@@ -138,7 +138,7 @@ export default function ConfirmDialog() {
ref={contentRef}
className={`${s.content} ${isCentered ? s.contentCentered : ''} ${isContentVisible ? s.visible : s.hidden}`}
dangerouslySetInnerHTML={{
- __html: DOMPurify.sanitize(confirm.message),
+ __html: safeSanitize(confirm.message),
}}
>
{confirm.customContent && (
diff --git a/src/components/common/ContentListMessage/index.tsx b/src/components/common/ContentListMessage/index.tsx
index 1a35773..af17318 100644
--- a/src/components/common/ContentListMessage/index.tsx
+++ b/src/components/common/ContentListMessage/index.tsx
@@ -1,6 +1,6 @@
import { useAtom } from 'jotai';
import { contentListMessageState } from '@/lib/jotai';
-import DOMPurify from 'dompurify';
+import { safeSanitize } from '@/utils/sanitize';
import { useTheme } from '@mui/material/styles';
export default function ContentListMessage() {
@@ -42,7 +42,7 @@ export default function ContentListMessage() {
>
diff --git a/src/components/layout/bottom/agreement/docDialog.tsx b/src/components/layout/bottom/agreement/docDialog.tsx
index a13c3da..bb2b6cb 100644
--- a/src/components/layout/bottom/agreement/docDialog.tsx
+++ b/src/components/layout/bottom/agreement/docDialog.tsx
@@ -4,7 +4,7 @@ import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import { useLingui } from '@lingui/react/macro';
-import DOMPurify from 'dompurify';
+import { safeSanitize } from '@/utils/sanitize';
import s from './docDialog.module.css';
export function DocsDialog({
@@ -24,7 +24,7 @@ export function DocsDialog({