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")}`;
+};