데이터 예시

const newPaymentInfo = {
    paymentList: [
      {
        paymentId: 1,
        companyId: 1,
        companyName: 'ATEMoS',
        meteredUsageId: 1,
        subscriptionServiceList: [
          'AI_ENERGY_USAGE_FORECAST',
          'AI_WATER_USAGE_ANALYSIS',
        ],
        apiCallCount: 50,
        iotInstallationCount: 5,
        storageUsage: 2048000,
        method: 'CARD',
        amount: 25.0,
        status: 'PAID',
        usageDate: '2024-08-25',
        scheduledPaymentDate: '2024-09-05',
        createdDate: '2024-08-26T09:30:00.000Z',
        modifiedDate: '2024-08-26T09:30:00.000Z',
      },
      {
        paymentId: 2,
        companyId: 2,
        companyName: 'GreenTech',
        meteredUsageId: 2,
        subscriptionServiceList: ['AI_SOLAR_POWER_MONITORING'],
        apiCallCount: 80,
        iotInstallationCount: 5,
        storageUsage: 3072000,
        method: 'BANK_TRANSFER',
        amount: 40.0,
        status: 'PAID',
        usageDate: '2024-08-26',
        scheduledPaymentDate: '2024-09-06',
        createdDate: '2024-08-26T10:00:00.000Z',
        modifiedDate: '2024-08-26T10:00:00.000Z',
      },
      {
        paymentId: 3,
        companyId: 3,
        companyName: 'EcoWatt',
        meteredUsageId: 3,
        subscriptionServiceList: [],
        apiCallCount: 5,
        iotInstallationCount: 8,
        storageUsage: 1024000,
        method: 'CASH',
        amount: 10.0,
        status: 'OUTSTANDING',
        usageDate: '2024-08-27',
        scheduledPaymentDate: '2024-09-07',
        createdDate: '2024-08-27T12:00:00.000Z',
        modifiedDate: '2024-08-27T12:00:00.000Z',
      },
      {
        paymentId: 4,
        companyId: 4,
        companyName: 'PowerTech',
        meteredUsageId: 4,
        subscriptionServiceList: [
          'AI_WIND_TURBINE_OPTIMIZATION',
          'AI_ENERGY_STORAGE_MANAGEMENT',
        ],
        apiCallCount: 100,
        iotInstallationCount: 7,
        storageUsage: 4096000,
        method: 'CARD',
        amount: 50.0,
        status: 'PAID',
        usageDate: '2024-08-28',
        scheduledPaymentDate: '2024-09-08',
        createdDate: '2024-08-28T14:00:00.000Z',
        modifiedDate: '2024-08-28T14:00:00.000Z',
      },
    ],
    summaryApiCallCount: 230,
    recentlyIotInstallationCount: 10,
    recentlyStorageUsage: {
      '1': 20480000,
      '2': 3072000,
      '3': 1024000,
      '4': 4096000,
    },
    subscribedCount: {
      '1': { AI_ENERGY_USAGE_FORECAST: 1, AI_WATER_USAGE_ANALYSIS: 1 },
      '2': { AI_SOLAR_POWER_MONITORING: 1 },
      '4': { AI_WIND_TURBINE_OPTIMIZATION: 1, AI_ENERGY_STORAGE_MANAGEMENT: 1 },
    },
    summaryAmount: 125.0,
    totalElements: 4,
    totalPages: 1,
  };

 

 

기존 React-calendar 코드

// 기존 React-calendar 코드
import ai from '../../images/icon/ai_icon.png';
import api from '../../images/icon/api_icon.png';
import facilities from '../../images/icon/facilities_icon.png';
import storage from '../../images/icon/storage_icon.png';
import Calendar from 'react-calendar';
// import 'react-calendar/dist/Calendar.css';
import '../../css/custom-calendar.css'; // 커스텀 스타일 파일 import
import { useEffect, useState } from 'react';
import { format, startOfMonth, endOfMonth } from 'date-fns';
import Breadcrumb from '../../components/Breadcrumbs/Breadcrumb';
import { fetchData } from '../../api';
import { loggedInUserState } from '../../store/loggedInUserAtom';
import { useRecoilValue } from 'recoil';

const Subscription = () => {
  const loggedInUser = useRecoilValue(loggedInUserState);

  const [currentMonth, setCurrentMonth] = useState(new Date());
  const [paymentInfo, setPaymentInfo] = useState({
    paymentList: [],
    summaryApiCallCount: 0,
    recentlyIotInstallationCount: 0,
    recentlyStorageUsage: {},
    subscribedCount: {},
  });

  // 현재 달의 1일과 말일 계산
  const startOfCurrentMonth = format(startOfMonth(currentMonth), 'yyyy-MM-dd');
  const endOfCurrentMonth = format(endOfMonth(currentMonth), 'yyyy-MM-dd');

  const handlePrevMonth = () => {
    setCurrentMonth((prev) => new Date(prev.setMonth(prev.getMonth() - 1)));
  };

  const handleNextMonth = () => {
    setCurrentMonth((prev) => new Date(prev.setMonth(prev.getMonth() + 1)));
  };

  const getMonthYear = (date: any) => {
    return format(date, 'MMMM yyyy').toUpperCase();
  };

  const fetchPaymentInfo = async () => {
    if (loggedInUser && loggedInUser.memberId && loggedInUser.companyId) {
      try {
        // API 호출을 통해 payment 정보 가져오기
        const data = await fetchData(
          /payment?companyId=${loggedInUser.companyId}&usageDateStart=${startOfCurrentMonth}&usageDateEnd=${endOfCurrentMonth},
        );
        setPaymentInfo(data.data);
      } catch (error) {
        console.error('Error while fetching payment info:', error);
      }
    }
  };

  useEffect(() => {
    // fetchPaymentInfo();
    setPaymentInfo(newPaymentInfo);
  }, [currentMonth]);

  // usageDate를 키로 하는 apiCallCount 매핑 생성
  const apiCallCounts = paymentInfo.paymentList.reduce(
    (acc, payment: any) => {
      const date = new Date(payment.usageDate).toDateString(); // 날짜를 문자열로 변환
      acc[date] = (acc[date] || 0) + payment.apiCallCount;
      return acc;
    },
    {} as Record<string, number>,
  );

  // 합산 Ai
  const getTotalSubscriptionCount = () => {
    return paymentInfo.paymentList.reduce(
      (total: number, payment: any) =>
        payment.subscriptionServiceList.length > 0 ? total + 1 : total,
      0,
    );
  };

  // 합산 ApiCall
  const getTotalApiCallCount = () => {
    return paymentInfo.paymentList.reduce(
      (total, payment: any) => total + payment.apiCallCount,
      0,
    );
  };

  // 합산 IotInstallation
  const getTotalIotInstallationCount = () => {
    // paymentList를 날짜 기준으로 정렬
    const sortedList = paymentInfo.paymentList.sort(
      (a, b) =>
        new Date(b.usageDate).getTime() - new Date(a.usageDate).getTime(),
    );

    // 가장 최근 항목의 iotInstallationCount 반환
    return sortedList[0]?.iotInstallationCount || 0;
  };

  // 합산 Storage
  // recentlyStorageUsage 객체의 첫 번째 키를 동적으로 추출
  const storageKey = Object.keys(paymentInfo.recentlyStorageUsage)[0];

  // 해당 키의 값을 기가바이트로 변환
  const storageUsageInGB = storageKey
    ? paymentInfo.recentlyStorageUsage[storageKey] / (1024 * 1024 * 1024)
    : 0;

  return (
    <div className="w-full">
      <Breadcrumb pageName="Service Usage History" />

      <div className="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
        <div className="flex items-center justify-between px-7.5 pt-7.5">
          <h3 className="text-lg font-semibold text-black dark:text-white">
            {getMonthYear(currentMonth)}
          </h3>
          <div>
            <button
              onClick={handlePrevMonth}
              className="mr-2 rounded border border-stroke p-1 text-xl font-bold shadow-card-2 hover:bg-meta-2
            "
            >
              <svg
                width="18"
                height="18"
                viewBox="0 0 18 18"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
                style={{ transform: 'rotate(90deg)' }}
              >
                <path
                  d="M9.0002 12.825C8.83145 12.825 8.69082 12.7688 8.5502 12.6563L2.08145 6.30002C1.82832 6.0469 1.82832 5.65315 2.08145 5.40002C2.33457 5.1469 2.72832 5.1469 2.98145 5.40002L9.0002 11.2781L15.0189 5.34377C15.2721 5.09065 15.6658 5.09065 15.9189 5.34377C16.1721 5.5969 16.1721 5.99065 15.9189 6.24377L9.45019 12.6C9.30957 12.7406 9.16895 12.825 9.0002 12.825Z"
                  fill="#64748B"
                />
              </svg>
            </button>
            <button
              onClick={handleNextMonth}
              className="rounded border border-stroke p-1 text-xl font-bold shadow-card-2 hover:bg-meta-2"
            >
              <svg
                width="18"
                height="18"
                viewBox="0 0 18 18"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
                style={{ transform: 'rotate(-90deg)' }}
              >
                <path
                  d="M9.0002 12.825C8.83145 12.825 8.69082 12.7688 8.5502 12.6563L2.08145 6.30002C1.82832 6.0469 1.82832 5.65315 2.08145 5.40002C2.33457 5.1469 2.72832 5.1469 2.98145 5.40002L9.0002 11.2781L15.0189 5.34377C15.2721 5.09065 15.6658 5.09065 15.9189 5.34377C16.1721 5.5969 16.1721 5.99065 15.9189 6.24377L9.45019 12.6C9.30957 12.7406 9.16895 12.825 9.0002 12.825Z"
                  fill="#64748B"
                />
              </svg>
            </button>
          </div>
        </div>
        <div className="grid grid-cols-1 gap-4 p-7.5 md:grid-cols-4">
          <div className="col-span-1 rounded border border-stroke bg-white dark:border-strokedark dark:bg-boxdark lg:col-span-1  md:col-span-2 xl:col-span-3 ">
            <Calendar
              calendarType="gregory"
              value={currentMonth}
              onChange={(newDate: any) => setCurrentMonth(newDate)}
              showNeighboringMonth={true}
              maxDetail="month"
              minDetail="month"
              formatShortWeekday={(locale: any, date) => {
                const weekdays = [
                  'SUN',
                  'MON',
                  'TUE',
                  'WED',
                  'THU',
                  'FRI',
                  'SAT',
                ];
                return weekdays[date.getDay()];
              }}
              formatDay={(locale: any, date) => date.getDate().toString()} // 날짜 포맷 수정
              tileContent={({ date }) => {
                const dateKey = new Date(date).toDateString();
                const currentDate = date.setHours(0, 0, 0, 0);

                // API 호출 수
                const apiCallCount = apiCallCounts[dateKey] || 0;

                // AI 서비스 사용 여부 확인 (특정 날짜만 체크)
                const isAnyServiceUsed = paymentInfo.paymentList.some(
                  (payment: any) =>
                    new Date(payment.usageDate).toDateString() === dateKey &&
                    payment.subscriptionServiceList.length > 0,
                );

                return (
                  <div className="flex w-full flex-col gap-1">
                    {apiCallCount > 0 && (
                      <div className="dot api">
                        <span className="mr-1">API</span>
                        {apiCallCount}
                      </div>
                    )}
                    {isAnyServiceUsed && (
                      <div className="dot ai">
                        <span className="mr-1">AI</span>
                      </div>
                    )}
                  </div>
                );
              }}
            />
          </div>
          <ul className="col-span-1 flex flex-col gap-4 md:col-span-2 xl:col-span-1">
            <li className="flex items-center justify-between rounded border border-stroke bg-white p-5 dark:border-strokedark dark:bg-boxdark">
              <div className="flex items-center gap-4">
                <p className="flex h-10 w-10 items-center justify-center rounded-full bg-meta-2">
                  <img src={ai} alt="Logo" />
                </p>
                <p className="text-lg font-semibold text-black dark:text-white">
                  AI Usage
                </p>
              </div>
              <p className="font-semibold text-black dark:text-white">
                {getTotalSubscriptionCount()}
                <span className="ml-0.5 text-sm">days</span>
              </p>
            </li>
            <li className="flex items-center justify-between rounded border border-stroke bg-white p-5 dark:border-strokedark dark:bg-boxdark">
              <div className="flex items-center gap-4">
                <p className="flex h-10 w-10 items-center justify-center rounded-full bg-meta-2">
                  <img src={api} alt="Logo" />
                </p>
                <p className="text-lg font-semibold text-black dark:text-white">
                  API Usage
                </p>
              </div>
              <p className="font-semibold text-black dark:text-white">
                {getTotalApiCallCount()}
                <span className="ml-0.5 text-sm"></span>
              </p>
            </li>
            <li className="flex items-center justify-between rounded border border-stroke bg-white p-5 dark:border-strokedark dark:bg-boxdark">
              <div className="flex items-center gap-4">
                <p className="flex h-10 w-10 items-center justify-center rounded-full bg-meta-2">
                  <img
                    src={facilities}
                    alt="
                  go"
                  />
                </p>
                <p className="text-lg font-semibold text-black dark:text-white">
                  facilities
                </p>
              </div>
              <p className="font-semibold text-black dark:text-white">
                {getTotalIotInstallationCount()}
                <span className="ml-0.5 text-sm"></span>
              </p>
            </li>
            <li className="flex items-center justify-between rounded border border-stroke bg-white p-5 dark:border-strokedark dark:bg-boxdark">
              <div className="flex items-center gap-4">
                <p className="flex h-10 w-10 items-center justify-center rounded-full bg-meta-2">
                  <img src={storage} alt="Logo" />
                </p>
                <p className="text-lg font-semibold text-black dark:text-white">
                  Storage
                </p>
              </div>
              <p className="font-semibold text-black dark:text-white">
                {storageUsageInGB < 0.01
                  ? ${storageUsageInGB.toFixed(3)}
                  : ${storageUsageInGB.toFixed(2)}}
                <span className="ml-0.5 text-sm">GB</span>
              </p>
            </li>
          </ul>
        </div>

        <div className="px-7.5 pb-7.5 pt-2.5">
          <h3 className="text-lg font-semibold text-black dark:text-white">
            {loggedInUser?.companyName} / {loggedInUser?.companyType}
          </h3>
          <div className="pt-7.5">
            <table className="w-full leading-loose">
              <tbody className="w-full">
                <tr className="h-10 w-full border-b border-t border-stroke">
                  <td className="w-3/12">AI Usage</td>
                  <td className="hidden w-5/12 md:block">
                    {startOfCurrentMonth} ~ {endOfCurrentMonth}
                  </td>
                  <td className="w-2/12">
                    {getTotalSubscriptionCount()}
                    <span className="ml-0.5">days</span>
                  </td>
                  <td className="w-2/12 text-right">$120.00</td>
                </tr>
                <tr className="h-10 w-full border-b border-stroke">
                  <td className="w-3/12">API Usage</td>
                  <td className="hidden w-5/12 md:block">
                    {startOfCurrentMonth} ~ {endOfCurrentMonth}
                  </td>
                  <td className="w-2/12">
                    {getTotalApiCallCount()}
                    <span className="ml-0.5"></span>
                  </td>
                  <td className="w-2/12 text-right">$120.00</td>
                </tr>
                <tr className="h-10 w-full border-b border-stroke">
                  <td className="w-3/12">facilities</td>
                  <td className="hidden w-5/12 md:block">
                    {startOfCurrentMonth} ~ {endOfCurrentMonth}
                  </td>
                  <td className="w-2/12">
                    {getTotalIotInstallationCount()}
                    <span className="ml-0.5"></span>
                  </td>
                  <td className="w-2/12 text-right">$120.00</td>
                </tr>
                <tr className="h-10 w-full border-b border-stroke">
                  <td className="w-3/12">Storage</td>
                  <td className="hidden w-5/12 md:block">
                    {startOfCurrentMonth} ~ {endOfCurrentMonth}
                  </td>
                  <td className="w-2/12">
                    {storageUsageInGB < 0.01
                      ? ${storageUsageInGB.toFixed(3)}
                      : ${storageUsageInGB.toFixed(2)}}
                    <span className="ml-0.5">GB</span>
                  </td>
                  <td className="w-2/12 text-right">$120.00</td>
                </tr>
              </tbody>
            </table>
          </div>

          <table className="w-full leading-loose">
            <tbody className="w-full">
              <tr className="h-10 w-full">
                <td className="hidden w-8/12 md:block"></td>
                <td className="w-2/12">Subtotal</td>
                <td className="w-2/12 text-right">$120.00</td>
              </tr>
              <tr className="h-10 w-full">
                <td className="hidden w-8/12 md:block"></td>
                <td className="w-2/12 border-b border-primary">Duty (+)</td>
                <td className="w-2/12 border-b border-primary text-right">
                  $10.00
                </td>
              </tr>
              <tr className="h-10 w-full font-medium text-primary">
                <td className="hidden w-8/12 md:block"></td>
                <td className="w-2/12">Total</td>
                <td className="w-2/12 text-right">$130.00</td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    </div>
  );
};

export default Subscription;

 

설치

npm install react-big-calendar

 

npm install date-fns

 

import 

import moment from 'moment'; // moment 추가
import { Calendar, momentLocalizer } from 'react-big-calendar';
import { format, parseISO, startOfMonth, endOfMonth } from 'date-fns';
import 'react-big-calendar/lib/css/react-big-calendar.css';

 

const localizer = momentLocalizer(moment);

const Subscription = () => {
  const [events, setEvents] = useState([]);
  
  const fetchPaymentInfo = async () => {
    if (loggedInUser && loggedInUser.memberId && loggedInUser.companyId) {
      try {
        // API call to get payment data
        const data = await fetchData(
          `/payment?companyId=${loggedInUser.companyId}&usageDateStart=${startOfCurrentMonth}&usageDateEnd=${endOfCurrentMonth}`
        );
        const paymentList = data.data.paymentList;
        
        const events = paymentList
        .filter((payment) => payment.subscriptionServiceList.length > 0) // 서비스가 있는 경우만 필터링
        .map((payment) => ({
          start: parseISO(payment.usageDate),
          end: parseISO(payment.usageDate),
          title: 'AI', // 서비스가 있을 때는 'AI'
          allDay: true,
          resource: payment,
        }));
        
        setEvents(events);
      } catch (error) {
        console.error('Error fetching payment info:', error);
      }
    }
  };
}

  useEffect(() => {
    fetchPaymentInfo();
  }, [currentMonth]);
  
   // Function to generate a set of continuous events with the same title
  const getContinuousEvents = (events) => {
    const sortedEvents = events.sort((a, b) => a.start - b.start);
    const continuousEvents = [];
    let currentEvent = null;

    sortedEvents.forEach(event => {
      if (currentEvent) {
        const lastEventDate = new Date(currentEvent.end);
        const currentEventDate = new Date(event.start);
        if (currentEvent.title === event.title && currentEventDate <= lastEventDate) {
          currentEvent.end = currentEvent.end > event.end ? currentEvent.end : event.end;
        } else {
          continuousEvents.push(currentEvent);
          currentEvent = event;
        }
      } else {
        currentEvent = event;
      }
    });

    if (currentEvent) {
      continuousEvents.push(currentEvent);
    }

    return continuousEvents;
  const continuousEvents = getContinuousEvents(events);

  return (
    <div style={{ height: '100vh' }}>
      <Calendar
        localizer={localizer}
        events={continuousEvents}
        startAccessor="start"
        endAccessor="end"
        style={{ height: 500 }}
        views={['month']}
        onNavigate={(date) => setCurrentMonth(date)}
      />
    </div>
  );
};

export default Subscription;

 

 

연속된 날짜로 배열 표시하려면 아래 함수 추가

const mergeContinuousEvents = (events) => {
  if (events.length === 0) return [];

  const sortedEvents = events.sort((a, b) => a.start.getTime() - b.start.getTime());
  const mergedEvents = [];
  let currentEvent = sortedEvents[0];

  for (let i = 1; i < sortedEvents.length; i++) {
    const nextEvent = sortedEvents[i];

    if (
      currentEvent.end.getTime() >= nextEvent.start.getTime() - 24 * 60 * 60 * 1000 &&
      currentEvent.title === nextEvent.title
    ) {
      currentEvent.end = new Date(
        Math.max(currentEvent.end.getTime(), nextEvent.end.getTime())
      );
    } else {
      // Adjust end date to include the entire day
      currentEvent.end = new Date(currentEvent.end.getTime() + 24 * 60 * 60 * 1000);
      mergedEvents.push(currentEvent);
      currentEvent = nextEvent;
    }
  }

  currentEvent.end = new Date(currentEvent.end.getTime() + 24 * 60 * 60 * 1000);
  mergedEvents.push(currentEvent);

  return mergedEvents;
};

 

아래처럼 수정

useEffect(() => {
  const paymentList = newPaymentInfo.paymentList;
  const events = paymentList
    .filter((payment) => payment.subscriptionServiceList.length > 0)
    .map((payment) => ({
      start: parseISO(payment.usageDate),
      end: parseISO(payment.usageDate),
      title: 'AI',
      allDay: true,
      resource: payment,
    }));

  const mergedEvents = mergeContinuousEvents(events);
  setEvents(mergedEvents);

  console.log(mergedEvents);
}, [currentMonth]);

 

월 필터 핸들러 기존 버튼에서 다루게 하기

import React, { useState, useEffect } from 'react';
import { Calendar as BigCalendar, momentLocalizer } from 'react-big-calendar';
import moment from 'moment';
import 'react-big-calendar/lib/css/react-big-calendar.css';
import { format, startOfMonth, endOfMonth, parseISO, addMonths, subMonths } from 'date-fns';

const localizer = momentLocalizer(moment);

const MyCustomCalendar = ({ newPaymentInfo }) => {
  const [currentMonth, setCurrentMonth] = useState(new Date());
  const [events, setEvents] = useState([]);

  // 현재 달의 1일과 말일 계산
  const startOfCurrentMonth = format(startOfMonth(currentMonth), 'yyyy-MM-dd');
  const endOfCurrentMonth = format(endOfMonth(currentMonth), 'yyyy-MM-dd');

  // 이벤트 생성 함수
  const generateEvents = () => {
    const paymentList = newPaymentInfo.paymentList;

    const aiEvents = paymentList
      .filter((payment) => payment.subscriptionServiceList.length > 0)
      .map((payment) => ({
        start: parseISO(payment.usageDate),
        end: parseISO(payment.usageDate),
        title: 'AI',
        allDay: true,
        resource: payment,
      }));

    const apiEvents = paymentList
      .filter((payment) => payment.apiCallCount > 0)
      .map((payment) => ({
        start: parseISO(payment.usageDate),
        end: parseISO(payment.usageDate),
        title: 'API',
        allDay: true,
        resource: payment,
      }));

    // 이벤트 병합 및 설정
    const mergedAIEvents = mergeAIEvents(aiEvents); // 기존의 mergeAIEvents 사용
    setEvents([...mergedAIEvents, ...apiEvents]);
  };

  useEffect(() => {
    generateEvents();
  }, [currentMonth, newPaymentInfo]); // currentMonth와 newPaymentInfo에 의존

  // 월 변경 처리 함수
  const handlePrevMonth = () => {
    setCurrentMonth((prev) => subMonths(prev, 1));
  };

  const handleNextMonth = () => {
    setCurrentMonth((prev) => addMonths(prev, 1));
  };

  return (
    <div>
      <div className="flex justify-between mb-4">
        <button
          onClick={handlePrevMonth}
          className="mr-2 rounded border border-stroke p-1 text-xl font-bold shadow-card-2 hover:bg-meta-2"
        >
          <svg
            width="18"
            height="18"
            viewBox="0 0 18 18"
            fill="none"
            xmlns="http://www.w3.org/2000/svg"
            style={{ transform: 'rotate(90deg)' }}
          >
            <path
              d="M9.0002 12.825C8.83145 12.825 8.69082 12.7688 8.5502 12.6563L2.08145 6.30002C1.82832 6.0469 1.82832 5.65315 2.08145 5.40002C2.33457 5.1469 2.72832 5.1469 2.98145 5.40002L9.0002 11.2781L15.0189 5.34377C15.2721 5.09065 15.6658 5.09065 15.9189 5.34377C16.1721 5.5969 16.1721 5.99065 15.9189 6.24377L9.45019 12.6C9.30957 12.7406 9.16895 12.825 9.0002 12.825Z"
              fill="#64748B"
            />
          </svg>
        </button>
        <button
          onClick={handleNextMonth}
          className="rounded border border-stroke p-1 text-xl font-bold shadow-card-2 hover:bg-meta-2"
        >
          <svg
            width="18"
            height="18"
            viewBox="0 0 18 18"
            fill="none"
            xmlns="http://www.w3.org/2000/svg"
            style={{ transform: 'rotate(-90deg)' }}
          >
            <path
              d="M9.0002 12.825C8.83145 12.825 8.69082 12.7688 8.5502 12.6563L2.08145 6.30002C1.82832 6.0469 1.82832 5.65315 2.08145 5.40002C2.33457 5.1469 2.72832 5.1469 2.98145 5.40002L9.0002 11.2781L15.0189 5.34377C15.2721 5.09065 15.6658 5.09065 15.9189 5.34377C16.1721 5.5969 16.1721 5.99065 15.9189 6.24377L9.45019 12.6C9.30957 12.7406 9.16895 12.825 9.0002 12.825Z"
              fill="#64748B"
            />
          </svg>
        </button>
      </div>

      <BigCalendar
        localizer={localizer}
        events={events}
        startAccessor="start"
        endAccessor="end"
        style={{ height: 500 }}
        views={['month']}
        date={currentMonth} // 현재 월을 표시
        className="rbc-calendar"
      />
    </div>
  );
};

export default MyCustomCalendar;

 

 

최종 코드

import ai from '../../images/icon/ai_icon.png';
import api from '../../images/icon/api_icon.png';
import facilities from '../../images/icon/facilities_icon.png';
import storage from '../../images/icon/storage_icon.png';
import { useEffect, useState } from 'react';
import { format, parseISO, startOfMonth, endOfMonth } from 'date-fns';
import Breadcrumb from '../../components/Breadcrumbs/Breadcrumb';
import { fetchData } from '../../api';
import { loggedInUserState } from '../../store/loggedInUserAtom';
import { useRecoilValue } from 'recoil';

import moment from 'moment'; // moment 추가
import { Calendar, momentLocalizer } from 'react-big-calendar';
import '../../css/react-big-calendar.css';

const localizer = momentLocalizer(moment);

const Subscription = () => {
  const loggedInUser = useRecoilValue(loggedInUserState);

  const [currentMonth, setCurrentMonth] = useState(new Date());
  const [paymentInfo, setPaymentInfo] = useState({
    paymentList: [],
    summaryApiCallCount: 0,
    recentlyIotInstallationCount: 0,
    recentlyStorageUsage: {},
    subscribedCount: {},
  });

  const [events, setEvents] = useState([]);

  // 현재 달의 1일과 말일 계산
  const startOfCurrentMonth = format(startOfMonth(currentMonth), 'yyyy-MM-dd');
  const endOfCurrentMonth = format(endOfMonth(currentMonth), 'yyyy-MM-dd');

  const handlePrevMonth = () => {
    setCurrentMonth((prev) => new Date(prev.setMonth(prev.getMonth() - 1)));
  };

  const handleNextMonth = () => {
    setCurrentMonth((prev) => new Date(prev.setMonth(prev.getMonth() + 1)));
  };

  const getMonthYear = (date: any) => {
    return format(date, 'MMMM yyyy').toUpperCase();
  };

  const fetchPaymentInfo = async () => {
    if (loggedInUser && loggedInUser.memberId && loggedInUser.companyId) {
      try {
        // API 호출을 통해 payment 정보 가져오기
        const data = await fetchData(
          `/payment?companyId=${loggedInUser.companyId}&usageDateStart=${startOfCurrentMonth}&usageDateEnd=${endOfCurrentMonth}`,
        );
        setPaymentInfo(data.data);
      } catch (error) {
        console.error('Error while fetching payment info:', error);
      }
    }
  };

  const mergeAIEvents = (events: any) => {
    if (events.length === 0) return [];

    const sortedEvents = events.sort(
      (a, b) => a.start.getTime() - b.start.getTime(),
    );
    const mergedEvents = [];
    let currentEvent = sortedEvents[0];

    for (let i = 1; i < sortedEvents.length; i++) {
      const nextEvent = sortedEvents[i];

      // 연속된 날짜이고, 같은 제목의 이벤트일 경우
      if (
        currentEvent.end.getTime() >=
          nextEvent.start.getTime() - 24 * 60 * 60 * 1000 &&
        currentEvent.title === nextEvent.title
      ) {
        currentEvent.end = new Date(
          Math.max(currentEvent.end.getTime(), nextEvent.end.getTime()),
        );
      } else {
        // 종료 날짜를 포함시키기 위해 1일 추가
        currentEvent.end = new Date(
          currentEvent.end.getTime() + 24 * 60 * 60 * 1000,
        );
        mergedEvents.push(currentEvent);
        currentEvent = nextEvent;
      }
    }

    currentEvent.end = new Date(
      currentEvent.end.getTime() + 24 * 60 * 60 * 1000,
    );
    mergedEvents.push(currentEvent);

    return mergedEvents;
  };

  const generateEvents = () => {
    const paymentList = paymentInfo.paymentList;

    const aiEvents = paymentList
      .filter((payment) => payment.subscriptionServiceList.length > 0)
      .map((payment) => ({
        start: parseISO(payment.usageDate),
        end: parseISO(payment.usageDate),
        title: 'AI',
        allDay: true,
        resource: payment,
        type: 'ai', // 추가 속성
      }));

    const apiEvents = paymentList
      .filter((payment) => payment.apiCallCount > 0)
      .map((payment) => ({
        start: parseISO(payment.usageDate),
        end: parseISO(payment.usageDate),
        title: `API ${payment.apiCallCount}`, // API 호출 수를 포함
        allDay: true,
        resource: payment,
        type: 'api', // 추가 속성
      }));

    const mergedAIEvents = mergeAIEvents(aiEvents);
    setEvents([...mergedAIEvents, ...apiEvents]);
  };

  useEffect(() => {
    fetchPaymentInfo();
  }, [currentMonth]);

  useEffect(() => {
    generateEvents();
  }, [paymentInfo]); // paymentInfo가 업데이트될 때마다 generateEvents 호출

  // 이벤트 스타일 설정 함수
  const eventStyleGetter = (event) => {
    let className = '';

    switch (event.type) {
      case 'ai':
        className = 'ai';
        break;
      case 'api':
        className = 'api';
        break;
      default:
        className = '';
        break;
    }

    return {
      className: `rbc-event ${className}`, // 클래스명 설정
    };
  };

  // usageDate를 키로 하는 apiCallCount 매핑 생성
  const apiCallCounts = paymentInfo.paymentList.reduce(
    (acc, payment: any) => {
      const date = new Date(payment.usageDate).toDateString(); // 날짜를 문자열로 변환
      acc[date] = (acc[date] || 0) + payment.apiCallCount;
      return acc;
    },
    {} as Record<string, number>,
  );

  // 합산 Ai
  const getTotalSubscriptionCount = () => {
    return paymentInfo.paymentList.reduce(
      (total: number, payment: any) =>
        payment.subscriptionServiceList.length > 0 ? total + 1 : total,
      0,
    );
  };

  // 합산 ApiCall
  const getTotalApiCallCount = () => {
    return paymentInfo.paymentList.reduce(
      (total, payment: any) => total + payment.apiCallCount,
      0,
    );
  };

  // 합산 IotInstallation
  const getTotalIotInstallationCount = () => {
    // paymentList를 날짜 기준으로 정렬
    const sortedList = paymentInfo.paymentList.sort(
      (a, b) =>
        new Date(b.usageDate).getTime() - new Date(a.usageDate).getTime(),
    );

    // 가장 최근 항목의 iotInstallationCount 반환
    return sortedList[0]?.iotInstallationCount || 0;
  };

  // 합산 Storage
  // recentlyStorageUsage 객체의 첫 번째 키를 동적으로 추출
  const storageKey = Object.keys(paymentInfo.recentlyStorageUsage)[0];

  // 해당 키의 값을 기가바이트로 변환
  const storageUsageInGB = storageKey
    ? paymentInfo.recentlyStorageUsage[storageKey] / (1024 * 1024 * 1024)
    : 0;

  return (
    <div className="w-full">
      <Breadcrumb pageName="Service Usage History" />

      <div className="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
        <div className="flex items-center justify-between px-7.5 pt-7.5">
          <h3 className="text-lg font-semibold text-black dark:text-white">
            {getMonthYear(currentMonth)}
          </h3>
          <div>
            <button
              onClick={handlePrevMonth}
              className="mr-2 rounded border border-stroke p-1 text-xl font-bold shadow-card-2 hover:bg-meta-2
            "
            >
              <svg
                width="18"
                height="18"
                viewBox="0 0 18 18"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
                style={{ transform: 'rotate(90deg)' }}
              >
                <path
                  d="M9.0002 12.825C8.83145 12.825 8.69082 12.7688 8.5502 12.6563L2.08145 6.30002C1.82832 6.0469 1.82832 5.65315 2.08145 5.40002C2.33457 5.1469 2.72832 5.1469 2.98145 5.40002L9.0002 11.2781L15.0189 5.34377C15.2721 5.09065 15.6658 5.09065 15.9189 5.34377C16.1721 5.5969 16.1721 5.99065 15.9189 6.24377L9.45019 12.6C9.30957 12.7406 9.16895 12.825 9.0002 12.825Z"
                  fill="#64748B"
                />
              </svg>
            </button>
            <button
              onClick={handleNextMonth}
              className="rounded border border-stroke p-1 text-xl font-bold shadow-card-2 hover:bg-meta-2"
            >
              <svg
                width="18"
                height="18"
                viewBox="0 0 18 18"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
                style={{ transform: 'rotate(-90deg)' }}
              >
                <path
                  d="M9.0002 12.825C8.83145 12.825 8.69082 12.7688 8.5502 12.6563L2.08145 6.30002C1.82832 6.0469 1.82832 5.65315 2.08145 5.40002C2.33457 5.1469 2.72832 5.1469 2.98145 5.40002L9.0002 11.2781L15.0189 5.34377C15.2721 5.09065 15.6658 5.09065 15.9189 5.34377C16.1721 5.5969 16.1721 5.99065 15.9189 6.24377L9.45019 12.6C9.30957 12.7406 9.16895 12.825 9.0002 12.825Z"
                  fill="#64748B"
                />
              </svg>
            </button>
          </div>
        </div>
        <div className="grid grid-cols-1 gap-4 p-7.5 md:grid-cols-4">
          <div className="col-span-1 bg-white dark:bg-boxdark lg:col-span-1  md:col-span-2 xl:col-span-3">
            <Calendar
              localizer={localizer}
              events={events}
              startAccessor="start"
              endAccessor="end"
              style={{ height: 500 }}
              views={['month']}
              date={currentMonth}
              eventPropGetter={eventStyleGetter}
              className="rbc-calendar"
            />
          </div>
          <ul className="col-span-1 flex flex-col gap-4 md:col-span-2 xl:col-span-1">
            <li className="flex items-center justify-between rounded border border-stroke bg-white p-5 dark:border-strokedark dark:bg-boxdark">
              <div className="flex items-center gap-4">
                <p className="flex h-10 w-10 items-center justify-center rounded-full bg-meta-2">
                  <img src={ai} alt="Logo" />
                </p>
                <p className="text-lg font-semibold text-black dark:text-white">
                  AI Usage
                </p>
              </div>
              <p className="font-semibold text-black dark:text-white">
                {getTotalSubscriptionCount()}
                <span className="ml-0.5 text-sm">days</span>
              </p>
            </li>
            <li className="flex items-center justify-between rounded border border-stroke bg-white p-5 dark:border-strokedark dark:bg-boxdark">
              <div className="flex items-center gap-4">
                <p className="flex h-10 w-10 items-center justify-center rounded-full bg-meta-2">
                  <img src={api} alt="Logo" />
                </p>
                <p className="text-lg font-semibold text-black dark:text-white">
                  API Usage
                </p>
              </div>
              <p className="font-semibold text-black dark:text-white">
                {getTotalApiCallCount()}
                <span className="ml-0.5 text-sm"></span>
              </p>
            </li>
            <li className="flex items-center justify-between rounded border border-stroke bg-white p-5 dark:border-strokedark dark:bg-boxdark">
              <div className="flex items-center gap-4">
                <p className="flex h-10 w-10 items-center justify-center rounded-full bg-meta-2">
                  <img
                    src={facilities}
                    alt="
                  go"
                  />
                </p>
                <p className="text-lg font-semibold text-black dark:text-white">
                  facilities
                </p>
              </div>
              <p className="font-semibold text-black dark:text-white">
                {getTotalIotInstallationCount()}
                <span className="ml-0.5 text-sm"></span>
              </p>
            </li>
            <li className="flex items-center justify-between rounded border border-stroke bg-white p-5 dark:border-strokedark dark:bg-boxdark">
              <div className="flex items-center gap-4">
                <p className="flex h-10 w-10 items-center justify-center rounded-full bg-meta-2">
                  <img src={storage} alt="Logo" />
                </p>
                <p className="text-lg font-semibold text-black dark:text-white">
                  Storage
                </p>
              </div>
              <p className="font-semibold text-black dark:text-white">
                {storageUsageInGB < 0.01
                  ? `${storageUsageInGB.toFixed(3)}`
                  : `${storageUsageInGB.toFixed(2)}`}
                <span className="ml-0.5 text-sm">GB</span>
              </p>
            </li>
          </ul>
        </div>

        <div className="px-7.5 pb-7.5 pt-2.5">
          <h3 className="text-lg font-semibold text-black dark:text-white">
            {loggedInUser?.companyName} / {loggedInUser?.companyType}
          </h3>
          <div className="pt-7.5">
            <table className="w-full leading-loose">
              <tbody className="w-full">
                <tr className="h-10 w-full border-b border-t border-stroke">
                  <td className="w-3/12">AI Usage</td>
                  <td className="hidden w-5/12 md:block">
                    {startOfCurrentMonth} ~ {endOfCurrentMonth}
                  </td>
                  <td className="w-2/12">
                    {getTotalSubscriptionCount()}
                    <span className="ml-0.5">days</span>
                  </td>
                  <td className="w-2/12 text-right">$120.00</td>
                </tr>
                <tr className="h-10 w-full border-b border-stroke">
                  <td className="w-3/12">API Usage</td>
                  <td className="hidden w-5/12 md:block">
                    {startOfCurrentMonth} ~ {endOfCurrentMonth}
                  </td>
                  <td className="w-2/12">
                    {getTotalApiCallCount()}
                    <span className="ml-0.5"></span>
                  </td>
                  <td className="w-2/12 text-right">$120.00</td>
                </tr>
                <tr className="h-10 w-full border-b border-stroke">
                  <td className="w-3/12">facilities</td>
                  <td className="hidden w-5/12 md:block">
                    {startOfCurrentMonth} ~ {endOfCurrentMonth}
                  </td>
                  <td className="w-2/12">
                    {getTotalIotInstallationCount()}
                    <span className="ml-0.5"></span>
                  </td>
                  <td className="w-2/12 text-right">$120.00</td>
                </tr>
                <tr className="h-10 w-full border-b border-stroke">
                  <td className="w-3/12">Storage</td>
                  <td className="hidden w-5/12 md:block">
                    {startOfCurrentMonth} ~ {endOfCurrentMonth}
                  </td>
                  <td className="w-2/12">
                    {storageUsageInGB < 0.01
                      ? `${storageUsageInGB.toFixed(3)}`
                      : `${storageUsageInGB.toFixed(2)}`}
                    <span className="ml-0.5">GB</span>
                  </td>
                  <td className="w-2/12 text-right">$120.00</td>
                </tr>
              </tbody>
            </table>
          </div>

          <table className="w-full leading-loose">
            <tbody className="w-full">
              <tr className="h-10 w-full">
                <td className="hidden w-8/12 md:block"></td>
                <td className="w-2/12">Subtotal</td>
                <td className="w-2/12 text-right">$120.00</td>
              </tr>
              <tr className="h-10 w-full">
                <td className="hidden w-8/12 md:block"></td>
                <td className="w-2/12 border-b border-primary">Duty (+)</td>
                <td className="w-2/12 border-b border-primary text-right">
                  $10.00
                </td>
              </tr>
              <tr className="h-10 w-full font-medium text-primary">
                <td className="hidden w-8/12 md:block"></td>
                <td className="w-2/12">Total</td>
                <td className="w-2/12 text-right">$130.00</td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    </div>
  );
};

export default Subscription;

'Framework > React' 카테고리의 다른 글

[React] React를 위한 상태관리 라이브러리 - Recoil  (1) 2024.09.04
[React] react-toastify custom  (0) 2024.09.04
[React] api 연결  (0) 2024.07.18
[React] 로딩 중  (0) 2024.07.09
[React] 웹 스토리지 이용하기  (0) 2024.07.09

+ Recent posts