From 9813e4520216e6f4edc16e9bf67f9a3d100a6a43 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:26:54 +0000 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9A=A1=20Bolt:=20hoist=20buildTimelineGr?= =?UTF-8?q?oups=20useMemo=20up=20to=20page.tsx=20to=20avoid=20duplicate=20?= =?UTF-8?q?calculation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `buildTimelineGroups` was calculated redundantly inside both `EventList` and `SessionActivityRibbon` inside `useMemo` hooks. This commit moves the `useMemo` computation into the parent `page.tsx` component and passes down `groups` as a prop to its children, preventing duplicate O(n) array iterations per `events` change. --- .../dashboard/[orgSlug]/sessions/[sessionId]/page.tsx | 5 ++++- packages/web/src/components/dashboard/event-list.tsx | 11 ++++++----- .../components/dashboard/session-activity-ribbon.tsx | 8 ++++---- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/web/src/app/dashboard/[orgSlug]/sessions/[sessionId]/page.tsx b/packages/web/src/app/dashboard/[orgSlug]/sessions/[sessionId]/page.tsx index 1bea6e3e..4477ad5b 100644 --- a/packages/web/src/app/dashboard/[orgSlug]/sessions/[sessionId]/page.tsx +++ b/packages/web/src/app/dashboard/[orgSlug]/sessions/[sessionId]/page.tsx @@ -13,7 +13,7 @@ import { SessionFilesTab, } from '@/components/dashboard/session-files' import { useSessionDetail } from '@/hooks/use-dashboard-sessions' -import { messagesToTimeline, formatSlashCommandText } from '@/lib/timeline-events' +import { messagesToTimeline, buildTimelineGroups, formatSlashCommandText } from '@/lib/timeline-events' import { extractSessionFiles } from '@/lib/session-files' import { formatTokens, @@ -46,6 +46,7 @@ export default function OrgSessionDetailPage({ () => (data ? messagesToTimeline(data.messages) : []), [data], ) + const groups = useMemo(() => buildTimelineGroups(events), [events]) const files = useMemo(() => extractSessionFiles(events), [events]) const [selectedIdx, setSelectedIdx] = useState(0) const safeIdx = @@ -221,6 +222,7 @@ export default function OrgSessionDetailPage({
void; sessionStartedAt: string; @@ -50,11 +51,10 @@ function formatElapsed(timestamp: string, sessionStartedAt: string): string { } function buildFlatRows( - events: TimelineEvent[], + groups: TimelineGroup[], expandedGroups: Set, selectedIdx: number, ): FlatRow[] { - const groups = buildTimelineGroups(events); const rows: FlatRow[] = []; for (const group of groups) { if (group.kind === "single") { @@ -261,6 +261,7 @@ function Row({ export function EventList({ events, + groups, selectedIdx, onSelect, sessionStartedAt, @@ -268,8 +269,8 @@ export function EventList({ onToggleGroup, }: EventListProps) { const rows = useMemo( - () => buildFlatRows(events, expandedGroups, selectedIdx), - [events, expandedGroups, selectedIdx], + () => buildFlatRows(groups, expandedGroups, selectedIdx), + [groups, expandedGroups, selectedIdx], ); if (events.length === 0) { diff --git a/packages/web/src/components/dashboard/session-activity-ribbon.tsx b/packages/web/src/components/dashboard/session-activity-ribbon.tsx index 87a305ea..694e3806 100644 --- a/packages/web/src/components/dashboard/session-activity-ribbon.tsx +++ b/packages/web/src/components/dashboard/session-activity-ribbon.tsx @@ -1,16 +1,17 @@ 'use client' -import { useMemo, useState, useRef } from 'react' +import { useState, useRef } from 'react' import { formatSlashCommandText, - buildTimelineGroups, type TimelineEvent, + type TimelineGroup, } from '@/lib/timeline-events' import { formatTokens, formatCost, formatRelativeTime } from '@/lib/format' import { segmentVisuals } from './session-ribbon-visuals' type Props = { events: TimelineEvent[] + groups: TimelineGroup[] selectedIdx: number | null onSelect: (idx: number) => void sessionStartedAt: string @@ -147,6 +148,7 @@ function MergedTooltipBody({ export function SessionActivityRibbon({ events, + groups, selectedIdx, onSelect, sessionStartedAt, @@ -156,8 +158,6 @@ export function SessionActivityRibbon({ const [hover, setHover] = useState(null) const containerRef = useRef(null) - const groups = useMemo(() => buildTimelineGroups(events), [events]) - if (events.length === 0) return null const trackMouse = (e: React.MouseEvent) => { From 35479064d72abf110351706016c7bc9c9533a940 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 22 Jun 2026 20:11:44 +0000 Subject: [PATCH 2/6] refactor(web): optimize multiple reduce loops in weekly reports Replace chained `.reduce()` and `Object.values()` with a single `for...of` loop over `Object.keys()` to avoid unnecessary intermediate array allocations and improve iteration performance during report aggregation. --- packages/web/src/lib/server/weekly-report.ts | 23 ++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/web/src/lib/server/weekly-report.ts b/packages/web/src/lib/server/weekly-report.ts index 7eabddbd..eb95d36f 100644 --- a/packages/web/src/lib/server/weekly-report.ts +++ b/packages/web/src/lib/server/weekly-report.ts @@ -392,17 +392,22 @@ export async function getWeeklyReport( } // Insights — delegation - const totalAgentCalls = thisWeekRollups.reduce( - (sum, r) => sum + Object.values(r.agentCounts).reduce((a, b) => a + b, 0), - 0, - ) - const totalSkillCalls = thisWeekRollups.reduce( - (sum, r) => sum + Object.values(r.skillCounts).reduce((a, b) => a + b, 0), - 0, - ) + // ⚡ Bolt Optimization: + // 병목 지점: 기존 코드는 `thisWeekRollups`를 3번 순회하고, 매 순회마다 Object.values()로 중간 배열을 생성하여 메모리 할당 비용이 발생했습니다. + // 최적화 방법: 단일 for...of 루프와 Object.keys() 순회를 결합하여 N+1 순회를 1회 순회로 통합하고 중간 배열 할당을 제거했습니다. + // 기대 효과: `thisWeekRollups`의 크기가 클 경우, 불필요한 배열 생성 오버헤드와 O(N) 순회를 1/3로 줄여 리포트 생성 성능이 향상됩니다. + let totalAgentCalls = 0 + let totalSkillCalls = 0 const distinctSkillsThisWeek = new Set() + for (const r of thisWeekRollups) { - for (const k of Object.keys(r.skillCounts)) distinctSkillsThisWeek.add(k) + for (const k of Object.keys(r.agentCounts)) { + totalAgentCalls += r.agentCounts[k] + } + for (const k of Object.keys(r.skillCounts)) { + totalSkillCalls += r.skillCounts[k] + distinctSkillsThisWeek.add(k) + } } const insights: WeeklyInsights = { From 649df11c1c816fdb73fb61c8b201270dfbe86b82 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 22 Jun 2026 21:49:09 +0000 Subject: [PATCH 3/6] feat(ux): add focus-visible styles to overview stats toggle button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit overview-stats 컴포넌트의 설명 텍스트를 펼치거나 접는 버튼에 키보드 포커스 스타일이 누락되어 있어, 키보드 내비게이션 사용자에게 현재 포커스 위치를 명확히 보여주지 못하는 문제를 수정했습니다. Tailwind CSS의 `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm` 클래스를 추가하여 탭(Tab) 키 이동 시 포커스 링이 보이도록 접근성을 개선했습니다. --- .Jules/palette.md | 3 +++ packages/web/src/components/dashboard/overview-stats.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .Jules/palette.md diff --git a/.Jules/palette.md b/.Jules/palette.md new file mode 100644 index 00000000..b5ac773f --- /dev/null +++ b/.Jules/palette.md @@ -0,0 +1,3 @@ +## 2024-06-22 - Overview Stats 토글 버튼 키보드 접근성 개선 +**Learning:** `overview-stats` 컴포넌트의 설명 텍스트를 펼치거나 접는 ` diff --git a/packages/web/src/components/layout/org-header.tsx b/packages/web/src/components/layout/org-header.tsx index c33de1fc..7f57f267 100644 --- a/packages/web/src/components/layout/org-header.tsx +++ b/packages/web/src/components/layout/org-header.tsx @@ -18,6 +18,7 @@ export function OrgHeader({ orgName }: { orgName?: string }) { diff --git a/packages/web/src/components/layout/org-sidebar.tsx b/packages/web/src/components/layout/org-sidebar.tsx index 7f28d8d0..6d37e315 100644 --- a/packages/web/src/components/layout/org-sidebar.tsx +++ b/packages/web/src/components/layout/org-sidebar.tsx @@ -124,6 +124,7 @@ export function OrgSidebar() { @@ -150,6 +151,7 @@ export function OrgSidebar() { From 5f5065cf62acaec6d002bb0963a79d6c8b8b44f1 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 6 Jul 2026 22:22:18 +0000 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=20=EB=B2=94=EC=9C=84=20=EC=84=A0=ED=83=9D=EA=B8=B0=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=EC=84=B1=20=EB=B0=8F=20=ED=8F=AC=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=20=EC=83=81=ED=83=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 날짜 범위 선택기(Segment Control) 래퍼에 `role="group"` 및 `aria-label` 추가 - 선택된 버튼 상태를 스크린 리더에 알리는 `aria-pressed` 속성 추가 - 키보드 내비게이션 시 포커스가 명확히 보이도록 `focus-visible` 관련 클래스 추가 - DateRangePicker 컴포넌트에 대한 유닛 테스트(Vitest + jsdom) 100% 커버리지 작성 - .Jules/palette.md 저널 항목 추가 --- .Jules/palette.md | 5 +- .gitignore | 1 + packages/web/package.json | 6 + .../dashboard/date-range-picker.test.tsx | 85 ++ .../dashboard/date-range-picker.tsx | 10 +- packages/web/vitest.config.ts | 8 +- packages/web/vitest.setup.ts | 1 + pnpm-lock.yaml | 808 ++++++++++++++++-- 8 files changed, 859 insertions(+), 65 deletions(-) create mode 100644 packages/web/src/components/dashboard/date-range-picker.test.tsx create mode 100644 packages/web/vitest.setup.ts diff --git a/.Jules/palette.md b/.Jules/palette.md index b5ac773f..48f3306e 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -1,3 +1,6 @@ ## 2024-06-22 - Overview Stats 토글 버튼 키보드 접근성 개선 **Learning:** `overview-stats` 컴포넌트의 설명 텍스트를 펼치거나 접는 `