From 57e41f47e2ac7244372fcf66be4fa98377258ccb Mon Sep 17 00:00:00 2001 From: hannah0352 Date: Thu, 2 Oct 2025 17:25:23 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=97=90=20interval=20=EA=B3=84=EC=82=B0=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search-result/AlarmCountdown.tsx | 241 ++++++++++++++++-- src/components/search-result/AlarmModal.tsx | 39 ++- .../search-result/ServerTimeResult.tsx | 32 ++- 3 files changed, 267 insertions(+), 45 deletions(-) diff --git a/src/components/search-result/AlarmCountdown.tsx b/src/components/search-result/AlarmCountdown.tsx index 9e3719d..83cbb29 100644 --- a/src/components/search-result/AlarmCountdown.tsx +++ b/src/components/search-result/AlarmCountdown.tsx @@ -1,19 +1,106 @@ // 알림 대기 상태 표시 (타이머) 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { AlarmData } from './AlarmModal'; +interface AlertSetting { + type: string; + time: number; + message: string; + priority?: string; +} + +interface IntervalCalculationResult { + success: boolean; + data: { + optimalRefreshTime: string; + refreshInterval: number; + alertSettings: AlertSetting[]; + confidence: number; + networkAnalysis: { + condition: string; + averageRTT: number; + }; + }; +} + interface AlarmCountdownProps { alarm: AlarmData; onComplete?: () => void; + finalUrl?: string; // 검색 결과의 최종 URL } export default function AlarmCountdown({ alarm, onComplete, + finalUrl, }: AlarmCountdownProps) { const [remainingSeconds, setRemainingSeconds] = useState(null); + const [intervalResult, setIntervalResult] = useState(null); + const [isCalculating, setIsCalculating] = useState(false); + const [alertMessages, setAlertMessages] = useState([]); + const [hasCalculated, setHasCalculated] = useState(false); + const [showCountdown, setShowCountdown] = useState(true); + + // Interval 계산 API 호출 + const calculateInterval = async (targetUrl: string, targetTime: string, userAlertOffsets: number[]) => { + try { + setIsCalculating(true); + const response = await fetch('http://localhost:3001/api/interval/calculate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + targetUrl, + targetTime, + userAlertOffsets: userAlertOffsets.length > 0 ? userAlertOffsets : undefined, + }), + }); + + const result = await response.json(); + if (result.success) { + setIntervalResult(result); + return result; + } else { + throw new Error(result.error || 'Interval 계산 실패'); + } + } catch (error) { + console.error('Interval 계산 오류:', error); + return null; + } finally { + setIsCalculating(false); + } + }; + + // 알림 메시지 체크 + const checkAlertMessages = useCallback(() => { + if (intervalResult?.data?.optimalRefreshTime) { + // optimalRefreshTime을 기준으로 정확한 시점 계산 + const optimalTime = new Date(intervalResult.data.optimalRefreshTime); + const now = new Date(); + const timeUntilOptimal = Math.floor((optimalTime.getTime() - now.getTime()) / 1000); + + // optimalRefreshTime 시점에 도달하면 "지금 새로고침하세요!" 표시하고 카운트다운 숨김 + if (timeUntilOptimal <= 0 && timeUntilOptimal >= -1) { + console.log('🔔 optimalRefreshTime 도달: 지금 새로고침하세요!'); + + setAlertMessages(['지금 새로고침하세요!']); + setShowCountdown(false); // 카운트다운 숨김 + + // 브라우저 알림 (사용자 허용 시) + if (alarm.options.sound && 'Notification' in window) { + if (Notification.permission === 'granted') { + new Notification('CheckTime 알림', { + body: '지금 새로고침하세요!', + icon: '/favicon.ico' + }); + } + } + } + } + }, [intervalResult, alarm.options.sound]); // 남은 시간을 HH:MM:SS로 포맷팅 const formatTime = (totalSeconds: number) => { @@ -27,42 +114,140 @@ export default function AlarmCountdown({ }; useEffect(() => { - const now = new Date(); - const target = new Date(); - - target.setHours(parseInt(alarm.time.hour)); - target.setMinutes(parseInt(alarm.time.minute)); - target.setSeconds(parseInt(alarm.time.second)); - target.setMilliseconds(0); + const initializeCountdown = async () => { + const now = new Date(); + const target = new Date(); - let seconds = Math.floor((target.getTime() - now.getTime()) / 1000); - if (seconds < 0) seconds = 0; - setRemainingSeconds(seconds); + target.setHours(parseInt(alarm.time.hour)); + target.setMinutes(parseInt(alarm.time.minute)); + target.setSeconds(parseInt(alarm.time.second)); + target.setMilliseconds(0); - const interval = setInterval(() => { - seconds -= 1; + let seconds = Math.floor((target.getTime() - now.getTime()) / 1000); + if (seconds < 0) seconds = 0; setRemainingSeconds(seconds); - if (seconds <= 0) { - clearInterval(interval); - setRemainingSeconds(0); - onComplete?.(); + + // 디버깅: 시간 계산 확인 + console.log('🕐 시간 계산:', { + now: now.toISOString(), + target: target.toISOString(), + seconds: seconds, + hours: Math.floor(seconds / 3600), + minutes: Math.floor((seconds % 3600) / 60) + }); + + // Interval 계산 사용 시 API 호출 (한 번만) + if (alarm.options.useIntervalCalculation && finalUrl && !hasCalculated) { + setHasCalculated(true); + const result = await calculateInterval( + finalUrl, + target.toISOString(), + alarm.options.customAlertOffsets + ); + + if (result?.success) { + // 디버깅: Interval 계산 결과 확인 + console.log('🎯 Interval 계산 결과:', { + optimalRefreshTime: result.data.optimalRefreshTime, + refreshInterval: result.data.refreshInterval, + alertSettings: result.data.alertSettings + }); + + // Interval 계산 결과에 따른 알림 스케줄링 + scheduleIntervalAlerts(); + } + } else if (!alarm.options.useIntervalCalculation) { + // 기본 알림 스케줄링 + scheduleDefaultAlerts(alarm.options.preAlerts, seconds); } - }, 1000); - return () => clearInterval(interval); - }, [alarm, onComplete]); + const interval = setInterval(() => { + seconds -= 1; + setRemainingSeconds(seconds); + + // 알림 메시지 체크 + checkAlertMessages(); + + // 카운트다운은 항상 목표 시간까지 계속 진행 + if (seconds <= 0) { + clearInterval(interval); + setRemainingSeconds(0); + onComplete?.(); + } + }, 1000); + + return () => clearInterval(interval); + }; + + initializeCountdown(); + }, [alarm, onComplete, finalUrl, checkAlertMessages, hasCalculated]); + + // Interval 계산 결과에 따른 알림 스케줄링 + const scheduleIntervalAlerts = () => { + // 알림을 즉시 표시하지 않고, 빈 배열로 초기화 + setAlertMessages([]); + console.log('🎯 Interval 알림 스케줄링 준비 완료'); + }; + + // 기본 알림 스케줄링 + const scheduleDefaultAlerts = (preAlerts: number[], totalSeconds: number) => { + const alerts: string[] = []; + + preAlerts.forEach((alertSeconds) => { + if (alertSeconds <= totalSeconds) { + alerts.push(`${alertSeconds}초 전 알림`); + } + }); + + setAlertMessages(alerts); + }; return (
-
+
+ {/* Interval 계산 상태 */} + {isCalculating && ( +
+
+
+ 네트워크 분석 중... +
+
+ )} + {/* 카운트다운 타이머 */} -
- {remainingSeconds !== null - ? remainingSeconds > 0 - ? formatTime(remainingSeconds) - : '⏰ 알림 시간입니다!' - : '대기 중...'} -
+ {showCountdown && ( +
+ {remainingSeconds !== null + ? remainingSeconds > 0 + ? formatTime(remainingSeconds) + : alarm.options.useIntervalCalculation + ? '' // Interval 옵션 사용자는 목표 시간에 도달해도 메시지 표시 안함 + : '⏰ 알림 시간입니다!' + : '대기 중...'} +
+ )} + + {/* 알림 메시지 */} + {alertMessages.length > 0 && ( +
+ {alertMessages.map((message, index) => ( +
+ {message} +
+ ))} +
+ )} + + {/* Interval 계산 사용 시 추가 정보 */} + {alarm.options.useIntervalCalculation && intervalResult && ( +
+
최적 새로고침: {(intervalResult.data.refreshInterval / 1000).toFixed(1)}초 전
+
+ )}
); diff --git a/src/components/search-result/AlarmModal.tsx b/src/components/search-result/AlarmModal.tsx index 0a48c58..8029080 100644 --- a/src/components/search-result/AlarmModal.tsx +++ b/src/components/search-result/AlarmModal.tsx @@ -13,6 +13,9 @@ export interface AlarmOptions { preAlerts: number[]; // [60, 30, 10] sound: boolean; red: boolean; + useIntervalCalculation: boolean; // interval 계산 사용 여부 + targetUrl: string; // interval 계산용 URL + customAlertOffsets: number[]; // 사용자 정의 알림 오프셋 } export interface AlarmData { @@ -24,6 +27,7 @@ export interface AlarmData { interface AlarmModalProps { onConfirm: (data: AlarmData) => void; onClose: () => void; + finalUrl?: string; // 검색 결과의 최종 URL } // 시간 옵션 생성 @@ -91,7 +95,7 @@ function Checkbox({ ); } -export default function AlarmModal({ onConfirm, onClose }: AlarmModalProps) { +export default function AlarmModal({ onConfirm, onClose, finalUrl }: AlarmModalProps) { const [hour, setHour] = useState('00'); const [minute, setMinute] = useState('00'); const [second, setSecond] = useState('00'); @@ -100,6 +104,9 @@ export default function AlarmModal({ onConfirm, onClose }: AlarmModalProps) { preAlerts: [], sound: true, red: false, + useIntervalCalculation: false, + targetUrl: '', + customAlertOffsets: [], }); const togglePreAlert = (secondsBefore: number) => { @@ -111,10 +118,12 @@ export default function AlarmModal({ onConfirm, onClose }: AlarmModalProps) { })); }; - const handleToggle = (key: 'sound' | 'red') => { + const handleToggle = (key: 'sound' | 'red' | 'useIntervalCalculation') => { setOptions((prev) => ({ ...prev, [key]: !prev[key] })); }; + + const handleSubmit = () => { const now = new Date(); const targetTime = new Date(); @@ -131,6 +140,12 @@ export default function AlarmModal({ onConfirm, onClose }: AlarmModalProps) { return; } + // Interval 계산 사용 시 finalUrl 검증 + if (options.useIntervalCalculation && !finalUrl) { + alert('❗ Interval 계산을 사용하려면 검색 결과 URL이 필요합니다.'); + return; + } + onConfirm({ time: { hour, minute, second }, options, @@ -207,7 +222,7 @@ export default function AlarmModal({ onConfirm, onClose }: AlarmModalProps) {
{/* 사전 알림 설정 */} -
+
@@ -253,6 +268,24 @@ export default function AlarmModal({ onConfirm, onClose }: AlarmModalProps) {
+ {/* 고급 설정 - Interval 계산 */} +
+ +
+ handleToggle('useIntervalCalculation')} + label={ +
+ Interval 계산 +
+ } + /> +
+
+ {/* 버튼 */}
)} - {/* 상세 정보 버튼 */} -
- -
- {/* 알람 카운트다운 컴포넌트 */} {alarmData && (
@@ -314,7 +307,7 @@ export default function ServerTimeResult({ hour: '2-digit', minute: '2-digit', second: '2-digit' - })} | 한국 표준시간 + })}
@@ -325,11 +318,22 @@ export default function ServerTimeResult({ 사용 안 함 - + )} + {/* 상세 정보 버튼 */} +
+ +
+ {/* 상세 정보 모달 */} {showDetailModal && (
Date: Thu, 2 Oct 2025 18:00:47 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EA=B8=B0=EB=B3=B8=20=EB=AA=A8?= =?UTF-8?q?=EB=93=9C,=20=EC=82=AC=EC=A0=84=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=AA=A8=EB=93=9C,=20Interval=20=EB=AA=A8=EB=93=9C=EB=B3=84?= =?UTF-8?q?=EB=A1=9C=20=EC=84=A4=EC=A0=95=EB=90=9C=20=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=EC=97=90=20=EB=AC=B8=EA=B5=AC=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search-result/AlarmCountdown.tsx | 65 +++++++++++++++---- src/components/search-result/AlarmModal.tsx | 4 +- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/src/components/search-result/AlarmCountdown.tsx b/src/components/search-result/AlarmCountdown.tsx index 83cbb29..88fb2be 100644 --- a/src/components/search-result/AlarmCountdown.tsx +++ b/src/components/search-result/AlarmCountdown.tsx @@ -42,6 +42,8 @@ export default function AlarmCountdown({ const [alertMessages, setAlertMessages] = useState([]); const [hasCalculated, setHasCalculated] = useState(false); const [showCountdown, setShowCountdown] = useState(true); + const [showAlertTime, setShowAlertTime] = useState(false); + const [showRefreshMessage, setShowRefreshMessage] = useState(false); // Interval 계산 API 호출 const calculateInterval = async (targetUrl: string, targetTime: string, userAlertOffsets: number[]) => { @@ -86,7 +88,7 @@ export default function AlarmCountdown({ if (timeUntilOptimal <= 0 && timeUntilOptimal >= -1) { console.log('🔔 optimalRefreshTime 도달: 지금 새로고침하세요!'); - setAlertMessages(['지금 새로고침하세요!']); + setShowRefreshMessage(true); // "지금 새로고침하세요!" 표시 setShowCountdown(false); // 카운트다운 숨김 // 브라우저 알림 (사용자 허용 시) @@ -158,7 +160,7 @@ export default function AlarmCountdown({ } } else if (!alarm.options.useIntervalCalculation) { // 기본 알림 스케줄링 - scheduleDefaultAlerts(alarm.options.preAlerts, seconds); + scheduleDefaultAlerts(alarm.options.preAlerts); } const interval = setInterval(() => { @@ -168,11 +170,31 @@ export default function AlarmCountdown({ // 알림 메시지 체크 checkAlertMessages(); + // 기본 알림 모드: 사전 알림 시간에 도달했을 때 체크 + if (!alarm.options.useIntervalCalculation && alarm.options.preAlerts.length > 0) { + alarm.options.preAlerts.forEach((alertSeconds) => { + if (seconds === alertSeconds) { + console.log(`🔔 ${alertSeconds}초 전 알림 도달`); + setShowCountdown(false); // 카운트다운 숨김 + setShowAlertTime(true); // 알림 시간 메시지 표시 + setRemainingSeconds(0); + // onComplete 호출하지 않고 여기서 멈춤 + } + }); + } + // 카운트다운은 항상 목표 시간까지 계속 진행 if (seconds <= 0) { clearInterval(interval); setRemainingSeconds(0); - onComplete?.(); + + // 기본 알림 모드에서 사전 알림이 없을 때도 "알림 시간입니다!" 표시 + if (!alarm.options.useIntervalCalculation) { + setShowCountdown(false); + setShowAlertTime(true); + } else { + onComplete?.(); + } } }, 1000); @@ -190,16 +212,24 @@ export default function AlarmCountdown({ }; // 기본 알림 스케줄링 - const scheduleDefaultAlerts = (preAlerts: number[], totalSeconds: number) => { + const scheduleDefaultAlerts = (preAlerts: number[]) => { + // 기본 알림 메시지 생성 const alerts: string[] = []; preAlerts.forEach((alertSeconds) => { - if (alertSeconds <= totalSeconds) { + if (alertSeconds === 60) { + alerts.push('1분 전 알림'); + } else if (alertSeconds === 30) { + alerts.push('30초 전 알림'); + } else if (alertSeconds === 10) { + alerts.push('10초 전 알림'); + } else { alerts.push(`${alertSeconds}초 전 알림`); } }); setAlertMessages(alerts); + console.log('🎯 기본 알림 스케줄링:', alerts); }; return ( @@ -217,25 +247,36 @@ export default function AlarmCountdown({ {/* 카운트다운 타이머 */} {showCountdown && ( -
+
{remainingSeconds !== null ? remainingSeconds > 0 ? formatTime(remainingSeconds) : alarm.options.useIntervalCalculation ? '' // Interval 옵션 사용자는 목표 시간에 도달해도 메시지 표시 안함 - : '⏰ 알림 시간입니다!' + : '' : '대기 중...'}
)} + {/* 기본 알림 모드: 사전 알림 시간 도달 시 메시지 표시 */} + {!alarm.options.useIntervalCalculation && showAlertTime && ( +
+ 알림 시간입니다! +
+ )} + + {/* Interval 모드: optimalRefreshTime 도달 시 메시지 표시 */} + {alarm.options.useIntervalCalculation && showRefreshMessage && ( +
+ 지금 새로고침하세요! +
+ )} + {/* 알림 메시지 */} {alertMessages.length > 0 && ( -
+
{alertMessages.map((message, index) => ( -
+
{message}
))} diff --git a/src/components/search-result/AlarmModal.tsx b/src/components/search-result/AlarmModal.tsx index 8029080..687a105 100644 --- a/src/components/search-result/AlarmModal.tsx +++ b/src/components/search-result/AlarmModal.tsx @@ -102,7 +102,7 @@ export default function AlarmModal({ onConfirm, onClose, finalUrl }: AlarmModalP const [options, setOptions] = useState({ preAlerts: [], - sound: true, + sound: false, red: false, useIntervalCalculation: false, targetUrl: '', @@ -180,7 +180,7 @@ export default function AlarmModal({ onConfirm, onClose, finalUrl }: AlarmModalP {/* 시간 설정 */}