Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8943a23
fix: correct formatting of Todo items in markdown conversion
Turtle-Hwan Oct 12, 2025
ef9ecd6
fix: initialize dueDate and dueTime with local values in TodoAddDialog
Turtle-Hwan Oct 16, 2025
7d8d52f
fix: set default value of rememberLogin to true in LoginDialog
Turtle-Hwan Nov 2, 2025
97bdf78
feat: make shared extension APIs safe for web preview
Turtle-Hwan Mar 14, 2026
75e4729
feat: add local ecampus sample fallback for web testing
Turtle-Hwan Mar 14, 2026
30a2830
feat: guard settings chrome access for web runtime
Turtle-Hwan Mar 14, 2026
f8c288d
feat: sync todo badges on popup load
Turtle-Hwan Mar 14, 2026
8632b27
feat: add realtime todo timer setting
Turtle-Hwan Mar 14, 2026
084ea7e
feat: add todo countdown and deadline utilities
Turtle-Hwan Mar 14, 2026
babab13
refactor: split todo list auth and settings logic
Turtle-Hwan Mar 14, 2026
24f985f
refactor: centralize todo count sync in badge
Turtle-Hwan Mar 14, 2026
bc11832
refactor: centralize ecampus todo loading flow
Turtle-Hwan Mar 14, 2026
aeb7858
refactor: move todo list state into dedicated hook
Turtle-Hwan Mar 14, 2026
0030f6a
fix(todo): align rebuilt changes with current main
Turtle-Hwan Jul 30, 2026
319da94
refactor(todo): separate ecampus loading from custom todos
Turtle-Hwan Jul 30, 2026
ddbb611
fix(analytics): skip lifecycle events in web preview
Turtle-Hwan Jul 30, 2026
8598cdd
fix(todo): complete timer and badge behavior
Turtle-Hwan Jul 30, 2026
3a3d2e1
refactor(todo): harden cache count and timer boundaries
Turtle-Hwan Jul 30, 2026
6cf1f1d
Merge remote-tracking branch 'origin/main' into feat/todo
Turtle-Hwan Aug 7, 2026
d2ad5d6
fix(todo): sync ecampus account changes
Turtle-Hwan Aug 7, 2026
92039b4
fix(todo): guard eCampus account state
Turtle-Hwan Aug 8, 2026
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"check:content-scripts": "node scripts/checkContentScripts.js",
"lint": "eslint .",
"test:timetable": "node --experimental-strip-types --test tests/timetable/*.test.ts",
"test:todo": "node --experimental-strip-types --test tests/todo/*.test.ts",
"preview": "vite preview",
"deploy": "git subtree push --prefix gh-pages origin gh-pages"
},
Expand Down
13 changes: 9 additions & 4 deletions src/apis/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import type { ApiResponse, RequestConfig } from "../types/api";
import { BackgroundMessageType } from "../background/types";
import type { SilentReauthResponse } from "../background/types";
import { getChromeApi, getStorage, removeStorage } from "../utils/chrome";
import { debugLog, errorLog, getErrorLogDetails, warnLog } from "@/utils/logger";

/**
Expand Down Expand Up @@ -79,13 +80,12 @@ export const ENDPOINTS = {
* Using chrome.storage.local for persistent token storage
*/
async function getAccessToken(): Promise<string | null> {
const result = await chrome.storage.local.get(["accessToken"]);
const token = result.accessToken;
const token = await getStorage<unknown>("accessToken");
return typeof token === "string" ? token : null;
}

async function clearAccessToken(): Promise<void> {
await chrome.storage.local.remove([
await removeStorage([
"accessToken",
"refreshToken",
"guestToken",
Expand All @@ -107,10 +107,15 @@ async function handleTokenExpired(): Promise<boolean> {

debugLog("[API Client] Token expired (5004), attempting silent reauth...");

const chromeApi = getChromeApi();
if (!chromeApi?.runtime?.sendMessage) {
return false;
}

isReauthenticating = true;
reauthPromise = (async () => {
try {
const response = await chrome.runtime.sendMessage<
const response = await chromeApi.runtime.sendMessage<
{ type: BackgroundMessageType.SILENT_REAUTH },
SilentReauthResponse
>({
Expand Down
139 changes: 138 additions & 1 deletion src/apis/external/ecampus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import { ECampusTodoItem } from '@/types/todo';
import { errorLog } from '@/utils/logger';
import { isExtensionEnvironment } from '@/utils/chrome';
import { calculateDDay } from '@/utils/todo/dateFormat';

export interface ECampusLoginResponse {
success: boolean;
Expand Down Expand Up @@ -36,6 +38,98 @@ export interface ECampusGoLectureResponse {
error?: string;
}

const LOCAL_SAMPLE_LECTURE_URL = '__LOCAL_SAMPLE_ECAMPUS_TODO__';

const isLocalSampleMode = () => {
return import.meta.env.MODE === 'development' && !isExtensionEnvironment();
};

const pad = (value: number) => String(value).padStart(2, '0');

const formatECampusDueDate = (date: Date) => {
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
const hours = date.getHours();
const minutes = pad(date.getMinutes());
const period = hours < 12 ? '오전' : '오후';
const twelveHour = hours % 12 === 0 ? 12 : hours % 12;

return `${year}.${month}.${day} ${period} ${twelveHour}:${minutes}`;
};

const createLocalSampleTodo = (
id: string,
title: string,
subject: string,
dueAt: Date,
kj: string,
seq: string,
gubun: string
): ECampusTodoItem => {
const dueDate = formatECampusDueDate(dueAt);
const dDay = calculateDDay(
`${dueAt.getFullYear()}.${pad(dueAt.getMonth() + 1)}.${pad(dueAt.getDate())}`,
`${pad(dueAt.getHours())}:${pad(dueAt.getMinutes())}`
);

return {
type: 'ecampus',
id,
title,
subject,
dDay,
dueDate,
kj,
gubun,
seq,
};
};

const getLocalSampleTodos = (): ECampusTodoItem[] => {
const now = new Date();

const urgentDue = new Date(now.getTime() + 2 * 60 * 60 * 1000);
urgentDue.setSeconds(0, 0);

const todayDue = new Date(now);
todayDue.setHours(23, 59, 0, 0);

const tomorrowMorningDue = new Date(now);
tomorrowMorningDue.setDate(tomorrowMorningDue.getDate() + 1);
tomorrowMorningDue.setHours(10, 0, 0, 0);

return [
createLocalSampleTodo(
'ecampus-local-1',
'캡스톤디자인 발표 자료 제출',
'캡스톤디자인',
urgentDue,
'local-kj-1',
'local-seq-1',
'report'
),
createLocalSampleTodo(
'ecampus-local-2',
'운영체제 퀴즈 응시',
'운영체제',
todayDue,
'local-kj-2',
'local-seq-2',
'quiz'
),
createLocalSampleTodo(
'ecampus-local-3',
'자료구조 5주차 강의 시청',
'자료구조',
tomorrowMorningDue,
'local-kj-3',
'local-seq-3',
'lecture_weeks'
),
];
};

/**
* Login to eCampus
* @param userId User ID
Expand All @@ -46,6 +140,20 @@ export async function eCampusLoginAPI(
userId: string,
userPw: string
): Promise<ECampusLoginResponse> {
if (isLocalSampleMode()) {
return {
success: true,
data: {
isError: false,
message: `${userId || 'local-user'} 로컬 로그인 성공`,
count: 0,
returnURL: '/local/ecampus',
ids_yn: 'Y',
VERIFY: 'LOCAL_SAMPLE_MODE',
},
};
}

try {
const response = await fetch(
'https://ecampus.konkuk.ac.kr/ilos/lo/login.acl?data=jsonLogin',
Expand Down Expand Up @@ -89,6 +197,15 @@ export async function eCampusLoginAPI(
* @returns Todo list response
*/
export async function eCampusTodoListAPI(): Promise<ECampusTodoResponse> {
if (isLocalSampleMode()) {
return {
success: true,
data: {
todoList: getLocalSampleTodos(),
},
};
}

try {
const response = await fetch(
'https://ecampus.konkuk.ac.kr/ilos/mp/todo_list.acl',
Expand All @@ -102,6 +219,16 @@ export async function eCampusTodoListAPI(): Promise<ECampusTodoResponse> {
credentials: 'include',
}
);

if (!response.ok) {
return {
success: false,
error: new Error(
`eCampus todo request failed: ${response.status} ${response.statusText}`,
),
};
}

const htmlText = await response.text();

// Parse HTML using DOM parser
Expand Down Expand Up @@ -160,7 +287,7 @@ export async function eCampusTodoListAPI(): Promise<ECampusTodoResponse> {
};
} catch (error) {
errorLog('Failed to fetch todo list:', error);
return { success: false, needLogin: true, error };
return { success: false, error };
}
}

Expand All @@ -176,6 +303,14 @@ export async function eCampusGoLectureAPI(
seq: string,
gubun: string
): Promise<ECampusGoLectureResponse> {
if (isLocalSampleMode()) {
return {
success: true,
isError: false,
message: LOCAL_SAMPLE_LECTURE_URL,
};
}

try {
const lectureUrl = `/ilos/mp/todo_list_connect.acl?SEQ=${seq}&gubun=${gubun}&KJKEY=${kj}`;

Expand All @@ -192,3 +327,5 @@ export async function eCampusGoLectureAPI(
};
}
}

export { LOCAL_SAMPLE_LECTURE_URL };
17 changes: 13 additions & 4 deletions src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ import type {
TimetableImportResponse,
} from "./types";
import { handleGoogleLogin } from "./handlers/oauth";
import {
formatTodoBadgeCount,
TODO_BADGE_BACKGROUND_COLOR,
TODO_BADGE_TEXT_COLOR,
} from "@/utils/todo/badge";
import {
handlePendingImportTabRemoved,
handlePendingImportTabUpdated,
Expand Down Expand Up @@ -212,10 +217,14 @@ chrome.tabs.onRemoved.addListener((tabId) => {
* Badge update for todo count
*/
function updateBadge(count: number) {
if (count > 0) {
chrome.action.setBadgeText({ text: count > 99 ? "99+" : String(count) });
chrome.action.setBadgeBackgroundColor({ color: "#00913A" });
chrome.action.setBadgeTextColor({ color: "#FFFFFF" });
const badgeText = formatTodoBadgeCount(count);

if (badgeText) {
chrome.action.setBadgeText({ text: badgeText });
chrome.action.setBadgeBackgroundColor({
color: TODO_BADGE_BACKGROUND_COLOR,
});
chrome.action.setBadgeTextColor({ color: TODO_BADGE_TEXT_COLOR });
} else {
chrome.action.setBadgeText({ text: "" });
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/EmailVerificationDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from '@/utils/formValidation';
import { errorLog } from '@/utils/logger';
import { sendAuthEmailVerificationStart, sendAuthEmailVerificationSuccess } from '@/utils/analytics';
import { setStorage } from '@/utils/chrome';

interface EmailVerificationDialogProps {
open: boolean;
Expand Down Expand Up @@ -107,7 +108,7 @@ export function EmailVerificationDialog({
if (response.success) {
toast.success('이메일 인증이 완료되었습니다!');
// Store verified email
await chrome.storage.local.set({ kuMail });
await setStorage({ kuMail });
sendAuthEmailVerificationSuccess('konkuk.ac.kr');
// Trigger re-login to get member token
onVerificationComplete();
Expand Down
Loading