---
title: "260522_F8-근력요약뷰프론트교체"
notion_id: "368229620868818a8a42e740641dc757"
notion_url: "https://app.notion.com/p/368229620868818a8a42e740641dc757"
category: "workreport"
parent: "Claude Code 작업보고"
updated: "2026-05-22"
priority: "Medium"
purpose: "대시보드 근력 최근 30일 요약/혼합차트를 strength_sets JS 재집계에서 strength_daily_summary 뷰 기반으로 교체 (최신 운동 상세 카드 구조는 유지)"
---

작업일: 2026-05-22
작업자: Claude Code (Opus 4.7, 1M context)
작업 경로: `d:\dallog\dallog_git`

---

## 1. 사용자 요청 (원문 raw)

```javascript
※ 스페셜 오더: F-8 프론트엔드 fetch 교체 작업

※ 주의: 동일 작업을 수행 중인 다른 에이전트가 있을 수 있음.
작업 시작 전 git status로 작업트리 상태를 확인한다.
기존 staged/uncommitted 파일이 있으면 목록을 먼저 보고한다.
이번 작업 대상 파일과 기존 변경 파일이 겹치면 즉시 중단하고 보고한다.

원격 최신 여부 확인은 git fetch로만 수행한다.
git pull은 임의 실행하지 않는다.
충돌 또는 merge 필요 상황이면 즉시 중단하고 보고한다.

작업 목표:
F-8 작업.
대시보드 근력 섹션의 최근 30일 요약/차트 계산을 기존 strength_sets IN쿼리 기반 JS 재집계에서
Supabase 뷰 strength_daily_summary 기반으로 교체한다.

이미 Supabase에는 strength_daily_summary 뷰가 생성되어 있다.
뷰 컬럼은 다음과 같다.

- session_id uuid
- recorded_at date
- label text
- total_sets bigint
- total_volume numeric
- avg_volume_per_set numeric

핵심 방향:
최신 운동 상세 카드 표시에는 기존 원본 데이터 구조를 유지한다.
즉, 최신 운동명/세트 반복수 표시용으로 strength_logs → strength_exercises → strength_sets 구조는 완전히 제거하지 않는다.

단, 최근 30일 카드와 혼합차트는 strength_daily_summary 데이터를 사용하도록 바꾼다.

대상 파일:
1. src/pages/Dashboard.tsx
2. src/components/dashboard/StrengthSection.tsx

사전 확인:
Get-ChildItem -Path D:\dallog\dallog_git\src -Recurse -Include *.tsx,*.ts | Select-Object FullName | Sort-Object FullName

검색:
Select-String -Path D:\dallog\dallog_git\src\pages\Dashboard.tsx,D:\dallog\dallog_git\src\components\dashboard\StrengthSection.tsx -Pattern "strength_logs|strength_exercises|strength_sets|strength_daily_summary|recent30|agg30|chartConfig|StrengthSection" -CaseSensitive:$false

구현 지시: (요약)
1. StrengthSummaryRow 타입 export 추가
2. Props에 summaryRows 추가
3. sorted/latest는 sessions 기반 유지
4. 최근 30일 카드·차트는 summaryRows 기반으로 교체
5. setVolume/exMap/latestBodyWeight 정리 최소화
6. Dashboard에 strengthSummaryRows state 추가
7. Promise.all에 strength_daily_summary fetch 추가
8. Number() 변환
9. StrengthSection 호출부에 summaryRows prop 추가
10. 빈 상태 판정은 strengths 기준 유지

주의사항:
- 최신 운동 상세 표시 구조 유지
- LogEntry / History 건드리지 않음
- CoachNotes / aiHandoff / SummaryBrief 범위 밖
- E-6, C-6, C-7, F-6d, F-6e 범위 밖이면 건드리지 않음
- 전역 UI/테마 수정 금지

검증:
1. npm run build 성공
2. TypeScript 오류 없음
3. Dashboard.tsx에서 strength_daily_summary fetch 추가됨
4. StrengthSection의 30일 카드/차트가 summaryRows 기반으로 계산됨
5. 최신 운동 상세 카드가 운동명/반복수 표시
6. git diff --name-only 보고

git add src/pages/Dashboard.tsx src/components/dashboard/StrengthSection.tsx 형식으로만 스테이징
git add . 또는 git add -A 절대 금지

완료 후 노션 Claude Code 작업보고 페이지 새 하위 페이지로 업로드
페이지 제목: 260522_F8-근력요약뷰프론트교체
```

---

## 2. 사전 확인

### git status (시작 시점)

```javascript
On branch main
Your branch is up to date with 'origin/main'.

Changes to be committed:
  new file:   .playwright-mcp/page-2026-05-20T08-23-18-022Z.yml
  new file:   docs/260520_playwright_browsing_report.md
  new file:   migrations/2026-05-22_records_updated_at.sql
  modified:   package-lock.json
  modified:   package.json
  modified:   src/App.tsx
  modified:   src/components/Layout.tsx
  new file:   src/components/MarkdownBriefRenderer.tsx
  modified:   src/lib/supabase.ts
  modified:   src/pages/CoachNotes.tsx
  modified:   src/pages/Dashboard.tsx
```

작업 대상 파일 중 `src/pages/Dashboard.tsx` 는 이미 staged (이전 F-6d/F-7 작업 결과). 동시 작업 에이전트 흔적은 없음. F-8 변경을 그 위에 추가하는 방향으로 진행.

### git fetch
- 원격 최신과 동기 상태.

### tsconfig 확인
- `noUnusedLocals: false`, `noUnusedParameters: false` 확인.
- 미사용 props 경고 없음. 단, 깔끔하게 미사용 함수(setVolume/exMap)는 제거.

---

## 3. 변경 사항

### 3-1. `src/components/dashboard/StrengthSection.tsx`
- 헤더 주석에 F-8 한 줄 추가.
- `StrengthSummaryRow` 타입 export 추가.
- Props에 `summaryRows: StrengthSummaryRow[]` 추가.
- 함수 시그니처는 `{ sessions, summaryRows }` 만 destructure (exConfigs/latestBodyWeight 는 props로 유지하되 내부 사용 없음).
- `setVolume()` 함수 제거 (호출 없음).
- `exMap` useMemo 제거 (호출 없음).
- 최근 30일 집계:
	- `recent30 = sorted.filter(...)` → `summary30 = summaryRows.filter(row => row.recorded_at >= since30)` useMemo.
	- `agg30`: `total_sets`, `total_volume` 합산 → `avgSetVolume = totalVolume / totalSets`.
	- `agg30.reps` 필드는 사용처 없어 제거.
- 혼합차트:
	- `chartConfig`: summary30을 `recorded_at` 기준 groupBy → `volume += row.total_volume`, `sets += row.total_sets`.
	- 차트 가시조건 `recent30.length >= 2` → `summary30.length >= 2`.
- 최신 운동 상세 카드 (`latest.exercises.map(...)`, `ex.sets.map(s => s.reps)...`) 구조 그대로 유지.
- `volumeDomain`, `niceFloor`, `niceCeil` 유틸은 차트에서 계속 사용 → 유지.

### 3-2. `src/pages/Dashboard.tsx`
- import에 `StrengthSummaryRow` 추가.
- `strengthSummaryRows` state 추가.
- `Promise.all` 에 `strength_daily_summary` select 추가 (limit 60, recorded_at desc).
- `setStrengthSummaryRows` 에서 numeric/bigint 를 `Number()` 변환, `avg_volume_per_set` 만 null 보존.
- `<StrengthSection>` 호출부에 `summaryRows={strengthSummaryRows}` 추가, 기존 `exConfigs`/`latestBodyWeight` props 도 유지.
- 기존 strengths(상세 카드용) 로드 로직(`strength_logs` + `strength_exercises` + `strength_sets`) 그대로 유지.

---

## 4. git diff (cached, F-8 분만)

### 4-1. StrengthSection.tsx 핵심 diff

```diff
+// F-8: 최근 30일 카드/차트를 strength_daily_summary 뷰 기반으로 전환 — 2026.05.22

+export type StrengthSummaryRow = {
+  session_id: string
+  recorded_at: string
+  label: string | null
+  total_sets: number
+  total_volume: number
+  avg_volume_per_set: number | null
+}

 type Props = {
   sessions: StrengthSessionFull[]
+  summaryRows: StrengthSummaryRow[]
   exConfigs: ExConfig[]
   latestBodyWeight: number | null
 }
```

```diff
-function setVolume(s: StrengthSetFull, cat: ExerciseCategory, ...) { ... }
-
-export default function StrengthSection({ sessions, exConfigs, latestBodyWeight }: Props) {
+export default function StrengthSection({ sessions, summaryRows }: Props) {
   const sorted = [...sessions].sort((a, b) => b.recorded_at.localeCompare(a.recorded_at))
   const latest = sorted[0]
-  const exMap = useMemo(() => { ... }, [exConfigs])
```

```diff
   const since30 = (() => { ... })()
-  const recent30 = sorted.filter(s => s.recorded_at >= since30)
+  const summary30 = useMemo(
+    () => summaryRows.filter(row => row.recorded_at >= since30),
+    [summaryRows, since30]
+  )

   const agg30 = useMemo(() => {
-    let sets = 0, reps = 0, volume = 0
-    for (const s of recent30) {
-      for (const ex of s.exercises) {
-        const conf = exMap[ex.exercise_name]
-        const cat = conf?.category ?? '기타'
-        const bw = conf?.bodyweight_ratio ?? null
-        for (const st of ex.sets) {
-          sets++; reps += st.reps || 0
-          volume += setVolume(st, cat, bw, latestBodyWeight)
-        }
-      }
+    let totalSets = 0
+    let totalVolume = 0
+    for (const row of summary30) {
+      totalSets += row.total_sets || 0
+      totalVolume += row.total_volume || 0
     }
     return {
-      sessions: recent30.length, sets, reps, volume,
-      avgSetVolume: sets > 0 ? volume / sets : null,
+      sessions: summary30.length,
+      sets: totalSets,
+      volume: totalVolume,
+      avgSetVolume: totalSets > 0 ? totalVolume / totalSets : null,
     }
-  }, [recent30, exMap, latestBodyWeight])
+  }, [summary30])
```

```diff
   const chartConfig = useMemo(() => {
     const byDay: Record<string, { volume: number; sets: number }> = {}
-    for (const s of recent30) {
-      if (!byDay[s.recorded_at]) byDay[s.recorded_at] = { volume: 0, sets: 0 }
-      for (const ex of s.exercises) {
-        ...setVolume 재집계...
-      }
+    for (const row of summary30) {
+      if (!byDay[row.recorded_at]) byDay[row.recorded_at] = { volume: 0, sets: 0 }
+      byDay[row.recorded_at].volume += row.total_volume || 0
+      byDay[row.recorded_at].sets += row.total_sets || 0
     }
     ...
-  }, [recent30, exMap, latestBodyWeight, activeMetrics])
+  }, [summary30, activeMetrics])
```

```diff
           {/* 혼합차트 */}
-          {recent30.length >= 2 && (
+          {summary30.length >= 2 && (
```

### 4-2. Dashboard.tsx 핵심 diff

```diff
-import type { StrengthSessionFull, ExConfig } from '../components/dashboard/StrengthSection'
+import type { StrengthSessionFull, ExConfig, StrengthSummaryRow } from '../components/dashboard/StrengthSection'

   const [strengths, setStrengths] = useState<StrengthSessionFull[]>([])
+  const [strengthSummaryRows, setStrengthSummaryRows] = useState<StrengthSummaryRow[]>([])
   const [exConfigs, setExConfigs] = useState<ExConfig[]>([])
```

```diff
-      const [b, r, sLogs, exConf] = await Promise.all([
+      const [b, r, sLogs, exConf, sSummary] = await Promise.all([
         supabase.from('body_records').select('*').order('recorded_at', { ascending: false }).limit(180),
         supabase.from('running_logs').select('*').order('recorded_at', { ascending: false }).limit(180),
         supabase.from('strength_logs').select('id, recorded_at, label').order('recorded_at', { ascending: false }).limit(60),
         supabase.from('exercise_configs').select('name, category, bodyweight_ratio'),
+        supabase.from('strength_daily_summary').select('session_id, recorded_at, label, total_sets, total_volume, avg_volume_per_set').order('recorded_at', { ascending: false }).limit(60),
       ])
```

```diff
+      setStrengthSummaryRows((sSummary.data || []).map((x: any) => ({
+        session_id: x.session_id,
+        recorded_at: x.recorded_at,
+        label: x.label,
+        total_sets: Number(x.total_sets ?? 0),
+        total_volume: Number(x.total_volume ?? 0),
+        avg_volume_per_set: x.avg_volume_per_set !== null && x.avg_volume_per_set !== undefined
+          ? Number(x.avg_volume_per_set)
+          : null,
+      })))
```

```diff
         {activeTab === 'strength' && (
-          <StrengthSection sessions={strengths} exConfigs={exConfigs} latestBodyWeight={bodies[0]?.weight_kg ?? null} />
+          <StrengthSection
+            sessions={strengths}
+            summaryRows={strengthSummaryRows}
+            exConfigs={exConfigs}
+            latestBodyWeight={bodies[0]?.weight_kg ?? null}
+          />
         )}
```

---

## 5. 검증 결과

### 5-1. npm run build

```javascript
> dallog@0.1.0 build
> tsc && vite build

vite v5.4.21 building for production...
transforming...
✓ 360 modules transformed.
dist/index.html                     1.79 kB │ gzip:   0.89 kB
dist/assets/index-D_f3UT5a.css      5.93 kB │ gzip:   1.81 kB
dist/assets/index-BUA8YdZY.js   1,013.48 kB │ gzip: 302.74 kB
✓ built in 2.43s
```

- tsc 통과, vite 번들 성공. TypeScript 오류 0건.

### 5-2. 항목별 체크
- Dashboard.tsx에 `strength_daily_summary` fetch 추가 — 확인.
- StrengthSection의 30일 카드(`agg30`)와 혼합차트(`chartConfig`)가 `summaryRows` 기반으로 계산 — 확인.
- 최신 운동 상세 카드(`latest.exercises.map(...)`, `ex.sets.map(s => s.reps)`) 구조 유지 — 확인.
- 빈 상태 판정 `bodies.length === 0 && runs.length === 0 && strengths.length === 0` 기존 그대로 유지.

---

## 6. git 스테이징 결과

```javascript
git add src/pages/Dashboard.tsx src/components/dashboard/StrengthSection.tsx
```

- `git add .` 또는 `git add -A` 사용 안 함.
- `git diff --name-only --cached` 결과는 이전 staged 파일들과 본 작업의 2개 파일을 포함:
	- .playwright-mcp/page-2026-05-20T08-23-18-022Z.yml (이전)
	- docs/260520_playwright_browsing_[report.md](http://report.md) (이전)
	- migrations/2026-05-22_records_updated_at.sql (이전)
	- package-lock.json (이전)
	- package.json (이전)
	- src/App.tsx (이전)
	- src/components/Layout.tsx (이전)
	- src/components/MarkdownBriefRenderer.tsx (이전)
	- **src/components/dashboard/StrengthSection.tsx ← F-8**
	- src/lib/supabase.ts (이전)
	- src/pages/CoachNotes.tsx (이전)
	- **src/pages/Dashboard.tsx ← F-8 변경 추가**

---

## 7. 비고
- `Dashboard.tsx` 는 이전 F-6d/F-7 작업이 이미 staged 되어 있던 상태에 F-8 변경을 누적해서 add 했음.
- 동시 작업 흔적은 발견되지 않음.
- `strength_logs / strength_exercises / strength_sets` 로드 로직은 최신 운동 상세 카드 표시용으로 그대로 유지 (지시 사항 준수).
- `setVolume()` 함수, `exMap` useMemo 는 더 이상 사용되지 않아 제거 (TS 미사용 경고 회피보다는 dead code 축적 방지 목적).
- `exConfigs`, `latestBodyWeight` props 는 호출부에서 계속 전달되도록 시그니처 유지 (지시 사항 준수). destructure 에서만 빠짐.
