901 lines
21 KiB
Markdown
901 lines
21 KiB
Markdown
# 화면관리 시스템 설계문서
|
|
|
|
## 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 || "",
|
|
});
|
|
|
|
// 입력과 동시에 업데이트
|
|
<Input
|
|
value={localInputs.title}
|
|
onChange={(e) => {
|
|
const newValue = e.target.value;
|
|
setLocalInputs(prev => ({ ...prev, title: newValue }));
|
|
onUpdateProperty("title", newValue);
|
|
}}
|
|
/>
|
|
```
|
|
|
|
#### 컴포넌트별 개별 상태 관리
|
|
|
|
```typescript
|
|
// 동적 ID 기반 상태 관리
|
|
const [localColumnInputs, setLocalColumnInputs] = useState<Record<string, string>>({});
|
|
|
|
// 컴포넌트 변경 시 기존 값 보존하면서 새 항목만 추가
|
|
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
|
|
<div
|
|
className="absolute"
|
|
style={{
|
|
left: `${displayComponent.position.x}px`,
|
|
top: `${displayComponent.position.y}px`,
|
|
width: displayComponent.style?.width || `${displayComponent.size.width}px`,
|
|
height: displayComponent.style?.height || `${displayComponent.size.height}px`,
|
|
zIndex: displayComponent.position.z || 1,
|
|
}}
|
|
>
|
|
<RealtimePreview
|
|
component={displayComponent}
|
|
position={{ x: 0, y: 0 }} // 래퍼 div 내에서는 상대 위치
|
|
/>
|
|
</div>
|
|
```
|
|
|
|
#### 자식 컴포넌트 상대 위치
|
|
|
|
```typescript
|
|
// 자식 컴포넌트는 부모 기준 상대 위치로 계산
|
|
const relativePosition = {
|
|
x: child.position.x - parent.position.x,
|
|
y: child.position.y - parent.position.y,
|
|
};
|
|
```
|
|
|
|
### 5.2 할당된 화면 렌더링 (InteractiveScreenViewer)
|
|
|
|
#### 라벨 외부 분리 렌더링
|
|
|
|
```typescript
|
|
// 라벨을 컴포넌트 외부에 별도 렌더링 (높이에 영향 없음)
|
|
{shouldShowLabel && (
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
left: `${component.position.x}px`,
|
|
top: `${component.position.y - 25}px`, // 컴포넌트 위쪽에 배치
|
|
zIndex: (component.position.z || 1) + 1,
|
|
...labelStyle,
|
|
}}
|
|
>
|
|
{labelText}
|
|
{component.required && <span style={{ color: "#f97316" }}>*</span>}
|
|
</div>
|
|
)}
|
|
|
|
// 실제 컴포넌트 (라벨 높이에 영향받지 않음)
|
|
<div style={{ height: component.style?.height || `${component.size.height}px` }}>
|
|
<InteractiveScreenViewer component={component} hideLabel={true} />
|
|
</div>
|
|
```
|
|
|
|
#### 스타일 적용 시스템
|
|
|
|
```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(
|
|
<Input
|
|
type={inputType}
|
|
placeholder={finalPlaceholder}
|
|
value={currentValue}
|
|
onChange={handleInputChange}
|
|
className="w-full"
|
|
style={{ height: "100%" }}
|
|
/>
|
|
);
|
|
|
|
case "button":
|
|
const config = widget.webTypeConfig as ButtonTypeConfig;
|
|
return (
|
|
<Button
|
|
onClick={handleButtonClick}
|
|
size={config?.size || "sm"}
|
|
variant={config?.variant || "default"}
|
|
className="w-full"
|
|
style={{
|
|
...comp.style,
|
|
height: "100%",
|
|
backgroundColor: config?.backgroundColor,
|
|
color: config?.textColor,
|
|
borderColor: config?.borderColor,
|
|
}}
|
|
>
|
|
{label || "버튼"}
|
|
</Button>
|
|
);
|
|
|
|
case "entity":
|
|
return (
|
|
<Select>
|
|
<SelectTrigger
|
|
className="w-full"
|
|
style={{
|
|
...comp.style,
|
|
height: "100%",
|
|
}}
|
|
>
|
|
<SelectValue placeholder={finalPlaceholder} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{options.map(option => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
);
|
|
}
|
|
```
|
|
|
|
## 6. 상태 관리
|
|
|
|
### 6.1 레이아웃 상태
|
|
|
|
```typescript
|
|
interface LayoutData {
|
|
components: ComponentData[];
|
|
gridSettings: GridSettings;
|
|
}
|
|
|
|
const [layout, setLayout] = useState<LayoutData>({
|
|
components: [],
|
|
gridSettings: defaultGridSettings,
|
|
});
|
|
```
|
|
|
|
### 6.2 선택 상태
|
|
|
|
```typescript
|
|
const [selectedComponent, setSelectedComponent] = useState<ComponentData | null>(null);
|
|
const [selectedComponents, setSelectedComponents] = useState<ComponentData[]>([]);
|
|
```
|
|
|
|
### 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<Record<string, PanelState>>({
|
|
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 (
|
|
<div className="space-y-4">
|
|
<h3 className="text-sm font-medium">새로운 타입 설정</h3>
|
|
|
|
{/* 설정 UI */}
|
|
<div>
|
|
<label className="text-sm">옵션 1</label>
|
|
<input
|
|
value={config.option1 || ""}
|
|
onChange={(e) => onConfigChange({ ...config, option1: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-sm">옵션 2</label>
|
|
<select
|
|
value={config.option2 || "default"}
|
|
onChange={(e) => onConfigChange({ ...config, option2: e.target.value })}
|
|
>
|
|
<option value="default">기본값</option>
|
|
<option value="custom">사용자 정의</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
#### **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 (
|
|
<MyNewInputComponent
|
|
value={currentValue}
|
|
onChange={handleInputChange}
|
|
placeholder={finalPlaceholder}
|
|
config={widget.webTypeConfig}
|
|
style={{ height: "100%" }}
|
|
/>
|
|
);
|
|
```
|
|
|
|
#### **타입 정의 추가 (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: <IconComponent />,
|
|
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. 실시간 입력 처리
|
|
<Input
|
|
value={localInputs.title}
|
|
onChange={(e) => {
|
|
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 활용
|
|
- 콘솔 로그를 통한 상태 추적
|
|
- 컴포넌트 트리 구조 시각화
|
|
|
|
---
|
|
|
|
_본 문서는 지속적으로 업데이트되며, 새로운 기능 추가 시 해당 섹션을 업데이트해야 합니다._
|