# SAM ERP 프로젝트 심층분석 종합 보고서
> 분석일: 2026-02-23 | 분석 영역: Util 분리 / 컴포넌트 공통화 / Zustand 통합

---

## 목차
1. [Executive Summary](#1-executive-summary)
2. [Util 함수 분리 분석](#2-util-함수-분리-분석)
3. [컴포넌트 공통화 분석](#3-컴포넌트-공통화-분석)
4. [Zustand 스토어 통합 분석](#4-zustand-스토어-통합-분석)
5. [통합 리팩토링 로드맵](#5-통합-리팩토링-로드맵)

---

## 1. Executive Summary

### 전체 현황 스코어카드

| 영역 | 현재 수준 | 주요 이슈 | 예상 절감 |
|------|----------|----------|----------|
| **Util 분리** | 🟡 보통 | 중복 함수 6건, 과대 파일 4개, 인라인 유틸 6패턴 | ~800줄 |
| **컴포넌트 공통화** | 🟡 보통 | 중복 다이얼로그 5건, Detail 버전 혼재, 패턴 비일관 | ~1,500줄 |
| **Zustand 통합** | 🟢 양호 | Context→Zustand 미전환 3건, 셀렉터 훅 미비 | 리렌더 최적화 |

### Top 5 우선 조치 항목

1. 🔴 **AuthContext → Zustand 마이그레이션** (전역 리렌더 제거)
2. 🔴 **GenericCRUDDialog 추출** (5개 중복 다이얼로그 통합)
3. 🔴 **파일 다운로드 로직 통합** (3곳 중복 → 1곳)
4. 🟡 **dashboard/transformers.ts 분할** (1,700줄 → 도메인별 분리)
5. 🟡 **Detail/DetailClient/DetailClientV2 정리** (버전 혼재 제거)

---

## 2. Util 함수 분리 분석

### 2.1 현재 유틸 파일 인벤토리

```
src/lib/
├── utils.ts                  (cn, safeJsonParse - 최소)
├── formatters.ts             (phone, businessNumber, card, account 포맷터)
├── print-utils.ts            (인쇄 유틸)
├── sanitize.ts               (데이터 정제)
├── error-reporting.ts        (에러 리포팅)
├── utils/                    (13개 파일, ~82KB)
│   ├── amount.ts             (금액 포맷: 원/만원)
│   ├── date.ts               (날짜 유틸)
│   ├── validation.ts         (Zod 스키마 - 725줄 ⚠️)
│   ├── excel-download.ts     (엑셀 다운로드 - 528줄 ⚠️)
│   ├── fileDownload.ts       (파일 다운로드)
│   ├── export.ts             (엑셀 내보내기 - 중복 ⚠️)
│   ├── search.ts             (검색/필터 파이프라인)
│   ├── materialTransform.ts  (자재 데이터 변환)
│   ├── menuTransform.ts      (메뉴 구조 변환)
│   ├── menuRefresh.ts        (메뉴 새로고침)
│   ├── status-config.ts      (상태 스타일 설정)
│   ├── redirect-error.ts     (Next.js 리다이렉트 에러)
│   └── locale.ts             (로케일 유틸)
├── api/                      (25개 파일)
│   ├── error-handler.ts      (API 에러 처리)
│   ├── toast-utils.ts        (토스트 유틸 - 중복 ⚠️)
│   ├── transformers.ts       (변환기 - 454줄 ⚠️)
│   ├── dashboard/transformers.ts  (대시보드 변환 - 1,700줄 🔴)
│   ├── execute-server-action.ts
│   ├── execute-paginated-action.ts
│   └── query-params.ts       (buildApiUrl - 표준화 완료)
├── permissions/              (3개 파일)
├── auth/                     (2개 파일)
└── cache/                    (2개 파일)
```

### 2.2 중복 로직 탐지 (6건)

#### 🔴 HIGH PRIORITY

| # | 중복 항목 | 위치 | 상세 |
|---|----------|------|------|
| 1 | **Blob 다운로드** | `export.ts`, `excel-download.ts`, `fileDownload.ts` | 동일한 `URL.createObjectURL → link.click → revokeObjectURL` 패턴이 3곳에 존재 |
| 2 | **날짜 문자열 생성** | `export.ts:58`, `excel-download.ts:78` | `toISOString().slice(0,10).replace(/-/g,'')` 동일 패턴, 시간 정밀도만 다름(초 vs 분) |
| 3 | **에러 메시지 포맷** | `error-handler.ts:122`, `toast-utils.ts:106` | `getErrorMessage()` vs `formatApiError()` - 동일 로직 |
| 4 | **숫자 포맷팅** | `amount.ts:15`, `formatters.ts:178` | `Intl.NumberFormat` vs regex 기반 - 3가지 접근법 혼재 |

#### 🟡 MEDIUM PRIORITY

| # | 중복 항목 | 위치 |
|---|----------|------|
| 5 | 엑셀 파일명 생성 | `export.ts:54` vs `excel-download.ts:78` |
| 6 | 쿼리 파라미터 빌드 | 레거시 `URLSearchParams` 패턴 (마이그레이션 완료 상태) |

### 2.3 인라인 유틸 추출 후보 (6패턴)

컴포넌트 내부에 반복적으로 등장하지만 util로 분리되지 않은 패턴:

| 패턴 | 발견 위치 | 영향 파일 | 추천 위치 |
|------|----------|----------|----------|
| 월/분기 날짜 범위 계산 | TaxInvoice, HR 페이지들 | 5+ | `lib/utils/dateRange.ts` |
| 시간 문자열 포맷팅 | TransactionFormModal, time-picker | 4+ | `lib/utils/timeFormatter.ts` |
| 포맷된 숫자 파싱 | VendorManagement, Withdrawal 등 | 8+ | `lib/formatters.ts` 확장 |
| 에러 객체→메시지 변환 | attendance/page, employee/page | 3+ | `lib/utils/errorFormatter.ts` |
| 배열 합계/카운트 reduce | 대시보드, 주문관리 등 | 6+ | `lib/utils/aggregation.ts` |
| 파일 크기 포맷팅 | file-input.tsx | 2 | `lib/utils/fileSizeFormatter.ts` |

### 2.4 과대 파일 (분할 필요)

| 파일 | 줄 수 | 문제 | 분할 방안 |
|------|-------|------|----------|
| 🔴 `api/dashboard/transformers.ts` | **1,700+** | 10+ 도메인 변환 혼재 | `dashboard/transformers/{sales,production,quality,accounting,hr,common}.ts` |
| 🟡 `utils/validation.ts` | 725 | 5개 아이템 타입 스키마 혼재 | `validations/{item-master-base,product,part,material,filters}.ts` |
| 🟡 `utils/excel-download.ts` | 528 | 다운로드/내보내기/템플릿 혼재 | `{blob-download,excel-export,excel-template}.ts` |
| 🟡 `api/transformers.ts` | 454 | 27개 export 함수 | `transformers/{pages,sections,fields,bom,templates,options}.ts` |

### 2.5 미사용 유틸 (후보)

| 함수 | 파일 | 상태 |
|------|------|------|
| `parsePhoneNumber()` | `formatters.ts:36` | import 0건 |
| `extractNumbers()` | `formatters.ts:220` | import 0건 |
| `formatPersonalNumber()` | `formatters.ts:84` | 실제 사용은 `formatPersonalNumberMasked` |

---

## 3. 컴포넌트 공통화 분석

### 3.1 현재 컴포넌트 계층 구조

```
src/components/
├── ui/          (49개 - Radix UI 래퍼)
├── atoms/       (3개 - 최소 단위)
├── molecules/   (9개 - 복합 폼/표시)
├── organisms/   (11개 - 비즈니스 컴포넌트)
├── templates/   (2+1개 - UniversalListPage, IntegratedDetailTemplate, IntegratedListTemplateV2)
├── accounting/  (18개 도메인 폴더, 100+ 컴포넌트)
├── settings/    (12개 도메인 폴더)
└── [기타 도메인] (15+ 폴더)
```

### 3.2 중복 컴포넌트 패턴 (핵심 발견)

#### 🔴 CRITICAL: 단순 CRUD 다이얼로그 중복 (5건)

거의 동일한 구조: Dialog 래퍼 → 폼 필드 → 유효성 검증 → 제출/취소 버튼

| 컴포넌트 | 줄 수 | 차이점 |
|----------|-------|--------|
| `settings/RankManagement/RankDialog.tsx` | 89 | 라벨명만 다름 |
| `settings/TitleManagement/TitleDialog.tsx` | 90 | 라벨명만 다름 |
| `settings/PermissionManagement/PermissionDialog.tsx` | ~90 | 라벨명만 다름 |
| `settings/NotificationSettings/ItemSettingsDialog.tsx` | ~90 | 라벨명만 다름 |
| `accounting/VendorManagement/CreditAnalysisModal/` | ~100 | 약간 복잡 |

**해결안**: `GenericCRUDDialog<T>` 제네릭 컴포넌트 생성
```typescript
// src/components/molecules/GenericCRUDDialog.tsx
interface GenericCRUDDialogProps<T> {
  isOpen: boolean;
  onOpenChange: (open: boolean) => void;
  mode: 'add' | 'edit';
  title: string;
  fields: FormFieldDefinition[];
  data?: T;
  onSubmit: (data: T) => Promise<void>;
}
```
→ **~400줄 절감**

#### 🔴 CRITICAL: Detail 파일 버전 혼재

한 엔티티에 대해 여러 버전의 Detail 파일이 공존:

| 엔티티 | 파일들 | 문제 |
|--------|--------|------|
| BadDebt | `BadDebtDetail.tsx`, `BadDebtDetailClientV2.tsx` | V2 마이그레이션 미완 |
| Withdrawal | `WithdrawalDetailClientV2.tsx` | ClientV2 접미사 |
| Deposit | `DepositDetailClientV2.tsx` | ClientV2 접미사 |
| Vendor | `VendorDetail.tsx`, `VendorDetailClient.tsx` | 두 파일 공존 |

→ **단일 소스로 통합 필요, ~300줄 절감**

#### 🟡 HIGH: 리스트 페이지 설정 중복

`UniversalListPage`로 통합은 잘 되어있으나, 설정(config) 코드가 각 페이지에 반복:

| 반복 요소 | 발견 위치 | 해결안 |
|-----------|----------|--------|
| 상태 관리 (data, filters, pagination) | Sales, Purchase, Vendor 등 | 설정 파일 분리 |
| DateRange 선택기 | 8+ 회계 페이지 | `useDateRange()` 훅 표준화 |
| Stats 계산 useMemo | 대부분의 리스트 페이지 | `DataStatsCard<T>` 추출 |

→ **~500줄 절감**

### 3.3 재사용률 분석

#### 높은 재사용 (Good)
- **UniversalListPage**: 40+ 페이지 (우수)
- **IntegratedDetailTemplate**: 20+ 상세 페이지
- **FormField**: 50+ 폼

#### 활용 부족 (Should Use More)
- **SearchableSelectionModal**: 실제 3곳만 사용 → 더 광범위 적용 가능
- **StandardDialog**: 존재하지만 단순 다이얼로그들이 미사용
- **MobileCard**: 정의되었지만 비일관적 사용

### 3.4 패턴 비일관성

| 패턴 | 현재 상태 | 표준화 방향 |
|------|----------|------------|
| 날짜 범위 선택 | 3가지 방식 혼재 (컴포넌트/훅/인라인) | `useDateRange()` + `<DateRangeSelector />` |
| 검색/필터 | 3가지 경쟁 패턴 (A: UniversalListPage, B: 커스텀 useState, C: IntegratedListTemplateV2) | Pattern A로 통일 |
| 모달 vs 페이지 | VendorDetail→풀페이지, PurchaseDetail→모달 혼재 | 도메인별 기준 확립 |

### 3.5 추출 필요 공유 컴포넌트

| 컴포넌트 | 사용처 | 설명 |
|----------|--------|------|
| `LineItemsTable<T>` | SalesDetail, PurchaseDetail | 품목 추가/삭제/계산 테이블 (~150줄×2 절감) |
| `DataStatsCard<T>` | 회계 리스트 페이지들 | 유연한 통계 표시 카드 |
| `DocumentTemplate` | CreditAnalysis, InspectionReport | 인쇄용 문서 래퍼 (헤더/푸터/워터마크) |
| `DataTableWithActions` | 대부분의 리스트 | 페이지네이션+선택+액션 통합 |

---

## 4. Zustand 스토어 통합 분석

### 4.1 현재 스토어 인벤토리 (7개)

| 스토어 | 파일 | 줄 수 | 미들웨어 | 용도 |
|--------|------|-------|---------|------|
| `useItemMasterStore` | `stores/item-master/useItemMasterStore.ts` | 1,150 | devtools, immer | 품목기준관리 정규화 상태 |
| `useMasterDataStore` | `stores/masterDataStore.ts` | 450 | devtools | 동적 폼 설정 캐싱 |
| `useMenuStore` | `stores/menuStore.ts` | ~100 | persist | 사이드바/메뉴 상태 |
| `useFavoritesStore` | `stores/favoritesStore.ts` | ~100 | persist + custom storage | 즐겨찾기 (최대 10개) |
| `useThemeStore` | `stores/themeStore.ts` | ~50 | persist | 테마 (light/dark/senior) |
| `useTableColumnStore` | `stores/useTableColumnStore.ts` | ~100 | persist + custom storage | 테이블 컬럼 가시성/너비 |
| `useCalendarScheduleStore` | `stores/useCalendarScheduleStore.ts` | ~100 | devtools | 캘린더 일정 연도별 캐싱 |

### 4.2 핵심 발견: Context → Zustand 미전환 (3건)

#### 🔴 #1: AuthContext (최우선)

| 항목 | 현재 | 문제 |
|------|------|------|
| **위치** | `/src/contexts/AuthContext.tsx` (278줄) | React Context + useState |
| **상태** | users[], currentUser, roles, tenants | Provider 리렌더 전파 |
| **localStorage** | 수동 동기화 (line 162-190) | Zustand persist가 자동 처리 가능 |
| **영향** | 사이드바, 대시보드, 모든 인증 페이지 | 상태 변경 시 전체 앱 리렌더 |

**전환 방안**:
```typescript
// /src/stores/authStore.ts
export const useAuthStore = create<AuthState>()(
  persist(
    devtools((set) => ({
      currentUser: null,
      setCurrentUser: (user) => set({ currentUser: user }),
      // ... 기타 액션
    })),
    { name: 'mes-currentUser' }
  )
);
```

#### 🟡 #2: ItemMasterContext (중복 제거)

| 항목 | 현재 | 문제 |
|------|------|------|
| **Context** | `contexts/ItemMasterContext.tsx` (27,922 토큰) | useState 13개+ 상태 |
| **Zustand** | `stores/item-master/useItemMasterStore.ts` (1,150줄) | 유사 데이터 관리 |
| **중복** | 양쪽에서 품목 마스터 데이터 관리 | 캐싱/API 레이어 분리 |

→ **Context를 Zustand 스토어로 통합, Context는 얇은 래퍼로만 유지**

#### 🟡 #3: PermissionContext

| 항목 | 현재 | 문제 |
|------|------|------|
| **위치** | `contexts/PermissionContext.tsx` | 순수 데이터/셀렉터 패턴 |
| **적합도** | Zustand 셀렉터 패턴에 완벽 부합 | Provider 불필요 |

### 4.3 셀렉터 훅 미비 (성능 이슈)

| 스토어 | 셀렉터 훅 | 문제 |
|--------|----------|------|
| ✅ `masterDataStore` | `usePageConfig()`, `usePageConfigLoading()` 등 | 양호 |
| ❌ `useTableColumnStore` | 없음 - 전체 스토어 구독 | 불필요한 리렌더 |
| ❌ `useMenuStore` | 없음 - 전체 스토어 구독 | 사이드바 토글이 모든 구독자 리렌더 |
| ❌ `useThemeStore` | 없음 | 경미 |

**해결 패턴**:
```typescript
// ✅ 추가 필요
export const useTableSettings = (pageId: string) =>
  useTableColumnStore((state) => state.pageSettings[pageId]);

export const useMenuActiveId = () =>
  useMenuStore((state) => state.activeMenu);

export const useSidebarCollapsed = () =>
  useMenuStore((state) => state.sidebarCollapsed);
```

### 4.4 Custom Storage 중복

`favoritesStore`와 `tableColumnStore`에서 동일한 사용자별 localStorage 래퍼가 반복:

```typescript
// 두 파일 모두 동일 패턴 반복:
const customStorage = {
  getItem: (name) => { /* userId 기반 키 생성 */ },
  setItem: (name, value) => { /* userId 기반 키로 저장 */ },
  removeItem: (name) => { /* userId 기반 키로 삭제 */ },
};
```

**해결안**: `/src/lib/storage/user-scoped-storage.ts` 추출
```typescript
export function createUserScopedStorage(prefix: string): StateStorage {
  return { getItem, setItem, removeItem };
}
```

### 4.5 누락된 스토어 기회

| 스토어 | 용도 | 현재 상태 |
|--------|------|----------|
| 🔴 `useUIStore` | 전역 모달/노티/로딩 | 각 컴포넌트에서 로컬 관리 |
| 🟡 글로벌 필터 상태 | 리스트 페이지 공통 필터 | useState로 산재 |

---

## 5. 통합 리팩토링 로드맵

### Phase 1: 즉시 (1주)

| 작업 | 영역 | 영향도 | 난이도 |
|------|------|--------|--------|
| AuthContext → Zustand 마이그레이션 | Zustand | 🔴 전역 리렌더 제거 | 중 |
| GenericCRUDDialog 추출 (5개 다이얼로그 통합) | 컴포넌트 | 🔴 ~400줄 절감 | 저 |
| Blob 다운로드 로직 통합 (3곳→1곳) | Util | 🔴 중복 제거 | 저 |
| 에러 메시지 포맷 통합 (`formatApiError` 제거) | Util | 🟡 API 레이어 정리 | 저 |
| Zustand 셀렉터 훅 추가 (3개 스토어) | Zustand | 🟡 리렌더 최적화 | 저 |

### Phase 2: 단기 (2~3주)

| 작업 | 영역 | 영향도 | 난이도 |
|------|------|--------|--------|
| `dashboard/transformers.ts` 분할 (1,700줄) | Util | 🟡 유지보수성 | 중 |
| Detail 파일 버전 정리 (V2 통합) | 컴포넌트 | 🟡 ~300줄 절감 | 중 |
| `LineItemsTable<T>` organism 추출 | 컴포넌트 | 🟡 Sales/Purchase 공통화 | 중 |
| Custom Storage 유틸 추출 | Zustand | 🟡 DRY | 저 |
| 날짜 범위 선택 표준화 | 컴포넌트 | 🟡 패턴 통일 | 중 |

### Phase 3: 중기 (3~4주)

| 작업 | 영역 | 영향도 | 난이도 |
|------|------|--------|--------|
| `validation.ts` 분할 (725줄) | Util | 🟢 유지보수성 | 저 |
| ItemMasterContext → Zustand 통합 | Zustand | 🟡 중복 제거 | 고 |
| IntegratedListTemplateV2 폐기 | 컴포넌트 | 🟢 레거시 제거 | 중 |
| 인라인 유틸 추출 (6패턴) | Util | 🟢 코드 품질 | 저 |
| 미사용 유틸 함수 정리 | Util | 🟢 코드 청결 | 저 |

### Phase 4: 장기 (4주+)

| 작업 | 영역 | 영향도 | 난이도 |
|------|------|--------|--------|
| PermissionContext → Zustand | Zustand | 🟢 아키텍처 통일 | 중 |
| DocumentTemplate organism 추출 | 컴포넌트 | 🟢 인쇄 공통화 | 중 |
| useUIStore 생성 (전역 UI 상태) | Zustand | 🟢 모달/노티 통합 | 중 |
| 숫자 포맷팅 API 표준화 | Util | 🟢 일관성 | 저 |

---

## 부록: 핵심 파일 참조

### 리팩토링 대상 (Util)
- `/src/lib/utils/export.ts` - 중복 제거 대상
- `/src/lib/utils/excel-download.ts` - 분할 대상 (528줄)
- `/src/lib/utils/validation.ts` - 분할 대상 (725줄)
- `/src/lib/api/dashboard/transformers.ts` - 분할 대상 (1,700줄)
- `/src/lib/api/toast-utils.ts` - `formatApiError` 제거 대상

### 리팩토링 대상 (컴포넌트)
- `/src/components/settings/RankManagement/RankDialog.tsx` - GenericCRUDDialog로 대체
- `/src/components/settings/TitleManagement/TitleDialog.tsx` - GenericCRUDDialog로 대체
- `/src/components/accounting/BadDebtCollection/BadDebtDetailClientV2.tsx` - 버전 통합
- `/src/components/accounting/WithdrawalManagement/WithdrawalDetailClientV2.tsx` - 버전 통합
- `/src/components/accounting/DepositManagement/DepositDetailClientV2.tsx` - 버전 통합

### 리팩토링 대상 (Zustand)
- `/src/contexts/AuthContext.tsx` → `/src/stores/authStore.ts`
- `/src/contexts/ItemMasterContext.tsx` → `/src/stores/item-master/` 통합
- `/src/stores/useTableColumnStore.ts` - 셀렉터 훅 추가
- `/src/stores/menuStore.ts` - 셀렉터 훅 추가
- `/src/stores/favoritesStore.ts` - custom storage 유틸 추출
