feature/v2-unified-renewal #379

Merged
kjs merged 145 commits from feature/v2-unified-renewal into main 2026-02-03 12:11:19 +09:00
1 changed files with 242 additions and 26 deletions
Showing only changes of commit e0ee375f01 - Show all commits

View File

@ -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 지원