feat(schedule-mobile): 날짜 미정 일정 렌더링 (B안)

모바일 일정 페이지에 date_precision='month' 일정을 점선 "N월 중"
카드(UndatedScheduleListCard)로 표시. 선택 날짜와 무관하게 해당
달이면 확정 일정 아래 "날짜 미정" 구분선과 함께 배치.
캘린더/날짜 점은 1일에 찍지 않도록 PC·모바일 dot 목록에서 제외.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-06-02 20:27:07 +09:00
parent 39aadf50e6
commit 39a6225897
4 changed files with 125 additions and 5 deletions

View file

@ -0,0 +1,86 @@
import { memo } from 'react';
import { motion } from 'framer-motion';
import { Link2 } from 'lucide-react';
import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo } from '@/utils';
/**
* Mobile용 날짜 미정(월만 확정) 일정 카드
* date_precision === 'month' 일정에 사용. 시간 대신 "N월 중" 표시하고
* 점선 테두리로 확정 일정과 구분한다.
*/
const UndatedScheduleListCard = memo(function UndatedScheduleListCard({
schedule,
onClick,
delay = 0,
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 (
<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 ${className}`}
>
{/* 카드 본체 (점선 테두리) */}
<div className="relative bg-white rounded-xl border-[1.5px] border-dashed border-gray-300 overflow-hidden active:bg-gray-50 transition-colors">
<div className="p-4">
{/* "N월 중" 및 카테고리 뱃지 */}
<div className="flex items-center gap-1.5 mb-2">
<div
className="px-2 py-0.5 rounded-full text-white text-xs font-medium"
style={{ backgroundColor: categoryInfo.color }}
>
{month}
</div>
<span
className="px-2 py-0.5 rounded-full text-xs font-medium"
style={{
backgroundColor: `${categoryInfo.color}15`,
color: categoryInfo.color,
}}
>
{categoryInfo.name}
</span>
</div>
{/* 제목 */}
<h3 className="font-bold text-[15px] text-gray-800 leading-snug">
{decodeHtmlEntities(schedule.title)}
</h3>
{/* 출처 */}
{sourceName && (
<div className="flex items-center gap-1 mt-1.5 text-xs text-gray-400">
<Link2 size={11} />
<span>{sourceName}</span>
</div>
)}
{/* 멤버 */}
{displayMembers.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-3 pt-3 border-t border-gray-100">
{displayMembers.map((name, i) => (
<span
key={i}
className="px-2.5 py-1 bg-gradient-to-r from-primary to-primary-dark text-white text-xs rounded-lg font-semibold shadow-sm"
>
{name}
</span>
))}
</div>
)}
</div>
</div>
</motion.div>
);
});
export default UndatedScheduleListCard;

View file

@ -1,6 +1,7 @@
export { default as Calendar } from './Calendar';
export { default as ScheduleCard } from './ScheduleCard';
export { default as ScheduleListCard } from './ScheduleListCard';
export { default as UndatedScheduleListCard } from './UndatedScheduleListCard';
export { default as ScheduleSearchCard } from './ScheduleSearchCard';
export { default as BirthdayCard } from './BirthdayCard';
export { default as DebutCard } from './DebutCard';

View file

@ -13,6 +13,7 @@ import { MIN_YEAR, SEARCH_LIMIT } from '@/constants';
import {
Calendar as MobileCalendar,
ScheduleListCard as MobileScheduleListCard,
UndatedScheduleListCard as MobileUndatedScheduleListCard,
ScheduleSearchCard as MobileScheduleSearchCard,
BirthdayCard as MobileBirthdayCard,
DebutCard as MobileDebutCard,
@ -354,11 +355,20 @@ function MobileSchedule() {
const dateStr = `${year}-${month}-${day}`;
// ( )
return schedules.filter((s) => {
if (s.datePrecision === 'month') return false; //
if (s.date.split('T')[0] !== dateStr) return false;
return selectedCategories.length === 0 || selectedCategories.includes(s.category_id);
});
}, [schedules, selectedDate, selectedCategories]);
// ( )
const undatedSchedules = useMemo(() => {
return schedules.filter((s) => {
if (s.datePrecision !== 'month') return false;
return selectedCategories.length === 0 || selectedCategories.includes(s.category_id);
});
}, [schedules, selectedCategories]);
// ( , calendarSchedules )
const monthCategories = useMemo(() => {
const map = new Map();
@ -376,9 +386,12 @@ function MobileSchedule() {
}, [schedules]);
// ( , )
// ( )
const dotSchedules = useMemo(() => {
if (selectedCategories.length === 0) return schedules;
return schedules.filter((s) => selectedCategories.includes(s.category_id));
return schedules.filter((s) => {
if (s.datePrecision === 'month') return false;
return selectedCategories.length === 0 || selectedCategories.includes(s.category_id);
});
}, [schedules, selectedCategories]);
//
@ -848,7 +861,7 @@ function MobileSchedule() {
<div className="flex-1 flex items-center justify-center">
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
</div>
) : selectedDateSchedules.length === 0 ? (
) : selectedDateSchedules.length === 0 && undatedSchedules.length === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center">
<div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mb-3">
<svg className="w-10 h-10 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -895,6 +908,23 @@ function MobileSchedule() {
/>
);
})}
{/* 날짜 미정 일정 — 우선순위 낮춰 기존 일정 아래 배치 */}
{undatedSchedules.length > 0 && selectedDateSchedules.length > 0 && (
<div className="flex items-center gap-3 pt-1">
<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) => (
<MobileUndatedScheduleListCard
key={`undated-${schedule.id}`}
schedule={schedule}
delay={(selectedDateSchedules.length + index) * 0.05}
onClick={() => navigate(`/schedule/${schedule.id}`)}
/>
))}
</div>
)}
</>

View file

@ -275,9 +275,12 @@ function PCSchedule() {
}, [schedules, currentYearMonth, selectedCategories, isSearchMode]);
// ( , )
// ( )
const calendarSchedules = useMemo(() => {
if (selectedCategories.length === 0) return schedules;
return schedules.filter((s) => selectedCategories.includes(s.category_id));
return schedules.filter((s) => {
if (s.datePrecision === 'month') return false;
return selectedCategories.length === 0 || selectedCategories.includes(s.category_id);
});
}, [schedules, selectedCategories]);
//