From 5cd97962b41936c4b422920131ddd7291fc93752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Wed, 11 Mar 2026 16:56:25 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20[frontend]=20v2=20=EB=8F=99=EC=A0=81=20?= =?UTF-8?q?=EB=A9=80=ED=8B=B0=ED=85=8C=EB=84=8C=ED=8A=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=84=A4?= =?UTF-8?q?=EA=B3=84=20=EC=B4=88=EC=95=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - v2/01-dynamic-multi-tenant-page-system.md 신규 작성 - _index.md에 v2 섹션 및 문서 목록 추가 - 빠른 참조 가이드에 v1/ 경로 접두사 반영 Co-Authored-By: Claude Opus 4.6 --- frontend/_index.md | 41 +- .../v2/01-dynamic-multi-tenant-page-system.md | 838 ++++++++++++++++++ 2 files changed, 867 insertions(+), 12 deletions(-) create mode 100644 frontend/v2/01-dynamic-multi-tenant-page-system.md diff --git a/frontend/_index.md b/frontend/_index.md index f05055b..468b4a2 100644 --- a/frontend/_index.md +++ b/frontend/_index.md @@ -2,7 +2,7 @@ > **프로젝트**: SAM ERP Next.js 프론트엔드 > **최종 갱신**: 2026-03-10 -> **현재 문서 버전**: v1 +> **현재 문서 버전**: v1 (운영 중) / v2 (설계 중) --- @@ -11,10 +11,12 @@ ``` frontend/ ├── _index.md ← 현재 문서 (목록 + 버전 관리) -├── v1/ ← 현재 활성 버전 +├── v1/ ← 현재 활성 버전 (운영 중) │ ├── 01 ~ 09 ← 프론트엔드 아키텍처/가이드 │ ├── 10 ← API 연동 스펙 │ └── 11 ← 브라우저 네비게이션 규칙 (AI/자동화) +├── v2/ ← 차기 버전 (동적 멀티테넌트, 설계 중) +│ └── 01 ← 동적 멀티테넌트 페이지 시스템 설계 └── api-specs/ ← (레거시, v1/10으로 이관됨) ``` @@ -36,6 +38,14 @@ frontend/ | 10 | [document-api-integration](v1/10-document-api-integration.md) | 1.0.0 | 2026-02-05 | API Team | FE/BE | 문서 관리 API 연동 (검사 성적서 resolve/upsert) | | 11 | [browser-navigation-rules](v1/11-browser-navigation-rules.md) | 1.0.0 | 2026-03-10 | Frontend | AI/QA | 브라우저 네비게이션 규칙 (URL 추측 금지, 메뉴 클릭 필수) | +### v2 — 동적 멀티테넌트 시스템 (설계 중) + +| # | 문서 | 버전 | 최종 수정 | 담당 | 대상 | 설명 | +|---|------|------|----------|------|------|------| +| 01 | [dynamic-multi-tenant-page-system](v2/01-dynamic-multi-tenant-page-system.md) | 1.1.0 | 2026-03-11 | FE/BE | 전체 | 동적 멀티테넌트 페이지 시스템 설계 (17개 규칙, JSON config, 동적 라우팅, 권한 통합) | + +> **v2 상태**: 초안 — 백엔드 회의 후 협의 항목 확정 예정 + ### 대상 범례 - **FE**: 프론트엔드 개발자 - **BE**: 백엔드 개발자 @@ -47,6 +57,12 @@ frontend/ ## 버전 변경 이력 +### v2 (2026-03-11 ~) + +| 날짜 | 문서 | 변경 | 버전 | +|------|------|------|------| +| 2026-03-11 | 01 | 동적 멀티테넌트 페이지 시스템 설계 초안 작성 | 1.1.0 | + ### v1 (2026-03-09 ~) | 날짜 | 문서 | 변경 | 버전 | @@ -88,13 +104,14 @@ PATCH: 오탈자, 코드 예시 수정, 사소한 수정 | 할 일 | 읽을 문서 | |-------|----------| -| 프로젝트 전체 구조 이해 | 01-architecture | -| API 호출 방법 알기 | 02-api-pattern | -| 새 리스트 페이지 만들기 | 03-component-design → 04-common-components | -| 새 폼 페이지 만들기 | 05-form-pattern | -| 디자인/스타일 규칙 확인 | 06-styling-guide | -| 인증 동작 이해 | 07-auth-flow | -| 대시보드 연동 작업 | 08-dashboard-system | -| 코딩 컨벤션 확인 | 09-conventions | -| 문서 관리 API 연동 | 10-document-api-integration | -| AI/E2E 페이지 이동 규칙 | 11-browser-navigation-rules | +| 프로젝트 전체 구조 이해 | v1/01-architecture | +| API 호출 방법 알기 | v1/02-api-pattern | +| 새 리스트 페이지 만들기 | v1/03-component-design → v1/04-common-components | +| 새 폼 페이지 만들기 | v1/05-form-pattern | +| 디자인/스타일 규칙 확인 | v1/06-styling-guide | +| 인증 동작 이해 | v1/07-auth-flow | +| 대시보드 연동 작업 | v1/08-dashboard-system | +| 코딩 컨벤션 확인 | v1/09-conventions | +| 문서 관리 API 연동 | v1/10-document-api-integration | +| AI/E2E 페이지 이동 규칙 | v1/11-browser-navigation-rules | +| **동적 멀티테넌트 설계** | **v2/01-dynamic-multi-tenant-page-system** | diff --git a/frontend/v2/01-dynamic-multi-tenant-page-system.md b/frontend/v2/01-dynamic-multi-tenant-page-system.md new file mode 100644 index 0000000..de87ff5 --- /dev/null +++ b/frontend/v2/01-dynamic-multi-tenant-page-system.md @@ -0,0 +1,838 @@ +# 동적 멀티테넌트 페이지 시스템 설계 + +> 작성일: 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 구조 (제안)**: + +```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 구조의 세부 스펙 확정 + +--- + +### 규칙 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 │ +│ 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" → + ├─ "detail" → + ├─ "form" → + ├─ "dashboard" → + ├─ "document" → + └─ 미지원 → (에러 표시) +``` + +--- + +### 규칙 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: 점진적 마이그레이션 전략 + +| 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로 이동해도 렌더링 컴포넌트 유지 | + +### 협의 필요 사항 + +| 항목 | 현재 상태 | 논의 포인트 | +|------|----------|------------| +| 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.1 +**마지막 업데이트**: 2026-03-11 +**다음 단계**: 백엔드 회의 → 협의 필요 항목 확정 → v2.0 작성 → `sam-docs/frontend/v2/`에 최종본 등록