# task-595.1 완료 보고서

## SCQA

**S**: 대시보드 프로젝트뷰에서 SSE EventSource로 실시간 데이터 갱신 시 ProjectCard가 리렌더링되어 사용자의 스크롤 위치가 초기화되는 문제가 이전 2회 시도(task-593.1, task-594.1)에서 해결되지 않았다.

**C**: `tasksByProject`/`todoByProject`가 매 렌더마다 새 객체로 재계산되어 React.memo의 참조 비교가 항상 실패하고, `savedScrollTop` useRef도 빠른 연속 리렌더에서 타이밍 경합에 실패했다. 이전 시도의 `useLayoutEffect` + `useCallback` + `requestAnimationFrame` 조합은 근본 원인(참조 불안정성)을 해결하지 못했다.

**Q**: React 라이프사이클에 의존하지 않는 방식으로 스크롤 위치를 안정적으로 보존할 수 있는가?

**A**: 모듈 레벨 `_scrollPositions` Map 도입 + `useMemo`로 참조 안정성 확보의 2중 방어 전략을 적용했다. Map은 컴포넌트 생명주기와 무관하게 유지되어 React.memo가 실패해도 스크롤이 복원되고, `useMemo`는 SSE 갱신 시 불필요한 리렌더 자체를 차단한다.

## 변경 파일

- `/home/jay/workspace/dashboard/index.html` (수정)

## 변경 내역

1. **line 581-582**: `const _scrollPositions = new Map()` 모듈 레벨 선언 추가
2. **line 622-648**: `tasksByProject`(의존: `[tasksData]`), `todoByProject`(의존: `[todoData]`), `doneIssueCount`(의존: `[todoData]`)를 `React.useMemo`로 감싸 참조 안정성 확보
3. **line 693-706**: `savedScrollTop` useRef 제거, `useLayoutEffect`에서 `_scrollPositions.get(projName)` 사용으로 교체, `handleScroll`을 `React.useCallback`으로 선언하여 `_scrollPositions.set()` 호출
4. **line 744**: 인라인 `onScroll` 핸들러를 `handleScroll` 참조로 교체
5. `requestAnimationFrame` 폴백 제거 (모듈 레벨 Map으로 불필요)

## 발견 이슈 및 해결

### 자체 해결 (3건)
1. **useMemo 내 classifyByDescription 참조 안정성** — `classifyByDescription`/`classifyTodoProject` 함수가 ProjectView 내부에 정의되어 매 렌더마다 새 참조이나, `useMemo`의 의존성이 `[tasksData]`/`[todoData]`이므로 함수 결과는 결정적(deterministic). 현재 구조에서 문제없음.
   - 상세: `PROJECT_MAP`은 상수 객체로 매 렌더 동일한 값 생성. 함수 자체가 의존성 배열에 포함되지 않으므로 stale closure 이슈 없음.

2. **SSE가 매번 새 배열 참조를 제공하면 useMemo 무효화** — App 컴포넌트에서 SSE 데이터를 setState로 갱신 시 매번 새 배열 참조가 생성되어 useMemo가 재계산됨. 이것이 **모듈 레벨 Map이 핵심 보험인 이유**. useMemo가 React.memo 효과를 부여하지 못해도, `_scrollPositions` Map에서 스크롤 위치를 복원한다.
   - 상세: 2중 방어 구조 — (1) useMemo로 참조 안정성 시도, (2) 실패 시 모듈 레벨 Map이 useLayoutEffect에서 복원

3. **useLayoutEffect 의존성 배열 미지정** — `useLayoutEffect(() => {...})` 형태로 의존성 배열 없이 사용. 이는 의도적 설계 — 모든 렌더 후 스크롤 복원을 시도하여 어떤 리렌더에서도 위치가 유지됨. 성능 영향은 Map.get() 1회 + DOM scrollTop 설정 1회로 미미함.

## 검증 방법
- 프론트엔드 코드이므로 수동 테스트 대상
- 대시보드(http://100.76.130.39:8000/dashboard/) 프로젝트뷰에서:
  1. ThreadAuto 카드의 안건을 펼치고 스크롤 내림
  2. 5~10초 대기 (SSE 갱신 발생)
  3. 스크롤 위치가 유지되는지 확인

## QC 자동 검증

- **overall**: PASS (3 PASS, 7 SKIP)
- **file_check**: PASS — 파일 존재, 보고서 3642 bytes
- **data_integrity**: PASS — task-timers.json 상태 일치
- **critical_gap**: PASS — CRITICAL 이슈 없음
- **tdd_check**: SKIP — HTML 내 JSX 코드로 자동 테스트 프레임워크 해당 없음 (수동 테스트 대상)
- **pyright/style/schema**: SKIP — Python 파일 아님
- **.done 생성**: /home/jay/workspace/memory/events/task-595.1.done
