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
11 changes: 11 additions & 0 deletions public/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@ <h3>나의 복습 주기</h3>
<button id="cycle-add-btn" class="btn btn-secondary btn-small">+ 새 주기 추가</button>
</div>

<button id="notification-time-btn" class="btn btn-secondary">알림 시간 설정</button>

<div id="notification-section" class="hidden">
<h3>알림 시간</h3>
<div class="form-group">
<label for="notification-time-input">매일 알림 받을 시간</label>
<input type="time" id="notification-time-input">
</div>
<button id="notification-time-save-btn" class="btn btn-primary btn-small">저장</button>
</div>

<button id="show-devices-btn" class="btn btn-secondary">디바이스 관리</button>

<div id="devices-section" class="hidden">
Expand Down
53 changes: 52 additions & 1 deletion src/__tests__/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
getCycleOptions,
createCustomCycle,
updateCustomCycle,
deleteCustomCycle
deleteCustomCycle,
getNotificationTime,
updateNotificationTime
} from '../api.js';
import { ERROR_CODES } from '../constants.js';

Expand Down Expand Up @@ -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({
Expand Down
28 changes: 28 additions & 0 deletions src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,31 @@ export async function deleteCustomCycle(identifier, id) {
headers: { 'X-Device-Id': identifier }
});
}

/**
* 알림 시간 조회
* @param {string} identifier - 디바이스 식별자
* @returns {Promise<Object>} { 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] }
});
}
1 change: 1 addition & 0 deletions src/handlers/device.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
3 changes: 3 additions & 0 deletions src/handlers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ export {

// URL 저장 관련
export { handleSaveUrl } from './url.js';

// 알림 시간 관련
export { handleShowNotificationTime, handleUpdateNotificationTime } from './notification.js';
69 changes: 69 additions & 0 deletions src/handlers/notification.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
6 changes: 5 additions & 1 deletion src/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ import {
handleDeleteCycle,
handleEditCycle,
handleAddCycle,
handleAddDuration
handleAddDuration,
handleShowNotificationTime,
handleUpdateNotificationTime
} from './handlers/index.js';

/**
Expand All @@ -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);

Expand Down
8 changes: 8 additions & 0 deletions src/ui/elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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');
Expand Down
1 change: 1 addition & 0 deletions src/ui/views.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down