- MODULE.md 경계 마커 4개 (production, quality, construction, vehicle-management) - verify-module-separation.sh: Common→Tenant 금지 임포트 검증 스크립트 - 영업 생산지시 3개 페이지에 useModules 가드 추가 - MODULE_SEPARATION_OK 주석 마커 (공유 래퍼 허용) - tsconfig @modules/* path alias 추가 - CLAUDE.md 모듈 분리 아키텍처 섹션 추가 - 모듈 분리 가이드 문서 (claudedocs/architecture/)
235 lines
8.6 KiB
Markdown
235 lines
8.6 KiB
Markdown
# SAM ERP 멀티테넌트 모듈 분리 아키텍처
|
|
|
|
> 작성일: 2026-03-18
|
|
> 상태: 프론트엔드 Phase 0~3 완료 / 백엔드 작업 필요
|
|
|
|
---
|
|
|
|
## 1. 개요
|
|
|
|
### 목표
|
|
하나의 SAM ERP 코드베이스에서 **테넌트(회사)별로 필요한 모듈만 활성화**하여,
|
|
불필요한 메뉴·페이지·대시보드 섹션을 숨기는 구조.
|
|
|
|
### 현재 테넌트별 모듈 구성
|
|
| 업종 코드 | 테넌트 예시 | 활성 모듈 |
|
|
|-----------|------------|-----------|
|
|
| `shutter_mes` | 경동 | 생산관리, 품질관리, 차량관리 |
|
|
| `construction` | 주일 | 시공관리, 차량관리 |
|
|
| (미설정) | 기타 모든 테넌트 | **전체 모듈 활성화 (기존과 동일)** |
|
|
|
|
### 안전 원칙
|
|
```
|
|
tenant.options.industry가 설정되지 않은 테넌트 → 모든 기능 그대로 사용 가능
|
|
= 기존 동작 100% 유지, 부작용 제로
|
|
```
|
|
|
|
---
|
|
|
|
## 2. 프론트엔드 구조 (완료)
|
|
|
|
### 파일 구조
|
|
```
|
|
src/modules/
|
|
├── types.ts # ModuleId 타입 정의
|
|
├── tenant-config.ts # 업종→모듈 매핑 (resolveEnabledModules)
|
|
└── index.ts # 모듈 레지스트리 (라우트 매핑, 대시보드 섹션)
|
|
|
|
src/hooks/
|
|
└── useModules.ts # React 훅: isEnabled(), isRouteAllowed(), tenantIndustry
|
|
```
|
|
|
|
### 모듈 ID 목록
|
|
| ModuleId | 이름 | 소유 라우트 | 대시보드 섹션 |
|
|
|----------|------|------------|--------------|
|
|
| `common` | 공통 ERP | /dashboard, /accounting, /sales, /hr, /approval, /settings 등 | 전부 |
|
|
| `production` | 생산관리 | /production | dailyProduction, unshipped |
|
|
| `quality` | 품질관리 | /quality | - |
|
|
| `construction` | 시공관리 | /construction | construction |
|
|
| `vehicle-management` | 차량관리 | /vehicle-management, /vehicle | - |
|
|
|
|
### 프론트엔드 동작 흐름
|
|
```
|
|
1. 로그인 → authStore에 tenant 정보 저장
|
|
2. useModules() 훅이 tenant.options.industry 읽음
|
|
3. industry 값으로 INDUSTRY_MODULE_MAP 조회 → 활성 모듈 목록 결정
|
|
4. 각 컴포넌트에서 isEnabled('production') 등으로 분기
|
|
```
|
|
|
|
### 적용된 영역
|
|
|
|
#### A. CEO 대시보드
|
|
- **섹션 필터링**: 비활성 모듈의 대시보드 섹션 자동 제외
|
|
- **API 호출 차단**: 비활성 모듈의 API는 호출하지 않음 (null endpoint)
|
|
- **설정 팝업**: 비활성 모듈 섹션은 설정에서도 안 보임
|
|
- **캘린더**: 비활성 모듈의 일정 유형 필터 숨김
|
|
- **요약 네비**: 비활성 섹션 자동 제외
|
|
|
|
#### B. 라우트 접근 제어
|
|
- `/production/*`, `/quality/*`, `/construction/*` 등 전용 라우트는 모듈 비활성 시 접근 차단
|
|
- `/sales/*/production-orders` 같은 공통 라우트 내 모듈 의존 페이지는 명시적 가드 적용
|
|
|
|
#### C. 사이드바 메뉴
|
|
- 비활성 모듈의 메뉴 항목 숨김 (isRouteAllowed 기반)
|
|
|
|
---
|
|
|
|
## 3. 백엔드 필요 작업
|
|
|
|
### 3.1 tenants 테이블 options 필드에 industry 추가
|
|
**우선순위: 🔴 필수**
|
|
|
|
현재 프론트엔드는 `tenant.options.industry` 값을 읽어서 모듈을 결정합니다.
|
|
이 값이 백엔드에서 내려와야 실제로 동작합니다.
|
|
|
|
```php
|
|
// tenants 테이블의 options JSON 컬럼에 industry 추가
|
|
// 예시 데이터:
|
|
{
|
|
"industry": "shutter_mes" // 경동: 셔터 MES
|
|
}
|
|
{
|
|
"industry": "construction" // 주일: 건설
|
|
}
|
|
// 다른 테넌트: industry 키 없음 → 프론트에서 전체 모듈 활성화
|
|
```
|
|
|
|
**작업 내용:**
|
|
1. `tenants` 테이블의 `options` JSON 컬럼에 `industry` 키 추가 (마이그레이션 불필요, JSON이므로)
|
|
2. 경동 테넌트: `options->industry = 'shutter_mes'`
|
|
3. 주일 테넌트: `options->industry = 'construction'`
|
|
4. 테넌트 정보 API 응답에 `options.industry` 포함 확인
|
|
|
|
**확인 포인트:**
|
|
- 프론트엔드에서 `authStore.currentUser.tenant.options.industry`로 접근
|
|
- 현재 로그인 API(`/api/v1/auth/me` 또는 유사)의 응답에서 tenant.options가 포함되는지 확인
|
|
- 포함 안 되면 응답에 추가 필요
|
|
|
|
### 3.2 (선택) 테넌트 관리 화면에서 industry 설정 UI
|
|
**우선순위: 🟡 선택**
|
|
|
|
관리자가 테넌트별 업종을 설정할 수 있는 UI. 급하지 않음 — DB 직접 수정으로 충분.
|
|
|
|
### 3.3 (Phase 2 예정) 명시적 모듈 목록 API
|
|
**우선순위: 🟢 향후**
|
|
|
|
현재는 `industry` → 프론트엔드 하드코딩 매핑으로 모듈 결정.
|
|
향후 백엔드에서 직접 모듈 목록을 내려주면 더 유연해짐.
|
|
|
|
```php
|
|
// tenant.options 예시 (Phase 2)
|
|
{
|
|
"industry": "shutter_mes",
|
|
"modules": ["production", "quality", "vehicle-management"] // 명시적 목록
|
|
}
|
|
```
|
|
|
|
프론트엔드는 이미 이 구조를 지원하도록 준비되어 있음:
|
|
```typescript
|
|
// src/modules/tenant-config.ts
|
|
export function resolveEnabledModules(options) {
|
|
// Phase 2: 백엔드가 명시적 모듈 목록 제공 → 우선 사용
|
|
if (explicitModules && explicitModules.length > 0) {
|
|
return explicitModules;
|
|
}
|
|
// Phase 1: industry 기반 기본값 (현재)
|
|
if (industry) {
|
|
return INDUSTRY_MODULE_MAP[industry] ?? [];
|
|
}
|
|
return [];
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. 업종별 모듈 매핑 (프론트엔드 하드코딩)
|
|
|
|
```typescript
|
|
// src/modules/tenant-config.ts
|
|
const INDUSTRY_MODULE_MAP: Record<string, ModuleId[]> = {
|
|
shutter_mes: ['production', 'quality', 'vehicle-management'],
|
|
construction: ['construction', 'vehicle-management'],
|
|
};
|
|
```
|
|
|
|
새로운 업종 추가 시:
|
|
1. 여기에 매핑 추가
|
|
2. 필요하면 `ModuleId` 타입에 새 모듈 ID 추가
|
|
3. `MODULE_REGISTRY` (src/modules/index.ts)에 라우트/대시보드 섹션 등록
|
|
|
|
---
|
|
|
|
## 5. 핵심 코드 패턴
|
|
|
|
### 기본 사용법
|
|
```typescript
|
|
import { useModules } from '@/hooks/useModules';
|
|
|
|
function MyComponent() {
|
|
const { isEnabled, tenantIndustry } = useModules();
|
|
|
|
// 안전 장치: industry 미설정이면 모든 기능 활성
|
|
const moduleAware = !!tenantIndustry;
|
|
|
|
if (moduleAware && !isEnabled('production')) {
|
|
return <div>생산관리 모듈이 비활성화되어 있습니다.</div>;
|
|
}
|
|
|
|
// 생산관리 기능 렌더링...
|
|
}
|
|
```
|
|
|
|
### 크로스 모듈 임포트 규칙
|
|
```
|
|
✅ Common → Common (자유)
|
|
✅ Tenant → Common (자유)
|
|
✅ Common → Tenant (래퍼 경유) (src/lib/api/에서 MODULE_SEPARATION_OK 주석과 함께)
|
|
❌ Common → Tenant (직접) (scripts/verify-module-separation.sh가 검출)
|
|
❌ Tenant → Tenant (금지, dynamic import만 허용)
|
|
```
|
|
|
|
---
|
|
|
|
## 6. 구현 이력
|
|
|
|
| Phase | 내용 | 커밋 | 상태 |
|
|
|-------|------|------|------|
|
|
| Phase 0 | 크로스 모듈 의존성 해소 | `a99c3b39` | ✅ 완료 |
|
|
| Phase 1 | 모듈 레지스트리 + 라우트 가드 | `0a65609e` | ✅ 완료 |
|
|
| Phase 2 | CEO 대시보드 모듈 디커플링 | `46501214` | ✅ 완료 |
|
|
| Phase 3 | 물리적 분리 (경계 마커, 검증, 가드, 문서) | (미커밋) | ✅ 완료 |
|
|
|
|
---
|
|
|
|
## 7. 테스트 시나리오
|
|
|
|
### 테스트 방법
|
|
백엔드에서 `tenant.options.industry`를 설정한 후:
|
|
|
|
| 시나리오 | 예상 결과 |
|
|
|----------|----------|
|
|
| industry 미설정 테넌트 로그인 | 기존과 완전 동일 (모든 메뉴/기능 표시) |
|
|
| `shutter_mes` 테넌트 로그인 | 시공관리 메뉴 숨김, 대시보드 시공 섹션 안 보임 |
|
|
| `construction` 테넌트 로그인 | 생산/품질 메뉴 숨김, 대시보드 생산 섹션 안 보임 |
|
|
| `shutter_mes`에서 `/construction` 직접 접근 | 접근 차단 메시지 표시 |
|
|
| `construction`에서 `/production` 직접 접근 | 접근 차단 메시지 표시 |
|
|
|
|
### 롤백 방법
|
|
문제 발생 시 DB에서 `tenant.options.industry` 값만 제거하면 즉시 원복.
|
|
프론트엔드 코드 변경 불필요.
|
|
|
|
---
|
|
|
|
## 8. 향후 로드맵
|
|
|
|
```
|
|
현재 (Phase 1) 향후 (Phase 2) 최종 (Phase 3)
|
|
┌─────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
|
|
│ industry 하드코딩 │ → │ 백엔드 modules 목록 │ → │ JSON 스키마 기반 │
|
|
│ 매핑으로 모듈 결정 │ │ API에서 직접 수신 │ │ 동적 페이지 조립 │
|
|
└─────────────────┘ └──────────────────────┘ └─────────────────────┘
|
|
```
|
|
|
|
- **Phase 2**: `tenant.options.modules = ["production", "quality"]` 형태로 백엔드에서 명시적 모듈 목록 전달 → 업종 매핑 테이블 불필요
|
|
- **Phase 3**: 각 모듈의 페이지 구성을 JSON 스키마로 정의 → 코드 변경 없이 테넌트별 화면 커스터마이징
|