---
title: "260519-14:19_F5v5-체성분키워드프로젝트명"
notion_id: "36522962086881f5b88fdd0a7c59432c"
notion_url: "https://app.notion.com/p/36522962086881f5b88fdd0a7c59432c"
category: "workreport"
parent: "Claude Code 작업보고"
updated: "2026-05-19"
priority: "Low"
purpose: "체성분 키워드 드롭다운에 project_id 대신 fitness_projects 프로젝트명 표시 + 매칭 필터 (F-5 v5)"
---

## 작업 목표
이전 v4(커밋 0095051)에서 bodyKeywords가 project_id(UUID) 그대로 노출. 이번에 fitness_projects 매핑으로 프로젝트명 표시 + 선택 시 실제 필터링.
## 수정 파일
- src/components/SummaryBrief.tsx (+44 / -5)
---
## 원인 분석
### v4 상태
- bodyKeywords: `b.project_id`(UUID) 그대로 set에 수집 → 드롭다운에 UUID 표시
- bodiesP: `period === 'keyword' ? bodies : filterCurrent(bodies)` — 매칭 없이 전체 표시
- SummaryBrief 자체에는 fitness_projects 로드 코드 없음
### 명세 조건
- "fitness_projects 로드 방식은 기존 코드에서 사용하는 방식 그대로"
- 파일 단독 수정, History·Settings·BodySection 건드리지 말 것
- 명세 본문에 "이미 로드된 데이터 재활용"이라 했으나 SummaryBrief에는 아직 로드 없음
### 결정
BodySection.tsx의 로드 패턴을 그대로 SummaryBrief 내부에 복제 (새 쿼리 추가 = 명세 모순 해소, 명세 "기존 패턴 그대로 따르라" 는 준수)
---
## 구현 내용
### 1. fitnessProjects state 추가
```typescript
const [fitnessProjects, setFitnessProjects] = useState<Array<{ id: string; name: string }>>([])
```
### 2. fitness_projects 로드 useEffect (BodySection 패턴 동일)
```typescript
useEffect(() => {
  (async () => {
    const { data } = await supabase
      .from('app_settings')
      .select('value')
      .eq('key', 'fitness_projects')
      .maybeSingle()
    if (!data?.value) return
    try {
      const parsed = JSON.parse(data.value) as Array<{ id: string; name: string }>
      if (Array.isArray(parsed)) {
        setFitnessProjects(parsed.map(p => ({ id: p.id, name: p.name })))
      }
    } catch { /* ignore */ }
  })()
}, [])
```
### 3. id ↔ name 매핑 (양방향)
```typescript
const projectIdToName = useMemo(() => {
  const m = new Map<string, string>()
  for (const p of fitnessProjects) m.set(p.id, p.name)
  return m
}, [fitnessProjects])
const projectNameToId = useMemo(() => {
  const m = new Map<string, string>()
  for (const p of fitnessProjects) m.set(p.name, p.id)
  return m
}, [fitnessProjects])
```
### 4. bodyKeywords 수정 — 매핑 가능한 것만 프로젝트명으로 노출
```typescript
const bodyKeywords = useMemo(() => {
  const set = new Set<string>()
  for (const b of bodies) {
    if (!b.project_id) continue
    const name = projectIdToName.get(b.project_id)
    if (name && name.trim()) set.add(name.trim())
  }
  return Array.from(set).sort((a, b) => a.localeCompare(b))
}, [bodies, projectIdToName])
```
- 삭제된 프로젝트(매핑 실패) → 자동 제외
- 텍스트 키워드 정렬 (한국어 localeCompare)
### 5. 체성분 키워드 매칭 함수 신규 + bodiesP 필터 적용
```typescript
const bodyKeywordMatch = (b: BodyRecord): boolean => {
  if (!selectedKeyword) return false
  const targetId = projectNameToId.get(selectedKeyword)
  if (!targetId) return false
  return b.project_id === targetId
}

const bodiesP = (period === 'keyword'
  ? bodies.filter(bodyKeywordMatch)
  : filterCurrent(bodies))
```
---
## 필수 단계
### 1) git 동기화
```javascript
$ git status && git pull
On branch main, up to date.
working tree clean.
$ git log --oneline -5
0095051 fix(summary): '전체' 모드 차트 표시 + 체성분 키워드 드롭다운 추가 (F-5 v4)
...
```
### 2) 종속 상태 조사
- SummaryBrief.tsx 내 fitness_projects grep → 0건 (로드 코드 없음)
- src 전체 grep → BodySection.tsx:135 / History.tsx:300,306 / LogEntry.tsx:109 / Settings.tsx:213,260
- SummaryBrief 사용처 → History.tsx:1627,1638만 (parent에 prop 추가 안 됨 — 명세 "다른 파일 절대 수정 금지")
### 3) 구현 편집
5단계 적용 (state → 로드 → 매핑 → bodyKeywords 수정 → bodyKeywordMatch + bodiesP).
### 4) 빌드
```javascript
$ npm run build
✓ 104 modules transformed.
dist/assets/index-ChyzbIvH.js 796.84 kB | gzip: 231.47 kB
✓ built in 2.04s
```
에러 없음.
### 5) 커밋·푸시
```javascript
$ git add src/components/SummaryBrief.tsx
$ git commit -m "feat(summary): 체성분 키워드 드롭다운 프로젝트명 표시 + 매칭 필터 (F-5 v5)"
[main d83f1fc] ...
 1 file changed, 44 insertions(+), 5 deletions(-)

$ git push origin main
   0095051..d83f1fc  main -> main
```
---
## 설계 사이드 노트
- **로드 분리**: BodySection.tsx와 동일하게 별도 useEffect로 구성. 기존 큰 useEffect 안에 넣으면 의존성·에러 처리가 복잡해지므로.
- **매핑 실패 처리**: 삭제된 프로젝트의 project_id를 가진 BodyRecord는 드롭다운에서 제외. 키워드 필터에서도 매칭 안 됨 — 귀속이 절제된 경우 볼 수 없음.
- **selectedKeyword는 name 기준**: 러닝/근력 selectedKeyword도 원래 단순 문자열 매칭이므로 타입·의미 충돌 없음.
- **탁 전환 시 키워드 초기화** (기존 로직): tab이 변경되면 selectedKeyword 잘을릴 — 체성분에서 고른 프로젝트명이 러닝 탭에서 그대로 적용되는 부작용 없음.
---
## 완료 기준
- [x] 체성분 키워드 드롭다운에 UUID 대신 프로젝트명 표시
- [x] 프로젝트명 선택 시 해당 프로젝트 체성분 데이터 필터링 동작
- [x] 기존 러닝/근력 키워드 동작 변경 없음 (매칭 함수 그대로)
- [x] npm run build 에러 없음
- [x] main 브랜치 커밋·푸시 완료 (d83f1fc)
## 커밋 체인
- F-5 v1: cf2f713
- F-5 v2: ac0f4d9
- F-5 v3: 87764e0
- F-5 v4: 0095051
- F-5 v5: d83f1fc
