From 37e9391d0dc8f9f71016d6936ea340f281cf87a6 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Tue, 28 Apr 2026 09:45:25 +0900 Subject: [PATCH] =?UTF-8?q?fix(study):=20AI=20=ED=92=80=EC=9D=B4=20?= =?UTF-8?q?=EB=B3=B8=EB=AC=B8=20markdown=20=EB=A0=8C=EB=8D=94=EB=A7=81=20(?= =?UTF-8?q?review=20+=20edit)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존엔 whitespace-pre-line 으로 plain 표시 → '**굵게**' 같은 markdown 문법이 그대로 노출. DocumentViewer 와 동일한 marked + DOMPurify 패턴 적용. prose 타이포그래피 클래스로 list/heading/inline 코드 스타일 자동. --- .../[id]/questions/[qid]/edit/+page.svelte | 18 +++++++++++++++++- .../study/topics/[id]/review/+page.svelte | 18 +++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes/study/topics/[id]/questions/[qid]/edit/+page.svelte b/frontend/src/routes/study/topics/[id]/questions/[qid]/edit/+page.svelte index 17ba13d..54c739e 100644 --- a/frontend/src/routes/study/topics/[id]/questions/[qid]/edit/+page.svelte +++ b/frontend/src/routes/study/topics/[id]/questions/[qid]/edit/+page.svelte @@ -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} -
{aiBody}
+
+ {@html renderMd(aiBody)} +
{#if aiEvidence?.length}
참고 근거 {aiEvidence.length}건 diff --git a/frontend/src/routes/study/topics/[id]/review/+page.svelte b/frontend/src/routes/study/topics/[id]/review/+page.svelte index 4e94cd3..2d5a4a1 100644 --- a/frontend/src/routes/study/topics/[id]/review/+page.svelte +++ b/frontend/src/routes/study/topics/[id]/review/+page.svelte @@ -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 @@
{aiExplError}
{/if} {#if aiExpl?.ai_explanation} -
{aiExpl.ai_explanation}
+
+ {@html renderMd(aiExpl.ai_explanation)} +
{/if} {#if aiExpl?.evidence?.length}