Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "recycle-study-extension",
"version": "1.1.1",
"version": "1.1.2",
"description": "복습 URL 저장 크롬 익스텐션",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Recycle Study",
"version": "1.1.1",
"version": "1.1.2",
"description": "복습 URL을 저장하고 스케줄에 따라 알림을 받는 익스텐션",
"permissions": [
"storage",
Expand Down
11 changes: 11 additions & 0 deletions public/popup.css
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,17 @@ body {
color: #666;
}

/* 다음 리뷰 정보 */
.next-review-info {
margin-bottom: 12px;
padding: 8px 12px;
background-color: #eaf4fb;
border-radius: 6px;
font-size: 12px;
color: #2980b9;
text-align: center;
}

/* 저장 결과 */
.result {
margin-top: 16px;
Expand Down
4 changes: 4 additions & 0 deletions public/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ <h1>Recycle Study</h1>
<span id="user-email"></span>
</div>

<div id="next-review-info" class="next-review-info hidden">
<span id="next-review-text"></span>
</div>

<!-- 복습 주기 선택 -->
<div class="form-group">
<label for="cycle-select">복습 주기</label>
Expand Down
13 changes: 13 additions & 0 deletions src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,16 @@ export async function updateNotificationTime(identifier, hour, minute) {
body: { notificationTime: [hour, minute] }
});
}

/**
* 다음 리뷰 정보 조회
* @param {string} identifier - 디바이스 식별자
* @returns {Promise<Object>} { scheduledAt, count }
*/
export async function getNextReview(identifier) {
return await sendApiRequest({
endpoint: '/api/v1/reviews/next',
method: 'GET',
headers: { 'X-Device-Id': identifier }
});
}
2 changes: 2 additions & 0 deletions src/handlers/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions src/handlers/notification.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
showMessage,
handleApiError
} from '../ui/index.js';
import { utcTimeStringToLocal, localTimeStringToUtcArray } from '../utils.js';

/**
* 알림 시간 설정 버튼 클릭 핸들러 (toggle)
Expand All @@ -31,7 +32,7 @@ export async function handleShowNotificationTime() {
const result = await getNotificationTime(storageData.identifier);

if (result.notificationTime) {
elements.notificationTimeInput.value = result.notificationTime.substring(0, 5);
elements.notificationTimeInput.value = utcTimeStringToLocal(result.notificationTime);
} else {
elements.notificationTimeInput.value = '';
}
Expand All @@ -58,7 +59,7 @@ export async function handleUpdateNotificationTime() {
try {
showLoading();
const storageData = await validateStorageForAuth();
const [hour, minute] = timeValue.split(':').map(Number);
const [hour, minute] = localTimeStringToUtcArray(timeValue);
await updateNotificationTime(storageData.identifier, hour, minute);
showMessage('알림 시간이 저장되었습니다.', 'success');
} catch (error) {
Expand Down
19 changes: 19 additions & 0 deletions src/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
showMessage,
hideCycleModal
} from './ui/index.js';
import { getNextReview } from './api.js';
import { formatNextReviewDate } from './utils.js';
import {
handleRegister,
handleCheckAuth,
Expand Down Expand Up @@ -98,6 +100,7 @@ async function initialize() {
showView('main');
// 주기 옵션 로드
handleLoadCycleOptions();
loadNextReview(storageData.identifier);
} else if (storageData.email && storageData.identifier) {
elements.emailDisplay.textContent = storageData.email;
showView('pending');
Expand All @@ -111,6 +114,22 @@ async function initialize() {
}
}

/**
* 다음 리뷰 정보 로드 및 표시
*/
async function loadNextReview(identifier) {
try {
const result = await getNextReview(identifier);
if (result.scheduledAt && result.count > 0) {
elements.nextReviewText.textContent =
`다음 리뷰: ${formatNextReviewDate(result.scheduledAt)} / 대기 중 ${result.count}개`;
elements.nextReviewInfo.classList.remove('hidden');
}
} catch (error) {
console.debug('다음 리뷰 정보 조회 실패:', error);
}
}

/**
* DOM 로드 후 실행
*/
Expand Down
4 changes: 4 additions & 0 deletions src/ui/elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export const elements = {

// 메인 화면
userEmail: null,
nextReviewInfo: null,
nextReviewText: null,
cycleSelect: null,
saveUrlBtn: null,
saveResult: null,
Expand Down Expand Up @@ -72,6 +74,8 @@ export function initializeElements() {
elements.resetBtn = document.getElementById('reset-btn');

elements.userEmail = document.getElementById('user-email');
elements.nextReviewInfo = document.getElementById('next-review-info');
elements.nextReviewText = document.getElementById('next-review-text');
elements.cycleSelect = document.getElementById('cycle-select');
elements.saveUrlBtn = document.getElementById('save-url-btn');
elements.saveResult = document.getElementById('save-result');
Expand Down
59 changes: 58 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,30 @@ export function formatDate(dateString) {
});
}

/**
* 다음 리뷰 날짜 포맷팅 (월/일/시:분)
* @param {string} dateString - ISO 형식 날짜 문자열
* @returns {string} 포맷된 날짜 문자열 (예: "3월 6일 09:00")
*/
export function formatNextReviewDate(dateString) {
// 타임존 정보가 없으면 UTC로 가정 (서버 버그 방어)
const hasTimezone = /Z$|[+-]\d{2}:\d{2}$/.test(dateString);
const normalized = hasTimezone ? dateString : dateString + 'Z';

const date = new Date(normalized);
const parts = new Intl.DateTimeFormat('ko-KR', {
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false,
// timeZone 미지정 → 브라우저 로컬 타임존 자동 사용 (글로벌 대응)
}).formatToParts(date);

const get = (type) => parts.find(p => p.type === type).value;
return `${get('month')}월 ${get('day')}일 ${get('hour')}:${get('minute')}`;
}

/**
* 이메일 형식 검증
* @param {string} email - 검증할 이메일
Expand All @@ -28,4 +52,37 @@ export function formatDate(dateString) {
export function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}

/**
* UTC 시간 문자열 → 로컬 HH:MM 변환
* 예: "00:00:00" (UTC) → "09:00" (서울 UTC+9)
* @param {string} utcTimeString - "HH:MM:SS" 형식의 UTC 시간 문자열
* @returns {string} 로컬 시간 "HH:MM" 문자열
*/
export function utcTimeStringToLocal(utcTimeString) {
const [h, m] = utcTimeString.split(':');
const today = new Date().toISOString().slice(0, 10);
const utcDate = new Date(`${today}T${h}:${m}:00Z`);
return utcDate.toLocaleTimeString('ko-KR', {
hour: '2-digit',
minute: '2-digit',
hour12: false
});
}

/**
* 로컬 HH:MM → UTC [hour, minute] 배열 변환
* 예: "09:00" (서울 UTC+9) → [0, 0] (UTC)
* @param {string} localTimeString - "HH:MM" 형식의 로컬 시간 문자열
* @returns {number[]} UTC 기준 [hour, minute] 배열
*/
export function localTimeStringToUtcArray(localTimeString) {
const [h, m] = localTimeString.split(':').map(Number);
const today = new Date().toISOString().slice(0, 10);
// 타임존 suffix 없는 형식은 로컬 시간으로 파싱됨 (ECMAScript 표준)
const localDate = new Date(
`${today}T${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:00`
);
return [localDate.getUTCHours(), localDate.getUTCMinutes()];
}