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.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 }
});
}
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()];
}