동적 멀티테넌트 페이지 시스템 설계
작성일: 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. 핵심 목표
2. 전체 아키텍처
3. 규칙 정의
규칙 1: 기준관리 → 백엔드 어드민
| 항목 |
내용 |
| 현재 |
프론트 ItemMasterDataManagement 등에서 기준관리 |
| 변경 |
백엔드 어드민(mng) 페이지로 이동 |
| 이유 |
프론트 번들 크기 감소, 설정 변경 = 배포 불필요 |
| 담당 |
🔵 백엔드 |
규칙 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가 필요한 이유 — 우리 시스템과의 연관:
JSONB 채택이 config 구조 설계에 미치는 영향:
| 영향 |
설명 |
| 구조 단순화 |
하나의 큰 JSONB에 전체 config를 담아도 부분 쿼리/수정 가능 → 테이블 분리 최소화 |
| 테넌트 분기 |
JSONB 인덱스로 테넌트+pageType 조합 쿼리가 빠름 → 별도 테이블 불필요 |
| 기준관리 UI |
섹션 하나만 수정해도 전체 config를 다시 저장할 필요 없음 → UX 향상 |
| 프론트 영향 |
없음 — 프론트는 동일한 JSON을 받아서 렌더링, 저장 방식 무관 |
⚠️ 백엔드 논의 필요: JSONB 기반 테이블 설계 세부 확정 (위 제안 구조 검토)
규칙 3: 정적 페이지 vs 동적 페이지 분류
| 분류 |
정적 페이지 |
동적 페이지 |
| 정의 |
테넌트 무관, 고정 UI |
테넌트 config 기반 동적 생성 |
| 예시 |
로그인, 회원가입, 404, 500 |
수주관리, 품목관리, 공정관리 등 |
| 라우팅 |
기존 파일 기반 라우트 |
catch-all [...slug] |
| 컴포넌트 |
직접 코딩 |
JSON → 동적 렌더러 |
| 변경 빈도 |
거의 없음 |
테넌트별/설정별 수시 변경 |
정적 페이지 목록 (확정):
| 경로 |
페이지 |
이유 |
/login |
로그인 |
인증 전 접근, 공통 UI |
/signup |
회원가입 |
인증 전 접근, 공통 UI |
/404 |
Not Found |
에러 페이지 |
/500 |
Server Error |
에러 페이지 |
/settings/* |
설정 |
시스템 설정은 공통 |
⚠️ 논의 필요: 설정 페이지 중 일부(구독, 결제)도 동적 대상인지?
⚠️ 논의 필요: 대시보드는 동적 페이지? 위젯 기반 별도 시스템?
규칙 4: 계층 구조 — 레이아웃 > 섹션 > 항목 > 속성
| 계층 |
역할 |
기준관리 등록 항목 |
프론트 컴포넌트 |
| Layout |
전체 배치 |
레이아웃 타입 선택 |
DynamicPageLayout |
| Section |
논리적 그룹 |
섹션 추가/순서/조건부 표시 |
DynamicSection |
| Field |
개별 항목 |
필드 타입/라벨/기본값 |
DynamicFieldRenderer (14종) |
| Attribute |
필드 속성 |
검증규칙/옵션/의존성 |
props로 전달 |
규칙 5: 컴포넌트 책임 분리
| 구분 |
상위 (Layout/Section) |
하위 (Field/Attribute) |
| 역할 |
데이터 처리, 조건 분기 |
순수 렌더링 |
| 상태 |
Zustand 구독 |
props only |
| API |
호출 가능 |
호출 안 함 |
| 예시 |
DynamicSection, DynamicListPage |
Input, Select, DatePicker |
| 테스트 |
통합 테스트 |
단위 테스트 |
규칙 6: Zustand 기반 상태 관리
| 항목 |
설명 |
| Store 위치 |
src/stores/pageConfigStore.ts (신규) |
| 캐시 전략 |
메모리(Zustand) → localStorage → API |
| 변경 감지 |
해시 비교 (메뉴 갱신과 동일 방식) |
| 테넌트 격리 |
기존 TenantAwareCache 패턴 재사용 |
규칙 7: 테넌트 + 하위 구성요소별 화면 분기
| 분기 기준 |
설명 |
예시 |
| 테넌트 (company) |
업종별 전체 화면 구성 |
셔터업 vs 건설업 |
| 부서 (department) |
같은 테넌트 내 부서별 |
영업팀 vs 생산팀 |
| 역할 (role) |
같은 부서 내 역할별 |
관리자 vs 일반 |
| 사용자 (user) |
개인 설정 |
즐겨찾기, 컬럼 순서 |
⚠️ 백엔드 논의 필요: 분기 우선순위 및 상속 정책
(테넌트 설정 → 부서 설정으로 오버라이드 → 사용자 설정으로 오버라이드?)
규칙 8: 정적/동적 컴포넌트 공유
| 레이어 |
공유 여부 |
예시 |
| 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 등 |
규칙 10: 동적 라우팅 전략
catch-all page.tsx 동작 흐름:
| 라우트 우선순위 |
경로 |
설명 |
| 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에 경로 포함)
→ 기존에 이미 만들어둔 API를 그대로 config에 연결
→ 견적 계산, 세금 처리 등 비즈니스 로직이 있는 페이지에 적합
방향 B: 범용 Entity API
→ 백엔드에서 entityType에 따라 테이블/모델 동적 매핑
→ 단순 CRUD(거래처, 설비, 자재 등) 마스터 데이터 페이지에 적합
방향 C: 하이브리드 (권장)
11-3. 프론트 처리 방식 (어느 방향이든 동일)
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() |
규칙 13: 필드 간 의존성
| 의존성 타입 |
설명 |
예시 |
visibility |
조건부 표시/숨김 |
품목타입=모터 → 전압 필드 표시 |
computed |
자동 계산 |
수량 × 단가 = 금액 |
cascade |
연쇄 선택 |
대분류 → 중분류 → 소분류 |
setValue |
값 자동 설정 |
거래처 선택 → 담당자 자동 입력 |
disable |
조건부 비활성화 |
상태=확정 → 수량 수정 불가 |
✅ 확정: 복잡한 계산식(견적 할인율 등)은 백엔드에서 전부 처리하여 결과만 전달
규칙 14: 권한 통합
14-1. 현재 권한 시스템 검증 결과
✅ 현재 권한 시스템으로 동적 페이지도 컨트롤 가능 (검증 완료)
현재 권한 시스템이 메뉴 ID 기반 + URL 패턴 매칭으로 동작하므로, 페이지가 정적이든 동적이든 해당 URL이 menu 테이블에 등록되어 있으면 권한 관리 페이지에서 동일하게 컨트롤됩니다.
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. 권한 적용 흐름
⚠️ 백엔드 논의 필요: 동적 페이지 URL(slug)을 menu 테이블에 자동 등록하는 방안
(기준관리에서 페이지 생성 시 → menu 테이블에도 자동 연동?)
규칙 15: 캐싱 & 성능 전략
| 전략 |
방법 |
갱신 주기 |
| 초기 로드 |
로그인 시 전체 config 프리페치 |
1회 |
| 변경 감지 |
해시 비교 (메뉴 갱신과 동일) |
30초~5분 |
| 강제 갱신 |
관리자가 기준관리 변경 시 push |
즉시 |
| 캐시 무효화 |
테넌트 전환 시 전체 클리어 |
즉시 |
규칙 16: 비즈니스 로직 처리
✅ 확정: 복잡한 계산 수식은 백엔드에서 전부 처리하여 결과만 전달
| 로직 복잡도 |
처리 방식 |
예시 |
| 단순 계산 |
config formula (프론트) |
수량 × 단가 = 금액 |
| 복잡한 계산 |
백엔드 API |
견적 할인, 세금, 재고 검증 등 |
프론트는 단순 사칙연산(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 |
기존 정적 → 동적 완전 전환 |
지속적 |
⏳ |
|
- 남은 하드코딩 페이지 점진적 전환 |
|
|
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] |
동적 필드 타입 백엔드 요청서 |
문서 버전: 1.2
마지막 업데이트: 2026-03-11
다음 단계: 백엔드 회의 → 협의 필요 항목 확정 → v2.0 작성 → sam-docs/frontend/v2/에 최종본 등록