11'use client' ;
22
3+ import { useState , useRef , useCallback } from "react" ;
34import { useAuthenticatedApi } from "@/hooks/useAuthenticatedApi" ;
4- import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
5-
6- const REFRESH_INTERVAL = 10000 ;
7-
8- const hashString = ( value ) => {
9- let hash = 0 ;
10- for ( let i = 0 ; i < value . length ; i += 1 ) {
11- hash = value . charCodeAt ( i ) + ( ( hash << 5 ) - hash ) ;
12- }
13- return hash ;
14- } ;
15-
16- const mapToRange = ( value , min , max ) => {
17- const normalized = Math . abs ( value % 1000 ) / 1000 ;
18- return min + normalized * ( max - min ) ;
19- } ;
5+ import { useGuestbookEntries } from "@/hooks/homecoming/useGuestbookEntries" ;
6+ import GuestbookWordCloud from "@/components/event/homecoming/GuestbookWordCloud" ;
207
218export default function GuestbookAdminPage ( ) {
9+ const { entries, isLoading, error, lastSyncedAt, refresh } = useGuestbookEntries ( ) ;
2210 const { apiClient } = useAuthenticatedApi ( ) ;
23- const [ entries , setEntries ] = useState ( [ ] ) ;
2411 const [ formValues , setFormValues ] = useState ( { wristbandSerial : "" , name : "" } ) ;
25- const [ isLoading , setIsLoading ] = useState ( true ) ;
2612 const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
27- const [ error , setError ] = useState ( "" ) ;
2813 const [ statusMessage , setStatusMessage ] = useState ( "" ) ;
29- const [ lastSyncedAt , setLastSyncedAt ] = useState ( null ) ;
3014 const statusTimerRef = useRef ( null ) ;
3115
32- const fetchEntries = useCallback ( async ( ) => {
33- try {
34- setError ( "" ) ;
35- const res = await apiClient . get ( "/guestbook/entries" ) ;
36- setEntries ( res ?. data ?. data ?? [ ] ) ;
37- setLastSyncedAt ( new Date ( ) ) ;
38- } catch ( err ) {
39- console . error ( "방명록 목록 조회 실패" , err ) ;
40- setError ( "방명록 목록을 불러오지 못했습니다." ) ;
41- } finally {
42- setIsLoading ( false ) ;
43- }
44- } , [ apiClient ] ) ;
45-
46- useEffect ( ( ) => {
47- fetchEntries ( ) ;
48- const interval = setInterval ( fetchEntries , REFRESH_INTERVAL ) ;
49-
50- return ( ) => {
51- clearInterval ( interval ) ;
52- } ;
53- } , [ fetchEntries ] ) ;
54-
5516 const handleChange = ( event ) => {
5617 const { name, value } = event . target ;
5718 setFormValues ( ( prev ) => ( { ...prev , [ name ] : value } ) ) ;
@@ -79,7 +40,7 @@ export default function GuestbookAdminPage() {
7940 setFormValues ( { wristbandSerial : "" , name : "" } ) ;
8041 setStatusMessage ( "입장 등록이 완료되었습니다." ) ;
8142 resetStatusTimer ( ) ;
82- await fetchEntries ( ) ;
43+ await refresh ( ) ;
8344 } catch ( err ) {
8445 console . error ( "방명록 등록 실패" , err ) ;
8546 const message = err ?. response ?. data ?. message || "입장 등록에 실패했습니다." ;
@@ -90,45 +51,6 @@ export default function GuestbookAdminPage() {
9051 }
9152 } ;
9253
93- useEffect ( ( ) => {
94- return ( ) => {
95- if ( statusTimerRef . current ) {
96- clearTimeout ( statusTimerRef . current ) ;
97- }
98- } ;
99- } , [ ] ) ;
100-
101- const words = useMemo ( ( ) => {
102- if ( ! entries . length ) {
103- return [ ] ;
104- }
105-
106- const recentThreshold = Math . max ( entries . length - 5 , 0 ) ;
107-
108- return entries . map ( ( entry , idx ) => {
109- const key = entry . id ?? `${ entry . wristbandSerial ?? "unknown" } -${ idx } ` ;
110- const baseHash = hashString ( `${ key } -${ entry . name } ` ) ;
111- const top = mapToRange ( baseHash , 8 , 92 ) ;
112- const left = mapToRange ( baseHash * 3 , 12 , 88 ) ;
113- const fontSize = mapToRange ( baseHash * 5 , 0.9 , 2.6 ) ;
114- const rotate = mapToRange ( baseHash * 7 , - 12 , 12 ) ;
115- const opacity = mapToRange ( baseHash * 11 , 0.35 , 0.85 ) ;
116-
117- return {
118- key,
119- label : entry . name ,
120- isRecent : idx >= recentThreshold ,
121- style : {
122- top : `${ top } %` ,
123- left : `${ left } %` ,
124- fontSize : `${ fontSize } rem` ,
125- opacity,
126- transform : `translate(-50%, -50%) rotate(${ rotate } deg)` ,
127- } ,
128- } ;
129- } ) ;
130- } , [ entries ] ) ;
131-
13254 const isSubmitDisabled = isSubmitting || ! formValues . wristbandSerial . trim ( ) || ! formValues . name . trim ( ) ;
13355
13456 return (
@@ -143,25 +65,7 @@ export default function GuestbookAdminPage() {
14365 "radial-gradient(circle at 50% 70%, rgba(52,168,83,0.18), transparent 45%)" ,
14466 } }
14567 />
146- < div className = "absolute inset-0 pointer-events-none select-none" >
147- { words . length ? (
148- words . map ( ( word ) => (
149- < span
150- key = { word . key }
151- style = { word . style }
152- className = { `absolute font-semibold tracking-wide drop-shadow-[0_2px_12px_rgba(15,23,42,0.08)] transition-all duration-700 ease-in-out ${ word . isRecent ? "text-cblue" : "text-slate-500" } ` }
153- >
154- { word . label }
155- </ span >
156- ) )
157- ) : (
158- ! isLoading && (
159- < div className = "w-full h-full flex items-center justify-center text-slate-400 text-lg" >
160- 아직 등록된 입장 정보가 없습니다.
161- </ div >
162- )
163- ) }
164- </ div >
68+ < GuestbookWordCloud entries = { entries } isLoading = { isLoading } recentCount = { 5 } />
16569 </ section >
16670
16771 < section className = "relative z-10 w-full px-4 pb-12" >
@@ -172,10 +76,11 @@ export default function GuestbookAdminPage() {
17276 < div className = "flex flex-col gap-3 text-slate-700" >
17377 < h2 className = "text-2xl font-semibold text-slate-900" > 입장 등록</ h2 >
17478 < p className = "text-sm" > 현재 { entries . length } 명의 게스트가 입장했습니다.</ p >
79+ { error && < p className = "text-sm text-cred" > { error } </ p > }
17580 </ div >
17681 < div className = "flex flex-col gap-4 md:flex-row" >
17782 < label className = "flex-1 text-sm text-slate-600" >
178- < span className = "block mb-2 font-medium text-slate-800" > 손목밴드 번호</ span >
83+ < span className = "block mb-2 font-medium text-slate-800" > 손목띠지 번호</ span >
17984 < input
18085 name = "wristbandSerial"
18186 value = { formValues . wristbandSerial }
@@ -212,7 +117,6 @@ export default function GuestbookAdminPage() {
212117 마지막 동기화: { lastSyncedAt . toLocaleTimeString ( "ko-KR" , { hour12 : false } ) }
213118 </ p >
214119 ) }
215- { error && < p className = "text-cred mt-1" > { error } </ p > }
216120 </ div >
217121 </ form >
218122 </ section >
0 commit comments