[FEAT] 통계 페이지 뷰 구현#143
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough통계 페이지가 날짜 선택과 월·일 사이드 패널 전환을 지원하도록 확장되었습니다. 캘린더와 헤더는 현재 로케일을 사용하고, 사이드 패널 라벨은 번역 메시지로 표시됩니다. Changes통계 페이지 상호작용
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant 사용자
participant StatisticsCalendar
participant StatisticsContainer
participant StatisticsSidePanel
사용자->>StatisticsCalendar: 날짜 버튼 클릭
StatisticsCalendar->>StatisticsContainer: 선택 날짜 전달
StatisticsContainer->>StatisticsContainer: 날짜 키로 상세 데이터 조회
StatisticsContainer->>StatisticsSidePanel: 일별 패널 데이터 전달
StatisticsSidePanel-->>사용자: 선택 날짜 통계 표시
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Timo Performance ReportBundle Size — timo-web
Lighthouse — timo-web
Image Optimization — timo-web
측정 커밋: |
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/timo-web/app/`[locale]/(main)/statistics/_components/StatisticsCalendar.tsx:
- Line 1: StatisticsCalendar의 onClick 등 인터랙티브 로직을 _containers/로 이동해
_components/가 순수 props 기반 UI로 동작하도록 분리하거나, 구조상 해당 컴포넌트를 _containers/로 옮기고 'use
client'를 유지하세요.
- Around line 117-140: 날짜 선택 버튼에 전체 날짜 맥락이 포함된 접근성 라벨이 없습니다. StatisticsCalendar의
날짜 버튼에 formatStatisticsCalendarDate(calendarDate.date)를 사용한 aria-label을 추가해 스크린
리더가 연도·월·일을 함께 읽도록 수정하세요.
- Around line 109-142: Move the invariant formatDateKey(selectedDate)
calculation outside the calendarDates.map callback, store it in a clearly named
variable near the component’s other derived values, and compare each dateKey
against that variable when computing isSelected.
In
`@apps/timo-web/app/`[locale]/(main)/statistics/_containers/StatisticsContainer.tsx:
- Line 17: Rename the union type alias StatisticsPanelMode to
StatisticsPanelModeTypes and update all references within
StatisticsContainer.tsx accordingly.
- Around line 22-24: MOCK_STATISTICS_CALENDAR.today is parsed as UTC and can
produce the previous local date. Update the mock date initialization used by
StatisticsContainer and the definition of MOCK_STATISTICS_CALENDAR to construct
July 10, 2026 in local time (for example, using numeric Date arguments) or
include an explicit local-time offset, while preserving formatDateKey
compatibility.
In
`@apps/timo-web/app/`[locale]/(main)/statistics/_containers/StatisticsHeaderContainer.tsx:
- Around line 23-26: StatisticsHeaderContainer의 인라인 월 포맷팅을 제거하고,
_utils/format-statistics-date.ts의 formatStatisticsMonth를 import하여
formatStatisticsMonth(currentMonth, locale)로 monthLabel을 생성하도록 변경하세요.
In `@apps/timo-web/app/`[locale]/(main)/statistics/_mocks/statistics-calendar.ts:
- Around line 55-61: MOCK_STATISTICS_MONTH_SUMMARY의 월간 요약 값이 일별 목 데이터와 일관되지
않습니다. MOCK_COMPLETION_RATES의 유효 일수와 일별 상세 데이터의 todo 개수를 기준으로 activeDayCount를 26,
totalTodoCount를 52로 맞추고, 일별 totalRecordMinutes 합계인 5340과 activeDayCount를 기준으로
totalRecordMinutes 및 averageRecordedMinutes를 조정하세요.
In `@apps/timo-web/messages/en.json`:
- Line 43: Update the activeDayCount message to use ICU pluralization, selecting
the singular “day” when count equals 1 and the plural “days” otherwise, while
preserving the count value in both variants.
- Line 48: Update the "overtime" translation value in the messages/en.json
localization resource from "+Overdue" to "+Overtime" (or "+Over") to accurately
represent exceeding the planned time.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 32ef4fa5-3816-4250-9998-0c81b8491eb0
📒 Files selected for processing (9)
apps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsCalendar.tsxapps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsSidePanel.tsxapps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsContainer.tsxapps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsHeaderContainer.tsxapps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsPageContainer.tsxapps/timo-web/app/[locale]/(main)/statistics/_mocks/statistics-calendar.tsapps/timo-web/app/[locale]/(main)/statistics/_utils/format-statistics-date.tsapps/timo-web/messages/en.jsonapps/timo-web/messages/ko.json
💤 Files with no reviewable changes (1)
- apps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsPageContainer.tsx
| @@ -1,10 +1,13 @@ | |||
| "use client"; | |||
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
_components/ 디렉터리의 'use client' 사용을 검토해주세요.
경로 규칙에 따르면 _components/는 'use client' 없이 동작 가능한 순수 UI여야 합니다. 이 컴포넌트는 onClick 핸들러가 추가되어 클라이언트 렌더링이 필수가 되었습니다. 인터랙티브 로직을 _containers/로 분리하거나, 이 컴포넌트가 여전히 props 기반 UI로서 _components/에 머무는 것이 타당한지 팀과 논의해보시길 권장합니다.
As per path instructions, _components/는 props만 받는 순수 UI — 'use client' 없이 동작 가능해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/timo-web/app/`[locale]/(main)/statistics/_components/StatisticsCalendar.tsx
at line 1, StatisticsCalendar의 onClick 등 인터랙티브 로직을 _containers/로 이동해
_components/가 순수 props 기반 UI로 동작하도록 분리하거나, 구조상 해당 컴포넌트를 _containers/로 옮기고 'use
client'를 유지하세요.
Source: Path instructions
| {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 ( | ||
| <div key={dateKey} className="flex flex-col items-center gap-2.5"> | ||
| <Icon /> | ||
| <span className="typo-body-sb-11 text-timo-gray-700"> | ||
| <button | ||
| key={dateKey} | ||
| type="button" | ||
| className="flex flex-col items-center gap-2.5" | ||
| onClick={() => onSelectDate(calendarDate.date)} | ||
| > | ||
| <span | ||
| className={cn( | ||
| "rounded-full", | ||
| isSelected && | ||
| "drop-shadow-[0_0_10px_var(--color-timo-blue-75)]", | ||
| )} | ||
| > | ||
| <Icon /> | ||
| </span> | ||
| <span | ||
| className={cn( | ||
| "typo-body-sb-11 text-timo-gray-700", | ||
| isSelected && "text-timo-blue-300", | ||
| )} | ||
| > | ||
| {calendarDate.day} | ||
| </span> | ||
| </div> | ||
| </button> | ||
| ); | ||
| })} |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
formatDateKey(selectedDate)를 루프 밖으로 이동해주세요.
formatDateKey(selectedDate)가 calendarDates.map 콜백 내부(Line 111)에서 매 반복마다 호출됩니다. selectedDate는 루프 내에서 변하지 않으므로 컴포넌트 본문 상단에서 한 번만 계산하는 것이 효율적입니다.
♻️ 제안하는 리팩터링
const todayLabel = formatStatisticsCalendarDate(today, locale);
+ const selectedDateKey = formatDateKey(selectedDate);
return (
<section className="w-full px-14.75 pt-10 pb-13">
...
{calendarDates.map((calendarDate) => {
const dateKey = formatDateKey(calendarDate.date);
- const isSelected = dateKey === formatDateKey(selectedDate);
+ const isSelected = dateKey === selectedDateKey;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {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 ( | |
| <div key={dateKey} className="flex flex-col items-center gap-2.5"> | |
| <Icon /> | |
| <span className="typo-body-sb-11 text-timo-gray-700"> | |
| <button | |
| key={dateKey} | |
| type="button" | |
| className="flex flex-col items-center gap-2.5" | |
| onClick={() => onSelectDate(calendarDate.date)} | |
| > | |
| <span | |
| className={cn( | |
| "rounded-full", | |
| isSelected && | |
| "drop-shadow-[0_0_10px_var(--color-timo-blue-75)]", | |
| )} | |
| > | |
| <Icon /> | |
| </span> | |
| <span | |
| className={cn( | |
| "typo-body-sb-11 text-timo-gray-700", | |
| isSelected && "text-timo-blue-300", | |
| )} | |
| > | |
| {calendarDate.day} | |
| </span> | |
| </div> | |
| </button> | |
| ); | |
| })} | |
| {calendarDates.map((calendarDate) => { | |
| const dateKey = formatDateKey(calendarDate.date); | |
| const isSelected = dateKey === selectedDateKey; | |
| const completionRate = completionRateByDate.get(dateKey) ?? null; | |
| const status = getIconStatus(completionRate); | |
| const Icon = STATUS_ICON[status]; | |
| return ( | |
| <button | |
| key={dateKey} | |
| type="button" | |
| className="flex flex-col items-center gap-2.5" | |
| onClick={() => onSelectDate(calendarDate.date)} | |
| > | |
| <span | |
| className={cn( | |
| "rounded-full", | |
| isSelected && | |
| "drop-shadow-[0_0_10px_var(--color-timo-blue-75)]", | |
| )} | |
| > | |
| <Icon /> | |
| </span> | |
| <span | |
| className={cn( | |
| "typo-body-sb-11 text-timo-gray-700", | |
| isSelected && "text-timo-blue-300", | |
| )} | |
| > | |
| {calendarDate.day} | |
| </span> | |
| </button> | |
| ); | |
| })} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/timo-web/app/`[locale]/(main)/statistics/_components/StatisticsCalendar.tsx
around lines 109 - 142, Move the invariant formatDateKey(selectedDate)
calculation outside the calendarDates.map callback, store it in a clearly named
variable near the component’s other derived values, and compare each dateKey
against that variable when computing isSelected.
| <button | ||
| key={dateKey} | ||
| type="button" | ||
| className="flex flex-col items-center gap-2.5" | ||
| onClick={() => onSelectDate(calendarDate.date)} | ||
| > | ||
| <span | ||
| className={cn( | ||
| "rounded-full", | ||
| isSelected && | ||
| "drop-shadow-[0_0_10px_var(--color-timo-blue-75)]", | ||
| )} | ||
| > | ||
| <Icon /> | ||
| </span> | ||
| <span | ||
| className={cn( | ||
| "typo-body-sb-11 text-timo-gray-700", | ||
| isSelected && "text-timo-blue-300", | ||
| )} | ||
| > | ||
| {calendarDate.day} | ||
| </span> | ||
| </div> | ||
| </button> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
날짜 버튼에 aria-label 추가를 권장합니다.
버튼 내부에 날짜 숫자 텍스트가 있지만, 스크린 리더 사용자에게 "15"만 들리는 것보다 "2024년 1월 15일"과 같은 맥락 정보가 포함된 aria-label이 접근성에 도움이 됩니다. formatStatisticsCalendarDate 유틸을 활용하면 쉽게 구현할 수 있습니다.
🛡️ 제안하는 접근성 개선
<button
key={dateKey}
type="button"
+ aria-label={formatStatisticsCalendarDate(calendarDate.date, locale)}
className="flex flex-col items-center gap-2.5"
onClick={() => onSelectDate(calendarDate.date)}
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button | |
| key={dateKey} | |
| type="button" | |
| className="flex flex-col items-center gap-2.5" | |
| onClick={() => onSelectDate(calendarDate.date)} | |
| > | |
| <span | |
| className={cn( | |
| "rounded-full", | |
| isSelected && | |
| "drop-shadow-[0_0_10px_var(--color-timo-blue-75)]", | |
| )} | |
| > | |
| <Icon /> | |
| </span> | |
| <span | |
| className={cn( | |
| "typo-body-sb-11 text-timo-gray-700", | |
| isSelected && "text-timo-blue-300", | |
| )} | |
| > | |
| {calendarDate.day} | |
| </span> | |
| </div> | |
| </button> | |
| <button | |
| key={dateKey} | |
| type="button" | |
| aria-label={formatStatisticsCalendarDate(calendarDate.date, locale)} | |
| className="flex flex-col items-center gap-2.5" | |
| onClick={() => onSelectDate(calendarDate.date)} | |
| > | |
| <span | |
| className={cn( | |
| "rounded-full", | |
| isSelected && | |
| "drop-shadow-[0_0_10px_var(--color-timo-blue-75)]", | |
| )} | |
| > | |
| <Icon /> | |
| </span> | |
| <span | |
| className={cn( | |
| "typo-body-sb-11 text-timo-gray-700", | |
| isSelected && "text-timo-blue-300", | |
| )} | |
| > | |
| {calendarDate.day} | |
| </span> | |
| </button> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/timo-web/app/`[locale]/(main)/statistics/_components/StatisticsCalendar.tsx
around lines 117 - 140, 날짜 선택 버튼에 전체 날짜 맥락이 포함된 접근성 라벨이 없습니다.
StatisticsCalendar의 날짜 버튼에 formatStatisticsCalendarDate(calendarDate.date)를 사용한
aria-label을 추가해 스크린 리더가 연도·월·일을 함께 읽도록 수정하세요.
| 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"; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
타입 alias 명명 컨벤션을 준수해 주세요.
StatisticsPanelMode는 유니언 타입 alias이므로, 컨벤션에 따라 Types 접미사를 붙여 StatisticsPanelModeTypes로 명명해야 합니다. 이 파일 내에서만 사용되므로 변경 범위가 제한적입니다.
As per coding guidelines, **/*.{ts,tsx}: "type alias는 유니언·튜플·리터럴에만 사용, 접미사 Types"
♻️ 제안 수정안
-type StatisticsPanelMode = "month" | "day";
+type StatisticsPanelModeTypes = "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<StatisticsPanelMode>("month");
+ const [panelMode, setPanelMode] = useState<StatisticsPanelModeTypes>("month");📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| type StatisticsPanelMode = "month" | "day"; | |
| type StatisticsPanelModeTypes = "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<StatisticsPanelModeTypes>("month"); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/timo-web/app/`[locale]/(main)/statistics/_containers/StatisticsContainer.tsx
at line 17, Rename the union type alias StatisticsPanelMode to
StatisticsPanelModeTypes and update all references within
StatisticsContainer.tsx accordingly.
Source: Coding guidelines
| const locale = useLocale(); | ||
| const monthLabel = new Intl.DateTimeFormat(locale, { | ||
| month: "long", | ||
| }).format(currentMonth); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
formatStatisticsMonth 유틸을 재사용해주세요.
StatisticsCalendar.tsx에서 이미 formatStatisticsMonth(currentMonth, locale)를 사용하고 있는데, 이 컨테이너에서는 동일한 로직을 new Intl.DateTimeFormat(locale, { month: "long" }).format(currentMonth)로 인라인하고 있습니다. _utils/format-statistics-date.ts의 formatStatisticsMonth를 import하여 중복을 제거해주세요.
♻️ 제안하는 리팩터링
+ import { formatStatisticsMonth } from "../_utils/format-statistics-date";
...
const locale = useLocale();
- const monthLabel = new Intl.DateTimeFormat(locale, {
- month: "long",
- }).format(currentMonth);
+ const monthLabel = formatStatisticsMonth(currentMonth, locale);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const locale = useLocale(); | |
| const monthLabel = new Intl.DateTimeFormat(locale, { | |
| month: "long", | |
| }).format(currentMonth); | |
| import { formatStatisticsMonth } from "../_utils/format-statistics-date"; | |
| const locale = useLocale(); | |
| - const monthLabel = new Intl.DateTimeFormat(locale, { | |
| - month: "long", | |
| const monthLabel = formatStatisticsMonth(currentMonth, locale); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/timo-web/app/`[locale]/(main)/statistics/_containers/StatisticsHeaderContainer.tsx
around lines 23 - 26, StatisticsHeaderContainer의 인라인 월 포맷팅을 제거하고,
_utils/format-statistics-date.ts의 formatStatisticsMonth를 import하여
formatStatisticsMonth(currentMonth, locale)로 monthLabel을 생성하도록 변경하세요.
| export const MOCK_STATISTICS_MONTH_SUMMARY: StatisticsMonthSummary = { | ||
| totalRecordMinutes: 1330, | ||
| activeDayCount: 28, | ||
| averageRecordedMinutes: 214, | ||
| completedTodoCount: 80, | ||
| totalTodoCount: 100, | ||
| }; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
목 요약 목 데이터의 수치 일관성을 점검해 주세요.
MOCK_STATISTICS_MONTH_SUMMARY 내부 값들이 서로 맞지 않습니다:
averageRecordedMinutes(214) ≠totalRecordMinutes / activeDayCount= 1330 / 28 ≈ 47.5activeDayCount(28) ≠MOCK_COMPLETION_RATES에서 null이 아닌 날 수(26개)totalTodoCount(100) ≠ 일별 상세의 실제 todo 수(26일 × 2개 = 52)
StatisticsSidePanel 월 패널에서 totalRecordMinutes와 averageRecordedMinutes가 함께 표시되므로, 개발 중 수치 불일치를 눈치챌 수 있습니다. 목 데이터라도 패널에 직접 노출되는 값들은 내부 연관성을 맞추는 것이 좋습니다.
🔧 제안 수정안
export const MOCK_STATISTICS_MONTH_SUMMARY: StatisticsMonthSummary = {
- totalRecordMinutes: 1330,
- activeDayCount: 28,
- averageRecordedMinutes: 214,
- completedTodoCount: 80,
- totalTodoCount: 100,
+ totalRecordMinutes: 5340,
+ activeDayCount: 26,
+ averageRecordedMinutes: 205,
+ completedTodoCount: 52,
+ totalTodoCount: 52,
};참고: totalRecordMinutes 5340는 일별 totalRecordMinutes 합계이고, averageRecordedMinutes는 5340 / 26 ≈ 205입니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const MOCK_STATISTICS_MONTH_SUMMARY: StatisticsMonthSummary = { | |
| totalRecordMinutes: 1330, | |
| activeDayCount: 28, | |
| averageRecordedMinutes: 214, | |
| completedTodoCount: 80, | |
| totalTodoCount: 100, | |
| }; | |
| export const MOCK_STATISTICS_MONTH_SUMMARY: StatisticsMonthSummary = { | |
| totalRecordMinutes: 5340, | |
| activeDayCount: 26, | |
| averageRecordedMinutes: 205, | |
| completedTodoCount: 52, | |
| totalTodoCount: 52, | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/timo-web/app/`[locale]/(main)/statistics/_mocks/statistics-calendar.ts
around lines 55 - 61, MOCK_STATISTICS_MONTH_SUMMARY의 월간 요약 값이 일별 목 데이터와 일관되지
않습니다. MOCK_COMPLETION_RATES의 유효 일수와 일별 상세 데이터의 todo 개수를 기준으로 activeDayCount를 26,
totalTodoCount를 52로 맞추고, 일별 totalRecordMinutes 합계인 5340과 activeDayCount를 기준으로
totalRecordMinutes 및 averageRecordedMinutes를 조정하세요.
| "sidePanel": { | ||
| "monthTotalRecordTime": "Total record time this month", | ||
| "activeDays": "Active days", | ||
| "activeDayCount": "{count} days", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
activeDayCount에 ICU 복수형을 적용해주세요.
현재 "{count} days"는 count가 1일 때 "1 days"가 되어 문법적으로 올바르지 않습니다. next-intl의 ICU MessageFormat 복수형 규칙을 사용해주세요.
💚 제안하는 수정
- "activeDayCount": "{count} days",
+ "activeDayCount": "{count, plural, one {# day} other {# days}}",참고: next-intl ICU MessageFormat 문서
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "activeDayCount": "{count} days", | |
| "activeDayCount": "{count, plural, one {# day} other {# days}}", |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/timo-web/messages/en.json` at line 43, Update the activeDayCount message
to use ICU pluralization, selecting the singular “day” when count equals 1 and
the plural “days” otherwise, while preserving the count value in both variants.
| "accumulatedTasks": "Accumulated tasks", | ||
| "dayTotalRecordTime": "Total record time today", | ||
| "plan": "Plan", | ||
| "overtime": "+Overdue", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
"Overdue"를 "Overtime"으로 수정해주세요.
한국어 "초과"는 계획 시간을 초과했음을 의미하지만, 영어 "Overdue"는 "기한이 지남"을 뜻합니다. 이 맥락에서는 "Overtime" 또는 "Over"가 올바른 번역입니다.
💚 제안하는 수정
- "overtime": "+Overdue",
+ "overtime": "+Overtime",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "overtime": "+Overdue", | |
| "overtime": "+Overtime", |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/timo-web/messages/en.json` at line 48, Update the "overtime" translation
value in the messages/en.json localization resource from "+Overdue" to
"+Overtime" (or "+Over") to accurately represent exceeding the planned time.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/timo-web/app/`[locale]/(main)/statistics/_utils/format-statistics-time.ts:
- Around line 12-25: Guard negative inputs in both formatStatisticsHourText and
formatStatisticsClockText by normalizing minutes to a nonnegative value before
performing time calculations. Preserve the existing formatting behavior for zero
and positive inputs, and ensure negative values cannot produce negative hour,
minute, or clock components.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 5d219b60-ae7e-43a5-86a7-dc06d50b348a
📒 Files selected for processing (4)
apps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsCalendar.tsxapps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsSidePanel.tsxapps/timo-web/app/[locale]/(main)/statistics/_components/SummaryTimeBlock.tsxapps/timo-web/app/[locale]/(main)/statistics/_utils/format-statistics-time.ts
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/timo-web/app/`[locale]/(main)/statistics/_utils/format-statistics-time.ts:
- Around line 12-25: Guard negative inputs in both formatStatisticsHourText and
formatStatisticsClockText by normalizing minutes to a nonnegative value before
performing time calculations. Preserve the existing formatting behavior for zero
and positive inputs, and ensure negative values cannot produce negative hour,
minute, or clock components.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 5d219b60-ae7e-43a5-86a7-dc06d50b348a
📒 Files selected for processing (4)
apps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsCalendar.tsxapps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsSidePanel.tsxapps/timo-web/app/[locale]/(main)/statistics/_components/SummaryTimeBlock.tsxapps/timo-web/app/[locale]/(main)/statistics/_utils/format-statistics-time.ts
🛑 Comments failed to post (1)
apps/timo-web/app/[locale]/(main)/statistics/_utils/format-statistics-time.ts (1)
12-25: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
음수 입력값에 대한 가드가 없습니다.
두 함수 모두
minutes가 음수일 경우 예상치 못한 결과를 반환합니다. 예를 들어formatStatisticsHourText(-30)은"-1h -30m"을,formatStatisticsClockText(-90)은"1:-30"을 반환합니다. 현재 목 데이터에서는 음수가 들어올 가능성이 낮지만, 방어적 차원에서 early return이나Math.max(0, minutes)처리를 고려해보세요.참고: MDN — Math.max()
Also applies to: 37-42
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/timo-web/app/`[locale]/(main)/statistics/_utils/format-statistics-time.ts around lines 12 - 25, Guard negative inputs in both formatStatisticsHourText and formatStatisticsClockText by normalizing minutes to a nonnegative value before performing time calculations. Preserve the existing formatting behavior for zero and positive inputs, and ensure negative values cannot produce negative hour, minute, or clock components.
kimminna
left a comment
There was a problem hiding this comment.
간단한 코멘트 하나만 확인해 주세요! 고생하셨어요~~!
Co-authored-by: 김민아 <kmina121777@naver.com>
Co-authored-by: 김민아 <kmina121777@naver.com>
…stics-calendar-view
- develop 병합 후 공용 date 유틸로 이동한 formatDateKey import 경로를 반영했습니다
jjangminii
left a comment
There was a problem hiding this comment.
API 연결까지 고려해서 설계해주신 게 느껴졌어요 이제 진짜 뷰 마감이 보이네요, 고생하셨습니다~
크리티컬 이슈는 아닌데 나중에 한번 고민해보시면 좋을 것 같은 부분들 코멘트 남겨뒀어요, 확인해주세요 😊
There was a problem hiding this comment.
♻️ Duplicate comments (3)
apps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsHeaderContainer.tsx (1)
22-24: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
formatStatisticsMonth유틸 재사용 건은 여전히 유효합니다.이전 리뷰에서 제안드린 내용이 현재 코드에 반영되지 않았습니다.
StatisticsCalendar.tsx에서 사용하는formatStatisticsMonth(currentMonth, locale)와 동일한 로직을 인라인으로 중복 작성하고 있습니다.♻️ 제안 수정안
+ import { formatStatisticsMonth } from "../_utils/format-statistics-date"; ... const locale = useLocale(); - const monthLabel = new Intl.DateTimeFormat(locale, { - month: "long", - }).format(currentMonth); + const monthLabel = formatStatisticsMonth(currentMonth, locale);As per coding guidelines,
**/*.{ts,tsx}: "절대 경로 import 사용 (상대 경로../../지양)" —formatStatisticsMonth를 공용 유틸로 통합하면 중복 로직을 제거할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/timo-web/app/`[locale]/(main)/statistics/_containers/StatisticsHeaderContainer.tsx around lines 22 - 24, StatisticsHeaderContainer의 인라인 Intl.DateTimeFormat 월 포맷 로직을 제거하고, StatisticsCalendar.tsx에서 사용하는 공용 formatStatisticsMonth(currentMonth, locale) 유틸을 절대 경로 import로 재사용하세요.apps/timo-web/messages/en.json (2)
100-100: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
activeDayCountICU 복수형 적용 건은 여전히 유효합니다.이전 리뷰에서 제안드린 ICU MessageFormat 복수형 규칙이 아직 반영되지 않았습니다. count가 1일 때 "1 days"가 되어 문법적으로 올바르지 않습니다.
💚 제안하는 수정
- "activeDayCount": "{count} days", + "activeDayCount": "{count, plural, one {# day} other {# days}}",참고: next-intl ICU MessageFormat 문서
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/timo-web/messages/en.json` at line 100, Update the activeDayCount message to use ICU MessageFormat pluralization so count 1 renders “1 day” and other counts render “{count} days,” preserving the existing count interpolation.
105-105: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win"Overdue" → "Overtime" 수정 건도 여전히 유효합니다.
한국어 "초과"는 계획 시간 초과를 의미하지만, 영어 "Overdue"는 "기한이 지남"을 뜻합니다. 이 맥락에서는 "Overtime"이 올바른 번역입니다.
💚 제안하는 수정
- "overtime": "+Overdue", + "overtime": "+Overtime",🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/timo-web/messages/en.json` at line 105, Update the overtime translation value in the messages resource from “Overdue” to “Overtime”, preserving the existing “+” prefix and key.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In
`@apps/timo-web/app/`[locale]/(main)/statistics/_containers/StatisticsHeaderContainer.tsx:
- Around line 22-24: StatisticsHeaderContainer의 인라인 Intl.DateTimeFormat 월 포맷 로직을
제거하고, StatisticsCalendar.tsx에서 사용하는 공용 formatStatisticsMonth(currentMonth,
locale) 유틸을 절대 경로 import로 재사용하세요.
In `@apps/timo-web/messages/en.json`:
- Line 100: Update the activeDayCount message to use ICU MessageFormat
pluralization so count 1 renders “1 day” and other counts render “{count} days,”
preserving the existing count interpolation.
- Line 105: Update the overtime translation value in the messages resource from
“Overdue” to “Overtime”, preserving the existing “+” prefix and key.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 78793cb3-4d34-4cec-b1d9-cb59dd663fa9
📒 Files selected for processing (6)
apps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsCalendar.tsxapps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsSidePanel.tsxapps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsContainer.tsxapps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsHeaderContainer.tsxapps/timo-web/messages/en.jsonapps/timo-web/messages/ko.json
ISSUE 🔗
close #142
What is this PR? 🔍
1. 통계 뷰 컨테이너 연결
StatisticsContainer에서 통계 화면의 주요 상태를 관리하도록 했습니다.월 이동과 날짜 클릭은 각각 다른 패널 모드로 이어지도록 분리했습니다.
이 구조를 선택한 이유는 API 연결 시에도 동일하게 유지될 수 있기 때문입니다.
나중에는 현재 mock 데이터를 아래처럼 query 결과로 바꾸면 됩니다.
2. 캘린더 날짜 선택 및 아이콘 상태 처리
캘린더 날짜 클릭 시 부모 컨테이너의 selectedDate가 갱신되도록 연결했습니다.
onClick={() => onSelectDate(calendarDate.date)}아이콘 상태는 완료율과 미래 날짜 여부를 기준으로 계산합니다.
미래 날짜는 아직 기록이 존재할 수 없는 날짜이므로 disabled 아이콘으로 보여주고 클릭도 막았습니다.
disabled={isFutureDate}반대로 과거 날짜의 completionRate: null은 미래 날짜가 아니므로 disabled가 아니라 0% 상태로 처리했습니다.
3. 오른쪽 패널 월별/일별 전환
StatisticsSidePanel은 variant에 따라 월별 패널과 일별 패널을 렌더링합니다.
4. 오른쪽 패널 고정 너비 처리
처음에는 w-76만 적용했는데, flex row 안에서 사이드 패널이 줄어들어 피그마 기준 304px이 유지되지 않는 문제가 있었습니다.
그래서 사이드 패널은 고정 너비와 shrink 방지를 함께 적용했습니다.
w-[304px] shrink-0캘린더 영역은 남은 공간을 차지하도록 변경했습니다.
min-w-0 flex-1이렇게 해서 레이아웃 역할이 명확해졌습니다.
5. 다국어 지원
오른쪽 패널의 고정 문구를 Statistics.sidePanel 네임스페이스로 분리했습니다.
const t = useTranslations("Statistics.sidePanel");예시:
ko.json, en.json에 각각 통계 패널 문구를 추가했습니다.
날짜와 월 표시는 useLocale()과 Intl.DateTimeFormat을 사용했습니다.
const locale = useLocale();이 화면에서는 한국어 화면이면 7월, 영어 화면이면 July처럼 현재 locale을 따라가야 하므로 useLocale() 기반 포맷을 유지했습니다.
To Reviewers
Screenshot 📷
2026-07-11.9.59.27.mov
Test Checklist ✔