072DATA

๋‚ ์งœ ์ฒ˜๋ฆฌ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…(Timezone ์ด์Šˆ) ๋ณธ๋ฌธ

Anything/์˜ค๋ฅ˜ ํ•ด๊ฒฐ(error)

๋‚ ์งœ ์ฒ˜๋ฆฌ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…(Timezone ์ด์Šˆ)

0720 2024. 11. 7. 03:11

๐Ÿ” ๋ฌธ์ œ ์ƒํ™ฉ

์‚ฌ์šฉ์ž๊ฐ€ ํ•˜๋ฃจ์— ํ•œ ๋ฒˆ๋งŒ ์ฑŒ๋ฆฐ์ง€๋ฅผ ์ œ์ถœํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๋Š”๋ฐ ์ค‘๋ณต ์ œ์ถœ์ด ๊ฐ€๋Šฅํ•œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

 

 

๋ฐœ์ƒํ•œ ๊ตฌ์ฒด์ ์ธ ๋ฌธ์ œ์ 

 

  1. ๊ฐ™์€ ๋‚ ์งœ์— ์—ฌ๋Ÿฌ ๋ฒˆ ์ฑŒ๋ฆฐ์ง€ ์ œ์ถœ์ด ๊ฐ€๋Šฅ
  2. ์‹œ๊ฐ„๋Œ€(Timezone) ์ฒ˜๋ฆฌ๋กœ ์ธํ•œ ๋‚ ์งœ ๊ณ„์‚ฐ ์˜ค๋ฅ˜
  3. DB์˜ timestamptz ํƒ€์ž…๊ณผ ํ”„๋ก ํŠธ์—”๋“œ์˜ ๋‚ ์งœ ์ฒ˜๋ฆฌ ๋ถˆ์ผ์น˜
  4. ์บ˜๋ฆฐ๋” ๋ฐ์ดํ„ฐ ํ‘œ์‹œ ๋ถˆ์ผ์น˜
    • ๋™์ผํ•œ ์‹œ๊ฐ„๋Œ€ ์ด์Šˆ๋กœ ์ธํ•ด ์บ˜๋ฆฐ๋”์— ์ฑŒ๋ฆฐ์ง€ ๋ฐ์ดํ„ฐ๊ฐ€ ์ž˜๋ชป๋œ ๋‚ ์งœ์— ํ‘œ์‹œ๋˜๊ฑฐ๋‚˜ ๋ˆ„๋ฝ๋˜๋Š” ํ˜„์ƒ ๋ฐœ์ƒ
    • ์›”๋ณ„ ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ ๊ณ„์‚ฐ ์‹œ ๋‚ ์งœ ๊ธฐ์ค€์ด ๋ถˆ๋ช…ํ™•ํ•œ ๋ฌธ์ œ

 

 ๋ฌธ์ œ์˜ ์›์ธ

 

1. ๋ณต์žกํ•œ ์‹œ๊ฐ„ ๋ฒ”์œ„ ์ฟผ๋ฆฌ ์ ‘๊ทผ

์ดˆ๊ธฐ ํ•ด๊ฒฐ ์‹œ๋„์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ณต์žกํ•œ ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค...

const { data: todayChallenge } = await supabase
  .from("challenges")
  .select("created_at")
  .eq("user_id", params.userId)
  .gte("created_at", today.utc().format())
  .lt("created_at", tomorrow.utc().format())

 

  • ์‹œ๊ฐ„๋Œ€ ๋ณ€ํ™˜ ๊ณผ์ •์—์„œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ๊ฐ€๋Šฅ์„ฑ
  • ์ฟผ๋ฆฌ ์กฐ๊ฑด์ด ๋ณต์žกํ•ด์ ธ ์˜๋„์น˜ ์•Š์€ ๊ฒฐ๊ณผ ๋ฐœ์ƒ
  • UTC/KST ๋ณ€ํ™˜ ๊ณผ์ •์—์„œ ๊ฒฝ๊ณ„ ์ผ€์ด์Šค ์ฒ˜๋ฆฌ ๋ฏธํก

 

2. ์‹œ๊ฐ„๋Œ€(Timezone) ์ฒ˜๋ฆฌ ์ด์Šˆ

// ์ž˜๋ชป๋œ ์ ‘๊ทผ
const today = dayjs().startOf("day");
const tomorrow = today.add(1, "day");

 

  • ์„œ๋ฒ„์˜ ์‹œ๊ฐ„๋Œ€, DB์˜ ์‹œ๊ฐ„๋Œ€, ํด๋ผ์ด์–ธํŠธ์˜ ์‹œ๊ฐ„๋Œ€๊ฐ€ ๊ฐ๊ฐ ๋‹ฌ๋ผ ํ˜ผ์„  ๋ฐœ์ƒ
  • ๋‚ ์งœ ๋ณ€ํ™˜ ์‹œ์ ์ด ๋ถˆ๋ช…ํ™•
  • KST(ํ•œ๊ตญ ์‹œ๊ฐ„)๋ฅผ ๊ณ ๋ คํ•˜์ง€ ์•Š์€ ๋‚ ์งœ ๊ณ„์‚ฐ

 

3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํƒ€์ž… ๋ถˆ์ผ์น˜

created_at TIMESTAMPTZ DEFAULT now()

 

  • DB๋Š” timestamptz ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜์ง€๋งŒ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋Š” ๋‹จ์ˆœ ๋ฌธ์ž์—ด๋กœ ์ฒ˜๋ฆฌ
  • ์‹œ๊ฐ„๋Œ€ ์ •๋ณด๊ฐ€ ์ €์žฅ/์กฐํšŒ ๊ณผ์ •์—์„œ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์Œ

 

4. ์บ˜๋ฆฐ๋” ๊ตฌํ˜„์—์„œ์˜ ์‹œ๊ฐ„๋Œ€ ์ฒ˜๋ฆฌ

// ๊ธฐ์กด ์บ˜๋ฆฐ๋” ๊ตฌํ˜„์˜ ๋ฌธ์ œ์ 
const startOfMonth = currentMonth
  .startOf("month")
  .toISOString();  // UTC ๋ณ€ํ™˜์œผ๋กœ ์ธํ•œ ๋‚ ์งœ ๋ถˆ์ผ์น˜

// DB ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์‹œ ์‹œ๊ฐ„๋Œ€ ๊ณ ๋ ค ๋ถ€์žฌ
challengeData.forEach((item) => {
  const date = item.created_at.split("T")[0];  // ๋‹จ์ˆœ ๋ฌธ์ž์—ด ๋ถ„๋ฆฌ๋กœ ์ธํ•œ ์˜ค๋ฅ˜
});

 

โœ… ํ•ด๊ฒฐ ๊ณผ์ •

 

1. ์ ‘๊ทผ ๋ฐฉ์‹ ๋‹จ์ˆœํ™”

 

๋ณต์žกํ•œ ์‹œ๊ฐ„ ๋ฒ”์œ„ ์ฟผ๋ฆฌ ๋Œ€์‹  ๊ฐ€์žฅ ์ตœ๊ทผ ์ฑŒ๋ฆฐ์ง€์˜ ๋‚ ์งœ๋งŒ ๋น„๊ตํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค

const { data: latestChallenge } = await supabase
  .from("challenges")
  .select("created_at")
  .eq("user_id", params.userId)
  .order("created_at", { ascending: false })
  .limit(1)
  .single();

 

2. ๋ช…ํ™•ํ•œ ์‹œ๊ฐ„๋Œ€ ์ฒ˜๋ฆฌ

 

dayjs์˜ timezone ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ํ™œ์šฉํ•˜์—ฌ ๋ช…์‹œ์ ์ธ ์‹œ๊ฐ„๋Œ€ ์ฒ˜๋ฆฌ

const lastChallengeDate = dayjs(latestChallenge.created_at)
  .tz("Asia/Seoul")
  .format("YYYY-MM-DD");

const todayDate = dayjs()
  .tz("Asia/Seoul")
  .format("YYYY-MM-DD");

 

3. ๋‚ ์งœ ๋น„๊ต ๋กœ์ง ๋‹จ์ˆœํ™”

 

๋‚ ์งœ ๋ฌธ์ž์—ด ๋น„๊ต๋กœ ๋‹จ์ˆœํ™”ํ•˜์—ฌ ์‹œ๊ฐ„๋Œ€ ๊ด€๋ จ ๋ณต์žก์„ฑ ์ œ๊ฑฐ

if (lastChallengeDate === todayDate) {
  throw new Error("์ด๋ฏธ ์˜ค๋Š˜์˜ ์ฑŒ๋ฆฐ์ง€๋ฅผ ์ œ์ถœํ•˜์…จ์Šต๋‹ˆ๋‹ค.");
}

 

4. ์บ˜๋ฆฐ๋” ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๊ฐœ์„ 

 

// ๊ฐœ์„ ๋œ ์บ˜๋ฆฐ๋” ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
const fetchMonthlyData = async () => {
  if (!user.id) return;

  try {
    const startOfMonth = currentMonth
      .tz("Asia/Seoul")
      .startOf("month")
      .format("YYYY-MM-DD");

    const endOfMonth = currentMonth
      .tz("Asia/Seoul")
      .endOf("month")
      .format("YYYY-MM-DD");

    const challengeData = await calendarApi.getByDateRange(
      startOfMonth,
      endOfMonth,
      user.id
    );

    const dataByDate: MonthlyData = {};
    challengeData.forEach((item) => {
      const date = dayjs(item.created_at)
        .tz("Asia/Seoul")
        .format("YYYY-MM-DD");
      
      dataByDate[date] = {
        co2: item.co2 || 0,
        point: item.point || 0,
        challenge_count: 1
      };
    });

    // ์›”๊ฐ„ ํ†ต๊ณ„ ๊ณ„์‚ฐ
    const monthlyStats = {
      totalCo2: challengeData.reduce((sum, item) => sum + (item.co2 || 0), 0),
      totalPoints: challengeData.reduce((sum, item) => sum + (item.point || 0), 0),
      totalChallenges: challengeData.length
    };

    setMonthlyData(dataByDate);
    setMonthlyStats(monthlyStats);
  } catch (error) {
    console.error("์บ˜๋ฆฐ๋” ๋ฐ์ดํ„ฐ ์š”์ฒญ ์‹คํŒจ:", error);
  }
};

 

ํ•ด๊ฒฐ๋‚ด์šฉ ํŠน์ง•

 

  1. ๋‹จ์ˆœ์„ฑ
    • ๋ณต์žกํ•œ ์‹œ๊ฐ„ ๋ฒ”์œ„ ์ฟผ๋ฆฌ ์ œ๊ฑฐ
    • ๋ช…ํ™•ํ•œ ๋‚ ์งœ ๋น„๊ต ๋กœ์ง
    • ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์šฉ์ดํ•œ ์ฝ”๋“œ ๊ตฌ์กฐ
  2. ์‹ ๋ขฐ์„ฑ
    • ์‹œ๊ฐ„๋Œ€ ์ฒ˜๋ฆฌ์˜ ์ผ๊ด€์„ฑ ํ™•๋ณด
    • ๊ฒฝ๊ณ„ ์ผ€์ด์Šค์—์„œ๋„ ์•ˆ์ •์ ์ธ ๋™์ž‘
    • ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ๋™์ž‘ ๋ฐฉ์‹
  3. ์„ฑ๋Šฅ
    • ์ตœ์†Œํ•œ์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ
    • ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ์ตœ์†Œํ™”
    • ๋‹จ์ˆœํ•œ ๋ฌธ์ž์—ด ๋น„๊ต๋กœ ์ฒ˜๋ฆฌ ์†๋„ ํ–ฅ์ƒ

 

 ํ•™์Šต๋œ ์ 

 

  1. ์‹œ๊ฐ„๋Œ€ ์ฒ˜๋ฆฌ
    • ํ•ญ์ƒ ๋ช…์‹œ์ ์ธ ์‹œ๊ฐ„๋Œ€ ์ง€์ •
    • ์ผ๊ด€๋œ ์‹œ๊ฐ„๋Œ€ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ์‚ฌ์šฉ
    • ์‹œ๊ฐ„๋Œ€ ๋ณ€ํ™˜์€ ์ตœ๋Œ€ํ•œ ๋Šฆ๊ฒŒ ์ˆ˜ํ–‰
  2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ
    • ๋ณต์žกํ•œ ์‹œ๊ฐ„ ๊ด€๋ จ ์ฟผ๋ฆฌ๋Š” ๊ฐ€๋Šฅํ•œ ํ”ผํ•˜๊ธฐ
    • ๋ช…ํ™•ํ•œ ์ธ๋ฑ์Šค ํ™œ์šฉ์ด ๊ฐ€๋Šฅํ•œ ์ฟผ๋ฆฌ ์„ค๊ณ„
    • ์ตœ์†Œํ•œ์˜ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ์กฐํšŒ
  3. ์—๋Ÿฌ ์ฒ˜๋ฆฌ
    • ๊ตฌ์ฒด์ ์ธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ œ๊ณต
    • ์˜ˆ์™ธ ์ƒํ™ฉ์— ๋Œ€ํ•œ ๋ช…ํ™•ํ•œ ์ฒ˜๋ฆฌ
    • ๋กœ๊น…์„ ํ†ตํ•œ ๋ฌธ์ œ ์ถ”์  ์šฉ์ด์„ฑ ํ™•๋ณด

 

 ์ถ”๊ฐ€ ๊ณ ๋ ค์‚ฌํ•ญ

  1. ํ™•์žฅ์„ฑ
    • ๋‹ค๋ฅธ ์‹œ๊ฐ„๋Œ€ ์ง€์› ํ•„์š”์‹œ ์‰ฝ๊ฒŒ ์ˆ˜์ • ๊ฐ€๋Šฅ
    • ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ณ€๊ฒฝ์— ์œ ์—ฐํ•˜๊ฒŒ ๋Œ€์‘ ๊ฐ€๋Šฅ
    • ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ ํ™•๋ณด
  2. ๋ชจ๋‹ˆํ„ฐ๋ง
    • ๋‚ ์งœ ๊ด€๋ จ ์˜ค๋ฅ˜ ๋ชจ๋‹ˆํ„ฐ๋ง ์ฒด๊ณ„ ๊ตฌ์ถ•
    • ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ ์ˆ˜์ง‘ ๋ฐ ๋ถ„์„
    • ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง ํฌ์ธํŠธ ์„ค์ •

 

๐Ÿ“š ์ฐธ๊ณ  ์ž๋ฃŒ

 

8.5. Date/Time Types

8.5. Date/Time Types # 8.5.1. Date/Time Input 8.5.2. Date/Time Output 8.5.3. Time Zones 8.5.4. Interval Input 8.5.5. Interval Output PostgreSQL supports โ€ฆ

www.postgresql.org

 

 

 

Database configuration | Supabase Docs

Updating the default configuration for your Postgres database.

supabase.com

 

 

๊ฒฐ๋ก 

๋ณต์žกํ•œ ์‹œ๊ฐ„ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ๋‹จ์ˆœํ™”ํ•˜๊ณ  ๋ช…ํ™•ํ•œ ์‹œ๊ฐ„๋Œ€ ์ฒ˜๋ฆฌ๋ฅผ ํ†ตํ•ด ์•ˆ์ •์ ์ธ ์ฑŒ๋ฆฐ์ง€ ์ œ์ถœ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ–ˆ์Šต๋‹ˆ๋‹ค 

์ฝ”๋“œ์˜ ๋ณต์žก์„ฑ์„ ์ค„์ด๊ณ  ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ค๋Š” ๋™์‹œ์— ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ ใ……์ผฏ์Šต๋‹ˆ๋‹ค

์ด๋Ÿฌํ•œ ๊ฒฝํ—˜์„ ํ†ตํ•ด ์‹œ๊ฐ„/๋‚ ์งœ ๊ด€๋ จ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์‹œ ์ดˆ๊ธฐ๋ถ€ํ„ฐ ์‹ ์ค‘ํ•œ ์„ค๊ณ„์™€

๋ช…ํ™•ํ•œ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ์„ ํƒ์˜ ์ค‘์š”์„ฑ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค!!!