---
title: "📷 260523-토요일_H1H2-OCR자동기록-MVP구현"
notion_id: "36922962086881a59e45e9a2eb5b2fd6"
notion_url: "https://app.notion.com/p/36922962086881a59e45e9a2eb5b2fd6"
category: "workreport"
parent: "Claude Code 작업보고"
updated: "2026-05-23"
priority: "High"
purpose: "달록 H-1/H-2 OCR 자동기록 기능 v0.9.1 MVP 1차 구현 (tesseract.js 브라우저 OCR + 공통 후보 정규화 + 보정 UI)"
read_when: ["OCR기능","기능파악"]
---

작업일: 2026-05-23 (토)
에이전트: Claude Opus 4.7 (1M context)

---

## 1. 작업 목표

달록 H-1/H-2 OCR 자동기록 기능을 v0.9.1 MVP 수준으로 1차 구현.

- H-1: 스크린샷 업로드 후 OCR로 텍스트 추출
- H-2: OCR 결과를 공통 기록 필드 후보로 정규화 → 사용자 보정 → 기존 LogEntry 폼에 값 주입
- 저장은 OCR이 직접 하지 않고, 기존 저장 버튼·ConfirmDialog·useDemoBlock 흐름을 그대로 사용

---

## 2. 구현 방향 (H-1/H-2 원칙)

- 특정 앱 전용 OCR로 만들지 않음 (삼성헬스/Garmin/Apple Health·Fitness/Strava/NRC/기타 공통)
- v0.9.1에서는 앱별 완벽 자동감지·전용 파서 보류
- 범용 OCR → 공통 필드 후보 추출 → 사용자 보정 → 기존 LogEntry 폼 주입
- 브라우저 내 OCR (`tesseract.js`), 이미지/원문 서버 저장 없음

---

## 3. 수정 파일 목록

신규:

- `src/lib/ocr/types.ts` — `OcrSourceApp`, `OcrRecordType`, `OcrCandidate`, `OcrNormalizedCandidates`, `OcrParseResult`, `APP_HINT_OPTIONS`
- `src/lib/ocr/normalize.ts` — 날짜·거리·시간·페이스·정수·소수 정규화, 페이스 재계산/불일치 판정, fat_pct→fat_kg 역산
- `src/lib/ocr/parseCandidates.ts` — OCR 원문 → 공통 후보 추출 (키워드 인접 검색, 정규식 기반), 앱 자동감지, 기록유형 추정
- `src/lib/ocr/extractText.ts` — `tesseract.js` 래퍼 (`kor+eng`)
- `src/components/ocr/OcrImportModal.tsx` — OCR 모달 (업로드·앱 힌트·OCR 실행·원문 접기/펼치기·기록유형 선택·보정폼·적용/취소)

수정:

- `src/pages/LogEntry.tsx` — 헤더에 `📷 OCR로 불러오기` 버튼, 모달 마운트, `applyOcrBody`/`applyOcrRunning`/`applyOcrStrength` 핸들러 추가
- `package.json` / `package-lock.json` — `tesseract.js@^7.0.0` 추가

금지사항 준수:

- Supabase 스키마 변경 없음
- OCR 이미지/원문 DB 저장 없음
- OCR 결과 자동 저장 없음
- 기존 `handleBodySave`/`handleRunSave`/`handleStrSave` 손대지 않음
- `History.tsx` 진입점 추가 없음
- AI 브리프/auth/session/router 수정 없음
- 기존 PostSaveDialog·연속 기록·최근값 유지 로직 유지
- `git add .` / `git add -A` 사용하지 않음 (파일 명시 스테이징)

---

## 4. 설치한 라이브러리

- `tesseract.js@^7.0.0` (브라우저 내 OCR)
- 다른 라이브러리 추가 없음

---

## 5. OCR 동작 방식

1. LogEntry 헤더의 `📷 OCR로 불러오기` 클릭 → `OcrImportModal` 열림 (현재 탭이 초기 기록유형으로 전달됨)
2. 모달에서 이미지 파일 업로드 + 앱 힌트 선택 (선택사항, sourceAppHint 메타로만 사용)
3. `OCR 실행` 클릭 → `tesseract.js` recognize(image, 'kor+eng') 호출, 진행률 표시
4. 완료 시 원문 텍스트 + 줄별 split → `parseOcrCandidates`로 공통 후보 추출
5. OCR 원문은 접기/펼치기로 사용자가 검토 가능
6. 사용자가 기록 유형 선택 (체성분/러닝/근력) — 추정 유형이 기본값
7. 선택 유형의 보정 폼에서 값 수정
8. `적용` 클릭 → 부모 LogEntry의 `setBodyForm`/`setRunForm`/`setStrForm` 호출, 해당 탭으로 이동
9. 사용자가 기존 저장 버튼 클릭 → 기존 ConfirmDialog → 기존 저장 함수 → PostSaveDialog

---

## 6. 후보 필드 추출 방식

- 줄 단위 인접 키워드 검색 (`findNearby`): 같은 줄에서 키워드 뒤에 숫자가 있으면 우선, 없으면 다음 줄
- 단위 기반 정규식 (km, kcal, kg, %, bpm, '"  M:SS /km 등)
- 모든 후보에 `confidence` (high/medium/low) + `needsReview` + `reason`
- 페이스 재계산: `distance_km`·`duration_sec`가 모두 있으면 `duration_sec / distance_km` 비교. 차이가 ±15초 또는 ±10%이면 needsReview
- fat_pct만 있을 때 `fat_kg = weight_kg * fat_pct / 100` 역산 (확인 필요 표시)
- 추출 가능 필드: 날짜·운동종류·거리·시간·페이스·평균/최대 심박·칼로리·메모·체중·체지방률·골격근량·BMR·루틴명·프로젝트명

---

## 7. 체성분 / 러닝 / 근력 적용 결과

### 체성분 (`applyOcrBody`)

- `recorded_at` → `bodyForm.recorded_at`
- `weight_kg`/`muscle_kg`/`fat_kg` (역산 가능) → 각 필드
- `note` → `bodyForm.note`
- `project_name`이 현재 `fitness_projects` 목록과 정확 일치 시 자동 `project_id` 선택, 아니면 사용자 수동 확인 유도
- `fat_pct`/`bmr`은 참고값으로만 표시 (저장은 기존 자동계산 유지)

### 러닝 (`applyOcrRunning`)

- `recorded_at`/`distance_km`/`avg_bpm`/`max_bpm`/`calories`/`note` → 직접 주입
- `duration_sec` → `duration_min` + `duration_sec` 분리
- `pace_sec_per_km` → 참고 표시만 (저장은 기존 자동계산)
- `workout_type` → `run_types` (run_type_configs) 와 대소문자 무시 정확 일치 시 자동 선택, 아니면 드롭다운에서 사용자 수동 선택

### 근력 (`applyOcrStrength`)

- `recorded_at` → `strForm.recorded_at`
- `routine_name` 또는 `workout_type` → `strForm.label` 후보
- 종목/세트/반복/중량 자동 구성은 v0.9.1에서 보류 (저장된 루틴 불러오기로 유도)
- 메모 필드는 현재 strForm에 없어 적용 안 함

---

## 8. 모바일 / 데스크탑 확인 결과

### 데스크탑 1440px

- 헤더의 `📷 OCR로 불러오기` 버튼이 `닫기 ×` 옆에 정상 표시
- 모달 max-width 560px로 화면 중앙 정렬
- 파일 업로드·앱 힌트·OCR 실행 버튼 그리드(1:1) 정상

### 모바일 390px

- 헤더에 `flexWrap: 'wrap'` 적용 — 제목·OCR 버튼·닫기 버튼이 자연스럽게 한 줄에 배치
- 모달은 화면 폭에 맞춰 가득 차고 좌우 16px 패딩, 세로 스크롤 정상
- 후보 보정 폼의 grid(1:1) 필드들도 모바일 폭에서 깨지지 않음

### 검증 스크린샷 (작업 디렉토리에 남김, 스테이징 안 함)

- `ocr_logentry_1440_header.png`
- `ocr_modal_1440_initial.png`
- `ocr_logentry_390_header.png`
- `ocr_modal_390_initial.png`

---

## 9. npm run build 결과

```javascript
vite v5.4.21 building for production...
✓ 412 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-BE_zh7jt.js   1,062.62 kB │ gzip: 318.99 kB
(!) Some chunks are larger than 500 kB after minification.
✓ built in 2.54s
```

TypeScript 컴파일 0 오류. 번들 사이즈 경고는 tesseract.js 포함에 따른 정상 증가 (gzip 319KB).

---

## 10. 남은 이슈

### v0.9.1 배포 비차단

- 번들 사이즈 1MB 경고 — 동적 import로 OCR 모듈을 코드 분할하면 초기 로딩 개선 가능 (후속 작업)
- tesseract.js의 worker/wasm/lang data는 CDN에서 로드 — 초회 OCR 실행 시 수 MB 다운로드 발생 (의도된 동작)
- 근력은 종목/세트 자동 구성 보류 — v0.9.1 범위로 명시
- 앱별 자동 감지는 보조용으로만 (sourceAppHint), 파싱 분기에는 영향 없음

### v0.9.1 배포 차단

- 없음

---

## 11. git diff 결과

### `git diff --name-only` (unstaged)

```javascript
(없음)
```

### `git diff --cached --name-only` (staged)

```javascript
package-lock.json
package.json
src/components/ocr/OcrImportModal.tsx
src/lib/ocr/extractText.ts
src/lib/ocr/normalize.ts
src/lib/ocr/parseCandidates.ts
src/lib/ocr/types.ts
src/pages/LogEntry.tsx
```

### untracked (보호 — 건드리지 않음)

```javascript
.playwright-mcp/
docs/260523_H1_H2_OCR_implementation_plan.md
docs/260523_pacelog_ai_handoff_data.md
docs/달록 PaceLog — AI 핸드오프 데이터_260523.md
verify_dashboard_1440.png
verify_dashboard_390.png
ocr_logentry_1440_header.png (검증용)
ocr_modal_1440_initial.png (검증용)
ocr_logentry_390_header.png (검증용)
ocr_modal_390_initial.png (검증용)
```

---

## 12. 작업 흐름 요약

1. 작업 전 `git status` 확인 (untracked 보호 대상 식별)
2. 설계서 `docs/260523_H1_H2_OCR_implementation_plan.md` 통독 (수정 안 함)
3. `src/pages/LogEntry.tsx`, `src/components/Modal.tsx`, `package.json` 구조 파악
4. `npm install tesseract.js` (^7.0.0 추가)
5. OCR 유틸 4종 작성 (types/normalize/parseCandidates/extractText)
6. `OcrImportModal.tsx` 작성 (file upload, 진행상태, 원문 토글, 기록유형 탭, 보정폼, 적용/취소)
7. `LogEntry.tsx` 수정 — import, ocrOpen state, 헤더 OCR 버튼, 3개 apply 핸들러, 모달 마운트
8. `npm run build` → 성공
9. dev 서버 띄우고 playwright로 1440px·390px 시각 검증 (마스터 로그인 후 LogEntry → OCR 모달 열기)
10. 8개 파일 명시적 스테이징 (`git add` 파일별)
11. dev 서버 종료, 노션 보고

---

## 13. 결론

H-1/H-2 OCR 자동기록 MVP가 v0.9.1 범위로 완료됨. 기존 LogEntry 흐름(저장 함수·ConfirmDialog·useDemoBlock·PostSaveDialog·연속기록·최근값 유지)은 손대지 않고, OCR 결과를 폼 상태에만 주입하는 입력 보조 방식으로 안전하게 통합. 앱별 전용 파서를 만들지 않아 H-1/H-2의 범용 설계 목적과 일치.

---

## 14. 부록 A — 설계서 원문 (`docs/260523_H1_H2_OCR_implementation_plan.md`)

> 📐 **원본 파일**: `docs/260523_H1_H2_OCR_implementation_plan.md` (2026-05-23 작성)
> 본 MVP 구현(§1~§13)의 직접 근거 설계서. 본 작업보고 §2 "구현 방향"이 본 설계서를 통독한 결과를 적용한 것이며, 본 부록은 노션 단일 참조 보존을 위해 본문 임베드, 디스크 원본은 정리 대상.
> 본 부록 내부 헤딩(## / ###)은 원본을 그대로 보존하므로 본 작업보고의 § 번호와 별개의 독립 트리로 본다.

작성일: 2026-05-23

## 1. 현재 기록 구조 요약

### 체성분 기록 입력 구조

- 진입 화면: `src/pages/LogEntry.tsx`
- 탭 값: `body`
- 폼 상태: `bodyForm`
	- `recorded_at`
	- `weight_kg`
	- `muscle_kg`
	- `fat_kg`
	- `note`
	- `project_id`
- 자동 계산:
	- `fat_pct`: `fat_kg / weight_kg * 100`
	- `bmr`: `370 + 21.6 * (weight_kg - fat_kg)`
- 프로젝트:
	- `app_settings.fitness_projects`에서 프로젝트 목록을 읽음
	- 목표일이 남은 프로젝트 중 가장 가까운 값을 기본 `project_id`로 1회 반영
- 저장:
	- `body_records`에 `recorded_at` 기준 `upsert`
	- 저장 컬럼: `recorded_at`, `weight_kg`, `muscle_kg`, `fat_kg`, `fat_pct`, `bmr`, `note`, `project_id`

### 러닝 기록 입력 구조

- 진입 화면: `src/pages/LogEntry.tsx`
- 탭 값: `running`
- 폼 상태: `runForm`
	- `recorded_at`
	- `distance_km`
	- `duration_min`
	- `duration_sec`
	- `avg_bpm`
	- `max_bpm`
	- `cadence_spm`
	- `calories`
	- `shoe`
	- `run_type`
	- `is_record`
	- `note`
- 별도 시각 상태: `runTime`
	- `period`
	- `hour`
	- `minute`
- 자동 계산:
	- `duration_sec`: 분/초 입력을 초 단위로 환산
	- `pace_sec_per_km`: `duration_sec / distance_km`
	- 화면 표시용 평균 페이스와 평균 속도
- 메타 목록:
	- `shoe_configs`에서 신발 목록 로드
	- `run_type_configs`에서 런타입 목록 로드
	- 런타입 설정이 비어 있으면 기본 런타입 후보 사용
- 저장:
	- `running_logs`에 `insert`
	- 저장 컬럼: `recorded_at`, `distance_km`, `duration_sec`, `pace_sec_per_km`, `avg_bpm`, `max_bpm`, `cadence_spm`, `calories`, `shoe`, `run_type`, `note`, `is_record`, `run_time_period`, `run_time_hour`, `run_time_minute`

### 근력 기록 입력 구조

- 진입 화면: `src/pages/LogEntry.tsx`
- 탭 값: `strength`
- 폼 상태: `strForm`
	- `recorded_at`
	- `label`
	- `exercises`
- 별도 시각 상태: `strTime`
	- `period`
	- `hour`
	- `minute`
- 운동 행 구조:
	- `name`
	- `sets`
- 세트 구조:
	- `reps`
	- `weight_kg`
	- `additional_weight_kg`
	- `use_additional`
- 메타 목록:
	- `exercise_configs`에서 운동명, 카테고리, 맨몸 비율 로드
	- `app_settings.saved_routines`, `app_settings.saved_exercises`에서 저장된 루틴/운동 로드
	- 최신 체성분 체중을 맨몸 볼륨 계산에 사용하고, 없으면 수동 체중 입력
- 저장:
	- `strength_logs`에 세션 본체 `insert`
	- 생성된 `log_id`로 `strength_exercises`를 운동별 `insert`
	- 생성된 `exercise_id`로 `strength_sets`를 세트별 `insert`
	- 저장 컬럼:
		- `strength_logs`: `recorded_at`, `label`, `workout_time_period`, `workout_time_hour`, `workout_time_minute`, `note`
		- `strength_exercises`: `log_id`, `exercise_name`, `order_index`
		- `strength_sets`: `exercise_id`, `set_index`, `reps`, `weight_kg`, `additional_weight_kg`, `use_additional`

### 기록 저장 함수 흐름

- 공통 진입:
	- 저장 버튼 클릭
	- `useDemoBlock().trySubmit`으로 데모 모드 저장 차단 확인
	- `ConfirmDialog`로 저장 여부 확인
	- 저장 함수 실행
	- 저장 성공 시 완료 알림 후 `/history?tab=...` 이동
- 체성분:
	- `handleBodySave`
	- `body_records.upsert(..., { onConflict: 'recorded_at' })`
- 러닝:
	- `handleRunSave`
	- 입력 문자열을 숫자와 초 단위로 정규화
	- `running_logs.insert`
	- 저장 후 러닝 폼과 러닝 시각 초기화
- 근력:
	- `handleStrSave`
	- `strength_logs.insert(...).select('id').single()`
	- 운동별 `strength_exercises.insert(...).select('id').single()`
	- 세트 배열을 만들어 `strength_sets.insert`
	- 저장 후 근력 폼 초기화

### 수정 UX/수정 로직 구조

- 수정 화면: `src/pages/History.tsx`
- 상세 모달에서 각 기록 유형별 수정 버튼 제공
- 다중 선택 모드에서는 1개 선택 시 수정 가능
- 체성분 수정:
	- `startEditBody`가 선택 기록을 `editBodyForm`에 복사
	- `doSaveEditBody`가 `body_records.update(...).eq('id', editingBodyId)` 실행
	- 현재 수정 로직은 `project_id`를 본문 수정 저장에는 포함하지 않고, 상세 화면의 프로젝트 드롭다운에서 `updateBodyProject`로 별도 변경
- 러닝 수정:
	- `startEditRun`이 DB 값을 문자열 폼으로 변환
	- `duration_sec`는 `M:SS` 또는 `H:MM:SS` 표시 문자열로 변환
	- `pace_sec_per_km`는 페이스 문자열로 변환
	- `doSaveEditRun`이 다시 초 단위와 숫자 값으로 환산해 `running_logs.update`
- 근력 수정:
	- `startEditStr`이 세션/운동/세트 구조를 `editStrForm`에 복사
	- `doSaveEditStr`은 `strength_logs` 본체를 업데이트한 뒤 기존 `strength_exercises`, `strength_sets`를 삭제하고 현재 폼 구조를 재삽입
	- 근력 수정은 부분 업데이트보다 재구성 방식이 단순하고 현재 코드도 이 구조를 채택
- 삭제:
	- 체성분/러닝은 해당 테이블에서 `id in (...)` 삭제
	- 근력은 `strength_sets` → `strength_exercises` → `strength_logs` 순으로 캐스케이드 수동 삭제

### 기록탭 데이터 흐름

- 라우팅:
	- `src/App.tsx`에서 `/history`가 `History`로 연결
	- `src/components/Layout.tsx`의 하단/사이드 내비게이션에서 기록 탭은 `/history`
	- `/log?tab=...`는 기록 입력 화면
- `History.tsx` 데이터 로드:
	- `body_records` 최근 365개
	- `running_logs` 최근 365개
	- `strength_logs` 최근 120개
	- `strength_exercises`, `strength_sets`는 세션별로 추가 조회해 중첩 구조 구성
	- `shoe_configs`, `exercise_configs`, `run_type_configs`, `app_settings` 메타도 함께 로드
- 표시:
	- 데스크톱은 왼쪽 `SummaryBrief`, 오른쪽 기록 목록
	- 모바일은 요약을 먼저 보여주고, 기록 목록은 전체 화면 오버레이로 열림
	- 탭: 체성분 / 러닝 / 근력
	- 보기 방식: 캘린더 / 디테일 / 행
	- 검색, 기간 필터, 정렬, 상세 모달, 수정/삭제 제공

## 2. OCR 기능 목표

- OCR 자동기록은 자동 저장 기능이 아니라 입력 보조 기능이다.
- 사용자가 삼성헬스, Garmin Connect, Apple Health/Fitness, Strava, Nike Run Club, 기타 러닝/운동/체성분 앱의 스크린샷을 올리면 화면 텍스트를 추출한다.
- 추출된 텍스트를 달록의 공통 기록 필드 후보로 정규화한다.
- OCR 결과는 신뢰도가 높아도 바로 저장하지 않는다.
- 반드시 사용자 보정 UI를 거쳐 최종값을 확정한 뒤 기존 달록 저장 흐름으로 저장한다.
- H-1/H-2의 핵심은 앱별 완벽 파서가 아니라 `범용 OCR → 공통 후보 추출 → 사용자 보정 → 기존 저장 함수 연결`이다.

## 3. v0.9.1 최소 MVP 범위

v0.9.1은 앱 자동 완벽 인식을 목표로 하지 않는다. 최소 사이클은 다음으로 제한한다.

1. 스크린샷 업로드
2. OCR/텍스트 추출
3. OCR 원문 표시
4. 공통 필드 후보 표시
5. 사용자가 기록 유형 선택: 체성분 / 러닝 / 근력
6. 선택한 기록 유형에 맞는 보정 폼 표시
7. 사용자가 값 수정
8. 기존 저장 확인 모달을 거쳐 최종 저장

MVP에서 지원할 후보 필드는 다음과 같다.

- 날짜
- 운동 종류
- 거리
- 시간
- 페이스
- 평균 심박수
- 최대 심박수
- 칼로리
- 메모
- 체중
- 체지방률
- 골격근량
- 기초대사량
- 루틴명
- 프로젝트명

MVP에서 근력 세트 상세 추출은 제한적으로 본다. 스크린샷에서 루틴명이나 운동 종류 후보는 추출하되, 종목/세트/반복/중량을 완전 자동 구성하는 것은 보류하는 편이 안전하다.

## 4. 추천 UX 흐름

### OCR 버튼 배치

가장 자연스러운 진입점은 기록탭 상단의 `기록하기` 버튼 근처와 기록 입력 화면 상단이다.

- `History.tsx` 상단:
	- 기존 사용자는 기록 탭에서 최근 기록을 보다가 새 기록을 추가한다.
	- 이 위치에 `OCR로 기록` 또는 아이콘 버튼을 두면 수동 입력과 같은 계층의 입력 방식으로 이해된다.
- `LogEntry.tsx` 상단:
	- 이미 `/log?tab=...`에 들어온 사용자는 특정 기록 유형을 생각하고 있다.
	- 현재 탭을 기본 기록 유형으로 잡아 OCR 보정 폼에 연결하기 쉽다.

v0.9.1에서는 `LogEntry.tsx` 상단에 우선 배치하는 방식을 추천한다. 이유는 기존 입력 탭, 저장 확인 모달, 데모 저장 차단, 저장 후 이동 흐름을 가장 짧게 재사용할 수 있기 때문이다. 이후 안정화되면 `History.tsx` 상단에도 동일 OCR 모달을 열 수 있는 보조 진입점을 추가한다.

### 기록탭, 기록 모달, 별도 OCR 모달

- 기록탭 안에 직접 OCR UI를 펼치는 방식:
	- 장점: 진입이 빠름
	- 단점: `History.tsx`가 이미 조회/검색/수정/삭제/요약을 많이 담당해 더 비대해짐
- 기록 입력 화면 안에 배치:
	- 장점: 기존 저장 폼과 연결이 단순함
	- 단점: OCR 업로드부터 보정까지 한 화면에 넣으면 입력 폼이 복잡해질 수 있음
- 별도 OCR 모달:
	- 장점: 업로드, 원문 확인, 후보 확인, 기록 유형 선택을 격리할 수 있음
	- 단점: 모달과 기존 폼 상태 동기화 설계가 필요함

추천안은 `LogEntry.tsx`에 `OCR로 불러오기` 버튼을 두고, 클릭 시 별도 `OcrImportModal`을 여는 구조다. 모달에서 추출과 후보 검토를 끝낸 뒤 `적용`을 누르면 현재 `bodyForm`, `runForm`, `strForm` 중 선택한 유형의 폼 상태에 값을 주입한다. 저장 버튼과 저장 확인은 기존 화면에서 그대로 처리한다.

### 앱명 선택 vs 자동 감지

v0.9.1에서는 앱 선택을 필수로 만들지 않는 편이 더 안전하다.

- `앱 선택 + OCR + 수동 보정`
	- 장점: 향후 앱별 힌트나 문구 사전을 적용하기 좋음
	- 단점: 사용자가 앱을 모르거나 기타 앱일 때 불필요한 마찰이 생김
	- 앱별 파서처럼 보이면 H-1/H-2의 범용 설계 목적과 어긋날 수 있음
- `앱 미선택 + OCR + 수동 보정`
	- 장점: 모든 스크린샷을 동일한 흐름으로 처리
	- 단점: 앱별 레이아웃 힌트를 활용하지 못함

추천안은 앱 선택을 선택사항으로 둔다.

- 기본값: `앱 선택 없음`
- 옵션: 삼성헬스, Garmin Connect, Apple Health/Fitness, Strava, Nike Run Club, 기타
- v0.9.1에서는 앱 선택값을 파싱 결과를 크게 바꾸는 조건으로 쓰지 않고, `sourceAppHint` 정도의 보조 메타로만 사용한다.
- 자동 감지는 시도하더라도 저장 판단에 쓰지 않는다. OCR 원문에 앱명이 보이면 `감지 후보`로 표시할 수 있지만 사용자가 수정 가능해야 한다.

### OCR 결과 신뢰도가 낮을 때

- 필드별로 `확인 필요` 상태를 표시한다.
- 값이 비어 있거나, 단위가 불명확하거나, 여러 후보가 충돌하면 해당 필드를 강조한다.
- 낮은 신뢰도여도 보정 UI로 넘긴다. 단, 저장 버튼은 기존 필수값 검증과 사용자 확인을 통과해야 한다.
- OCR 원문은 접을 수 있는 영역으로 남겨 사용자가 원본 텍스트와 후보 필드를 대조할 수 있게 한다.

### 기록 유형 최종 선택

OCR 모달에서 최종 기록 유형을 사용자가 선택한다.

1. 업로드 후 OCR 원문과 전체 후보 표시
2. 사용자가 체성분 / 러닝 / 근력 중 하나 선택
3. 선택 유형에 맞는 필드만 보정 UI에 우선 노출
4. `적용`을 누르면 해당 탭으로 이동하고 기존 입력 폼에 값 주입
5. 기존 저장 버튼을 눌러 저장 확인 후 저장

자동 유형 추론은 보조만 한다. 예를 들어 `거리`, `페이스`, `심박`이 있으면 러닝을 추천하고, `체중`, `체지방률`, `골격근량`이 있으면 체성분을 추천한다. 추천된 유형도 사용자가 바꿀 수 있어야 한다.

## 5. 추천 기술 방식

### 브라우저 내 OCR 라이브러리 방식

- 예: `tesseract.js`
- 장점:
	- 이미지 원본을 서버로 보내지 않아 개인정보 부담이 낮음
	- Supabase 외 별도 서버가 없어도 작동
	- v0.9.1에서 로컬 처리 MVP로 설명하기 쉬움
- 단점:
	- 번들 크기와 초기 로딩 비용 증가
	- 모바일 브라우저에서 느릴 수 있음
	- 한글/영문/숫자 혼합 화면 인식 품질 편차
- package 변경 가능성:
	- 본작업 시 `package.json`에 `tesseract.js` 추가 가능성이 있음

### 서버/API OCR 방식

- 예: Google Cloud Vision, Azure AI Vision, AWS Textract
- 장점:
	- OCR 품질과 속도가 상대적으로 안정적
	- 모바일 기기 성능 영향이 적음
- 단점:
	- 이미지가 외부 서버/API로 전송됨
	- 비용, 키 관리, 서버/프록시 구현 필요
	- 현재 구조는 프론트엔드 + Supabase 중심이라 MVP로는 무거움

### LLM Vision/API 방식

- 예: 이미지 입력 가능한 LLM API
- 장점:
	- 단순 OCR을 넘어 필드 의미 추론에 강함
	- 다양한 앱 레이아웃에서 공통 필드 정규화까지 한 번에 처리 가능
- 단점:
	- 이미지 전송에 대한 개인정보 고지와 동의가 필요
	- 비용과 응답 지연이 있음
	- 숫자 정확도 검증이 반드시 필요
	- 현재 달록에는 Anthropic 프록시성 API 유틸은 있으나 이미지 OCR 전용 구조는 없음

### 수동 파싱 보조 방식

- OCR 결과 텍스트를 줄 단위로 보여주고 사용자가 필드에 드래그/클릭으로 배정하는 방식
- 장점:
	- 파서 정확도가 낮아도 사용자가 빠르게 보정 가능
	- 특정 앱에 종속되지 않음
- 단점:
	- 자동화 체감이 약할 수 있음
	- 초기 UX 설계가 중요함

### 현재 달록 구조에서 가장 가벼운 MVP 추천

추천은 `브라우저 내 OCR + 규칙 기반 공통 후보 추출 + 사용자 보정`이다.

- 서버 저장 전 이미지 원본을 외부로 보내지 않는다.
- OCR 원문은 상태로만 보관하고 DB에 저장하지 않는다.
- 파싱은 날짜, 거리, 시간, 페이스, bpm, kcal, kg, %, BMR 등 단위 기반 정규식으로 시작한다.
- 앱 선택은 선택사항으로 두고, 파싱의 핵심 분기에는 쓰지 않는다.
- 본작업에서는 라이브러리 설치가 필요할 수 있으나 이번 설계 작업에서는 설치하지 않는다.

## 6. 공통 필드 정규화 구조

### 핵심 원칙

- 앱별 화면 구조가 달라도 최종 목표는 달록 공통 필드다.
- OCR 원문과 추출 후보 필드는 분리한다.
- 추출 후보는 저장값이 아니라 보정 전 임시값이다.
- 사용자가 보정한 최종값만 기존 저장 함수로 전달한다.
- OCR 원문, 이미지 원본, 후보 confidence는 v0.9.1에서 DB에 저장하지 않는다.

### 권장 데이터 모델

```typescript
type OcrSourceApp =
  | 'none'
  | 'samsung_health'
  | 'garmin_connect'
  | 'apple_health'
  | 'apple_fitness'
  | 'strava'
  | 'nike_run_club'
  | 'other'

type OcrRecordType = 'body' | 'running' | 'strength' | 'unknown'

type OcrCandidate<T = string> = {
  value: T | null
  rawText?: string
  confidence: 'high' | 'medium' | 'low'
  needsReview: boolean
  reason?: string
}

type OcrNormalizedCandidates = {
  sourceAppHint: OcrSourceApp
  detectedType: OcrCandidate<OcrRecordType>
  recorded_at: OcrCandidate<string>
  workout_type: OcrCandidate<string>
  distance_km: OcrCandidate<number>
  duration_sec: OcrCandidate<number>
  pace_sec_per_km: OcrCandidate<number>
  avg_bpm: OcrCandidate<number>
  max_bpm: OcrCandidate<number>
  calories: OcrCandidate<number>
  note: OcrCandidate<string>
  weight_kg: OcrCandidate<number>
  fat_pct: OcrCandidate<number>
  muscle_kg: OcrCandidate<number>
  bmr: OcrCandidate<number>
  routine_name: OcrCandidate<string>
  project_name: OcrCandidate<string>
}

type OcrParseResult = {
  rawText: string
  lines: string[]
  candidates: OcrNormalizedCandidates
}
```

### 단위와 값 정규화

- 날짜:
	- `YYYY-MM-DD`로 정규화
	- `2026.05.23`, `2026/05/23`, `5월 23일`, `May 23` 등은 후보만 제공
	- 연도가 없으면 현재 연도를 임시 적용하되 `needsReview: true`
- 거리:
	- km 기준 `distance_km`
	- `5.2 km`, `5.20km`, `10K`, `10 km` 등 처리
	- mile 단위가 보이면 km 환산 가능하지만 `needsReview: true`
- 시간:
	- `duration_sec`로 정규화
	- `1:02:30`, `42:18`, `42분 18초`, `1시간 2분` 처리
- 페이스:
	- `pace_sec_per_km`로 정규화
	- `5'30"/km`, `5:30 /km`, `5분 30초/km` 처리
	- 거리와 시간이 모두 있으면 페이스 재계산 후보도 만들고 OCR 페이스와 차이가 크면 확인 필요
- 심박:
	- `avg_bpm`, `max_bpm`
	- `평균 심박`, `Avg HR`, `Average Heart Rate`, `최대 심박`, `Max HR` 주변 숫자 우선
- 칼로리:
	- `calories`
	- `kcal`, `Cal`, `Calories` 주변 숫자
- 체성분:
	- `weight_kg`, `fat_pct`, `muscle_kg`, `bmr`
	- `체중`, `Weight`, `골격근량`, `Skeletal Muscle`, `체지방률`, `Body Fat`, `BMR`, `기초대사량` 주변 숫자
- 루틴명/프로젝트명:
	- OCR만으로 확정하지 않는다.
	- `routine_name`, `project_name` 후보로 표시하고 사용자가 현재 달록의 루틴/프로젝트 선택지에 매칭한다.

### 사용자 보정 후 저장 매핑

- 체성분 저장 매핑:
	- `recorded_at` → `bodyForm.recorded_at`
	- `weight_kg` → `bodyForm.weight_kg`
	- `muscle_kg` → `bodyForm.muscle_kg`
	- `fat_pct`는 직접 저장보다 현재 구조에 맞춰 참고 후보로 표시
	- 현재 신규 입력은 `fat_kg` 기반으로 `fat_pct`와 `bmr`를 자동 계산하므로, OCR이 `fat_pct`만 제공할 때는 `fat_kg = weight_kg * fat_pct / 100` 역산 후보를 제공할지 별도 검토 필요
	- `bmr`는 현재 신규 입력에서 자동 계산값을 저장하므로, OCR `bmr`와 계산값이 다르면 확인 필요 표시
	- `project_name` → 기존 프로젝트 목록에서 사용자 선택 후 `project_id`
	- `note` → `bodyForm.note`
- 러닝 저장 매핑:
	- `recorded_at` → `runForm.recorded_at`
	- `distance_km` → `runForm.distance_km`
	- `duration_sec` → `runForm.duration_min`, `runForm.duration_sec`
	- `pace_sec_per_km`은 현재 신규 저장에서 자동 계산하므로 참고 후보로 표시
	- OCR 페이스와 거리/시간 재계산 페이스가 다르면 확인 필요
	- `avg_bpm`, `max_bpm`, `calories` → 해당 필드
	- `workout_type` → `run_type` 후보. 기존 `run_type_configs` 목록과 직접 일치하지 않으면 수동 선택
	- `note` → `runForm.note`
- 근력 저장 매핑:
	- `recorded_at` → `strForm.recorded_at`
	- `routine_name` → `strForm.label`
	- `workout_type` → `strForm.label` 후보 또는 메모 후보
	- v0.9.1에서는 종목/세트 자동 구성은 보류하고, 루틴명과 날짜 중심으로 보정 UI에 전달
	- 저장된 루틴과 이름이 일치하면 사용자가 `저장된 루틴 불러오기`로 적용하도록 유도

## 7. 예상 수정 파일

H-1/H-2 본작업 시 수정 가능성이 높은 파일은 다음과 같다.

### 기록 입력 관련

- `src/pages/LogEntry.tsx`
	- `OCR로 불러오기` 버튼 추가
	- `OcrImportModal` 열기/닫기 상태 추가
	- OCR 적용 결과를 `bodyForm`, `runForm`, `strForm`, `runTime`, `strTime`에 주입하는 핸들러 추가
	- 기존 저장 함수는 최대한 유지

### 기록 수정 관련

- `src/pages/History.tsx`
	- 기록탭 상단 보조 진입점 추가 가능
	- OCR에서 저장 후 기록 목록 갱신 흐름을 이 화면에서 열 경우 `fetchData` 연결 검토
	- 본 MVP에서는 수정 로직을 직접 건드리지 않는 편이 안전

### 저장 함수 관련

- `src/pages/LogEntry.tsx`
	- 저장 함수 자체보다, 저장 함수에 들어가기 전 폼 상태 주입 레이어를 추가하는 방식 추천
	- 중복 저장을 피하려면 체성분 `upsert(recorded_at)`와 러닝/근력 `insert` 차이를 사용자에게 명확히 보여야 함
- 장기적으로 분리 가능:
	- `src/lib/recordSave.ts`
	- 체성분/러닝/근력 저장 함수를 화면 밖으로 빼면 OCR, 수동 입력, 향후 가져오기 기능이 같은 저장 함수를 공유 가능
	- 단 v0.9.1에서는 대규모 리팩토링으로 번질 수 있어 보류 권장

### 새로 만들 가능성이 있는 OCR 관련 파일

- `src/components/ocr/OcrImportModal.tsx`
	- 이미지 업로드
	- 앱 선택 옵션
	- OCR 실행 상태
	- 원문 텍스트 표시
	- 기록 유형 선택
	- 후보 필드 보정 UI
	- 적용 버튼
- `src/lib/ocr/types.ts`
	- `OcrCandidate`, `OcrParseResult`, 공통 후보 타입
- `src/lib/ocr/normalize.ts`
	- 단위 변환, 날짜/시간/페이스 정규화
- `src/lib/ocr/parseCandidates.ts`
	- OCR 원문에서 공통 후보 추출
- `src/lib/ocr/extractText.ts`
	- 브라우저 OCR 라이브러리 또는 API 호출 래퍼
- `src/lib/ocr/appHints.ts`
	- 선택 앱명과 감지 후보를 보조 메타로 다루는 작은 유틸

### package.json 변경 가능성

이번 설계 작업에서는 `package.json`을 수정하지 않는다. 본작업에서 브라우저 내 OCR을 택하면 다음 라이브러리 추가 가능성이 있다.

- `tesseract.js`: 브라우저 내 OCR

서버/API OCR이나 LLM Vision 방식을 선택하면 프론트엔드 라이브러리보다 서버/프록시 코드와 환경변수 변경이 필요할 가능성이 높다.

## 8. 기록 UX 작업과 충돌 위험

### 충돌 가능성이 높은 파일

- `src/pages/LogEntry.tsx`
	- 현재 기록 입력 피로도 개선, 저장 후 액션, 폼 구조 개선 작업과 직접 충돌 가능성이 가장 높음
	- OCR 버튼, OCR 적용 핸들러, 폼 상태 주입이 모두 이 파일에 걸림
- `src/pages/History.tsx`
	- 기록탭 UX, 검색/필터/상세 모달/수정 UX 개선 작업과 충돌 가능
	- OCR 진입점을 여기에도 추가하면 충돌 범위가 커짐
- `src/components/Modal.tsx`
	- 공통 모달 UX 개선과 충돌 가능
	- OCR 전용 모달을 공통 모달에 합치려 하면 위험 증가
- `src/lib/supabase.ts`
	- 타입 확장 또는 저장 함수 분리 시 충돌 가능
- `package.json`
	- OCR 라이브러리 추가 시 충돌 가능

### 병렬 작업이 가능한 부분

- OCR 파싱 유틸 설계와 구현:
	- `src/lib/ocr/*` 신규 파일 중심이면 기존 기록 UX 작업과 병렬 가능
- OCR 후보 타입 정의:
	- 독립 타입 파일로 만들면 충돌 낮음
- OCR 원문 샘플 기반 파서 테스트:
	- 이미지 OCR 없이 텍스트 샘플을 대상으로 후보 추출 로직을 검증 가능
- UX 문구와 상태 설계:
	- 실제 `LogEntry.tsx` 연결 전까지 독립 설계 가능

### 병렬 작업을 피해야 하는 부분

- `LogEntry.tsx`의 폼 구조 변경과 OCR 적용 핸들러 추가를 동시에 진행하는 작업
- `History.tsx`의 기록탭 레이아웃 개편과 OCR 진입점 추가를 동시에 진행하는 작업
- 저장 함수를 화면 밖으로 분리하는 리팩토링과 OCR 본작업을 같은 차수에 묶는 작업
- `Modal.tsx` 공통 모달 변경과 OCR 모달 신규 도입을 동시에 진행하는 작업

### H-1/H-2 본작업 전 선행 조건

- 현재 진행 중인 기록 UX / 입력 피로도 개선 작업의 대상 파일 확정
- `LogEntry.tsx`를 본작업 중 수정해도 되는 시점 확인
- OCR MVP 기술 방식 결정:
	- 브라우저 내 OCR
	- 서버/API OCR
	- LLM Vision/API
- 이미지 원본 저장 여부 명확화:
	- v0.9.1 권장: 저장하지 않음
- 개인정보 고지 문구 필요 여부 확인:
	- 브라우저 내 OCR이면 간단 안내
	- 외부 API/LLM이면 명시적 고지와 동의 필요
- 체성분 `fat_pct`만 있을 때 `fat_kg` 역산을 허용할지 결정
- 근력 OCR 범위를 루틴명 수준으로 제한할지, 세트 상세까지 시도할지 결정

## 9. 본작업 투입용 권장 순서

### 1단계: UI 진입점

- `LogEntry.tsx` 상단에 `OCR로 불러오기` 버튼 추가
- 현재 탭을 OCR 모달의 기본 기록 유형으로 전달
- OCR 모달은 새 컴포넌트로 분리

### 2단계: OCR 원문 추출

- `extractTextFromImage(file)` 래퍼 작성
- 브라우저 OCR 라이브러리 또는 임시 수동 텍스트 입력 모드로 시작
- 로딩, 실패, 재시도 상태 제공
- 이미지 원본은 상태에서만 사용하고 저장하지 않음

### 3단계: 필드 후보 파싱

- `parseOcrCandidates(rawText)` 작성
- 줄 단위 원문과 정규화 후보를 분리
- 날짜, 거리, 시간, 페이스, 심박, 칼로리, 체성분 단위 중심으로 후보 추출
- 필드별 `confidence`, `needsReview`, `reason` 계산

### 4단계: 사용자 보정 UI

- 기록 유형 선택: 체성분 / 러닝 / 근력
- 선택 유형별 후보 필드만 우선 표시
- 낮은 신뢰도 필드에 `확인 필요` 표시
- OCR 원문 접기/펼치기 제공
- `적용` 시 기존 입력 폼에 값을 주입

### 5단계: 기존 저장 함수 연결

- OCR 모달에서 직접 저장하지 않음
- `적용` 후 사용자가 기존 저장 버튼을 누름
- `ConfirmDialog`, `useDemoBlock`, 저장 성공 알림, `/history?tab=...` 이동 흐름 재사용
- 체성분은 `upsert`, 러닝/근력은 `insert`라는 현재 차이를 유지

### 6단계: 브라우저 테스트

- 업로드 버튼 동작
- OCR 성공/실패/재시도
- 체성분 후보 적용
- 러닝 후보 적용
- 근력 루틴명 후보 적용
- 낮은 신뢰도 표시
- 기존 수동 입력 저장 회귀
- 모바일 폭에서 OCR 모달과 보정 폼 텍스트 넘침 확인

## 10. 보류할 기능

v0.9.1에서는 다음 기능을 보류한다.

- 앱별 완벽 자동 감지
- 앱별 전용 고급 파서
- 자동 저장
- OCR 결과 무검증 저장
- 대규모 리팩토링
- OCR 히스토리 저장
- 이미지 원본 서버 저장
- 앱별 우선순위 선정
- 근력 세트 상세 완전 자동 구성
- OCR 원문을 DB에 영구 저장
- OCR confidence만으로 저장 가능 여부 자동 판단

## 결론

H-1/H-2 본작업은 현재 구조에 붙일 수 있다. 다만 가장 안전한 방식은 저장 함수를 즉시 리팩토링하지 않고, OCR 결과를 기존 `LogEntry.tsx`의 폼 상태에 주입한 뒤 기존 저장 버튼과 확인 모달을 그대로 통과시키는 것이다.

권장 순서는 `UI 진입점 → OCR 원문 추출 → 공통 후보 파싱 → 사용자 보정 UI → 기존 저장 함수 연결 → 브라우저 테스트`다. 이 순서라면 특정 앱 전용 구현으로 흐르지 않고, 여러 트래킹 앱 스크린샷을 달록 공통 기록 필드로 정규화하는 H-1/H-2 목적에 맞게 진행할 수 있다.
