diff --git a/frontend/src/lib/components/DocumentViewer.svelte b/frontend/src/lib/components/DocumentViewer.svelte
index 1bc32e8..5aa4b5b 100644
--- a/frontend/src/lib/components/DocumentViewer.svelte
+++ b/frontend/src/lib/components/DocumentViewer.svelte
@@ -3,10 +3,14 @@
import { addToast } from '$lib/stores/toast';
import { marked } from 'marked';
import DOMPurify from 'dompurify';
- import { ExternalLink, Save, RefreshCw } from 'lucide-svelte';
+ import { ExternalLink, Save } from 'lucide-svelte';
import Tabs from '$lib/components/ui/Tabs.svelte';
+ import MarkdownDoc from '$lib/components/MarkdownDoc.svelte';
+ import MarkdownStatusBadge from '$lib/components/MarkdownStatusBadge.svelte';
+ import { getViewerType } from '$lib/utils/viewerType';
+ import { isMdSuccess } from '$lib/utils/mdStatus';
- // marked + sanitize
+ // 편집 미리보기 전용 plain marked (본문 렌더는 MarkdownDoc 가 담당).
marked.use({ mangle: false, headerIds: false });
function renderMd(text) {
return DOMPurify.sanitize(marked(text), {
@@ -22,33 +26,19 @@
let loading = $state(true);
let viewerType = $state('none');
- // Markdown 편집
+ // Markdown 편집 (md/txt — extracted_text 가 표시·편집 단일 필드)
let editMode = $state(false);
let editContent = $state('');
let editTab = $state('edit');
let saving = $state(false);
let rawMarkdown = $state('');
- function getViewerType(format) {
- if (['md', 'txt'].includes(format)) return 'markdown';
- if (format === 'pdf') return 'pdf';
- if (['hwp', 'hwpx'].includes(format)) return 'preview-pdf';
- if (['odoc', 'osheet', 'docx', 'xlsx', 'pptx', 'odt', 'ods', 'odp'].includes(format)) return 'preview-pdf';
- if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff'].includes(format)) return 'image';
- if (['csv', 'json', 'xml', 'html'].includes(format)) return 'text';
- if (['dwg', 'dxf'].includes(format)) return 'cad';
- return 'unsupported';
- }
-
const ODF_FORMATS = ['ods', 'odt', 'odp', 'odoc', 'osheet'];
- function getEditInfo(doc) {
- // DB에 저장된 편집 URL 우선
- if (doc.edit_url) return { url: doc.edit_url, label: '편집' };
- // ODF 포맷 → Synology Drive
- if (ODF_FORMATS.includes(doc.file_format)) return { url: 'https://link.hyungi.net', label: 'Synology Drive에서 열기' };
- // CAD
- if (['dwg', 'dxf'].includes(doc.file_format)) return { url: 'https://web.autocad.com', label: 'AutoCAD Web' };
+ function getEditInfo(d) {
+ if (d.edit_url) return { url: d.edit_url, label: '편집' };
+ if (ODF_FORMATS.includes(d.file_format)) return { url: 'https://link.hyungi.net', label: 'Synology Drive에서 열기' };
+ if (['dwg', 'dxf'].includes(d.file_format)) return { url: 'https://web.autocad.com', label: 'AutoCAD Web' };
return null;
}
@@ -61,18 +51,17 @@
async function loadFullDoc(id) {
loading = true;
+ rawMarkdown = '';
try {
fullDoc = await api(`/documents/${id}`);
- viewerType = fullDoc.source_channel === 'news' ? 'article' : getViewerType(fullDoc.file_format);
+ viewerType = getViewerType(fullDoc.file_format, fullDoc.source_channel);
- // Markdown: extracted_text 없으면 원본 파일 직접 가져오기
+ // 본문 markdown(md/txt) 인데 extracted_text 가 비면 원본 파일 직접 로드.
if (viewerType === 'markdown' && !fullDoc.extracted_text) {
try {
const resp = await fetch(`/api/documents/${id}/file?token=${getAccessToken()}`);
if (resp.ok) rawMarkdown = await resp.text();
} catch (e) { rawMarkdown = ''; }
- } else {
- rawMarkdown = '';
}
} catch (err) {
fullDoc = null;
@@ -82,6 +71,23 @@
}
}
+ // PDF markdown-first: marker 가 만든 canonical md_content 가 있으면 기본으로 그것을 보여주고
+ // "PDF 원본" 토글 제공. lastDocId 는 prop(fullDoc.id) 로 키잉 — 3-pane 은 라우트 리마운트가
+ // 없어 page.params 가드는 no-op 이 된다.
+ let pdfViewMode = $state('markdown');
+ let lastDocId = $state(null);
+ let canShowMarkdown = $derived(
+ !!(isMdSuccess(fullDoc?.md_status) && fullDoc?.md_content?.trim())
+ );
+ $effect(() => {
+ if (!fullDoc) return;
+ if (fullDoc.id !== lastDocId) {
+ lastDocId = fullDoc.id;
+ pdfViewMode = canShowMarkdown ? 'markdown' : 'pdf';
+ }
+ if (!canShowMarkdown && pdfViewMode === 'markdown') pdfViewMode = 'pdf';
+ });
+
function startEdit() {
editContent = fullDoc?.extracted_text || rawMarkdown || '';
editMode = true;
@@ -113,6 +119,7 @@
}
let editInfo = $derived(fullDoc ? getEditInfo(fullDoc) : null);
+ const PROSE = 'prose prose-invert prose-base max-w-none';
로딩 중...
-로딩 중...
{fullDoc.extracted_text || '텍스트 없음'}
+ {fullDoc.extracted_text || '텍스트 없음'}Synology Office 문서 — 외부 편집기에서 열어야 합니다.
+ +미리보기를 지원하지 않는 형식입니다 ({fullDoc.file_format})
-미리보기를 지원하지 않는 형식입니다 ({fullDoc.file_format})
로 원형 보존.
+const TEXT = new Set(['csv', 'json', 'xml', 'html']);
+const HWP = new Set(['hwp', 'hwpx']);
+// LibreOffice headless → PDF preview (/preview) 로 인앱 표시.
+const OFFICE_PREVIEW = new Set(['docx', 'xlsx', 'pptx', 'odt', 'ods', 'odp']);
+// Synology Office 네이티브 — 인앱 변환 부적합, 외부 편집기로.
+const SYNOLOGY = new Set(['odoc', 'osheet']);
+const IMAGE = new Set(['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff']);
+const CAD = new Set(['dwg', 'dxf']);
+
+export function getViewerType(
+ format: string | null | undefined,
+ sourceChannel?: string | null,
+): ViewerType {
+ if (sourceChannel === 'news') return 'article';
+ const f = (format ?? '').toLowerCase();
+ if (MARKDOWN.has(f)) return 'markdown';
+ if (f === 'pdf') return 'pdf';
+ if (HWP.has(f)) return 'hwp-markdown';
+ if (OFFICE_PREVIEW.has(f)) return 'preview-pdf';
+ if (SYNOLOGY.has(f)) return 'synology';
+ if (IMAGE.has(f)) return 'image';
+ if (TEXT.has(f)) return 'text';
+ if (CAD.has(f)) return 'cad';
+ return 'unsupported';
+}