Skip to content

Feature/search result3#13

Merged
hannah0352 merged 3 commits intomainfrom
feature/search-result3
Oct 2, 2025
Merged

Feature/search result3#13
hannah0352 merged 3 commits intomainfrom
feature/search-result3

Conversation

@hannah0352
Copy link
Collaborator

@hannah0352 hannah0352 commented Oct 2, 2025

📌 작업 내용

  • 알림 설정에 Interval 계산 옵션 추가

📸 스크린샷

스크린샷 2025-10-02 오후 10 48 15

📝 기타

Summary by CodeRabbit

  • New Features
    • 간격 기반 알림 모드(Interval 계산) 추가: 최적 새로고침 시점 안내, 카운트다운 자동 전환, 사운드 알림 지원.
    • 로딩 상태(“네트워크 분석 중...”), 모드별 메시지, 전체 화면 경고 배경 효과 추가.
    • 알림 설정에 고급 설정(Interval 토글)과 유효성 검사 추가; 활성 시 사전 알림 영역 비활성화.
  • UI Changes
    • 상세 정보 버튼 위치 조정.
    • 목표 URL 전달을 통해 목표 시간/알림 계산과 연동 강화.

@hannah0352 hannah0352 self-assigned this Oct 2, 2025
@hannah0352 hannah0352 added the feat🛠️ 기능 구현 label Oct 2, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 2, 2025

Walkthrough

알림 카운트다운에 인터벌 기반 계산 기능과 사운드/전체화면 경고를 추가하고, 모달과 결과 화면에서 finalUrl 전달을 통해 새 옵션(useIntervalCalculation 등)을 연동했습니다. 카운트다운은 /api/interval/calculate에 POST로 계산을 요청하고 결과에 따라 메시지/표시 로직을 분기합니다.

Changes

Cohort / File(s) Summary
Countdown 인터벌 계산/알림 확장
src/components/search-result/AlarmCountdown.tsx
AlarmCountdownPropsfinalUrl?: string 추가. 인터벌 계산 비동기 플로우(calculateInterval/api/interval/calculate) 도입, 상태 확장(계산/표시/사운드/메시지 제어). 인터벌 모드와 기본 사전 알림 모드 분기(scheduleIntervalAlerts/scheduleDefaultAlerts). 최적 갱신 시점에 카운트다운 숨김 및 “지금 새로고침하세요!” 메시지. Web Audio API 기반 사운드, 알람 시 전체 화면 빨간 배경 처리. 렌더링에 로딩/모드별 정보/메시지 추가.
모달 옵션/검증 및 UI 갱신
src/components/search-result/AlarmModal.tsx
AlarmModalPropsfinalUrl?: string. AlarmOptionsuseIntervalCalculation: boolean, targetUrl: string, customAlertOffsets: number[] 추가. 토글/초기값 처리, 인터벌 사용 시 finalUrl 필수 검증(미존재 시 alert 및 제출 중단). 라벨/섹션 업데이트 및 사전 알림 영역 비활성화 처리.
상위 컴포넌트 연동 및 배치 조정
src/components/search-result/ServerTimeResult.tsx
AlarmModalAlarmCountdownfinalUrldata.url 전달. 상세 정보 버튼 위치 조정(알림 섹션 이후로 이동). 경미한 포매팅 정리.

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 검증 수행
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

당근 들고 깡충, 타이머를 본다
빨간 하늘 번쩍, 시간은 온다
딱- 맞춘 인터벌, 새로고침 신호
삐빅♪ 소리 타고, 알림이 피어
오늘도 코드밭, 토끼는 지켜본다 🥕⏱️

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive 제목이 변경된 기능을 명확히 설명하지 않고 브랜치명과 숫자만 포함하고 있어 무엇이 변경되었는지 파악하기 어렵습니다. PR 제목을 핵심 변경 사항인 “알림 설정에 Interval 계산 옵션 추가” 등으로 간결하고 구체적으로 수정해 주세요.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed PR 설명은 템플릿에 따라 작업 내용과 스크린샷이 포함되어 있으며 작업 내용도 명확히 기술되어 있어 전체적인 요구 사항을 충족합니다.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/search-result3

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3e27d4a and 760cae8.

📒 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 전달 추가 LGTM

AlarmModal에 finalUrl={data.url} 전달이 인터벌 계산 의존성에 부합합니다.


321-321: finalUrl 전달 LGTM

AlarmCountdown에 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

Comment on lines +68 to +90
// 소리 재생 함수 (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);
};

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

Comment on lines +100 to +129
// 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);
}
};
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)도 함께 반영해 주세요.

Comment on lines 161 to +251
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]);

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.

@hannah0352 hannah0352 merged commit 36088fb into main Oct 2, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat🛠️ 기능 구현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant