Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 122 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"lucide-react": "^0.471.2",
"node-fetch": "^3.3.2",
"react": "^18.3.1",
"react-colorful": "^5.6.1",
"react-dom": "^18.3.1",
"react-router-dom": "^7.1.3",
"tailwind-merge": "^2.6.0",
Expand Down
37 changes: 27 additions & 10 deletions src/content/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useEffect, useMemo, useState } from 'react';
import icon from '@/assets/icon.png';
import exit from '@/assets/exit.png';
import { Assign, Filters, Quiz, TAB_TYPE, Vod } from './types';
import { ListFilter, RefreshCw, Search } from 'lucide-react';
import { ListFilter, OctagonAlert, RefreshCw, Search } from 'lucide-react';
import filter from '@/assets/filter.svg';
import PopoverFooter from './components/PopoverFooter';
import { Spinner } from '@/components/ui/spinner';
Expand Down Expand Up @@ -31,7 +31,7 @@ export default function App() {
const { courses } = useGetCourses();

// 데이터 관련 상태를 useCourseData 커스텀 훅으로 관리
const { vods, assigns, quizes, isPending, remainingTime, refreshTime, updateData, setIsPending } =
const { vods, assigns, quizes, isPending, remainingTime, refreshTime, isError, updateData, setIsPending } =
useCourseData(courses);

// activeTab의 타입을 TAB_TYPE으로 지정
Expand Down Expand Up @@ -198,10 +198,8 @@ export default function App() {
: '오류'}
</div>
<div className="flex justify-center items-center">
{/* refreshTime 대신 남은 시간을 표시 */}
<span
className={`text-sm px-1 ${
// 필요에 따라 색상 조건은 수정 가능 (예시로 30분 기준)
remainingTime < 60
? Math.round(remainingTime) >= 30
? 'text-amber-500 font-semibold'
Expand All @@ -216,10 +214,12 @@ export default function App() {
: `${Math.floor(remainingTime / 60)}시간 전`}
</span>
<button
className={`flex rounded-lg gap-1 bg-white hover:bg-zinc-100 transition-all duration-200 p-2 ml-1 ${(isPending || remainingTime <= 5) && 'cursor-not-allowed'}`}
disabled={isPending || remainingTime <= 5}
className={`flex rounded-lg gap-1 bg-white hover:bg-zinc-100 transition-all duration-200 p-2 ml-1 ${(isPending || remainingTime <= 1) && 'cursor-not-allowed'}`}
disabled={isPending || remainingTime <= 1}
onClick={() => {
if (isPending || remainingTime <= 5) return;
if (isPending || remainingTime <= 1) {
return;
}
setIsPending(true);
updateData();
}}
Expand Down Expand Up @@ -318,9 +318,26 @@ export default function App() {
</div>
) : (
<>
{activeTab === 'VIDEO' && <Video courseData={filteredVods} />}
{activeTab === 'ASSIGN' && <Assignment courseData={filteredAssigns} />}
{activeTab === 'QUIZ' && <QuizTab courseData={filteredQuizes} />}
{isError ? (
<div className="w-full h-full flex flex-col items-center justify-center gap-2">
<OctagonAlert className="w-12 h-12 text-red-800" />
<p className="py-4 text-2xl font-semibold text-red-800">오류가 발생했습니다.</p>
<p
onClick={() => {
location.reload();
}}
className="py-4 text-xl font-medium underline text-zinc-500 hover:text-zinc-950 hover:cursor-pointer transition-all duration-200"
>
페이지 새로고침
</p>
</div>
) : (
<>
{activeTab === 'VIDEO' && <Video courseData={filteredVods} />}
{activeTab === 'ASSIGN' && <Assignment courseData={filteredAssigns} />}
{activeTab === 'QUIZ' && <QuizTab courseData={filteredQuizes} />}
</>
)}
</>
)}
</div>
Expand Down
7 changes: 5 additions & 2 deletions src/hooks/useCourseData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ export function useCourseData(courses: any[]) {
const [refreshTime, setRefreshTime] = useState<string | null>(null);
const [isPending, setIsPending] = useState<boolean>(false);
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([]);
Expand Down Expand Up @@ -98,7 +100,8 @@ export function useCourseData(courses: any[]) {
saveDataToStorage('lastRequestTime', currentTime.toString());
setIsPending(false);
} catch (error) {
console.error('Error fetching data:', error);
localStorage.removeItem('lastRequestTime');
setIsError(true);
setIsPending(false);
}
}, [courses]);
Expand Down Expand Up @@ -142,5 +145,5 @@ export function useCourseData(courses: any[]) {
});
}
}, [courses, updateData]);
return { vods, assigns, quizes, isPending, remainingTime, refreshTime, updateData, setIsPending };
return { vods, assigns, quizes, isPending, remainingTime, refreshTime, isError, updateData, setIsPending };
}
41 changes: 20 additions & 21 deletions src/hooks/useGetCourse.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CourseBase } from '@/content/types';
import { saveDataToStorage } from '@/lib/storage';
import { removeSquareBrackets } from '@/lib/utils';
import { useState, useEffect } from 'react';

Expand All @@ -12,27 +13,25 @@ export const useGetCourses = (): UseCouresResult => {
if (!document) return;
const courseData = Array.from(document.querySelectorAll('.course_box'));
const data = courseData
.map((div) => {
const label = div.querySelector('.course_link .course-name .course-label')?.textContent?.trim() || null;
if (!label || label === '커뮤니티') return null;
const a = div.querySelector('a');
const url = new URL((a as HTMLAnchorElement).href);
const urlParams = new URLSearchParams(url.search);
const courseId = urlParams.get('id') || '';
const titleSection = div.querySelector('.course_link .course-name .course-title');
const prof = titleSection?.querySelector('p')?.textContent?.trim() || '';
let courseTitle =
titleSection?.querySelector('h1, h2, h3')?.textContent?.replace(/new/i, '').trim() ||
'';
courseTitle = removeSquareBrackets(courseTitle);
return { courseId, courseTitle, prof };
})
.filter(
(item): item is { courseId: string; courseTitle: string; prof: string } =>
item !== null && item.courseId !== '' && item.courseTitle !== '' && item.prof !== ''
);
setCourses(data);

.map((div) => {
const label = div.querySelector('.course_link .course-name .course-label')?.textContent?.trim() || null;
if (!label || label === '커뮤니티') return null;
const a = div.querySelector('a');
const url = new URL((a as HTMLAnchorElement).href);
const urlParams = new URLSearchParams(url.search);
const courseId = urlParams.get('id') || '';
const titleSection = div.querySelector('.course_link .course-name .course-title');
const prof = titleSection?.querySelector('p')?.textContent?.trim() || '';
let courseTitle = titleSection?.querySelector('h1, h2, h3')?.textContent?.replace(/new/i, '').trim() || '';
courseTitle = removeSquareBrackets(courseTitle);
return { courseId, courseTitle, prof };
})
.filter(
(item): item is { courseId: string; courseTitle: string; prof: string } =>
item !== null && item.courseId !== '' && item.courseTitle !== '' && item.prof !== ''
);
setCourses(data);
saveDataToStorage('courses', JSON.stringify(data));
}, []);

return { courses };
Expand Down
4 changes: 0 additions & 4 deletions src/lib/calendarUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ export type GoogleCalendarEvent = {
};
};

/**
* OAuth 토큰을 localStorage에서 가져옵니다.
* 실제 구현에서는 OAuth 플로우에 따라 토큰을 갱신하거나, Context/API를 통해 관리할 수 있습니다.
*/
export const getOAuthToken = async (): Promise<string | null> => {
return new Promise((resolve) => {
chrome.identity.getAuthToken({ interactive: false }, (cachedToken) => {
Expand Down
Loading
Loading