From 0255b3561691cb3bf2b18316c0eb5e5c2ece91c0 Mon Sep 17 00:00:00 2001 From: caadiq Date: Wed, 21 Jan 2026 20:40:07 +0900 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=EC=9D=BC=EC=A0=95=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=205?= =?UTF-8?q?=EC=A2=85=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PC 카드 (2종): - ScheduleCard: 일반 일정 카드 (홈, 스케줄 페이지) - AdminScheduleCard: 관리자 일정 카드 (편집/삭제 버튼 포함) Mobile 카드 (3종): - MobileScheduleCard: 홈 페이지용 (간결한 레이아웃) - MobileScheduleListCard: 스케줄 타임라인용 (날짜 없이 시간/카테고리) - MobileScheduleSearchCard: 검색 결과용 (왼쪽에 날짜 표시) 공통 변경: - decodeHtmlEntities를 사용하여 HTML 엔티티 디코딩 - getDisplayMembers 유틸 추가 (5명 이상일 때 프로미스나인 표시) - getMemberList가 member_names 문자열 처리하도록 개선 Co-Authored-By: Claude Opus 4.5 --- .../components/schedule/AdminScheduleCard.jsx | 139 ++++++++++++++++++ .../schedule/MobileScheduleCard.jsx | 32 ++-- .../schedule/MobileScheduleListCard.jsx | 86 +++++++++++ .../schedule/MobileScheduleSearchCard.jsx | 115 +++++++++++++++ .../src/components/schedule/ScheduleCard.jsx | 50 +++---- .../src/components/schedule/index.js | 6 + frontend-temp/src/utils/index.js | 1 + frontend-temp/src/utils/schedule.js | 29 +++- 8 files changed, 408 insertions(+), 50 deletions(-) create mode 100644 frontend-temp/src/components/schedule/AdminScheduleCard.jsx create mode 100644 frontend-temp/src/components/schedule/MobileScheduleListCard.jsx create mode 100644 frontend-temp/src/components/schedule/MobileScheduleSearchCard.jsx diff --git a/frontend-temp/src/components/schedule/AdminScheduleCard.jsx b/frontend-temp/src/components/schedule/AdminScheduleCard.jsx new file mode 100644 index 0000000..3f30a0a --- /dev/null +++ b/frontend-temp/src/components/schedule/AdminScheduleCard.jsx @@ -0,0 +1,139 @@ +import { Clock, Tag, Link2, ExternalLink, Edit2, Trash2 } from 'lucide-react'; +import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils'; + +/** + * PC 관리자 일정 카드 컴포넌트 + * 관리자 일정 관리 페이지에서 사용 + * 편집/삭제 버튼 포함 + */ +function AdminScheduleCard({ + schedule, + onEdit, + onDelete, + className = '', +}) { + const scheduleDate = new Date(schedule.date || schedule.datetime); + 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 categoryInfo = getCategoryInfo(schedule); + const timeStr = getScheduleTime(schedule); + const displayMembers = getDisplayMembers(schedule); + const sourceName = schedule.source?.name; + const sourceUrl = schedule.source?.url; + const isBirthday = schedule.is_birthday || String(schedule.id).startsWith('birthday-'); + + const dayNames = ['일', '월', '화', '수', '목', '금', '토']; + + return ( +
+
+ {/* 날짜 영역 */} +
+ {!isCurrentYear && ( +
+ {scheduleYear}.{scheduleMonth + 1} +
+ )} + {isCurrentYear && !isCurrentMonth && ( +
+ {scheduleMonth + 1}월 +
+ )} +
+ {scheduleDate.getDate()} +
+
+ {dayNames[scheduleDate.getDay()]}요일 +
+
+ + {/* 색상 바 */} +
+ + {/* 스케줄 내용 */} +
+

+ {decodeHtmlEntities(schedule.title)} +

+
+ {timeStr && ( + + + {timeStr} + + )} + {categoryInfo.name && ( + + + {categoryInfo.name} + + )} + {sourceName && ( + + + {sourceName} + + )} +
+ {displayMembers.length > 0 && ( +
+ {displayMembers.map((name, i) => ( + + {name} + + ))} +
+ )} +
+ + {/* 액션 버튼 (생일 일정은 수정/삭제 불가) */} + {!isBirthday && ( +
+ {sourceUrl && ( + e.stopPropagation()} + className="p-2 hover:bg-blue-100 rounded-lg transition-colors text-blue-500" + > + + + )} + {onEdit && ( + + )} + {onDelete && ( + + )} +
+ )} +
+
+ ); +} + +export default AdminScheduleCard; diff --git a/frontend-temp/src/components/schedule/MobileScheduleCard.jsx b/frontend-temp/src/components/schedule/MobileScheduleCard.jsx index 754d725..38cf6b9 100644 --- a/frontend-temp/src/components/schedule/MobileScheduleCard.jsx +++ b/frontend-temp/src/components/schedule/MobileScheduleCard.jsx @@ -1,8 +1,10 @@ import { Clock, Tag, Link2 } from 'lucide-react'; +import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils'; /** - * Mobile 일정 카드 컴포넌트 - * 홈, 스케줄 페이지 등에서 공통으로 사용 + * Mobile 일정 카드 컴포넌트 (홈용) + * 홈 페이지의 다가오는 일정 섹션에서 사용 + * 간결한 레이아웃 */ function MobileScheduleCard({ schedule, onClick, className = '' }) { const scheduleDate = new Date(schedule.date); @@ -15,17 +17,13 @@ function MobileScheduleCard({ schedule, onClick, className = '' }) { 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 categoryInfo = getCategoryInfo(schedule); + const timeStr = getScheduleTime(schedule); + const displayMembers = getDisplayMembers(schedule); const sourceName = schedule.source?.name; + const dayNames = ['일', '월', '화', '수', '목', '금', '토']; + return (
- {['일', '월', '화', '수', '목', '금', '토'][scheduleDate.getDay()]} + {dayNames[scheduleDate.getDay()]}
@@ -57,20 +55,20 @@ function MobileScheduleCard({ schedule, onClick, className = '' }) { {/* 내용 영역 */}

- {schedule.title} + {decodeHtmlEntities(schedule.title)}

{/* 시간 + 카테고리 + 소스 */}
- {schedule.time && ( + {timeStr && ( - {schedule.time.slice(0, 5)} + {timeStr} )} - {categoryName && ( + {categoryInfo.name && ( - {categoryName} + {categoryInfo.name} )} {sourceName && ( diff --git a/frontend-temp/src/components/schedule/MobileScheduleListCard.jsx b/frontend-temp/src/components/schedule/MobileScheduleListCard.jsx new file mode 100644 index 0000000..543cb21 --- /dev/null +++ b/frontend-temp/src/components/schedule/MobileScheduleListCard.jsx @@ -0,0 +1,86 @@ +import { motion } from 'framer-motion'; +import { Clock, Link2 } from 'lucide-react'; +import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils'; + +/** + * Mobile 일정 리스트 카드 컴포넌트 (타임라인용) + * 스케줄 페이지에서 날짜별 일정 목록에 사용 + * 날짜가 이미 헤더에 표시되므로 날짜 없이 표시 + */ +function MobileScheduleListCard({ + schedule, + onClick, + delay = 0, + className = '', +}) { + const categoryInfo = getCategoryInfo(schedule); + const timeStr = getScheduleTime(schedule); + const displayMembers = getDisplayMembers(schedule); + const sourceName = schedule.source?.name; + + return ( + + {/* 카드 본체 */} +
+
+ {/* 시간 및 카테고리 뱃지 */} +
+ {timeStr && ( +
+ + {timeStr} +
+ )} + + {categoryInfo.name} + +
+ + {/* 제목 */} +

+ {decodeHtmlEntities(schedule.title)} +

+ + {/* 출처 */} + {sourceName && ( +
+ + {sourceName} +
+ )} + + {/* 멤버 */} + {displayMembers.length > 0 && ( +
+ {displayMembers.map((name, i) => ( + + {name} + + ))} +
+ )} +
+
+
+ ); +} + +export default MobileScheduleListCard; diff --git a/frontend-temp/src/components/schedule/MobileScheduleSearchCard.jsx b/frontend-temp/src/components/schedule/MobileScheduleSearchCard.jsx new file mode 100644 index 0000000..285222e --- /dev/null +++ b/frontend-temp/src/components/schedule/MobileScheduleSearchCard.jsx @@ -0,0 +1,115 @@ +import { motion } from 'framer-motion'; +import { Clock, Link2 } from 'lucide-react'; +import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils'; + +/** + * Mobile 일정 검색 카드 컴포넌트 + * 스케줄 페이지의 검색 결과에서 사용 + * 날짜를 왼쪽에 표시하는 레이아웃 + */ +function MobileScheduleSearchCard({ + schedule, + onClick, + delay = 0, + className = '', +}) { + const scheduleDate = new Date(schedule.date || schedule.datetime); + const categoryInfo = getCategoryInfo(schedule); + const timeStr = getScheduleTime(schedule); + const displayMembers = getDisplayMembers(schedule); + const sourceName = schedule.source?.name; + + const dayNames = ['일', '월', '화', '수', '목', '금', '토']; + const isSunday = scheduleDate.getDay() === 0; + const isSaturday = scheduleDate.getDay() === 6; + + return ( + + {/* 카드 본체 */} +
+
+ {/* 왼쪽 날짜 영역 */} +
+ + {scheduleDate.getFullYear()} + + + {scheduleDate.getMonth() + 1}.{scheduleDate.getDate()} + + + {dayNames[scheduleDate.getDay()]}요일 + +
+ + {/* 오른쪽 콘텐츠 영역 */} +
+ {/* 시간 및 카테고리 뱃지 */} +
+ {timeStr && ( +
+ + {timeStr} +
+ )} + + {categoryInfo.name} + +
+ + {/* 제목 */} +

+ {decodeHtmlEntities(schedule.title)} +

+ + {/* 출처 */} + {sourceName && ( +
+ + {sourceName} +
+ )} + + {/* 멤버 */} + {displayMembers.length > 0 && ( +
+ {displayMembers.map((name, i) => ( + + {name} + + ))} +
+ )} +
+
+
+
+ ); +} + +export default MobileScheduleSearchCard; diff --git a/frontend-temp/src/components/schedule/ScheduleCard.jsx b/frontend-temp/src/components/schedule/ScheduleCard.jsx index 82e1b70..f19d026 100644 --- a/frontend-temp/src/components/schedule/ScheduleCard.jsx +++ b/frontend-temp/src/components/schedule/ScheduleCard.jsx @@ -1,8 +1,9 @@ import { Clock, Tag, Link2 } from 'lucide-react'; +import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils'; /** - * PC 일정 카드 컴포넌트 - * 홈, 스케줄 페이지 등에서 공통으로 사용 + * PC 일정 카드 컴포넌트 (일반용) + * 홈, 스케줄 페이지에서 공통으로 사용 */ function ScheduleCard({ schedule, onClick, className = '' }) { const scheduleDate = new Date(schedule.date); @@ -15,22 +16,13 @@ function ScheduleCard({ schedule, onClick, className = '' }) { 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 categoryInfo = getCategoryInfo(schedule); + const timeStr = getScheduleTime(schedule); + const displayMembers = getDisplayMembers(schedule); const sourceName = schedule.source?.name; + const dayNames = ['일', '월', '화', '수', '목', '금', '토']; + return (
{!isCurrentYear && ( - + {scheduleYear}.{scheduleMonth + 1} )} {isCurrentYear && !isCurrentMonth && ( - + {scheduleMonth + 1}월 )} - {day} - {weekday} + {scheduleDate.getDate()} + + {dayNames[scheduleDate.getDay()]} +
- {/* 내용 영역 */} + {/* 스케줄 내용 */}
-

{schedule.title}

+

+ {decodeHtmlEntities(schedule.title)} +

- {schedule.time && ( + {timeStr && ( - {schedule.time.slice(0, 5)} + {timeStr} )} - {categoryName && ( + {categoryInfo.name && ( - {categoryName} + {categoryInfo.name} )} {sourceName && ( diff --git a/frontend-temp/src/components/schedule/index.js b/frontend-temp/src/components/schedule/index.js index 94dd3e2..f25b711 100644 --- a/frontend-temp/src/components/schedule/index.js +++ b/frontend-temp/src/components/schedule/index.js @@ -1,2 +1,8 @@ +// PC 컴포넌트 export { default as ScheduleCard } from './ScheduleCard'; +export { default as AdminScheduleCard } from './AdminScheduleCard'; + +// Mobile 컴포넌트 export { default as MobileScheduleCard } from './MobileScheduleCard'; +export { default as MobileScheduleListCard } from './MobileScheduleListCard'; +export { default as MobileScheduleSearchCard } from './MobileScheduleSearchCard'; diff --git a/frontend-temp/src/utils/index.js b/frontend-temp/src/utils/index.js index 38957df..f7b3625 100644 --- a/frontend-temp/src/utils/index.js +++ b/frontend-temp/src/utils/index.js @@ -40,6 +40,7 @@ export { getScheduleDate, getScheduleTime, getMemberList, + getDisplayMembers, isBirthdaySchedule, groupSchedulesByDate, countByCategory, diff --git a/frontend-temp/src/utils/schedule.js b/frontend-temp/src/utils/schedule.js index b84947f..5aca8d1 100644 --- a/frontend-temp/src/utils/schedule.js +++ b/frontend-temp/src/utils/schedule.js @@ -57,23 +57,40 @@ export function getScheduleTime(schedule) { } /** - * 스케줄에서 멤버 목록 추출 - * 다양한 형식 처리 (문자열 배열, 객체 배열) + * 스케줄에서 멤버 이름 목록 추출 + * 다양한 형식 처리 (member_names 문자열, 문자열 배열, 객체 배열) * @param {object} schedule - 스케줄 객체 - * @returns {Array<{ id: number, name: string }>} + * @returns {string[]} 멤버 이름 배열 */ export function getMemberList(schedule) { - const members = schedule.members || []; + // member_names 문자열이 있으면 사용 (쉼표 구분) + if (schedule.member_names) { + return schedule.member_names.split(',').map((n) => n.trim()).filter(Boolean); + } + const members = schedule.members || []; if (members.length === 0) return []; // 문자열 배열인 경우 (검색 결과) if (typeof members[0] === 'string') { - return members.map((name, idx) => ({ id: idx, name })); + return members.filter(Boolean); } // 객체 배열인 경우 - return members; + return members.map((m) => m.name).filter(Boolean); +} + +/** + * 멤버 표시 이름 가져오기 (5명 이상이면 '프로미스나인') + * @param {object} schedule - 스케줄 객체 + * @returns {string[]} 표시할 멤버 이름 배열 + */ +export function getDisplayMembers(schedule) { + const memberList = getMemberList(schedule); + if (memberList.length >= 5) { + return ['프로미스나인']; + } + return memberList; } /**