-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] 통계 페이지 사이드 패널 구현 #128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9b8a12b
ef2f104
ccef315
5e0bd09
f8b4784
56f3b57
4654f0e
57e2d66
90f5e81
598ebda
c18589f
2fa956f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"; | ||
| }; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| export const StatisticsSidePanel = (props: StatisticsSidePanelProps) => { | ||
| if (props.variant === "month") { | ||
| const { summary } = props; | ||
|
|
||
| return ( | ||
| <aside className={cn(SIDE_PANEL_CLASS_NAME, "pt-20.75 pb-148")}> | ||
| <div className="flex h-46.25 flex-col gap-5"> | ||
| <div className="px-7.5 py-2.5"> | ||
| <SummaryTimeBlock | ||
| label="이번 달의 총 기록 시간" | ||
| minutes={summary.totalRecordMinutes} | ||
| /> | ||
| </div> | ||
|
|
||
| <dl className="typo-headline-r-14 flex flex-col gap-2 px-7.5"> | ||
| <div className="flex items-center gap-3"> | ||
| <dt className="text-timo-gray-900">활동일</dt> | ||
| <dd>{summary.activeDayCount}일</dd> | ||
| </div> | ||
|
|
||
| <div className="flex items-center gap-3"> | ||
| <dt className="text-timo-gray-900">일평균</dt> | ||
| <dd> | ||
| {formatStatisticsHourText(summary.averageRecordedMinutes)} | ||
| </dd> | ||
| </div> | ||
|
|
||
| <div className="flex items-center gap-3"> | ||
| <dt className="text-timo-gray-900">누적 태스크</dt> | ||
| <dd> | ||
| {summary.completedTodoCount}/{summary.totalTodoCount} | ||
| </dd> | ||
| </div> | ||
| </dl> | ||
| </div> | ||
| </aside> | ||
| ); | ||
| } | ||
|
|
||
| const { detail } = props; | ||
|
|
||
| return ( | ||
| <aside className={cn(SIDE_PANEL_CLASS_NAME, "px-7.5 pt-20.5 pb-76")}> | ||
| <div className="flex flex-col gap-6"> | ||
| <h2 className="typo-headline-b-24">{detail.date}</h2> | ||
|
|
||
| <SummaryTimeBlock | ||
| label="오늘의 총 기록 시간" | ||
| minutes={detail.totalRecordMinutes} | ||
| /> | ||
| </div> | ||
|
|
||
| <ul className="mt-5.5 flex flex-col"> | ||
| {detail.todos.map((todo) => { | ||
| const diffMinutes = | ||
| todo.actualTimeMinutes - todo.estimatedTimeMinutes; | ||
|
|
||
| return ( | ||
| <li | ||
| key={todo.todoId} | ||
| className="border-timo-gray-500 flex justify-between border-b py-2.5" | ||
| > | ||
| <div className="flex flex-col items-start gap-2"> | ||
| <p className="typo-headline-r-14 text-timo-gray-900"> | ||
| {todo.title} | ||
| </p> | ||
| {todo.tagName && <TagIcon text={todo.tagName} />} | ||
| </div> | ||
|
|
||
| <div className="typo-body-r-12 flex flex-col items-end gap-2"> | ||
| <strong className="typo-body-b-12"> | ||
| {formatStatisticsClockText(todo.actualTimeMinutes)} | ||
| </strong> | ||
|
|
||
| <span> | ||
| 계획: {formatStatisticsClockText(todo.estimatedTimeMinutes)} | ||
| </span> | ||
|
|
||
| <span className={getDiffClassName(diffMinutes)}> | ||
| {getDiffLabel(diffMinutes)} | ||
| </span> | ||
| </div> | ||
| </li> | ||
| ); | ||
| })} | ||
| </ul> | ||
| </aside> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) => ( | ||
| <div className="flex flex-col"> | ||
| <p className="typo-headline-r-14 text-timo-gray-700">{label}</p> | ||
| <strong className="typo-headline-b-30"> | ||
| {formatStatisticsHourText(minutes)} | ||
| </strong> | ||
| </div> | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<typeof statisticsDayDetailSchema>; | ||
|
|
||
| export type StatisticsTodoRecord = z.infer<typeof statisticsTodoRecordSchema>; |
|
yumin-kim2 marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| /** | ||
| * 분 단위 기록 시간을 통계 요약 영역의 시간 텍스트로 변환합니다. | ||
| * | ||
| * @param minutes - 분 단위 기록 시간 | ||
| * @returns 화면에 표시할 시간 텍스트 | ||
| * | ||
| * @example | ||
| * formatStatisticsHourText(260); // "4h 20m" | ||
| * formatStatisticsHourText(120); // "2h" | ||
| * formatStatisticsHourText(30); // "30m" | ||
| */ | ||
|
Comment on lines
+1
to
+11
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. jsdoc 에 관한 문서 첨부해드릴게요!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 태그도 같이 붙이는게 확실히 문서 구조가 명확해져서 좋은 것 같네요. 이해하기도 편하구요! 반영완료했습니다😍🤟 |
||
| 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")}`; | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.