# Phase 1-5: Zustand 셀렉터 훅 추가 (3개 스토어)

> 난이도: 저 | 영향도: 🟡 리렌더 최적화 | 예상 변경: 3 스토어 + 4 컨슈머

---

## 현황 요약

셀렉터 없이 전체 스토어를 구독하면, 무관한 상태 변경에도 컴포넌트가 리렌더됩니다.

| 스토어 | 셀렉터 훅 | 사용처 | 문제 |
|--------|----------|--------|------|
| ✅ `masterDataStore` | `usePageConfig()` 등 | 다수 | 양호 |
| ✅ `authStore` | `useCurrentUser()` 등 | 4곳 | 양호 (방금 추가) |
| ❌ `useTableColumnStore` | 없음 | 1곳 | 전체 스토어 구독 |
| ❌ `useMenuStore` | 없음 | 15곳 | 일부 전체 구독 |
| ❌ `useThemeStore` | 없음 | 2곳 | 전체 구독 |

---

## 작업 내역

### Step 1: `src/stores/useTableColumnStore.ts` — 셀렉터 훅 추가

파일 끝에 추가:

```typescript
// ===== 셀렉터 훅 =====

/** 특정 페이지의 컬럼 설정만 구독 */
export const usePageColumnSettings = (pageId: string) =>
  useTableColumnStore((state) => state.pageSettings[pageId] ?? DEFAULT_PAGE_SETTINGS);

/** 특정 페이지의 숨김 컬럼만 구독 */
export const useHiddenColumns = (pageId: string) =>
  useTableColumnStore((state) => state.pageSettings[pageId]?.hiddenColumns ?? []);

/** 특정 페이지의 컬럼 너비만 구독 */
export const useColumnWidths = (pageId: string) =>
  useTableColumnStore((state) => state.pageSettings[pageId]?.columnWidths ?? {});
```

**주의**: `DEFAULT_PAGE_SETTINGS` 객체는 파일 내에 이미 정의되어 있음 (line 30-33).

**컨슈머 변경** — `src/hooks/useColumnSettings.ts`:

```typescript
// Before (line 17)
const store = useTableColumnStore();        // 전체 스토어 구독
const settings = store.getPageSettings(pageId);

// After
const settings = usePageColumnSettings(pageId);  // 해당 페이지 설정만 구독
const { setColumnWidth: storeSetWidth, toggleColumnVisibility: storeToggle, resetPageSettings } = useTableColumnStore.getState();
// 또는 액션만 별도 구독 (액션은 참조 안정적이라 리렌더 유발 안 함):
const setColumnWidth = useTableColumnStore((s) => s.setColumnWidth);
const toggleColumnVisibility = useTableColumnStore((s) => s.toggleColumnVisibility);
const resetPageSettings = useTableColumnStore((s) => s.resetPageSettings);
```

---

### Step 2: `src/stores/menuStore.ts` — 셀렉터 훅 추가

파일 끝에 추가:

```typescript
// ===== 셀렉터 훅 =====

/** 사이드바 접힘 상태만 구독 */
export const useSidebarCollapsed = () =>
  useMenuStore((state) => state.sidebarCollapsed);

/** 활성 메뉴 ID만 구독 */
export const useActiveMenu = () =>
  useMenuStore((state) => state.activeMenu);

/** 메뉴 아이템 목록만 구독 */
export const useMenuItems = () =>
  useMenuStore((state) => state.menuItems);

/** 하이드레이션 완료 여부만 구독 */
export const useMenuHydrated = () =>
  useMenuStore((state) => state._hasHydrated);
```

**컨슈머 변경 대상**:

#### 2-A. `src/layouts/AuthenticatedLayout.tsx` (line 99) — 🔴 핵심

현재: 전체 스토어 디스트럭처링
```typescript
const { menuItems, activeMenu, setActiveMenu, setMenuItems, sidebarCollapsed, toggleSidebar, _hasHydrated } = useMenuStore();
```

변경:
```typescript
const menuItems = useMenuItems();
const activeMenu = useActiveMenu();
const sidebarCollapsed = useSidebarCollapsed();
const _hasHydrated = useMenuHydrated();
// 액션은 참조 안정적이므로 별도 셀렉터:
const setActiveMenu = useMenuStore((s) => s.setActiveMenu);
const setMenuItems = useMenuStore((s) => s.setMenuItems);
const toggleSidebar = useMenuStore((s) => s.toggleSidebar);
```

#### 2-B. `src/components/production/WorkerScreen/index.tsx` (line 327)

현재:
```typescript
const { sidebarCollapsed } = useMenuStore();  // 전체 구독
```

변경:
```typescript
const sidebarCollapsed = useSidebarCollapsed();
```

#### 2-C. `src/components/layout/CommandMenuSearch.tsx` (line 68)

현재:
```typescript
const { menuItems } = useMenuStore();  // 전체 구독
```

변경:
```typescript
const menuItems = useMenuItems();
```

#### 2-D. 나머지 sidebarCollapsed 사용 파일 (이미 셀렉터 패턴)

아래 파일들은 이미 `useMenuStore((state) => state.sidebarCollapsed)` 패턴을 사용 중이므로 **변경 불필요**:
- `ItemDetail.tsx`, `ChecklistDetail.tsx`, `PriceDistributionDetail.tsx`
- `StepDetail.tsx`, `PermissionDetailClient.tsx`, `BoardDetail/index.tsx`
- `ProcessDetail.tsx`, `PricingTableForm.tsx`, `DynamicItemForm/index.tsx`
- `ItemDetailClient.tsx`, `ClientDetail.tsx`, `DetailActions.tsx`

단, 셀렉터 훅이 추가되면 이 파일들도 향후 `useSidebarCollapsed()`로 전환 가능 (선택).

---

### Step 3: `src/stores/themeStore.ts` — 셀렉터 훅 추가

파일 끝에 추가:

```typescript
// ===== 셀렉터 훅 =====

/** 현재 테마만 구독 */
export const useTheme = () =>
  useThemeStore((state) => state.theme);

/** setTheme 액션만 구독 */
export const useSetTheme = () =>
  useThemeStore((state) => state.setTheme);
```

**컨슈머 변경 대상**:

#### 3-A. `src/layouts/AuthenticatedLayout.tsx` (line 100)

현재:
```typescript
const { theme, setTheme } = useThemeStore();
```

변경:
```typescript
const theme = useTheme();
const setTheme = useSetTheme();
```

#### 3-B. `src/components/ThemeSelect.tsx` (line 24)

현재:
```typescript
const { theme, setTheme } = useThemeStore();
```

변경:
```typescript
const theme = useTheme();
const setTheme = useSetTheme();
```

---

## 검증

```bash
npx tsc --noEmit
```

셀렉터 훅은 기존 API에 추가만 하는 것이므로 기존 코드에 영향 없음.
컨슈머 변경은 import 경로와 호출 패턴만 바뀌므로 타입 에러 가능성 낮음.

---

## 변경 파일 총 정리

| # | 파일 | 작업 | 내용 |
|---|------|------|------|
| 1 | `src/stores/useTableColumnStore.ts` | 추가 | 셀렉터 훅 3개 (`usePageColumnSettings`, `useHiddenColumns`, `useColumnWidths`) |
| 2 | `src/stores/menuStore.ts` | 추가 | 셀렉터 훅 4개 (`useSidebarCollapsed`, `useActiveMenu`, `useMenuItems`, `useMenuHydrated`) |
| 3 | `src/stores/themeStore.ts` | 추가 | 셀렉터 훅 2개 (`useTheme`, `useSetTheme`) |
| 4 | `src/hooks/useColumnSettings.ts` | 수정 | `useTableColumnStore()` → 셀렉터 패턴 |
| 5 | `src/layouts/AuthenticatedLayout.tsx` | 수정 | menuStore/themeStore 전체 구독 → 셀렉터 |
| 6 | `src/components/production/WorkerScreen/index.tsx` | 수정 | `useMenuStore()` → `useSidebarCollapsed()` |
| 7 | `src/components/layout/CommandMenuSearch.tsx` | 수정 | `useMenuStore()` → `useMenuItems()` |
| 8 | `src/components/ThemeSelect.tsx` | 수정 | `useThemeStore()` → `useTheme()` + `useSetTheme()` |

---

## 참고: Zustand 셀렉터가 중요한 이유

```
// ❌ 전체 구독 — menuItems 변경 시 sidebarCollapsed만 쓰는 컴포넌트도 리렌더
const { sidebarCollapsed } = useMenuStore();

// ✅ 셀렉터 — sidebarCollapsed 변경 시에만 리렌더
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
// 또는
const sidebarCollapsed = useSidebarCollapsed();
```

Zustand는 `Object.is`로 반환값을 비교. 셀렉터가 원시값(string, boolean, number)을 반환하면 참조 비교로 정확히 변경 감지.
객체를 반환하는 셀렉터(예: `usePageColumnSettings`)는 같은 참조를 반환하므로 해당 pageId의 설정이 변경될 때만 리렌더.
