Skip to content

Commit ec02a03

Browse files
authored
다음 리뷰 일정 조회 기능 추가 (#23)
* feat: 다음 주기 표시 기능 추가 * style: 개행 누락 수정
1 parent a26273f commit ec02a03

7 files changed

Lines changed: 112 additions & 3 deletions

File tree

public/popup.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,17 @@ body {
167167
color: #666;
168168
}
169169

170+
/* 다음 리뷰 정보 */
171+
.next-review-info {
172+
margin-bottom: 12px;
173+
padding: 8px 12px;
174+
background-color: #eaf4fb;
175+
border-radius: 6px;
176+
font-size: 12px;
177+
color: #2980b9;
178+
text-align: center;
179+
}
180+
170181
/* 저장 결과 */
171182
.result {
172183
margin-top: 16px;

public/popup.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ <h1>Recycle Study</h1>
3838
<span id="user-email"></span>
3939
</div>
4040

41+
<div id="next-review-info" class="next-review-info hidden">
42+
<span id="next-review-text"></span>
43+
</div>
44+
4145
<!-- 복습 주기 선택 -->
4246
<div class="form-group">
4347
<label for="cycle-select">복습 주기</label>

src/api.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,16 @@ export async function updateNotificationTime(identifier, hour, minute) {
176176
body: { notificationTime: [hour, minute] }
177177
});
178178
}
179+
180+
/**
181+
* 다음 리뷰 정보 조회
182+
* @param {string} identifier - 디바이스 식별자
183+
* @returns {Promise<Object>} { scheduledAt, count }
184+
*/
185+
export async function getNextReview(identifier) {
186+
return await sendApiRequest({
187+
endpoint: '/api/v1/reviews/next',
188+
method: 'GET',
189+
headers: { 'X-Device-Id': identifier }
190+
});
191+
}

src/handlers/notification.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
showMessage,
1414
handleApiError
1515
} from '../ui/index.js';
16+
import { utcTimeStringToLocal, localTimeStringToUtcArray } from '../utils.js';
1617

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

3334
if (result.notificationTime) {
34-
elements.notificationTimeInput.value = result.notificationTime.substring(0, 5);
35+
elements.notificationTimeInput.value = utcTimeStringToLocal(result.notificationTime);
3536
} else {
3637
elements.notificationTimeInput.value = '';
3738
}
@@ -58,7 +59,7 @@ export async function handleUpdateNotificationTime() {
5859
try {
5960
showLoading();
6061
const storageData = await validateStorageForAuth();
61-
const [hour, minute] = timeValue.split(':').map(Number);
62+
const [hour, minute] = localTimeStringToUtcArray(timeValue);
6263
await updateNotificationTime(storageData.identifier, hour, minute);
6364
showMessage('알림 시간이 저장되었습니다.', 'success');
6465
} catch (error) {

src/popup.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
showMessage,
1313
hideCycleModal
1414
} from './ui/index.js';
15+
import { getNextReview } from './api.js';
16+
import { formatNextReviewDate } from './utils.js';
1517
import {
1618
handleRegister,
1719
handleCheckAuth,
@@ -98,6 +100,7 @@ async function initialize() {
98100
showView('main');
99101
// 주기 옵션 로드
100102
handleLoadCycleOptions();
103+
loadNextReview(storageData.identifier);
101104
} else if (storageData.email && storageData.identifier) {
102105
elements.emailDisplay.textContent = storageData.email;
103106
showView('pending');
@@ -111,6 +114,22 @@ async function initialize() {
111114
}
112115
}
113116

117+
/**
118+
* 다음 리뷰 정보 로드 및 표시
119+
*/
120+
async function loadNextReview(identifier) {
121+
try {
122+
const result = await getNextReview(identifier);
123+
if (result.scheduledAt && result.count > 0) {
124+
elements.nextReviewText.textContent =
125+
`다음 리뷰: ${formatNextReviewDate(result.scheduledAt)} / 대기 중 ${result.count}개`;
126+
elements.nextReviewInfo.classList.remove('hidden');
127+
}
128+
} catch (error) {
129+
console.debug('다음 리뷰 정보 조회 실패:', error);
130+
}
131+
}
132+
114133
/**
115134
* DOM 로드 후 실행
116135
*/

src/ui/elements.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export const elements = {
2424

2525
// 메인 화면
2626
userEmail: null,
27+
nextReviewInfo: null,
28+
nextReviewText: null,
2729
cycleSelect: null,
2830
saveUrlBtn: null,
2931
saveResult: null,
@@ -72,6 +74,8 @@ export function initializeElements() {
7274
elements.resetBtn = document.getElementById('reset-btn');
7375

7476
elements.userEmail = document.getElementById('user-email');
77+
elements.nextReviewInfo = document.getElementById('next-review-info');
78+
elements.nextReviewText = document.getElementById('next-review-text');
7579
elements.cycleSelect = document.getElementById('cycle-select');
7680
elements.saveUrlBtn = document.getElementById('save-url-btn');
7781
elements.saveResult = document.getElementById('save-result');

src/utils.js

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,30 @@ export function formatDate(dateString) {
2020
});
2121
}
2222

23+
/**
24+
* 다음 리뷰 날짜 포맷팅 (월/일/시:분)
25+
* @param {string} dateString - ISO 형식 날짜 문자열
26+
* @returns {string} 포맷된 날짜 문자열 (예: "3월 6일 09:00")
27+
*/
28+
export function formatNextReviewDate(dateString) {
29+
// 타임존 정보가 없으면 UTC로 가정 (서버 버그 방어)
30+
const hasTimezone = /Z$|[+-]\d{2}:\d{2}$/.test(dateString);
31+
const normalized = hasTimezone ? dateString : dateString + 'Z';
32+
33+
const date = new Date(normalized);
34+
const parts = new Intl.DateTimeFormat('ko-KR', {
35+
month: 'numeric',
36+
day: 'numeric',
37+
hour: '2-digit',
38+
minute: '2-digit',
39+
hour12: false,
40+
// timeZone 미지정 → 브라우저 로컬 타임존 자동 사용 (글로벌 대응)
41+
}).formatToParts(date);
42+
43+
const get = (type) => parts.find(p => p.type === type).value;
44+
return `${get('month')}${get('day')}${get('hour')}:${get('minute')}`;
45+
}
46+
2347
/**
2448
* 이메일 형식 검증
2549
* @param {string} email - 검증할 이메일
@@ -28,4 +52,37 @@ export function formatDate(dateString) {
2852
export function isValidEmail(email) {
2953
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3054
return emailRegex.test(email);
31-
}
55+
}
56+
57+
/**
58+
* UTC 시간 문자열 → 로컬 HH:MM 변환
59+
* 예: "00:00:00" (UTC) → "09:00" (서울 UTC+9)
60+
* @param {string} utcTimeString - "HH:MM:SS" 형식의 UTC 시간 문자열
61+
* @returns {string} 로컬 시간 "HH:MM" 문자열
62+
*/
63+
export function utcTimeStringToLocal(utcTimeString) {
64+
const [h, m] = utcTimeString.split(':');
65+
const today = new Date().toISOString().slice(0, 10);
66+
const utcDate = new Date(`${today}T${h}:${m}:00Z`);
67+
return utcDate.toLocaleTimeString('ko-KR', {
68+
hour: '2-digit',
69+
minute: '2-digit',
70+
hour12: false
71+
});
72+
}
73+
74+
/**
75+
* 로컬 HH:MM → UTC [hour, minute] 배열 변환
76+
* 예: "09:00" (서울 UTC+9) → [0, 0] (UTC)
77+
* @param {string} localTimeString - "HH:MM" 형식의 로컬 시간 문자열
78+
* @returns {number[]} UTC 기준 [hour, minute] 배열
79+
*/
80+
export function localTimeStringToUtcArray(localTimeString) {
81+
const [h, m] = localTimeString.split(':').map(Number);
82+
const today = new Date().toISOString().slice(0, 10);
83+
// 타임존 suffix 없는 형식은 로컬 시간으로 파싱됨 (ECMAScript 표준)
84+
const localDate = new Date(
85+
`${today}T${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:00`
86+
);
87+
return [localDate.getUTCHours(), localDate.getUTCMinutes()];
88+
}

0 commit comments

Comments
 (0)