-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/search result3 #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Walkthrough์๋ฆผ ์นด์ดํธ๋ค์ด์ ์ธํฐ๋ฒ ๊ธฐ๋ฐ ๊ณ์ฐ ๊ธฐ๋ฅ๊ณผ ์ฌ์ด๋/์ ์ฒดํ๋ฉด ๊ฒฝ๊ณ ๋ฅผ ์ถ๊ฐํ๊ณ , ๋ชจ๋ฌ๊ณผ ๊ฒฐ๊ณผ ํ๋ฉด์์ Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User as ์ฌ์ฉ์
participant STR as ServerTimeResult
participant Modal as AlarmModal
participant AC as AlarmCountdown
participant API as /api/interval/calculate
User->>STR: ํ์ด์ง ์กฐํ
STR->>Modal: props(finalUrl, options)
STR->>AC: props(alarm, finalUrl)
alt ์ธํฐ๋ฒ ๊ณ์ฐ ์ฌ์ฉ
AC->>API: POST finalUrl๋ก ์ธํฐ๋ฒ ๊ณ์ฐ ์์ฒญ
API-->>AC: 200 OK (optimalRefreshTime, interval ๋ฑ)
AC->>AC: alertMessages/์ํ ๊ตฌ์ฑ, ์นด์ดํธ๋ค์ด ํ์
AC-->>User: ์ต์ ์์ ๋๋ฌ ์ "์ง๊ธ ์๋ก๊ณ ์นจํ์ธ์!" ํ์ ๋ฐ ์ฌ์ด๋/๋ฐฐ๊ฒฝ
else ๊ธฐ๋ณธ ๋ชจ๋
AC->>AC: ์ฌ์ ์๋ฆผ ์ค์ผ์ค ๊ตฌ์ฑ
AC-->>User: ์๋ฆผ ์๊ฐ ๋๋ฌ ์ ๋ฉ์์ง/์ฌ์ด๋/๋ฐฐ๊ฒฝ
end
Note over Modal,AC: Modal์์ ์ธํฐ๋ฒ ํ ๊ธ ์ finalUrl ๊ฒ์ฆ ์ํ
Estimated code review effort๐ฏ 4 (Complex) | โฑ๏ธ ~60 minutes Poem
Pre-merge checks and finishing touchesโ Failed checks (1 warning, 1 inconclusive)
โ Passed checks (1 passed)
โจ Finishing touches
๐งช Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
๐งน Nitpick comments (5)
src/components/search-result/AlarmCountdown.tsx (3)
131-149: optimalRefreshTime ์ฒดํฌ ์๋์ฐ๊ฐ ๋๋ฌด ์ข์[-1, 0]s ์ฐฝ์ ํญ ๋นํ์ฑ/์ค์ผ์ค ์ง์ฐ ์ ๋์น ์ ์์ต๋๋ค. 1) ์๊ณ๊ฐ ๊ต์ฐจ ๊ฐ์ง(์ด์ >0, ํ์ฌ<=0) ๋๋ 2) ์ฌ์ ์๋์ฐ ํ์ฅ(์: >= -2)์ ๊ถ์ฅํฉ๋๋ค.
๊ฐ๋จ ๋์:
- if (timeUntilOptimal <= 0 && timeUntilOptimal >= -1) { + if (timeUntilOptimal <= 0) { // ํ์ ๋ฐ ์๋ฆฌ ์ฌ์์ ๊ธฐ์กด ๋ก์ง ์ ์ง๋๋ ์ด์ ๊ฐ useRef๋ก ์ ์งํด crossing์ ๊ฐ์งํ์ธ์. ์ํ์๋ฉด ์ฝ๋ ์ ๊ณตํ๊ฒ ์ต๋๋ค.
252-258: Interval ๋ชจ๋ ์๋ฆผ ์ค์ผ์ค๋ง์ด ๋น์ด ์์intervalResult.data.alertSettings๋ฅผ ์ฌ์ฉํด ์๋ด ๋ฉ์์ง๋ฅผ ์ฑ์ฐ๋ฉด ์ ์ฉํฉ๋๋ค.
- const scheduleIntervalAlerts = () => { - // ์๋ฆผ์ ์ฆ์ ํ์ํ์ง ์๊ณ , ๋น ๋ฐฐ์ด๋ก ์ด๊ธฐํ - setAlertMessages([]); - console.log('๐ฏ Interval ์๋ฆผ ์ค์ผ์ค๋ง ์ค๋น ์๋ฃ'); - }; + const scheduleIntervalAlerts = () => { + if (!intervalResult?.data?.alertSettings) { + setAlertMessages([]); + return; + } + const msgs = intervalResult.data.alertSettings.map((s) => + s.message || `${s.time}์ด ์ ์๋ฆผ` + ); + setAlertMessages(msgs); + console.log('๐ฏ Interval ์๋ฆผ ์ค์ผ์ค๋ง:', msgs); + };
49-67: ์ ์ญ body ๋ฐฐ๊ฒฝ ๋ณ๊ฒฝ์ ์ ํ ์ํฅ ํผ (์ ํ์ฌํญ)ํ์ด์ง ์ ์ฒด์ ๋ถ์์ฉ์ด ํผ์ง ์ ์์ต๋๋ค. ๊ณ ์ ์ค๋ฒ๋ ์ด(div, fixed inset-0 bg-red-500 opacity-โฆ])๋ก ๋์ฒดํ๋ฉด ๊ฒฉ๋ฆฌ๋ฉ๋๋ค.
์ํ์๋ฉด ์ค๋ฒ๋ ์ด ๊ตฌํ ์์ ๋๋ฆด๊ฒ์.
src/components/search-result/ServerTimeResult.tsx (1)
326-335: ์์ธ ์ ๋ณด ๋ฒํผ ์ ๊ทผ์ฑ ๊ฐํ ์ ์์คํฌ๋ฆฐ ๋ฆฌ๋ ๋ช ํ์ฑ์ ์ํด aria-label์ ์ถ๊ฐํ์ธ์.
- <button + <button + aria-label="์์ธ ์ ๋ณด ์ด๊ธฐ" onClick={() => setShowDetailModal(true)} className="inline-flex items-center gap-1.5 px-3 py-1.5 text-gray-500 hover:text-gray-700 transition-colors duration-150 text-xs font-medium" >src/components/search-result/AlarmModal.tsx (1)
16-19: ์ต์ ํ๋ ์ผ๊ด์ฑ: targetUrl์ด ์ฌ์ฉ๋์ง ์์ํ์ฌ options.targetUrl์ ํญ์ ''๋ก ๋จ์ต๋๋ค. ์ ์ถ ์ finalUrl์ ๋ฐ์ํด ์ผ๊ด์ฑ ์ ์ง๊ฐ ์ข์ต๋๋ค(ํน์ ํ๋ ์ ๊ฑฐ).
- const [options, setOptions] = useState<AlarmOptions>({ + const [options, setOptions] = useState<AlarmOptions>({ preAlerts: [], sound: false, red: false, useIntervalCalculation: false, - targetUrl: '', + targetUrl: '', customAlertOffsets: [], });handleSubmit์์ finalUrl์ ์ฃผ์ :
- onConfirm({ - time: { hour, minute, second }, - options, - targetTime: targetTime.toISOString(), - }); + onConfirm({ + time: { hour, minute, second }, + options: options.useIntervalCalculation && finalUrl + ? { ...options, targetUrl: finalUrl } + : options, + targetTime: targetTime.toISOString(), + });Also applies to: 103-110
๐ Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (3)
src/components/search-result/AlarmCountdown.tsx(2 hunks)src/components/search-result/AlarmModal.tsx(8 hunks)src/components/search-result/ServerTimeResult.tsx(3 hunks)
๐งฐ Additional context used
๐งฌ Code graph analysis (2)
src/components/search-result/AlarmCountdown.tsx (1)
src/components/search-result/AlarmModal.tsx (1)
AlarmData(21-25)
src/components/search-result/ServerTimeResult.tsx (2)
src/components/search-result/AlarmModal.tsx (1)
AlarmModal(98-307)src/components/search-result/AlarmCountdown.tsx (1)
AlarmCountdown(34-340)
๐ Additional comments (4)
src/components/search-result/ServerTimeResult.tsx (2)
285-289: finalUrl ์ ๋ฌ ์ถ๊ฐ LGTMAlarmModal์ finalUrl={data.url} ์ ๋ฌ์ด ์ธํฐ๋ฒ ๊ณ์ฐ ์์กด์ฑ์ ๋ถํฉํฉ๋๋ค.
321-321: finalUrl ์ ๋ฌ LGTMAlarmCountdown์ finalUrl={data.url} ์ ๋ฌ๋ก ๋ฐ์ดํฐ ํ๋ฆ ์ผ๊ด์ฑ ํ๋ณด.
src/components/search-result/AlarmModal.tsx (2)
143-148: Interval ์ฌ์ฉ ์ finalUrl ๊ฒ์ฆ LGTM์ ํจ์ฑ ๊ฒ์ฌ๋ก ์ ๋ฐฉ์ดํ๊ณ ์์ต๋๋ค.
Interval ์ฌ์ฉ ์ UI์์๋ ๊ฐ๋จ ์๋ด๋ฌธ๊ตฌ(โ๊ฒ์ ๊ฒฐ๊ณผ URL ํ์โ)๋ฅผ ์ถ๊ฐํ๋ฉด ์ฌ์ฉ์ฑ์ด ๋ ์ข์์ง๋๋ค. ํ์ํ์๋ฉด ๋ฌธ๊ตฌ/์์น ์ ์ ๋๋ฆด๊ฒ์.
224-226: UI ์ํธ์์ฉ ์ค๊ณ ์ ์ ํจ
- Interval ํ์ฑํ ์ ์ฌ์ ์๋ฆผ ์น์ ๋นํ์ฑํ ์ฒ๋ฆฌ ์ ์ .
- ๊ณ ๊ธ ์ค์ ํ ๊ธ ์ถ๊ฐ๋ ๊น๋.
Also applies to: 271-287
| // ์๋ฆฌ ์ฌ์ ํจ์ (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); | ||
| }; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
| // 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); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)๋ ํจ๊ป ๋ฐ์ํด ์ฃผ์ธ์.
| 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]); | ||
|
|
There was a problem hiding this comment.
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.
| 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.
๐ ์์ ๋ด์ฉ
๐ธ ์คํฌ๋ฆฐ์ท
๐ ๊ธฐํ
Summary by CodeRabbit