# Task: 프로젝트뷰 스크롤 위치 보존 버그 수정 (v2)

## 파일
`/home/jay/workspace/dashboard/index.html`

## 문제
task-593.1에서 `useLayoutEffect` + `savedScrollTop` 패턴으로 스크롤 보존을 구현했으나, SSE 갱신 시 여전히 스크롤이 초기화됨.

## 근본 원인
`React.memo`가 사실상 무효화되어 SSE 갱신마다 `ProjectCard`가 전체 리렌더링됨:

1. **`toggleIssue` (line 576)**: `useCallback` 미사용 → 매 렌더마다 새 함수 참조
2. **`tasksByProject`, `todoByProject` (line 824-825)**: 전체 dict를 prop으로 전달 → SSE 갱신마다 새 객체
3. **`PROJECT_ACCENT` (line 826)**: 컴포넌트 내부 정의 → 매 렌더마다 재생성
4. **`expandedIssues` (line 827)**: Set 객체 → shallow compare에서 항상 다름

결과: `React.memo`의 shallow compare가 항상 fail → 매 SSE 갱신마다 불필요한 리렌더링 → `useLayoutEffect`의 scrollTop 복원이 빠른 연속 리렌더에서 race condition 발생

## 수정 방법

### 수정 1: `toggleIssue`에 `useCallback` 적용 (line 576)
```jsx
// Before
const toggleIssue = (id) => setExpandedIssues(prev => { ... });

// After
const toggleIssue = React.useCallback((id) => setExpandedIssues(prev => {
    const next = new Set(prev);
    next.has(id) ? next.delete(id) : next.add(id);
    return next;
}), []);
```

### 수정 2: ProjectCard에 프로젝트별 데이터만 전달 (line 821-829)
```jsx
// Before
<ProjectCard
    key={proj}
    projName={proj}
    tasksByProject={tasksByProject}
    todoByProject={todoByProject}
    PROJECT_ACCENT={PROJECT_ACCENT}
    expandedIssues={expandedIssues}
    toggleIssue={toggleIssue}
/>

// After
<ProjectCard
    key={proj}
    projName={proj}
    projectData={tasksByProject[proj] || { tasks: [], running: 0, completed: 0 }}
    issues={todoByProject[proj] || []}
    accent={PROJECT_ACCENT[proj] || PROJECT_ACCENT['기타']}
    expandedIssues={expandedIssues}
    toggleIssue={toggleIssue}
/>
```

### 수정 3: ProjectCard의 props 시그니처 변경 + custom comparator (line 674)
```jsx
const ProjectCard = React.memo(({ projName, projectData, issues, accent, expandedIssues, toggleIssue }) => {
    const data = projectData;
    // todoByProject[projName] → issues로 직접 사용
    // accent → 직접 사용
    // ... 나머지 동일
}, (prevProps, nextProps) => {
    // custom comparator: 실제 데이터가 변경됐을 때만 리렌더
    return prevProps.projName === nextProps.projName &&
           prevProps.projectData === nextProps.projectData &&
           prevProps.issues === nextProps.issues &&
           prevProps.expandedIssues === nextProps.expandedIssues;
    // toggleIssue, accent는 비교 제외 (toggleIssue는 useCallback으로 안정화, accent는 projName과 1:1 매핑)
});
```

### 수정 4: PROJECT_ACCENT을 컴포넌트 외부 또는 useMemo로 이동
`PROJECT_ACCENT`은 상수이므로 `ProjectView` 바깥으로 이동하거나 `useMemo(()=>..., [])`로 감싸기.

### 수정 5: scrollTop 복원에 requestAnimationFrame 폴백 추가 (line 687-691)
연속 리렌더에서도 확실히 복원되도록:
```jsx
React.useLayoutEffect(() => {
    if (scrollRef.current && savedScrollTop.current > 0) {
        scrollRef.current.scrollTop = savedScrollTop.current;
        // 폴백: 다음 프레임에서도 복원 시도
        requestAnimationFrame(() => {
            if (scrollRef.current) {
                scrollRef.current.scrollTop = savedScrollTop.current;
            }
        });
    }
});
```

## 검증
- 대시보드 프로젝트 뷰에서 안건을 펼치고 스크롤 내린 후, SSE 갱신이 와도 스크롤 위치가 유지되는지 확인
- React DevTools Profiler로 SSE 갱신 시 ProjectCard의 불필요한 리렌더가 방지되는지 확인

## 테스트
프론트엔드 코드이므로 수동 테스트. 대시보드 접속: http://100.76.130.39:8000/dashboard/
