# 화면관리 시스템 설계문서 ## 1. 개요 ### 1.1 목적 ERP 시스템에서 사용자가 직관적인 드래그앤드롭 인터페이스를 통해 동적으로 화면을 설계하고 관리할 수 있는 시스템 ### 1.2 주요 기능 - 드래그앤드롭 기반 화면 설계 - 실시간 미리보기 및 속성 편집 - 다양한 위젯 타입 지원 - 데이터베이스 테이블/컬럼과의 연동 - 템플릿 기반 빠른 화면 생성 - 스타일 및 레이아웃 커스터마이징 ## 2. 시스템 아키텍처 ### 2.1 전체 구조 ``` 화면 편집기 (ScreenDesigner) ├── 템플릿 패널 (TemplatesPanel) ├── 테이블 패널 (TablesPanel) ├── 속성 편집 패널 (PropertiesPanel) ├── 스타일 편집 패널 (StyleEditor) ├── 상세설정 패널 (DetailSettingsPanel) ├── 격자 설정 패널 (GridPanel) └── 캔버스 영역 (RealtimePreview) 할당된 화면 (InteractiveScreenViewer) ├── 라벨 렌더링 ├── 위젯 렌더링 └── 폼 데이터 관리 ``` ### 2.2 데이터 흐름 ``` 사용자 입력 → 컴포넌트 상태 → 레이아웃 데이터 → API 저장/불러오기 → 할당된 화면 렌더링 ``` ## 3. 컴포넌트 구조 ### 3.1 컴포넌트 타입 ```typescript type ComponentType = "container" | "widget" | "group" | "datatable"; interface BaseComponent { id: string; type: ComponentType; position: { x: number; y: number; z?: number }; size: { width: number; height: number }; parentId?: string; label?: string; required?: boolean; readonly?: boolean; style?: ComponentStyle; } interface WidgetComponent extends BaseComponent { type: "widget"; widgetType: WebType; placeholder?: string; columnName?: string; webTypeConfig?: WebTypeConfig; } ``` ### 3.2 지원하는 웹 타입 - **텍스트 입력**: text, email, tel - **숫자 입력**: number, decimal - **날짜/시간**: date, datetime - **선택**: select, dropdown, radio - **체크박스**: checkbox, boolean - **텍스트 영역**: textarea - **파일**: file - **코드**: code - **엔티티**: entity - **버튼**: button ## 4. 주요 기능 상세 ### 4.1 드래그앤드롭 시스템 #### 템플릿 드래그 - 사전 정의된 템플릿을 캔버스에 드롭 - 컨테이너와 자식 컴포넌트 관계 자동 설정 - 격자 스냅 및 자동 크기 조정 #### 컬럼 드래그 - 데이터베이스 테이블의 컬럼을 위젯으로 변환 - 컬럼 타입에 따른 자동 웹타입 매핑 - 폼 컨테이너에 드롭 시 자동 부모-자식 관계 설정 #### 다중 컴포넌트 드래그 - Ctrl/Cmd + 클릭으로 다중 선택 - 선택된 모든 컴포넌트 동시 이동 - 실시간 미리보기 제공 ### 4.2 속성 편집 시스템 #### 실시간 속성 편집 패턴 ```typescript // 로컬 상태 기반 즉시 반영 const [localInputs, setLocalInputs] = useState({ title: component.title || "", placeholder: component.placeholder || "", }); // 입력과 동시에 업데이트 { const newValue = e.target.value; setLocalInputs(prev => ({ ...prev, title: newValue })); onUpdateProperty("title", newValue); }} /> ``` #### 컴포넌트별 개별 상태 관리 ```typescript // 동적 ID 기반 상태 관리 const [localColumnInputs, setLocalColumnInputs] = useState>({}); // 컴포넌트 변경 시 기존 값 보존하면서 새 항목만 추가 useEffect(() => { setLocalColumnInputs((prev) => { const newInputs = { ...prev }; component.columns?.forEach((col) => { if (!(col.id in newInputs)) { newInputs[col.id] = col.label; } }); return newInputs; }); }, [component.columns]); ``` ### 4.3 스타일 시스템 #### 스타일 적용 계층 1. **컴포넌트 기본 스타일**: `component.style` 2. **웹타입 설정 스타일**: `webTypeConfig`에서 정의 3. **라벨 스타일**: 별도 관리 (`labelColor`, `labelFontSize` 등) #### 스타일 패널 구성 - **여백**: margin, padding, gap - **테두리**: borderWidth, borderStyle, borderColor, borderRadius - **배경**: backgroundColor, backgroundImage - **텍스트**: color, fontSize, fontWeight, textAlign ### 4.4 템플릿 시스템 #### 데이터 테이블 템플릿 ```typescript { id: "data-table", name: "데이터 테이블", category: "table", components: [ { id: "table-container", type: "datatable", searchFilters: [], columns: [ { id: "col1", label: "컬럼 1", visible: true, sortable: true }, { id: "col2", label: "컬럼 2", visible: true, sortable: false } ], pagination: { enabled: true, pageSize: 10 }, actions: { create: { enabled: true, label: "추가" }, edit: { enabled: true, label: "수정" }, delete: { enabled: true, label: "삭제" } } } ] } ``` #### 입력 폼 템플릿 ```typescript { id: "input-form", name: "입력 폼", category: "form", components: [ { id: "form-container", type: "container", style: { backgroundColor: "#f8f9fa", borderRadius: "8px" }, children: [ { id: "save-button", type: "widget", widgetType: "button", parentId: "form-container", position: { x: 0, y: 0 }, style: { position: "absolute", bottom: "24px", right: "104px" } }, { id: "cancel-button", type: "widget", widgetType: "button", parentId: "form-container", position: { x: 0, y: 0 }, style: { position: "absolute", bottom: "24px", right: "24px" } } ] } ] } ``` #### 범용 버튼 템플릿 ```typescript { id: "universal-button", name: "버튼", category: "button", components: [ { id: "button", type: "widget", widgetType: "button", webTypeConfig: { actionType: "save", variant: "default", size: "sm" } } ] } ``` ### 4.5 웹타입별 상세 설정 #### 버튼 설정 (ButtonConfigPanel) ```typescript interface ButtonTypeConfig { actionType: ButtonActionType; variant: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"; size: "default" | "sm" | "lg" | "icon"; icon?: string; confirmMessage?: string; popupTitle?: string; popupContent?: string; popupSize?: "sm" | "md" | "lg"; navigateUrl?: string; navigateTarget?: "_self" | "_blank"; customAction?: string; backgroundColor?: string; textColor?: string; borderColor?: string; } type ButtonActionType = | "save" | "cancel" | "delete" | "edit" | "add" | "search" | "reset" | "submit" | "close" | "popup" | "navigate" | "custom"; ``` #### 텍스트 설정 (TextTypeConfig) ```typescript interface TextTypeConfig { format: "none" | "korean" | "english" | "alphanumeric" | "numeric" | "email" | "phone" | "url"; minLength?: number; maxLength?: number; pattern?: string; placeholder?: string; multiline?: boolean; } ``` #### 숫자 설정 (NumberTypeConfig) ```typescript interface NumberTypeConfig { min?: number; max?: number; step?: number; format?: "integer" | "decimal" | "currency" | "percentage"; decimalPlaces?: number; thousandSeparator?: boolean; } ``` #### 날짜 설정 (DateTypeConfig) ```typescript interface DateTypeConfig { format: "YYYY-MM-DD" | "YYYY-MM-DD HH:mm" | "YYYY-MM-DD HH:mm:ss"; showTime: boolean; minDate?: string; maxDate?: string; defaultValue?: string; } ``` #### 선택박스 설정 (SelectTypeConfig) ```typescript interface SelectTypeConfig { options: Array<{ label: string; value: string }>; multiple?: boolean; searchable?: boolean; placeholder?: string; } ``` #### 엔티티 설정 (EntityTypeConfig) ```typescript interface EntityTypeConfig { entityName: string; displayField: string; valueField: string; filters: Array<{ field: string; operator: string; value: any }>; multiple: boolean; searchable: boolean; allowClear: boolean; placeholder?: string; displayFormat?: string; defaultValue?: any; } ``` ### 4.6 격자 시스템 #### 격자 설정 ```typescript interface GridSettings { columns: number; // 격자 컬럼 수 gap: number; // 격자 간격 (px) padding: number; // 캔버스 패딩 (px) snapToGrid: boolean; // 격자 스냅 활성화 showGrid: boolean; // 격자 표시 여부 gridColor: string; // 격자 선 색상 gridOpacity: number; // 격자 투명도 (0-1) } ``` #### 격자 스냅 로직 ```typescript const snapToGrid = (value: number, gridSize: number): number => { return Math.round(value / gridSize) * gridSize; }; const snapSizeToGrid = (size: number, gridSize: number): number => { return Math.max(gridSize, Math.round(size / gridSize) * gridSize); }; ``` ## 5. 렌더링 시스템 ### 5.1 편집기 렌더링 (ScreenDesigner) #### 컴포넌트 위치 계산 ```typescript // 절대 위치 래퍼 div
``` #### 자식 컴포넌트 상대 위치 ```typescript // 자식 컴포넌트는 부모 기준 상대 위치로 계산 const relativePosition = { x: child.position.x - parent.position.x, y: child.position.y - parent.position.y, }; ``` ### 5.2 할당된 화면 렌더링 (InteractiveScreenViewer) #### 라벨 외부 분리 렌더링 ```typescript // 라벨을 컴포넌트 외부에 별도 렌더링 (높이에 영향 없음) {shouldShowLabel && (
{labelText} {component.required && *}
)} // 실제 컴포넌트 (라벨 높이에 영향받지 않음)
``` #### 스타일 적용 시스템 ```typescript const applyStyles = (element: React.ReactElement) => { if (!comp.style) return element; return React.cloneElement(element, { style: { ...element.props.style, // 기존 스타일 유지 ...comp.style, // 컴포넌트 스타일 적용 width: "100%", // 부모 컨테이너에 맞춤 height: "100%", minHeight: "100%", // 강제 높이 적용 maxHeight: "100%", boxSizing: "border-box", }, }); }; ``` #### 위젯별 렌더링 ```typescript switch (widgetType) { case "text": case "email": case "tel": return applyStyles( ); case "button": const config = widget.webTypeConfig as ButtonTypeConfig; return ( ); case "entity": return ( ); } ``` ## 6. 상태 관리 ### 6.1 레이아웃 상태 ```typescript interface LayoutData { components: ComponentData[]; gridSettings: GridSettings; } const [layout, setLayout] = useState({ components: [], gridSettings: defaultGridSettings, }); ``` ### 6.2 선택 상태 ```typescript const [selectedComponent, setSelectedComponent] = useState(null); const [selectedComponents, setSelectedComponents] = useState([]); ``` ### 6.3 드래그 상태 ```typescript interface DragState { isDragging: boolean; draggedComponents: ComponentData[]; startPosition: { x: number; y: number }; currentPosition: { x: number; y: number }; } ``` ### 6.4 패널 상태 ```typescript interface PanelState { isOpen: boolean; position: { x: number; y: number }; size: { width: number; height: number }; } const [panelStates, setPanelStates] = useState>({ templates: { isOpen: true, position: { x: 0, y: 0 }, size: { width: 300, height: 400 } }, properties: { isOpen: false, position: { x: 0, y: 0 }, size: { width: 360, height: 600 } }, styles: { isOpen: false, position: { x: 0, y: 0 }, size: { width: 360, height: 400 } }, // ... }); ``` ## 7. API 연동 ### 7.1 화면 정보 API ```typescript // 화면 목록 조회 GET /api/screens Response: { screens: Array<{ id: number; name: string; description: string; createdAt: string; updatedAt: string; }> } // 화면 상세 조회 GET /api/screens/:id Response: { id: number; name: string; description: string; layout: LayoutData; } // 화면 저장 POST /api/screens/:id/layout Request: { layout: LayoutData } ``` ### 7.2 테이블 정보 API ```typescript // 테이블 목록 조회 GET / api / tables; Response: { tables: Array<{ id: string; name: string; description: string; columns: Array<{ id: string; name: string; type: string; nullable: boolean; primaryKey: boolean; }>; }>; } ``` ## 8. 성능 최적화 ### 8.1 렌더링 최적화 - `useCallback`으로 이벤트 핸들러 메모이제이션 - `useMemo`로 계산 비용이 큰 값 캐싱 - 컴포넌트 분할을 통한 불필요한 리렌더링 방지 ### 8.2 상태 최적화 - 로컬 상태 기반 즉시 반영으로 UI 응답성 향상 - 디바운싱을 통한 과도한 API 호출 방지 - 컴포넌트별 개별 상태 관리로 전역 상태 오염 방지 ## 9. 개발 가이드 ### 9.1 새로운 웹타입 추가 (간편한 3단계) 새로운 웹타입 추가가 대폭 간편해졌습니다! 이제 **데이터베이스 기반 동적 웹타입 시스템**을 사용합니다. #### **1단계: 데이터베이스에 웹타입 등록** ```sql -- web_type_standard 테이블에 새 웹타입 추가 INSERT INTO web_type_standard ( web_type, type_name, config_panel, active ) VALUES ( 'my_new_type', -- 웹타입 코드 (영문) '새로운 입력 타입', -- 한글 표시명 'MyNewTypeConfigPanel', -- 설정 패널 컴포넌트명 (선택사항) 'Y' -- 활성화 여부 ); ``` #### **2단계: 설정 패널 컴포넌트 생성 (선택사항)** 웹타입에 특별한 설정이 필요한 경우만 생성: ```typescript // frontend/components/screen/config-panels/MyNewTypeConfigPanel.tsx export const MyNewTypeConfigPanel = ({ config, onConfigChange }) => { return (

새로운 타입 설정

{/* 설정 UI */}
onConfigChange({ ...config, option1: e.target.value })} />
); }; ``` #### **3단계: 레지스트리에 등록** ```typescript // frontend/lib/utils/availableConfigPanels.ts import { MyNewTypeConfigPanel } from "@/components/screen/config-panels/MyNewTypeConfigPanel"; export const availableConfigPanels = { // 기존 패널들... ButtonConfigPanel, TextTypeConfigPanel, NumberTypeConfigPanel, // 새 패널 추가 MyNewTypeConfigPanel, // ← 이 한 줄만 추가! }; ``` #### **완료! 🎉** - ✅ **PropertiesPanel**: 자동으로 드롭다운에 "새로운 입력 타입" 표시 - ✅ **DetailSettingsPanel**: 위젯 타입 변경 시 자동으로 설정 패널 표시 - ✅ **실시간 업데이트**: React key props로 즉시 반영 #### **설정 패널이 없는 경우** config_panel을 NULL로 설정하거나 레지스트리에 등록하지 않으면 "기본 설정" 메시지가 표시됩니다. ```sql -- 설정 패널 없는 간단한 웹타입 INSERT INTO web_type_standard (web_type, type_name, active) VALUES ('simple_type', '간단한 타입', 'Y'); ``` #### **렌더링 로직 추가 (필요한 경우)** 새 웹타입이 특별한 렌더링이 필요한 경우에만 추가: ```typescript // RealtimePreviewDynamic.tsx 또는 InteractiveScreenViewer.tsx case "my_new_type": return ( ); ``` #### **타입 정의 추가 (TypeScript 지원)** ```typescript // types/screen.ts export type WebType = | "text" | "number" | "date" | "my_new_type" // ← 새 타입 추가 | /* 기타 타입들 */; export interface MyNewTypeConfig { option1?: string; option2?: "default" | "custom"; // 기타 설정 옵션들 } export type WebTypeConfig = | TextTypeConfig | NumberTypeConfig | MyNewTypeConfig // ← 새 설정 타입 추가 | /* 기타 설정 타입들 */; ``` ### 🎯 **핵심 장점** - **플러그 앤 플레이**: 코드 수정 최소화 - **데이터베이스 기반**: 개발자 도구나 어드민에서 웹타입 관리 가능 - **자동 감지**: 별도 등록 로직 없이 자동으로 시스템에 반영 - **실시간 업데이트**: React key props로 즉시 설정 변경 반영 ### 9.2 새로운 템플릿 추가 1. **TemplatesPanel에 템플릿 정의 추가** ```typescript const templates: TemplateComponent[] = [ { id: "새로운-템플릿", name: "새로운 템플릿", category: "카테고리", icon: , defaultSize: { width: 400, height: 300 }, components: [ // 템플릿 구성 컴포넌트들 ] } ]; ``` 2. **필요한 경우 특별한 렌더링 로직 추가** ### 9.3 코딩 컨벤션 #### 실시간 속성 편집 패턴 (필수) ```typescript // 1. 로컬 상태 정의 const [localInputs, setLocalInputs] = useState({ title: component.title || "", }); // 2. 컴포넌트 변경 시 동기화 useEffect(() => { setLocalInputs({ title: component.title || "", }); }, [component.title]); // 3. 실시간 입력 처리 { const newValue = e.target.value; setLocalInputs(prev => ({ ...prev, title: newValue })); onUpdateProperty("title", newValue); }} /> ``` ## 10. 테스트 전략 ### 10.1 단위 테스트 - 유틸리티 함수 (격자 계산, 스타일 적용 등) - 컴포넌트 상태 관리 로직 - 데이터 변환 함수 ### 10.2 통합 테스트 - 드래그앤드롭 시나리오 - 속성 편집 플로우 - API 연동 테스트 ### 10.3 E2E 테스트 - 화면 생성부터 렌더링까지 전체 플로우 - 복잡한 사용자 시나리오 ## 11. 향후 개선 계획 ### 11.1 단기 계획 - 웹타입별 상세 설정 완성 (Date, Number, Select, Radio, File, Code, Entity) - 조건부 표시 기능 (특정 조건에 따른 컴포넌트 표시/숨김) - 계산 필드 기능 (다른 필드 값을 기반으로 한 자동 계산) ### 11.2 중장기 계획 - 컴포넌트 간 데이터 바인딩 - 워크플로우 연동 - 다국어 지원 - 반응형 디자인 - 컴포넌트 라이브러리 확장 ## 12. 트러블슈팅 ### 12.1 일반적인 문제 #### 높이가 적용되지 않는 문제 - **원인**: Tailwind CSS의 `h-full` 클래스가 인라인 스타일을 무시 - **해결**: `className="w-full"` + `style={{ height: "100%" }}` 사용 #### 라벨이 컴포넌트 높이에 포함되는 문제 - **원인**: 라벨과 위젯이 같은 컨테이너 내에 위치 - **해결**: 라벨을 외부에 별도 렌더링하여 높이에서 제외 #### 스타일이 할당된 화면에서 적용되지 않는 문제 - **원인**: `applyStyles` 함수 미사용 또는 잘못된 스타일 병합 - **해결**: 모든 위젯에서 일관된 스타일 적용 로직 사용 #### 다중 드래그 시 성능 문제 - **원인**: 과도한 리렌더링 - **해결**: `useCallback`, `useMemo` 적극 활용 ### 12.2 디버깅 팁 - 브라우저 개발자 도구의 React DevTools 활용 - 콘솔 로그를 통한 상태 추적 - 컴포넌트 트리 구조 시각화 --- _본 문서는 지속적으로 업데이트되며, 새로운 기능 추가 시 해당 섹션을 업데이트해야 합니다._