From 39aadf50e6470bc00952cd11f3265da1c84bb6c7 Mon Sep 17 00:00:00 2001 From: caadiq Date: Tue, 2 Jun 2026 20:10:26 +0900 Subject: [PATCH] =?UTF-8?q?feat(schedule-pc):=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월 중" 카드(UndatedScheduleCard)로 표시. 선택 날짜와 무관하게 해당 월이면 확정 일정 아래에 배치하고 사이에 "날짜 미정" 구분선 추가. 카운트에도 포함. Co-Authored-By: Claude Opus 4.7 --- .../public/schedule/UndatedScheduleCard.jsx | 66 +++++++++++++++++ .../components/pc/public/schedule/index.js | 1 + .../src/pages/pc/public/schedule/Schedule.jsx | 70 ++++++++++++++----- 3 files changed, 119 insertions(+), 18 deletions(-) create mode 100644 frontend/src/components/pc/public/schedule/UndatedScheduleCard.jsx diff --git a/frontend/src/components/pc/public/schedule/UndatedScheduleCard.jsx b/frontend/src/components/pc/public/schedule/UndatedScheduleCard.jsx new file mode 100644 index 0000000..4fb833a --- /dev/null +++ b/frontend/src/components/pc/public/schedule/UndatedScheduleCard.jsx @@ -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 ( +
+ {/* 월 영역 (연한 카테고리 색) */} +
+ {month}월 + +
+ + {/* 내용 */} +
+

{decodeHtmlEntities(schedule.title)}

+
+ {categoryInfo.name && ( + + + {categoryInfo.name} + + )} + {sourceName && ( + + + {sourceName} + + )} +
+ {displayMembers.length > 0 && ( +
+ {displayMembers.map((name, i) => ( + + {name} + + ))} +
+ )} +
+
+ ); +}); + +export default UndatedScheduleCard; diff --git a/frontend/src/components/pc/public/schedule/index.js b/frontend/src/components/pc/public/schedule/index.js index ccb3c55..28bbf5d 100644 --- a/frontend/src/components/pc/public/schedule/index.js +++ b/frontend/src/components/pc/public/schedule/index.js @@ -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'; diff --git a/frontend/src/pages/pc/public/schedule/Schedule.jsx b/frontend/src/pages/pc/public/schedule/Schedule.jsx index 36b38dd..79aa224 100644 --- a/frontend/src/pages/pc/public/schedule/Schedule.jsx +++ b/frontend/src/pages/pc/public/schedule/Schedule.jsx @@ -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() { )} - {filteredSchedules.length}개 일정 + {filteredSchedules.length + undatedSchedules.length}개 일정 )} @@ -548,7 +561,7 @@ function PCSchedule() {
{loading ? (
로딩 중...
- ) : filteredSchedules.length > 0 ? ( + ) : filteredSchedules.length > 0 || undatedSchedules.length > 0 ? ( isSearchMode && searchTerm ? ( <>
@@ -593,22 +606,43 @@ function PCSchedule() {
) : ( - filteredSchedules.map((schedule, index) => ( - - {schedule.is_birthday ? ( - handleScheduleClick(schedule)} /> - ) : schedule.is_debut || schedule.is_anniversary ? ( - - ) : ( - handleScheduleClick(schedule)} /> - )} - - )) + <> + {filteredSchedules.map((schedule, index) => ( + + {schedule.is_birthday ? ( + handleScheduleClick(schedule)} /> + ) : schedule.is_debut || schedule.is_anniversary ? ( + + ) : ( + handleScheduleClick(schedule)} /> + )} + + ))} + + {/* 날짜 미정 일정 — 우선순위 낮춰 기존 일정 아래 배치 */} + {undatedSchedules.length > 0 && filteredSchedules.length > 0 && ( +
+
+ 날짜 미정 +
+
+ )} + {undatedSchedules.map((schedule, index) => ( + + handleScheduleClick(schedule)} /> + + ))} + ) ) : isSearchMode && searchTerm ? ( isSearchLoading ? (