chore(WEB): 견적 액션 정리 및 아키텍처 문서 추가
- quotes/actions.ts 중복 코드 제거 및 간소화 - quotes/types.ts 타입 확장 - claudedocs/_index.md 업데이트 - 멀티테넌시 최적화 로드맵 문서 추가 - 리팩토링 로드맵 문서 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -179,10 +179,12 @@ claudedocs/
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `[FIX-2026-01-29] masterdata-cache-tenant-isolation.md` | 🔴 **NEW** - masterDataStore 캐시 테넌트 격리 수정 (page_config 키에 tenantId 추가, dead code 해소) |
|
||||
| `[PLAN-2026-02-06] refactoring-roadmap.md` | 🔴 **NEW** - 리팩토링 종합 로드맵 (5 Phase, 공통훅~성능최적화, **전부 프론트 단독**) |
|
||||
| `[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md` | 🔴 **NEW** - 멀티테넌시 공통화/최적화 종합 로드맵 (8 Phase, API 테넌트 주입~라우팅) |
|
||||
| `[FIX-2026-01-29] masterdata-cache-tenant-isolation.md` | masterDataStore 캐시 테넌트 격리 수정 (page_config 키에 tenantId 추가, dead code 해소) |
|
||||
| `[PLAN-2025-12-29] dynamic-menu-refresh.md` | 동적 메뉴 갱신 시스템 (1단계: 폴링, 2단계: SSE) |
|
||||
| `multi-tenancy-implementation.md` | 멀티테넌시 구현 |
|
||||
| `multi-tenancy-test-guide.md` | 멀티테넌시 테스트 |
|
||||
| `multi-tenancy-implementation.md` | ✅ **Phase 1-2 완료** - 초기 멀티테넌시 구현 (AuthContext, TenantAwareCache) |
|
||||
| `multi-tenancy-test-guide.md` | 멀티테넌시 캐시 격리 테스트 가이드 |
|
||||
| `architecture-integration-risks.md` | 통합 리스크 |
|
||||
| `browser-support-policy.md` | 브라우저 지원 정책 |
|
||||
| `ssr-hydration-fix.md` | SSR 하이드레이션 수정 |
|
||||
|
||||
@@ -0,0 +1,666 @@
|
||||
# 멀티테넌시 공통화 및 최적화 로드맵
|
||||
|
||||
**작성일**: 2026-02-06
|
||||
**목적**: 전체 프로젝트 멀티테넌시 준비 상태 점검 및 공통화/최적화 계획 수립
|
||||
**이전 문서**: `[REF-2025-11-19] multi-tenancy-implementation.md` (Phase 1-2 완료)
|
||||
|
||||
---
|
||||
|
||||
## 현재 상태 요약 (2026-02-06 기준)
|
||||
|
||||
### 완료된 항목 (이전 로드맵 Phase 1-2)
|
||||
|
||||
| 항목 | 상태 | 파일 |
|
||||
|------|------|------|
|
||||
| User 타입에 Tenant 객체 포함 | ✅ 완료 | `src/contexts/AuthContext.tsx` |
|
||||
| Tenant 인터페이스 정의 (id, company_name 등) | ✅ 완료 | `src/contexts/AuthContext.tsx` |
|
||||
| TenantAwareCache 유틸리티 | ✅ 완료 | `src/lib/cache/TenantAwareCache.ts` |
|
||||
| 테넌트 전환 감지 + 캐시 클리어 | ✅ 완료 | `src/contexts/AuthContext.tsx` |
|
||||
| masterDataStore 테넌트 스코프 캐시 키 | ✅ 완료 | `src/stores/masterDataStore.ts` |
|
||||
| sessionStorage/localStorage 테넌트 격리 | ✅ 완료 | `mes-{tenantId}-{key}` 패턴 |
|
||||
|
||||
### 미완료 / 개선 필요 항목
|
||||
|
||||
| 영역 | 우선순위 | 현재 상태 |
|
||||
|------|----------|-----------|
|
||||
| API 프록시에 테넌트 컨텍스트 전달 | 🔴 | X-Tenant-ID 헤더 없음 |
|
||||
| Server Actions 테넌트 인식 | 🔴 | 70+ actions.ts에 테넌트 미포함 |
|
||||
| 포매터/유틸리티 다국어/다통화 | 🔴 | 한국어 하드코딩 |
|
||||
| 브랜딩 동적화 (로고, 앱이름) | 🟡 | "SAM", sam-logo.png 하드코딩 |
|
||||
| 상수/공휴일 외부화 | 🟡 | 한국 공휴일 하드코딩 |
|
||||
| localStorage 직접 사용 잔재 | 🟡 | TenantAwareCache 미사용 곳 존재 |
|
||||
| tenantId 타입 불일치 | 🟡 | string vs number 혼재 |
|
||||
| 테넌트 라우팅 | 🟢 | 현재 없음 (필요 시 추가) |
|
||||
| TenantContext Provider | 🟢 | 테넌트 설정 전용 Context 없음 |
|
||||
|
||||
---
|
||||
|
||||
## 작업 영역 구분: 프론트 단독 vs 백엔드 협의
|
||||
|
||||
### 선행 확인 사항
|
||||
|
||||
> **핵심 질문**: "백엔드가 이미 JWT 토큰 안의 tenant_id로 데이터를 필터링하고 있는가?"
|
||||
>
|
||||
> - **Yes** → 프론트에서 별도 X-Tenant-ID 안 보내도 됨. Phase 1은 불필요하고 프론트 단독 Phase부터 진행
|
||||
> - **No** → 백엔드도 같이 수정 필요. Phase 1이 최우선
|
||||
|
||||
### 프론트 단독 가능 (백엔드 수정 불필요)
|
||||
|
||||
| Phase | 작업 | 이유 |
|
||||
|-------|------|------|
|
||||
| **3** | 포매터 다국어/다통화 전환 | `formatAmount()`, `formatDate()` 등 프론트 유틸리티 내부 수정. 기본값을 한국어로 유지하면 하위 호환 |
|
||||
| **6** | localStorage 잔재 정리 + tenantId 타입 통일 | 프론트 코드 정리. TenantAwareCache 미사용 곳 마이그레이션, `string` → `number` 통일 |
|
||||
| **8** | 테넌트 라우팅 (필요 시) | Next.js App Router 구조 변경. 순수 프론트 라우팅 |
|
||||
|
||||
> **즉시 착수 가능**: 백엔드 협의 결과를 기다리지 않고 바로 시작할 수 있음
|
||||
|
||||
### 백엔드 협의 필요 (프론트 + 백엔드 동시 수정)
|
||||
|
||||
| Phase | 작업 | 백엔드 필요 이유 |
|
||||
|-------|------|-----------------|
|
||||
| **1** | API 테넌트 컨텍스트 주입 | 프론트에서 `X-Tenant-ID` 헤더를 보내도, **백엔드가 읽고 필터링**해줘야 의미 있음. 안 읽으면 보내봤자 무용지물 |
|
||||
| **2** | Server Actions 마이그레이션 | Phase 1에 종속. 백엔드가 헤더 or URL로 테넌트를 구분 안 하면 프론트만 바꿔도 소용없음 |
|
||||
| **4** | 브랜딩 동적화 | 테넌트별 로고/앱이름을 **어디서 가져오나?** → 백엔드 API 필요 (`GET /api/v1/tenant/config`) |
|
||||
| **5** | 상수/공휴일 외부화 | 공휴일 데이터를 **DB에서 서빙**해야 함 → 백엔드 API 필요 (`GET /api/v1/holidays?year=2026`) |
|
||||
| **7** | TenantConfigService | 테넌트 설정 통합 API 필요 → branding + regional + features를 한 번에 가져오는 엔드포인트 |
|
||||
|
||||
### 추천 진행 순서
|
||||
|
||||
```
|
||||
[즉시 시작 - 프론트 단독]
|
||||
Phase 3 (포매터) + Phase 6 (localStorage/타입) 병렬 진행
|
||||
|
||||
[백엔드 협의 후 시작]
|
||||
Phase 1 (API 헤더) → Phase 2 (Actions)
|
||||
|
||||
[백엔드 API 준비 후 시작]
|
||||
Phase 7 (TenantConfigService) → Phase 4 (브랜딩) + Phase 5 (상수)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: API 레이어 테넌트 컨텍스트 주입 🔴 `백엔드 협의 필요`
|
||||
|
||||
> **목표**: 모든 백엔드 API 호출에 테넌트 식별 정보가 전달되도록 함
|
||||
> **예상**: 3-5일
|
||||
|
||||
### 1-1. 로그인 시 tenant_id 쿠키 추가
|
||||
|
||||
**파일**: `src/app/api/auth/login/route.ts`
|
||||
|
||||
**현재**: access_token, refresh_token 쿠키만 설정
|
||||
**변경**: tenant_id 쿠키 추가 (HttpOnly, API 프록시에서 읽기용)
|
||||
|
||||
```typescript
|
||||
// 로그인 성공 후 추가
|
||||
const tenantCookie = [
|
||||
`tenant_id=${data.tenant.id}`,
|
||||
'HttpOnly',
|
||||
...(isProduction ? ['Secure'] : []),
|
||||
'SameSite=Lax',
|
||||
'Path=/',
|
||||
`Max-Age=${data.expires_in || 7200}`,
|
||||
].join('; ');
|
||||
response.headers.append('Set-Cookie', tenantCookie);
|
||||
```
|
||||
|
||||
### 1-2. API 프록시에 X-Tenant-ID 헤더 추가
|
||||
|
||||
**파일**: `src/app/api/proxy/[...path]/route.ts`
|
||||
|
||||
**현재**:
|
||||
```typescript
|
||||
const headers = {
|
||||
'Accept': 'application/json',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
};
|
||||
```
|
||||
|
||||
**변경**:
|
||||
```typescript
|
||||
const tenantId = request.cookies.get('tenant_id')?.value;
|
||||
const headers = {
|
||||
'Accept': 'application/json',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
...(tenantId && { 'X-Tenant-ID': tenantId }),
|
||||
};
|
||||
```
|
||||
|
||||
### 1-3. serverFetch 래퍼에 테넌트 헤더 추가
|
||||
|
||||
**파일**: `src/lib/api/fetch-wrapper.ts`
|
||||
|
||||
**현재**: Authorization 헤더만 전달
|
||||
**변경**: tenant_id 쿠키 읽어서 X-Tenant-ID 헤더 자동 추가
|
||||
|
||||
```typescript
|
||||
export async function serverFetch(url: string, options?: RequestInit) {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
const tenantId = cookieStore.get('tenant_id')?.value;
|
||||
|
||||
const headers = {
|
||||
...options?.headers,
|
||||
'Authorization': `Bearer ${token}`,
|
||||
...(tenantId && { 'X-Tenant-ID': tenantId }),
|
||||
};
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 1-4. ApiClient 클래스에 테넌트 지원
|
||||
|
||||
**파일**: `src/lib/api/client.ts`
|
||||
|
||||
**변경**: `getAuthHeaders()`에 X-Tenant-ID 포함
|
||||
|
||||
### 체크리스트
|
||||
|
||||
```
|
||||
- [ ] login/route.ts에 tenant_id 쿠키 Set-Cookie 추가
|
||||
- [ ] proxy/[...path]/route.ts에서 tenant_id 쿠키 읽기 + X-Tenant-ID 헤더 전달
|
||||
- [ ] fetch-wrapper.ts serverFetch에 X-Tenant-ID 자동 추가
|
||||
- [ ] client.ts ApiClient에 tenantId 옵션 추가
|
||||
- [ ] authenticated-fetch.ts에도 테넌트 헤더 전파 확인
|
||||
- [ ] 로그아웃 시 tenant_id 쿠키 삭제 확인
|
||||
- [ ] 토큰 갱신 시 tenant_id 쿠키 유지 확인
|
||||
- [ ] 백엔드와 X-Tenant-ID 헤더 수신 방식 협의
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Server Actions 점진적 마이그레이션 🔴 `백엔드 협의 필요`
|
||||
|
||||
> **목표**: 70+ actions.ts에서 테넌트 컨텍스트가 자동 전달되도록 함
|
||||
> **예상**: 1-2주 (Phase 1 완료 후 자동 적용되는 부분 다수)
|
||||
|
||||
### 2-1. 현재 패턴 분석
|
||||
|
||||
대부분의 actions.ts가 이 패턴을 따름:
|
||||
```typescript
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/endpoint`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
```
|
||||
|
||||
### 2-2. 자동 적용 범위 (Phase 1 완료 시)
|
||||
|
||||
Phase 1에서 `serverFetch`에 X-Tenant-ID를 자동 추가하면, **기존 actions.ts 대부분은 수정 없이** 테넌트 헤더가 전달됨.
|
||||
|
||||
### 2-3. 수동 확인 필요 케이스
|
||||
|
||||
`serverFetch`를 사용하지 않고 직접 `fetch()`를 호출하는 곳:
|
||||
```bash
|
||||
# 검색 대상
|
||||
grep -r "fetch(" src/components/*/actions.ts --include="*.ts" | grep -v serverFetch
|
||||
```
|
||||
|
||||
### 2-4. 선택적 URL 테넌트 프리픽스
|
||||
|
||||
백엔드가 URL 경로에 테넌트를 요구하는 경우만:
|
||||
```typescript
|
||||
// 필요한 경우에만 적용
|
||||
function buildTenantUrl(endpoint: string, tenantId?: string): string {
|
||||
if (endpoint.startsWith('http')) return endpoint; // 레거시 호환
|
||||
const base = process.env.NEXT_PUBLIC_API_URL;
|
||||
return tenantId
|
||||
? `${base}/api/v1/tenant/${tenantId}/${endpoint}`
|
||||
: `${base}/api/v1/${endpoint}`;
|
||||
}
|
||||
```
|
||||
|
||||
### 체크리스트
|
||||
|
||||
```
|
||||
- [ ] serverFetch 사용하지 않는 actions.ts 목록 확인
|
||||
- [ ] 직접 fetch() 호출하는 곳 serverFetch로 마이그레이션
|
||||
- [ ] 백엔드와 URL 패턴 vs 헤더 패턴 최종 협의
|
||||
- [ ] 고빈도 도메인 우선 검증: clients, items, production, sales
|
||||
- [ ] 에러 시 테넌트 컨텍스트 누락 로그 추가
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 포매터 & 유틸리티 테넌트 설정 기반 전환 🔴 `프론트 단독 가능`
|
||||
|
||||
> **목표**: 한국어 하드코딩된 포매터를 테넌트 설정 기반으로 변경
|
||||
> **예상**: 3-5일
|
||||
|
||||
### 3-1. 영향받는 파일 목록
|
||||
|
||||
| 파일 | 함수 | 하드코딩 내용 |
|
||||
|------|------|---------------|
|
||||
| `src/utils/formatAmount.ts` | `formatAmount()` | "원", "만원" |
|
||||
| `src/utils/formatAmount.ts` | `formatKoreanAmount()` | "억", "만" |
|
||||
| `src/lib/formatters.ts` | `formatBusinessNumber()` | 한국 사업자번호 (XXX-XX-XXXXX) |
|
||||
| `src/lib/formatters.ts` | `formatPhoneNumber()` | 한국 전화 (02-, 010-) |
|
||||
| `src/utils/date.ts` | `formatDate()` | `'ko-KR'` 로케일 |
|
||||
|
||||
### 3-2. TenantRegionalConfig 인터페이스
|
||||
|
||||
```typescript
|
||||
// src/types/tenant-config.ts (신규)
|
||||
export interface TenantRegionalConfig {
|
||||
locale: string; // 'ko-KR' | 'en-US' | 'ja-JP'
|
||||
timezone: string; // 'Asia/Seoul' | 'America/New_York'
|
||||
currency: {
|
||||
code: string; // 'KRW' | 'USD' | 'JPY'
|
||||
symbol: string; // '원' | '$' | '¥'
|
||||
locale: string; // Intl.NumberFormat 로케일
|
||||
largeUnitName?: string; // '만' (한국 전용)
|
||||
largeUnitValue?: number; // 10000
|
||||
};
|
||||
phone: {
|
||||
countryCode: string; // '+82' | '+1' | '+81'
|
||||
format: string; // 'XXX-XXXX-XXXX'
|
||||
};
|
||||
businessNumber: {
|
||||
format: string; // 'XXX-XX-XXXXX'
|
||||
label: string; // '사업자번호' | 'Business No.' | '法人番号'
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3-3. 마이그레이션 접근 (하위 호환)
|
||||
|
||||
기존 함수를 바로 변경하지 않고, 오버로드 + 기본값 패턴 적용:
|
||||
|
||||
```typescript
|
||||
// 기존 호출 코드를 깨지 않는 방식
|
||||
export function formatAmount(amount: number, config?: TenantCurrencyConfig): string {
|
||||
const cfg = config ?? DEFAULT_KR_CURRENCY_CONFIG; // 기본값: 한국
|
||||
// ... 테넌트 설정 기반 포매팅
|
||||
}
|
||||
```
|
||||
|
||||
### 3-4. 기존 공통화 작업 참조
|
||||
|
||||
**이미 작성된 관련 문서**:
|
||||
- `claudedocs/[IMPL-2026-02-05] formatter-commonization-plan.md`
|
||||
- `claudedocs/[ANALYSIS-2026-01-20] 공통화-현황-분석.md`
|
||||
|
||||
이 문서들의 포매터 공통화 계획과 병합하여 진행.
|
||||
|
||||
### 체크리스트
|
||||
|
||||
```
|
||||
- [ ] TenantRegionalConfig 인터페이스 정의
|
||||
- [ ] DEFAULT_KR_CONFIG 기본값 생성 (하위 호환)
|
||||
- [ ] formatAmount() 테넌트 설정 지원 추가
|
||||
- [ ] formatDate() 테넌트 로케일 지원 추가
|
||||
- [ ] formatBusinessNumber() 포맷 설정 지원 추가
|
||||
- [ ] formatPhoneNumber() 국가 코드 지원 추가
|
||||
- [ ] 기존 호출 코드 깨지지 않는지 검증
|
||||
- [ ] formatter-commonization-plan.md와 통합
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 브랜딩 동적화 🟡 `백엔드 API 필요`
|
||||
|
||||
> **목표**: 하드코딩된 회사명/로고를 테넌트 설정 기반으로 변경
|
||||
> **예상**: 2-3일
|
||||
|
||||
### 4-1. 영향받는 파일 목록
|
||||
|
||||
| 파일 | 하드코딩 | 변경 방향 |
|
||||
|------|----------|-----------|
|
||||
| `src/layouts/AuthenticatedLayout.tsx` | `APP_NAME = 'SAM'` | `tenant.company_name` 또는 테넌트 설정 |
|
||||
| `src/layouts/AuthenticatedLayout.tsx` | `<Image src="/sam-logo.png">` | 테넌트별 로고 URL |
|
||||
| `src/layouts/AuthenticatedLayout.tsx` | `MOCK_COMPANIES` 배열 | `user.tenant.other_tenants` 연동 |
|
||||
| `src/app/[locale]/layout.tsx` | `APP_TITLE = 'SAM - 내 손안의 대시보드'` | 테넌트 설정 기반 |
|
||||
| `src/app/[locale]/layout.tsx` | SEO 메타데이터 | 테넌트별 (단, 폐쇄형이므로 낮은 우선순위) |
|
||||
|
||||
### 4-2. 테넌트 브랜딩 설정 구조
|
||||
|
||||
```typescript
|
||||
// src/types/tenant-config.ts에 추가
|
||||
export interface TenantBrandingConfig {
|
||||
appName: string; // 'SAM' | '주일 MES' | 커스텀
|
||||
appSubtitle?: string; // 'Smart Automation Management'
|
||||
logoUrl: string; // '/sam-logo.png' | '/tenants/282/logo.png'
|
||||
faviconUrl?: string;
|
||||
primaryColor?: string; // 테마 주색상
|
||||
loginBackground?: string; // 로그인 페이지 배경
|
||||
}
|
||||
```
|
||||
|
||||
### 4-3. 적용 방식
|
||||
|
||||
```typescript
|
||||
// AuthenticatedLayout.tsx 내부
|
||||
const { currentUser } = useAuth();
|
||||
const branding = currentUser?.tenant?.branding ?? DEFAULT_BRANDING;
|
||||
|
||||
// 로고
|
||||
<Image src={branding.logoUrl} alt={branding.appName} />
|
||||
|
||||
// 앱 이름
|
||||
<h1>{branding.appName}</h1>
|
||||
```
|
||||
|
||||
### 4-4. MOCK_COMPANIES → other_tenants 연동
|
||||
|
||||
**현재**: 하드코딩 목업
|
||||
```typescript
|
||||
const MOCK_COMPANIES = [
|
||||
{ id: 'all', name: '전체' },
|
||||
{ id: 'company1', name: '(주)삼성건설' },
|
||||
...
|
||||
];
|
||||
```
|
||||
|
||||
**변경**: 실제 테넌트 데이터 연동
|
||||
```typescript
|
||||
const tenantOptions = useMemo(() => {
|
||||
const current = currentUser?.tenant;
|
||||
const others = current?.other_tenants ?? [];
|
||||
return [current, ...others].filter(Boolean);
|
||||
}, [currentUser]);
|
||||
```
|
||||
|
||||
### 체크리스트
|
||||
|
||||
```
|
||||
- [ ] TenantBrandingConfig 인터페이스 정의
|
||||
- [ ] DEFAULT_BRANDING 기본값 (현재 SAM 설정)
|
||||
- [ ] AuthenticatedLayout 로고/앱이름 동적화
|
||||
- [ ] MOCK_COMPANIES를 other_tenants 기반으로 교체
|
||||
- [ ] 로그인 페이지 브랜딩 동적화
|
||||
- [ ] favicon 동적 변경 (선택)
|
||||
- [ ] 테넌트별 로고 파일 서빙 방식 결정 (public/ vs API)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: 상수 & 비즈니스 로직 외부화 🟡 `백엔드 API 필요`
|
||||
|
||||
> **목표**: 한국 특화 상수를 테넌트/국가별 설정으로 외부화
|
||||
> **예상**: 3-5일
|
||||
|
||||
### 5-1. 영향받는 항목
|
||||
|
||||
| 항목 | 파일 | 현재 | 변경 |
|
||||
|------|------|------|------|
|
||||
| 공휴일 | `src/constants/calendarEvents.ts` | 한국 공휴일 하드코딩 | DB/API 기반 |
|
||||
| 프로세스 타입 | `src/types/process.ts` | "생산", "검사" 등 | i18n 라벨 |
|
||||
| 상태 라벨 | `src/lib/utils/status-config.ts` | "대기", "완료" 등 | i18n 라벨 |
|
||||
| 품목 타입 | `src/types/item.ts` | "제품", "부품" 등 | i18n 라벨 |
|
||||
| 근무일 | 관련 컴포넌트 | 월-금 하드코딩 | 테넌트 설정 |
|
||||
|
||||
### 5-2. 외부화 전략
|
||||
|
||||
**공휴일**: 백엔드 API로 이동 (테넌트별 국가 설정에 따라 반환)
|
||||
```typescript
|
||||
// AS-IS: 하드코딩
|
||||
const HOLIDAYS_2026 = [
|
||||
{ date: '2026-01-01', name: '신정', type: 'holiday' },
|
||||
...
|
||||
];
|
||||
|
||||
// TO-BE: API 호출
|
||||
const holidays = await getHolidays(tenantId, year);
|
||||
```
|
||||
|
||||
**라벨/상태**: next-intl 다국어 시스템 활용 (이미 ko/en/ja 구조 있음)
|
||||
```typescript
|
||||
// AS-IS
|
||||
const statusLabels = { pending: '대기', completed: '완료' };
|
||||
|
||||
// TO-BE
|
||||
const t = useTranslations('status');
|
||||
const label = t('pending'); // 로케일에 따라 자동 변환
|
||||
```
|
||||
|
||||
### 체크리스트
|
||||
|
||||
```
|
||||
- [ ] calendarEvents.ts 공휴일 데이터 → API 엔드포인트로 이동
|
||||
- [ ] 프로세스 타입 라벨 → messages/ko.json, en.json, ja.json으로 이동
|
||||
- [ ] 상태 라벨 → i18n 키로 변환
|
||||
- [ ] 품목 타입 라벨 → i18n 키로 변환
|
||||
- [ ] 근무일 설정 → 테넌트 config로 이동
|
||||
- [ ] 백엔드에 공휴일 API 요청
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: localStorage 잔재 정리 & 타입 통일 🟡 `프론트 단독 가능`
|
||||
|
||||
> **목표**: TenantAwareCache 미사용 곳 정리 + tenantId 타입 통일
|
||||
> **예상**: 2-3일
|
||||
|
||||
### 6-1. localStorage 직접 사용 감사
|
||||
|
||||
```bash
|
||||
# 검색 대상
|
||||
grep -r "localStorage\.\(setItem\|getItem\)" src/ --include="*.ts" --include="*.tsx"
|
||||
```
|
||||
|
||||
**알려진 비-테넌트-스코프 키**:
|
||||
- `mes-users` → 사용자 목록 (테넌트 스코프 필요 여부 검토)
|
||||
- `mes-currentUser` → 현재 사용자 (로그인 상태이므로 테넌트 무관)
|
||||
- 기타 직접 사용 곳 → TenantAwareCache 또는 테넌트 프리픽스 적용
|
||||
|
||||
### 6-2. tenantId 타입 통일
|
||||
|
||||
**현재 상황**:
|
||||
- `User.tenant.id` → `number` (AuthContext)
|
||||
- `PageConfig.tenantId` → `string` (masterDataStore)
|
||||
- TenantAwareCache → `number`
|
||||
|
||||
**통일**: `number`로 표준화 (백엔드 응답 기준)
|
||||
|
||||
```typescript
|
||||
// 수정 대상 찾기
|
||||
grep -r "tenantId.*string" src/ --include="*.ts"
|
||||
```
|
||||
|
||||
### 체크리스트
|
||||
|
||||
```
|
||||
- [ ] localStorage 직접 사용 전수 조사
|
||||
- [ ] TenantAwareCache로 마이그레이션 가능한 곳 목록화
|
||||
- [ ] 테넌트 스코프 불필요한 곳 명시 (mes-currentUser 등)
|
||||
- [ ] tenantId: string → number 통일
|
||||
- [ ] PageConfig 타입 수정
|
||||
- [ ] 관련 타입 참조 전부 업데이트
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: TenantConfigService & TenantContext (선택) 🟢 `백엔드 API 필요`
|
||||
|
||||
> **목표**: 테넌트 설정을 한곳에서 관리하는 서비스 레이어
|
||||
> **예상**: 3-5일 (Phase 3-5 진행 중 필요에 따라)
|
||||
|
||||
### 7-1. TenantConfigService
|
||||
|
||||
```typescript
|
||||
// src/services/TenantConfigService.ts (신규)
|
||||
export interface TenantConfiguration {
|
||||
tenantId: number;
|
||||
branding: TenantBrandingConfig;
|
||||
regional: TenantRegionalConfig;
|
||||
features: {
|
||||
enabledModules: string[];
|
||||
customFields?: Record<string, unknown>;
|
||||
};
|
||||
calendar: {
|
||||
workingDays: number[]; // [1,2,3,4,5] = 월-금
|
||||
holidays: HolidayEntry[];
|
||||
};
|
||||
}
|
||||
|
||||
class TenantConfigService {
|
||||
private cache: Map<number, TenantConfiguration> = new Map();
|
||||
|
||||
async getConfig(tenantId: number): Promise<TenantConfiguration> {
|
||||
if (this.cache.has(tenantId)) return this.cache.get(tenantId)!;
|
||||
const config = await this.fetchFromApi(tenantId);
|
||||
this.cache.set(tenantId, config);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7-2. TenantContext Provider
|
||||
|
||||
```typescript
|
||||
// src/contexts/TenantContext.tsx (신규)
|
||||
export function TenantProvider({ children }: { children: ReactNode }) {
|
||||
const { currentUser } = useAuth();
|
||||
const [config, setConfig] = useState<TenantConfiguration>();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser?.tenant?.id) {
|
||||
tenantConfigService.getConfig(currentUser.tenant.id)
|
||||
.then(setConfig);
|
||||
}
|
||||
}, [currentUser?.tenant?.id]);
|
||||
|
||||
return (
|
||||
<TenantContext.Provider value={config}>
|
||||
{children}
|
||||
</TenantContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// 사용
|
||||
const tenantConfig = useTenantConfig();
|
||||
const currencySymbol = tenantConfig.regional.currency.symbol;
|
||||
```
|
||||
|
||||
### 체크리스트
|
||||
|
||||
```
|
||||
- [ ] TenantConfiguration 통합 인터페이스 설계
|
||||
- [ ] TenantConfigService 구현 (캐시 + API 호출)
|
||||
- [ ] TenantContext Provider 구현
|
||||
- [ ] useTenantConfig() 훅 구현
|
||||
- [ ] Protected Layout에 TenantProvider 추가
|
||||
- [ ] 기존 코드에서 점진적 마이그레이션
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: 테넌트 라우팅 (필요 시) 🟢 `프론트 단독 가능`
|
||||
|
||||
> **목표**: URL에 테넌트 식별자 포함 (필요한 경우에만)
|
||||
> **예상**: 1주+
|
||||
|
||||
### 현재 라우팅
|
||||
```
|
||||
/[locale]/(protected)/dashboard
|
||||
```
|
||||
|
||||
### 옵션 A: 경로 기반 (권장 - 필요 시)
|
||||
```
|
||||
/[tenant]/[locale]/(protected)/dashboard
|
||||
/acme/ko/dashboard
|
||||
```
|
||||
|
||||
### 옵션 B: 서브도메인 기반
|
||||
```
|
||||
acme.sam.com/ko/dashboard
|
||||
```
|
||||
|
||||
### 옵션 C: 현재 유지 (인증 기반만)
|
||||
```
|
||||
/[locale]/(protected)/dashboard ← 테넌트는 JWT/쿠키로만 식별
|
||||
```
|
||||
|
||||
**결정**: 현재는 **옵션 C 유지**. 다중 테넌트 URL 분리가 필요해지면 옵션 A 도입.
|
||||
|
||||
---
|
||||
|
||||
## 백엔드 협의 사항
|
||||
|
||||
### 필수 협의 (Phase 1 시작 전)
|
||||
|
||||
| 항목 | 질문 | 결정 사항 |
|
||||
|------|------|-----------|
|
||||
| 테넌트 식별 방식 | `X-Tenant-ID` 헤더 vs URL 경로 vs JWT만? | TBD |
|
||||
| X-Tenant-ID 수신 | 백엔드가 이 헤더를 읽고 필터링하는지? | TBD |
|
||||
| JWT 내 tenant_id | 토큰에 tenant_id가 포함되어 있는지? | TBD |
|
||||
| 공휴일 API | `GET /api/v1/holidays?year=2026` 지원? | TBD |
|
||||
| 테넌트 설정 API | `GET /api/v1/tenant/config` 지원? | TBD |
|
||||
|
||||
### 선택 협의 (Phase 4-5 시작 전)
|
||||
|
||||
| 항목 | 질문 | 결정 사항 |
|
||||
|------|------|-----------|
|
||||
| 테넌트 로고 | 로고 URL을 어디서 제공? (API vs 파일서버) | TBD |
|
||||
| 브랜딩 설정 | 테넌트별 앱이름/테마 API 제공 가능? | TBD |
|
||||
| 다국어 라벨 | 백엔드 코드 라벨이 다국어 지원? | TBD |
|
||||
|
||||
---
|
||||
|
||||
## 실행 우선순위 요약
|
||||
|
||||
```
|
||||
[프론트 단독] Phase 3: 포매터 테넌트 설정 기반 🔴 3-5일 ← 즉시 시작 가능
|
||||
[프론트 단독] Phase 6: localStorage 정리/타입 통일 🟡 2-3일 ← 즉시 시작 가능
|
||||
[프론트 단독] Phase 8: 테넌트 라우팅 🟢 필요시 ← 당분간 불필요
|
||||
|
||||
[백엔드 협의] Phase 1: API 테넌트 컨텍스트 주입 🔴 3-5일 ← 백엔드 확인 후
|
||||
[백엔드 협의] Phase 2: Server Actions 마이그레이션 🔴 1-2주 ← Phase 1 후 자동 적용 범위 큼
|
||||
|
||||
[백엔드 API] Phase 4: 브랜딩 동적화 🟡 2-3일 ← 테넌트 설정 API 필요
|
||||
[백엔드 API] Phase 5: 상수/공휴일 외부화 🟡 3-5일 ← 공휴일 API 필요
|
||||
[백엔드 API] Phase 7: TenantConfigService 🟢 3-5일 ← 통합 설정 API 필요
|
||||
```
|
||||
|
||||
### 병렬 진행 가능 조합
|
||||
|
||||
```
|
||||
[즉시 시작 - 프론트 단독]
|
||||
├─ Phase 3 (포매터) ─────────→ 독립 완료
|
||||
└─ Phase 6 (localStorage) ──→ 독립 완료
|
||||
|
||||
[백엔드 협의 후 - 프론트+백엔드]
|
||||
└─ Phase 1 (API 헤더) ──────→ Phase 2 (Actions 자동 적용)
|
||||
|
||||
[백엔드 API 준비 후 - 프론트+백엔드]
|
||||
└─ Phase 7 (TenantConfig) ──→ Phase 4 (브랜딩) + Phase 5 (상수)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 위험 요소 & 대응
|
||||
|
||||
| 위험 | 확률 | 영향 | 대응 |
|
||||
|------|------|------|------|
|
||||
| 70+ actions.ts 수동 마이그레이션 | 높음 | 중간 | serverFetch 자동 주입으로 대부분 해결 |
|
||||
| 백엔드 X-Tenant-ID 미지원 | 중간 | 높음 | Phase 1 시작 전 백엔드 팀 협의 필수 |
|
||||
| 포매터 변경 시 기존 UI 깨짐 | 낮음 | 중간 | 기본값 패턴으로 하위 호환 유지 |
|
||||
| 캐시 무효화 누락 | 낮음 | 높음 | TenantAwareCache 이미 검증됨 |
|
||||
| 다국어 번역 리소스 부족 | 중간 | 낮음 | 한국어 기본값 유지, 점진 추가 |
|
||||
|
||||
---
|
||||
|
||||
## 관련 문서
|
||||
|
||||
| 문서 | 설명 |
|
||||
|------|------|
|
||||
| `architecture/[REF-2025-11-19] multi-tenancy-implementation.md` | 이전 멀티테넌시 구현 (Phase 1-2 → 완료됨) |
|
||||
| `architecture/[TEST-2025-11-19] multi-tenancy-test-guide.md` | 캐시 격리 테스트 가이드 |
|
||||
| `architecture/[FIX-2026-01-29] masterdata-cache-tenant-isolation.md` | masterDataStore 캐시 테넌트 격리 수정 |
|
||||
| `[IMPL-2026-02-05] formatter-commonization-plan.md` | 포매터 공통화 계획 (Phase 3과 병합) |
|
||||
| `[ANALYSIS-2026-01-20] 공통화-현황-분석.md` | 공통화 현황 분석 |
|
||||
| `[ANALYSIS-2026-02-05] list-page-commonization-status.md` | 리스트 페이지 공통화 현황 |
|
||||
| `auth/[IMPL-2025-11-07] jwt-cookie-authentication-final.md` | JWT 쿠키 인증 구현 |
|
||||
| `api/[REF] api-requirements.md` | API 요구사항 |
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
|
||||
| 날짜 | 변경 내용 |
|
||||
|------|-----------|
|
||||
| 2026-02-06 | 초기 작성 - 전체 코드베이스 분석 기반 8 Phase 로드맵 |
|
||||
|
||||
---
|
||||
|
||||
**다음 액션**: Phase 1 시작 전 백엔드 팀과 `X-Tenant-ID` 헤더 수신 방식 협의
|
||||
389
claudedocs/architecture/[PLAN-2026-02-06] refactoring-roadmap.md
Normal file
389
claudedocs/architecture/[PLAN-2026-02-06] refactoring-roadmap.md
Normal file
@@ -0,0 +1,389 @@
|
||||
# 리팩토링 로드맵
|
||||
|
||||
**작성일**: 2026-02-06
|
||||
**목적**: 전체 코드베이스 리팩토링 포인트 점검 및 실행 계획
|
||||
**상태**: 분석 완료, 실행 대기
|
||||
|
||||
---
|
||||
|
||||
## 현재 코드베이스 수치 (2026-02-06 기준)
|
||||
|
||||
| 지표 | 수치 | 비고 |
|
||||
|------|------|------|
|
||||
| 전체 코드 | ~301,000줄 | TS/TSX |
|
||||
| 컴포넌트 파일 | ~551개 | |
|
||||
| 페이지 파일 | ~253개 | |
|
||||
| action.ts 파일 | 80개 | 거의 동일 CRUD 패턴 |
|
||||
| types.ts 파일 | 94개 | 중복 타입 다수 |
|
||||
| 모달 컴포넌트 | 42개 | 유사 패턴 반복 |
|
||||
| 2000줄+ 파일 | 4개 | God 컴포넌트 |
|
||||
| 1000~2000줄 파일 | 25+개 | 분리 대상 |
|
||||
| 500~1000줄 파일 | 50+개 | 검토 대상 |
|
||||
|
||||
---
|
||||
|
||||
## God 컴포넌트 / 대형 파일 목록
|
||||
|
||||
### 🔴 2000줄 이상 (즉시 분리 필요)
|
||||
|
||||
| 파일 | 줄수 | 핵심 문제 | 분리 방향 |
|
||||
|------|------|-----------|-----------|
|
||||
| `components/business/MainDashboard.tsx` | 2,651 | CEO/영업/생산/품질 대시보드 한 파일 | 역할별 섹션 컴포넌트 분리 |
|
||||
| `contexts/ItemMasterContext.tsx` | 2,406 | useState 17개, useEffect 15개, 함수 50+개 | 도메인별 5개 Context 분리 |
|
||||
| `lib/api/item-master.ts` | 2,232 | 모든 품목 API 한 파일 | 도메인별 API 모듈 분리 |
|
||||
| `lib/api/dashboard/transformers.ts` | 1,576 | 전체 대시보드 변환 로직 | 섹션별 transformer 분리 |
|
||||
|
||||
### 🟡 1000~2000줄 (우선 검토)
|
||||
|
||||
| 파일 | 줄수 | 도메인 | 분리 방향 |
|
||||
|------|------|--------|-----------|
|
||||
| `components/orders/actions.ts` | 1,394 | 수주 | 서비스 레이어 분리 |
|
||||
| `components/accounting/ExpectedExpenseManagement/index.tsx` | 1,299 | 회계 | 서브 컴포넌트 추출 |
|
||||
| `layouts/AuthenticatedLayout.tsx` | 1,289 | 레이아웃 | 훅 24개 → 섹션별 분리 |
|
||||
| `components/quotes/QuoteRegistration.tsx` | 1,268 | 견적 | 폼 섹션 추출, useState 13개 |
|
||||
| `components/quotes/actions.ts` | 1,266 | 견적 | API 레이어 분리 |
|
||||
| `components/business/construction/management/actions.ts` | 1,222 | 건설 | 도메인 서비스 추출 |
|
||||
| `components/business/construction/estimates/actions.ts` | 1,222 | 건설 | 도메인 서비스 추출 |
|
||||
| `components/production/WorkerScreen/index.tsx` | 1,198 | 생산 | 화면 섹션 분리 |
|
||||
| `hooks/useCEODashboard.ts` | 1,172 | 대시보드 | useState 18개 → 섹션별 훅 분리 |
|
||||
| `components/material/ReceivingManagement/actions.ts` | 1,152 | 자재 | API 서비스 레이어 |
|
||||
| `components/quotes/types.ts` | 1,149 | 견적 | 타입 조직화 |
|
||||
| `components/quality/InspectionManagement/InspectionDetail.tsx` | 1,125 | 품질 | 컴포넌트 추출 |
|
||||
| `components/hr/VacationManagement/actions.ts` | 1,125 | HR | 서비스 레이어 분리 |
|
||||
| `components/orders/OrderRegistration.tsx` | 1,123 | 수주 | 폼 섹션 추출, useState 12개 |
|
||||
| `components/items/DynamicItemForm/index.tsx` | 1,073 | 품목 | 복합 폼 로직 추출 |
|
||||
| `components/templates/IntegratedListTemplateV2.tsx` | 1,066 | 템플릿 | 템플릿 특화 |
|
||||
| `components/hr/EmployeeManagement/EmployeeForm.tsx` | 1,051 | HR | 폼 섹션 분리 |
|
||||
| `components/quotes/QuoteRegistrationV2.tsx` | 1,020 | 견적 | 폼 리팩토링 |
|
||||
| `components/templates/UniversalListPage/index.tsx` | 1,007 | 템플릿 | 템플릿 최적화 |
|
||||
| `components/items/ItemMasterDataManagement.tsx` | 1,005 | 품목 | 도메인 로직 추출 |
|
||||
|
||||
---
|
||||
|
||||
## 중복 패턴 분석
|
||||
|
||||
### 1. 액션 파일 80개 동일 패턴 (~24,000줄 중복)
|
||||
|
||||
**현재**: 모든 도메인이 이 구조를 복붙
|
||||
```typescript
|
||||
'use server';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
|
||||
interface Api[Domain]Data { ... } // 타입 정의 100~300줄
|
||||
function transform(data) { ... } // API→프론트 변환 50~100줄
|
||||
|
||||
export async function getList(params) { // 목록 조회
|
||||
const url = `${API_URL}/api/v1/endpoint`;
|
||||
const { response } = await serverFetch(url, { method: 'GET' });
|
||||
return transform(response);
|
||||
}
|
||||
export async function getById(id) { ... } // 상세 조회
|
||||
export async function create(data) { ... } // 생성
|
||||
export async function update(id, data) { ... } // 수정
|
||||
export async function delete(id) { ... } // 삭제
|
||||
export async function bulkDelete(ids) { ... } // 일괄 삭제
|
||||
```
|
||||
|
||||
**해당 도메인**: orders, quotes, clients, accounting(13모듈), hr(6모듈), production(4모듈), material(2모듈), quality(2모듈), construction(17모듈), settings(14모듈)
|
||||
|
||||
**해결 방향**: 제네릭 API 서비스 팩토리
|
||||
```typescript
|
||||
// lib/api/createCrudService.ts
|
||||
function createCrudService<TApi, TFront>(config: {
|
||||
endpoint: string;
|
||||
transform: (api: TApi) => TFront;
|
||||
reverseTransform: (front: TFront) => Partial<TApi>;
|
||||
}) {
|
||||
return {
|
||||
getList: async (params) => { ... },
|
||||
getById: async (id) => { ... },
|
||||
create: async (data) => { ... },
|
||||
update: async (id, data) => { ... },
|
||||
delete: async (id) => { ... },
|
||||
bulkDelete: async (ids) => { ... },
|
||||
};
|
||||
}
|
||||
|
||||
// 사용: 10줄로 끝
|
||||
const orderService = createCrudService<ApiOrder, Order>({
|
||||
endpoint: 'orders',
|
||||
transform: transformOrder,
|
||||
reverseTransform: reverseTransformOrder,
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 데이터 페칭 패턴 3가지 혼재
|
||||
|
||||
| 패턴 | 사용 비율 | 위치 |
|
||||
|------|-----------|------|
|
||||
| useEffect + .then() 직접 호출 | ~75% (99+ 컴포넌트) | 대부분의 도메인 |
|
||||
| 커스텀 훅 (useDetailData 등) | ~15% (~15 컴포넌트) | 신규 구현 |
|
||||
| ApiClient 클래스 | ~10% (15 컴포넌트) | 건설 도메인만 |
|
||||
|
||||
**수동 로딩 상태 관리**: 262곳에서 반복
|
||||
```typescript
|
||||
// 이 패턴이 262번 반복됨
|
||||
const [data, setData] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
fetchData()
|
||||
.then(result => setData(result))
|
||||
.catch(err => setError(err))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
```
|
||||
|
||||
### 3. 폼 검증 3가지 방식 혼재
|
||||
|
||||
| 방식 | 사용 파일 수 | 비율 |
|
||||
|------|-------------|------|
|
||||
| Zod 스키마 (정석) | 3개 (로그인, 회원가입, 품목) | 5% |
|
||||
| 수동 if문 검증 | 50+개 | 60% |
|
||||
| 검증 없음 | ~30개 | 35% |
|
||||
|
||||
### 4. 리스트 페이지 템플릿 이중화
|
||||
|
||||
| 방식 | 사용 | 비율 |
|
||||
|------|------|------|
|
||||
| `UniversalListPage` (신규 표준) | 20개 페이지 | 25% |
|
||||
| 수동 구현 (레거시) | 60+ 페이지 | 75% |
|
||||
|
||||
### 5. 모달/다이얼로그 42개 유사 패턴
|
||||
|
||||
**검색/선택 모달 5개+ 거의 동일**:
|
||||
- `quotes/ItemSearchModal.tsx`
|
||||
- `production/WorkOrders/AssigneeSelectModal.tsx`
|
||||
- `material/ReceivingManagement/SupplierSearchModal.tsx`
|
||||
- `quality/InspectionManagement/OrderSelectModal.tsx`
|
||||
- `production/WorkOrders/SalesOrderSelectModal.tsx`
|
||||
|
||||
전부 "검색 입력 → API 호출 → 목록 표시 → 체크박스 선택 → 확인" 동일 구조
|
||||
→ `SearchableSelectionModal<T>` 하나로 통합 가능
|
||||
|
||||
---
|
||||
|
||||
## 성능 최적화 포인트
|
||||
|
||||
| 항목 | 현재 상태 | 영향도 | 해결 방향 |
|
||||
|------|-----------|--------|-----------|
|
||||
| React.memo | 551개 컴포넌트 중 **1개만** 사용 | 🔴 높음 | 리스트 아이템/카드 컴포넌트에 적용 |
|
||||
| 인라인 화살표 함수 | **746곳** `onClick={() => ...}` | 🟡 중간 | 대형 컴포넌트에서 useCallback 적용 |
|
||||
| useMemo 미사용 | 대용량 배열 필터링/정렬 곳곳 | 🟡 중간 | 비용 높은 계산에 적용 |
|
||||
|
||||
**React.memo 우선 적용 대상** (리스트 내 반복 렌더링 컴포넌트):
|
||||
- `production/WorkerScreen/WorkItemCard.tsx`
|
||||
- `board/CommentSection/CommentItem.tsx`
|
||||
- `business/construction/management/ProjectCard.tsx`
|
||||
- 기타 *Row, *Item, *Card 컴포넌트 30+개
|
||||
|
||||
---
|
||||
|
||||
## 타입 시스템 문제
|
||||
|
||||
| 항목 | 수치 | 영향 |
|
||||
|------|------|------|
|
||||
| `any` 타입 사용 | 102곳 (29개 파일) | 타입 안전성 저하 |
|
||||
| 동일 엔티티 다중 타입 정의 | Vendor, Item, Order 등 | 변환 코드 ~800줄 중복 |
|
||||
| types.ts 파일 | 94개 | 정규 타입 찾기 어려움 |
|
||||
| @ts-ignore/eslint-disable | 25개 파일 | 숨겨진 타입 에러 |
|
||||
|
||||
---
|
||||
|
||||
## 추출 가능한 공통 훅 목록
|
||||
|
||||
### 즉시 생성 가능 (프론트 단독)
|
||||
|
||||
| 훅 이름 | 대체 범위 | 예상 절감 | 기존 참고 |
|
||||
|---------|-----------|-----------|-----------|
|
||||
| `useListData` | 60+ 리스트 페이지 | ~4,000줄 | hooks/useDetailData.ts 패턴 확장 |
|
||||
| `useFormSubmit` | 80+ 폼 | ~3,000줄 | 신규 |
|
||||
| `usePagination` | 60+ 컴포넌트 | ~1,000줄 | 신규 |
|
||||
| `useModal<T>` | 42 모달 | ~500줄 | 신규 |
|
||||
| `useClientSideFiltering` | 55+ 컴포넌트 | ~800줄 | 신규 |
|
||||
|
||||
### 기존 훅 (활용 확대 필요)
|
||||
|
||||
| 훅 | 현재 사용 | 전체 적용 시 |
|
||||
|----|-----------|-------------|
|
||||
| `useDetailData` | ~15 컴포넌트 | 100+ 상세 페이지 |
|
||||
| `useDetailPageState` | ~10 컴포넌트 | 100+ 상세 페이지 |
|
||||
| `useCRUDHandlers` | ~10 컴포넌트 | 80+ CRUD 페이지 |
|
||||
|
||||
---
|
||||
|
||||
## 실행 계획
|
||||
|
||||
### Phase 1: 공통 훅 추출 (1-2주) `프론트 단독` `ROI 최고`
|
||||
|
||||
> 새 훅 생성 + 고빈도 페이지 20개 우선 적용
|
||||
|
||||
**작업 항목**:
|
||||
```
|
||||
- [ ] useListData 훅 생성 (페칭 + 페이지네이션 + 필터 통합)
|
||||
- [ ] useFormSubmit 훅 생성 (제출 + 로딩 + 에러 + 토스트)
|
||||
- [ ] usePagination 훅 생성
|
||||
- [ ] useModal<T> 훅 생성
|
||||
- [ ] 고빈도 리스트 페이지 10개 마이그레이션
|
||||
- vendors, clients, items, orders, quotes
|
||||
- production, quality, material, hr, accounting 각 1개
|
||||
- [ ] 고빈도 폼 페이지 10개 마이그레이션
|
||||
- 위 도메인의 등록/수정 폼 각 1개
|
||||
```
|
||||
|
||||
**예상 효과**: ~8,500줄 절감, 패턴 일관성 +60%
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: God 컴포넌트 분리 (2-3주) `프론트 단독`
|
||||
|
||||
> 2000줄+ 파일 4개 + 핵심 1000줄+ 파일 우선 분리
|
||||
|
||||
**작업 항목**:
|
||||
```
|
||||
- [ ] MainDashboard.tsx (2,651줄) 분리
|
||||
→ sections/CEOSection, SalesSection, ProductionSection, QualitySection
|
||||
→ hooks/useDashboardData
|
||||
→ utils/calculations
|
||||
- [ ] ItemMasterContext.tsx (2,406줄) 분리
|
||||
→ ItemContext, SpecificationContext, MaterialContext
|
||||
→ TemplateContext, AttributeContext
|
||||
- [ ] useCEODashboard.ts (1,172줄) 분리
|
||||
→ useDailyReport, useReceivables, useMonthlyExpense 등 개별 훅
|
||||
→ 훅 팩토리 패턴 적용
|
||||
- [ ] lib/api/item-master.ts (2,232줄) 분리
|
||||
→ 도메인별 API 모듈 (items, specifications, materials, templates)
|
||||
- [ ] AuthenticatedLayout.tsx (1,289줄)
|
||||
→ useLayoutState, useNavigation, useTenantBranding 훅 추출
|
||||
```
|
||||
|
||||
**예상 효과**: 유지보수성 +50%, 단위 테스트 가능성 확보
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: 액션 파일 제네릭화 (2-3주) `프론트 단독` `최대 코드 감소`
|
||||
|
||||
> CRUD 서비스 팩토리 생성 + 80개 actions.ts 점진적 마이그레이션
|
||||
|
||||
**작업 항목**:
|
||||
```
|
||||
- [ ] createCrudService<TApi, TFront> 팩토리 함수 구현
|
||||
- getList, getById, create, update, delete, bulkDelete
|
||||
- transform / reverseTransform 자동 적용
|
||||
- 에러 핸들링 표준화
|
||||
- [ ] 공통 transform 유틸리티 (lib/transformers/)
|
||||
- API snake_case → 프론트 camelCase 자동 변환
|
||||
- 날짜 문자열 파싱 공통화
|
||||
- [ ] 고빈도 도메인 마이그레이션 (10개)
|
||||
- orders, quotes, clients, vendors
|
||||
- production, quality, material
|
||||
- hr/employee, hr/vacation, accounting
|
||||
- [ ] 나머지 70개 점진적 마이그레이션
|
||||
```
|
||||
|
||||
**예상 효과**: ~24,000줄 → ~8,000줄 (67% 감소)
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: 템플릿/패턴 통일 (2-3주) `프론트 단독`
|
||||
|
||||
> UniversalListPage 확대 + 검증 표준화 + 모달 통합
|
||||
|
||||
**작업 항목**:
|
||||
```
|
||||
- [ ] UniversalListPage 기능 보강
|
||||
- 고급 필터 UI
|
||||
- 컬럼 커스터마이징
|
||||
- 내보내기 기능
|
||||
- [ ] 레거시 리스트 페이지 → UniversalListPage 마이그레이션 (우선 20개)
|
||||
- [ ] SearchableSelectionModal<T> 공통 컴포넌트 생성
|
||||
- ItemSearchModal, AssigneeSelectModal 등 5개 통합
|
||||
- [ ] Zod 검증 스키마 라이브러리 구축
|
||||
- lib/validations/common.ts (이메일, 전화, 사업자번호)
|
||||
- lib/validations/vendor.ts, order.ts, item.ts 등
|
||||
- [ ] 수동 검증 50+ 폼 → Zod 마이그레이션 (우선 10개)
|
||||
```
|
||||
|
||||
**예상 효과**: ~5,000줄 절감, UX 일관성 +80%
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: 성능 + 타입 정리 (1-2주) `프론트 단독`
|
||||
|
||||
> React.memo 적용 + any 제거 + 타입 통합
|
||||
|
||||
**작업 항목**:
|
||||
```
|
||||
- [ ] React.memo 적용 (리스트 아이템 컴포넌트 30+개)
|
||||
- WorkItemCard, CommentItem, ProjectCard 등
|
||||
- *Row, *Item, *Card 패턴 전수 조사
|
||||
- [ ] 대형 컴포넌트 useCallback 적용
|
||||
- MainDashboard, WorkerScreen, DynamicItemForm
|
||||
- [ ] any 타입 제거 (102곳)
|
||||
- Phase 1: types/ 파일 (4개)
|
||||
- Phase 2: action error handler (50+ 파일)
|
||||
- Phase 3: 컴포넌트 props (20개)
|
||||
- [ ] 공통 타입 라이브러리 정리
|
||||
- types/shared/ 폴더 생성
|
||||
- ApiResponse<T>, PaginatedResponse<T>, FormState<T> 등
|
||||
- 엔티티별 정규 타입 단일화
|
||||
- [ ] @ts-ignore / eslint-disable 제거 (25개 파일)
|
||||
```
|
||||
|
||||
**예상 효과**: 리스트 렌더링 30-50% 개선, 타입 안전성 +60%
|
||||
|
||||
---
|
||||
|
||||
## 전체 예상 효과 요약
|
||||
|
||||
| 지표 | Phase 1 | Phase 2 | Phase 3 | Phase 4 | Phase 5 | 합계 |
|
||||
|------|---------|---------|---------|---------|---------|------|
|
||||
| 코드 절감 | ~8,500줄 | (구조 개선) | ~16,000줄 | ~5,000줄 | (품질 개선) | **~29,500줄** |
|
||||
| 패턴 일관성 | +60% | +50% | +40% | +80% | +60% | 종합 개선 |
|
||||
| 유지보수성 | 높음 | 매우 높음 | 높음 | 중간 | 중간 | 종합 개선 |
|
||||
| 위험도 | 낮음 | 중간 | 중간 | 낮음 | 낮음 | - |
|
||||
|
||||
---
|
||||
|
||||
## 병렬 진행 가능 조합
|
||||
|
||||
```
|
||||
[독립 진행 가능]
|
||||
├─ Phase 1 (공통 훅) ──→ 즉시 시작
|
||||
├─ Phase 5 (성능/타입) ─→ 즉시 시작 (Phase 1과 병렬)
|
||||
│
|
||||
[Phase 1 완료 후]
|
||||
├─ Phase 2 (God 컴포넌트 분리) ──→ 훅 활용하여 분리
|
||||
├─ Phase 3 (액션 제네릭화) ────→ 독립 진행 가능
|
||||
│
|
||||
[Phase 1+3 완료 후]
|
||||
└─ Phase 4 (템플릿 통일) ─────→ 훅 + 서비스 활용
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 관련 문서
|
||||
|
||||
| 문서 | 설명 |
|
||||
|------|------|
|
||||
| `[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md` | 멀티테넌시 공통화 로드맵 (별도 트랙) |
|
||||
| `[ANALYSIS-2026-01-20] 공통화-현황-분석.md` | 공통화 현황 분석 |
|
||||
| `[ANALYSIS-2026-02-05] list-page-commonization-status.md` | 리스트 페이지 공통화 현황 |
|
||||
| `[IMPL-2026-02-05] detail-hooks-migration-plan.md` | 상세 페이지 훅 마이그레이션 계획 |
|
||||
| `[IMPL-2026-02-05] formatter-commonization-plan.md` | 포매터 공통화 계획 |
|
||||
| `[PLAN-2026-01-22] ui-component-abstraction.md` | UI 컴포넌트 추상화 계획 |
|
||||
| `guides/[PLAN-2025-12-23] common-component-extraction-plan.md` | 공통 컴포넌트 추출 계획 |
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
|
||||
| 날짜 | 변경 내용 |
|
||||
|------|-----------|
|
||||
| 2026-02-06 | 초기 작성 - 전체 코드베이스 분석 기반 5 Phase 로드맵 |
|
||||
|
||||
---
|
||||
|
||||
**모든 Phase 프론트 단독 가능** - 백엔드 의존성 없음
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
QuoteStatus,
|
||||
ProductCategory,
|
||||
BomCalculationResultItem,
|
||||
BomCalculationResult,
|
||||
} from './types';
|
||||
import { transformApiToFrontend, transformFrontendToApi } from './types';
|
||||
|
||||
@@ -901,36 +902,8 @@ export interface BomCalculateItem {
|
||||
inspectionFee?: number;
|
||||
}
|
||||
|
||||
export interface BomCalculationResult {
|
||||
success?: boolean;
|
||||
finished_goods: {
|
||||
code: string;
|
||||
name: string;
|
||||
item_category?: string;
|
||||
};
|
||||
variables?: Record<string, unknown>;
|
||||
items: Array<{
|
||||
item_code: string;
|
||||
item_name: string;
|
||||
item_category?: string;
|
||||
specification?: string;
|
||||
unit?: string;
|
||||
quantity: number;
|
||||
quantity_formula?: string;
|
||||
base_price?: number;
|
||||
multiplier?: number;
|
||||
unit_price: number;
|
||||
total_price: number;
|
||||
calculation_note?: string;
|
||||
category_group?: string;
|
||||
process_group?: string;
|
||||
process_group_key?: string;
|
||||
}>;
|
||||
grouped_items?: Record<string, { items: BomCalculationResultItem[]; [key: string]: unknown }>;
|
||||
subtotals: Record<string, { name?: string; count?: number; subtotal?: number } | number>;
|
||||
grand_total: number;
|
||||
debug_steps?: Array<{ step: number; name: string; data: Record<string, unknown> }>; // 10단계 계산 과정
|
||||
}
|
||||
// BomCalculationResult는 types.ts에서 import하고 re-export
|
||||
export type { BomCalculationResult } from './types';
|
||||
|
||||
// API 서버 응답 구조 (QuoteCalculationService::calculateBomBulk)
|
||||
export interface BomBulkResponse {
|
||||
|
||||
@@ -411,11 +411,17 @@ export interface BomMaterial {
|
||||
export interface BomCalculationResultItem {
|
||||
item_code: string;
|
||||
item_name: string;
|
||||
item_category?: string; // 품목 카테고리
|
||||
specification?: string;
|
||||
unit?: string;
|
||||
quantity: number; // 1개당 BOM 수량 (base_quantity)
|
||||
quantity: number; // 1개당 BOM 수량 (base_quantity)
|
||||
quantity_formula?: string; // 수량 계산 공식
|
||||
base_price?: number; // 기본 단가
|
||||
multiplier?: number; // 승수
|
||||
unit_price: number;
|
||||
total_price: number;
|
||||
calculation_note?: string; // 계산 메모
|
||||
category_group?: string; // 카테고리 그룹
|
||||
process_group?: string;
|
||||
process_group_key?: string; // Legacy 공정 그룹 키
|
||||
category_code?: string; // 아이템 카테고리 코드 (동적 카테고리 시스템)
|
||||
@@ -431,6 +437,7 @@ export interface BomDebugStep {
|
||||
|
||||
// BOM 계산 결과 타입
|
||||
export interface BomCalculationResult {
|
||||
success?: boolean; // API 응답 시 성공 여부
|
||||
finished_goods: {
|
||||
code: string;
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user