# Phase 1 수정: 실제 중복 기반 리팩토링 체크리스트

**작성일**: 2026-02-09
**상태**: Step 1-4 전체 완료 ✅
**관련 문서**: `architecture/[PLAN-2026-02-06] refactoring-roadmap.md`

---

## 계획 수정 사유

기존 계획의 `useListData`, `usePagination`, `useClientSideFiltering`, `useSelection` 4개 훅은
**`UniversalListPage` 템플릿이 이미 내부적으로 처리**하고 있어 불필요.
(리스트 페이지 38개 중 33개+가 이미 UniversalListPage 사용 중)

실제 코드 분석 결과, 진짜 중복 핫스팟은 다른 곳에 있음.

---

## 실제 중복 핫스팟 (영향도 순)

| 순위 | 대상 | 파일 수 | 중복 라인 | ROI |
|------|------|---------|-----------|-----|
| 🔴 1 | action.ts CRUD 에러처리 보일러플레이트 | 82개 | ~3,000줄 | 최고 |
| 🟡 2 | 삭제 다이얼로그 상태 반복 | 7개+ | ~150줄 | 높음 (난이도 낮음) |
| 🟡 3 | Stats 로딩 패턴 반복 | 30개+ | ~600줄 | 중간 |
| 🟢 4 | React.memo 미적용 (Phase 5) | 30개+ | 성능 개선 | 중간 |
| 🟢 5 | any 타입 102곳 (Phase 5) | 29개 | 타입 안전성 | 중간 |

---

## Step 1: action.ts 에러처리 래퍼 (ROI 최고)

> 82개 action.ts에서 함수마다 반복되는 try/catch + 인증 에러 + 응답 검증 패턴 추출

### 현재 반복 패턴 (함수마다 ~15줄씩 반복)

```typescript
// 이 패턴이 82개 파일, 평균 5-8개 함수 = 400-600회 반복
try {
  const { response, error } = await serverFetch(url, options);
  if (error?.__authError) {
    return { success: false, __authError: true };
  }
  if (!response) {
    return { success: false, error: error?.message || '서버 오류가 발생했습니다.' };
  }
  const result = await response.json();
  if (!response.ok || !result.success) {
    return { success: false, error: result.message || '처리에 실패했습니다.' };
  }
  return { success: true, data: result.data };
} catch (error) {
  if (isNextRedirectError(error)) throw error;
  console.error('[FunctionName] error:', error);
  return { success: false, error: '서버 오류가 발생했습니다.' };
}
```

### 생성할 유틸리티

- [x] `src/lib/api/execute-server-action.ts` - 에러처리 래퍼 (138줄)

```typescript
// 사용 전: ~20줄
export async function getRanks() {
  try {
    const { response, error } = await serverFetch(url, { method: 'GET' });
    if (error?.__authError) return { success: false, __authError: true };
    if (!response) return { success: false, error: error?.message || '...' };
    const result = await response.json();
    if (!response.ok || !result.success) return { success: false, error: result.message || '...' };
    return { success: true, data: result.data.map(transform) };
  } catch (error) {
    if (isNextRedirectError(error)) throw error;
    console.error('[getRanks] error:', error);
    return { success: false, error: '...' };
  }
}

// 사용 후: ~5줄
export const getRanks = createServerAction({
  url: `${API_URL}/api/v1/ranks`,
  method: 'GET',
  transform: (data) => data.map(transformRank),
});
```

### 파일럿 마이그레이션 (5개)

| # | 파일 | 함수 수 | 실제 절감 | 상태 |
|---|------|---------|-----------|------|
| 1 | `settings/RankManagement/actions.ts` | 5개 | 276→110줄 (-166) | [x] |
| 2 | `settings/TitleManagement/actions.ts` | 5개 | 276→110줄 (-166) | [x] |
| 3 | `clients/actions.ts` | 4개+1유틸 | 258→76줄 (-182) | [x] |
| 4 | `hr/DepartmentManagement/actions.ts` | 6개 | 327→246줄 (-81) | [x] |
| 5 | `vehicle-management/VehicleList/actions.ts` | - | Mock 데이터 (스킵) | [x] 스킵 |

### 파일럿 검증

- [x] 4개 파일 타입체크 통과 (tsc --noEmit)
- [x] executeServerAction 인터페이스 확정 (transform, body, method, cache)
- [x] 에지 케이스 처리 (FormData 지원 완료, Mock fallback 패턴 확립)
- [ ] 실제 동작 테스트 (사용자 확인 필요)

### 확대 적용 (77개) → 완료

> 도메인별 순차 적용 완료

| Wave | 도메인 | 파일 수 | 상태 |
|------|--------|---------|------|
| A | settings (나머지) | 8개 | [x] |
| B | accounting | 13개 | [x] |
| C | hr (나머지) | 5개 | [x] |
| D | construction | 17개 | [x] |
| E | production | 4개 | [x] |
| F | quality + material | 4개 | [x] |
| G | 기타 (board, approval, outbound, pricing 등) | 26개 | [x] |

### 최종 잔여 serverFetch (의도적 유지 - 2개 파일)

| 파일 | 함수 | 유지 사유 |
|------|------|-----------|
| `material/ReceivingManagement/actions.ts` | `checkInspectionTemplate` | HTTP 404 구분 필요 |
| `quotes/actions.ts` | 특정 함수 | 특수 응답 처리 필요 |

### 추가 마이그레이션 (lib/actions)

| 파일 | 함수 | 상태 |
|------|------|------|
| `lib/actions/fcm.ts` | `sendFcmNotification` | [x] |
| `lib/actions/bulk-actions.ts` | `bulkUpdateAccountCode` | [x] |
| `lib/actions/bulk-actions.ts` | `exportToExcel` | 유지 (Blob 반환, native fetch) |

---

## Step 2: useDeleteDialog 훅 (난이도 최저)

> 삭제 확인 다이얼로그 상태 + 핸들러 추출

### 생성할 훅

- [x] `src/hooks/useDeleteDialog.ts` (75줄)

```typescript
// 사용 전: ~25줄
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
const [bulkDeleteIds, setBulkDeleteIds] = useState<string[]>([]);
const handleDeleteClick = useCallback((id) => { ... }, []);
const handleConfirmDelete = useCallback(async () => { ... }, []);
const handleBulkDelete = useCallback((ids) => { ... }, []);
const handleConfirmBulkDelete = useCallback(async () => { ... }, []);

// 사용 후: ~3줄
const { single, bulk } = useDeleteDialog({
  onDelete: deleteVehicle,
  onBulkDelete: bulkDeleteVehicles,
  onSuccess: () => loadData(),
});
```

### 적용 대상

| # | 파일 | 상태 |
|---|------|------|
| 1 | `vehicle-management/VehicleList/index.tsx` | [x] |
| 2 | `vehicle-management/VehicleLogList/index.tsx` | [x] |
| 3 | `vehicle-management/ForkliftList/index.tsx` | [x] |
| 4 | `process-management/ProcessListClient.tsx` | [x] 스킵 (공유 isLoading + 커스텀 stats 업데이트 + 비표준 벌크삭제) |
| 5 | `quotes/QuoteManagementClient.tsx` | [x] |
| 6 | `accounting/BillManagement/BillManagementClient.tsx` | [x] (커스텀 onDelete 콜백) |
| 7 | `accounting/VendorManagement/VendorManagementClient.tsx` | [x] (커스텀 onDelete 콜백) |

---

## Step 3: useStatsLoader 훅 (30+ 파일)

> Stats 별도 로딩 패턴 추출

### 생성할 훅

- [x] `src/hooks/useStatsLoader.ts` (45줄)

```typescript
// 사용 전: ~15줄
const [stats, setStats] = useState<Stats>({ total: 0, active: 0, inactive: 0 });
useEffect(() => {
  const loadStats = async () => {
    try {
      const result = await getStats();
      if (result.success && result.data) setStats(result.data);
    } catch (error) {
      if (isNextRedirectError(error)) throw error;
      console.error('loadStats error:', error);
    }
  };
  loadStats();
}, []);

// 사용 후: ~1줄
const { data: stats } = useStatsLoader(getProcessStats, { total: 0, active: 0, inactive: 0 });
```

### 적용 대상 (고빈도 10개 우선)

| # | 파일 | 상태 |
|---|------|------|
| 1 | `process-management/ProcessListClient.tsx` | [x] 스킵 (stats UI 미사용, Promise.all 로딩) |
| 2 | `production/WorkOrders/WorkOrderList.tsx` | [x] 스킵 (tabCounts 동기화 복잡도) |
| 3 | `quality/InspectionManagement/InspectionList.tsx` | [x] |
| 4 | `outbound/ShipmentManagement/ShipmentList.tsx` | [x] |
| 5 | `business/construction/contract/ContractListClient.tsx` | [x] |
| 6 | `business/construction/management/ConstructionManagementListClient.tsx` | [x] |
| 7 | `business/construction/bidding/BiddingListClient.tsx` | [x] |
| 8 | `business/construction/estimates/EstimateListClient.tsx` | [x] |
| 9 | `business/construction/order-management/OrderManagementListClient.tsx` | [x] 스킵 (stats 완전 미사용) |
| 10 | `business/construction/progress-billing/ProgressBillingManagementListClient.tsx` | [x] |

---

## Step 4: Phase 5 항목 (성능/타입)

### React.memo 적용

> 전수 조사 결과: Card 13개, Item 4개, Row 2개 발견. 리스트 반복 렌더링 3개 우선 적용.

| # | 컴포넌트 | 파일 | 적용 사유 | 상태 |
|---|---------|------|----------|------|
| 1 | `InfoField` | `organisms/MobileCard.tsx` | 모든 리스트 페이지에서 반복 사용 | [x] |
| 2 | `CommentItem` | `board/CommentSection/CommentItem.tsx` | map() 반복 렌더링 | [x] |
| 3 | `WorkItemCard` | `production/WorkerScreen/WorkItemCard.tsx` | map() 반복 렌더링 | [x] |

> 스킵 사유:
> - MobileCard: ReactNode props (headerBadges, statusBadge, infoGrid) 매 렌더 새 참조 → memo 비효율적
> - DepartmentTreeItem: Set props (expandedIds, selectedIds) 매 렌더 새 참조 → memo 비효율적
> - construction cards (OrderMemoCard 등 8개): 단일 사용, 폼 카드 → ROI 낮음

### any 타입 제거

> 전수 조사 결과: 139건/58파일. Step 1에서 action 에러핸들러 50곳+ 자동 해결 완료.

| # | 대상 | 건수 | 상태 |
|---|------|------|------|
| 1 | action error handler | 50+ | [x] Step 1에서 자동 해결 |
| 2 | `lib/api/logger.ts` (any → unknown) | 7건 | [x] |
| 3 | items/ 도메인 (ItemMasterDataManagement 등) | ~60건 | [x] 스킵 (도메인 복잡도 높음, 별도 작업 필요) |
| 4 | dev/ 대시보드 프로토타입 | 6건 | [x] 스킵 (비프로덕션 코드) |
| 5 | Form 에러 캐스팅 (errors as any) | 26건 | [x] 스킵 (React Hook Form 타입 시스템 변경 필요) |

### @ts-ignore / @ts-expect-error 제거

- [x] 전수 조사 결과: **0건** (이미 제거 완료)

---

## 진행 규칙

1. **Step 1 createServerAction이 최우선** (ROI 최고, 82개 파일 영향)
2. **Step 2-3은 병렬 가능** (독립적)
3. **Step 4는 Step 1 완료 후** (any 제거가 createServerAction과 겹침)
4. **각 Step 사이 안전한 중단점** (다른 작업 끼워넣기 가능)

---

## 예상 효과 (수정)

| 지표 | 기존 계획 | 수정 계획 |
|------|-----------|-----------|
| 코드 절감 | ~8,500줄 (잘못된 추정) | **~3,750줄** (실증 기반) |
| 영향 파일 수 | 39개 | **82개+ (action) + 37개 (훅)** |
| 패턴 통일 | 데이터 페칭 (이미 완료) | **에러처리 + 상태관리** |
| 추가 효과 | 없음 | **타입 안전성 향상 (any 50곳 자동 해결)** |

---

## 변경 이력

| 날짜 | 변경 내용 |
|------|-----------|
| 2026-02-09 | 초기 작성 - 39개 페이지 대상 |
| 2026-02-09 | **전면 수정** - 실제 코드 분석 결과 UniversalListPage가 이미 처리 중. action.ts 에러처리 래퍼 + 삭제 다이얼로그 훅 + Stats 로딩 훅으로 변경 |
| 2026-02-09 | **Step 1 완료** - 전체 action.ts 마이그레이션 완료 (파일럿 4개 + Wave A~G + lib/actions 2개). serverFetch 의도적 유지 2개 파일 외 전부 executeServerAction으로 전환. 타입체크 0 에러. |
| 2026-02-09 | **Step 2 완료** - useDeleteDialog 훅 생성 및 6개 파일 적용 (1개 스킵). 표준 패턴(VehicleList, VehicleLogList, ForkliftList, QuoteManagementClient)은 4 useState+4 핸들러 제거. 커스텀 패턴(BillManagement, VendorManagement)은 커스텀 onDelete 콜백으로 로컬 상태 업데이트 유지. ProcessListClient는 패턴 차이로 스킵. |
| 2026-02-09 | **Step 3 완료** - useStatsLoader 훅 생성(45줄) 및 7개 파일 적용 (3개 스킵). Construction 패턴(5개: Contract, ConstructionManagement, Bidding, Estimate, ProgressBilling) - useState+useEffect→useStatsLoader 1줄 전환. Standard 패턴(2개: InspectionList, ShipmentList) - reload 함수 활용하여 getList 내 stats 리로드 단순화. 스킵: ProcessListClient(stats UI 미사용), WorkOrderList(tabCounts 동기화 복잡), OrderManagementListClient(stats 완전 미사용). 타입체크 0 에러. |
| 2026-02-09 | **Step 4 완료** - React.memo 3개 적용(InfoField, CommentItem, WorkItemCard - 리스트 반복 렌더링 대상). any→unknown 7건(logger.ts). @ts-ignore 0건(이미 제거됨). 잔여 any 92건은 items/ 도메인(60건, 복잡도 높음), dev/ 프로토타입(6건), Form 에러캐스팅(26건, RHF 타입 변경 필요)으로 별도 작업 필요. |
| 2026-02-09 | **Phase 1 전체 완료** - Step 1(executeServerAction 82개 파일) + Step 2(useDeleteDialog 6개 파일) + Step 3(useStatsLoader 7개 파일) + Step 4(React.memo 3개 + any→unknown 7건 + @ts-ignore 0건). |
| 2026-02-09 | **Phase 1 자동 검증 통과** - TypeScript 0 에러(기존 QuoteRegistrationV2만), 미사용 import 0건, executeServerAction 59파일 일관 패턴, 핵심 훅 3개 무결성 확인. |
| 2026-02-09 | **Phase 1 수동 검증 통과** - 화면 버디 검수 완료. |
| 2026-02-09 | **Phase 3 프로토타입 검증** - `createCrudService` 팩토리 생성(`src/lib/api/create-crud-service.ts`). RankManagement/actions.ts 마이그레이션(111줄→77줄). 직급관리 5/5 CRUD 정상 동작 확인. Server Action 호환성 검증 완료. |
