11'use client' ;
22
3+ import { useState , useEffect , useCallback } from 'react' ;
34import Link from 'next/link' ;
5+ import { ChevronLeft , ChevronRight , X } from 'lucide-react' ;
46import { useLang } from '@/components/layout/LangProvider' ;
57import i18n from '@/lib/i18n' ;
68
9+ interface GuestMessage {
10+ id : string ;
11+ name : string ;
12+ message : string ;
13+ created_at : string ;
14+ }
15+
16+ const PER_PAGE = 3 ;
17+
18+ type T = typeof i18n [ 'ko' ] | typeof i18n [ 'en' ] ;
19+
20+ function GuestBookModal ( { t, onClose } : { t : T ; onClose : ( ) => void } ) {
21+ const [ page , setPage ] = useState ( 0 ) ;
22+ const [ messages , setMessages ] = useState < GuestMessage [ ] > ( [ ] ) ;
23+ const [ name , setName ] = useState ( '' ) ;
24+ const [ password , setPassword ] = useState ( '' ) ;
25+ const [ message , setMessage ] = useState ( '' ) ;
26+ const [ submitting , setSubmitting ] = useState ( false ) ;
27+
28+ const totalPages = Math . ceil ( messages . length / PER_PAGE ) ;
29+ const pageMessages = messages . slice ( page * PER_PAGE , page * PER_PAGE + PER_PAGE ) ;
30+
31+ const fetchMessages = useCallback ( async ( ) => {
32+ const res = await fetch ( '/api/guestbook' ) ;
33+ const data = await res . json ( ) as { messages : GuestMessage [ ] } ;
34+ setMessages ( data . messages ?? [ ] ) ;
35+ } , [ ] ) ;
36+
37+ useEffect ( ( ) => { fetchMessages ( ) ; } , [ fetchMessages ] ) ;
38+
39+ const handleSubmit = async ( ) => {
40+ if ( ! name . trim ( ) || ! password . trim ( ) || ! message . trim ( ) ) return ;
41+ setSubmitting ( true ) ;
42+ await fetch ( '/api/guestbook' , {
43+ method : 'POST' ,
44+ headers : { 'Content-Type' : 'application/json' } ,
45+ body : JSON . stringify ( { name, password, message } ) ,
46+ } ) ;
47+ setName ( '' ) ; setPassword ( '' ) ; setMessage ( '' ) ;
48+ await fetchMessages ( ) ;
49+ setPage ( 0 ) ;
50+ setSubmitting ( false ) ;
51+ } ;
52+
53+ return (
54+ < >
55+ { /* 백드롭 */ }
56+ < div
57+ className = "fixed inset-0 z-40 bg-black/30 backdrop-blur-[2px]"
58+ onClick = { onClose }
59+ />
60+
61+ { /* 모달 */ }
62+ < div
63+ className = "fixed left-1/2 top-1/2 z-50 w-full max-w-xl -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-2xl"
64+ style = { {
65+ background : 'rgba(20,12,4,0.6)' ,
66+ backdropFilter : 'blur(24px)' ,
67+ maskImage : 'linear-gradient(to bottom, transparent, black 15%, black 85%, transparent), linear-gradient(to right, transparent, black 10%, black 90%, transparent)' ,
68+ WebkitMaskImage : 'linear-gradient(to bottom, transparent, black 15%, black 85%, transparent), linear-gradient(to right, transparent, black 10%, black 90%, transparent)' ,
69+ maskComposite : 'intersect' ,
70+ WebkitMaskComposite : 'source-in' ,
71+ } }
72+ >
73+ { /* 파피루스 텍스처 */ }
74+ < div
75+ className = "pointer-events-none absolute inset-0"
76+ style = { {
77+ backgroundImage : 'url(/images/background-image.svg)' ,
78+ backgroundSize : 'cover' ,
79+ opacity : 0.5 ,
80+ } }
81+ />
82+
83+ < div className = "relative px-6 py-6" >
84+ { /* 헤더 */ }
85+ < div className = "mb-5 flex items-center justify-between" >
86+ < p className = "text-xs font-semibold tracking-widest uppercase text-white" >
87+ { t . guestBookTitle }
88+ </ p >
89+ < button
90+ onClick = { onClose }
91+ className = "rounded-lg p-1 text-zinc-300 transition-colors hover:text-white"
92+ >
93+ < X size = { 15 } />
94+ </ button >
95+ </ div >
96+
97+ { /* 입력 */ }
98+ < div className = "mb-5 flex flex-col gap-2" >
99+ < div className = "flex gap-2" >
100+ < input
101+ type = "text"
102+ placeholder = { t . guestBookNickname }
103+ value = { name }
104+ onChange = { ( e ) => setName ( e . target . value ) }
105+ className = "flex-1 rounded-lg border border-white/20 bg-white/10 px-3 py-2 text-sm text-zinc-100 outline-none placeholder:text-zinc-300 focus:border-white/40"
106+ />
107+ < input
108+ type = "password"
109+ placeholder = { t . guestBookPassword }
110+ value = { password }
111+ onChange = { ( e ) => setPassword ( e . target . value ) }
112+ className = "flex-1 rounded-lg border border-white/20 bg-white/10 px-3 py-2 text-sm text-zinc-100 outline-none placeholder:text-zinc-300 focus:border-white/40"
113+ />
114+ </ div >
115+ < div className = "flex gap-2" >
116+ < input
117+ type = "text"
118+ placeholder = { t . guestBookMessage }
119+ value = { message }
120+ onChange = { ( e ) => setMessage ( e . target . value ) }
121+ onKeyDown = { ( e ) => { if ( e . key === 'Enter' ) handleSubmit ( ) ; } }
122+ className = "flex-1 rounded-lg border border-white/20 bg-white/10 px-3 py-2 text-sm text-zinc-100 outline-none placeholder:text-zinc-300 focus:border-white/40"
123+ />
124+ < button
125+ onClick = { handleSubmit }
126+ disabled = { submitting }
127+ className = "shrink-0 rounded-lg bg-white/20 px-4 py-2 text-sm font-semibold text-zinc-100 transition-colors hover:bg-white/30 disabled:opacity-40"
128+ >
129+ { t . guestBookSubmit }
130+ </ button >
131+ </ div >
132+ </ div >
133+
134+ { /* 구분선 */ }
135+ < div className = "mb-4 h-px bg-white/10" />
136+
137+ { /* 메시지 */ }
138+ < div className = "flex flex-col gap-4" >
139+ { pageMessages . map ( ( msg ) => (
140+ < div key = { msg . id } className = "flex flex-col gap-0.5" >
141+ < div className = "flex items-center gap-2" >
142+ < span className = "text-sm font-semibold text-zinc-100" > { msg . name } </ span >
143+ < span className = "text-[11px] text-zinc-300" > { new Date ( msg . created_at ) . toLocaleDateString ( 'ko-KR' , { year : '2-digit' , month : '2-digit' , day : '2-digit' } ) } </ span >
144+ </ div >
145+ < p className = "text-sm leading-relaxed text-white" > { msg . message } </ p >
146+ </ div >
147+ ) ) }
148+ </ div >
149+
150+ { /* 페이지네이션 */ }
151+ < div className = "mt-4 flex items-center justify-end gap-2" >
152+ < button
153+ onClick = { ( ) => setPage ( ( p ) => Math . max ( 0 , p - 1 ) ) }
154+ disabled = { page === 0 }
155+ className = "text-amber-300 transition-opacity disabled:opacity-20"
156+ >
157+ < ChevronLeft size = { 18 } />
158+ </ button >
159+ < button
160+ onClick = { ( ) => setPage ( ( p ) => Math . min ( totalPages - 1 , p + 1 ) ) }
161+ disabled = { page === totalPages - 1 }
162+ className = "text-amber-300 transition-opacity disabled:opacity-20"
163+ >
164+ < ChevronRight size = { 18 } />
165+ </ button >
166+ </ div >
167+ </ div >
168+ </ div >
169+ </ >
170+ ) ;
171+ }
172+
7173export default function HomeClient ( ) {
8174 const { lang } = useLang ( ) ;
9175 const t = i18n [ lang ] ;
176+ const [ guestBookOpen , setGuestBookOpen ] = useState ( false ) ;
10177
11178 return (
12179 < main className = "flex flex-1 flex-col items-center justify-center px-6 pb-20 text-center" >
@@ -23,7 +190,17 @@ export default function HomeClient() {
23190 >
24191 { t . enterArchive }
25192 </ Link >
193+ < button
194+ onClick = { ( ) => setGuestBookOpen ( true ) }
195+ className = "rounded-full border border-zinc-600 px-8 py-2.5 text-sm font-medium text-zinc-300 backdrop-blur-sm transition-all hover:border-zinc-400 hover:text-white active:scale-95"
196+ >
197+ { t . guestBookTitle }
198+ </ button >
26199 </ div >
200+
201+ { guestBookOpen && (
202+ < GuestBookModal t = { t } onClose = { ( ) => setGuestBookOpen ( false ) } />
203+ ) }
27204 </ main >
28205 ) ;
29206}
0 commit comments