# 동적 멀티테넌트 페이지 시스템 설계

> 작성일: 2026-03-11
> 최종 업데이트: 2026-03-20
> 상태: 초안 (백엔드 논의 진행 중)
> 관련 문서:
> - `[VISION-2026-02-19] dynamic-rendering-platform-strategy.md`
> - `[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md`
> - `[DESIGN-2026-02-11] dynamic-field-type-extension.md`
> - `[ANALYSIS-2026-03-17] tenant-module-separation-dependency-audit.md`
> - `[PLAN-2026-03-17] tenant-module-separation-plan.md` — Phase 0~3 실행 계획
> - `[IMPL-2026-03-20] permission-whitelist-gate.md` — PermissionGate 화이트리스트 전환
> - `sam-docs/frontend/v1/12-permission-whitelist.md` — 권한 기반 접근 제어 가이드

---

## 1. 핵심 목표

```
현재: 테넌트(업종)별 페이지를 하드코딩 → 신규 테넌트마다 개발 필요
목표: 백엔드 기준관리에서 설정 → JSON API → 프론트 동적 렌더링
결과: 프론트엔드 코드 변경 0줄로 새 테넌트 대응
```

---

## 2. 전체 아키텍처

```
┌─────────────────────────────────────────────────────────┐
│  백엔드 어드민 (mng)                                      │
│  ┌───────────────────────────────────────────────────┐  │
│  │  기준관리 페이지                                     │  │
│  │  레이아웃 / 섹션 / 항목 / 속성 등록                    │  │
│  └───────────────┬───────────────────────────────────┘  │
│                  │ 저장                                   │
│                  ↓                                       │
│  ┌───────────────────────────────────────────────────┐  │
│  │  DB (테넌트별 페이지 config)                         │  │
│  └───────────────┬───────────────────────────────────┘  │
│                  │ API                                    │
└──────────────────┼──────────────────────────────────────┘
                   ↓
┌──────────────────────────────────────────────────────────┐
│  프론트엔드 (Next.js)                                      │
│                                                          │
│  ┌────────────┐    ┌─────────────────────────────────┐  │
│  │ 정적 페이지  │    │ 동적 페이지                       │  │
│  │ - 로그인    │    │ - catch-all route               │  │
│  │ - 회원가입  │    │ - JSON config → 동적 렌더링       │  │
│  │ - 404 등   │    │ - pageType별 렌더러 선택          │  │
│  └────────────┘    └─────────────────────────────────┘  │
│        │                        │                        │
│        └──── 공유 컴포넌트 ───────┘                        │
│              (ui/, molecules/, organisms/)                │
└──────────────────────────────────────────────────────────┘
```

---

## 3. 규칙 정의

### 규칙 1: 기준관리 → 백엔드 어드민

| 항목 | 내용 |
|------|------|
| 현재 | 프론트 `ItemMasterDataManagement` 등에서 기준관리 |
| 변경 | 백엔드 어드민(mng) 페이지로 이동 |
| 이유 | 프론트 번들 크기 감소, 설정 변경 = 배포 불필요 |
| 담당 | 🔵 백엔드 |

```
Before: 프론트 기준관리 UI → 프론트 API 호출 → DB 저장
After:  백엔드 어드민 UI → 직접 DB 저장 → API로 config 전달
```

---

### 규칙 2: 페이지 정보를 JSON API로 제공

| 항목 | 내용 |
|------|------|
| 방식 | 메뉴 API처럼 페이지 config도 JSON API로 제공 |
| 엔드포인트 | `GET /api/v1/page-configs/{slug}` (제안) |
| 응답 | 페이지 타입, 레이아웃, 섹션, 필드, 검증규칙, API 매핑 등 |
| 담당 | 🔵 백엔드 API 설계 |

**페이지 config JSON 구조 (제안)**:

```jsonc
{
  "pageId": "sales-order-list",
  "pageType": "list",            // list | detail | form | dashboard | document
  "title": "수주 관리",
  "slug": "sales/order-management",

  // --- 규칙 11: API 엔드포인트 매핑 ---
  "api": {
    "list": "/api/v1/orders",
    "detail": "/api/v1/orders/:id",
    "create": "/api/v1/orders",
    "update": "/api/v1/orders/:id",
    "delete": "/api/v1/orders/:id"
  },

  // --- 규칙 4: 레이아웃 > 섹션 > 항목 > 속성 ---
  "layout": {
    "sections": [
      {
        "sectionId": "filters",
        "sectionType": "filter",
        "fields": [
          {
            "fieldId": "status",
            "type": "select",
            "label": "상태",
            "options": [
              { "value": "all", "label": "전체" },
              { "value": "pending", "label": "대기" },
              { "value": "confirmed", "label": "확정" }
            ],
            "defaultValue": "all"
          },
          {
            "fieldId": "dateRange",
            "type": "dateRange",
            "label": "기간"
          }
        ]
      },
      {
        "sectionId": "table",
        "sectionType": "dataTable",
        "columns": [
          { "key": "orderNo", "label": "수주번호", "width": 120 },
          { "key": "clientName", "label": "거래처명", "width": 150 },
          { "key": "amount", "label": "금액", "type": "currency", "align": "right" },
          { "key": "status", "label": "상태", "type": "badge" }
        ],
        "actions": ["view", "edit", "delete"],
        "pagination": true
      }
    ]
  },

  // --- 규칙 12: 검증 규칙 ---
  "validation": {
    "quantity": { "required": true, "min": 1, "message": "1 이상 입력하세요" },
    "clientId": { "required": true, "message": "거래처를 선택하세요" }
  },

  // --- 규칙 13: 필드 간 의존성 ---
  "dependencies": [
    {
      "type": "visibility",
      "when": { "field": "itemType", "equals": "motor" },
      "show": ["motorSpec", "voltage"]
    },
    {
      "type": "computed",
      "target": "amount",
      "formula": "quantity * unitPrice"
    },
    {
      "type": "cascade",
      "source": "category1",
      "target": "category2",
      "api": "/api/v1/categories/:parentId/children"
    }
  ],

  // --- 규칙 14: 권한 ---
  "permissions": {
    "fieldLevel": {
      "unitPrice": { "view": ["admin", "sales_manager"], "edit": ["admin"] }
    },
    "actionLevel": {
      "delete": ["admin"],
      "export": ["admin", "sales_manager"]
    }
  }
}
```

> ⚠️ **백엔드 논의 필요**: JSON 구조의 세부 스펙 확정

#### 2-2. 백엔드 저장 방식: JSONB (확정)

> ✅ **확정**: 페이지 config는 PostgreSQL **JSONB** 타입으로 저장

| 항목 | JSON | JSONB (채택) |
|------|------|:---:|
| 저장 형태 | 텍스트 그대로 | 바이너리 (파싱된 형태) |
| 읽기 속도 | 매번 파싱 필요 | 이미 파싱됨 → **빠름** |
| 인덱싱 | ❌ 불가 | ✅ **GIN 인덱스 가능** |
| 내부 검색 | ❌ 전체 꺼내서 비교 | ✅ **특정 키/값으로 쿼리** |
| 부분 수정 | ❌ 전체 교체 | ✅ **특정 키만 업데이트** |

**JSONB가 필요한 이유 — 우리 시스템과의 연관**:

```sql
-- 1. 테넌트별 특정 타입 페이지만 조회 (인덱싱)
SELECT * FROM page_configs
WHERE tenant_id = 282
AND config->>'pageType' = 'list';

-- 2. 특정 필드 타입을 쓰는 페이지 검색 (내부 검색)
SELECT * FROM page_configs
WHERE config @> '{"layout":{"sections":[{"fields":[{"type":"reference"}]}]}}';

-- 3. 기준관리에서 섹션 하나만 수정 (부분 수정)
UPDATE page_configs
SET config = jsonb_set(config, '{layout,sections,0,title}', '"수정된 섹션명"');
```

**JSONB 채택이 config 구조 설계에 미치는 영향**:

| 영향 | 설명 |
|------|------|
| **구조 단순화** | 하나의 큰 JSONB에 전체 config를 담아도 부분 쿼리/수정 가능 → 테이블 분리 최소화 |
| **테넌트 분기** | JSONB 인덱스로 테넌트+pageType 조합 쿼리가 빠름 → 별도 테이블 불필요 |
| **기준관리 UI** | 섹션 하나만 수정해도 전체 config를 다시 저장할 필요 없음 → UX 향상 |
| **프론트 영향** | **없음** — 프론트는 동일한 JSON을 받아서 렌더링, 저장 방식 무관 |

```
DB 테이블 구조 (제안):

page_configs
├── id (PK)
├── tenant_id (FK, 인덱스)
├── slug (UNIQUE per tenant, 인덱스)
├── config (JSONB)          ← 페이지 config 전체
├── created_at
└── updated_at

GIN 인덱스: config에 대해 생성 → 내부 검색 고속화
복합 인덱스: (tenant_id, slug) → 테넌트별 페이지 조회 최적화
```

> ⚠️ **백엔드 논의 필요**: JSONB 기반 테이블 설계 세부 확정 (위 제안 구조 검토)

---

### 규칙 3: 정적 페이지 vs 동적 페이지 분류

| 분류 | 정적 페이지 | 동적 페이지 |
|------|------------|------------|
| 정의 | 테넌트 무관, 고정 UI | 테넌트 config 기반 동적 생성 |
| 예시 | 로그인, 회원가입, 404, 500 | 수주관리, 품목관리, 공정관리 등 |
| 라우팅 | 기존 파일 기반 라우트 | catch-all `[...slug]` |
| 컴포넌트 | 직접 코딩 | JSON → 동적 렌더러 |
| 변경 빈도 | 거의 없음 | 테넌트별/설정별 수시 변경 |

**정적 페이지 목록 (확정)**:

| 경로 | 페이지 | 이유 |
|------|--------|------|
| `/login` | 로그인 | 인증 전 접근, 공통 UI |
| `/signup` | 회원가입 | 인증 전 접근, 공통 UI |
| `/404` | Not Found | 에러 페이지 |
| `/500` | Server Error | 에러 페이지 |
| `/settings/*` | 설정 | 시스템 설정은 공통 |

> ⚠️ **논의 필요**: 설정 페이지 중 일부(구독, 결제)도 동적 대상인지?
> ⚠️ **논의 필요**: 대시보드는 동적 페이지? 위젯 기반 별도 시스템?

---

### 규칙 4: 계층 구조 — 레이아웃 > 섹션 > 항목 > 속성

```
Page (pageType에 의해 렌더러 결정)
 └─ Layout (전체 레이아웃: single-column, two-column, tabs 등)
     └─ Section (논리적 그룹: 기본정보, 상세정보, 테이블 등)
         └─ Field (개별 입력 항목: input, select, date 등)
             └─ Attribute (필드의 속성: label, placeholder, validation 등)
```

| 계층 | 역할 | 기준관리 등록 항목 | 프론트 컴포넌트 |
|------|------|-------------------|----------------|
| Layout | 전체 배치 | 레이아웃 타입 선택 | `DynamicPageLayout` |
| Section | 논리적 그룹 | 섹션 추가/순서/조건부 표시 | `DynamicSection` |
| Field | 개별 항목 | 필드 타입/라벨/기본값 | `DynamicFieldRenderer` (14종) |
| Attribute | 필드 속성 | 검증규칙/옵션/의존성 | props로 전달 |

---

### 규칙 5: 컴포넌트 책임 분리

```
┌─────────────────────────────────────────────────┐
│  상위: 데이터 처리 컴포넌트 (Layout, Section)      │
│  - API 호출 / 데이터 가공                          │
│  - 조건부 표시 로직                                │
│  - props 전달 / 이벤트 핸들링                      │
│  - Zustand store 구독                             │
└──────────────────┬──────────────────────────────┘
                   │ props (순수 데이터)
                   ↓
┌─────────────────────────────────────────────────┐
│  하위: 순수 기능 컴포넌트 (Field, Attribute)       │
│  - UI 렌더링만 담당                               │
│  - 외부 의존성 없음                               │
│  - value + onChange 패턴                          │
│  - 테스트 용이                                    │
└─────────────────────────────────────────────────┘
```

| 구분 | 상위 (Layout/Section) | 하위 (Field/Attribute) |
|------|----------------------|----------------------|
| 역할 | 데이터 처리, 조건 분기 | 순수 렌더링 |
| 상태 | Zustand 구독 | props only |
| API | 호출 가능 | 호출 안 함 |
| 예시 | `DynamicSection`, `DynamicListPage` | `Input`, `Select`, `DatePicker` |
| 테스트 | 통합 테스트 | 단위 테스트 |

---

### 규칙 6: Zustand 기반 상태 관리

```
┌────────────────────────────────────────────────┐
│  pageConfigStore (Zustand)                     │
│                                                │
│  state:                                        │
│    configs: Map<slug, PageConfig>              │
│    currentPage: PageConfig | null              │
│    loading: boolean                            │
│                                                │
│  actions:                                      │
│    fetchPageConfig(slug) → API 호출 + 캐시     │
│    invalidateConfig(slug) → 캐시 무효화        │
│    subscribeToPage(slug) → 실시간 구독         │
└────────────────────────────────────────────────┘
         │
         │ 구독
         ↓
┌────────────────┐  ┌────────────────┐
│ DynamicListPage │  │ DynamicFormPage │  ...
└────────────────┘  └────────────────┘
```

| 항목 | 설명 |
|------|------|
| Store 위치 | `src/stores/pageConfigStore.ts` (신규) |
| 캐시 전략 | 메모리(Zustand) → localStorage → API |
| 변경 감지 | 해시 비교 (메뉴 갱신과 동일 방식) |
| 테넌트 격리 | 기존 `TenantAwareCache` 패턴 재사용 |

---

### 규칙 7: 테넌트 + 하위 구성요소별 화면 분기

```
테넌트 A (셔터 제조업)
 ├─ 메뉴: 품목관리, 생산관리, 출하관리
 ├─ 품목 폼: 셔터 규격 필드 포함
 └─ 생산 공정: 셔터 전용 공정 단계

테넌트 B (건설업)
 ├─ 메뉴: 프로젝트관리, 공사관리, 기성관리
 ├─ 프로젝트 폼: 현장정보 필드 포함
 └─ 공사 공정: 건설 전용 단계

같은 테넌트 내에서도:
 ├─ 부서 A → 메뉴 5개, 필드 20개 표시
 └─ 부서 B → 메뉴 3개, 필드 12개 표시
```

| 분기 기준 | 설명 | 예시 |
|----------|------|------|
| 테넌트 (company) | 업종별 전체 화면 구성 | 셔터업 vs 건설업 |
| 부서 (department) | 같은 테넌트 내 부서별 | 영업팀 vs 생산팀 |
| 역할 (role) | 같은 부서 내 역할별 | 관리자 vs 일반 |
| 사용자 (user) | 개인 설정 | 즐겨찾기, 컬럼 순서 |

> ⚠️ **백엔드 논의 필요**: 분기 우선순위 및 상속 정책
> (테넌트 설정 → 부서 설정으로 오버라이드 → 사용자 설정으로 오버라이드?)

---

### 규칙 8: 정적/동적 컴포넌트 공유

```
src/components/
 ├── ui/                    ← 공유 (정적+동적 모두 사용)
 │   ├── Input.tsx
 │   ├── Select.tsx
 │   ├── DatePicker.tsx
 │   └── ...
 │
 ├── molecules/             ← 공유
 │   ├── FormField.tsx
 │   ├── SearchFilter.tsx
 │   └── ...
 │
 ├── organisms/             ← 공유
 │   ├── DataTable.tsx
 │   ├── MobileCard.tsx
 │   └── ...
 │
 ├── dynamic/               ← 동적 전용 (신규)
 │   ├── renderers/
 │   │   ├── DynamicListPage.tsx
 │   │   ├── DynamicDetailPage.tsx
 │   │   ├── DynamicFormPage.tsx
 │   │   └── DynamicDashboardPage.tsx
 │   ├── sections/
 │   │   ├── DynamicSection.tsx
 │   │   ├── DynamicFilterSection.tsx
 │   │   └── DynamicTableSection.tsx   ← 기존 이동
 │   ├── fields/
 │   │   └── DynamicFieldRenderer.tsx  ← 기존 이동 (14종)
 │   └── store/
 │       └── pageConfigStore.ts
 │
 └── static/                ← 정적 전용 (기존 유지)
     ├── auth/LoginPage.tsx
     └── auth/SignupPage.tsx
```

| 레이어 | 공유 여부 | 예시 |
|--------|----------|------|
| ui/ | ✅ 100% 공유 | Input, Select, Button |
| molecules/ | ✅ 100% 공유 | FormField, StatusBadge |
| organisms/ | ✅ 대부분 공유 | DataTable, SearchFilter |
| dynamic/renderers/ | ❌ 동적 전용 | DynamicListPage |
| 기존 도메인 컴포넌트 | ❌ 정적 전용 (점진적 전환) | OrderSalesDetailEdit |

---

### 규칙 9: 페이지 타입 분류 체계

| pageType | 용도 | 핵심 구성 요소 | 기존 대응 패턴 |
|----------|------|--------------|---------------|
| `list` | 목록 조회 | 필터 + 테이블 + 페이지네이션 + 액션 | UniversalListPage |
| `detail` | 상세 보기 | 읽기전용 섹션 + 수정/삭제 버튼 | IntegratedDetailTemplate |
| `form` | 등록/수정 | 입력 섹션 + 저장/취소 | DynamicItemForm (범용화) |
| `dashboard` | 대시보드 | 위젯/카드 그리드 | CEODashboard |
| `document` | 문서/프린트 | 프린트 레이아웃 + 결재란 | ContractDocument 등 |

```
pageType 결정 흐름:

API 응답의 pageType 값
  │
  ├─ "list"      → <DynamicListPage config={...} />
  ├─ "detail"    → <DynamicDetailPage config={...} />
  ├─ "form"      → <DynamicFormPage config={...} />
  ├─ "dashboard" → <DynamicDashboardPage config={...} />
  ├─ "document"  → <DynamicDocumentPage config={...} />
  └─ 미지원       → <FallbackPage /> (에러 표시)
```

---

### 규칙 10: 동적 라우팅 전략

```
src/app/[locale]/(protected)/
  │
  ├── (static-pages)/           ← 정적 페이지 그룹
  │   ├── settings/
  │   └── ...
  │
  └── [...slug]/                ← 동적 페이지 catch-all
      └── page.tsx              ← 아래 로직 수행
```

**catch-all page.tsx 동작 흐름**:

```
1. URL에서 slug 추출 (예: ["sales", "order-management"])
2. slug로 pageConfigStore에서 config 조회 (캐시 우선)
3. 캐시 없으면 → API 호출: GET /api/v1/page-configs/sales/order-management
4. config.pageType으로 렌더러 선택
5. 렌더러에 config 전달 → 동적 페이지 렌더링
```

| 라우트 우선순위 | 경로 | 설명 |
|---------------|------|------|
| 1 (최우선) | `/login`, `/signup` | 정적 페이지 (파일 존재) |
| 2 | `/settings/*` | 정적 그룹 (파일 존재) |
| 3 (폴백) | `/*` (나머지 전부) | catch-all → 동적 처리 |

> Next.js 라우팅 규칙: 구체적 경로 > catch-all → 충돌 없음

> ⚠️ **논의 필요**: 기존 정적 페이지를 동적으로 전환 시, 해당 파일 삭제 후 catch-all로 자연스럽게 이관

---

### 규칙 11: API 엔드포인트 동적 매핑

#### 11-1. API 호출 유형

| API 유형 | config 키 | 용도 |
|---------|----------|------|
| `list` | `api.list` | 목록 조회 (GET) |
| `detail` | `api.detail` | 상세 조회 (GET) |
| `create` | `api.create` | 등록 (POST) |
| `update` | `api.update` | 수정 (PUT/PATCH) |
| `delete` | `api.delete` | 삭제 (DELETE) |
| `export` | `api.export` | 엑셀 다운로드 (GET) |
| `custom` | `api.custom[actionName]` | 커스텀 액션 |

#### 11-2. 백엔드 API 제공 방식 (3가지 방향)

동적 페이지는 **데이터를 어디서 가져올지도 동적**이어야 합니다.
백엔드가 API를 어떤 방식으로 제공하느냐에 따라 3가지 방향이 있습니다.

| 방향 | 설명 | 장점 | 단점 |
|------|------|------|------|
| **A. 개별 API** | 페이지마다 전용 API 존재, config에 경로 명시 | 기존 API 재사용, 복잡한 로직 처리 가능 | 새 페이지마다 백엔드 개발 필요 |
| **B. 범용 Entity API** | 하나의 엔드포인트가 entityType으로 분기 | 새 페이지 추가 시 백엔드 코드 변경 없음 | 복잡한 비즈니스 로직 처리 어려움 |
| **C. 하이브리드 (권장)** | 단순 CRUD는 범용 API, 복잡한 로직은 전용 API | 양쪽 장점 모두 취함 | 두 방식 공존에 따른 관리 비용 |

**방향 A: 개별 API (config에 경로 포함)**
```jsonc
{
  "pageType": "list",
  "slug": "sales/order-management",
  "api": {
    "list": "/api/v1/orders",
    "detail": "/api/v1/orders/:id",
    "create": "/api/v1/orders",
    "delete": "/api/v1/orders/:id"
  }
}
```
→ 기존에 이미 만들어둔 API를 그대로 config에 연결
→ 견적 계산, 세금 처리 등 **비즈니스 로직이 있는 페이지에 적합**

**방향 B: 범용 Entity API**
```jsonc
{
  "pageType": "list",
  "slug": "master/equipment",
  "entityType": "equipment"
}
```
```
// 범용 API 1개로 모든 entity 처리
GET    /api/v1/entities/{entityType}
GET    /api/v1/entities/{entityType}/{id}
POST   /api/v1/entities/{entityType}
PUT    /api/v1/entities/{entityType}/{id}
DELETE /api/v1/entities/{entityType}/{id}
```
→ 백엔드에서 entityType에 따라 테이블/모델 동적 매핑
→ 단순 CRUD(거래처, 설비, 자재 등) **마스터 데이터 페이지에 적합**

**방향 C: 하이브리드 (권장)**
```
┌──────────────────────────────────────────────────┐
│  단순 CRUD 페이지 (거래처, 설비, 자재 등)           │
│  → 방향 B: 범용 entity API                        │
│  → config에 entityType만 지정                     │
│  → 새 페이지 추가 시 백엔드 코드 변경 없음           │
│  → 동적 시스템의 최대 효과                          │
└──────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────┐
│  비즈니스 로직 페이지 (견적, 생산, 세금계산서)        │
│  → 방향 A: 전용 API 경로를 config에 명시            │
│  → 계산/검증/워크플로우 등 복잡한 로직 처리           │
│  → 기존 API 재사용으로 마이그레이션 용이             │
└──────────────────────────────────────────────────┘
```

#### 11-3. 프론트 처리 방식 (어느 방향이든 동일)

```
프론트는 API 제공 방식에 무관하게 동일한 패턴으로 처리:

1. config에서 API 경로 결정
   ├─ api.list 있으면 → 그 경로 사용 (방향 A)
   └─ entityType 있으면 → `/api/v1/entities/${entityType}` 생성 (방향 B)
       ↓
2. buildApiUrl(경로, params)  ← 기존 유틸 재사용
       ↓
3. Server Action에서 API 프록시 호출
       ↓
4. 응답을 config.columns 기준으로 렌더링
```

```typescript
// 프론트 API 경로 결정 유틸 (예시)
function resolveApiUrl(config: PageConfig, action: 'list' | 'detail' | 'create' | 'update' | 'delete') {
  // 방향 A: 전용 API 경로가 있으면 사용
  if (config.api?.[action]) {
    return config.api[action];
  }
  // 방향 B: entityType으로 범용 API 생성
  if (config.entityType) {
    const base = `/api/v1/entities/${config.entityType}`;
    if (action === 'list' || action === 'create') return base;
    return `${base}/:id`;
  }
  throw new Error(`No API config for action: ${action}`);
}
```

#### 11-4. API 응답 구조 통일

어느 방향이든 **응답 구조는 통일**되어야 프론트가 범용 처리 가능:

| API 유형 | 응답 구조 |
|---------|----------|
| list | `{ data: [...], meta: { total, current_page, per_page, last_page } }` |
| detail | `{ data: { ... } }` |
| create | `{ data: { id, ... }, message: "..." }` |
| update | `{ data: { id, ... }, message: "..." }` |
| delete | `{ message: "..." }` |

> ⚠️ **백엔드 논의 필요**:
> - 범용 entity API 도입 여부 및 범위
> - 기존 API 중 응답 구조가 통일되지 않은 것 정리
> - 전용 API와 범용 API의 분류 기준 합의

---

### 규칙 12: 검증(Validation) 규칙

| 검증 타입 | JSON 표현 | 프론트 변환 |
|----------|----------|------------|
| 필수값 | `{ "required": true }` | `z.string().min(1)` |
| 최솟값 | `{ "min": 1 }` | `z.number().min(1)` |
| 최댓값 | `{ "max": 100 }` | `z.number().max(100)` |
| 정규식 | `{ "pattern": "^\\d{3}-\\d{2}$" }` | `z.string().regex()` |
| 커스텀 메시지 | `{ "message": "올바른 형식이 아닙니다" }` | 에러 메시지 |
| 이메일 | `{ "type": "email" }` | `z.string().email()` |
| 전화번호 | `{ "type": "phone" }` | `z.string().regex()` |

```
JSON validation config
  ↓ 런타임 변환
Zod 스키마 자동 생성
  ↓
react-hook-form zodResolver에 주입
  ↓
폼 검증 자동 적용
```

---

### 규칙 13: 필드 간 의존성

| 의존성 타입 | 설명 | 예시 |
|------------|------|------|
| `visibility` | 조건부 표시/숨김 | 품목타입=모터 → 전압 필드 표시 |
| `computed` | 자동 계산 | 수량 × 단가 = 금액 |
| `cascade` | 연쇄 선택 | 대분류 → 중분류 → 소분류 |
| `setValue` | 값 자동 설정 | 거래처 선택 → 담당자 자동 입력 |
| `disable` | 조건부 비활성화 | 상태=확정 → 수량 수정 불가 |

```
기존 자산 활용:
DynamicItemForm의 DisplayCondition → visibility 타입으로 범용화
DynamicItemForm의 ComputedField   → computed 타입으로 범용화
```

> ✅ **확정**: 복잡한 계산식(견적 할인율 등)은 **백엔드에서 전부 처리**하여 결과만 전달

---

### 규칙 14: 권한 통합

#### 14-1. 현재 권한 시스템 검증 결과

✅ **현재 권한 시스템으로 동적 페이지도 컨트롤 가능** (검증 완료)

현재 권한 시스템이 **메뉴 ID 기반 + URL 패턴 매칭**으로 동작하므로, 페이지가 정적이든 동적이든 해당 URL이 menu 테이블에 등록되어 있으면 권한 관리 페이지에서 동일하게 컨트롤됩니다.

```
현재 (정적 페이지):
  백엔드 menu 테이블에 URL 등록 → 권한 매트릭스 체크박스 on/off
  → PermissionGate가 URL 매칭 → 접근 허용/차단

동적 페이지도 동일:
  백엔드 menu 테이블에 동적 페이지 URL(slug) 등록
  → 권한 매트릭스에서 동일하게 체크박스 on/off
  → PermissionGate가 URL 매칭 → 동일하게 동작
```

#### 14-2. 권한 레벨별 동적 페이지 호환성

| 권한 레벨 | 현재 지원 | 동적 페이지 호환 | 사용 컴포넌트 |
|----------|:---:|:---:|------|
| 페이지 접근 (view) | ✅ | ✅ | `PermissionGate` (URL 매칭) |
| 생성 (create) | ✅ | ✅ | `usePermission()` / `PermissionGuard` |
| 수정 (update) | ✅ | ✅ | `usePermission()` / `PermissionGuard` |
| 삭제 (delete) | ✅ | ✅ | `usePermission()` / `PermissionGuard` |
| 승인 (approve) | ✅ | ✅ | `usePermission()` / `PermissionGuard` |
| 내보내기 (export) | ✅ | ✅ | `usePermission()` / `PermissionGuard` |
| 관리 (manage) | ✅ | ✅ | `usePermission()` / `PermissionGuard` |
| **필드 단위 권한** | ❌ | ❌ | 현재 미지원 → **v2 고려사항** |

#### 14-3. 권한 적용 흐름

```
권한 적용 흐름 (정적/동적 공통):

1. 페이지 접근: PermissionGate → URL longest prefix 매칭 → view 권한 확인
2. 액션 권한: usePermission() → canCreate/canDelete 등 → 버튼 표시/숨김
3. 필드 권한: 현재 미지원 (v2에서 config.permissions.fieldLevel 추가 시 구현)
```

> ⚠️ **백엔드 논의 필요**: 동적 페이지 URL(slug)을 menu 테이블에 자동 등록하는 방안
> (기준관리에서 페이지 생성 시 → menu 테이블에도 자동 연동?)

---

### 규칙 15: 캐싱 & 성능 전략

```
요청 흐름:

1차 캐시 (Zustand 메모리)
  ↓ miss
2차 캐시 (localStorage, 테넌트별 격리)
  ↓ miss
3차 (API 호출)
  ↓ 응답
1차 + 2차 캐시 갱신
```

| 전략 | 방법 | 갱신 주기 |
|------|------|----------|
| 초기 로드 | 로그인 시 전체 config 프리페치 | 1회 |
| 변경 감지 | 해시 비교 (메뉴 갱신과 동일) | 30초~5분 |
| 강제 갱신 | 관리자가 기준관리 변경 시 push | 즉시 |
| 캐시 무효화 | 테넌트 전환 시 전체 클리어 | 즉시 |

---

### 규칙 16: 비즈니스 로직 처리

> ✅ **확정**: 복잡한 계산 수식은 **백엔드에서 전부 처리**하여 결과만 전달

| 로직 복잡도 | 처리 방식 | 예시 |
|------------|----------|------|
| 단순 계산 | config formula (프론트) | 수량 × 단가 = 금액 |
| 복잡한 계산 | **백엔드 API** | 견적 할인, 세금, 재고 검증 등 |

```jsonc
// config에서 로직 지정
{
  "businessLogic": {
    // 단순: 프론트 formula (기존 ComputedField 재사용)
    "amount": { "type": "formula", "expression": "quantity * unitPrice" },

    // 복잡: 백엔드 위임 (확정)
    "totalDiscount": {
      "type": "api",
      "endpoint": "/api/v1/quotes/:id/calculate-discount",
      "trigger": "onFieldChange",
      "watchFields": ["quantity", "unitPrice", "discountRate"]
    }
  }
}
```

프론트는 단순 사칙연산(ComputedField)만 담당하고, 그 외 모든 비즈니스 로직은 백엔드 API로 위임합니다.

---

### 규칙 17: 점진적 마이그레이션 전략

#### 17-1. 3단계 아키텍처 방향 (2026-03-17 확인)

```
1단계: 현재 → 모듈 분리
  - 공통 ERP / 테넌트별 모듈 물리적 분리
  - 선결과제 해소 (아래 17-2 참조)

2단계: 모듈 분리 → JSON 동적 조립
  - 테넌트 모듈을 manifest/JSON 기반으로 전환
  - 동적 페이지 렌더러 도입

3단계: 최종 — 빈 페이지 셸 + 백엔드 JSON으로 페이지 자동 조립
  - 이 문서의 최종 목표
```

#### 17-2. 선결과제 (모듈 분리 전 해결 필수)

| # | 과제 | 내용 | 예상 |
|---|------|------|------|
| 1 | CEO 대시보드 테넌트 의존성 해소 | 생산/건설 섹션 직접 import → 동적 로딩 전환 | - |
| 2 | 공유 컴포넌트 추출 | 결재/영업(공통)이 생산(경동) 코드 직접 import | - |
| 3 | 라우트 가드 추가 | 테넌트 미보유 모듈 URL 직접 접근 차단 | - |
| 4 | dashboard-invalidation 동적화 | production/construction 도메인 키 하드코딩 제거 | - |

> 선결과제 해소 예상: 3~4일, 이후 모듈 분리 본작업은 별도 산정

**핵심 의존성 위반 (공통 → 테넌트 방향, 수정 필요)**:
```
ApprovalBox → production/InspectionReportModal
Sales/production-orders → production/ProductionOrders (actions+types+UI)
Sales → router.push("/production/work-orders") 하드코딩
CEODashboard → DailyProductionSection, ConstructionSection 직접 import
dashboard-invalidation.ts → production/construction 도메인 키
```

**안전한 부분**:
- 테넌트 간 교차 의존성 없음 (생산↔건설 = 0)
- 건설(주일) 모듈 완전 독립 → 바로 분리 가능
- Zustand 스토어, API 프록시, 메뉴 시스템은 무관

#### 17-3. 테넌트별 페이지 현황 (2026-03-17 분석)

| 테넌트 | 업종 | 전용 모듈 | 페이지 수 |
|--------|------|----------|:---:|
| 공통 ERP | 전 업종 | 회계, 인사, 결재, 게시판, 설정, 고객센터 등 | ~165 |
| 경동 | 셔터 제조 (MES) | 생산, 품질관리 | ~27 |
| 주일 | 건설 시공 | 건설/프로젝트, 입찰, 기성 | ~48 |
| (옵션) | - | 차량관리 | ~13 |

#### 17-4. 마이그레이션 Phase

| Phase | 범위 | 예상 기간 | 상태 |
|-------|------|----------|------|
| **선결과제** | 의존성 해소 (17-2) | 3-4일 | ⏳ 준비 |
| **Phase 0** | 인프라 구축 | 2-3주 | ⏳ |
| | - catch-all 라우터 | | |
| | - pageConfigStore | | |
| | - DynamicListPage/FormPage 렌더러 | | |
| | - 백엔드 page-config API | | |
| **Phase 1** | 신규 테넌트/페이지만 동적 | 2-4주 | ⏳ |
| | - 새로 추가되는 페이지는 동적으로 생성 | | |
| | - 기존 페이지는 그대로 유지 | | |
| **Phase 2** | 단순 CRUD 페이지 전환 | 4-6주 | ⏳ |
| | - 리스트+상세만 있는 단순 페이지 | | |
| | - 거래처관리, 설비관리 등 | | |
| **Phase 3** | 복잡한 비즈니스 페이지 전환 | 6-8주 | ⏳ |
| | - 견적, 수주, 생산 등 로직 있는 페이지 | | |
| **Phase 4** | 기존 정적 → 동적 완전 전환 | 지속적 | ⏳ |
| | - 남은 하드코딩 페이지 점진적 전환 | | |

```
전환 판단 기준:

[선행] 선결과제 해소 (의존성 분리)   → 선결과제 Phase
[쉬움] 순수 CRUD (리스트+폼)         → Phase 2에서 전환
[보통] CRUD + 단순 계산               → Phase 2~3
[어려움] 복잡한 비즈니스 로직          → Phase 3
[마지막] 문서/프린트, 대시보드          → Phase 4
```

---

## 4. 이미 있는 자산 → 재사용 매핑

| 기존 자산 | 현재 용도 | 동적 시스템에서의 역할 |
|----------|----------|---------------------|
| DynamicFieldRenderer (14종) | 품목 폼 필드 | → 모든 동적 폼 필드 |
| DynamicTableSection | 품목 BOM 테이블 | → 모든 동적 테이블 |
| DisplayCondition | 품목 조건부 표시 | → 범용 visibility 규칙 |
| ComputedField | 품목 자동 계산 | → 범용 computed 규칙 |
| UniversalListPage | 리스트 페이지 템플릿 | → DynamicListPage 기반 |
| IntegratedDetailTemplate | 상세 페이지 템플릿 | → DynamicDetailPage 기반 |
| TenantAwareCache | 캐시 격리 | → pageConfigStore 캐시 |
| menuRefresh (해시 비교) | 메뉴 갱신 | → config 변경 감지 |
| buildApiUrl | URL 빌더 | → 동적 API 호출에 재사용 |

---

## 5. 논의 현황 정리

### 확정 사항

| 항목 | 확정 내용 | 비고 |
|------|----------|------|
| API 제공 방식 | 하이브리드 (C) — 단순 CRUD는 범용, 복잡 로직은 전용 | 범용 API 세분화 가능성 있음 |
| 복잡한 계산 수식 | 백엔드에서 전부 처리, 결과만 전달 | 프론트는 단순 사칙연산만 |
| 권한 관리 호환성 | 현재 권한 시스템으로 동적 페이지 컨트롤 가능 | 메뉴 ID + URL 패턴 매칭 방식 |
| 기존 동적 필드 재사용 | DynamicFieldRenderer 14종 등 90%+ 재사용 가능 | 기준관리 UI가 mng로 이동해도 렌더링 컴포넌트 유지 |
| DB 저장 방식 | PostgreSQL **JSONB** 사용 | 인덱싱/부분수정/내부검색 가능, 프론트 영향 없음 |

### 협의 필요 사항

| 항목 | 현재 상태 | 논의 포인트 |
|------|----------|------------|
| JSON config 세부 구조 | 제안 구조 작성됨 (규칙 2 참조) | 회의에서 세부 항목 결정 후 확정 |
| 정적/동적 페이지 분류 | 초안 목록 작성됨 (규칙 3 참조) | 어떤 페이지를 정적으로 남길지 최종 확정 |
| 테넌트 하위 분기 정책 | 개념 정리됨 (규칙 7 참조) | 테넌트→부서→역할 오버라이드 정책, config를 최종 결과물로 줄지 프론트가 조합할지 |
| 동적 라우팅 전략 | catch-all 방식 제안 (규칙 10 참조) | 기존 정적 페이지와의 공존/전환 전략 |
| 범용 entity API 범위 | 하이브리드 방향 합의 | 페이지 렌더링 분기에 따라 범용 API 세분화 가능 |
| page-config API 스펙 | 미정 | `GET /api/v1/page-configs/{slug}` 응답 구조 |
| 기준관리 어드민 UI | 미정 | mng에서 레이아웃/섹션/필드 등록 화면 설계 |
| API 응답 통일 | 미정 | list/detail/create/update/delete 응답 포맷 표준화 |
| 캐시 무효화 | 미정 | 기준관리 변경 시 프론트 캐시 갱신 방법 (polling vs push) |
| 프리페치 범위 | 미정 | 로그인 시 전체 config vs 페이지 접근 시 개별 로드 |
| 검증/의존성 JSON 스펙 | 제안 구조 작성됨 (규칙 12, 13 참조) | 세부 스펙 확정 |
| 마이그레이션 순서 | Phase 0~4 제안 (규칙 17 참조) | 어떤 페이지부터 동적 전환할지 |
| 동적 페이지 → menu 자동 등록 | 미정 | 기준관리에서 페이지 생성 시 menu 테이블 자동 연동 방안 |
| 필드 단위 권한 | 현재 미지원 | v2 고려사항 (필요 시 추가 개발) |

---

## 6. 기존 자산 재사용 현황

### 즉시 재사용 가능 (코드 변경 없음)

| 자산 | 현재 용도 | 동적 시스템 역할 | 재사용도 |
|------|----------|----------------|:---:|
| DynamicFieldRenderer (14종) | 품목 폼 필드 | 모든 동적 폼 필드 | 100% |
| DynamicTableSection | 품목 BOM 테이블 | 모든 동적 테이블 | 99% |
| DisplayCondition (9개 연산자) | 품목 조건부 표시 | 범용 visibility 규칙 | 100% |
| ComputedField | 품목 자동 계산 | 범용 단순 계산 | 100% |
| Reference Sources 프리셋 | 거래처/품목 등 조회 | 새 source 추가만으로 확장 | 100% |
| TenantAwareCache | 캐시 격리 | pageConfigStore 캐시 | 100% |
| menuRefresh (해시 비교) | 메뉴 갱신 | config 변경 감지 | 100% |
| buildApiUrl | URL 빌더 | 동적 API 호출 | 100% |
| PermissionGate / usePermission | 정적 페이지 권한 | 동적 페이지 권한 (동일) | 100% |

### 범용화 필요 (약간의 리팩토링)

| 자산 | 변경 사항 |
|------|----------|
| useDynamicFormState | API URL을 파라미터로 받도록 |
| useFormStructure | 품목 전용 API → 범용 API 경로 |
| types.ts | `ItemFieldResponse` → `DynamicFieldResponse` 리네이밍 |

### 신규 개발 필요

| 자산 | 역할 |
|------|------|
| DynamicListPage | 동적 리스트 페이지 렌더러 (UniversalListPage 기반) |
| DynamicDetailPage | 동적 상세 페이지 렌더러 (IntegratedDetailTemplate 기반) |
| DynamicDashboardPage | 동적 대시보드 렌더러 |
| pageConfigStore | 페이지 config Zustand 스토어 |
| catch-all route | `[...slug]/page.tsx` 동적 라우터 |
| resolveApiUrl | API 경로 결정 유틸 (개별/범용 분기) |

---

## 7. 관련 문서

| 문서 | 위치 | 내용 |
|------|------|------|
| 동적 렌더링 플랫폼 비전 | `claudedocs/architecture/[VISION-2026-02-19]` | 전체 비전 및 자산 현황 |
| 멀티테넌시 최적화 로드맵 | `claudedocs/architecture/[PLAN-2026-02-06]` | 테넌트 격리/최적화 8 Phase |
| 동적 필드 타입 설계 | `claudedocs/architecture/[DESIGN-2026-02-11]` | 4-Level 구조, 14종 필드 |
| 동적 필드 구현 현황 | `claudedocs/architecture/[IMPL-2026-02-11]` | Phase 1~3 프론트 구현 완료 |
| 백엔드 API 스펙 | `claudedocs/item-master/[API-REQUEST-2026-02-12]` | 동적 필드 타입 백엔드 요청서 |
| 테넌트 모듈 의존성 분석 | `claudedocs/architecture/[ANALYSIS-2026-03-17]` | 3테넌트 분리, 선결과제 4개, 의존성 위반 목록 |

---

**문서 버전**: 1.3
**마지막 업데이트**: 2026-03-18
**다음 단계**: 백엔드 회의 → 협의 필요 항목 확정 → v2.0 작성 → `sam-docs/frontend/v2/`에 최종본 등록
