---
title: "작업보고 — 종합 브리프 뷰어 신설(기간 일/주/월/커스텀별 집계 + 날짜 네비게이션, AI 토큰 0)"
category: "workreport"
document_type: "작업보고"
source_status: "generated"
knowledge_group: "03_history"
priority: "High"
purpose: "대시보드 BODY/RUNNING/STRENGTH 탭과 별개로, 사용자가 기간(일·주·월·커스텀)을 골라 그 구간의 운동·체성분 집계 수치를 카드로 보는 종합 브리프 뷰어를 신설한 작업의 풀 깊이 기록이다. 핵심은 모든 수치를 프론트엔드 순수함수(computeBrief)로 계산해 AI 토큰을 한 톨도 쓰지 않으면서, 평균 페이스 거리가중·심박 시간가중·체성분 합산금지 등 통계적으로 정확한 집계 규칙을 강제한 점이다. 향후 동일 집계 결과(BriefResult)에 AI 코칭 코멘트(aiComment)만 채워 넣으면 되도록 자리를 비워둔 확장 토대도 함께 남겼다."
read_when: ["브리프","집계","computeBrief","기간뷰","대시보드","최신상태복구"]
updated: "2026-06-12"
work_timestamp: "20260612_141400"
context: "달록본레포CC (D:\\dallog\\dallog_git) — 종합 브리프 뷰어(브랜치 feat/brief-viewer) 작업보고"
source_of_truth: "https://dallog-tools.hansbridge.co.kr/knowledge/"
---

# 작업보고 — 종합 브리프 뷰어 신설

> **이 문서가 무엇인가 (비개발자용 한 줄 설명)**
> 달록 대시보드 옆에 "기간을 골라서 그 동안의 운동·몸 상태 요약을 보여주는 새 화면"을 만든 작업 기록이다. 일/주/월/직접지정(커스텀) 네 가지로 묶어 보여주고, 화살표·날짜선택·손가락 밀기(스와이프)로 날짜를 이동한다. 이 모든 숫자 계산은 AI에게 묻지 않고 코드가 직접 계산하므로 **AI 사용료(토큰)가 전혀 들지 않는다.**

---

## 0. 한눈 요약

| 항목 | 내용 |
|---|---|
| 무엇 | 신규 `/brief` 라우트(화면 주소) — 기간별 집계 뷰어 |
| 왜 | 대시보드는 "지금 상태"만 보여줌. "이 주/이 달 어땠나"를 묶어 보는 화면이 없었음 |
| 어떻게 | 집계 순수함수(입력만으로 출력 결정·부작용 없음) `computeBrief` + 기간 해석 유틸 `period.ts` + 뷰어 페이지 `Brief.tsx` |
| AI 토큰 | **0** (모든 수치는 코드 수식, LLM 호출 없음) |
| 기존 화면 영향 | **0** (대시보드 집계 로직 무수정, 진입 링크만 1줄 추가) |
| 커밋 | `0dbb4cc` (2026-06-12 14:14, 브랜치 `feat/brief-viewer`) — **main 머지 전 상태** |
| 변경 규모 | 6파일 696줄 추가(신규 3파일 + 기존 3파일 소폭 추가) |
| 검증 | CC↔Codex 산술 교차검증 통과 + 런타임 31단언 |

---

## 1. 무엇을 만들었나 (산출물)

### 1.1 신규 파일 3종

| 파일 | 줄수 | 역할 |
|---|---|---|
| `src/lib/brief/computeBrief.ts` | 259 (신규) | 집계 순수함수. 도메인 3종(BODY/RUNNING/STRENGTH) × 기간 4종을 동일 스키마로 집계 |
| `src/lib/brief/period.ts` | 142 (신규) | 기간(일/주/월/커스텀) → 실제 날짜 구간 `[start, end]` 변환 + 네비게이션 유틸 |
| `src/pages/Brief.tsx` | 261 (신규) | 뷰어 화면. 도메인/기간 탭 + 날짜 네비(화살표·피커·스와이프) + 카드 렌더 |

### 1.2 기존 파일 소폭 추가(수정 아닌 추가만)

| 파일 | 추가 | 내용 |
|---|---|---|
| `src/App.tsx` | +3 | `/brief` 라우트 등록(`<Route path="brief" element={<Brief />} />`) + import |
| `src/index.css` | +28 | 뷰어 전용 클래스(`.brief-vw-*`). 기존 클래스 미수정 |
| `src/pages/Dashboard.tsx` | +3 | 대시보드 상단에 "종합 브리프 →" 진입 링크 1줄 + `Link` import. **집계 로직 무수정** |

---

## 2. 왜 만들었나 (배경·문제)

기존 대시보드의 BODY/RUNNING/STRENGTH 탭은 "현재 상태"와 개별 기록을 보여주지만, **"이번 주/이번 달을 묶어서 어땠는지"를 한눈에 보는 집계 뷰가 없었다.** 또한 `SummaryBrief.tsx`(AI에게 보낼 요약 카드)는 있었으나, 사용자가 기간을 직접 골라 날짜를 넘겨가며 보는 인터랙티브 뷰어는 부재했다.

핵심 제약은 **비용**이다. 단순히 "AI에게 이 기간 기록 요약해줘"로 만들면 매 조회마다 LLM 토큰이 소모된다. 수치 집계(합산·평균·증감)는 결정론적 계산이므로 AI가 필요 없다. 그래서 **수치는 코드가 계산하고 AI는 차후 해석(코멘트)만 얹는** 2레이어 구조로 설계했다.

---

## 3. 핵심 설계원칙 (절대 규칙)

설계 명세 `docs/go_work/brief_feature_prompt_260612.md` §1에서 못박은 5대 원칙이다. 코드 전반이 이 규칙을 강제하도록 작성됐다.

| # | 원칙 | 의미 | 코드상 보장 |
|---|---|---|---|
| **P1** | 수치는 코드, 해석은 AI | 합산·평균·증감률은 순수 JS 함수. **LLM 호출 금지** | `computeBrief.ts` 어디에도 fetch/LLM 호출 없음 |
| **P2** | 집계함수는 순수함수 | 입력(기록 배열 + 기간) → 출력(집계 객체). 부작용(side-effect) 없음 | I/O·DOM 접근 0. 동일 입력 → 동일 출력 |
| **P3** | 기간모드 단일 인터페이스 | daily/weekly/monthly/custom이 **동일한 출력 스키마**(`BriefResult`) 반환 | `metricsFor()`가 모드 무관 동일 구조 반환 |
| **P4** | 텍스트 자리는 비워둔다 | `aiComment` 필드는 null 허용. UI는 없으면 숨김 | `aiComment: null` 고정. `{result.aiComment && ...}` 조건 렌더 |
| **P5** | 기존 대시보드 영향 0 | 신규 `/brief` 라우트로 분리. 기존 BODY/RUNNING/STRENGTH 미수정 | Dashboard.tsx는 진입 링크 1줄만 추가 |

> 라우트(route)=화면 주소. 순수함수=입력만으로 출력이 정해지고 외부 상태를 건드리지 않는 함수. aiComment=차후 AI가 채울 코칭 코멘트 텍스트 자리.

---

## 4. 집계 규칙 — 산술 정확성이 생명 (`computeBrief.ts`)

집계는 통계적으로 틀리기 쉬운 지점이 많아, 규칙을 명시적으로 못박았다. 각 메트릭(metric, 지표)은 `value`와 함께 **`aggType`(집계 방식)을 메타데이터로** 들고 다닌다.

### 4.1 RUNNING — 가중평균이 핵심

| 지표 | 집계 방식 | 공식·주의 |
|---|---|---|
| 총 거리 | 합산(sum) | `Σ distance_km` |
| 세션 수 | 카운트(count) | 기록 건수 |
| 총 시간 | 합산(sum) | `Σ duration_sec` |
| **평균 페이스** | **거리 가중평균** | **`총시간초 ÷ 총거리km`** — 각 세션 페이스를 단순 산술평균하면 **틀림** |
| 평균 심박 | 시간 가중평균 | `Σ(bpm·duration) ÷ Σduration`. 시간 없는 항목 제외 |
| 평균 케이던스 | 시간 가중평균 | 동일 가중 방식 |
| 총 칼로리 | 합산(sum) | `Σ calories` |

> **왜 거리 가중인가** — 1km를 5분에 뛴 세션과 10km를 6분 페이스로 뛴 세션을 단순평균하면 "5.5분"이 나오지만, 실제 평균 페이스는 총시간을 총거리로 나눈 값이라 6분에 가깝다. 거리(또는 시간)로 가중해야 정확하다. 가중평균=각 값에 비중을 곱해 평균낸 값.
> `is_record === false`(비기록 러닝)는 제외 — 대시보드·SummaryBrief와 동일 규칙으로 통일.

### 4.2 BODY(체성분) — 합산 금지

| 지표 | 집계 방식 | 비고 |
|---|---|---|
| 체중/골격근/체지방량/체지방률 | **마지막값(last) + 평균(avg) + 변화(change)** | last=현재 상태, avg=대표값, change=첫값→마지막값 차이 |

> ⚠️ **체성분은 합산 절대 금지.** 체중 70kg + 71kg = 141kg 은 무의미하다. 그래서 `last`(구간 마지막 측정값)·`avg`(구간 평균)·`change`(구간 첫값 대비 변화량, 예 -1.8kg)만 제공한다. `change`는 데이터 2건 이상일 때 차이, 1건이면 0, 0건이면 null.
> 방향 색칠 기준 `betterUp`: 체중·체지방량·체지방률은 감소가 좋음(false), 골격근은 증가가 좋음(true).

### 4.3 STRENGTH(근력) — SummaryBrief와 동일 볼륨 공식

| 지표 | 집계 방식 | 공식 |
|---|---|---|
| 세션 수 | 카운트 | 세션 건수 |
| 총 세트 | 합산 | `Σ sets.length` |
| 총 볼륨(kg·rep) | 합산 | 카테고리별 세트 볼륨 합 |
| 세트당 평균볼륨 | perSet | `총볼륨 ÷ 총세트수` |
| 종목별 분포 | 카운트 그룹핑 | 종목명 → 세트수, 내림차순 |

**세트 볼륨 공식(`setVolume`)** — `SummaryBrief.tsx`와 **동일 카테고리 공식**으로 수치 일관성을 보장했다.

| 카테고리 | 볼륨 |
|---|---|
| 맨몸 | `(체중 × bodyweight_ratio/100 + 추가중량) × reps` |
| 웨이트·머신 | `weight_kg × reps` |
| 아이소메트릭/기타 | 0 (볼륨 정의 없음) |

> 맨몸 볼륨 산정용 체중은 body 기록(오름차순)의 마지막 비-null 값을 사용. 종목 카테고리·체중부하율은 `exercise_configs` 테이블에서 조회(조회 실패 시 조용히 넘기지 않고 throw — 볼륨 정확성 의존).

### 4.4 기간(주 시작) — weeklyMileage와 통일

- **주 시작 = 월요일.** 달록 RUNNING "주간 마일리지"(`weeklyMileage.ts`)의 `mondayOf`와 동일 규칙을 복제했다(그쪽 함수는 미-export이라 `period.ts`에 동일 로직 재구현).
- 날짜는 전부 **로컬 자정 기준**으로 파싱(`parseLocalDate`) — UTC 파싱 시 타임존 밀림으로 날짜가 하루 어긋나는 버그 회피.

| mode | 구간 정의 |
|---|---|
| daily | anchor 당일 |
| weekly | anchor가 속한 주(월~일) |
| monthly | anchor가 속한 달(1일~말일, `new Date(y, m+1, 0)`로 말일 산출) |
| custom | rangeStart~rangeEnd(양 끝 포함, 역순이면 스왑) |

### 4.5 delta(증감) 규칙

- daily→전일, weekly→전주, monthly→전월 대비. **custom→null**(비교 대상 모호, 명세 §2.4).
- `prevAnchor()`로 직전 구간 기준일 산출 → 같은 키 메트릭끼리 `{abs, pct, direction}` 비교.
- `change`(체성분 변화) 메트릭은 그 자체가 구간 내 증감이라 직전구간 대비 표시는 생략(혼동 방지).

---

## 5. 뷰어 UI 동작 (`Brief.tsx`)

### 5.1 레이아웃·재사용

기존 디자인 시스템 클래스를 **재사용**해 일관성을 유지했다.

- 도메인 탭(체성분/러닝/근력) — 기존 `.toggle-group`·`.toggle-opt`
- 기간 모드 탭(일/주/월/커스텀) — 기존 `.pill`·`.is-on`
- 신규 클래스는 `.brief-vw-*` 접두어로만 추가(기존 클래스 미수정)

### 5.2 날짜 네비게이션 3종

| 방식 | 동작 |
|---|---|
| 화살표 ‹ › | 한 단위(일/주/월) 이전·다음. `shiftAnchor` |
| 날짜 버튼 | 라벨 위에 투명 `<input type="date">` 오버레이 → 클릭 시 네이티브 피커 |
| 스와이프/드래그 | `pointerdown`/`pointerup`. 좌=다음, 우=이전 |

**스와이프 임계값** — 가로 이동 **50px 이상** & 세로 이동 **30px 미만**일 때만 발동(세로 스크롤 오작동 방지). 카드에 `touch-action: pan-y` 적용해 세로 스크롤은 살리고 가로만 가로챈다. 스와이프=손가락 밀기.

**미래 상한** — 오늘 이후로는 이동 불가. `canNext`가 `next.start <= todayISO()`로 판정, 다음 화살표 `disabled` 처리, custom 모드는 네비 비활성.

### 5.3 모드 전환·표시 포맷

- 모드 전환 시 anchorDate 유지하되 해당 모드 구간으로 재계산(예: 일간 6/12 → "주" 누르면 6/8~6/14 주간). custom 진입 시 현재 anchor 구간으로 초기화.
- 표시 포맷: 페이스 `m'ss"`, 시간 `h:mm:ss`, change 메트릭은 양수에 `+` 부호, 값 null이면 `—`.
- 기록 0건이면 "해당 기간 기록 없음", 근력은 종목별 분포 칩 추가 렌더.

### 5.4 데이터 흐름

```
기록(Supabase: fetchBodyRecords/fetchRunningLogs/fetchStrengthSessions + exercise_configs)
   → computeBrief(records, period)  ← 순수함수, AI 토큰 0
   → BriefResult(수치)
   → 브리프 카드 렌더 (aiComment=null이라 텍스트 영역 숨김)
   → [차후] AI에게 BriefResult 전달 → aiComment 채움
```

---

## 6. Codex 검토 반영 결과

본 작업은 산술 정확성이 핵심이라 CC↔Codex 검증 루프를 거쳤다.

- **CC↔Codex 산술 교차검증 통과** — 평균 페이스 거리가중, 심박/케이던스 시간가중, 체성분 합산금지, 근력 볼륨 공식(SummaryBrief 일치), 주/월 경계(월요일 시작·말일) 오프바이원을 교차 확인.
- **런타임 31단언(assertion) 통과** — 실제 계산값과 수기 기대값을 31개 단언으로 대조, 전부 통과. (단언=코드가 "이 값은 반드시 이래야 한다"고 검사하는 자동 확인)
- 4분류 처리: 본 작업에서 Codex 지적은 [즉시반영](가중평균 공식·경계 처리) 위주로 흡수, [반려]·[보류] 잔여 없음.

---

## 7. 결과·검증

- 신규 `/brief` 라우트 정상 진입, 도메인·기간 탭 전환·날짜 네비 3종(화살표/피커/스와이프) 동작 확인.
- 대시보드 상단 "종합 브리프 →" 링크에서 진입, "← 대시보드"로 복귀.
- **기존 대시보드·SummaryBrief 무영향**(공존). 집계 로직 무수정 확인.
- AI 토큰 소모 0 확인(LLM 호출 코드 부재).

---

## 8. 미해결·후속(명시적 비범위)

이번 작업에서 **의도적으로 안 한 것**(명세 §6)이다.

| 항목 | 상태 |
|---|---|
| AI 코칭 코멘트(aiComment) 텍스트 생성 | 차후. 자리(null)만 비워둠 — BriefResult를 AI에 넘겨 채우면 됨 |
| 브리프 DB 저장 | 안 함. 조회 시점 즉석 집계 |
| 브리프 PDF/이미지 내보내기 | 차후 |
| 소셜 공유 | 차후 |
| **main 머지** | **미완** — 본 커밋(`0dbb4cc`)은 브랜치 `feat/brief-viewer` 상태, main 머지 전 |

---

## 9. 판단 근거(컨텍스트 노트)

- **2레이어 분리(수치=코드 / 해석=AI)의 근거** — 집계는 결정론적이라 AI가 불필요하고, 매 조회마다 토큰을 쓰는 건 낭비다. 수치는 즉시·무료·정확하게, AI는 차후 가치 더하는 코멘트에만. 트레이드오프: 지금은 텍스트 인사이트가 없지만, aiComment 자리를 표준화해둬 확장 비용을 최소화했다.
- **신규 라우트 분리의 근거(P5)** — 기존 대시보드를 건드리면 회귀 위험. 진입 링크 1줄만 추가해 영향 면적을 0에 수렴시켰다.
- **weeklyMileage 규칙 복제의 근거** — 그쪽 함수가 미-export라 import 불가. 주 시작 요일 불일치는 사용자 혼란의 직접 원인이라, 규칙을 복제해서라도 통일을 우선했다(미해결 의문: 추후 공통 유틸로 추출해 중복 제거 검토 여지).

---
