이미 구현된 CalendarNav 중 AI 요약 관련 부분
const CalendarNav = ({ loadSummaries }) => {
const [aiLoading, setAiLoading] = useState(false);
const [aiError, setAiError] = useState(null);
...
// AI 요약 버튼
const handleSummarize = async () => {
setAiLoading(true);
setAiError(null);
try {
await api.post("/api/summarize");
// await loadSummaries();
} catch (e) {
console.error(e);
setAiError(`요약 실패: ${e.response?.data?.message || e.message}`);
} finally {
setAiLoading(false);
}
};
...
retuen(
{googleUser && (
<button
className="btn-ai"
onClick={handleSummarize}
disabled={aiLoading}
title="DB에 저장된 Gmail로부터 AI 요약 생성/업데이트"
>
{aiLoading ? "요약 중…" : "AI 요약하기"}
</button>
)}
{/* AI 오류 메시지 */}
{aiError && <span className="ai-error">{aiError}</span>}
);
};
현재 구현상황 → 버튼에서 "요약중 ..." 으로보임

캘린더 영역 오버레이로 로딩화면을 구현해보자
# 구현 구조
[캘린더 전체 박스 div (position: relative)]
├─ (로딩중이면) 오버레이 <div class="mb-calendar-overlay">
├─ CalendarNav (AI 요약 버튼 있는 헤더)
└─ 나머지 캘린더 본문 (날짜 그리드, 리스트 등)
1. 캘린더 전체를 감싸는 div 추가
- aiLoading : CalendarHeader → (상위)CalendarContainer 로 이동
- 요약 끝난 후 월별 일정+요약 reload 함수 추가
- return에 overlay 덮을 <div> 추가
import CalendarNav from "./CalendarNav";
import { useEffect, useState } from "react";
...
import CalendarLoadingOverlay from "../calendarpopup/CalendarLoadingOverlay";
const CalendarContainer = () => {
// AI 로딩 상태: 캘린더 전체를 덮을 오버레이를 제어해야 해서 장 바깥(캘린더 컨테이너)에 둠
const [aiLoading, setAiLoading] = useState(false);
...
// AI 요약이 끝난 후 “월별 일정/요약”을 같이 다시 불러줄 함수
const reloadMonthlyData = async () => {
await Promise.all([loadMonthlyEvents(), loadSummaryMonthlyEvents()]);
};
// currentDate 또는 refreshCount가 바뀔 때마다 월별 일정과 월별 요약을 다시 가져오기
useEffect(() => {
loadMonthlyEvents();
loadSummaryMonthlyEvents();
}, [currentDate, refreshCount]);
...
return (
// div가 “캘린더 전체 영역”
<div className="mb-calendar-shell">
{/* aiLoading이 true일 때만 캘린더 영역을 덮는 오버레이 */}
{aiLoading && (
<CalendarLoadingOverlay text="AI가 메일을 요약하는 중입니다..." />
)}
<div className="calpage-wrap">
<div className="cal-body">
<div className="cal-card cal-left-card">
{/* 캘린더 상단 버튼 영역 */}
<CalendarNav
aiLoading={aiLoading}
setAiLoading={setAiLoading}
loadSummaries={reloadMonthlyData} // 끝나면 월별 데이터 다시 로드
/>
<div className="cal-left">
{/* 리액트 캘린더 라이브러리, 캘린더 전체를 감싸는 컴포넌트 */}
<CalendarHome />
</div>
</div>
</div>
...
</div>
</div>
);
};
export default CalendarContainer;
2. 오버레이 컴포넌트 만들기
import "../../css/Schedule.css";
export default function CalendarLoadingOverlay({
text = "AI가 메일을 요약중입니다...",
}) {
return (
<div className="mb-calendar-overlay">
<div className="mb-calendar-overlay-box">
<div className="mb-calendar-overlay-spinner" />
<p className="mb-calendar-overlay-text">{text}</p>
</div>
</div>
);
}
3. CSS 추가
/* 캘린더 전체 영역 */
.mb-calendar-shell {
position: relative; /* 오버레이가 이 박스를 기준으로 깔리게 */
background: #ffffff;
border-radius: 16px;
border: 1px solid #e5e7eb;
padding: 16px;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
}
/* 캘린더 내부 레이아웃 (원래 있던 구조에 맞게 조정해서 써) */
.mb-calendar-body {
display: flex;
gap: 16px;
}
/* 오버레이: 캘린더 영역만 전체 덮기 */
.mb-calendar-overlay {
position: absolute;
inset: 0; /* top:0, right:0, bottom:0, left:0과 동일 */
background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(2px);
display: flex;
align-items: center;
justify-content: center;
z-index: 50; /* 캘린더 안의 어떤 내용보다 위에 오도록 */
}
/* 가운데 작은 박스 */
.mb-calendar-overlay-box {
background: #ffffff;
border-radius: 14px;
border: 1px solid #e5e7eb;
padding: 18px 24px;
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.25);
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
/* 동그란 스피너 */
.mb-calendar-overlay-spinner {
width: 28px;
height: 28px;
border-radius: 999px;
border: 3px solid #e5e7eb;
border-top-color: #111827; /* MailBuddy 포인트 컬러 */
animation: mb-calendar-spin 0.7s linear infinite;
}
/* 텍스트 */
.mb-calendar-overlay-text {
font-family: "Pretendard", "Noto Sans KR", system-ui, -apple-system,
BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 0.9rem;
color: #111827;
}
/* 회전 애니메이션 */
@keyframes mb-calendar-spin {
to {
transform: rotate(360deg);
}
}
4. AI 요약 버튼에서 로딩 상태 켜고 끄기
import { useState } from "react";
import api from "../../api/axiosConfig";
import useApi from "../../hooks/useApi";
// ... 기타 import
const CalendarNav = ({ aiLoading, setAiLoading, loadSummaries }) => {
const [message, setMessage] = useState("");
const [aiError, setAiError] = useState(null);
const { error, loading, request, setError } = useApi();
const handleAiSummarize = async () => {
setAiLoading(true); // 오버레이 켜기
setAiError(null);
setMessage("");
try {
// 실제 AI 요약 API 호출 부분
await api.post("/api/summarize/ai", null, { withCredentials: true });
// 약 완료 후 목록 다시 불러오기
await loadSummaries?.();
setMessage("AI가 메일을 요약해서 캘린더에 저장했어요!");
} catch (e) {
console.error(e);
setAiError("AI 요약 중 오류가 발생했어요. 다시 시도해 주세요.");
} finally {
setAiLoading(false); // 오버레이 끄기
}
};
return (
<div className="mb-calendar-nav">
...
<button
type="button"
className="mb-btn primary"
onClick={handleAiSummarize}
disabled={aiLoading} // 로딩중엔 중복 클릭 방지
>
{aiLoading ? "AI 요약 중..." : "AI 요약하기"}
</button>
{/* 에러/성공 메시지 */}
{aiError && <span className="mini-msg error">{aiError}</span>}
{message && !aiError && (
<span className="mini-msg success">{message}</span>
)}
</div>
);
};
export default CalendarNav;
✅︎ 흐름 정리
- AI 요약 버튼 클릭 → handleAiSummarize
- setAiLoading(true)
→ CalendarContainer의 aiLoading이 true
→ CalendarLoadingOverlay 보임 (캘린더 영역만 덮음)
→ 버튼도 "AI 요약 중..." + disabled - API 호출 끝 → setAiLoading(false) → 오버레이 사라짐, 버튼도 원래대로
구현 화면

'Project > Project01.MailBuddy' 카테고리의 다른 글
| 일반 폼로그인(세션) → JWT 연동 프로세스 (0) | 2025.11.27 |
|---|---|
| detail , summary 이용 FAQ 페이지 구현 (0) | 2025.11.24 |
| React 전역 상태 디버깅: selectedItem null 오류 해결 (0) | 2025.11.21 |
| 앵커 이용 Guide_Nav 만들어서 해당 section 이동 (0) | 2025.11.20 |
| Gmail 연동 중복 에러 처리 (0) | 2025.11.18 |