# 즐겨찾기(Favorites) 기능 구현

> 2026-02-11 | 상태: 완료 (localStorage 기반) | 추후: API 전환 예정

## 개요

사이드바 메뉴에 별표(즐겨찾기) 기능을 추가하여 사용자가 자주 쓰는 메뉴를 헤더에 동적으로 표시.
기존 하드코딩된 종합분석/품질인정심사 버튼을 제거하고 사용자 선택 기반으로 전환.

## 파일 구조

| 파일 | 작업 | 설명 |
|------|------|------|
| `src/stores/favoritesStore.ts` | NEW | Zustand persist 스토어 |
| `src/lib/utils/menuTransform.ts` | MODIFY | reverseIconMap, getIconName, DEFAULT_FAVORITES 추가 |
| `src/components/layout/HeaderFavoritesBar.tsx` | NEW | 헤더 즐겨찾기 바 (반응형) |
| `src/components/layout/Sidebar.tsx` | MODIFY | leaf 메뉴에 별표 토글 추가 |
| `src/layouts/AuthenticatedLayout.tsx` | MODIFY | 하드코딩 버튼 → HeaderFavoritesBar 교체 |

## 핵심 동작

### 즐겨찾기 등록/해제
- 사이드바 leaf 메뉴(children 없는 항목) hover 시 별표 아이콘 표시
- 이미 즐겨찾기인 항목은 항상 노란 별표 표시
- 별표 클릭으로 토글 (메뉴 클릭과 분리 - `e.stopPropagation()`)
- sidebar collapsed 상태에서는 별표 숨김

### 헤더 표시 (반응형)
| 조건 | 표시 방식 |
|------|----------|
| 데스크톱 (1024px+), 1~8개 | 아이콘 버튼 + Tooltip |
| 데스크톱 (1024px+), 9~10개 | ★ 드롭다운 |
| 태블릿 (768~1024px) | ★ 드롭다운 |
| 모바일 (<768px) | ★ 드롭다운 |

### 제한
- 최대 **10개**
- 초과 시 토스트: `즐겨찾기는 최대 10개까지 등록할 수 있습니다.`

### 저장
- **현재**: localStorage (`sam-favorites-{userId}` 키)
- Zustand persist 사용, 사용자별 분리 저장
- 기본값 없음 (사용자가 직접 추가한 것만 표시)

## 주요 구현 상세

### favoritesStore.ts
```typescript
interface FavoriteItem {
  id: string;       // 메뉴 id
  label: string;    // 메뉴 라벨
  iconName: string; // 아이콘 문자열 (iconMap 키)
  path: string;     // 라우트 경로
  addedAt: number;  // 추가 시각 (정렬용)
}

// toggleFavorite 반환값으로 토스트 제어
type ToggleResult = 'added' | 'removed' | 'max_reached';
```

### menuTransform.ts 확장
- `reverseIconMap`: iconMap을 뒤집어 `LucideIcon → string` 조회
- `getIconName(icon)`: 아이콘 컴포넌트 → 문자열 이름 변환
- `DEFAULT_FAVORITES`: 종합분석 + 품질인정심사 (현재 미사용, 참고용 보관)

### button 중첩 방지
별표 아이콘은 메뉴 `<button>` 바깥에 배치 (`<div className="group/row">` 래퍼).
HTML 규격상 `<button>` 안에 `<button>`은 불가 → hydration 에러 유발.

```
<div class="flex items-center group/row">
  <button>메뉴 항목</button>     ← 메뉴 클릭
  <button>★</button>             ← 별표 클릭 (형제 관계)
</div>
```

## API 전환 계획

현재 localStorage 기반이라 기기/브라우저별로 동기화되지 않음.
추후 API 준비되면 스토어 내부만 교체하면 됨.

### 변경 범위
- `favoritesStore.ts`만 수정 (컴포넌트 수정 불필요)

### 예상 API 엔드포인트
```
GET    /api/v1/favorites          → 목록 조회
POST   /api/v1/favorites          → 추가
DELETE /api/v1/favorites/{menuId} → 삭제
```

### 전환 방식
```typescript
// Before (localStorage)
toggleFavorite: (item) => {
  // Zustand set()으로 localStorage 저장
}

// After (API)
toggleFavorite: async (item) => {
  const exists = get().favorites.some(f => f.id === item.id);
  if (exists) {
    await fetch(`/api/v1/favorites/${item.id}`, { method: 'DELETE' });
  } else {
    await fetch('/api/v1/favorites', { method: 'POST', body: JSON.stringify(item) });
  }
  // 성공 후 로컬 상태 업데이트
}
```

### 초기 로딩
```typescript
// AuthenticatedLayout useEffect에서
const res = await fetch('/api/v1/favorites');
const data = await res.json();
useFavoritesStore.getState().setFavorites(data);
```

### 주의사항
- API 실패 시 localStorage fallback 고려
- 낙관적 업데이트(optimistic update)로 UX 저하 방지
- 서버 응답 전에 UI 먼저 반영 → 실패 시 롤백
