From c638994e3849facd2df7ffe21a8b8aa18c012e64 Mon Sep 17 00:00:00 2001
From: flinter <56645802+jhan0121@users.noreply.github.com>
Date: Mon, 2 Mar 2026 17:22:32 +0900
Subject: [PATCH 1/3] =?UTF-8?q?v1.1.1-ext=20=EB=B0=B0=ED=8F=AC=20(#20)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 복습 URL 관리 크롬 익스텐션 구현 (#15)
* feat: 크롬 익스텐션 기능 추가
* refactor: vite 기반 프로젝트 구조 리팩터링
* chore: EOF 개행 추가
* refactor: XSS 취약점 방지를 위해 innerHTML 제거
* refactor: .env 기반 환경 관리 적용
* fix: 설정 변수명 오기입 수정
* refactor: json 파싱 에러 처리 명시
* refactor: 불필요한 리턴 제거
* refactor: 불필요한 import 제거
* chore: 파일별 주석 내용 정리
* refactor: CORS 우회를 위해 API 요청 구조 변경 (#18)
* CICD 자동화 스크립트 추가 (#2)
* refactor: 운영/개발 서버 환경 관리 추가
* feat: 배포 스크립트 추가
* test: 테스트 의존성 추가 및 작성
* feat: 테스트 검증 자동화 스크립트 추가
* feat: 배포 스크립트 추가
* 테스트 검증 스크립트 수정 (#4)
* feat: v1.0.1-ext 배포 (#5)
* 디바이스 식별자 전송 방식 변경 및 테스트 추가 (#8)
* refactor: 디바이스 관련 api 호출 구조 변경
* fix: 로그아웃 시 디바이스 id 삭제 api 호출 추가
* test: 테스트 추가
* build: dev 빌드 옵션 추가
* chore: 버전 v1.0.2로 수정 (#9)
* 주기 옵션 설정 기능 추가 (#13)
* v1.1.0-ext 배포 사전 작업 (#14)
* 알림 시간 설정 기능 추가 (#19)
* feat: 알림 시간 설정 기능 추가
* test: 알림 시간 설정 api 테스트 추가
* v1.1.1-ext 배포 사전 작업 (#21)
---
package.json | 2 +-
public/manifest.json | 2 +-
public/popup.html | 11 ++++++
src/__tests__/api.test.js | 53 ++++++++++++++++++++++++++-
src/api.js | 28 +++++++++++++++
src/handlers/device.js | 1 +
src/handlers/index.js | 3 ++
src/handlers/notification.js | 69 ++++++++++++++++++++++++++++++++++++
src/popup.js | 6 +++-
src/ui/elements.js | 8 +++++
src/ui/views.js | 1 +
11 files changed, 180 insertions(+), 4 deletions(-)
create mode 100644 src/handlers/notification.js
diff --git a/package.json b/package.json
index 214c45c..497174f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "recycle-study-extension",
- "version": "1.1.0",
+ "version": "1.1.1",
"description": "복습 URL 저장 크롬 익스텐션",
"type": "module",
"scripts": {
diff --git a/public/manifest.json b/public/manifest.json
index f443b7a..016f2e6 100644
--- a/public/manifest.json
+++ b/public/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Recycle Study",
- "version": "1.1.0",
+ "version": "1.1.1",
"description": "복습 URL을 저장하고 스케줄에 따라 알림을 받는 익스텐션",
"permissions": [
"storage",
diff --git a/public/popup.html b/public/popup.html
index ad77867..e5b1515 100644
--- a/public/popup.html
+++ b/public/popup.html
@@ -70,6 +70,17 @@
나의 복습 주기
+ 새 주기 추가
+ 알림 시간 설정
+
+
+
알림 시간
+
+ 매일 알림 받을 시간
+
+
+
저장
+
+
디바이스 관리
diff --git a/src/__tests__/api.test.js b/src/__tests__/api.test.js
index b9f6e55..6b6a0eb 100644
--- a/src/__tests__/api.test.js
+++ b/src/__tests__/api.test.js
@@ -7,7 +7,9 @@ import {
getCycleOptions,
createCustomCycle,
updateCustomCycle,
- deleteCustomCycle
+ deleteCustomCycle,
+ getNotificationTime,
+ updateNotificationTime
} from '../api.js';
import { ERROR_CODES } from '../constants.js';
@@ -222,6 +224,55 @@ describe('api.js', () => {
});
});
+ describe('getNotificationTime', () => {
+ it('식별자를 헤더에 포함하여 알림 시간을 조회한다', async () => {
+ const identifier = 'my-device-id';
+ const mockResponse = { success: true, data: { notificationTime: '09:00:00' } };
+ sendMessageMock.mockResolvedValue(mockResponse);
+
+ const result = await getNotificationTime(identifier);
+
+ expect(sendMessageMock).toHaveBeenCalledWith({
+ type: 'API_REQUEST',
+ request: {
+ endpoint: '/api/v1/members/notification-time',
+ method: 'GET',
+ headers: { 'X-Device-Id': identifier }
+ }
+ });
+ expect(result).toEqual(mockResponse.data);
+ });
+
+ it('알림 시간이 설정되지 않은 경우 null을 반환한다', async () => {
+ const mockResponse = { success: true, data: { notificationTime: null } };
+ sendMessageMock.mockResolvedValue(mockResponse);
+
+ const result = await getNotificationTime('my-device-id');
+
+ expect(result.notificationTime).toBeNull();
+ });
+ });
+
+ describe('updateNotificationTime', () => {
+ it('[hour, minute] 배열로 알림 시간을 업데이트한다', async () => {
+ const identifier = 'my-device-id';
+ const mockResponse = { success: true, data: null };
+ sendMessageMock.mockResolvedValue(mockResponse);
+
+ await updateNotificationTime(identifier, 9, 0);
+
+ expect(sendMessageMock).toHaveBeenCalledWith({
+ type: 'API_REQUEST',
+ request: {
+ endpoint: '/api/v1/members/notification-time',
+ method: 'PATCH',
+ headers: { 'X-Device-Id': identifier },
+ body: { notificationTime: [9, 0] }
+ }
+ });
+ });
+ });
+
describe('Error Handling', () => {
it('API 실패 시 에러를 던진다', async () => {
sendMessageMock.mockResolvedValue({
diff --git a/src/api.js b/src/api.js
index 5660700..ecd7784 100644
--- a/src/api.js
+++ b/src/api.js
@@ -148,3 +148,31 @@ export async function deleteCustomCycle(identifier, id) {
headers: { 'X-Device-Id': identifier }
});
}
+
+/**
+ * 알림 시간 조회
+ * @param {string} identifier - 디바이스 식별자
+ * @returns {Promise} { notificationTime: "09:00:00" } or { notificationTime: null }
+ */
+export async function getNotificationTime(identifier) {
+ return await sendApiRequest({
+ endpoint: '/api/v1/members/notification-time',
+ method: 'GET',
+ headers: { 'X-Device-Id': identifier }
+ });
+}
+
+/**
+ * 알림 시간 업데이트
+ * @param {string} identifier - 디바이스 식별자
+ * @param {number} hour - 시 (0-23)
+ * @param {number} minute - 분 (0-59)
+ */
+export async function updateNotificationTime(identifier, hour, minute) {
+ return await sendApiRequest({
+ endpoint: '/api/v1/members/notification-time',
+ method: 'PATCH',
+ headers: { 'X-Device-Id': identifier },
+ body: { notificationTime: [hour, minute] }
+ });
+}
diff --git a/src/handlers/device.js b/src/handlers/device.js
index fb8c806..33d6390 100644
--- a/src/handlers/device.js
+++ b/src/handlers/device.js
@@ -139,6 +139,7 @@ export async function handleLogout() {
elements.saveResult.classList.add('hidden');
elements.devicesSection.classList.add('hidden');
elements.cycleSection.classList.add('hidden');
+ elements.notificationSection.classList.add('hidden');
showView('login');
showMessage('로그아웃 되었습니다.', 'info');
}
diff --git a/src/handlers/index.js b/src/handlers/index.js
index db65c28..d673f8b 100644
--- a/src/handlers/index.js
+++ b/src/handlers/index.js
@@ -24,3 +24,6 @@ export {
// URL 저장 관련
export { handleSaveUrl } from './url.js';
+
+// 알림 시간 관련
+export { handleShowNotificationTime, handleUpdateNotificationTime } from './notification.js';
diff --git a/src/handlers/notification.js b/src/handlers/notification.js
new file mode 100644
index 0000000..2e7df38
--- /dev/null
+++ b/src/handlers/notification.js
@@ -0,0 +1,69 @@
+/**
+ * 알림 시간 관련 핸들러
+ *
+ * 알림 시간 조회 및 업데이트 처리를 담당한다.
+ */
+
+import { getNotificationTime, updateNotificationTime } from '../api.js';
+import { validateStorageForAuth } from '../storage.js';
+import {
+ elements,
+ showLoading,
+ hideLoading,
+ showMessage,
+ handleApiError
+} from '../ui/index.js';
+
+/**
+ * 알림 시간 설정 버튼 클릭 핸들러 (toggle)
+ */
+export async function handleShowNotificationTime() {
+ const isVisible = !elements.notificationSection.classList.contains('hidden');
+
+ if (isVisible) {
+ elements.notificationSection.classList.add('hidden');
+ return;
+ }
+
+ try {
+ showLoading();
+ const storageData = await validateStorageForAuth();
+ const result = await getNotificationTime(storageData.identifier);
+
+ if (result.notificationTime) {
+ elements.notificationTimeInput.value = result.notificationTime.substring(0, 5);
+ } else {
+ elements.notificationTimeInput.value = '';
+ }
+
+ elements.notificationSection.classList.remove('hidden');
+ } catch (error) {
+ await handleApiError(error);
+ } finally {
+ hideLoading();
+ }
+}
+
+/**
+ * 알림 시간 저장 버튼 클릭 핸들러
+ */
+export async function handleUpdateNotificationTime() {
+ const timeValue = elements.notificationTimeInput.value;
+
+ if (!timeValue) {
+ showMessage('알림 시간을 입력해주세요.', 'error');
+ return;
+ }
+
+ try {
+ showLoading();
+ const storageData = await validateStorageForAuth();
+ const [hour, minute] = timeValue.split(':').map(Number);
+ await updateNotificationTime(storageData.identifier, hour, minute);
+ showMessage('알림 시간이 저장되었습니다.', 'success');
+ } catch (error) {
+ await handleApiError(error);
+ } finally {
+ hideLoading();
+ }
+}
diff --git a/src/popup.js b/src/popup.js
index bcf4f1c..12292ff 100644
--- a/src/popup.js
+++ b/src/popup.js
@@ -26,7 +26,9 @@ import {
handleDeleteCycle,
handleEditCycle,
handleAddCycle,
- handleAddDuration
+ handleAddDuration,
+ handleShowNotificationTime,
+ handleUpdateNotificationTime
} from './handlers/index.js';
/**
@@ -37,6 +39,8 @@ function setupEventListeners() {
elements.checkAuthBtn.addEventListener('click', handleCheckAuth);
elements.resetBtn.addEventListener('click', handleReset);
elements.saveUrlBtn.addEventListener('click', handleSaveUrl);
+ elements.notificationTimeBtn.addEventListener('click', handleShowNotificationTime);
+ elements.notificationTimeSaveBtn.addEventListener('click', handleUpdateNotificationTime);
elements.showDevicesBtn.addEventListener('click', handleShowDevices);
elements.logoutBtn.addEventListener('click', handleLogout);
diff --git a/src/ui/elements.js b/src/ui/elements.js
index 8badbfe..0564a44 100644
--- a/src/ui/elements.js
+++ b/src/ui/elements.js
@@ -32,6 +32,10 @@ export const elements = {
cycleSection: null,
cycleList: null,
cycleAddBtn: null,
+ notificationTimeBtn: null,
+ notificationSection: null,
+ notificationTimeInput: null,
+ notificationTimeSaveBtn: null,
showDevicesBtn: null,
devicesSection: null,
devicesList: null,
@@ -76,6 +80,10 @@ export function initializeElements() {
elements.cycleSection = document.getElementById('cycle-section');
elements.cycleList = document.getElementById('cycle-list');
elements.cycleAddBtn = document.getElementById('cycle-add-btn');
+ elements.notificationTimeBtn = document.getElementById('notification-time-btn');
+ elements.notificationSection = document.getElementById('notification-section');
+ elements.notificationTimeInput = document.getElementById('notification-time-input');
+ elements.notificationTimeSaveBtn = document.getElementById('notification-time-save-btn');
elements.showDevicesBtn = document.getElementById('show-devices-btn');
elements.devicesSection = document.getElementById('devices-section');
elements.devicesList = document.getElementById('devices-list');
diff --git a/src/ui/views.js b/src/ui/views.js
index b3e3f09..e10a249 100644
--- a/src/ui/views.js
+++ b/src/ui/views.js
@@ -38,6 +38,7 @@ export async function forceLogout(message) {
await clearStorage();
elements.saveResult.classList.add('hidden');
elements.devicesSection.classList.add('hidden');
+ elements.notificationSection.classList.add('hidden');
elements.emailInput.value = '';
showView('login');
From f14a1a731573efb25605f525ef9f5f45da418527 Mon Sep 17 00:00:00 2001
From: jhan0121
Date: Tue, 10 Mar 2026 02:17:11 +0900
Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=EC=B5=9C=EC=B4=88=20=EB=A1=9C?=
=?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=8B=9C,=20=EC=A3=BC=EA=B8=B0=20?=
=?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EC=98=A4=EB=A5=98=20?=
=?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/handlers/auth.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/handlers/auth.js b/src/handlers/auth.js
index 5034a9d..efbdfda 100644
--- a/src/handlers/auth.js
+++ b/src/handlers/auth.js
@@ -8,6 +8,7 @@ import { STORAGE_KEYS } from '../config.js';
import { ERROR_CODES } from '../constants.js';
import { registerDevice, getDevices } from '../api.js';
import { setStorageData, clearStorage, validateStorageForAuth } from '../storage.js';
+import { handleLoadCycleOptions } from './cycle.js';
import {
elements,
showLoading,
@@ -69,6 +70,7 @@ export async function handleCheckAuth() {
elements.userEmail.textContent = result.email;
showView('main');
+ await handleLoadCycleOptions();
showMessage('인증이 완료되었습니다!', 'success');
} catch (error) {
if (error.code === ERROR_CODES.UNAUTHORIZED) {
From e571dfd4a73f618282a9bcc5ac111dedcd58654c Mon Sep 17 00:00:00 2001
From: jhan0121
Date: Tue, 10 Mar 2026 02:25:46 +0900
Subject: [PATCH 3/3] =?UTF-8?q?chore:=20v1.1.2-ext=20=EB=B0=B0=ED=8F=AC=20?=
=?UTF-8?q?=EC=82=AC=EC=A0=84=20=EC=9E=91=EC=97=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
package.json | 2 +-
public/manifest.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index 497174f..b56a3b0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "recycle-study-extension",
- "version": "1.1.1",
+ "version": "1.1.2",
"description": "복습 URL 저장 크롬 익스텐션",
"type": "module",
"scripts": {
diff --git a/public/manifest.json b/public/manifest.json
index 016f2e6..f5fe730 100644
--- a/public/manifest.json
+++ b/public/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Recycle Study",
- "version": "1.1.1",
+ "version": "1.1.2",
"description": "복습 URL을 저장하고 스케줄에 따라 알림을 받는 익스텐션",
"permissions": [
"storage",