diff --git a/src/content/components/Video.tsx b/src/content/components/Video.tsx index 27ba1b9..020ca19 100644 --- a/src/content/components/Video.tsx +++ b/src/content/components/Video.tsx @@ -1,7 +1,13 @@ import { useState } from 'react'; import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'; import { Vod } from '../types'; -import { calculateRemainingTimeByRange, calculateTimeDifference, cn, formatDateString } from '@/lib/utils'; +import { + calculateRemainingTimeByRange, + calculateTimeDifference, + cn, + formatDateString, + isCurrentDateInRange, +} from '@/lib/utils'; import { AlarmClock, BadgeCheck, ChevronDown, ChevronUp, Clock, Siren, TriangleAlert } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; @@ -48,17 +54,45 @@ export default function Video({ courseData }: Props) { const isAX = firstA.weeklyAttendance.toUpperCase().startsWith('X'); const isBX = firstB.weeklyAttendance.toUpperCase().startsWith('X'); + // X가 있는 항목을 먼저 정렬 if (isAX && !isBX) return -1; if (!isAX && isBX) return 1; - const rangeStartA = firstA.range.split(' ~ ')[0]; - const rangeStartB = firstB.range.split(' ~ ')[0]; - const dateA = new Date(rangeStartA); - const dateB = new Date(rangeStartB); - - if (dateA < dateB) return -1; - if (dateA > dateB) return 1; + const rangeA = firstA.range; + const rangeB = firstB.range; + const isRangeANull = rangeA === null; + const isRangeBNull = rangeB === null; + + // isCurrentDateInRange가 true인 항목을 먼저 정렬 (X와 O 모두) + const isCurrentDateInRangeA = isCurrentDateInRange(firstA.range); + const isCurrentDateInRangeB = isCurrentDateInRange(firstB.range); + + if (isAX) { + // X일 때는 isCurrentDateInRange가 true인 항목을 먼저 배치, 그 다음 null + if (isCurrentDateInRangeA && !isCurrentDateInRangeB) return -1; + if (!isCurrentDateInRangeA && isCurrentDateInRangeB) return 1; + if (isRangeANull && !isRangeBNull) return 1; + if (!isRangeANull && isRangeBNull) return -1; + } else { + // O일 때는 isCurrentDateInRange가 true인 항목을 먼저 배치, 그 다음 null, 그 다음 시간순 정렬 + if (isCurrentDateInRangeA && !isCurrentDateInRangeB) return -1; + if (!isCurrentDateInRangeA && isCurrentDateInRangeB) return 1; + if (isRangeANull && !isRangeBNull) return 1; + if (!isRangeANull && isRangeBNull) return -1; + + // rangeStart 날짜 기준으로 시간순으로 정렬 + if (!isRangeANull && !isRangeBNull) { + const rangeStartA = rangeA.split(' ~ ')[0]; + const rangeStartB = rangeB.split(' ~ ')[0]; + const dateA = new Date(rangeStartA); + const dateB = new Date(rangeStartB); + + if (dateA < dateB) return -1; + if (dateA > dateB) return 1; + } + } + // courseTitle로 기본 정렬 if (firstA.courseTitle < firstB.courseTitle) return -1; if (firstA.courseTitle > firstB.courseTitle) return 1; @@ -76,12 +110,14 @@ export default function Video({ courseData }: Props) { if (isAX && !isBX) return -1; if (!isAX && isBX) return 1; - const rangeStartA = a.range.split(' ~ ')[0]; - const rangeStartB = b.range.split(' ~ ')[0]; - const dateA = new Date(rangeStartA); - const dateB = new Date(rangeStartB); - if (dateA < dateB) return -1; - if (dateA > dateB) return 1; + if (a.range && b.range) { + const rangeStartA = a.range.split(' ~ ')[0]; + const rangeStartB = b.range.split(' ~ ')[0]; + const dateA = new Date(rangeStartA); + const dateB = new Date(rangeStartB); + if (dateA < dateB) return -1; + if (dateA > dateB) return 1; + } if (a.courseTitle < b.courseTitle) return -1; if (a.courseTitle > b.courseTitle) return 1; diff --git a/src/content/types.ts b/src/content/types.ts index d18fe4f..c82f5a9 100644 --- a/src/content/types.ts +++ b/src/content/types.ts @@ -13,7 +13,7 @@ export type VodData = { subject: string; title: string; url: string; - range: string; + range: string | null; length: string; }; @@ -31,7 +31,7 @@ export type AssignData = { title: string; url: string; isSubmit: boolean; - dueDate: string; + dueDate: string | null; }; export type Quiz = CourseBase & QuizData; @@ -40,7 +40,7 @@ export type QuizData = { subject: string; title: string; url: string; - dueDate: string; + dueDate: string | null; }; export type TimeDifferenceResult = { @@ -59,8 +59,8 @@ export interface Item { export interface Filters { courseTitles: string[]; - attendanceStatuses?: string[]; - submitStatuses?: boolean[]; + attendanceStatuses?: string[]; + submitStatuses?: boolean[]; } export enum TAB_TYPE { diff --git a/src/hooks/useCalendarEvents.ts b/src/hooks/useCalendarEvents.ts index 3dbbf8d..cc5f1ac 100644 --- a/src/hooks/useCalendarEvents.ts +++ b/src/hooks/useCalendarEvents.ts @@ -9,8 +9,8 @@ export type CalendarEvent = { type: 'vod' | 'assign' | 'quiz'; title: string; subject: string; - start: Date; - end: Date; + start: Date | null; + end: Date | null; }; function useCalendarEvents() { @@ -52,12 +52,13 @@ function useCalendarEvents() { ); return Object.entries(groupedData).map(([key, vodItems]) => { - const [start, end] = vodItems[0].range.split(' ~ '); + const range = vodItems[0].range; + const [start, end] = range ? range.split(' ~ ') : [null, null]; return { id: key, type: 'vod', - start: new Date(start.replace(/-/g, '/')), - end: new Date(end.replace(/-/g, '/')), + start: start ? new Date(start.replace(/-/g, '/')) : null, + end: end ? new Date(end.replace(/-/g, '/')) : null, title: removeSquareBrackets(vodItems[0].courseTitle), subject: removeSquareBrackets(vodItems[0].subject), }; @@ -67,7 +68,8 @@ function useCalendarEvents() { // assign 데이터 로딩 및 변환 loadEvents('assign', (assigns) => assigns.map((assign) => { - const normalizedDate = startOfDay(new Date(assign.dueDate)); + const dueDate = assign.dueDate; + const normalizedDate = dueDate ? startOfDay(new Date(dueDate)) : null; return { id: assign.courseId + assign.title + assign.dueDate, type: 'assign', @@ -82,7 +84,8 @@ function useCalendarEvents() { // quiz 데이터 로딩 및 변환 loadEvents('quiz', (quizzes) => quizzes.map((quiz) => { - const normalizedDate = startOfDay(new Date(quiz.dueDate)); + const dueDate = quiz.dueDate; + const normalizedDate = dueDate ? startOfDay(new Date(dueDate)) : null; return { id: quiz.courseId + quiz.title + quiz.dueDate, type: 'quiz', diff --git a/src/hooks/useCourseData.tsx b/src/hooks/useCourseData.tsx index 6db514f..de94422 100644 --- a/src/hooks/useCourseData.tsx +++ b/src/hooks/useCourseData.tsx @@ -2,7 +2,6 @@ import { useState, useEffect, useCallback } from 'react'; import { Vod, Assign, Quiz, TAB_TYPE } from '@/content/types'; import { loadDataFromStorage, saveDataToStorage } from '@/lib/storage'; import { requestData } from '@/lib/fetchCourseData'; -import { isCurrentDateInRange, isCurrentDateByDate } from '@/lib/utils'; // courses 배열을 받아 vod, assign, quiz 데이터를 관리하는 커스텀 훅 export function useCourseData(courses: any[]) { @@ -14,28 +13,34 @@ export function useCourseData(courses: any[]) { const [remainingTime, setRemainingTime] = useState(0); const [isError, setIsError] = useState(false); - // updateData 함수를 useCallback으로 선언하여 useEffect 등에서 재사용 const updateData = useCallback(async () => { try { setIsError(false); setIsPending(true); const currentTime = new Date().getTime(); - setVods([]); - setAssigns([]); - setQuizes([]); - const tempVods: Vod[] = []; - const tempAssigns: Assign[] = []; - const tempQuizes: Quiz[] = []; + // 기존 데이터를 유지하면서 새로운 데이터만 추가 + const tempVods: Vod[] = [...vods]; + const tempAssigns: Assign[] = [...assigns]; + const tempQuizes: Quiz[] = [...quizes]; + + // Set을 사용하여 중복 방지 (각 데이터 유형별로 title을 기준으로) + const vodSet = new Set(tempVods.map((vod) => `${vod.courseId}-${vod.title}-${vod.range}-vod`)); + const assignSet = new Set( + tempAssigns.map((assign) => `${assign.courseId}-${assign.title}-${assign.dueDate}-assign`) + ); + const quizSet = new Set(tempQuizes.map((quiz) => `${quiz.courseId}-${quiz.title}-${quiz.dueDate}-quiz`)); await Promise.all( courses.map(async (course) => { const result = await requestData(course.courseId); result.vodDataArray.forEach((vodData) => { - if (isCurrentDateInRange(vodData.range)) { - result.vodAttendanceArray.forEach((vodAttendanceData) => { - if (vodAttendanceData.title === vodData.title && vodAttendanceData.week === vodData.week) { + result.vodAttendanceArray.forEach((vodAttendanceData) => { + const vodKey = `${vodAttendanceData.title}-${vodAttendanceData.week}`; + if (vodAttendanceData.title === vodData.title && vodAttendanceData.week === vodData.week) { + if (!vodSet.has(vodKey)) { + vodSet.add(vodKey); tempVods.push({ courseId: course.courseId, prof: course.prof, @@ -50,12 +55,13 @@ export function useCourseData(courses: any[]) { url: vodData.url, }); } - }); - } + } + }); }); result.assignDataArray.forEach((assignData) => { - if (isCurrentDateByDate(assignData.dueDate)) { + if (!assignSet.has(assignData.title)) { + assignSet.add(assignData.title); tempAssigns.push({ courseId: course.courseId, prof: course.prof, @@ -70,7 +76,8 @@ export function useCourseData(courses: any[]) { }); result.quizDataArray.forEach((quizData) => { - if (isCurrentDateByDate(quizData.dueDate)) { + if (!quizSet.has(quizData.title)) { + quizSet.add(quizData.title); tempQuizes.push({ courseId: course.courseId, prof: course.prof, @@ -94,17 +101,17 @@ export function useCourseData(courses: any[]) { saveDataToStorage('quiz', tempQuizes); setRefreshTime(new Date(currentTime).toLocaleTimeString()); - setRemainingTime(0); localStorage.setItem('lastRequestTime', currentTime.toString()); saveDataToStorage('lastRequestTime', currentTime.toString()); + setIsPending(false); } catch (error) { localStorage.removeItem('lastRequestTime'); setIsError(true); setIsPending(false); } - }, [courses]); + }, [courses, vods, assigns, quizes]); useEffect(() => { let timer: ReturnType; @@ -135,13 +142,16 @@ export function useCourseData(courses: any[]) { const minutes = (currentTime - parseInt(lastRequestTime, 10)) / (60 * 1000); setRemainingTime(minutes); loadDataFromStorage('vod', (data) => { - setVods((data as Vod[]).filter((vod) => isCurrentDateInRange(vod.range))); + // setVods((data as Vod[]).filter((vod) => isCurrentDateInRange(vod.range))); + setVods((data as Vod[]) || []); }); loadDataFromStorage('assign', (data) => { - setAssigns((data as Assign[]).filter((assign) => isCurrentDateByDate(assign.dueDate))); + // setAssigns((data as Assign[]).filter((assign) => isCurrentDateByDate(assign.dueDate))); + setAssigns((data as Assign[]) || []); }); loadDataFromStorage('quiz', (data) => { - setQuizes((data as Quiz[]).filter((quiz) => isCurrentDateByDate(quiz.dueDate))); + // setQuizes((data as Quiz[]).filter((quiz) => isCurrentDateByDate(quiz.dueDate))); + setQuizes((data as Quiz[]) || []); }); } }, [courses, updateData]); diff --git a/src/lib/calendarUtils.ts b/src/lib/calendarUtils.ts index 3464504..b363825 100644 --- a/src/lib/calendarUtils.ts +++ b/src/lib/calendarUtils.ts @@ -81,16 +81,18 @@ export async function getCalendarEvents(token: string): Promise ({ - summary: `${event.title}`, - description: `${event.subject}`, - start: { - dateTime: event.start.toISOString(), - timeZone: 'Asia/Seoul', - }, - end: { - dateTime: event.end.toISOString(), - timeZone: 'Asia/Seoul', - }, - })); + return events + .filter((event) => event.start !== null && event.end !== null) + .map((event) => ({ + summary: `${event.title}`, + description: `${event.subject}`, + start: { + dateTime: event.start!.toISOString(), + timeZone: 'Asia/Seoul', + }, + end: { + dateTime: event.end!.toISOString(), + timeZone: 'Asia/Seoul', + }, + })); } diff --git a/src/lib/fetchAssign.ts b/src/lib/fetchAssign.ts index 703c351..82834ce 100644 --- a/src/lib/fetchAssign.ts +++ b/src/lib/fetchAssign.ts @@ -42,7 +42,7 @@ export const fetchAssign = async (link: string) => { const isSubmit = row.querySelector(headerMap.isSubmit)?.textContent?.trim() === '미제출' ? false : true; if (sbj.length !== 0) subject = sbj; - if (!title || !url || !dueDate) return null; + if (!title || !url) return null; return { subject, title, url, dueDate, isSubmit }; }) .filter((assign) => assign !== null); diff --git a/src/lib/fetchQuiz.ts b/src/lib/fetchQuiz.ts index 6a413c1..ed1a1ae 100644 --- a/src/lib/fetchQuiz.ts +++ b/src/lib/fetchQuiz.ts @@ -43,7 +43,7 @@ export const fetchQuiz = async (link: string) => { url = url.slice(0, index) + 'mod/quiz/' + url.slice(index); } - if (title && dueDate && url) { + if (title && url) { return { title, subject, url, dueDate }; } return null; diff --git a/src/lib/filterData.tsx b/src/lib/filterData.tsx index 3f4b7aa..10f820e 100644 --- a/src/lib/filterData.tsx +++ b/src/lib/filterData.tsx @@ -29,6 +29,7 @@ export function filterVods(vods: Vod[], filters: Filters, searchTerm: string, so return data.sort((a, b) => { const attendanceA = a.isAttendance.toLowerCase().trim() === 'o'; const attendanceB = b.isAttendance.toLowerCase().trim() === 'o'; + if (attendanceA !== attendanceB) { return attendanceA ? -1 : 1; } @@ -37,7 +38,10 @@ export function filterVods(vods: Vod[], filters: Filters, searchTerm: string, so case 'title': return a.title.localeCompare(b.title); default: - return a.range.localeCompare(b.range); + if (a.range === null && b.range !== null) return attendanceA ? -1 : 1; + if (a.range !== null && b.range === null) return attendanceA ? 1 : -1; + if (a.range === null && b.range === null) return 0; + return (a.range ?? '').localeCompare(b.range ?? ''); } }); } @@ -74,7 +78,10 @@ export function filterAssigns(assigns: Assign[], filters: Filters, searchTerm: s case 'title': return a.title.localeCompare(b.title); default: - return a.dueDate.localeCompare(b.dueDate); + if (a.dueDate === null && b.dueDate !== null) return a.isSubmit ? -1 : 1; + if (a.dueDate !== null && b.dueDate === null) return a.isSubmit ? 1 : -1; + if (a.dueDate === null && b.dueDate === null) return 0; + return (a.dueDate ?? '').localeCompare(b.dueDate ?? ''); } }); } @@ -103,7 +110,10 @@ export function filterQuizes(quizes: Quiz[], filters: Filters, searchTerm: strin case 'title': return a.title.localeCompare(b.title); default: - return a.dueDate.localeCompare(b.dueDate); + if (a.dueDate === null && b.dueDate !== null) return 1; + if (a.dueDate !== null && b.dueDate === null) return -1; + if (a.dueDate === null && b.dueDate === null) return 0; + return (a.dueDate ?? '').localeCompare(b.dueDate ?? ''); } }); } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e73cc7d..be5d45e 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -40,7 +40,16 @@ export function isWithinSevenDays(date: string) { return diffDays <= 7 && diffDays >= 0; } -export const calculateTimeDifference = (timeRange: string): TimeDifferenceResult => { +export const calculateTimeDifference = (timeRange: string | null): TimeDifferenceResult => { + if (!timeRange) { + return { + message: `정보없음`, + borderColor: 'border-amber-500', + borderLeftColor: 'border-l-amber-500', + textColor: 'text-amber-500', + }; + } + const now = new Date(); const [startString, endString] = timeRange.split(' ~ '); const startDate = new Date(startString); @@ -81,7 +90,16 @@ export const calculateTimeDifference = (timeRange: string): TimeDifferenceResult } }; -export const calculateDueDate = (dueDate: string): TimeDifferenceResult => { +export const calculateDueDate = (dueDate: string | null): TimeDifferenceResult => { + if (!dueDate) { + return { + message: `정보없음`, + borderColor: 'border-amber-500', + borderLeftColor: 'border-l-amber-500', + textColor: 'text-amber-500', + }; + } + const now = new Date(); const endDate = new Date(dueDate); @@ -120,7 +138,8 @@ export const calculateDueDate = (dueDate: string): TimeDifferenceResult => { } }; -export const formatDateString = (input: string) => { +export const formatDateString = (input: string | null) => { + if (!input) return '기한없음'; const regex = /(\d{4})-(\d{2})-(\d{2}) (\d{2}:\d{2}):\d{2} ~ (\d{4})-(\d{2})-(\d{2}) (\d{2}:\d{2}):\d{2}/; const formatted = input.replace( @@ -132,7 +151,8 @@ export const formatDateString = (input: string) => { return formatted; }; -export const calculateRemainingTimeByRange = (range: string) => { +export const calculateRemainingTimeByRange = (range: string | null) => { + if (!range) return '정보없음'; const [startDateStr, endDateStr] = range.split(' ~ '); const endDate = new Date(endDateStr); @@ -147,7 +167,8 @@ export const calculateRemainingTimeByRange = (range: string) => { return `${daysLeft === 0 ? '' : daysLeft + '일'} ${hoursLeft === 0 ? '' : hoursLeft + '시간'} ${minutesLeft}분 남음`; }; -export const calculateRemainingTime = (endTime: string) => { +export const calculateRemainingTime = (endTime: string | null) => { + if (!endTime) return '정보없음'; const endDate = new Date(endTime); const now = new Date(); diff --git a/src/option/calendar.tsx b/src/option/calendar.tsx index 7ae0f89..2e22fbc 100644 --- a/src/option/calendar.tsx +++ b/src/option/calendar.tsx @@ -34,14 +34,18 @@ function isSameDate(d1: Date, d2: Date) { } function isInEventRange(day: Date, event: CalendarEvent) { + if (!event.start || !event.end) return false; return day >= event.start && day <= event.end; } function isSingleDayEvent(event: CalendarEvent) { + if (!event.start || !event.end) return false; return isSameDate(event.start, event.end); } function getRangePosition(day: Date, event: CalendarEvent): 'single' | 'start' | 'middle' | 'end' | 'after' | null { + if (!event.start || !event.end) return null; + if (!isInEventRange(day, event)) return null; if (isSingleDayEvent(event)) return 'single'; if (isSameDate(day, addDays(event.start, 1))) return 'after'; @@ -51,9 +55,11 @@ function getRangePosition(day: Date, event: CalendarEvent): 'single' | 'start' | } function eventsOverlap(a: CalendarEvent, b: CalendarEvent) { + if (a.start === null || a.end === null || b.start === null || b.end === null) { + return false; + } return a.start <= b.end && b.start <= a.end; } - // 헬퍼: hex 색상을 rgba 문자열로 변환 (투명도 적용) function hexToRgba(hex: string, opacity: number): string { hex = hex.replace('#', ''); @@ -138,7 +144,12 @@ export function Calendar() { }; const eventsWithRow = useMemo(() => { - const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime()); + const sorted = [...events].sort((a, b) => { + const aTime = a.start ? a.start.getTime() : Number.MIN_SAFE_INTEGER; + const bTime = b.start ? b.start.getTime() : Number.MIN_SAFE_INTEGER; + return aTime - bTime; + }); + const assigned: CalendarEventWithRow[] = []; for (const event of sorted) { let row = 0; @@ -193,11 +204,17 @@ export function Calendar() { const weekStart = startOfWeek(day, { weekStartsOn: 0 }); const weekEnd = addDays(weekStart, 6); - const weekEvents = eventsWithRow.filter((event) => event.end >= weekStart && event.start <= weekEnd); + const weekEvents = eventsWithRow.filter( + (event) => event.end !== null && event.start !== null && event.end >= weekStart && event.start <= weekEnd + ); const weekStack: { [eventId: string]: number } = {}; weekEvents - .sort((a, b) => a.start.getTime() - b.start.getTime()) + .sort((a, b) => { + const aTime = a.start ? a.start.getTime() : Number.MIN_SAFE_INTEGER; + const bTime = b.start ? b.start.getTime() : Number.MIN_SAFE_INTEGER; + return aTime - bTime; + }) .forEach((event) => { let row = 0; while ( @@ -205,10 +222,19 @@ export function Calendar() { if (id === event.id) return false; const assignedEvent = weekEvents.find((e) => e.id === id); if (!assignedEvent) return false; - const eventStartInWeek = event.start < weekStart ? weekStart : event.start; - const eventEndInWeek = event.end > weekEnd ? weekEnd : event.end; - const assignedStartInWeek = assignedEvent.start < weekStart ? weekStart : assignedEvent.start; - const assignedEndInWeek = assignedEvent.end > weekEnd ? weekEnd : assignedEvent.end; + const eventStartInWeek = event.start ? (event.start < weekStart ? weekStart : event.start) : weekStart; + const eventEndInWeek = event.end ? (event.end > weekEnd ? weekEnd : event.end) : weekEnd; + const assignedStartInWeek = assignedEvent.start + ? assignedEvent.start < weekStart + ? weekStart + : assignedEvent.start + : weekStart; + const assignedEndInWeek = assignedEvent.end + ? assignedEvent.end > weekEnd + ? weekEnd + : assignedEvent.end + : weekEnd; + return ( assignedRow === row && eventStartInWeek <= assignedEndInWeek && assignedStartInWeek <= eventEndInWeek ); @@ -253,12 +279,19 @@ export function Calendar() { let customStyle = {}; if (isVodCustom) { - const courseData = courseColors![eventId]; - const totalDays = Math.floor((event.end.getTime() - event.start.getTime()) / (1000 * 3600 * 24)) + 1; - if (courseData.colorType === 'gradient' && courseData.gradient && totalDays > 1) { - const dayIndex = Math.floor((day.getTime() - event.start.getTime()) / (1000 * 3600 * 24)); + const courseData = courseColors?.[eventId]; + + const eventStart = event.start ?? new Date(0); + const eventEnd = event.end ?? new Date(0); + + const totalDays = Math.floor((eventEnd.getTime() - eventStart.getTime()) / (1000 * 3600 * 24)) + 1; + + if (courseData?.colorType === 'gradient' && courseData.gradient && totalDays > 1) { + const dayIndex = Math.floor((day.getTime() - eventStart.getTime()) / (1000 * 3600 * 24)); + const regex = /linear-gradient\(to right, (#[0-9a-fA-F]+), (#[0-9a-fA-F]+)\)/; const match = courseData.gradient.match(regex); + if (match) { const rgba1 = hexToRgba(match[1], courseData.opacity); const rgba2 = hexToRgba(match[2], courseData.opacity); @@ -271,8 +304,8 @@ export function Calendar() { customStyle = { backgroundImage: courseData.gradient, opacity: courseData.opacity }; } } else { - // solid인 경우: hex 색상에 opacity 반영 - customStyle = { background: hexToRgba(courseData.color, courseData.opacity) }; + // 단색(hex)인 경우: null 값 방지 후 rgba 변환 + customStyle = { background: hexToRgba(courseData?.color ?? '#000000', courseData?.opacity ?? 1) }; } } diff --git a/src/option/components/AssignCard.tsx b/src/option/components/AssignCard.tsx index f594f7a..375534c 100644 --- a/src/option/components/AssignCard.tsx +++ b/src/option/components/AssignCard.tsx @@ -10,61 +10,12 @@ import { calculateDueDate, calculateRemainingTime, calculateTimeDifference, remo import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; interface TaskStatusCardProps { - notification: boolean; assign: Assign; } -const AssignCard: React.FC = ({ notification, assign }) => { +const AssignCard: React.FC = ({ assign }) => { if (!assign) return <>; - const [toggle, setToggle] = useState(notification); - - const [userInitiatedToggle, setUserInitiatedToggle] = useState(false); - const { toast } = useToast(); - - useEffect(() => { - setToggle(notification); - }, [notification]); - - useEffect(() => { - loadDataFromStorage('assign-notification', (data: string | null) => { - try { - let parsedData: Record = {}; - if (data) { - try { - parsedData = JSON.parse(data); - } catch (error) { - console.error('저장된 데이터를 파싱하는 중 오류가 발생했습니다.', error); - } - } - const key = `${assign.courseId}-${assign.title}-${assign.dueDate}`; - - if (toggle) { - parsedData[key] = true; - } else { - if (key in parsedData) { - delete parsedData[key]; - } - } - saveDataToStorage('assign-notification', JSON.stringify(parsedData)); - - if (userInitiatedToggle) { - toast({ - title: toggle ? '알림 설정 🔔' : '알림 취소 🔕', - description: removeSquareBrackets(`${assign.courseTitle} - ${assign.title}`), - variant: 'default', - }); - setUserInitiatedToggle(false); - } - } catch (error) { - toast({ - title: '오류가 발생 했습니다. 🚨', - variant: 'destructive', - }); - } - }); - }, [toggle, userInitiatedToggle, toast, assign]); - const [showRemainingTime, setShowRemainingTime] = useState(false); useEffect(() => { @@ -92,35 +43,6 @@ const AssignCard: React.FC = ({ notification, assign }) =>

{assign.courseTitle}

-

- { - if (!toggle) { - chrome.runtime.sendMessage( - { - action: 'scheduleAlarm', - alarmId: `${assign.courseId}-${assign.title}-${assign.dueDate}`, - dateTime: assign.dueDate, - title: '하루 뒤 과제 마감!', - message: removeSquareBrackets(assign.courseTitle) + '-' + assign.title, - }, - (response) => {} - ); - } else { - chrome.runtime.sendMessage( - { - action: 'cancelAlarm', - alarmId: `${assign.courseId}-${assign.title}-${assign.dueDate}`, - }, - (response) => {} - ); - } - setToggle((prev) => !prev); - setUserInitiatedToggle(true); - }} - /> -

{assign.title}
diff --git a/src/option/components/AssignContent.tsx b/src/option/components/AssignContent.tsx index c0ac12f..797efc4 100644 --- a/src/option/components/AssignContent.tsx +++ b/src/option/components/AssignContent.tsx @@ -5,6 +5,7 @@ import { loadDataFromStorage } from '@/lib/storage'; import AssignCard from './AssignCard'; import { ScrollArea } from '@/components/ui/scroll-area'; import thung from '@/assets/thung.png'; +import { isCurrentDateByDate } from '@/lib/utils'; export function AssignContent() { const date = new Date(); @@ -15,7 +16,6 @@ export function AssignContent() { }); const [assignArray, setAssignArray] = useState([]); - const [notificationMap, setNotificationMap] = useState>({}); useEffect(() => { loadDataFromStorage('assign', (data: string | null) => { @@ -37,15 +37,41 @@ export function AssignContent() { const isAX = a.isSubmit; const isBX = b.isSubmit; - if (isAX && !isBX) return -1; - if (!isAX && isBX) return 1; + // isSubmit이 false인 항목을 우선 배치 + if (!isAX && isBX) return -1; + if (isAX && !isBX) return 1; - const dateA = new Date(a.dueDate); - const dateB = new Date(b.dueDate); + const isCurrentDateByDateA = isCurrentDateByDate(a.dueDate); // isCurrentDateByDate 적용 + const isCurrentDateByDateB = isCurrentDateByDate(b.dueDate); + + // isSubmit이 false일 때는 isCurrentDateByDate가 true인 항목을 먼저 배치, 그 다음 dueDate가 null인 항목 + if (!isAX) { + if (isCurrentDateByDateA && !isCurrentDateByDateB) return -1; + if (!isCurrentDateByDateA && isCurrentDateByDateB) return 1; + const isANull = a.dueDate === null; + const isBNull = b.dueDate === null; + if (isANull && !isBNull) return 1; + if (!isANull && isBNull) return -1; + } + + // isSubmit이 true일 때는 isCurrentDateByDate가 true인 항목을 먼저 배치, 그 다음 dueDate가 null인 항목 + if (isAX) { + if (isCurrentDateByDateA && !isCurrentDateByDateB) return -1; + if (!isCurrentDateByDateA && isCurrentDateByDateB) return 1; + const isANull = a.dueDate === null; + const isBNull = b.dueDate === null; + if (isANull && !isBNull) return -1; + if (!isANull && isBNull) return 1; + } + + // dueDate 기준으로 날짜 순으로 정렬 + const dateA = a.dueDate === null ? Number.MAX_SAFE_INTEGER : new Date(a.dueDate!).getTime(); + const dateB = b.dueDate === null ? Number.MAX_SAFE_INTEGER : new Date(b.dueDate!).getTime(); if (dateA < dateB) return -1; if (dateA > dateB) return 1; + // courseTitle로 기본 정렬 if (a.courseTitle < b.courseTitle) return -1; if (a.courseTitle > b.courseTitle) return 1; @@ -56,14 +82,6 @@ export function AssignContent() { }); }, []); - useEffect(() => { - loadDataFromStorage('assign-notification', (data: string | null) => { - if (!data) return; - const parsedData = JSON.parse(data); - setNotificationMap(parsedData); - }); - }, []); - return (
@@ -77,9 +95,7 @@ export function AssignContent() {
{assignArray.map((assign, index) => { const key = `${assign.courseId}-${assign.title}-${assign.dueDate}`; - const notification = - notificationMap[key] === null || notificationMap[key] === undefined ? false : true; - return ; + return ; })}
diff --git a/src/option/components/QuizCard.tsx b/src/option/components/QuizCard.tsx index d43d77b..10146ab 100644 --- a/src/option/components/QuizCard.tsx +++ b/src/option/components/QuizCard.tsx @@ -3,68 +3,16 @@ import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Quiz } from '@/content/types'; -import NotificationSwitch from '@/components/ui/notification-switch'; -import { loadDataFromStorage, saveDataToStorage } from '@/lib/storage'; -import { useToast } from '@/hooks/use-toast'; import { calculateDueDate, calculateRemainingTime, calculateTimeDifference, removeSquareBrackets } from '@/lib/utils'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; interface TaskStatusCardProps { - notification: boolean; quiz: Quiz; } -const QuizCard: React.FC = ({ notification, quiz }) => { +const QuizCard: React.FC = ({ quiz }) => { if (!quiz) return <>; - const [toggle, setToggle] = useState(notification); - - const [userInitiatedToggle, setUserInitiatedToggle] = useState(false); - const { toast } = useToast(); - - useEffect(() => { - setToggle(notification); - }, [notification]); - - useEffect(() => { - loadDataFromStorage('quiz-notification', (data: string | null) => { - try { - let parsedData: Record = {}; - if (data) { - try { - parsedData = JSON.parse(data); - } catch (error) { - console.error('저장된 데이터를 파싱하는 중 오류가 발생했습니다.', error); - } - } - const key = `${quiz.courseId}-${quiz.title}-${quiz.dueDate}`; - - if (toggle) { - parsedData[key] = true; - } else { - if (key in parsedData) { - delete parsedData[key]; - } - } - saveDataToStorage('quiz-notification', JSON.stringify(parsedData)); - - if (userInitiatedToggle) { - toast({ - title: toggle ? '알림 설정 🔔' : '알림 취소 🔕', - description: removeSquareBrackets(`${quiz.courseTitle} - ${quiz.title}`), - variant: 'default', - }); - setUserInitiatedToggle(false); - } - } catch (error) { - toast({ - title: '오류가 발생 했습니다. 🚨', - variant: 'destructive', - }); - } - }); - }, [toggle, userInitiatedToggle, toast, quiz]); - const [showRemainingTime, setShowRemainingTime] = useState(false); useEffect(() => { @@ -92,35 +40,6 @@ const QuizCard: React.FC = ({ notification, quiz }) => {

{quiz.courseTitle}

-

- { - if (!toggle) { - chrome.runtime.sendMessage( - { - action: 'scheduleAlarm', - alarmId: `${quiz.courseId}-${quiz.title}-${quiz.dueDate}`, - dateTime: quiz.dueDate, - title: '하루 뒤 퀴즈 마감!', - message: removeSquareBrackets(quiz.courseTitle) + '-' + quiz.title, - }, - (response) => {} - ); - } else { - chrome.runtime.sendMessage( - { - action: 'cancelAlarm', - alarmId: `${quiz.courseId}-${quiz.title}-${quiz.dueDate}`, - }, - (response) => {} - ); - } - setToggle((prev) => !prev); - setUserInitiatedToggle(true); - }} - /> -

{quiz.title}
diff --git a/src/option/components/QuizContent.tsx b/src/option/components/QuizContent.tsx index 67f7eec..0016043 100644 --- a/src/option/components/QuizContent.tsx +++ b/src/option/components/QuizContent.tsx @@ -5,6 +5,7 @@ import { loadDataFromStorage } from '@/lib/storage'; import QuizCard from './QuizCard'; import { ScrollArea } from '@radix-ui/react-scroll-area'; import thung from '@/assets/thung.png'; +import { isCurrentDateByDate } from '@/lib/utils'; export function QuizContent() { const date = new Date(); @@ -15,7 +16,6 @@ export function QuizContent() { }); const [quizArray, setQuizArray] = useState([]); - const [notificationMap, setNotificationMap] = useState>({}); useEffect(() => { loadDataFromStorage('quiz', (data: string | null) => { @@ -34,12 +34,27 @@ export function QuizContent() { } const sortedQuizArray = parsedData.sort((a, b) => { - const dateA = new Date(a.dueDate); - const dateB = new Date(b.dueDate); + const isCurrentDateByDateA = isCurrentDateByDate(a.dueDate); // isCurrentDateByDate 적용 + const isCurrentDateByDateB = isCurrentDateByDate(b.dueDate); + + // isCurrentDateByDate가 true인 항목을 우선 배치, 그 다음 dueDate가 null인 항목 + if (isCurrentDateByDateA && !isCurrentDateByDateB) return -1; + if (!isCurrentDateByDateA && isCurrentDateByDateB) return 1; + + const isANull = a.dueDate === null; + const isBNull = b.dueDate === null; + + if (isANull && !isBNull) return 1; // A가 null이면 B가 우선 + if (!isANull && isBNull) return -1; // B가 null이면 A가 우선 + + // dueDate 기준으로 날짜 순으로 정렬 + const dateA = isANull ? Number.MAX_SAFE_INTEGER : new Date(a.dueDate!).getTime(); + const dateB = isBNull ? Number.MAX_SAFE_INTEGER : new Date(b.dueDate!).getTime(); if (dateA < dateB) return -1; if (dateA > dateB) return 1; + // courseTitle로 기본 정렬 if (a.courseTitle < b.courseTitle) return -1; if (a.courseTitle > b.courseTitle) return 1; @@ -50,14 +65,6 @@ export function QuizContent() { }); }, []); - useEffect(() => { - loadDataFromStorage('quiz-notification', (data: string | null) => { - if (!data) return; - const parsedData = JSON.parse(data); - setNotificationMap(parsedData); - }); - }, []); - return (
@@ -71,9 +78,7 @@ export function QuizContent() {
{quizArray.map((quiz, index) => { const key = `${quiz.courseId}-${quiz.title}-${quiz.dueDate}`; - const notification = - notificationMap[key] === null || notificationMap[key] === undefined ? false : true; - return ; + return ; })}
diff --git a/src/option/components/VodCard.tsx b/src/option/components/VodCard.tsx index aeca9f8..853016a 100644 --- a/src/option/components/VodCard.tsx +++ b/src/option/components/VodCard.tsx @@ -1,28 +1,18 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { Card, CardContent } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; import { Badge } from '@/components/ui/badge'; -import { Check, Play, Clock } from 'lucide-react'; import { Vod } from '@/content/types'; -import NotificationSwitch from '@/components/ui/notification-switch'; -import { loadDataFromStorage, saveDataToStorage } from '@/lib/storage'; -import { toast, useToast } from '@/hooks/use-toast'; -import { - calculateRemainingTimeByRange, - calculateTimeDifference, - formatDateString, - removeSquareBrackets, -} from '@/lib/utils'; +import { calculateRemainingTimeByRange, formatDateString, removeSquareBrackets } from '@/lib/utils'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import CourseDetailModal from './CourseDetailModal'; interface TaskStatusCardProps { - notification: boolean; vodList: Vod[]; } -const VodCard: React.FC = ({ notification, vodList }) => { +const VodCard: React.FC = ({ vodList }) => { if (vodList.length === 0) return <>; let value = 0; @@ -32,53 +22,6 @@ const VodCard: React.FC = ({ notification, vodList }) => { const total = (value * 100) / vodList.length; const [isVisible, setIsVisible] = useState(false); - const [toggle, setToggle] = useState(notification); - const [userInitiatedToggle, setUserInitiatedToggle] = useState(false); - const { toast } = useToast(); - - useEffect(() => { - setToggle(notification); - }, [notification]); - - useEffect(() => { - loadDataFromStorage('vod-notification', (data: string | null) => { - try { - let parsedData: Record = {}; - if (data) { - try { - parsedData = JSON.parse(data); - } catch (error) { - console.error('저장된 데이터를 파싱하는 중 오류가 발생했습니다.', error); - } - } - const item = vodList[0]; - const key = `${item.courseId}-${item.subject}-${item.range}`; - - if (toggle) { - parsedData[key] = true; - } else { - if (key in parsedData) { - delete parsedData[key]; - } - } - saveDataToStorage('vod-notification', JSON.stringify(parsedData)); - - if (userInitiatedToggle) { - toast({ - title: toggle ? '알림 설정 🔔' : '알림 취소 🔕', - description: removeSquareBrackets(`${vodList[0].courseTitle} - ${vodList[0].subject}`), - variant: 'default', - }); - setUserInitiatedToggle(false); - } - } catch (error) { - toast({ - title: '오류가 발생 했습니다. 🚨', - variant: 'destructive', - }); - } - }); - }, [toggle, userInitiatedToggle, toast, vodList]); return ( <> @@ -104,38 +47,6 @@ const VodCard: React.FC = ({ notification, vodList }) => {

{removeSquareBrackets(vodList[0].courseTitle)}

-

- { - if (!toggle) { - chrome.runtime.sendMessage( - { - action: 'scheduleAlarm', - alarmId: `${vodList[0].courseId}-${vodList[0].subject}-${vodList[0].range.split(' ~ ')[1]}`, - dateTime: vodList[0].range.split(' ~ ')[1], - title: '하루 뒤 출석 마감!', - message: - removeSquareBrackets(vodList[0].courseTitle) + - '-' + - removeSquareBrackets(vodList[0].subject), - }, - (response) => {} - ); - } else { - chrome.runtime.sendMessage( - { - action: 'cancelAlarm', - alarmId: `${vodList[0].courseId}-${vodList[0].subject}-${vodList[0].range.split(' ~ ')[1]}`, - }, - (response) => {} - ); - } - setToggle((prev) => !prev); - setUserInitiatedToggle(true); - }} - /> -

{vodList[0].subject}
diff --git a/src/option/components/VodContent.tsx b/src/option/components/VodContent.tsx index 79e44a0..e1b452d 100644 --- a/src/option/components/VodContent.tsx +++ b/src/option/components/VodContent.tsx @@ -5,11 +5,11 @@ import { loadDataFromStorage } from '@/lib/storage'; import VodCard from './VodCard'; import { ScrollArea } from '@/components/ui/scroll-area'; import thung from '@/assets/thung.png'; +import { isCurrentDateInRange } from '@/lib/utils'; export function VodContent() { const date = new Date(); const [vodArray, setVodArray] = useState([]); - const [notificationMap, setNotificationMap] = useState>({}); useEffect(() => { loadDataFromStorage('vod', (data: string | null) => { if (!data) return; @@ -45,31 +45,52 @@ export function VodContent() { const isAX = firstA.weeklyAttendance.toUpperCase().startsWith('X'); const isBX = firstB.weeklyAttendance.toUpperCase().startsWith('X'); + // X가 있는 항목을 먼저 정렬 if (isAX && !isBX) return -1; if (!isAX && isBX) return 1; - const rangeStartA = firstA.range.split(' ~ ')[0]; - const rangeStartB = firstB.range.split(' ~ ')[0]; - const dateA = new Date(rangeStartA); - const dateB = new Date(rangeStartB); + const rangeA = firstA.range; + const rangeB = firstB.range; + const isRangeANull = rangeA === null; + const isRangeBNull = rangeB === null; - if (dateA < dateB) return -1; - if (dateA > dateB) return 1; + // isCurrentDateInRange가 true인 항목을 먼저 정렬 (X와 O 모두) + const isCurrentDateInRangeA = isCurrentDateInRange(firstA.range); + const isCurrentDateInRangeB = isCurrentDateInRange(firstB.range); + if (isAX) { + // X일 때는 isCurrentDateInRange가 true인 항목을 먼저 배치, 그 다음 null + if (isCurrentDateInRangeA && !isCurrentDateInRangeB) return -1; + if (!isCurrentDateInRangeA && isCurrentDateInRangeB) return 1; + if (isRangeANull && !isRangeBNull) return 1; + if (!isRangeANull && isRangeBNull) return -1; + } else { + // O일 때는 isCurrentDateInRange가 true인 항목을 먼저 배치, 그 다음 null, 그 다음 시간순 정렬 + if (isCurrentDateInRangeA && !isCurrentDateInRangeB) return -1; + if (!isCurrentDateInRangeA && isCurrentDateInRangeB) return 1; + if (isRangeANull && !isRangeBNull) return 1; + if (!isRangeANull && isRangeBNull) return -1; + + // rangeStart 날짜 기준으로 시간순으로 정렬 + if (!isRangeANull && !isRangeBNull) { + const rangeStartA = rangeA.split(' ~ ')[0]; + const rangeStartB = rangeB.split(' ~ ')[0]; + const dateA = new Date(rangeStartA); + const dateB = new Date(rangeStartB); + + if (dateA < dateB) return -1; + if (dateA > dateB) return 1; + } + } + + // courseTitle로 기본 정렬 if (firstA.courseTitle < firstB.courseTitle) return -1; if (firstA.courseTitle > firstB.courseTitle) return 1; return 0; }); - setVodArray(sortedVodGroups); - }); - }, []); - useEffect(() => { - loadDataFromStorage('vod-notification', (data: string | null) => { - if (!data) return; - const parsedData = JSON.parse(data); - setNotificationMap(parsedData); + setVodArray(sortedVodGroups); }); }, []); @@ -87,8 +108,7 @@ export function VodContent() { {vodArray.map((vodGroup, index) => { const item = vodGroup[0]; const key = `${item.courseId}-${item.subject}-${item.range}`; - const notification = notificationMap[key] != null; - return ; + return ; })}