Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
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";
Comment thread
yumin-kim2 marked this conversation as resolved.
};
Comment thread
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>
);
31 changes: 31 additions & 0 deletions apps/timo-web/app/[locale]/(main)/statistics/_types/statistics.ts
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>;
Comment thread
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsdoc 에 관한 문서 첨부해드릴게요!
https://www.typescriptlang.org/ko/docs/handbook/jsdoc-supported-types.html
이거 보고 유틸 파일 추후에 수정해 주면 좋을 것 같아요 😍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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