refactor(frontend): 일정 카드를 공유 컴포넌트로 분리
- ScheduleCard (PC): 카테고리와 source name 분리, Link2 아이콘 사용 - MobileScheduleCard (Mobile): 동일한 구조로 분리 - Home 페이지에서 컴포넌트 사용하도록 변경 - 멤버 5명 이상이면 '프로미스나인'으로 표시 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d660340cc5
commit
9b96c475a7
6 changed files with 236 additions and 185 deletions
|
|
@ -1,6 +1,9 @@
|
|||
// 공통 컴포넌트 (디바이스 무관)
|
||||
export * from './common';
|
||||
|
||||
// 스케줄 컴포넌트
|
||||
export * from './schedule';
|
||||
|
||||
// PC 컴포넌트
|
||||
export * as PC from './pc';
|
||||
|
||||
|
|
|
|||
101
frontend-temp/src/components/schedule/MobileScheduleCard.jsx
Normal file
101
frontend-temp/src/components/schedule/MobileScheduleCard.jsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { Clock, Tag, Link2 } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* Mobile 일정 카드 컴포넌트
|
||||
* 홈, 스케줄 페이지 등에서 공통으로 사용
|
||||
*/
|
||||
function MobileScheduleCard({ schedule, onClick, className = '' }) {
|
||||
const scheduleDate = new Date(schedule.date);
|
||||
const today = new Date();
|
||||
const currentYear = today.getFullYear();
|
||||
const currentMonth = today.getMonth();
|
||||
|
||||
const scheduleYear = scheduleDate.getFullYear();
|
||||
const scheduleMonth = scheduleDate.getMonth();
|
||||
const isCurrentYear = scheduleYear === currentYear;
|
||||
const isCurrentMonth = isCurrentYear && scheduleMonth === currentMonth;
|
||||
|
||||
// 멤버 처리
|
||||
const memberList = schedule.member_names
|
||||
? schedule.member_names.split(',').map((n) => n.trim()).filter(Boolean)
|
||||
: schedule.members?.map((m) => m.name) || [];
|
||||
|
||||
// 5명 이상이면 '프로미스나인'으로 표시
|
||||
const displayMembers = memberList.length >= 5 ? ['프로미스나인'] : memberList;
|
||||
|
||||
const categoryName = schedule.category_name || schedule.category?.name;
|
||||
const sourceName = schedule.source?.name;
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={`flex gap-4 bg-white p-4 rounded-xl shadow-sm border border-gray-100 overflow-hidden ${className}`}
|
||||
>
|
||||
{/* 날짜 영역 */}
|
||||
<div className="flex flex-col items-center justify-center min-w-[50px]">
|
||||
{!isCurrentYear && (
|
||||
<span className="text-[10px] text-gray-400 font-medium">
|
||||
{scheduleYear}.{scheduleMonth + 1}
|
||||
</span>
|
||||
)}
|
||||
{isCurrentYear && !isCurrentMonth && (
|
||||
<span className="text-[10px] text-gray-400 font-medium">
|
||||
{scheduleMonth + 1}월
|
||||
</span>
|
||||
)}
|
||||
<span className="text-2xl font-bold text-primary">
|
||||
{scheduleDate.getDate()}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400 font-medium">
|
||||
{['일', '월', '화', '수', '목', '금', '토'][scheduleDate.getDay()]}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 세로 구분선 */}
|
||||
<div className="w-px bg-gray-100" />
|
||||
|
||||
{/* 내용 영역 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-semibold text-sm text-gray-800 line-clamp-2 leading-snug">
|
||||
{schedule.title}
|
||||
</p>
|
||||
{/* 시간 + 카테고리 + 소스 */}
|
||||
<div className="flex flex-wrap items-center gap-3 mt-2 text-xs text-gray-400">
|
||||
{schedule.time && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock size={12} />
|
||||
{schedule.time.slice(0, 5)}
|
||||
</span>
|
||||
)}
|
||||
{categoryName && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Tag size={12} />
|
||||
{categoryName}
|
||||
</span>
|
||||
)}
|
||||
{sourceName && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Link2 size={12} />
|
||||
{sourceName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* 멤버 */}
|
||||
{displayMembers.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{displayMembers.map((name, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="px-2 py-0.5 bg-primary/10 text-primary text-[10px] rounded-full font-medium"
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MobileScheduleCard;
|
||||
98
frontend-temp/src/components/schedule/ScheduleCard.jsx
Normal file
98
frontend-temp/src/components/schedule/ScheduleCard.jsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { Clock, Tag, Link2 } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* PC 일정 카드 컴포넌트
|
||||
* 홈, 스케줄 페이지 등에서 공통으로 사용
|
||||
*/
|
||||
function ScheduleCard({ schedule, onClick, className = '' }) {
|
||||
const scheduleDate = new Date(schedule.date);
|
||||
const today = new Date();
|
||||
const currentYear = today.getFullYear();
|
||||
const currentMonth = today.getMonth();
|
||||
|
||||
const scheduleYear = scheduleDate.getFullYear();
|
||||
const scheduleMonth = scheduleDate.getMonth();
|
||||
const isCurrentYear = scheduleYear === currentYear;
|
||||
const isCurrentMonth = isCurrentYear && scheduleMonth === currentMonth;
|
||||
|
||||
const day = scheduleDate.getDate();
|
||||
const weekdays = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
const weekday = weekdays[scheduleDate.getDay()];
|
||||
|
||||
// 멤버 처리
|
||||
const memberList = schedule.member_names
|
||||
? schedule.member_names.split(',').map((n) => n.trim()).filter(Boolean)
|
||||
: schedule.members?.map((m) => m.name) || [];
|
||||
|
||||
// 5명 이상이면 '프로미스나인'으로 표시
|
||||
const displayMembers = memberList.length >= 5 ? ['프로미스나인'] : memberList;
|
||||
|
||||
const categoryColor = schedule.category_color || '#6366f1';
|
||||
const categoryName = schedule.category_name || schedule.category?.name;
|
||||
const sourceName = schedule.source?.name;
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={`flex items-stretch bg-white rounded-2xl shadow-sm hover:shadow-md transition-shadow overflow-hidden cursor-pointer ${className}`}
|
||||
>
|
||||
{/* 날짜 영역 */}
|
||||
<div
|
||||
className="w-24 flex flex-col items-center justify-center text-white py-6"
|
||||
style={{ backgroundColor: categoryColor }}
|
||||
>
|
||||
{!isCurrentYear && (
|
||||
<span className="text-xs font-medium opacity-70">
|
||||
{scheduleYear}.{scheduleMonth + 1}
|
||||
</span>
|
||||
)}
|
||||
{isCurrentYear && !isCurrentMonth && (
|
||||
<span className="text-xs font-medium opacity-70">
|
||||
{scheduleMonth + 1}월
|
||||
</span>
|
||||
)}
|
||||
<span className="text-3xl font-bold">{day}</span>
|
||||
<span className="text-sm font-medium opacity-80">{weekday}</span>
|
||||
</div>
|
||||
|
||||
{/* 내용 영역 */}
|
||||
<div className="flex-1 p-6 flex flex-col justify-center">
|
||||
<h3 className="font-bold text-lg mb-2">{schedule.title}</h3>
|
||||
<div className="flex flex-wrap gap-3 text-base text-gray-500">
|
||||
{schedule.time && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock size={16} className="opacity-60" />
|
||||
{schedule.time.slice(0, 5)}
|
||||
</span>
|
||||
)}
|
||||
{categoryName && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Tag size={16} className="opacity-60" />
|
||||
{categoryName}
|
||||
</span>
|
||||
)}
|
||||
{sourceName && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Link2 size={16} className="opacity-60" />
|
||||
{sourceName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{displayMembers.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mt-2">
|
||||
{displayMembers.map((name, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="px-2 py-0.5 bg-primary/10 text-primary text-sm font-medium rounded-full"
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ScheduleCard;
|
||||
2
frontend-temp/src/components/schedule/index.js
Normal file
2
frontend-temp/src/components/schedule/index.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as ScheduleCard } from './ScheduleCard';
|
||||
export { default as MobileScheduleCard } from './MobileScheduleCard';
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { motion } from 'framer-motion';
|
||||
import { ChevronRight, Clock, Tag } from 'lucide-react';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useMembers, useAlbums, useUpcomingSchedules } from '@/hooks';
|
||||
import { MobileScheduleCard } from '@/components';
|
||||
|
||||
/**
|
||||
* Mobile 홈 페이지
|
||||
|
|
@ -157,101 +158,20 @@ function MobileHome() {
|
|||
</div>
|
||||
{schedules.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{schedules.map((schedule, index) => {
|
||||
const scheduleDate = new Date(schedule.date);
|
||||
const today = new Date();
|
||||
const currentYear = today.getFullYear();
|
||||
const currentMonth = today.getMonth();
|
||||
|
||||
const scheduleYear = scheduleDate.getFullYear();
|
||||
const scheduleMonth = scheduleDate.getMonth();
|
||||
const isCurrentYear = scheduleYear === currentYear;
|
||||
const isCurrentMonth =
|
||||
isCurrentYear && scheduleMonth === currentMonth;
|
||||
|
||||
// 멤버 처리
|
||||
const memberList = schedule.member_names
|
||||
? schedule.member_names
|
||||
.split(',')
|
||||
.map((n) => n.trim())
|
||||
.filter(Boolean)
|
||||
: schedule.members?.map((m) => m.name) || [];
|
||||
|
||||
return (
|
||||
{schedules.map((schedule, index) => (
|
||||
<motion.div
|
||||
key={schedule.id}
|
||||
className="flex gap-4 bg-white p-4 rounded-xl shadow-sm border border-gray-100 overflow-hidden"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.8 + index * 0.1, duration: 0.3 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<MobileScheduleCard
|
||||
schedule={schedule}
|
||||
onClick={() => navigate('/schedule')}
|
||||
>
|
||||
{/* 날짜 영역 */}
|
||||
<div className="flex flex-col items-center justify-center min-w-[50px]">
|
||||
{!isCurrentYear && (
|
||||
<span className="text-[10px] text-gray-400 font-medium">
|
||||
{scheduleYear}.{scheduleMonth + 1}
|
||||
</span>
|
||||
)}
|
||||
{isCurrentYear && !isCurrentMonth && (
|
||||
<span className="text-[10px] text-gray-400 font-medium">
|
||||
{scheduleMonth + 1}월
|
||||
</span>
|
||||
)}
|
||||
<span className="text-2xl font-bold text-primary">
|
||||
{scheduleDate.getDate()}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400 font-medium">
|
||||
{
|
||||
['일', '월', '화', '수', '목', '금', '토'][
|
||||
scheduleDate.getDay()
|
||||
]
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 세로 구분선 */}
|
||||
<div className="w-px bg-gray-100" />
|
||||
|
||||
{/* 내용 영역 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-semibold text-sm text-gray-800 line-clamp-2 leading-snug">
|
||||
{schedule.title}
|
||||
</p>
|
||||
{/* 시간 + 카테고리 */}
|
||||
<div className="flex items-center gap-3 mt-2 text-xs text-gray-400">
|
||||
{schedule.time && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock size={12} />
|
||||
{schedule.time.slice(0, 5)}
|
||||
</span>
|
||||
)}
|
||||
{schedule.category_name && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Tag size={12} />
|
||||
{schedule.category_name}
|
||||
{schedule.source?.name && ` · ${schedule.source.name}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* 멤버 */}
|
||||
{memberList.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{memberList.map((name, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="px-2 py-0.5 bg-primary/10 text-primary text-[10px] rounded-full font-medium"
|
||||
>
|
||||
{name.trim()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { motion } from 'framer-motion';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Calendar, ArrowRight, Clock, Tag, Music } from 'lucide-react';
|
||||
import { Calendar, ArrowRight, Music } from 'lucide-react';
|
||||
import { useMembers, useAlbums, useUpcomingSchedules } from '@/hooks';
|
||||
import { ScheduleCard } from '@/components';
|
||||
|
||||
/**
|
||||
* PC 홈 페이지
|
||||
|
|
@ -218,30 +219,7 @@ function Home() {
|
|||
visible: { opacity: 1, transition: { staggerChildren: 0.1 } },
|
||||
}}
|
||||
>
|
||||
{upcomingSchedules.map((schedule) => {
|
||||
const scheduleDate = new Date(schedule.date);
|
||||
const todayDate = new Date();
|
||||
const currentYear = todayDate.getFullYear();
|
||||
const currentMonth = todayDate.getMonth();
|
||||
|
||||
const scheduleYear = scheduleDate.getFullYear();
|
||||
const scheduleMonth = scheduleDate.getMonth();
|
||||
const isCurrentYear = scheduleYear === currentYear;
|
||||
const isCurrentMonth =
|
||||
isCurrentYear && scheduleMonth === currentMonth;
|
||||
|
||||
const day = scheduleDate.getDate();
|
||||
const weekdays = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
const weekday = weekdays[scheduleDate.getDay()];
|
||||
|
||||
// 멤버 처리
|
||||
const memberList = schedule.member_names
|
||||
? schedule.member_names.split(',')
|
||||
: schedule.members?.map((m) => m.name) || [];
|
||||
|
||||
const categoryColor = schedule.category_color || '#6366f1';
|
||||
|
||||
return (
|
||||
{upcomingSchedules.map((schedule) => (
|
||||
<motion.div
|
||||
key={schedule.id}
|
||||
variants={{
|
||||
|
|
@ -252,61 +230,10 @@ function Home() {
|
|||
transition: { duration: 0.4, ease: 'easeOut' },
|
||||
},
|
||||
}}
|
||||
className="flex items-stretch bg-white rounded-2xl shadow-sm hover:shadow-md transition-shadow overflow-hidden cursor-pointer"
|
||||
>
|
||||
{/* 날짜 영역 */}
|
||||
<div
|
||||
className="w-24 flex flex-col items-center justify-center text-white py-6"
|
||||
style={{ backgroundColor: categoryColor }}
|
||||
>
|
||||
{!isCurrentYear && (
|
||||
<span className="text-xs font-medium opacity-70">
|
||||
{scheduleYear}.{scheduleMonth + 1}
|
||||
</span>
|
||||
)}
|
||||
{isCurrentYear && !isCurrentMonth && (
|
||||
<span className="text-xs font-medium opacity-70">
|
||||
{scheduleMonth + 1}월
|
||||
</span>
|
||||
)}
|
||||
<span className="text-3xl font-bold">{day}</span>
|
||||
<span className="text-sm font-medium opacity-80">
|
||||
{weekday}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 내용 영역 */}
|
||||
<div className="flex-1 p-6 flex flex-col justify-center">
|
||||
<h3 className="font-bold text-lg mb-2">{schedule.title}</h3>
|
||||
<div className="flex flex-wrap gap-3 text-base text-gray-500">
|
||||
{schedule.time && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock size={16} className="opacity-60" />
|
||||
{schedule.time.slice(0, 5)}
|
||||
</span>
|
||||
)}
|
||||
<span className="flex items-center gap-1">
|
||||
<Tag size={16} className="opacity-60" />
|
||||
{schedule.category_name}
|
||||
{schedule.source?.name && ` · ${schedule.source.name}`}
|
||||
</span>
|
||||
</div>
|
||||
{memberList.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mt-2">
|
||||
{memberList.map((name, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="px-2 py-0.5 bg-primary/10 text-primary text-sm font-medium rounded-full"
|
||||
>
|
||||
{name.trim()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ScheduleCard schedule={schedule} />
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue