Project/Project01.MailBuddy

loading overlay 구현 해보기

develop_mii 2025. 11. 20. 14:56

 

이미 구현된 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;

 

✅︎  흐름 정리

  1. AI 요약 버튼 클릭 → handleAiSummarize
  2. setAiLoading(true)
    → CalendarContainer의 aiLoading이 true
    → CalendarLoadingOverlay 보임 (캘린더 영역만 덮음)
    → 버튼도 "AI 요약 중..." + disabled
  3. API 호출 끝 → setAiLoading(false) → 오버레이 사라짐, 버튼도 원래대로

 

 

구현 화면