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
6 changes: 5 additions & 1 deletion manifest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ const manifest = {
},
],
options_page: '/option.html',
permissions: ['storage', 'notifications', 'alarms'],
permissions: ['storage', 'notifications', 'alarms', 'identity'],
host_permissions: ['https://*/*', 'http://*/*'],
oauth2: {
client_id: '804067218183-3pev3tppten6i94lrfvmk729hmbdejqb.apps.googleusercontent.com',
scopes: ['https://www.googleapis.com/auth/calendar.events'],
},
} as ManifestV3Export;

export default manifest;
Binary file removed public/fonts/NotoSansKR-Black.ttf
Binary file not shown.
Binary file removed public/fonts/NotoSansKR-Bold.ttf
Binary file not shown.
Binary file removed public/fonts/NotoSansKR-ExtraBold.ttf
Binary file not shown.
Binary file removed public/fonts/NotoSansKR-ExtraLight.ttf
Binary file not shown.
Binary file removed public/fonts/NotoSansKR-Light.ttf
Binary file not shown.
Binary file removed public/fonts/NotoSansKR-Medium.ttf
Binary file not shown.
Binary file removed public/fonts/NotoSansKR-Regular.ttf
Binary file not shown.
Binary file removed public/fonts/NotoSansKR-SemiBold.ttf
Binary file not shown.
Binary file removed public/fonts/NotoSansKR-Thin.ttf
Binary file not shown.
100 changes: 100 additions & 0 deletions src/lib/calendarUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { CalendarEvent } from '@/hooks/useCalendarEvents';

export type GoogleCalendarEvent = {
summary: string;
description?: string;
start: {
dateTime: string;
timeZone: string;
};
end: {
dateTime: string;
timeZone: string;
};
};

/**
* OAuth 토큰을 localStorage에서 가져옵니다.
* 실제 구현에서는 OAuth 플로우에 따라 토큰을 갱신하거나, Context/API를 통해 관리할 수 있습니다.
*/
export const getOAuthToken = async (): Promise<string | null> => {
return new Promise((resolve) => {
chrome.identity.getAuthToken({ interactive: false }, (cachedToken) => {
if (chrome.runtime.lastError || !cachedToken) {
console.error('자동 로그인 실패:', chrome.runtime.lastError?.message);
resolve(null);
} else {
resolve(cachedToken);
}
});
});
};

/**
* 캘린더 API에 이벤트를 추가합니다.
* @param event 캘린더에 추가할 이벤트 객체
* @param token OAuth 토큰
*/
export async function addCalendarEvent(event: GoogleCalendarEvent, token: string): Promise<void> {
try {
fetch('https://www.googleapis.com/calendar/v3/calendars/primary/events', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(event),
})
.then((response) => response.json())
.then(() => {})
.catch((error) => console.error('이벤트 추가 실패:', error));
} catch (error) {
console.error('Error adding calendar event:', error);
}
}

/**
* 구글 캘린더 API를 사용해 현재 이벤트 목록을 가져옵니다.
* @param token OAuth 토큰
* @returns CalendarEvent 배열
*/
export async function getCalendarEvents(token: string): Promise<GoogleCalendarEvent[]> {
try {
const pastDate = new Date();
pastDate.setMonth(pastDate.getMonth() - 3);
const response = await fetch(
`https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin=${pastDate.toISOString()}&orderBy=startTime&singleEvents=true`,
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const data = await response.json();
return data.items || [];
} catch (error) {
console.error('캘린더 이벤트 가져오기 실패:', error);
return [];
}
}

/**
* CalendarEvent 배열을 받아 GoogleCalendarEvent 배열로 변환합니다.
* 여기서는 title과 subject를 summary로 조합하는 예시입니다.
* @param events CalendarEvent 배열
* @returns GoogleCalendarEvent 배열
*/
export function convertCalendarEventsToGoogleEvents(events: CalendarEvent[]): GoogleCalendarEvent[] {
return events.map((event) => ({
summary: `${event.title}`,
description: `${event.subject}`,
start: {
dateTime: event.start.toISOString(),
timeZone: 'Asia/Seoul',
},
end: {
dateTime: event.end.toISOString(),
timeZone: 'Asia/Seoul',
},
}));
}
2 changes: 2 additions & 0 deletions src/option/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import AssignmentPage from 'src/pages/AssignmentPage';
import DashboardPage from '@/pages/DashboardPage';
import QuizPage from '@/pages/QuizPage';
import Header from './Header';
import Labo from './Labo';

const pageVariants = {
initial: {
Expand Down Expand Up @@ -48,6 +49,7 @@ const AnimatedRoutes = () => {
<Route path="/vod" element={<VodPage />} />
<Route path="/assignment" element={<AssignmentPage />} />
<Route path="/quiz" element={<QuizPage />} />
<Route path="/labo" element={<Labo />} />
<Route path="*" element={<div>404 Not Found</div>} />
</Routes>
</motion.div>
Expand Down
110 changes: 110 additions & 0 deletions src/option/Labo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React, { useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';

const Labo: React.FC = () => {
const [token, setToken] = useState<string | null>(null);
const [events, setEvents] = useState<any[]>([]);

const fetchCalendarEvents = (token: string) => {
fetch(
'https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin=' +
new Date().toISOString() +
'&orderBy=startTime&singleEvents=true',
{
headers: {
Authorization: `Bearer ${token}`,
},
}
)
.then((response) => response.json())
.then((data) => setEvents(data.items || []))
.catch((error) => console.error('캘린더 이벤트 가져오기 실패:', error));
};

useEffect(() => {
chrome.identity.getAuthToken({ interactive: false }, (cachedToken) => {
if (chrome.runtime.lastError || !cachedToken) {
console.error('자동 로그인 실패:', chrome.runtime.lastError?.message);
} else {
setToken(cachedToken);
fetchCalendarEvents(cachedToken);
}
});
}, []);

const handleLogin = () => {
chrome.identity.getAuthToken({ interactive: true }, (newToken) => {
if (chrome.runtime.lastError || !newToken) {
console.error(chrome.runtime.lastError);
return;
}
setToken(newToken);
fetchCalendarEvents(newToken);
});
};

const addCalendarEvent = () => {
if (!token) return;

const event = {
summary: '테스트 이벤트 🎉',
description: '이것은 Google Calendar API를 사용한 이벤트 생성입니다.',
start: {
dateTime: new Date().toISOString(),
timeZone: 'Asia/Seoul',
},
end: {
dateTime: new Date(new Date().getTime() + 60 * 60 * 1000).toISOString(),
timeZone: 'Asia/Seoul',
},
};

fetch('https://www.googleapis.com/calendar/v3/calendars/primary/events', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(event),
})
.then((response) => response.json())
.then(() => {
alert('새로운 이벤트가 추가되었습니다!');
fetchCalendarEvents(token); // 이벤트 추가 후 다시 조회
})
.catch((error) => console.error('이벤트 추가 실패:', error));
};

return (
<div className="px-4 pt-6 pb-20">
<h1 className="text-xl font-bold mb-4">Google Calendar 연동</h1>
{token ? (
<div>
<Button className="mt-4 mr-2" onClick={addCalendarEvent}>
새 이벤트 추가
</Button>
<h2 className="text-lg font-semibold mt-6">내 캘린더 일정</h2>
<ul className="mt-2">
{events.length > 0 ? (
events.map((event, index) => (
<li key={index} className="mt-2 p-2 border rounded-lg bg-gray-100">
<strong>{event.summary}</strong>
<p>
{event.start?.dateTime?.replace('T', ' ').substring(0, 16)} ~{' '}
{event.end?.dateTime?.replace('T', ' ').substring(0, 16)}
</p>
</li>
))
) : (
<p className="text-gray-500">일정이 없습니다.</p>
)}
</ul>
</div>
) : (
<Button onClick={handleLogin}>Google 계정 연동</Button>
)}
</div>
);
};

export default Labo;
20 changes: 17 additions & 3 deletions src/option/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Home, LayoutDashboard, NotebookText, NotepadText, Video, Zap } from 'lucide-react';
import { Calendar, Home, LayoutDashboard, NotebookText, NotepadText, Video, Zap } from 'lucide-react';
import type React from 'react';
import { Link, useLocation } from 'react-router-dom';
import icon from '@/assets/icon.png';
Expand Down Expand Up @@ -28,17 +28,31 @@ const Sidebar: React.FC = () => {
</div>

<nav className="flex flex-col flex-grow">
<div className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-4">메인</div>
<div className="flex items-center mt-6 mb-2">
<div className="text-xs font-semibold text-gray-400 uppercase tracking-wider pr-2">메인</div>
<div className="flex-grow border-t border-gray-300"></div>
</div>
<ul className="space-y-2">
<SidebarItem to="/" icon={<LayoutDashboard size={20} />} label="대시보드" />
</ul>

<div className="text-xs font-semibold text-gray-400 uppercase tracking-wider mt-8 mb-4">항목</div>
<div className="flex items-center mt-6 mb-2">
<div className="text-xs font-semibold text-gray-400 uppercase tracking-wider pr-2">할 일</div>
<div className="flex-grow border-t border-gray-300"></div>
</div>
<ul className="space-y-2">
<SidebarItem to="/vod" icon={<Video size={20} />} label="강의" />
<SidebarItem to="/assignment" icon={<NotebookText size={20} />} label="과제" />
<SidebarItem to="/quiz" icon={<Zap size={20} />} label="퀴즈" />
</ul>
<div className="flex items-center mt-6 mb-2">
<div className="text-xs font-semibold text-gray-400 uppercase tracking-wider pr-2">실험실</div>
<div className="flex-grow border-t border-gray-300"></div>
</div>

<ul className="space-y-2">
<SidebarItem to="/labo" icon={<Calendar size={20} />} label="캘린더 연동" />
</ul>
</nav>
</div>
</aside>
Expand Down
Loading
Loading