1+ 'use client' ;
2+
3+ import { useState , useEffect , useRef } from 'react' ;
4+ import { motion , AnimatePresence , PanInfo } from 'framer-motion' ;
5+ import { useRouter } from 'next/navigation' ;
6+ import { CATEGORY_COLORS } from '../types/common' ;
7+
8+ interface EmotionForecast {
9+ timeSlot : string ;
10+ emotion : string ;
11+ temperature : number ;
12+ image : string ;
13+ category : string ;
14+ }
15+
16+ interface EmotionForecastPopupProps {
17+ isOpen : boolean ;
18+ onClose : ( ) => void ;
19+ forecasts : EmotionForecast [ ] ;
20+ }
21+
22+ const getEmotionImage = ( imageUrl : string ) => {
23+ // DB에서 받은 이미지 URL을 SVG big 버전으로 변환
24+ console . log ( '원본 이미지 URL:' , imageUrl ) ;
25+
26+ if ( ! imageUrl ) {
27+ console . error ( '이미지 URL이 없습니다' ) ;
28+ return '/icon/기쁜-big.svg' ; // 기본 이미지
29+ }
30+
31+ let processedUrl = imageUrl ;
32+
33+ // PNG를 SVG로 변경하고 big 버전 사용
34+
35+ // small을 big으로 변경
36+
37+ console . log ( '최종 SVG URL:' , processedUrl ) ;
38+ return processedUrl ;
39+ } ;
40+
41+ const getTimeSlotLabel = ( timeSlot : string ) => {
42+ const timeLabels : { [ key : string ] : string } = {
43+ 'morning' : '아침' ,
44+ 'afternoon' : '점심' ,
45+ 'evening' : '저녁'
46+ } ;
47+
48+ return timeLabels [ timeSlot ] || timeSlot ;
49+ } ;
50+
51+ const getCategoryColor = ( category : string ) => {
52+ return CATEGORY_COLORS [ category as keyof typeof CATEGORY_COLORS ] || '#666666' ;
53+ } ;
54+
55+ export default function EmotionForecastPopup ( { isOpen, onClose, forecasts } : EmotionForecastPopupProps ) {
56+ const [ currentIndex , setCurrentIndex ] = useState ( 0 ) ;
57+ const router = useRouter ( ) ;
58+ const [ dragDirection , setDragDirection ] = useState < 'left' | 'right' | null > ( null ) ;
59+ const [ hasSwiped , setHasSwiped ] = useState ( false ) ;
60+
61+ const handleClose = ( ) => {
62+ onClose ( ) ;
63+ router . push ( '/baby' ) ;
64+ } ;
65+
66+ useEffect ( ( ) => {
67+ if ( isOpen ) {
68+ setCurrentIndex ( 0 ) ;
69+ setHasSwiped ( false ) ;
70+ console . log ( '팝업 열림 - 감정 데이터:' , forecasts ) ;
71+ }
72+ } , [ isOpen , forecasts ] ) ;
73+
74+ // 키보드 이벤트 처리
75+ useEffect ( ( ) => {
76+ const handleKeyDown = ( event : KeyboardEvent ) => {
77+ if ( ! isOpen || forecasts . length <= 1 ) return ;
78+
79+ switch ( event . key ) {
80+ case 'ArrowLeft' :
81+ event . preventDefault ( ) ;
82+ setCurrentIndex ( ( prev ) => ( prev + 1 ) % forecasts . length ) ;
83+ setHasSwiped ( true ) ;
84+ break ;
85+ case 'ArrowRight' :
86+ event . preventDefault ( ) ;
87+ setCurrentIndex ( ( prev ) => ( prev - 1 + forecasts . length ) % forecasts . length ) ;
88+ setHasSwiped ( true ) ;
89+ break ;
90+ }
91+ } ;
92+
93+ if ( isOpen ) {
94+ document . addEventListener ( 'keydown' , handleKeyDown ) ;
95+ }
96+
97+ return ( ) => {
98+ document . removeEventListener ( 'keydown' , handleKeyDown ) ;
99+ } ;
100+ } , [ isOpen , forecasts . length ] ) ;
101+
102+ const nextSlide = ( ) => {
103+ setCurrentIndex ( ( prev ) => ( prev + 1 ) % forecasts . length ) ;
104+ } ;
105+
106+ const prevSlide = ( ) => {
107+ setCurrentIndex ( ( prev ) => ( prev - 1 + forecasts . length ) % forecasts . length ) ;
108+ } ;
109+
110+ const handleDragEnd = ( event : any , info : PanInfo ) => {
111+ const threshold = 50 ; // 슬라이드 임계값
112+
113+ if ( info . offset . x > threshold ) {
114+ // 오른쪽으로 드래그 - 이전 슬라이드
115+ setCurrentIndex ( ( prev ) => ( prev - 1 + forecasts . length ) % forecasts . length ) ;
116+ setHasSwiped ( true ) ;
117+ } else if ( info . offset . x < - threshold ) {
118+ // 왼쪽으로 드래그 - 다음 슬라이드
119+ setCurrentIndex ( ( prev ) => ( prev + 1 ) % forecasts . length ) ;
120+ setHasSwiped ( true ) ;
121+ }
122+
123+ setDragDirection ( null ) ;
124+ } ;
125+
126+ if ( ! isOpen || forecasts . length === 0 ) return null ;
127+
128+ const currentForecast = forecasts [ currentIndex ] ;
129+
130+ return (
131+ < AnimatePresence >
132+ < motion . div
133+ initial = { { opacity : 0 } }
134+ animate = { { opacity : 1 } }
135+ exit = { { opacity : 0 } }
136+ className = "fixed inset-0 bg-black bg-opacity-40 backdrop-blur-md flex items-center justify-center z-50 p-4"
137+ onClick = { handleClose }
138+ >
139+ < motion . div
140+ initial = { { scale : 0.8 , opacity : 0 } }
141+ animate = { { scale : 1 , opacity : 1 } }
142+ exit = { { scale : 0.8 , opacity : 0 } }
143+ className = "w-full max-w-sm relative aspect-[3/3] rounded-2xl overflow-hidden"
144+ style = { { backgroundColor : getCategoryColor ( currentForecast . category ) } }
145+ onClick = { ( e ) => e . stopPropagation ( ) }
146+ >
147+ { /* Slide Container */ }
148+ < motion . div
149+ key = { currentIndex }
150+ initial = { { x : 300 , opacity : 0 } }
151+ animate = { { x : 0 , opacity : 1 } }
152+ exit = { { x : - 300 , opacity : 0 } }
153+ transition = { {
154+ type : "spring" ,
155+ stiffness : 300 ,
156+ damping : 30 ,
157+ duration : 0.5
158+ } }
159+ drag = "x"
160+ dragConstraints = { { left : 0 , right : 0 } }
161+ dragElastic = { 0.2 }
162+ onDragEnd = { handleDragEnd }
163+ className = "w-full h-full cursor-grab active:cursor-grabbing"
164+ >
165+ { /* Forecast Card */ }
166+ < div className = "p-8 h-full flex flex-col" >
167+ { /* Top Row - Temperature */ }
168+ < div className = "mb-6" >
169+ { /* Temperature - 왼쪽 상단 */ }
170+ < div className = "flex justify-start" >
171+ < span
172+ className = "text-5xl font-bold text-white text-right"
173+ style = { { fontFamily : 'Cafe24Syongsyong, sans-serif' } }
174+ >
175+ { currentForecast . temperature } °
176+ </ span >
177+ </ div >
178+ </ div >
179+
180+ { /* Content Area */ }
181+ < div className = "flex-1 flex flex-col justify-center items-center" >
182+ { /* Emotion Icon - 가운데 */ }
183+ < div className = "w-40 h-40 mb-6" >
184+ < img
185+ src = { getEmotionImage ( currentForecast . image ) }
186+ alt = { currentForecast . emotion }
187+ className = "w-full h-full object-contain"
188+ onError = { ( e ) => {
189+ console . error ( '이미지 로딩 실패:' , currentForecast . image ) ;
190+ console . error ( '감정 데이터:' , currentForecast ) ;
191+ // 이미지 로딩 실패 시 기본 이모지 표시
192+ e . currentTarget . style . display = 'none' ;
193+ const fallbackDiv = document . createElement ( 'div' ) ;
194+ fallbackDiv . className = 'w-full h-full flex items-center justify-center text-4xl' ;
195+ fallbackDiv . textContent = '😐' ;
196+ e . currentTarget . parentNode ?. appendChild ( fallbackDiv ) ;
197+ } }
198+ onLoad = { ( ) => {
199+ console . log ( '이미지 로딩 성공:' , currentForecast . image ) ;
200+ } }
201+ />
202+ </ div >
203+ </ div >
204+
205+ { /* Bottom Text */ }
206+ < div className = "flex justify-end" >
207+ { /* Forecast Text */ }
208+ < div
209+ className = "text-white text-base font-medium leading-relaxed text-right"
210+ style = { { fontFamily : 'Cafe24Syongsyong, sans-serif' } }
211+ >
212+ { getTimeSlotLabel ( currentForecast . timeSlot ) } 의 감정은{ ' ' }
213+ < span className = "font-bold" > { currentForecast . emotion } </ span > 으로< br />
214+ { currentForecast . temperature } ° 예정이에요.
215+ </ div >
216+ </ div >
217+ </ div >
218+
219+ { /* Swipe Hint */ }
220+ { forecasts . length > 1 && ! hasSwiped && (
221+ < div className = "absolute bottom-4 left-1/2 transform -translate-x-1/2 text-white text-xs opacity-60 text-center" >
222+ < div > ← 슬라이드 →</ div >
223+ < div className = "mt-1" > 또는 화살표 키 사용</ div >
224+ </ div >
225+ ) }
226+ </ motion . div >
227+ </ motion . div >
228+ </ motion . div >
229+ </ AnimatePresence >
230+ ) ;
231+ }
0 commit comments