From 39a6225897988931100ed3eb3b10f4b46bb7381f Mon Sep 17 00:00:00 2001 From: caadiq Date: Tue, 2 Jun 2026 20:27:07 +0900 Subject: [PATCH] =?UTF-8?q?feat(schedule-mobile):=20=EB=82=A0=EC=A7=9C=20?= =?UTF-8?q?=EB=AF=B8=EC=A0=95=20=EC=9D=BC=EC=A0=95=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=20(B=EC=95=88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 모바일 일정 페이지에 date_precision='month' 일정을 점선 "N월 중" 카드(UndatedScheduleListCard)로 표시. 선택 날짜와 무관하게 해당 달이면 확정 일정 아래 "날짜 미정" 구분선과 함께 배치. 캘린더/날짜 점은 1일에 찍지 않도록 PC·모바일 dot 목록에서 제외. Co-Authored-By: Claude Opus 4.7 --- .../schedule/UndatedScheduleListCard.jsx | 86 +++++++++++++++++++ .../src/components/mobile/schedule/index.js | 1 + .../src/pages/mobile/schedule/Schedule.jsx | 36 +++++++- .../src/pages/pc/public/schedule/Schedule.jsx | 7 +- 4 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/mobile/schedule/UndatedScheduleListCard.jsx diff --git a/frontend/src/components/mobile/schedule/UndatedScheduleListCard.jsx b/frontend/src/components/mobile/schedule/UndatedScheduleListCard.jsx new file mode 100644 index 0000000..0e155ee --- /dev/null +++ b/frontend/src/components/mobile/schedule/UndatedScheduleListCard.jsx @@ -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 ( + + {/* 카드 본체 (점선 테두리) */} +
+
+ {/* "N월 중" 및 카테고리 뱃지 */} +
+
+ {month}월 중 +
+ + {categoryInfo.name} + +
+ + {/* 제목 */} +

+ {decodeHtmlEntities(schedule.title)} +

+ + {/* 출처 */} + {sourceName && ( +
+ + {sourceName} +
+ )} + + {/* 멤버 */} + {displayMembers.length > 0 && ( +
+ {displayMembers.map((name, i) => ( + + {name} + + ))} +
+ )} +
+
+
+ ); +}); + +export default UndatedScheduleListCard; diff --git a/frontend/src/components/mobile/schedule/index.js b/frontend/src/components/mobile/schedule/index.js index 6cff5ca..ed5d484 100644 --- a/frontend/src/components/mobile/schedule/index.js +++ b/frontend/src/components/mobile/schedule/index.js @@ -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'; diff --git a/frontend/src/pages/mobile/schedule/Schedule.jsx b/frontend/src/pages/mobile/schedule/Schedule.jsx index 177b287..2ce141d 100644 --- a/frontend/src/pages/mobile/schedule/Schedule.jsx +++ b/frontend/src/pages/mobile/schedule/Schedule.jsx @@ -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() {
- ) : selectedDateSchedules.length === 0 ? ( + ) : selectedDateSchedules.length === 0 && undatedSchedules.length === 0 ? (
@@ -895,6 +908,23 @@ function MobileSchedule() { /> ); })} + + {/* 날짜 미정 일정 — 우선순위 낮춰 기존 일정 아래 배치 */} + {undatedSchedules.length > 0 && selectedDateSchedules.length > 0 && ( +
+
+ 날짜 미정 +
+
+ )} + {undatedSchedules.map((schedule, index) => ( + navigate(`/schedule/${schedule.id}`)} + /> + ))}
)} diff --git a/frontend/src/pages/pc/public/schedule/Schedule.jsx b/frontend/src/pages/pc/public/schedule/Schedule.jsx index 79aa224..ae96c69 100644 --- a/frontend/src/pages/pc/public/schedule/Schedule.jsx +++ b/frontend/src/pages/pc/public/schedule/Schedule.jsx @@ -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]); // 가상 스크롤