refactor(WEB): DataTable 개선 및 회계 상세 컴포넌트 리팩토링

- DataTable 컴포넌트 기능 확장 및 코드 개선
- 회계 상세 컴포넌트(Bill/Deposit/Purchase/Sales/Withdrawal) 리팩토링
- 엑셀 다운로드 유틸리티 개선
- 대시보드 및 각종 리스트 페이지 업데이트
- dashboard_type2 페이지 추가
- 프론트엔드 개선 로드맵 문서 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-11 11:03:19 +09:00
parent 0db6302652
commit e14335b635
33 changed files with 1354 additions and 217 deletions

View File

@@ -0,0 +1,207 @@
# SAM ERP 프론트엔드 개선 로드맵
> 작성일: 2025-02-10
> 분석 기준: src/ 전체 (500+ 파일, ~163K줄)
---
## Phase A: 즉시 개선 (1~2일)
### A-1. `<img>` → `next/image` 전환
- **문제**: raw `<img>` 태그 ~10건 — 이미지 최적화/lazy loading 미적용
- **대상 파일**:
- `src/app/[locale]/(protected)/quality/qms/components/documents/ProductInspectionDocument.tsx`
- `src/components/quality/InspectionManagement/ProductInspectionInputModal.tsx`
- `src/components/vehicle-management/VehicleLogDetail/config.tsx` (2건)
- `src/components/process-management/InspectionPreviewModal.tsx` (2건)
- `src/app/[locale]/(protected)/dev/dashboard/_components/AIPoweredDashboard.tsx`
- `src/components/ui/image-upload.tsx`
- **작업**: `<img src={...}>``<Image src={...} width={} height={} alt={} />` 전환
- **주의**: 외부 URL 이미지는 `next.config.ts``images.remotePatterns` 설정 필요
- **효과**: LCP 개선, 자동 lazy loading, WebP 변환
### A-2. DataTable 렌더링 최적화
- **문제**: `src/components/organisms/DataTable.tsx:254` — 행마다 인라인 함수 생성
```tsx
// 현재: 매 렌더마다 새 함수 100개 생성
onClick={() => onRowClick?.(row)}
```
- **작업**:
1. TableRow를 별도 컴포넌트로 추출 + `React.memo` 적용
2. `onRowClick` 핸들러를 `useCallback`으로 감싸기
3. 행 데이터 비교를 위한 커스텀 비교 함수 작성
- **관련 파일**:
- `src/components/organisms/DataTable.tsx`
- `src/components/business/construction/estimates/sections/EstimateDetailTableSection.tsx` (행당 6+ 인라인 함수)
- `src/components/business/construction/progress-billing/tables/PhotoTable.tsx`
- **효과**: 대형 테이블 30~50% 재렌더 감소
---
## Phase B: 단기 개선 (1주)
### B-1. `next/dynamic` 코드 스플리팅 도입
- **문제**: `dynamic()` 사용 0건 — 전체 앱이 단일 번들로 로드
- **우선 적용 대상**:
| 컴포넌트 | 줄 수 | 이유 |
|---------|-------|------|
| `MainDashboard.tsx` | 2,651 | recharts (~60KB) 포함, 대시보드 미방문 시 불필요 |
| `WorkerScreen/index.tsx` | 1,439 | 생산직 전용, 일반 사용자 불필요 |
| 각종 모달 컴포넌트 | 다수 | 열기 전까지 불필요 |
- **작업 예시**:
```tsx
import dynamic from 'next/dynamic';
const MainDashboard = dynamic(
() => import('@/components/business/MainDashboard'),
{ loading: () => <DashboardSkeleton />, ssr: false }
);
```
- **효과**: 초기 번들 사이즈 대폭 감소, 페이지별 로딩 속도 개선
### B-2. API 병렬 호출 적용
- **문제**: `Promise.all` 사용 0건 — 독립적인 API 호출이 순차 실행될 가능성
- **우선 점검 대상**:
- 대시보드 초기 데이터 로딩 (5~10개 API)
- 상세 페이지 초기 데이터 + 공통코드 + 마스터데이터
- 폼 페이지 초기값 + 선택지 목록
- **작업**:
```tsx
// Before: 순차
const categories = await fetchCategories();
const units = await fetchUnits();
const codes = await fetchCommonCodes();
// After: 병렬
const [categories, units, codes] = await Promise.all([
fetchCategories(),
fetchUnits(),
fetchCommonCodes(),
]);
```
- **효과**: 초기 데이터 로딩 30~40% 단축
### B-3. `store/` vs `stores/` 디렉토리 통합
- **문제**: Zustand 스토어가 두 디렉토리에 분산
- `src/store/` — menuStore, themeStore, demoStore (3개)
- `src/stores/` — itemStore, masterDataStore, useItemMasterStore (3개)
- **작업**:
1. `src/store/` 내용을 `src/stores/`로 이동
2. import 경로 일괄 수정
3. `src/store/` 디렉토리 삭제
- **추가 점검**: ThemeContext ↔ themeStore 중복 → 하나로 통합
---
## Phase C: 중기 개선 (2~3주)
### C-1. 대형 테이블 가상화 (react-window)
- **문제**: 100행 이상 테이블에서 전체 DOM 렌더 — 스크롤 성능 저하
- **대상**:
- `src/components/templates/IntegratedListTemplateV2.tsx` (1,086줄)
- `src/components/templates/UniversalListPage/index.tsx` (1,006줄)
- `src/components/organisms/DataTable.tsx`
- **작업**:
1. `react-window` 패키지 설치
2. DataTable 내부에 `FixedSizeList` 또는 `VariableSizeList` 적용
3. 기존 페이지네이션과 조합 (50건/페이지 + 가상화)
- **주의**: 테이블 헤더 고정, 체크박스 선택, rowSpan 등 기존 기능 호환 필요
- **효과**: 대용량 테이블 렌더 10배 이상 개선
### C-2. SWR 또는 React Query 도입
- **문제**: API 캐싱 전략 없음 — 중복 요청, stale 데이터 가능
- **권장**: SWR (가볍고 Next.js 팀 제작)
- **적용 범위**:
1. **1단계**: 공통코드/마스터데이터 (변경 빈도 낮음, 캐싱 효과 큼)
2. **2단계**: 리스트 페이지 데이터
3. **3단계**: 상세 페이지 데이터
- **기대 효과**:
- 자동 중복 요청 제거
- stale-while-revalidate로 체감 속도 개선
- 포커스 복귀 시 자동 재검증
- **작업량**: 6~8시간 (기본 설정 + 공통코드 적용)
### C-3. Action 팩토리 패턴 확대
- **문제**: 81개 `actions.ts` 파일에서 15~20% 코드 중복
- **현황**: `src/lib/api/create-crud-service.ts` (177줄) 존재하지만 미활용
- **작업**:
1. 기존 팩토리 분석 및 확장
2. 도메인별 actions를 팩토리 기반으로 전환
3. 커스텀 로직만 오버라이드
- **우선 적용**: 가장 단순한 CRUD 도메인부터 (clients, vendors 등)
### C-4. V1/V2 컴포넌트 정리
- **문제**: 12+개 파일이 V1/V2 중복 존재
- **대상 파일** (예시):
- `ClientDetailClient.tsx` ↔ `ClientDetailClientV2.tsx`
- `BadDebtDetail.tsx` ↔ `BadDebtDetailClientV2.tsx`
- `QuoteRegistration.tsx` ↔ `QuoteRegistrationV2.tsx`
- `InspectionModal.tsx` ↔ `InspectionModalV2.tsx`
- **작업**:
1. 각 V1/V2 쌍의 실제 사용처 확인 (import 추적)
2. V2를 최종본으로 확정
3. V1 참조를 V2로 전환
4. V1 파일 삭제 + V2에서 "V2" 접미사 제거
---
## Phase D: 장기 개선 (필요 시)
### D-1. God 컴포넌트 분리
| 파일 | 줄 수 | 분리 방안 |
|------|-------|----------|
| `MainDashboard.tsx` | 2,651 | DashboardShell + ChartSection + StatSection + FilterSection |
| `ItemMasterContext.tsx` | 2,701 | PageContext + SectionContext + FieldContext + BOMContext |
| `item-master.ts` (API) | 2,232 | pages.ts + sections.ts + fields.ts + bom.ts |
| `QuoteRegistration.tsx` | 1,251 | LocationPanel + PricingPanel + LineItemsPanel |
| `WorkerScreen/index.tsx` | 1,439 | ProcessSection + MaterialSection + IssueSection |
### D-2. `as` 타입 캐스트 점진적 제거 (926건)
- 주요 집중 영역: API 트랜스포머, 컴포넌트 props
- 제네릭 타입 활용으로 캐스트 대체
- 도메인별 점진적 개선 (items → quotes → accounting 순)
### D-3. `@deprecated` 함수 정리 (13파일)
- deprecated 선언했지만 아직 import되는 함수들 제거
- ItemMasterContext 내 deprecated 메서드 마이그레이션
### D-4. Molecules 레이어 활성화
- 현재 8개만 존재, 대부분 도메인 컴포넌트가 UI 직접 사용
- 반복되는 UI 패턴을 Molecules로 추출
- FormField, StatusBadge, DateRangeSelector 활용도 높이기
### D-5. 모달 컴포넌트 통합 (47+개)
- SearchableSelectionModal 패턴으로 통합 가능한 모달 식별
- 도메인별 재구현된 유사 모달을 공통 컴포넌트로 전환
### D-6. 기타
- TODO/FIXME 102건 정리 (useItemMasterStore 15건, DraftBox 24건 집중)
- `reactStrictMode: true`로 복원 (개발 환경)
- puppeteer/chromium 패키지 활성 사용 여부 확인 → 미사용 시 제거
- error-handler.ts의 `SHOW_ERROR_CODE` 환경변수로 전환
---
## 이전 리팩토링 완료 항목 (참고)
| 항목 | 상태 | 날짜 |
|------|------|------|
| Phase 1: 공통 훅 추출 (executeServerAction 등) | ✅ 완료 | 이전 세션 |
| Phase 3: 공용 유틸 추출 (PaginatedApiResponse 등) | ✅ 완료 | 이전 세션 |
| Phase 4: SearchableSelectionModal 공통화 | ✅ 완료 | 이전 세션 |
| Phase 5: any 21건 + memo 3개 정리 | ✅ 완료 | 이전 세션 |
| console.log 524건 → 22건 정리 | ✅ 완료 | 2025-02-10 |
| TODO 주석 정리 (login route) | ✅ 완료 | 2025-02-10 |
| SSR 가드 추가 (ThemeContext, ApiErrorContext, useDetailPageState) | ✅ 완료 | 2025-02-10 |
| 커스텀 훅 불필요 'use client' 15개 제거 | ✅ 완료 | 2025-02-10 |
| formatDate 이름 충돌 해소 → formatCalendarDate | ✅ 완료 | 2025-02-10 |
---
## 우선순위 요약
```
즉시 (Phase A) → img 최적화, DataTable 최적화
단기 (Phase B) → 코드 스플리팅, API 병렬화, 스토어 통합
중기 (Phase C) → 가상화, 캐싱, Action 팩토리, V2 정리
장기 (Phase D) → God 컴포넌트 분리, 타입 안전성, 모달 통합
```

View File

@@ -1,6 +1,6 @@
# claudedocs 문서 맵
> 프로젝트 기술 문서 인덱스 (Last Updated: 2026-02-09)
> 프로젝트 기술 문서 인덱스 (Last Updated: 2026-02-10)
## 빠른 참조
@@ -10,6 +10,74 @@
---
## 프로젝트 기술 결정 사항
### `<img>` 태그 사용 — `next/image` 미사용 이유 (2026-02-10)
**현황**: 프로젝트 전체 `<img>` 태그 10건, `next/image` 0건
**결정**: `<img>` 유지, `next/image` 전환 불필요
**근거**:
1. **폐쇄형 ERP 시스템** — SEO 불필요, LCP 점수 무의미
2. **전량 외부 동적 이미지** — 백엔드 API에서 받아오는 URL (정적 내부 이미지 0건)
3. **프린트/문서 레이아웃** — 10건 중 8건이 검사 기준서·도해 등 인쇄용. `next/image``width`/`height` 강제 지정이 프린트 레이아웃을 깰 위험
4. **blob URL 비호환** — 업로드 미리보기(blob:)는 `next/image`가 지원 안 함
5. **설정 부담 > 이점**`remotePatterns` 설정 + 백엔드 도메인 관리 비용이 실질 이점보다 큼
**사용처 (9개 파일)**:
| 파일 | 용도 | 이미지 소스 |
|------|------|-------------|
| `DocumentHeader.tsx` (2건) | 문서 헤더 로고 | `logo.imageUrl` (API) |
| `ProductInspectionInputModal.tsx` | 제품검사 사진 미리보기 | blob URL |
| `ProductInspectionDocument.tsx` | 제품검사 문서 | `data.productImage` (API) |
| `inspection-shared.tsx` | 검사 기준서 이미지 | `standardImage` (API) |
| `SlatInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) |
| `ScreenInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) |
| `BendingInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) |
| `SlatJointBarInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) |
| `BendingWipInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) |
**참고**: `next/image`가 유효한 케이스는 공개 사이트 + 정적/내부 이미지 + SEO 중요한 상황
### `next/dynamic` 코드 스플리팅 적용 (2026-02-10)
**결정**: 대형 컴포넌트 + 무거운 라이브러리에 `next/dynamic` / 동적 `import()` 적용
**핵심 개념 — Suspense vs dynamic()**:
- **`Suspense` + 정적 import** → 코드가 부모와 같은 번들 청크에 포함. 유저가 안 봐도 이미 다운로드됨. UI fallback만 제공하고 **코드 분할은 안 일어남**
- **`dynamic()`** → webpack이 별도 `.js` 청크로 분리. 컴포넌트가 실제 렌더될 때만 네트워크 요청으로 해당 청크 다운로드. **진짜 코드 분할**
**적용 내역**:
| 파일 | 대상 | 절감 |
|------|------|------|
| `reports/comprehensive-analysis/page.tsx` | MainDashboard (2,651줄 + recharts) | ~350KB |
| `components/business/Dashboard.tsx` | CEODashboard | ~200KB |
| `construction/ConstructionDashboard.tsx` | ConstructionMainDashboard | ~100KB |
| `production/dashboard/page.tsx` | ProductionDashboard | ~100KB |
| `lib/utils/excel-download.ts` | xlsx 라이브러리 (~400KB) | ~400KB |
| `quotes/LocationListPanel.tsx` | xlsx 직접 import 제거 | (위와 중복) |
**xlsx 동적 로드 패턴**:
```typescript
// Before: 모든 페이지에 xlsx ~400KB 포함
import * as XLSX from 'xlsx';
// After: 엑셀 버튼 클릭 시에만 로드
async function loadXLSX() {
return await import('xlsx');
}
export async function downloadExcel(...) {
const XLSX = await loadXLSX();
// ...
}
```
**총 절감**: 초기 번들에서 ~850KB 제외 (대시보드 미방문 + 엑셀 미사용 시)
---
## 폴더 구조
```