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.
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.
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.
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.
🛠️ 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