feat(schedule-pc): 날짜 미정 일정 렌더링 (B안)
date_precision='month' 일정을 점선 "N월 중" 카드(UndatedScheduleCard)로 표시. 선택 날짜와 무관하게 해당 월이면 확정 일정 아래에 배치하고 사이에 "날짜 미정" 구분선 추가. 카운트에도 포함. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
44d30d48f6
commit
39aadf50e6
3 changed files with 119 additions and 18 deletions
|
|
@ -0,0 +1,66 @@
|
|||
import { memo } from 'react';
|
||||
import { Tag, Link2 } from 'lucide-react';
|
||||
import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo } from '@/utils';
|
||||
|
||||
/**
|
||||
* 날짜 미정(월만 확정) 일정 카드 (PC)
|
||||
* date_precision === 'month' 인 일정에 사용. 날짜 대신 "날짜 미정"으로 표시하고
|
||||
* 점선 테두리로 확정 일정과 구분한다.
|
||||
*/
|
||||
const UndatedScheduleCard = memo(function UndatedScheduleCard({ schedule, onClick, className = '' }) {
|
||||
const categoryInfo = getCategoryInfo(schedule);
|
||||
const displayMembers = getDisplayMembers(schedule);
|
||||
const sourceName = schedule.source?.name;
|
||||
|
||||
// date는 해당 월 1일(YYYY-MM-01)로 저장됨 → 월만 추출
|
||||
const month = new Date(schedule.date).getMonth() + 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={`flex items-stretch bg-white rounded-2xl border-2 border-dashed border-gray-300 hover:border-gray-400 transition-colors overflow-hidden cursor-pointer ${className}`}
|
||||
>
|
||||
{/* 월 영역 (연한 카테고리 색) */}
|
||||
<div
|
||||
className="w-24 flex flex-col items-center justify-center py-6 leading-tight"
|
||||
style={{ backgroundColor: `${categoryInfo.color}1a`, color: categoryInfo.color }}
|
||||
>
|
||||
<span className="text-2xl font-bold">{month}월</span>
|
||||
<span className="text-xs font-semibold mt-1 opacity-90">중</span>
|
||||
</div>
|
||||
|
||||
{/* 내용 */}
|
||||
<div className="flex-1 p-6 flex flex-col justify-center">
|
||||
<h3 className="font-bold text-lg mb-2">{decodeHtmlEntities(schedule.title)}</h3>
|
||||
<div className="flex flex-wrap gap-3 text-base text-gray-500">
|
||||
{categoryInfo.name && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Tag size={16} className="opacity-60" />
|
||||
{categoryInfo.name}
|
||||
</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 UndatedScheduleCard;
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
export { default as Calendar } from './Calendar';
|
||||
export { default as ScheduleCard } from './ScheduleCard';
|
||||
export { default as UndatedScheduleCard } from './UndatedScheduleCard';
|
||||
export { default as BirthdayCard } from './BirthdayCard';
|
||||
export { default as DebutCard } from './DebutCard';
|
||||
export { default as CategoryFilter } from './CategoryFilter';
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
Calendar,
|
||||
CategoryFilter,
|
||||
ScheduleCard,
|
||||
UndatedScheduleCard,
|
||||
BirthdayCard,
|
||||
DebutCard,
|
||||
} from '@/components/pc/public';
|
||||
|
|
@ -255,12 +256,24 @@ function PCSchedule() {
|
|||
}
|
||||
|
||||
return schedules.filter((s) => {
|
||||
if (s.datePrecision === 'month') return false; // 날짜 미정은 별도 처리
|
||||
const matchesDate = selectedDate ? s.date === selectedDate : s.date?.startsWith(currentYearMonth);
|
||||
const matchesCategory = selectedCategories.length === 0 || selectedCategories.includes(s.category_id);
|
||||
return matchesDate && matchesCategory;
|
||||
});
|
||||
}, [schedules, selectedDate, currentYearMonth, selectedCategories, isSearchMode, searchTerm, searchResults]);
|
||||
|
||||
// 날짜 미정(월만 확정) 일정 — 선택 날짜와 무관하게 해당 월이면 항상 하단에 표시
|
||||
const undatedSchedules = useMemo(() => {
|
||||
if (isSearchMode) return [];
|
||||
return schedules.filter((s) => {
|
||||
if (s.datePrecision !== 'month') return false;
|
||||
const matchesMonth = s.date?.startsWith(currentYearMonth);
|
||||
const matchesCategory = selectedCategories.length === 0 || selectedCategories.includes(s.category_id);
|
||||
return matchesMonth && matchesCategory;
|
||||
});
|
||||
}, [schedules, currentYearMonth, selectedCategories, isSearchMode]);
|
||||
|
||||
// 달력 점 표시용 (카테고리만 필터링, 날짜는 한 달 전체 유지)
|
||||
const calendarSchedules = useMemo(() => {
|
||||
if (selectedCategories.length === 0) return schedules;
|
||||
|
|
@ -538,7 +551,7 @@ function PCSchedule() {
|
|||
</AnimatePresence>
|
||||
</div>
|
||||
)}
|
||||
<span className="text-sm text-gray-400">{filteredSchedules.length}개 일정</span>
|
||||
<span className="text-sm text-gray-400">{filteredSchedules.length + undatedSchedules.length}개 일정</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
@ -548,7 +561,7 @@ function PCSchedule() {
|
|||
<div ref={scrollContainerRef} className="flex-1 min-h-0 overflow-y-auto space-y-4 py-2 pr-2">
|
||||
{loading ? (
|
||||
<div className="h-full flex items-center justify-center text-gray-500">로딩 중...</div>
|
||||
) : filteredSchedules.length > 0 ? (
|
||||
) : filteredSchedules.length > 0 || undatedSchedules.length > 0 ? (
|
||||
isSearchMode && searchTerm ? (
|
||||
<>
|
||||
<div style={{ height: `${virtualizer.getTotalSize()}px`, width: '100%', position: 'relative' }}>
|
||||
|
|
@ -593,22 +606,43 @@ function PCSchedule() {
|
|||
</div>
|
||||
</>
|
||||
) : (
|
||||
filteredSchedules.map((schedule, index) => (
|
||||
<motion.div
|
||||
key={`${schedule.id}-${selectedDate || 'all'}`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: Math.min(index, 10) * 0.03 }}
|
||||
>
|
||||
{schedule.is_birthday ? (
|
||||
<BirthdayCard schedule={schedule} onClick={() => handleScheduleClick(schedule)} />
|
||||
) : schedule.is_debut || schedule.is_anniversary ? (
|
||||
<DebutCard schedule={schedule} />
|
||||
) : (
|
||||
<ScheduleCard schedule={schedule} onClick={() => handleScheduleClick(schedule)} />
|
||||
)}
|
||||
</motion.div>
|
||||
))
|
||||
<>
|
||||
{filteredSchedules.map((schedule, index) => (
|
||||
<motion.div
|
||||
key={`${schedule.id}-${selectedDate || 'all'}`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: Math.min(index, 10) * 0.03 }}
|
||||
>
|
||||
{schedule.is_birthday ? (
|
||||
<BirthdayCard schedule={schedule} onClick={() => handleScheduleClick(schedule)} />
|
||||
) : schedule.is_debut || schedule.is_anniversary ? (
|
||||
<DebutCard schedule={schedule} />
|
||||
) : (
|
||||
<ScheduleCard schedule={schedule} onClick={() => handleScheduleClick(schedule)} />
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{/* 날짜 미정 일정 — 우선순위 낮춰 기존 일정 아래 배치 */}
|
||||
{undatedSchedules.length > 0 && filteredSchedules.length > 0 && (
|
||||
<div className="flex items-center gap-3 pt-2 text-gray-300">
|
||||
<div className="flex-1 border-t border-dashed border-gray-200" />
|
||||
<span className="text-xs font-medium text-gray-400">날짜 미정</span>
|
||||
<div className="flex-1 border-t border-dashed border-gray-200" />
|
||||
</div>
|
||||
)}
|
||||
{undatedSchedules.map((schedule, index) => (
|
||||
<motion.div
|
||||
key={`undated-${schedule.id}`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: Math.min(filteredSchedules.length + index, 10) * 0.03 }}
|
||||
>
|
||||
<UndatedScheduleCard schedule={schedule} onClick={() => handleScheduleClick(schedule)} />
|
||||
</motion.div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
) : isSearchMode && searchTerm ? (
|
||||
isSearchLoading ? (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue