docs: 컴포넌트 개발 가이드 업데이트 및 V2 + Zod 레이아웃 시스템 추가
- 컴포넌트 개발 가이드의 목차를 재구성하여 V2 + Zod 레이아웃 저장/로드 시스템에 대한 내용을 추가하였습니다. - V2 방식의 저장 및 로드 구조, 핵심 필드, 저장 및 로드 로직을 상세히 설명하였습니다. - 기존의 섹션 번호를 조정하여 새로운 내용이 자연스럽게 통합되도록 하였습니다. - Zod 스키마 구조와 관련 파일 목록을 추가하여 개발자들이 쉽게 참고할 수 있도록 하였습니다.
This commit is contained in:
parent
2fac9371c8
commit
e0ee375f01
|
|
@ -12,19 +12,20 @@ alwaysApply: false
|
|||
## 목차
|
||||
|
||||
1. [V2 컴포넌트 규칙 (최우선)](#1-v2-컴포넌트-규칙-최우선)
|
||||
2. [표준 Props 인터페이스](#2-표준-props-인터페이스)
|
||||
3. [멀티테넌시 (company_code)](#3-멀티테넌시-company_code)
|
||||
4. [디자인 모드 vs 인터랙티브 모드](#4-디자인-모드-vs-인터랙티브-모드)
|
||||
5. [로딩 및 에러 처리](#5-로딩-및-에러-처리)
|
||||
6. [테이블 컬럼 기반 입력 위젯](#6-테이블-컬럼-기반-입력-위젯)
|
||||
7. [컴포넌트별 테이블 설정](#7-컴포넌트별-테이블-설정)
|
||||
8. [엔티티 조인 컬럼 활용](#8-엔티티-조인-컬럼-활용)
|
||||
9. [폼 데이터 관리](#9-폼-데이터-관리)
|
||||
10. [다국어 지원](#10-다국어-지원)
|
||||
11. [저장 버튼 및 플로우 연동](#11-저장-버튼-및-플로우-연동)
|
||||
12. [표준 코드 스타일 가이드](#12-표준-코드-스타일-가이드)
|
||||
13. [성능 최적화](#13-성능-최적화)
|
||||
14. [체크리스트](#14-체크리스트)
|
||||
2. [V2 + Zod 레이아웃 저장/로드 시스템 (핵심)](#2-v2--zod-레이아웃-저장로드-시스템-핵심)
|
||||
3. [표준 Props 인터페이스](#3-표준-props-인터페이스)
|
||||
4. [멀티테넌시 (company_code)](#4-멀티테넌시-company_code)
|
||||
5. [디자인 모드 vs 인터랙티브 모드](#5-디자인-모드-vs-인터랙티브-모드)
|
||||
6. [로딩 및 에러 처리](#6-로딩-및-에러-처리)
|
||||
7. [테이블 컬럼 기반 입력 위젯](#7-테이블-컬럼-기반-입력-위젯)
|
||||
8. [컴포넌트별 테이블 설정](#8-컴포넌트별-테이블-설정)
|
||||
9. [엔티티 조인 컬럼 활용](#9-엔티티-조인-컬럼-활용)
|
||||
10. [폼 데이터 관리](#10-폼-데이터-관리)
|
||||
11. [다국어 지원](#11-다국어-지원)
|
||||
12. [저장 버튼 및 플로우 연동](#12-저장-버튼-및-플로우-연동)
|
||||
13. [표준 코드 스타일 가이드](#13-표준-코드-스타일-가이드)
|
||||
14. [성능 최적화](#14-성능-최적화)
|
||||
15. [체크리스트](#15-체크리스트)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -90,7 +91,212 @@ export const V2TableListDefinition = createComponentDefinition({
|
|||
|
||||
---
|
||||
|
||||
## 2. 표준 Props 인터페이스
|
||||
## 2. V2 + Zod 레이아웃 저장/로드 시스템 (핵심)
|
||||
|
||||
### 핵심 원칙
|
||||
|
||||
**컴포넌트 코드 수정 시 모든 화면에 자동 반영되도록 V2 + Zod 기반 저장/로드 방식을 사용합니다.**
|
||||
|
||||
```
|
||||
저장: component_url + overrides (차이값만)
|
||||
로드: Zod 기본값 + overrides 병합
|
||||
```
|
||||
|
||||
### 기존 방식 vs V2 방식
|
||||
|
||||
| 항목 | 기존 (문제점) | V2 (해결) |
|
||||
|-----|-------------|----------|
|
||||
| 저장 | 전체 설정 "박제" | url + overrides (차이값만) |
|
||||
| 코드 수정 반영 | 안 됨 | 자동 반영 |
|
||||
| 테이블 | `screen_layouts` (다중 레코드) | `screen_layouts_v2` (1 레코드) |
|
||||
|
||||
### 저장 구조 (screen_layouts_v2)
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.0",
|
||||
"components": [
|
||||
{
|
||||
"id": "comp_xxx",
|
||||
"url": "@/lib/registry/components/unified-select",
|
||||
"position": { "x": 100, "y": 50 },
|
||||
"size": { "width": 180, "height": 30 },
|
||||
"displayOrder": 0,
|
||||
"overrides": {
|
||||
"tableName": "warehouse_info",
|
||||
"columnName": "warehouse_code",
|
||||
"label": "창고코드",
|
||||
"webType": "select"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 핵심 필드: overrides에 반드시 포함되어야 하는 속성
|
||||
|
||||
컴포넌트가 테이블 컬럼에서 드래그되어 생성된 경우, 다음 속성들이 `overrides`에 저장됩니다:
|
||||
|
||||
| 속성 | 설명 | 예시 |
|
||||
|-----|------|-----|
|
||||
| `tableName` | 연결된 테이블명 | `"warehouse_info"` |
|
||||
| `columnName` | 연결된 컬럼명 | `"warehouse_code"` |
|
||||
| `label` | 표시 라벨 | `"창고코드"` |
|
||||
| `required` | 필수 입력 여부 | `true` / `false` |
|
||||
| `readonly` | 읽기 전용 여부 | `true` / `false` |
|
||||
| `inputType` | 입력 타입 | `"text"`, `"select"`, `"date"` 등 |
|
||||
| `webType` | 웹 타입 | `"text"`, `"select"`, `"date"` 등 |
|
||||
| `codeCategory` | 코드 카테고리 (코드 타입인 경우) | `"WAREHOUSE_TYPE"` |
|
||||
|
||||
### 저장 로직 (convertLegacyToV2)
|
||||
|
||||
```typescript
|
||||
// frontend/lib/utils/layoutV2Converter.ts
|
||||
|
||||
export function convertLegacyToV2(legacyLayout: LegacyLayoutData): LayoutV2 {
|
||||
const components = legacyLayout.components.map((comp, index) => {
|
||||
const componentType = comp.componentType || comp.widgetType || comp.type;
|
||||
const url = getComponentUrl(componentType);
|
||||
const defaults = getDefaultsByUrl(url);
|
||||
|
||||
// 상위 레벨 속성들도 overrides에 포함 (중요!)
|
||||
const topLevelProps: Record<string, any> = {};
|
||||
if (comp.tableName) topLevelProps.tableName = comp.tableName;
|
||||
if (comp.columnName) topLevelProps.columnName = comp.columnName;
|
||||
if (comp.label) topLevelProps.label = comp.label;
|
||||
if (comp.required !== undefined) topLevelProps.required = comp.required;
|
||||
if (comp.readonly !== undefined) topLevelProps.readonly = comp.readonly;
|
||||
|
||||
// componentConfig에서 차이값만 추출
|
||||
const configOverrides = extractCustomConfig(comp.componentConfig || {}, defaults);
|
||||
|
||||
// 병합
|
||||
const overrides = { ...topLevelProps, ...configOverrides };
|
||||
|
||||
return {
|
||||
id: comp.id,
|
||||
url: url,
|
||||
position: comp.position,
|
||||
size: comp.size,
|
||||
displayOrder: index,
|
||||
overrides: overrides,
|
||||
};
|
||||
});
|
||||
|
||||
return { version: "2.0", components };
|
||||
}
|
||||
```
|
||||
|
||||
### 로드 로직 (convertV2ToLegacy)
|
||||
|
||||
```typescript
|
||||
// frontend/lib/utils/layoutV2Converter.ts
|
||||
|
||||
export function convertV2ToLegacy(v2Layout: LayoutV2): LegacyLayoutData {
|
||||
const components = v2Layout.components.map((comp) => {
|
||||
const componentType = getComponentTypeFromUrl(comp.url);
|
||||
const defaults = getDefaultsByUrl(comp.url);
|
||||
const mergedConfig = mergeComponentConfig(defaults, comp.overrides);
|
||||
|
||||
// overrides에서 상위 레벨 속성들 복원
|
||||
const overrides = comp.overrides || {};
|
||||
|
||||
return {
|
||||
id: comp.id,
|
||||
componentType: componentType,
|
||||
position: comp.position,
|
||||
size: comp.size,
|
||||
componentConfig: mergedConfig,
|
||||
// 상위 레벨 속성 복원 (중요!)
|
||||
tableName: overrides.tableName,
|
||||
columnName: overrides.columnName,
|
||||
label: overrides.label || "",
|
||||
required: overrides.required,
|
||||
readonly: overrides.readonly,
|
||||
};
|
||||
});
|
||||
|
||||
return { components };
|
||||
}
|
||||
```
|
||||
|
||||
### Zod 스키마 구조
|
||||
|
||||
```typescript
|
||||
// frontend/lib/schemas/componentConfig.ts
|
||||
|
||||
// 컴포넌트별 overrides 스키마
|
||||
export const unifiedSelectOverridesSchema = z.object({
|
||||
mode: z.enum(["dropdown", "combobox", "radio", "checkbox"]).default("dropdown"),
|
||||
source: z.enum(["static", "code", "entity", "db", "distinct"]).default("distinct"),
|
||||
multiple: z.boolean().default(false),
|
||||
searchable: z.boolean().default(true),
|
||||
placeholder: z.string().default("선택하세요"),
|
||||
}).passthrough(); // 정의되지 않은 필드도 통과 (tableName, columnName 등)
|
||||
|
||||
// 스키마 레지스트리
|
||||
export const componentOverridesSchemaRegistry: Record<string, z.ZodType<any>> = {
|
||||
"unified-select": unifiedSelectOverridesSchema,
|
||||
"unified-input": unifiedInputOverridesSchema,
|
||||
"v2-table-list": v2TableListOverridesSchema,
|
||||
// ...
|
||||
};
|
||||
|
||||
// 기본값 레지스트리
|
||||
export const componentDefaultsRegistry: Record<string, any> = {
|
||||
"unified-select": {
|
||||
mode: "dropdown",
|
||||
source: "distinct", // 기본: 테이블 컬럼에서 자동 로드
|
||||
multiple: false,
|
||||
searchable: true,
|
||||
},
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
### unified-select 자동 옵션 로드
|
||||
|
||||
`webType`이 `"select"`인 컬럼을 드래그하면:
|
||||
|
||||
1. **저장 시**: `tableName`, `columnName`이 `overrides`에 저장됨
|
||||
2. **로드 시**: `source`가 `"distinct"`이면 자동으로 `/entity/{tableName}/distinct/{columnName}` API 호출
|
||||
3. **결과**: 해당 컬럼의 고유 값들이 옵션으로 표시됨
|
||||
|
||||
```typescript
|
||||
// DynamicComponentRenderer.tsx
|
||||
|
||||
case "unified-select":
|
||||
return (
|
||||
<UnifiedSelect
|
||||
{...commonProps}
|
||||
config={{
|
||||
mode: config.mode || "dropdown",
|
||||
source: config.source || "distinct", // 기본: 테이블에서 자동 로드
|
||||
// ...
|
||||
}}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
### 관련 파일
|
||||
|
||||
| 파일 | 역할 |
|
||||
|------|------|
|
||||
| `frontend/lib/schemas/componentConfig.ts` | Zod 스키마 및 기본값 레지스트리 |
|
||||
| `frontend/lib/utils/layoutV2Converter.ts` | V2 ↔ Legacy 변환 유틸리티 |
|
||||
| `frontend/lib/api/screen.ts` | `getLayoutV2`, `saveLayoutV2` API |
|
||||
| `backend-node/src/services/screenManagementService.ts` | 백엔드 저장/로드 로직 |
|
||||
|
||||
### 새 컴포넌트 추가 시 체크리스트
|
||||
|
||||
1. [ ] `componentConfig.ts`에 Zod 스키마 추가 (`.passthrough()` 필수)
|
||||
2. [ ] `componentOverridesSchemaRegistry`에 등록
|
||||
3. [ ] `componentDefaultsRegistry`에 기본값 등록
|
||||
4. [ ] 테이블 컬럼 드래그 시 `tableName`, `columnName` 저장 확인
|
||||
|
||||
---
|
||||
|
||||
## 3. 표준 Props 인터페이스
|
||||
|
||||
### 컴포넌트가 받아야 하는 표준 Props
|
||||
|
||||
|
|
@ -150,7 +356,7 @@ export const MyComponent: React.FC<StandardComponentProps> = ({
|
|||
|
||||
---
|
||||
|
||||
## 3. 멀티테넌시 (company_code)
|
||||
## 4. 멀티테넌시 (company_code)
|
||||
|
||||
### 핵심 원칙
|
||||
|
||||
|
|
@ -199,7 +405,7 @@ const response = await apiClient.post(`/table-management/tables/${tableName}/add
|
|||
|
||||
---
|
||||
|
||||
## 4. 디자인 모드 vs 인터랙티브 모드
|
||||
## 5. 디자인 모드 vs 인터랙티브 모드
|
||||
|
||||
### 모드 구분
|
||||
|
||||
|
|
@ -257,7 +463,7 @@ const handleClick = useCallback(() => {
|
|||
|
||||
---
|
||||
|
||||
## 5. 로딩 및 에러 처리
|
||||
## 6. 로딩 및 에러 처리
|
||||
|
||||
### 로딩 상태 관리
|
||||
|
||||
|
|
@ -378,7 +584,7 @@ if (!silentActions.includes(actionType)) {
|
|||
|
||||
---
|
||||
|
||||
## 6. 테이블 컬럼 기반 입력 위젯
|
||||
## 7. 테이블 컬럼 기반 입력 위젯
|
||||
|
||||
### 드래그 방식으로 입력 폼 생성
|
||||
|
||||
|
|
@ -492,7 +698,7 @@ const handleSave = async (context: ButtonActionContext) => {
|
|||
|
||||
---
|
||||
|
||||
## 7. 컴포넌트별 테이블 설정
|
||||
## 8. 컴포넌트별 테이블 설정
|
||||
|
||||
### 핵심 원칙
|
||||
|
||||
|
|
@ -606,7 +812,7 @@ const response = await apiClient.get(
|
|||
|
||||
---
|
||||
|
||||
## 8. 엔티티 조인 컬럼 활용
|
||||
## 9. 엔티티 조인 컬럼 활용
|
||||
|
||||
### 핵심 원칙
|
||||
|
||||
|
|
@ -712,7 +918,7 @@ const getEntityJoinValue = (item: any, columnName: string): any => {
|
|||
|
||||
---
|
||||
|
||||
## 9. 폼 데이터 관리
|
||||
## 10. 폼 데이터 관리
|
||||
|
||||
### 통합 폼 시스템 (UnifiedFormContext)
|
||||
|
||||
|
|
@ -756,7 +962,7 @@ const handleChange = useCallback((value: any) => {
|
|||
|
||||
---
|
||||
|
||||
## 10. 다국어 지원
|
||||
## 11. 다국어 지원
|
||||
|
||||
### 타입 정의 시 다국어 필드 추가
|
||||
|
||||
|
|
@ -824,7 +1030,7 @@ const MyComponent = ({ component }) => {
|
|||
|
||||
---
|
||||
|
||||
## 11. 저장 버튼 및 플로우 연동
|
||||
## 12. 저장 버튼 및 플로우 연동
|
||||
|
||||
### beforeFormSave 이벤트 처리 (필수)
|
||||
|
||||
|
|
@ -912,7 +1118,7 @@ const MyFormComponent = ({ formData, onFormDataChange }) => {
|
|||
|
||||
---
|
||||
|
||||
## 12. 표준 코드 스타일 가이드
|
||||
## 13. 표준 코드 스타일 가이드
|
||||
|
||||
**`v2-unified-repeater`** 컴포넌트를 표준으로 삼아 동일한 구조로 작성합니다.
|
||||
|
||||
|
|
@ -1276,7 +1482,7 @@ const tableName = config?.dataSource?.tableName;
|
|||
|
||||
---
|
||||
|
||||
## 13. 성능 최적화
|
||||
## 14. 성능 최적화
|
||||
|
||||
### useMemo로 계산 비용 줄이기
|
||||
|
||||
|
|
@ -1452,7 +1658,7 @@ const derivedValue = useMemo(() => data.map(x => x.value), [data]);
|
|||
|
||||
---
|
||||
|
||||
## 14. 체크리스트
|
||||
## 15. 체크리스트
|
||||
|
||||
### V2 컴포넌트 규칙
|
||||
|
||||
|
|
@ -1461,6 +1667,16 @@ const derivedValue = useMemo(() => data.map(x => x.value), [data]);
|
|||
- [ ] Definition 이름에 `V2` 접두사 사용
|
||||
- [ ] 원본 폴더 수정하지 않음
|
||||
|
||||
### V2 + Zod 레이아웃 시스템
|
||||
|
||||
- [ ] `componentConfig.ts`에 Zod 스키마 추가 (`.passthrough()` 필수)
|
||||
- [ ] `componentOverridesSchemaRegistry`에 컴포넌트 등록
|
||||
- [ ] `componentDefaultsRegistry`에 기본값 등록
|
||||
- [ ] 테이블 컬럼 드래그 시 `tableName`, `columnName` 저장 확인
|
||||
- [ ] `convertLegacyToV2`에서 상위 레벨 속성 포함 확인
|
||||
- [ ] `convertV2ToLegacy`에서 상위 레벨 속성 복원 확인
|
||||
- [ ] unified-select는 `source: "distinct"` 기본값 확인
|
||||
|
||||
### 표준 Props
|
||||
|
||||
- [ ] `component`, `isDesignMode` props 지원
|
||||
|
|
|
|||
Loading…
Reference in New Issue