diff --git a/frontend/src/lib/components/MarkdownDoc.svelte b/frontend/src/lib/components/MarkdownDoc.svelte index fcd7313..bf00d3f 100644 --- a/frontend/src/lib/components/MarkdownDoc.svelte +++ b/frontend/src/lib/components/MarkdownDoc.svelte @@ -28,6 +28,9 @@ mdStatus?: string | null; mdExtractionError?: string | null; mdExtractionQuality?: Record | null; + /** 개요 점프용 anchor: {chunk_id: md_content 내 char offset}. 렌더 전 해당 위치에 + * 주입(점프 타깃). buildAnchorMap(outlineAnchors) 산출물. */ + anchorMap?: Record | null; placeholder?: string; /** 추가 래퍼 클래스. tailwind prose-* / spacing 등을 호출 측에서 입혀야 할 때. */ class?: string; @@ -41,10 +44,27 @@ mdStatus = null, mdExtractionError = null, mdExtractionQuality = null, + anchorMap = null, placeholder = '*텍스트 추출 대기 중*', class: klass = '', }: Props = $props(); + // 개요 anchor 주입: body 의 각 offset(내림차순)에 빈 삽입(점프 타깃). + // offset 은 buildAnchorMap 이 body 와 동일 문자열 기준으로 산출했어야 함(호출측 책임). + function spliceAnchors(text: string, map: Record | null): string { + if (!map) return text; + const ents = Object.entries(map) + .map(([id, off]) => [id, Number(off)] as [string, number]) + .filter(([, o]) => Number.isFinite(o) && o >= 0 && o <= text.length) + .sort((a, b) => b[1] - a[1]); + if (!ents.length) return text; + let out = text; + for (const [id, off] of ents) { + out = out.slice(0, off) + `\n` + out.slice(off); + } + return out; + } + let usingMarkdown = $derived(!!(mdContent && mdContent.trim())); let body = $derived( usingMarkdown @@ -53,7 +73,7 @@ ? extractedText : placeholder, ); - let renderedHtml = $derived(renderDocMarkdown(body)); + let renderedHtml = $derived(renderDocMarkdown(spliceAnchors(body, anchorMap))); let frontmatterEntries = $derived.by(() => { if (!usingMarkdown || !mdFrontmatter) return [] as [string, unknown][]; diff --git a/frontend/src/lib/components/SectionOutline.svelte b/frontend/src/lib/components/SectionOutline.svelte index d63591f..e9a5709 100644 --- a/frontend/src/lib/components/SectionOutline.svelte +++ b/frontend/src/lib/components/SectionOutline.svelte @@ -15,8 +15,12 @@ interface Props { sections: DocumentSection[]; + /** 항목 클릭 시 본문 점프 콜백(부모가 #sec-{chunkId} scrollIntoView). 없으면 아코디언만. */ + onJump?: (chunkId: number) => void; + /** scroll-spy 현재 절(chunk_id) — 강조용. */ + activeKey?: number | null; } - let { sections }: Props = $props(); + let { sections, onJump, activeKey = null }: Props = $props(); let layout = $derived(groupOrFlat(sections)); let total = $derived(sections.length); @@ -37,15 +41,17 @@ {#snippet itemRow(item: OutlineItem)} {@const s = item.section} {@const open = selectedId === s.chunk_id} + {@const active = activeKey != null && activeKey === s.chunk_id} {@const typeLabel = sectionTypeLabel(s.section_type)}