Files
sam-docs/frontend/v2/01-dynamic-multi-tenant-page-system.md
유병철 65b6a27479 docs: [frontend] 동적 멀티테넌트 페이지 시스템 JSONB 저장 방식 확정
- JSONB vs JSON 비교 및 채택 근거 추가
- DB 테이블 구조 제안 (page_configs)
- JSONB가 config 설계에 미치는 영향 정리
- 백엔드 협의 필요 사항 업데이트
- 문서 버전 1.1 → 1.2
2026-03-11 22:33:38 +09:00

37 KiB
Raw Blame History

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

작성일: 2026-03-11 상태: 초안 (백엔드 논의 필요) 관련 문서:

  • [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

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 구조 (제안):

{
  "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가 필요한 이유 — 우리 시스템과의 연관:

-- 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에 경로 포함)

{
  "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

{
  "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 기준으로 렌더링
// 프론트 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 견적 할인, 세금, 재고 검증 등
// 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: 점진적 마이그레이션 전략

Phase 범위 예상 기간 상태
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 기존 정적 → 동적 완전 전환 지속적
- 남은 하드코딩 페이지 점진적 전환
전환 판단 기준:

[쉬움] 순수 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 ItemFieldResponseDynamicFieldResponse 리네이밍

신규 개발 필요

자산 역할
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] 동적 필드 타입 백엔드 요청서

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