1- import { GetStaticPaths , GetStaticProps , InferGetStaticPropsType } from 'next' ;
1+ import {
2+ GetStaticPaths ,
3+ GetStaticProps ,
4+ InferGetStaticPropsType
5+ } from 'next' ;
26import { MDXRemote , MDXRemoteSerializeResult } from 'next-mdx-remote' ;
37import { getAllPostIds , getPostData } from '../../lib/posts' ;
48import Header from '../../components/Header' ;
59import Panel from '../../components/Panel' ;
610import { BlueText , RedText , GreenText } from '../../components/Highlight' ;
7- import { useTocObserver } from '../../hooks/useTocObserver' ;
11+ import { useTocObserver } from '../../hooks/useTocObserver' ;
12+ import ScrollProgressBar from '../../components/ScrollProgressBar' ;
13+ import { useEffect , useRef , useState } from 'react' ;
14+ import Link from 'next/link' ;
15+ import { List } from 'lucide-react' ;
16+ import { ArrowDownCircle } from 'lucide-react' ;
817
918type PostData = {
1019 id : string ;
@@ -26,7 +35,23 @@ export const getStaticProps: GetStaticProps<{ postData: PostData }> = async ({ p
2635export default function Post ( { postData } : InferGetStaticPropsType < typeof getStaticProps > ) {
2736 const [ toc , activeId ] = useTocObserver ( ) ;
2837
29- console . log ( activeId )
38+ const scrollRef = useRef < HTMLDivElement > ( null ) ;
39+ const [ showScrollHint , setShowScrollHint ] = useState ( false ) ;
40+
41+ useEffect ( ( ) => {
42+ const el = scrollRef . current ;
43+ if ( el && el . scrollHeight > el . clientHeight ) {
44+ setShowScrollHint ( true ) ;
45+ }
46+ } , [ toc ] ) ;
47+
48+ const handleScroll = ( ) => {
49+ const el = scrollRef . current ;
50+ if ( ! el ) return ;
51+
52+ const isAtBottom = el . scrollTop + el . clientHeight >= el . scrollHeight - 10 ;
53+ setShowScrollHint ( ! isAtBottom ) ;
54+ } ;
3055
3156 const components = {
3257 info : ( props : any ) => < Panel type = "info" { ...props } /> ,
@@ -44,38 +69,65 @@ export default function Post({ postData }: InferGetStaticPropsType<typeof getSta
4469
4570 return (
4671 < >
72+ < ScrollProgressBar />
4773 < Header isDark = { false } />
48- < div className = "max-w-7xl mx-auto px-4 sm:px-6 lg:px-12 py-20 flex" >
74+ < div className = "max-w-[90rem] mx-auto px-6 py-20 flex gap-16 " >
4975 { /* Main content */ }
50- < main className = "flex-1 prose prose-xl max-w-4xl " >
76+ < main className = "flex-1 max-w-3xl prose prose-xl mr-[17rem] " >
5177 < h1 className = "text-4xl font-bold mb-4" > { postData . title } </ h1 >
5278 < p className = "text-gray-500 text-sm mb-8" > { postData . date } </ p >
5379 < article >
54- < MDXRemote { ...postData . mdxSource } components = { components } />
80+ < MDXRemote { ...postData . mdxSource } components = { components } />
5581 </ article >
5682 </ main >
5783
5884 { /* Table of contents */ }
59- < aside className = "hidden lg:block w-48 pl-6" >
60- < div className = "sticky top-24 text-sm leading-relaxed space-y-2" >
61- < h2 className = "text-lg font-bold mb-3" > 📑 목차</ h2 >
62- < ul className = "space-y-1 text-sm text-gray-700" >
63- { toc . map ( ( item ) => (
64- < li key = { item . id } >
65- < a
66- href = { `#${ item . id } ` }
67- className = { `hover:text-blue-600 hover:underline ${
68- ( item . id !== activeId ? '' : 'text-blue-600 font-bold' )
69- } `}
70- >
71- { item . text }
72- </ a >
73- </ li >
74- ) ) }
75- </ ul >
85+ < aside
86+ className = "hidden lg:block fixed right-10 top-[96px] w-60 h-[calc(100vh-96px)] pl-4 z-40 pb-[120px]"
87+ >
88+ < div className = "relative h-full" >
89+ { /* Scrollable 목차 영역 */ }
90+ < div
91+ ref = { scrollRef }
92+ className = "overflow-y-auto h-full pr-2 scrollbar-hide"
93+ onScroll = { handleScroll }
94+ >
95+ < h2 className = "text-sm font-semibold mb-2" > 🧾 목차</ h2 >
96+ < ul className = "space-y-1 text-xs sm:text-sm text-gray-700 pb-12" >
97+ { toc . map ( ( item ) => (
98+ < li key = { item . id } className = { item . level === 3 ? 'ml-4' : '' } >
99+ < a
100+ href = { `#${ item . id } ` }
101+ className = { `hover:text-blue-600 hover:underline ${
102+ item . id === activeId ? 'text-blue-600 font-semibold' : ''
103+ } `}
104+ >
105+ { item . text }
106+ </ a >
107+ </ li >
108+ ) ) }
109+ </ ul >
110+ </ div >
111+
112+ { /* 스크롤 힌트 */ }
113+ { showScrollHint && (
114+ < div
115+ className = "absolute bottom-2 left-1/2 -translate-x-1/2 bg-blue-500 text-white px-2 py-2 rounded-full shadow animate-bounce z-10" >
116+ < ArrowDownCircle className = "w-5 h-5" />
117+ </ div >
118+ ) }
76119 </ div >
77120 </ aside >
121+
122+
78123 </ div >
124+ < Link
125+ href = "/post"
126+ className = "fixed bottom-6 right-6 z-50 bg-white border border-gray-300 rounded-lg shadow-md p-3 hover:shadow-lg transition"
127+ aria-label = "목록으로 가기"
128+ >
129+ < List className = "w-6 h-6 text-gray-800" />
130+ </ Link >
79131 </ >
80132 ) ;
81133}
0 commit comments