072DATA

์บ˜๋ฆฐ๋”์— ๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™”ํ•˜๊ธฐ - ํŠน์ • ๊ธฐ๊ฐ„ ์กฐํšŒ ๋ฐ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ ๋ณธ๋ฌธ

FrontEnd/HTML, CSS, JavaScript

์บ˜๋ฆฐ๋”์— ๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™”ํ•˜๊ธฐ - ํŠน์ • ๊ธฐ๊ฐ„ ์กฐํšŒ ๋ฐ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ

0720 2024. 11. 1. 02:00

๐Ÿ” 1. Supabase API ๊ตฌํ˜„ - ํŠน์ • ๊ธฐ๊ฐ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ

export const calendarApi = {
  getByDateRange: async (startDate: string, endDate: string, userId: string) => {
    try {
      // ์ง€์ •๋œ ์‹œ์ž‘ ๋ฐ ์ข…๋ฃŒ ๋‚ ์งœ์˜ ์‹œ๊ฐ„ ๋ฒ”์œ„๋ฅผ ์„ค์ •
      const startDateTime = `${startDate}T00:00:00.000Z`; // ์‹œ์ž‘ ๋‚ ์งœ์˜ 00์‹œ ์„ค์ •
      const endDateTime = `${endDate}T23:59:59.999Z`; // ์ข…๋ฃŒ ๋‚ ์งœ์˜ 23์‹œ 59๋ถ„ ์„ค์ •

      const { data, error } = await supabase
        .from("challenges")
        .select("*")                  // ๋ชจ๋“  ์ปฌ๋Ÿผ์„ ์„ ํƒ
        .eq("user_id", userId)         // ์‚ฌ์šฉ์ž์˜ ID์— ํ•ด๋‹นํ•˜๋Š” ํ–‰์„ ํ•„ํ„ฐ๋ง
        .gte("created_at", startDateTime) // ์‹œ์ž‘ ๋‚ ์งœ ์ดํ›„์˜ ๋ฐ์ดํ„ฐ๋งŒ ์„ ํƒ
        .lte("created_at", endDateTime)   // ์ข…๋ฃŒ ๋‚ ์งœ ์ด์ „์˜ ๋ฐ์ดํ„ฐ๋งŒ ์„ ํƒ
        .order("created_at", { ascending: true }); // ๋‚ ์งœ๋ณ„๋กœ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ

      if (error) throw error; // ์˜ค๋ฅ˜๊ฐ€ ์žˆ์œผ๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ
      return data; // ํ•„ํ„ฐ๋ง๋œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜
    } catch (error) {
      console.error("๊ธฐ๊ฐ„๋ณ„ ์ฑŒ๋ฆฐ์ง€ ์กฐํšŒ ์˜ค๋ฅ˜:", error);
      throw error; // ํ˜ธ์ถœํ•œ ํ•จ์ˆ˜๋กœ ์˜ˆ์™ธ ์ „๋‹ฌ
    }
  }
};

 

calendarApi.getByDateRange ํ•จ์ˆ˜๋Š” Supabase ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ

์‚ฌ์šฉ์ž ์ง€์ • ๋‚ ์งœ ๋ฒ”์œ„์— ํ•ด๋‹นํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ํ•„ํ„ฐ๋งํ•˜์—ฌ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

 

startDate, endDate, userId๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›์•„์„œ

์ฃผ์–ด์ง„ ๊ธฐ๊ฐ„ ๋‚ด์— ์ƒ์„ฑ๋œ ์‚ฌ์šฉ์ž์˜ ์ฑŒ๋ฆฐ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.

 

๐Ÿ“Š 2. ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ - ๋‚ ์งœ๋ณ„ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ

const fetchMonthlyData = async () => {
  if (!userId) return; // userId๊ฐ€ ์—†์œผ๋ฉด ํ•จ์ˆ˜ ์ข…๋ฃŒ

  try {
    // ํ˜„์žฌ ์›”์˜ ์‹œ์ž‘ ๋ฐ ์ข…๋ฃŒ ๋‚ ์งœ๋ฅผ YYYY-MM-DD ํ˜•์‹์œผ๋กœ ์„ค์ •
    const startOfMonth = currentMonth.startOf("month").format("YYYY-MM-DD");
    const endOfMonth = currentMonth.endOf("month").format("YYYY-MM-DD");

    // ์ง€์ •๋œ ์›”์— ํ•ด๋‹นํ•˜๋Š” ์ฑŒ๋ฆฐ์ง€ ๋ฐ์ดํ„ฐ ์กฐํšŒ
    const challengeData = await calendarApi.getByDateRange(
      startOfMonth,
      endOfMonth,
      userId
    );

    // ๋ฐ์ดํ„ฐ๋ฅผ ๋‚ ์งœ๋ณ„๋กœ ์ •๋ฆฌ
    const dataByDate: MonthlyData = {};
    challengeData.forEach((item) => {
      const date = item.created_at.split("T")[0];  // ํƒ€์ž„์Šคํƒฌํ”„์—์„œ ๋‚ ์งœ๋งŒ ์ถ”์ถœ (YYYY-MM-DD ํ˜•์‹)
      // ๋‚ ์งœ๋ณ„๋กœ co2, point ๊ฐ’์„ ๋ˆ„์  ๋˜๋Š” ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ €์žฅ
      dataByDate[date] = {
        co2: item.co2 || 0,
        point: item.point || 0,
        challenge_count: 1
      };
    });

    setMonthlyData(dataByDate); // ์ƒํƒœ ์—…๋ฐ์ดํŠธ
  } catch (error) {
    console.error("์บ˜๋ฆฐ๋” ๋ฐ์ดํ„ฐ ์š”์ฒญ ์‹คํŒจ:", error);
  }
};

 

fetchMonthlyData ํ•จ์ˆ˜๋Š” API์—์„œ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ๋‚ ์งœ๋ณ„๋กœ ์ •๋ฆฌํ•˜์—ฌ

์บ˜๋ฆฐ๋”์— ํ‘œ์‹œํ•  ์ˆ˜ ๋„๋ก ํ•ด์คฌ์Šต๋‹ˆ๋‹ค

 

์›”์˜ ์‹œ์ž‘๊ณผ ๋ ๋‚ ์งœ๋ฅผ ์ •์˜ํ•˜์—ฌ ํ•œ ๋‹ฌ์น˜ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•œ ๋’ค์—

๋ฐ์ดํ„ฐ์˜ created_at ํƒ€์ž„์Šคํƒฌํ”„์—์„œ ๋‚ ์งœ๋งŒ ์ถ”์ถœํ•˜์—ฌ ๋‚ ์งœ๋ณ„๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

 

๐Ÿ“… 3. UI ๊ตฌํ˜„ - ๋‚ ์งœ๋ณ„ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ

{week.map(({ day, isInCurrentMonth }) => {
  const dateStr = day.format("YYYY-MM-DD"); // ๋‚ ์งœ๋ฅผ YYYY-MM-DD ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜
  const dayData = monthlyData[dateStr]; // ํ•ด๋‹น ๋‚ ์งœ์˜ ๋ฐ์ดํ„ฐ
  const today = new Date();
  const isToday = day.format("YYYY-MM-DD") === dayjs(today).format("YYYY-MM-DD");

  return (
    <div className={`flex flex-col items-center p-4 ${!isInCurrentMonth ? "text-gray-400" : ""}`}>
      <div className={`
        w-10 h-10 font-medium flex items-center justify-center
        ${isToday ? "bg-red-500" : dayData ? "bg-gray-400" : ""}
        ${(isToday || dayData) && "rounded-full text-white"}
      `}>
        {day.format("D")}
      </div>
      {dayData && (
        <>
          <p className="text-sm font-medium">{dayData.co2.toFixed(2)}kg</p>
          <p className="text-sm font-medium">+{dayData.point}P</p>
        </>
      )}
    </div>
  );
})}

 

์ฃผ์–ด์ง„ ์ฃผ์˜ ๋‚ ์งœ๋“ค์„ ๋ฐ˜๋ณตํ•˜๋ฉด์„œ monthlyData์—

์ €์žฅ๋œ ๋ฐ์ดํ„ฐ์™€ ๋งค์นญํ•˜์—ฌ ์บ˜๋ฆฐ๋” ์…€์— ํ‘œ์‹œํ–ˆ์œผ๋ฉฐ

 

์กฐ๊ฑด์— ๋”ฐ๋ผ ์˜ค๋Š˜ ๋‚ ์งœ์™€ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š” ๋‚ ์งœ์— ์Šคํƒ€์ผ๋ง์„ ์ถ”๊ฐ€ํ•ด

์‚ฌ์šฉ์ž๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ์ง๊ด€์ ์œผ๋กœ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์คฌ์Šต๋‹ˆ๋‹ค

 

์‚ฌ์šฉํ•œ ํƒ€์ž…

interface MonthlyData {
  [key: string]: {
    co2: number;    
    point: number;  
    challenge_count: number; 
  };
}

interface MonthlyStats {
  totalCo2: number;
  totalPoints: number;
  totalChallenges: number;
}

 

 

โœจ ๊ตฌํ˜„ ํฌ์ธํŠธ

  1. ๋ฐ์ดํ„ฐ ์ •๋ฆฌ: ํƒ€์ž„์Šคํƒฌํ”„์—์„œ ๋‚ ์งœ ๋ถ€๋ถ„๋งŒ ๊ฐ€์ ธ์™€ ์บ˜๋ฆฐ๋”์—์„œ ํ‚ค๋กœ ์‚ฌ์šฉ
  2. ์กฐ๊ฑด๋ถ€ ์Šคํƒ€์ผ๋ง: ์˜ค๋Š˜ ๋‚ ์งœ์™€ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š” ๋‚ ์งœ์— ์Šคํƒ€์ผ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์‰ฝ๊ฒŒ ๊ตฌ๋ณ„ ๊ฐ€๋Šฅ
  3. ์›”๊ฐ„ ํ†ต๊ณ„ ๊ณ„์‚ฐ: ํ•œ ๋‹ฌ ๋™์•ˆ์˜ ์ด COโ‚‚ ์ ˆ๊ฐ๋Ÿ‰, ํฌ์ธํŠธ, ์ฐธ์—ฌ ํšŸ์ˆ˜๋ฅผ ์กฐํšŒ ๊ฐ€๋Šฅ

๐Ÿ’ก ๋ฐฐ์šด ์ 

  1. Supabase์—์„œ ๋‚ ์งœ๋กœ ํ•„ํ„ฐ๋งํ•  ๋•Œ๋Š” ์‹œ๊ฐ„ ๋ฒ”์œ„๋ฅผ ์ •ํ™•ํžˆ ์„ค์ •ํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•˜๋‹ค๋Š” ์ .
  2. ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ๋Š” ์‹œ๊ฐ„๋Œ€ ์ฐจ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด UTC ํ˜•์‹์„ ์‚ฌ์šฉํ•˜๊ธฐ
  3. ์บ˜๋ฆฐ๋”์—์„œ ๋‚ ์งœ๋ณ„ ๋ฐ์ดํ„ฐ๋ฅผ ๋งคํ•‘ํ•  ๋•Œ ์ผ๊ด€๋œ ๋‚ ์งœ ํ˜•์‹์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ค‘์š”

 

 

 

๋งˆ์น˜๋ฉฐ

 

์›ํ•˜๋Š” ๊ธฐ๊ฐ„์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์บ˜๋ฆฐ๋”์— ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๊ฒŒ ๊ตฌํ˜„์ด ์™„๋ฃŒ๋˜์—ˆ๊ณ 

์•ž์œผ๋กœ๋Š” ํ•„์š”ํ•˜๋‹ค๋ฉด ์• ๋‹ˆ๋ฉ”์ด์…˜์ด๋‚˜ ์ƒํ˜ธ์ž‘์šฉ์„ ์ถ”๊ฐ€ํ• ๋“ฏ ํ•˜๋„ค์šฅ