diff --git a/.cursor/rules/component-development-guide.mdc b/.cursor/rules/component-development-guide.mdc index 4a18e80a..91611e51 100644 --- a/.cursor/rules/component-development-guide.mdc +++ b/.cursor/rules/component-development-guide.mdc @@ -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 = {}; + 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> = { + "unified-select": unifiedSelectOverridesSchema, + "unified-input": unifiedInputOverridesSchema, + "v2-table-list": v2TableListOverridesSchema, + // ... +}; + +// 기본값 레지스트리 +export const componentDefaultsRegistry: Record = { + "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 ( + + ); +``` + +### 관련 파일 + +| 파일 | 역할 | +|------|------| +| `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 = ({ --- -## 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 지원