# 동적 메뉴 갱신 시스템

## 개요

관리자가 게시판/메뉴를 추가하면 사용자가 **재로그인 없이** 즉시 메뉴를 갱신받을 수 있는 시스템 구현.

## 현재 문제점

```
현재 흐름:
  로그인 → API 응답에서 메뉴 수신 → localStorage.user.menu 저장 → 세션 종료까지 고정

문제:
  - 관리자가 게시판 추가해도 사용자는 재로그인 전까지 새 메뉴 안 보임
  - 메뉴 전용 갱신 API 없음
  - 실시간 알림 메커니즘 없음
```

## 데이터 흐름 (현재)

```
┌─────────────────────────────────────────────────────────────┐
│                       로그인 시                              │
├─────────────────────────────────────────────────────────────┤
│  POST /api/v1/login                                         │
│       ↓                                                     │
│  응답: { user, tenant, roles, menus }                       │
│       ↓                                                     │
│  transformApiMenusToMenuItems(menus)                        │
│       ↓                                                     │
│  localStorage.setItem('user', { ...userData, menu })        │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│                   페이지 로드 시                              │
├─────────────────────────────────────────────────────────────┤
│  AuthenticatedLayout.tsx                                    │
│       ↓                                                     │
│  localStorage.getItem('user') → userData.menu               │
│       ↓                                                     │
│  deserializeMenuItems(userData.menu)                        │
│       ↓                                                     │
│  menuStore.setMenuItems(deserializedMenus)                  │
│       ↓                                                     │
│  Sidebar 컴포넌트 렌더링                                     │
└─────────────────────────────────────────────────────────────┘
```

## 관련 파일

| 파일 | 역할 |
|------|------|
| `src/store/menuStore.ts` | Zustand 메뉴 상태 관리 |
| `src/lib/utils/menuTransform.ts` | API 메뉴 → UI 메뉴 변환 |
| `src/layouts/AuthenticatedLayout.tsx` | 메뉴 로드 및 스토어 설정 |
| `src/components/layout/Sidebar.tsx` | 메뉴 렌더링 |
| `src/contexts/AuthContext.tsx` | 사용자 인증 컨텍스트 |

---

## 구현 계획

### 1단계: 폴링 방식 (현재 구현 목표)

**방식**: 30초마다 메뉴 API 호출하여 변경사항 확인

```
┌─────────────────────────────────────────────────────────────┐
│                    폴링 방식 흐름                            │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  [30초마다]                                                  │
│       ↓                                                     │
│  GET /api/menus (메뉴 전용 API 필요)                         │
│       ↓                                                     │
│  현재 메뉴와 비교 (해시 또는 버전 비교)                        │
│       ↓                                                     │
│  변경 있으면 → refreshMenus() 호출                           │
│       ↓                                                     │
│  localStorage.user.menu 업데이트                             │
│  menuStore.setMenuItems() 호출                               │
│       ↓                                                     │
│  UI 즉시 반영                                                │
│                                                              │
└─────────────────────────────────────────────────────────────┘
```

**장점**:
- 구현 단순
- 백엔드 수정 최소화 (메뉴 조회 API만 추가)
- 기존 인프라 그대로 사용

**단점**:
- 최대 30초 지연
- 불필요한 API 호출 발생

#### 프론트엔드 구현 사항

1. **메뉴 갱신 유틸리티 함수** (`src/lib/utils/menuRefresh.ts`)
2. **폴링 훅** (`src/hooks/useMenuPolling.ts`)
3. **AuthenticatedLayout에 훅 적용**

#### 백엔드 요청 사항

| 항목 | 설명 |
|------|------|
| **엔드포인트** | `GET /api/v1/menus` |
| **인증** | Bearer 토큰 필요 |
| **응답** | 현재 사용자의 메뉴 목록 (로그인 응답의 menus와 동일 구조) |
| **선택사항** | `menu_version` 또는 `menu_hash` 필드 추가 (변경 감지 최적화용) |

---

### 2단계: SSE 고도화 (향후 계획)

**방식**: 서버에서 메뉴 변경 시 SSE로 클라이언트에 푸시

```
┌─────────────────────────────────────────────────────────────┐
│                      백엔드 (Laravel)                        │
├─────────────────────────────────────────────────────────────┤
│  1. 관리자가 메뉴 추가 → DB 저장                              │
│  2. MenuUpdatedEvent 발생                                    │
│  3. 해당 테넌트의 SSE 채널로 푸시                             │
└─────────────────────────────────────────────────────────────┘
                            ↓ SSE
┌─────────────────────────────────────────────────────────────┐
│                    프론트엔드 (Next.js)                       │
├─────────────────────────────────────────────────────────────┤
│  1. EventSource로 SSE 연결 유지                              │
│  2. 'menu-updated' 이벤트 수신                               │
│  3. refreshMenus() 호출 → UI 즉시 갱신                       │
└─────────────────────────────────────────────────────────────┘
```

**장점**:
- 실시간 갱신 (지연 없음)
- 효율적 (변경 시에만 통신)

**단점**:
- 백엔드 SSE 인프라 구축 필요
- 동시 접속자 관리 필요
- 멀티테넌트 채널 분리 필요

#### 백엔드 요구사항 (SSE)

| 항목 | 설명 |
|------|------|
| **SSE 엔드포인트** | `GET /api/v1/sse/menu-updates` |
| **인증** | Bearer 토큰 또는 쿼리 파라미터 |
| **이벤트 타입** | `menu-updated` |
| **채널 분리** | 테넌트별로 분리 필요 |
| **구현 옵션** | Laravel Broadcasting + Redis, 직접 구현 등 |

---

## 구현 체크리스트

### 1단계: 폴링 방식

#### 프론트엔드 ✅ 구현 완료 (2025-12-29)
- [x] `src/lib/utils/menuRefresh.ts` 생성
  - [x] `refreshMenus()` 함수 구현
  - [x] `forceRefreshMenus()` 강제 갱신 함수
  - [x] localStorage + Zustand 동시 업데이트
  - [x] 해시 기반 변경 감지
- [x] `src/hooks/useMenuPolling.ts` 생성
  - [x] 30초 간격 폴링 로직
  - [x] 탭 가시성 변경 시 자동 중지/재개
  - [x] pause/resume 기능
  - [x] 컴포넌트 언마운트 시 정리
- [x] `src/app/api/menus/route.ts` 생성 (Next.js 프록시)
  - [x] 백엔드 메뉴 API 프록시
  - [x] HttpOnly 쿠키 토큰 처리
  - [x] `{ data: [...] }` 응답 구조 처리
- [x] `AuthenticatedLayout.tsx`에 훅 적용
- [ ] 테스트: 관리자 메뉴 추가 → 30초 내 사용자 메뉴 갱신 확인

#### 백엔드 (이미 존재!)
- [x] `GET /api/v1/menus` API 존재 확인 ✅
- [x] `MenuController::index` → `MenuService::index` (사용자 권한 기반 필터링)
- [x] 응답 구조: `{ data: [...] }` (ApiResponse::handle 표준)

### 2단계: SSE 고도화 (향후)

- [ ] 백엔드 SSE 인프라 구축
- [ ] 프론트엔드 EventSource 훅 구현
- [ ] 폴링 → SSE 전환
- [ ] 폴백: SSE 연결 실패 시 폴링으로 대체

---

## 코드 스니펫

### refreshMenus 함수

```typescript
// src/lib/utils/menuRefresh.ts
import { transformApiMenusToMenuItems, deserializeMenuItems } from './menuTransform';
import { useMenuStore } from '@/store/menuStore';

export async function refreshMenus(): Promise<boolean> {
  try {
    const response = await fetch('/api/menus');
    if (!response.ok) return false;

    const { menus } = await response.json();
    const transformedMenus = transformApiMenusToMenuItems(menus);

    // 1. localStorage 업데이트 (새로고침 대응)
    const userData = JSON.parse(localStorage.getItem('user') || '{}');
    userData.menu = transformedMenus;
    localStorage.setItem('user', JSON.stringify(userData));

    // 2. Zustand 스토어 업데이트 (UI 즉시 반영)
    const { setMenuItems } = useMenuStore.getState();
    setMenuItems(deserializeMenuItems(transformedMenus));

    console.log('[Menu] 메뉴 갱신 완료');
    return true;
  } catch (error) {
    console.error('[Menu] 메뉴 갱신 실패:', error);
    return false;
  }
}
```

### useMenuPolling 훅

```typescript
// src/hooks/useMenuPolling.ts
import { useEffect, useRef } from 'react';
import { refreshMenus } from '@/lib/utils/menuRefresh';

const POLLING_INTERVAL = 30000; // 30초

export function useMenuPolling(enabled: boolean = true) {
  const intervalRef = useRef<NodeJS.Timeout | null>(null);

  useEffect(() => {
    if (!enabled) return;

    // 초기 실행은 하지 않음 (로그인 시 이미 받아옴)
    intervalRef.current = setInterval(() => {
      refreshMenus();
    }, POLLING_INTERVAL);

    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, [enabled]);
}
```

### Next.js API 프록시

```typescript
// src/app/api/menus/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const token = request.cookies.get('access_token')?.value;

  if (!token) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/menus`, {
    headers: {
      'Authorization': `Bearer ${token}`,
      'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
    },
  });

  const data = await response.json();
  return NextResponse.json(data);
}
```

---

## 참고 사항

### 메뉴 데이터 저장 위치

| 저장소 | 키 | 용도 |
|--------|-----|------|
| localStorage | `user.menu` | 새로고침 시 복구용 |
| Zustand | `menuStore.menuItems` | UI 렌더링용 |

### 갱신 시 동기화 필수

```typescript
// 반드시 둘 다 업데이트!
localStorage.user.menu = newMenus;      // 새로고침 대응
menuStore.setMenuItems(newMenus);       // UI 즉시 반영
```

---

## 작성 정보

- **작성일**: 2025-12-29
- **상태**: ✅ 1단계 구현 완료 (테스트 대기)
- **담당**: 프론트엔드 팀
- **백엔드**: `GET /api/v1/menus` API 이미 존재 ✅