From a0d9c1c494c401d32a87997d24aff919b06de6f8 Mon Sep 17 00:00:00 2001 From: yumin-kim4757 Date: Sat, 11 Jul 2026 06:05:39 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat(web):=20=ED=86=B5=EA=B3=84=20=EC=BA=98?= =?UTF-8?q?=EB=A6=B0=EB=8D=94=EC=99=80=20=EC=82=AC=EC=9D=B4=EB=93=9C=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=20=EC=97=B0=EA=B2=B0=20(#142)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_containers/StatisticsContainer.tsx | 61 ++++++++++++++++--- .../_containers/StatisticsPageContainer.tsx | 24 -------- 2 files changed, 53 insertions(+), 32 deletions(-) delete mode 100644 apps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsPageContainer.tsx diff --git a/apps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsContainer.tsx b/apps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsContainer.tsx index e926fcf..1a5cc84 100644 --- a/apps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsContainer.tsx +++ b/apps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsContainer.tsx @@ -1,24 +1,69 @@ "use client"; +import { useLocale } from "next-intl"; import { useState } from "react"; import { StatisticsCalendar } from "@/app/[locale]/(main)/statistics/_components/StatisticsCalendar"; +import { StatisticsSidePanel } from "@/app/[locale]/(main)/statistics/_components/StatisticsSidePanel"; import { StatisticsHeaderContainer } from "@/app/[locale]/(main)/statistics/_containers/StatisticsHeaderContainer"; -import { MOCK_STATISTICS_CALENDAR } from "@/app/[locale]/(main)/statistics/_mocks/statistics-calendar"; +import { + MOCK_STATISTICS_CALENDAR, + MOCK_STATISTICS_DAY_DETAILS, + MOCK_STATISTICS_MONTH_SUMMARY, +} from "@/app/[locale]/(main)/statistics/_mocks/statistics-calendar"; +import { formatStatisticsSidePanelDate } from "@/app/[locale]/(main)/statistics/_utils/format-statistics-date"; +import { formatDateKey } from "@/app/[locale]/(main)/statistics/_utils/statistics-calendar"; + +type StatisticsPanelMode = "month" | "day"; export const StatisticsContainer = () => { + const locale = useLocale(); const [currentMonth, setCurrentMonth] = useState(() => new Date()); + const [selectedDate, setSelectedDate] = useState( + () => new Date(MOCK_STATISTICS_CALENDAR.today), + ); + const [panelMode, setPanelMode] = useState("month"); + const selectedDateKey = formatDateKey(selectedDate); + const selectedDetailBase = MOCK_STATISTICS_DAY_DETAILS[selectedDateKey] ?? { + date: selectedDateKey, + totalRecordMinutes: 0, + todos: [], + }; + const selectedDetail = { + ...selectedDetailBase, + date: formatStatisticsSidePanelDate(selectedDate, locale), + }; + const handleChangeMonth: typeof setCurrentMonth = (value) => { + setCurrentMonth(value); + setPanelMode("month"); + }; + const handleSelectDate = (date: Date) => { + setSelectedDate(date); + setPanelMode("day"); + }; return ( - <> +
- - +
+ + {panelMode === "month" ? ( + + ) : ( + + )} +
+
); }; diff --git a/apps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsPageContainer.tsx b/apps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsPageContainer.tsx deleted file mode 100644 index e926fcf..0000000 --- a/apps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsPageContainer.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -import { useState } from "react"; - -import { StatisticsCalendar } from "@/app/[locale]/(main)/statistics/_components/StatisticsCalendar"; -import { StatisticsHeaderContainer } from "@/app/[locale]/(main)/statistics/_containers/StatisticsHeaderContainer"; -import { MOCK_STATISTICS_CALENDAR } from "@/app/[locale]/(main)/statistics/_mocks/statistics-calendar"; - -export const StatisticsContainer = () => { - const [currentMonth, setCurrentMonth] = useState(() => new Date()); - - return ( - <> - - - - ); -}; From 93a2e8f8c9ee6223ce0bdf58057fa0f78521bd02 Mon Sep 17 00:00:00 2001 From: yumin-kim4757 Date: Sat, 11 Jul 2026 06:05:56 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat(web):=20=ED=86=B5=EA=B3=84=20=EC=BA=98?= =?UTF-8?q?=EB=A6=B0=EB=8D=94=20=EB=82=A0=EC=A7=9C=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=B6=94=EA=B0=80=20(#142)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/StatisticsCalendar.tsx | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/apps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsCalendar.tsx b/apps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsCalendar.tsx index 3ed0fff..397efb3 100644 --- a/apps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsCalendar.tsx +++ b/apps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsCalendar.tsx @@ -1,3 +1,5 @@ +"use client"; + import { StatisticsClockEmptyIcon, StatisticsClockFilledIcon, @@ -5,6 +7,7 @@ import { StatisticsClockOutlineIcon, } from "@repo/timo-design-system/icons"; import { cn } from "@repo/timo-design-system/utils"; +import { useLocale } from "next-intl"; import type { StatisticsCalendarResponse } from "@/app/[locale]/(main)/statistics/_types/statistics"; @@ -52,27 +55,32 @@ const WEEKDAYS = [ interface StatisticsCalendarProps { currentMonth: Date; + selectedDate: Date; + onSelectDate: (date: Date) => void; calendarData: StatisticsCalendarResponse; } export const StatisticsCalendar = ({ currentMonth, + selectedDate, + onSelectDate, calendarData, }: StatisticsCalendarProps) => { + const locale = useLocale(); const today = new Date(calendarData.today); const calendarDates = getCalendarDates(currentMonth); const firstDayOffset = getFirstDayOffset(currentMonth); const completionRateByDate = new Map( calendarData.days.map(({ date, completionRate }) => [date, completionRate]), ); - const todayLabel = formatStatisticsCalendarDate(today); + const todayLabel = formatStatisticsCalendarDate(today, locale); return (

- {formatStatisticsMonth(currentMonth)} + {formatStatisticsMonth(currentMonth, locale)}

@@ -100,17 +108,36 @@ export const StatisticsCalendar = ({ {calendarDates.map((calendarDate) => { const dateKey = formatDateKey(calendarDate.date); + const isSelected = dateKey === formatDateKey(selectedDate); const completionRate = completionRateByDate.get(dateKey) ?? null; const status = getIconStatus(completionRate); const Icon = STATUS_ICON[status]; return ( -

- - +
+ ); })}
From 62fbb8348a048e2da676cb4b02d67778e2a6b228 Mon Sep 17 00:00:00 2001 From: yumin-kim4757 Date: Sat, 11 Jul 2026 06:06:21 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat(web):=20=ED=86=B5=EA=B3=84=20=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=20=ED=8C=A8=EB=84=90=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80=20(#142)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/_mocks/statistics-calendar.ts | 103 ++++++++++++++++-- .../_utils/format-statistics-date.ts | 17 +++ 2 files changed, 111 insertions(+), 9 deletions(-) diff --git a/apps/timo-web/app/[locale]/(main)/statistics/_mocks/statistics-calendar.ts b/apps/timo-web/app/[locale]/(main)/statistics/_mocks/statistics-calendar.ts index 9bc7768..967774d 100644 --- a/apps/timo-web/app/[locale]/(main)/statistics/_mocks/statistics-calendar.ts +++ b/apps/timo-web/app/[locale]/(main)/statistics/_mocks/statistics-calendar.ts @@ -1,13 +1,98 @@ -import type { StatisticsCalendarResponse } from "@/app/[locale]/(main)/statistics/_types/statistics"; +import type { + StatisticsCalendarResponse, + StatisticsDayDetail, + StatisticsMonthSummary, +} from "@/app/[locale]/(main)/statistics/_types/statistics"; + +const MOCK_YEAR_MONTH = "2026-07"; + +const MOCK_COMPLETION_RATES = [ + 0, + 25, + 75, + 100, + null, + 25, + 100, + 100, + 100, + 75, + 100, + 25, + null, + 100, + 75, + 0, + 0, + 100, + 25, + 75, + 75, + 25, + 100, + 25, + 0, + 0, + 100, + 75, + null, + null, + null, +] as const; + +const formatMockDate = (day: number) => + `${MOCK_YEAR_MONTH}-${String(day).padStart(2, "0")}`; export const MOCK_STATISTICS_CALENDAR: StatisticsCalendarResponse = { - yearMonth: "2026-07", + yearMonth: MOCK_YEAR_MONTH, today: "2026-07-10", - days: [ - { date: "2026-07-01", completionRate: 0 }, - { date: "2026-07-02", completionRate: 25 }, - { date: "2026-07-03", completionRate: 75 }, - { date: "2026-07-04", completionRate: 100 }, - { date: "2026-07-05", completionRate: null }, - ], + days: MOCK_COMPLETION_RATES.map((completionRate, index) => ({ + date: formatMockDate(index + 1), + completionRate, + })), }; + +export const MOCK_STATISTICS_MONTH_SUMMARY: StatisticsMonthSummary = { + totalRecordMinutes: 1330, + activeDayCount: 28, + averageRecordedMinutes: 214, + completedTodoCount: 80, + totalTodoCount: 100, +}; + +export const MOCK_STATISTICS_DAY_DETAILS: Record = + Object.fromEntries( + MOCK_COMPLETION_RATES.map((completionRate, index) => { + const day = index + 1; + const date = formatMockDate(day); + const totalRecordMinutes = + completionRate === null ? 0 : 90 + completionRate * 2; + + return [ + date, + { + date, + totalRecordMinutes, + todos: + completionRate === null + ? [] + : [ + { + todoId: day * 2 - 1, + title: "통계 뷰 통합", + actualTimeMinutes: Math.round(totalRecordMinutes * 0.55), + estimatedTimeMinutes: 120, + tagName: "개발", + }, + { + todoId: day * 2, + title: "캘린더 선택 상태 점검", + actualTimeMinutes: Math.round(totalRecordMinutes * 0.45), + estimatedTimeMinutes: 90, + tagName: "QA", + }, + ], + }, + ]; + }), + ); diff --git a/apps/timo-web/app/[locale]/(main)/statistics/_utils/format-statistics-date.ts b/apps/timo-web/app/[locale]/(main)/statistics/_utils/format-statistics-date.ts index 4985b47..17bda9f 100644 --- a/apps/timo-web/app/[locale]/(main)/statistics/_utils/format-statistics-date.ts +++ b/apps/timo-web/app/[locale]/(main)/statistics/_utils/format-statistics-date.ts @@ -32,3 +32,20 @@ export const formatStatisticsCalendarDate = (date: Date, locale = "ko") => { weekday: "long", }).format(date); }; + +/** + * 통계 사이드 패널 제목에 표시할 날짜 문구를 반환합니다. + * + * @param date - 표시할 날짜 + * @param locale - 날짜 표기에 사용할 locale 값 + * @returns 월과 일이 포함된 짧은 날짜 문구 + * + * @example + * formatStatisticsSidePanelDate(new Date(2026, 5, 28)); // "6월 28일" + */ +export const formatStatisticsSidePanelDate = (date: Date, locale = "ko") => { + return new Intl.DateTimeFormat(locale, { + month: "long", + day: "numeric", + }).format(date); +}; From dea7333b6eaa21087fee7fe0a3d9c01d5bdb6aad Mon Sep 17 00:00:00 2001 From: yumin-kim4757 Date: Sat, 11 Jul 2026 06:06:38 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat(web):=20=ED=86=B5=EA=B3=84=20=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=20=ED=8C=A8=EB=84=90=20=EB=8B=A4=EA=B5=AD?= =?UTF-8?q?=EC=96=B4=20=EB=AC=B8=EA=B5=AC=20=EC=B6=94=EA=B0=80=20(#142)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/StatisticsSidePanel.tsx | 33 ++++++++++++------- .../_containers/StatisticsHeaderContainer.tsx | 12 ++++--- apps/timo-web/messages/en.json | 13 ++++++++ apps/timo-web/messages/ko.json | 13 ++++++++ 4 files changed, 54 insertions(+), 17 deletions(-) diff --git a/apps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsSidePanel.tsx b/apps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsSidePanel.tsx index eec7b33..0b0d8c0 100644 --- a/apps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsSidePanel.tsx +++ b/apps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsSidePanel.tsx @@ -1,5 +1,8 @@ +"use client"; + import { TagIcon } from "@repo/timo-design-system/ui"; import { cn } from "@repo/timo-design-system/utils"; +import { useTranslations } from "next-intl"; import type { StatisticsDayDetail, @@ -29,9 +32,12 @@ type StatisticsSidePanelProps = 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 "-단축"; +const getDiffLabel = ( + diffMinutes: number, + t: ReturnType>, +) => { + if (diffMinutes > 0) return t("overtime"); + if (diffMinutes < 0) return t("shortened"); return ""; }; @@ -42,34 +48,36 @@ const getDiffClassName = (diffMinutes: number) => { }; export const StatisticsSidePanel = (props: StatisticsSidePanelProps) => { + const t = useTranslations("Statistics.sidePanel"); + if (props.variant === "month") { const { summary } = props; return ( -