변경 전:
components/
├── pc/admin/ (플랫)
├── pc/public/ (플랫)
└── mobile/ (플랫)
변경 후:
components/
├── pc/admin/
│ ├── layout/ (Layout, Header)
│ ├── common/ (ConfirmDialog, DatePicker, TimePicker, NumberPicker)
│ └── schedule/ (AdminScheduleCard, CategorySelector)
├── pc/public/
│ ├── layout/ (Layout, Header, Footer)
│ └── schedule/ (Calendar, ScheduleCard, BirthdayCard, CategoryFilter)
└── mobile/
├── layout/ (Layout, Header, BottomNav)
└── schedule/ (Calendar, ScheduleCard 등)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
85 lines
3 KiB
JavaScript
85 lines
3 KiB
JavaScript
import { memo } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { dayjs, decodeHtmlEntities } from '@/utils';
|
|
|
|
/**
|
|
* Mobile용 생일 카드 컴포넌트
|
|
* @param {Object} schedule - 일정 데이터
|
|
* @param {boolean} showYear - 년도 표시 여부
|
|
* @param {number} delay - 애니메이션 딜레이 (초)
|
|
* @param {function} onClick - 클릭 핸들러
|
|
*/
|
|
const BirthdayCard = memo(function BirthdayCard({ schedule, showYear = false, delay = 0, onClick }) {
|
|
const scheduleDate = dayjs(schedule.date);
|
|
const formatted = {
|
|
year: scheduleDate.year(),
|
|
month: scheduleDate.month() + 1,
|
|
day: scheduleDate.date(),
|
|
};
|
|
|
|
const CardContent = (
|
|
<div className="relative overflow-hidden bg-gradient-to-r from-pink-400 via-purple-400 to-indigo-400 rounded-xl shadow-md hover:shadow-lg transition-shadow cursor-pointer">
|
|
{/* 배경 장식 */}
|
|
<div className="absolute inset-0 overflow-hidden">
|
|
<div className="absolute -top-3 -right-3 w-16 h-16 bg-white/10 rounded-full" />
|
|
<div className="absolute -bottom-4 -left-4 w-20 h-20 bg-white/10 rounded-full" />
|
|
<div className="absolute bottom-3 left-8 text-sm">🎉</div>
|
|
</div>
|
|
|
|
<div className="relative flex items-center p-4 gap-3">
|
|
{/* 멤버 사진 */}
|
|
{schedule.member_image && (
|
|
<div className="flex-shrink-0">
|
|
<div className="w-14 h-14 rounded-full border-2 border-white/50 shadow-md overflow-hidden bg-white">
|
|
<img
|
|
src={schedule.member_image}
|
|
alt={schedule.member_names}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 내용 */}
|
|
<div className="flex-1 text-white flex items-center gap-2 min-w-0">
|
|
<span className="text-2xl flex-shrink-0">🎂</span>
|
|
<h3 className="font-bold text-base tracking-wide truncate">
|
|
{decodeHtmlEntities(schedule.title)}
|
|
</h3>
|
|
</div>
|
|
|
|
{/* 날짜 뱃지 (showYear가 true일 때만 표시) */}
|
|
{showYear && (
|
|
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-lg px-3 py-1.5 text-center">
|
|
<div className="text-white/70 text-[10px] font-medium">{formatted.year}</div>
|
|
<div className="text-white/70 text-[10px] font-medium">{formatted.month}월</div>
|
|
<div className="text-white text-xl font-bold">{formatted.day}</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
// delay가 있으면 motion 사용
|
|
if (delay > 0) {
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, x: -10 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay, type: 'spring', stiffness: 300, damping: 30 }}
|
|
onClick={onClick}
|
|
className="cursor-pointer"
|
|
>
|
|
{CardContent}
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div onClick={onClick} className="cursor-pointer">
|
|
{CardContent}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
export default BirthdayCard;
|