[FEAT] 통계 페이지 뷰 구현 (PR수정중)#143
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough통계 페이지에 날짜 선택과 월·일 사이드 패널 전환이 추가되었습니다. 캘린더와 헤더는 현재 로케일을 사용하며, 사이드 패널 문구는 영어·한국어 번역 키로 표시됩니다. Changes통계 페이지 상호작용
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant 사용자
participant StatisticsCalendar
participant StatisticsContainer
participant StatisticsSidePanel
사용자->>StatisticsCalendar: 날짜 버튼 클릭
StatisticsCalendar->>StatisticsContainer: 선택 날짜 전달
StatisticsContainer->>StatisticsContainer: 일별 상세 데이터 조회
StatisticsContainer->>StatisticsSidePanel: 일별 패널 데이터 전달
StatisticsSidePanel-->>사용자: 선택 날짜 통계 표시
Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 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 [selectedDate, setSelectedDate] = useState( | ||
| () => new Date(MOCK_STATISTICS_CALENDAR.today), | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
fd -i 'StatisticsContainer.tsx|formatDateKey|MOCK_STATISTICS_CALENDAR' apps/timo-web app packages . && \
printf '\n--- outline ---\n' && \
ast-grep outline apps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsContainer.tsx --view expanded && \
printf '\n--- search formatDateKey ---\n' && \
rg -n "formatDateKey|MOCK_STATISTICS_CALENDAR|selectedDateKey|today" apps/timo-web/app packages -g '!**/node_modules/**'Repository: Team-Timo/Timo-client
Length of output: 347
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- files ---'
git ls-files 'apps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsContainer.tsx' \
'apps/timo-web/app' 'packages' | sed -n '1,120p'
printf '\n%s\n' '--- outline StatisticsContainer.tsx ---'
ast-grep outline 'apps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsContainer.tsx' --view expanded
printf '\n%s\n' '--- search formatDateKey / mock data ---'
rg -n "formatDateKey|MOCK_STATISTICS_CALENDAR|selectedDateKey|today" apps/timo-web/app packages -g '!**/node_modules/**'Repository: Team-Timo/Timo-client
Length of output: 15349
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- StatisticsContainer.tsx ---'
sed -n '1,140p' 'apps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsContainer.tsx'
printf '\n%s\n' '--- statistics-calendar util ---'
sed -n '1,160p' 'apps/timo-web/app/[locale]/(main)/statistics/_utils/statistics-calendar.ts'
printf '\n%s\n' '--- statistics mocks ---'
sed -n '1,140p' 'apps/timo-web/app/[locale]/(main)/statistics/_mocks/statistics-calendar.ts'
printf '\n%s\n' '--- StatisticsCalendar.tsx date usage ---'
sed -n '1,180p' 'apps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsCalendar.tsx'
printf '\n%s\n' '--- date parse probe ---'
python3 - <<'PY'
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
s = "2026-07-10"
utc = datetime.fromisoformat(s).replace(tzinfo=timezone.utc)
for tz_name in ["UTC", "Asia/Seoul", "America/New_York", "America/Los_Angeles"]:
dt = utc.astimezone(ZoneInfo(tz_name))
print(tz_name, dt.date().isoformat())
PYRepository: Team-Timo/Timo-client
Length of output: 12844
🏁 Script executed:
#!/bin/bash
set -euo pipefail
node - <<'JS'
const inputs = ["2026-07-10", "2026-07-10T00:00:00"];
const timezones = ["UTC", "Asia/Seoul", "America/New_York", "America/Los_Angeles"];
for (const tz of timezones) {
process.env.TZ = tz;
console.log(`TZ=${tz}`);
for (const input of inputs) {
const d = new Date(input);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
console.log(` ${input} -> ${y}-${m}-${day} | ${d.toISOString()}`);
}
}
JSRepository: Team-Timo/Timo-client
Length of output: 691
MOCK_STATISTICS_CALENDAR.today은 로컬 시간으로 생성해 주세요.
new Date("2026-07-10")은 UTC 자정으로 해석돼서, formatDateKey()의 로컬 getDate()와 어긋날 수 있습니다. 그래서 UTC-5/UTC-8 환경에서는 selectedDateKey가 전날로 밀리고, MOCK_STATISTICS_DAY_DETAILS를 못 찾아 빈 패널이 나옵니다. new Date(2026, 6, 10)처럼 로컬 기준으로 만들거나, mock 문자열에 시간 정보를 명시해 두면 됩니다.
MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse
🤖 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
around lines 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.
| 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.
ISSUE 🔗
What is this PR? 🔍
To Reviewers
Screenshot 📷
Test Checklist ✔