diff --git a/apps/timo-web/app/[locale]/(main)/statistics/_components/.gitkeep b/apps/timo-web/app/[locale]/(main)/statistics/_components/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsSidePanel.tsx b/apps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsSidePanel.tsx new file mode 100644 index 00000000..eec7b333 --- /dev/null +++ b/apps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsSidePanel.tsx @@ -0,0 +1,132 @@ +import { TagIcon } from "@repo/timo-design-system/ui"; +import { cn } from "@repo/timo-design-system/utils"; + +import type { + StatisticsDayDetail, + StatisticsMonthSummary, +} from "@/app/[locale]/(main)/statistics/_types/statistics"; + +import { SummaryTimeBlock } from "@/app/[locale]/(main)/statistics/_components/SummaryTimeBlock"; +import { + formatStatisticsClockText, + formatStatisticsHourText, +} from "@/app/[locale]/(main)/statistics/_utils/formatStatisticsTime"; + +interface StatisticsMonthSidePanelProps { + variant: "month"; + summary: StatisticsMonthSummary; +} + +interface StatisticsDaySidePanelProps { + variant: "day"; + detail: StatisticsDayDetail; +} + +type StatisticsSidePanelProps = + | StatisticsMonthSidePanelProps + | StatisticsDaySidePanelProps; + +const SIDE_PANEL_CLASS_NAME = + "border-timo-gray-500 min-h-full w-76 border-l text-timo-black"; + +const getDiffLabel = (diffMinutes: number) => { + if (diffMinutes > 0) return "+초과"; + if (diffMinutes < 0) return "-단축"; + return ""; +}; + +const getDiffClassName = (diffMinutes: number) => { + if (diffMinutes > 0) return "text-timo-red"; + if (diffMinutes < 0) return "text-timo-blue-300"; + return "invisible"; +}; + +export const StatisticsSidePanel = (props: StatisticsSidePanelProps) => { + if (props.variant === "month") { + const { summary } = props; + + return ( + + ); + } + + const { detail } = props; + + return ( + + ); +}; diff --git a/apps/timo-web/app/[locale]/(main)/statistics/_components/SummaryTimeBlock.tsx b/apps/timo-web/app/[locale]/(main)/statistics/_components/SummaryTimeBlock.tsx new file mode 100644 index 00000000..ead9033a --- /dev/null +++ b/apps/timo-web/app/[locale]/(main)/statistics/_components/SummaryTimeBlock.tsx @@ -0,0 +1,15 @@ +import { formatStatisticsHourText } from "@/app/[locale]/(main)/statistics/_utils/formatStatisticsTime"; + +interface SummaryTimeBlockProps { + label: string; + minutes: number; +} + +export const SummaryTimeBlock = ({ label, minutes }: SummaryTimeBlockProps) => ( +
+

{label}

+ + {formatStatisticsHourText(minutes)} + +
+); diff --git a/apps/timo-web/app/[locale]/(main)/statistics/_types/statistics.ts b/apps/timo-web/app/[locale]/(main)/statistics/_types/statistics.ts new file mode 100644 index 00000000..febf963e --- /dev/null +++ b/apps/timo-web/app/[locale]/(main)/statistics/_types/statistics.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; + +export const statisticsMonthSummarySchema = z.object({ + totalRecordMinutes: z.number(), + activeDayCount: z.number(), + averageRecordedMinutes: z.number(), + completedTodoCount: z.number(), + totalTodoCount: z.number(), +}); + +export const statisticsTodoRecordSchema = z.object({ + todoId: z.number(), + title: z.string(), + actualTimeMinutes: z.number(), + estimatedTimeMinutes: z.number(), + tagName: z.string(), +}); + +export const statisticsDayDetailSchema = z.object({ + date: z.string(), + totalRecordMinutes: z.number(), + todos: z.array(statisticsTodoRecordSchema), +}); + +export type StatisticsMonthSummary = z.infer< + typeof statisticsMonthSummarySchema +>; + +export type StatisticsDayDetail = z.infer; + +export type StatisticsTodoRecord = z.infer; diff --git a/apps/timo-web/app/[locale]/(main)/statistics/_utils/formatStatisticsTime.ts b/apps/timo-web/app/[locale]/(main)/statistics/_utils/formatStatisticsTime.ts new file mode 100644 index 00000000..04e9726b --- /dev/null +++ b/apps/timo-web/app/[locale]/(main)/statistics/_utils/formatStatisticsTime.ts @@ -0,0 +1,42 @@ +/** + * 분 단위 기록 시간을 통계 요약 영역의 시간 텍스트로 변환합니다. + * + * @param minutes - 분 단위 기록 시간 + * @returns 화면에 표시할 시간 텍스트 + * + * @example + * formatStatisticsHourText(260); // "4h 20m" + * formatStatisticsHourText(120); // "2h" + * formatStatisticsHourText(30); // "30m" + */ +export const formatStatisticsHourText = (minutes: number) => { + const hours = Math.floor(minutes / 60); + const restMinutes = minutes % 60; + + if (hours === 0) { + return `${restMinutes}m`; + } + + if (restMinutes === 0) { + return `${hours}h`; + } + + return `${hours}h ${restMinutes}m`; +}; + +/** + * 분 단위 기록 시간을 투두별 실제/계획 시간 표기용 시계 텍스트로 변환합니다. + * + * @param minutes - 분 단위 기록 시간 + * @returns 시:분 형태의 시간 텍스트 + * + * @example + * formatStatisticsClockText(90); // "1:30" + * formatStatisticsClockText(60); // "1:00" + */ +export const formatStatisticsClockText = (minutes: number) => { + const hours = Math.floor(minutes / 60); + const restMinutes = minutes % 60; + + return `${hours}:${String(restMinutes).padStart(2, "0")}`; +};