Skip to content

Commit 84fc29c

Browse files
authored
Merge pull request #24 from HeartForecast/feat/#23
예보/기록 생성 후 팝업 띄우기
2 parents c4ad6df + f9ebdc8 commit 84fc29c

27 files changed

Lines changed: 1129 additions & 286 deletions
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
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

Comments
 (0)