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
327 changes: 299 additions & 28 deletions src/components/search-result/AlarmCountdown.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,151 @@
// ์•Œ๋ฆผ ๋Œ€๊ธฐ ์ƒํƒœ ํ‘œ์‹œ (ํƒ€์ด๋จธ)
'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<number | null>(null);
const [intervalResult, setIntervalResult] = useState<IntervalCalculationResult | null>(null);
const [isCalculating, setIsCalculating] = useState(false);
const [alertMessages, setAlertMessages] = useState<string[]>([]);
const [hasCalculated, setHasCalculated] = useState(false);
const [showCountdown, setShowCountdown] = useState(true);
const [showAlertTime, setShowAlertTime] = useState(false);
const [showRefreshMessage, setShowRefreshMessage] = useState(false);
const [hasPlayedSound, setHasPlayedSound] = useState(false);

// ๋นจ๊ฐ„์ƒ‰ ๋ฐฐ๊ฒฝ ํšจ๊ณผ (์ „์ฒด ํ™”๋ฉด)
useEffect(() => {
if (alarm.options.red && (showAlertTime || showRefreshMessage)) {
// ์ „์ฒด ํ™”๋ฉด์„ ๋นจ๊ฐ„์ƒ‰์œผ๋กœ ๋ณ€๊ฒฝ
document.body.style.backgroundColor = '#ef4444'; // bg-red-500
document.body.style.transition = 'background-color 0.3s ease';
} else {
// ์›๋ž˜ ๋ฐฐ๊ฒฝ์ƒ‰์œผ๋กœ ๋ณต์›
document.body.style.backgroundColor = '';
document.body.style.transition = 'background-color 0.3s ease';
}

// ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ ์›๋ž˜ ๋ฐฐ๊ฒฝ์ƒ‰์œผ๋กœ ๋ณต์›
return () => {
document.body.style.backgroundColor = '';
document.body.style.transition = '';
};
}, [alarm.options.red, showAlertTime, showRefreshMessage]);

// ์†Œ๋ฆฌ ์žฌ์ƒ ํ•จ์ˆ˜ (5์ดˆ๊ฐ„ ์‚ก ์†Œ๋ฆฌ)
const playAlarmSound = () => {
if (!alarm.options.sound) return;

// AudioContext๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‚ก ์†Œ๋ฆฌ ์ƒ์„ฑ
const AudioContextClass = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
const audioContext = new AudioContextClass();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();

oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);

oscillator.frequency.setValueAtTime(800, audioContext.currentTime); // 800Hz ์‚ก ์†Œ๋ฆฌ
oscillator.type = 'sine';

gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 5); // 5์ดˆ๊ฐ„ ๊ฐ์†Œ

oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 5);
};

Comment on lines +68 to +90
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major

Web Audio ์ปจํ…์ŠคํŠธ ์ •๋ฆฌ ๋ˆ„๋ฝ/์žฌ์ƒ ์ •์ฑ… ๋Œ€์‘ ๋ถ€์กฑ

AudioContext๋ฅผ ๋งค๋ฒˆ ์ƒ์„ฑ ํ›„ ๋‹ซ์ง€ ์•Š์•„ ๋ˆ„์ˆ˜ ์œ„ํ—˜. ๋˜ํ•œ iOS/Safari๋Š” ์‚ฌ์šฉ์ž ์ œ์Šค์ฒ˜ ํ›„ resume ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. onended์—์„œ closeํ•˜๊ณ , ํ•„์š” ์‹œ resume ํ•˜์„ธ์š”.

-  const playAlarmSound = () => {
+  const playAlarmSound = async () => {
     if (!alarm.options.sound) return;
     
     // AudioContext๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‚ก ์†Œ๋ฆฌ ์ƒ์„ฑ
-    const AudioContextClass = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
-    const audioContext = new AudioContextClass();
+    const AudioContextClass =
+      (window as any).AudioContext || (window as any).webkitAudioContext;
+    const audioContext = new AudioContextClass();
+    if (audioContext.state === 'suspended') {
+      try { await audioContext.resume(); } catch {}
+    }
     const oscillator = audioContext.createOscillator();
     const gainNode = audioContext.createGain();
@@
-    oscillator.start(audioContext.currentTime);
-    oscillator.stop(audioContext.currentTime + 5);
+    oscillator.onended = () => {
+      audioContext.close().catch(() => {});
+    };
+    oscillator.start();
+    oscillator.stop(audioContext.currentTime + 5);
   };

useEffect์—์„œ Promise๋Š” ๋Œ€๊ธฐํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค.

Also applies to: 91-99

// ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ์‹œ ์†Œ๋ฆฌ ์žฌ์ƒ
useEffect(() => {
if ((showAlertTime || showRefreshMessage) && !hasPlayedSound) {
playAlarmSound();
setHasPlayedSound(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showAlertTime, showRefreshMessage, hasPlayedSound]);

// 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);
}
};
Comment on lines +100 to +129
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major

API ํ˜ธ์ถœ: ํ•˜๋“œ์ฝ”๋”ฉ๋œ localhost, ์‘๋‹ต ์ฝ”๋“œ ๋ฏธ๊ฒ€์ฆ, ํƒ€์ž„์•„์›ƒ ์—†์Œ

ํ”„๋กœ๋•์…˜์—์„œ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค. ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ธฐ๋ฐ˜ ๋ฒ ์ด์Šค URL, response.ok ๊ฒ€์‚ฌ, ์š”์ฒญ ํƒ€์ž„์•„์›ƒ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

์˜ˆ์‹œ ์ˆ˜์ •:

-      const response = await fetch('http://localhost:3001/api/interval/calculate', {
+      const base = process.env.NEXT_PUBLIC_API_BASE_URL ?? '';
+      const controller = new AbortController();
+      const timeoutId = setTimeout(() => controller.abort(), 5000); // 5s timeout
+      const response = await fetch(`${base}/api/interval/calculate`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
         },
         body: JSON.stringify({
           targetUrl,
           targetTime,
           userAlertOffsets: userAlertOffsets.length > 0 ? userAlertOffsets : undefined,
-        }),
+        }),
+        signal: controller.signal,
       });
-
-      const result = await response.json();
+      if (!response.ok) {
+        throw new Error(`HTTP ${response.status}`);
+      }
+      const result = await response.json();
@@
-    } finally {
-      setIsCalculating(false);
-    }
+    } finally {
+      setIsCalculating(false);
+      if (typeof timeoutId !== 'undefined') clearTimeout(timeoutId);
+    }

ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ธํŒ…(NEXT_PUBLIC_API_BASE_URL)๋„ ํ•จ๊ป˜ ๋ฐ˜์˜ํ•ด ์ฃผ์„ธ์š”.


// ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ ์ฒดํฌ
const checkAlertMessages = useCallback(() => {
if (intervalResult?.data?.optimalRefreshTime && !hasPlayedSound && !showRefreshMessage) {
// 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 ๋„๋‹ฌ: ์ง€๊ธˆ ์ƒˆ๋กœ๊ณ ์นจํ•˜์„ธ์š”!');

setShowRefreshMessage(true); // "์ง€๊ธˆ ์ƒˆ๋กœ๊ณ ์นจํ•˜์„ธ์š”!" ํ‘œ์‹œ
setShowCountdown(false); // ์นด์šดํŠธ๋‹ค์šด ์ˆจ๊น€
// ์†Œ๋ฆฌ๋Š” useEffect์—์„œ ์žฌ์ƒ
}
}
}, [intervalResult, hasPlayedSound, showRefreshMessage]);

// ๋‚จ์€ ์‹œ๊ฐ„์„ HH:MM:SS๋กœ ํฌ๋งทํŒ…
const formatTime = (totalSeconds: number) => {
Expand All @@ -27,42 +159,181 @@ export default function AlarmCountdown({
};

useEffect(() => {
const now = new Date();
const target = new Date();
const initializeCountdown = async () => {
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);
target.setHours(parseInt(alarm.time.hour));
target.setMinutes(parseInt(alarm.time.minute));
target.setSeconds(parseInt(alarm.time.second));
target.setMilliseconds(0);

let seconds = Math.floor((target.getTime() - now.getTime()) / 1000);
if (seconds < 0) seconds = 0;
setRemainingSeconds(seconds);

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);
}
}, 1000);

return () => clearInterval(interval);
}, [alarm, onComplete]);
const interval = setInterval(() => {
seconds -= 1;
setRemainingSeconds(seconds);

// ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ ์ฒดํฌ
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);

// ๊ธฐ๋ณธ ์•Œ๋ฆผ ๋ชจ๋“œ์—์„œ ์‚ฌ์ „ ์•Œ๋ฆผ์ด ์—†์„ ๋•Œ๋„ "์•Œ๋ฆผ ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค!" ํ‘œ์‹œ
if (!alarm.options.useIntervalCalculation) {
setShowCountdown(false);
setShowAlertTime(true);
// ์†Œ๋ฆฌ๋Š” useEffect์—์„œ ์žฌ์ƒ
} else {
onComplete?.();
}
}
}, 1000);

return () => clearInterval(interval);
};

initializeCountdown();
}, [alarm, onComplete, finalUrl, checkAlertMessages, hasCalculated, playAlarmSound]);

Comment on lines 161 to +251
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐Ÿ› ๏ธ Refactor suggestion | ๐ŸŸ  Major

useEffect ๋น„๋™๊ธฐ ์ดˆ๊ธฐํ™”๋กœ interval ์ •๋ฆฌ ๋ˆ„๋ฝ๋จ โ€” ์ค‘๋ณต ํƒ€์ด๋จธ/๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ์œ„ํ—˜

async ํ•จ์ˆ˜ ๋‚ด๋ถ€์—์„œ ๋ฐ˜ํ™˜ํ•œ cleanup์€ useEffect์— ์ „๋‹ฌ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ์˜์กด์„ฑ ๋ณ€ํ™” ๋•Œ๋งˆ๋‹ค ํƒ€์ด๋จธ๊ฐ€ ์ค‘์ฒฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์™ธ๋ถ€์—์„œ intervalId๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ , useEffect ์ž์ฒด์—์„œ cleanup์„ ๋ฐ˜ํ™˜ํ•˜์„ธ์š”. ๋˜ํ•œ ๋ถˆํ•„์š”ํ•œ ์˜์กด์„ฑ(playAlarmSound, hasCalculated) ์ œ๊ฑฐ๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

์ ์šฉ ์˜ˆ์‹œ:

-  useEffect(() => {
-    const initializeCountdown = async () => {
+  useEffect(() => {
+    let intervalId: ReturnType<typeof setInterval> | null = null;
+    const initializeCountdown = async () => {
       const now = new Date();
       const target = new Date();
@@
-      const interval = setInterval(() => {
+      intervalId = setInterval(() => {
         seconds -= 1;
         setRemainingSeconds(seconds);
@@
-      }, 1000);
-
-      return () => clearInterval(interval);
-    };
-
-    initializeCountdown();
-  }, [alarm, onComplete, finalUrl, checkAlertMessages, hasCalculated, playAlarmSound]);
+      }, 1000);
+    };
+    initializeCountdown();
+    return () => {
+      if (intervalId) clearInterval(intervalId);
+    };
+  }, [alarm, onComplete, finalUrl, checkAlertMessages]);

์ถ”๊ฐ€๋กœ, ์ค‘๋ณต API ํ˜ธ์ถœ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด hasCalculated๋Š” useRef๋กœ ๋Œ€์ฒดํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. ํ•„์š” ์‹œ ์ฝ”๋“œ ์ œ๊ณต ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“ Committable suggestion

โ€ผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const now = new Date();
const target = new Date();
const initializeCountdown = async () => {
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);
target.setHours(parseInt(alarm.time.hour));
target.setMinutes(parseInt(alarm.time.minute));
target.setSeconds(parseInt(alarm.time.second));
target.setMilliseconds(0);
let seconds = Math.floor((target.getTime() - now.getTime()) / 1000);
if (seconds < 0) seconds = 0;
setRemainingSeconds(seconds);
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);
}
}, 1000);
return () => clearInterval(interval);
}, [alarm, onComplete]);
const interval = setInterval(() => {
seconds -= 1;
setRemainingSeconds(seconds);
// ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ ์ฒดํฌ
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);
// ๊ธฐ๋ณธ ์•Œ๋ฆผ ๋ชจ๋“œ์—์„œ ์‚ฌ์ „ ์•Œ๋ฆผ์ด ์—†์„ ๋•Œ๋„ "์•Œ๋ฆผ ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค!" ํ‘œ์‹œ
if (!alarm.options.useIntervalCalculation) {
setShowCountdown(false);
setShowAlertTime(true);
// ์†Œ๋ฆฌ๋Š” useEffect์—์„œ ์žฌ์ƒ
} else {
onComplete?.();
}
}
}, 1000);
return () => clearInterval(interval);
};
initializeCountdown();
}, [alarm, onComplete, finalUrl, checkAlertMessages, hasCalculated, playAlarmSound]);
useEffect(() => {
let intervalId: ReturnType<typeof setInterval> | null = null;
const initializeCountdown = async () => {
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);
let seconds = Math.floor((target.getTime() - now.getTime()) / 1000);
if (seconds < 0) seconds = 0;
setRemainingSeconds(seconds);
// ๋””๋ฒ„๊น…: ์‹œ๊ฐ„ ๊ณ„์‚ฐ ํ™•์ธ
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);
}
intervalId = setInterval(() => {
seconds -= 1;
setRemainingSeconds(seconds);
// ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ ์ฒดํฌ
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(intervalId!);
setRemainingSeconds(0);
// ๊ธฐ๋ณธ ์•Œ๋ฆผ ๋ชจ๋“œ์—์„œ ์‚ฌ์ „ ์•Œ๋ฆผ์ด ์—†์„ ๋•Œ๋„ "์•Œ๋ฆผ ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค!" ํ‘œ์‹œ
if (!alarm.options.useIntervalCalculation) {
setShowCountdown(false);
setShowAlertTime(true);
// ์†Œ๋ฆฌ๋Š” useEffect์—์„œ ์žฌ์ƒ
} else {
onComplete?.();
}
}
}, 1000);
};
initializeCountdown();
return () => {
if (intervalId) clearInterval(intervalId);
};
}, [alarm, onComplete, finalUrl, checkAlertMessages]);
๐Ÿค– Prompt for AI Agents
In src/components/search-result/AlarmCountdown.tsx around lines 161-251, the
useEffect creates an async initializer that sets an interval but returns its
cleanup from inside the async function (so React never receives the cleanup),
causing duplicated timers and potential leaks; refactor so the useEffect itself
is synchronous, declare the intervalId in the outer scope (let intervalId:
ReturnType<typeof setInterval> | null = null), start the async work via an inner
async function but ensure the effect returns a cleanup function that clears
intervalId, use a mounted/aborted flag (or AbortController) to cancel/ignore
async results, replace hasCalculated state with a useRef to prevent re-renders
and duplicate API calls, and remove unnecessary dependencies (playAlarmSound and
hasCalculated) from the dependency array so the interval is cleaned and
recreated correctly on relevant prop changes.

// Interval ๊ณ„์‚ฐ ๊ฒฐ๊ณผ์— ๋”ฐ๋ฅธ ์•Œ๋ฆผ ์Šค์ผ€์ค„๋ง
const scheduleIntervalAlerts = () => {
// ์•Œ๋ฆผ์„ ์ฆ‰์‹œ ํ‘œ์‹œํ•˜์ง€ ์•Š๊ณ , ๋นˆ ๋ฐฐ์—ด๋กœ ์ดˆ๊ธฐํ™”
setAlertMessages([]);
console.log('๐ŸŽฏ Interval ์•Œ๋ฆผ ์Šค์ผ€์ค„๋ง ์ค€๋น„ ์™„๋ฃŒ');
};

// ๊ธฐ๋ณธ ์•Œ๋ฆผ ์Šค์ผ€์ค„๋ง
const scheduleDefaultAlerts = (preAlerts: number[]) => {
// ๊ธฐ๋ณธ ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ
const alerts: string[] = [];

preAlerts.forEach((alertSeconds) => {
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 (
<div className="my-8 text-center">
<div className="max-w-lg rounded-2xl p-6 mx-auto">
<div className="max-w-lg rounded-2xl mx-auto">
{/* Interval ๊ณ„์‚ฐ ์ƒํƒœ */}
{isCalculating && (
<div className="mb-4 p-3 rounded-lg">
<div className="flex items-center justify-center gap-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
<span className="text-sm">๋„คํŠธ์›Œํฌ ๋ถ„์„ ์ค‘...</span>
</div>
</div>
)}

{/* ์นด์šดํŠธ๋‹ค์šด ํƒ€์ด๋จธ */}
<div className="text-4xl font-bold tracking-widest">
{remainingSeconds !== null
? remainingSeconds > 0
? formatTime(remainingSeconds)
: 'โฐ ์•Œ๋ฆผ ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค!'
: '๋Œ€๊ธฐ ์ค‘...'}
</div>
{showCountdown && (
<div className="text-4xl font-bold tracking-widest text-black">
{remainingSeconds !== null
? remainingSeconds > 0
? formatTime(remainingSeconds)
: alarm.options.useIntervalCalculation
? '' // Interval ์˜ต์…˜ ์‚ฌ์šฉ์ž๋Š” ๋ชฉํ‘œ ์‹œ๊ฐ„์— ๋„๋‹ฌํ•ด๋„ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ์•ˆํ•จ
: ''
: '๋Œ€๊ธฐ ์ค‘...'}
</div>
)}

{/* ๊ธฐ๋ณธ ์•Œ๋ฆผ ๋ชจ๋“œ: ์‚ฌ์ „ ์•Œ๋ฆผ ์‹œ๊ฐ„ ๋„๋‹ฌ ์‹œ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ */}
{!alarm.options.useIntervalCalculation && showAlertTime && (
<div className="text-4xl font-bold tracking-widest text-red-600">
์•Œ๋ฆผ ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค!
</div>
)}

{/* Interval ๋ชจ๋“œ: optimalRefreshTime ๋„๋‹ฌ ์‹œ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ */}
{alarm.options.useIntervalCalculation && showRefreshMessage && (
<div className="text-4xl font-bold tracking-widest text-red-600">
์ง€๊ธˆ ์ƒˆ๋กœ๊ณ ์นจํ•˜์„ธ์š”!
</div>
)}

{/* ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ */}
{alertMessages.length > 0 && (
<div className="mt-4 text-xs text-gray-600">
{alertMessages.map((message, index) => (
<div key={index}>
{message}
</div>
))}
</div>
)}

{/* Interval ๊ณ„์‚ฐ ์‚ฌ์šฉ ์‹œ ์ถ”๊ฐ€ ์ •๋ณด */}
{alarm.options.useIntervalCalculation && intervalResult && (
<div className="mt-4 text-xs text-gray-600">
<div>์ตœ์  ์ƒˆ๋กœ๊ณ ์นจ: {(intervalResult.data.refreshInterval / 1000).toFixed(1)}์ดˆ ์ „</div>
</div>
)}
</div>
</div>
);
Expand Down
Loading