fromis_9/frontend/src/components/pc/admin/schedule/ScheduleItem.jsx

173 lines
5.7 KiB
React
Raw Normal View History

/**
* 일정 아이템 컴포넌트
* - 일정 목록에서 사용되는 개별 아이템
* - 일반 모드와 검색 모드에서 공통 사용
*/
import { memo } from 'react';
import { motion } from 'framer-motion';
import { Edit2, Trash2, ExternalLink, Clock, Tag, Link2 } from 'lucide-react';
import { decodeHtmlEntities } from '@/utils';
import {
getMemberList,
getScheduleDate,
getScheduleTime,
getCategoryInfo,
} from '@/utils/schedule';
/**
* 카테고리별 수정 경로 반환
*/
export const getEditPath = (scheduleId, categoryName) => {
switch (categoryName) {
case '유튜브':
return `/admin/schedule/${scheduleId}/edit/youtube`;
case 'X':
return `/admin/schedule/${scheduleId}/edit/x`;
default:
return `/admin/schedule/${scheduleId}/edit`;
}
};
/**
* 일정 아이템 컴포넌트 - React.memo로 불필요한 리렌더링 방지
* @param {Object} props
* @param {Object} props.schedule - 일정 데이터
* @param {number} props.index - 목록 인덱스 (애니메이션 지연용)
* @param {string} props.selectedDate - 선택된 날짜
* @param {Function} props.getColorStyle - 색상 스타일 함수
* @param {Function} props.navigate - 네비게이션 함수
* @param {Function} props.openDeleteDialog - 삭제 다이얼로그 열기 함수
* @param {boolean} props.showYear - 연도 표시 여부 (검색 모드용)
* @param {boolean} props.animated - 애니메이션 적용 여부 (기본: true)
* @param {string} props.className - 추가 클래스명
*/
const ScheduleItem = memo(function ScheduleItem({
schedule,
index = 0,
selectedDate,
getColorStyle,
navigate,
openDeleteDialog,
showYear = false,
animated = true,
className = '',
}) {
const scheduleDate = new Date(getScheduleDate(schedule));
const isBirthday = schedule.is_birthday || String(schedule.id).startsWith('birthday-');
const categoryInfo = getCategoryInfo(schedule);
const categoryColor =
getColorStyle(categoryInfo.color)?.style?.backgroundColor || categoryInfo.color || '#6b7280';
const memberList = getMemberList(schedule);
const timeStr = getScheduleTime(schedule);
const content = (
<div className="flex items-start gap-4">
<div className="w-20 text-center flex-shrink-0">
{showYear && (
<div className="text-xs text-gray-400 mb-0.5">
{scheduleDate.getFullYear()}.{scheduleDate.getMonth() + 1}
</div>
)}
<div className="text-2xl font-bold text-gray-900">{scheduleDate.getDate()}</div>
<div className="text-sm text-gray-500">
{['일', '월', '화', '수', '목', '금', '토'][scheduleDate.getDay()]}요일
</div>
</div>
<div
className="w-1.5 rounded-full flex-shrink-0 self-stretch"
style={{ backgroundColor: categoryColor }}
/>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900">{decodeHtmlEntities(schedule.title)}</h3>
<div className="flex items-center gap-3 mt-1 text-sm text-gray-500">
{timeStr && (
<span className="flex items-center gap-1">
<Clock size={14} />
{timeStr}
</span>
)}
<span className="flex items-center gap-1">
<Tag size={14} />
{categoryInfo.name}
</span>
{schedule.source?.name && (
<span className="flex items-center gap-1">
<Link2 size={14} />
{schedule.source?.name}
</span>
)}
</div>
{memberList.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-2">
{memberList.length >= 5 ? (
<span className="px-2 py-0.5 bg-primary/10 text-primary text-xs font-medium rounded-full">
프로미스나인
</span>
) : (
memberList.map((name, i) => (
<span
key={i}
className="px-2 py-0.5 bg-primary/10 text-primary text-xs font-medium rounded-full"
>
{name.trim()}
</span>
))
)}
</div>
)}
</div>
{/* 생일 일정은 수정/삭제 불가 */}
{!isBirthday && (
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
{schedule.source?.url && (
<a
href={schedule.source?.url}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="p-2 hover:bg-blue-100 rounded-lg transition-colors text-blue-500"
>
<ExternalLink size={18} />
</a>
)}
<button
onClick={() => navigate(getEditPath(schedule.id, categoryInfo.name))}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors text-gray-500"
>
<Edit2 size={18} />
</button>
<button
onClick={() => openDeleteDialog(schedule)}
className="p-2 hover:bg-red-100 rounded-lg transition-colors text-red-500"
>
<Trash2 size={18} />
</button>
</div>
)}
</div>
);
const baseClassName = `${showYear ? 'p-5' : 'p-6'} hover:bg-gray-50 transition-colors group ${className}`;
if (animated) {
return (
<motion.div
key={`${schedule.id}-${selectedDate || 'all'}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: Math.min(index, 10) * 0.03 }}
className={baseClassName}
>
{content}
</motion.div>
);
}
return <div className={baseClassName}>{content}</div>;
});
export default ScheduleItem;