fix(study): AI 풀이 본문 markdown 렌더링 (review + edit)

기존엔 whitespace-pre-line 으로 plain 표시 → '**굵게**' 같은 markdown 문법이
그대로 노출. DocumentViewer 와 동일한 marked + DOMPurify 패턴 적용. prose
타이포그래피 클래스로 list/heading/inline 코드 스타일 자동.
This commit is contained in:
Hyungi Ahn
2026-04-28 09:45:25 +09:00
parent 8803e6a0fd
commit 37e9391d0d
2 changed files with 34 additions and 2 deletions
@@ -12,12 +12,26 @@
import { api } from '$lib/api';
import { addToast } from '$lib/stores/toast';
import { ArrowLeft, Save, Trash2, AlertCircle, Sparkles, GitCompare, ArrowRight, CheckCircle2, XCircle } from 'lucide-svelte';
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import Button from '$lib/components/ui/Button.svelte';
import Card from '$lib/components/ui/Card.svelte';
import Skeleton from '$lib/components/ui/Skeleton.svelte';
import TextInput from '$lib/components/ui/TextInput.svelte';
import Textarea from '$lib/components/ui/Textarea.svelte';
// AI 풀이 markdown 렌더링 (DocumentViewer 와 동일 패턴)
marked.use({ mangle: false, headerIds: false });
function renderMd(text) {
if (!text) return '';
return DOMPurify.sanitize(marked(text), {
USE_PROFILES: { html: true },
FORBID_TAGS: ['style', 'script'],
FORBID_ATTR: ['onerror', 'onclick'],
ALLOW_UNKNOWN_PROTOCOLS: false,
});
}
let topicId = $derived(Number($page.params.id));
let questionId = $derived(Number($page.params.qid));
@@ -289,7 +303,9 @@
{/if}
{#if aiOpen && aiBody}
<div class="text-sm text-text whitespace-pre-line leading-relaxed bg-bg/30 border border-default rounded p-3">{aiBody}</div>
<div class="text-sm text-text leading-relaxed bg-bg/30 border border-default rounded p-3 prose prose-sm prose-invert max-w-none">
{@html renderMd(aiBody)}
</div>
{#if aiEvidence?.length}
<details class="text-[10px] text-dim">
<summary class="cursor-pointer hover:text-text">참고 근거 {aiEvidence.length}건</summary>
@@ -12,11 +12,25 @@
import { api } from '$lib/api';
import { addToast } from '$lib/stores/toast';
import { ArrowLeft, Play, CheckCircle2, XCircle, RotateCcw, Sparkles, AlertCircle, GitCompare, ArrowRight } from 'lucide-svelte';
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import Button from '$lib/components/ui/Button.svelte';
import Card from '$lib/components/ui/Card.svelte';
import Skeleton from '$lib/components/ui/Skeleton.svelte';
import TextInput from '$lib/components/ui/TextInput.svelte';
// AI 풀이 markdown 렌더링 (DocumentViewer 와 동일 패턴)
marked.use({ mangle: false, headerIds: false });
function renderMd(text) {
if (!text) return '';
return DOMPurify.sanitize(marked(text), {
USE_PROFILES: { html: true },
FORBID_TAGS: ['style', 'script'],
FORBID_ATTR: ['onerror', 'onclick'],
ALLOW_UNKNOWN_PROTOCOLS: false,
});
}
let topicId = $derived(Number($page.params.id));
let topicName = $state('');
@@ -357,7 +371,9 @@
<div class="text-[11px] text-error">{aiExplError}</div>
{/if}
{#if aiExpl?.ai_explanation}
<div class="text-xs text-text whitespace-pre-line leading-relaxed">{aiExpl.ai_explanation}</div>
<div class="text-xs text-text leading-relaxed prose prose-sm prose-invert max-w-none">
{@html renderMd(aiExpl.ai_explanation)}
</div>
{/if}
{#if aiExpl?.evidence?.length}
<details class="text-[10px] text-dim">