796 lines
22 KiB
Markdown
796 lines
22 KiB
Markdown
|
|
# 화면관리 시스템 컴포넌트 개발 가이드
|
||
|
|
|
||
|
|
화면관리 시스템에서 새로운 컴포넌트, 템플릿, 웹타입을 추가하는 완전한 가이드입니다.
|
||
|
|
|
||
|
|
## 🎯 목차
|
||
|
|
|
||
|
|
1. [컴포넌트 추가하기](#1-컴포넌트-추가하기)
|
||
|
|
2. [웹타입 추가하기](#2-웹타입-추가하기)
|
||
|
|
3. [템플릿 추가하기](#3-템플릿-추가하기)
|
||
|
|
4. [설정 패널 개발](#4-설정-패널-개발)
|
||
|
|
5. [데이터베이스 설정](#5-데이터베이스-설정)
|
||
|
|
6. [테스트 및 검증](#6-테스트-및-검증)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. 컴포넌트 추가하기
|
||
|
|
|
||
|
|
### 1.1 컴포넌트 렌더러 생성
|
||
|
|
|
||
|
|
새로운 컴포넌트 렌더러를 생성합니다.
|
||
|
|
|
||
|
|
**파일 위치**: `frontend/lib/registry/components/{ComponentName}Renderer.tsx`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 예시: AlertRenderer.tsx
|
||
|
|
import React from "react";
|
||
|
|
import { ComponentRenderer } from "../DynamicComponentRenderer";
|
||
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||
|
|
import { AlertTriangle, Info, CheckCircle, XCircle } from "lucide-react";
|
||
|
|
|
||
|
|
const AlertRenderer: ComponentRenderer = ({
|
||
|
|
component,
|
||
|
|
children,
|
||
|
|
isInteractive,
|
||
|
|
...props
|
||
|
|
}) => {
|
||
|
|
const config = component.componentConfig || {};
|
||
|
|
const {
|
||
|
|
title = "알림",
|
||
|
|
message = "알림 메시지입니다.",
|
||
|
|
type = "info", // info, warning, success, error
|
||
|
|
showIcon = true,
|
||
|
|
style = {}
|
||
|
|
} = config;
|
||
|
|
|
||
|
|
// 타입별 아이콘 매핑
|
||
|
|
const iconMap = {
|
||
|
|
info: Info,
|
||
|
|
warning: AlertTriangle,
|
||
|
|
success: CheckCircle,
|
||
|
|
error: XCircle,
|
||
|
|
};
|
||
|
|
|
||
|
|
const Icon = iconMap[type as keyof typeof iconMap] || Info;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Alert
|
||
|
|
className={`h-full w-full ${type === 'error' ? 'border-red-500' : ''}`}
|
||
|
|
style={style}
|
||
|
|
>
|
||
|
|
{showIcon && <Icon className="h-4 w-4" />}
|
||
|
|
<AlertTitle>{title}</AlertTitle>
|
||
|
|
<AlertDescription>
|
||
|
|
{isInteractive ? (
|
||
|
|
// 실제 할당된 화면에서는 설정된 메시지 표시
|
||
|
|
message
|
||
|
|
) : (
|
||
|
|
// 디자이너에서는 플레이스홀더 + children 표시
|
||
|
|
children && React.Children.count(children) > 0 ? (
|
||
|
|
children
|
||
|
|
) : (
|
||
|
|
<div className="text-sm">
|
||
|
|
<div>{message}</div>
|
||
|
|
<div className="mt-1 text-xs text-gray-400">
|
||
|
|
알림 컴포넌트 - {type} 타입
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
)}
|
||
|
|
</AlertDescription>
|
||
|
|
</Alert>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default AlertRenderer;
|
||
|
|
```
|
||
|
|
|
||
|
|
### 1.2 컴포넌트 등록
|
||
|
|
|
||
|
|
**파일**: `frontend/lib/registry/index.ts`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 컴포넌트 렌더러 import 추가
|
||
|
|
import AlertRenderer from "./components/AlertRenderer";
|
||
|
|
|
||
|
|
// 컴포넌트 레지스트리에 등록
|
||
|
|
export const registerComponents = () => {
|
||
|
|
// 기존 컴포넌트들...
|
||
|
|
ComponentRegistry.register("alert", AlertRenderer);
|
||
|
|
ComponentRegistry.register("alert-info", AlertRenderer);
|
||
|
|
ComponentRegistry.register("alert-warning", AlertRenderer);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 1.3 InteractiveScreenViewer에 등록
|
||
|
|
|
||
|
|
**파일**: `frontend/components/screen/InteractiveScreenViewerDynamic.tsx`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 컴포넌트 렌더러들을 강제로 로드하여 레지스트리에 등록
|
||
|
|
import "@/lib/registry/components/ButtonRenderer";
|
||
|
|
import "@/lib/registry/components/CardRenderer";
|
||
|
|
import "@/lib/registry/components/DashboardRenderer";
|
||
|
|
import "@/lib/registry/components/AlertRenderer"; // 추가
|
||
|
|
import "@/lib/registry/components/WidgetRenderer";
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. 웹타입 추가하기
|
||
|
|
|
||
|
|
### 2.1 웹타입 컴포넌트 생성
|
||
|
|
|
||
|
|
**파일 위치**: `frontend/components/screen/widgets/types/{WebTypeName}Widget.tsx`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 예시: ColorPickerWidget.tsx
|
||
|
|
import React from "react";
|
||
|
|
import { WebTypeComponentProps } from "@/types/screen";
|
||
|
|
import { WidgetComponent } from "@/types/screen";
|
||
|
|
|
||
|
|
interface ColorPickerConfig {
|
||
|
|
defaultColor?: string;
|
||
|
|
showAlpha?: boolean;
|
||
|
|
presetColors?: string[];
|
||
|
|
}
|
||
|
|
|
||
|
|
export const ColorPickerWidget: React.FC<WebTypeComponentProps> = ({
|
||
|
|
component,
|
||
|
|
value,
|
||
|
|
onChange,
|
||
|
|
readonly = false
|
||
|
|
}) => {
|
||
|
|
const widget = component as WidgetComponent;
|
||
|
|
const { placeholder, required, style } = widget || {};
|
||
|
|
const config = widget?.webTypeConfig as ColorPickerConfig | undefined;
|
||
|
|
|
||
|
|
const handleColorChange = (color: string) => {
|
||
|
|
if (!readonly && onChange) {
|
||
|
|
onChange(color);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="h-full w-full" style={style}>
|
||
|
|
{/* 라벨 표시 */}
|
||
|
|
{widget?.label && (
|
||
|
|
<label className="mb-1 block text-sm font-medium">
|
||
|
|
{widget.label}
|
||
|
|
{required && <span className="text-orange-500">*</span>}
|
||
|
|
</label>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="flex gap-2 items-center">
|
||
|
|
{/* 색상 입력 */}
|
||
|
|
<input
|
||
|
|
type="color"
|
||
|
|
value={value || config?.defaultColor || "#000000"}
|
||
|
|
onChange={(e) => handleColorChange(e.target.value)}
|
||
|
|
disabled={readonly}
|
||
|
|
className="h-10 w-16 rounded border border-gray-300 cursor-pointer disabled:cursor-not-allowed"
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* 색상 값 표시 */}
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={value || config?.defaultColor || "#000000"}
|
||
|
|
onChange={(e) => handleColorChange(e.target.value)}
|
||
|
|
placeholder={placeholder || "색상을 선택하세요"}
|
||
|
|
disabled={readonly}
|
||
|
|
className="flex-1 h-10 px-3 rounded border border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 disabled:bg-gray-100"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 미리 설정된 색상들 */}
|
||
|
|
{config?.presetColors && (
|
||
|
|
<div className="mt-2 flex gap-1 flex-wrap">
|
||
|
|
{config.presetColors.map((color, idx) => (
|
||
|
|
<button
|
||
|
|
key={idx}
|
||
|
|
type="button"
|
||
|
|
onClick={() => handleColorChange(color)}
|
||
|
|
disabled={readonly}
|
||
|
|
className="w-6 h-6 rounded border border-gray-300 cursor-pointer hover:scale-110 transition-transform disabled:cursor-not-allowed"
|
||
|
|
style={{ backgroundColor: color }}
|
||
|
|
title={color}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.2 웹타입 등록
|
||
|
|
|
||
|
|
**파일**: `frontend/lib/registry/index.ts`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 웹타입 컴포넌트 import 추가
|
||
|
|
import { ColorPickerWidget } from "@/components/screen/widgets/types/ColorPickerWidget";
|
||
|
|
|
||
|
|
// 웹타입 레지스트리에 등록
|
||
|
|
export const registerWebTypes = () => {
|
||
|
|
// 기존 웹타입들...
|
||
|
|
WebTypeRegistry.register("color", ColorPickerWidget);
|
||
|
|
WebTypeRegistry.register("colorpicker", ColorPickerWidget);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.3 웹타입 설정 인터페이스 추가
|
||
|
|
|
||
|
|
**파일**: `frontend/types/screen.ts`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 웹타입별 설정 인터페이스에 추가
|
||
|
|
export interface ColorPickerTypeConfig {
|
||
|
|
defaultColor?: string;
|
||
|
|
showAlpha?: boolean;
|
||
|
|
presetColors?: string[];
|
||
|
|
}
|
||
|
|
|
||
|
|
// 전체 웹타입 설정 유니온에 추가
|
||
|
|
export type WebTypeConfig =
|
||
|
|
| TextTypeConfig
|
||
|
|
| NumberTypeConfig
|
||
|
|
| DateTypeConfig
|
||
|
|
| SelectTypeConfig
|
||
|
|
| FileTypeConfig
|
||
|
|
| ColorPickerTypeConfig; // 추가
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. 템플릿 추가하기
|
||
|
|
|
||
|
|
### 3.1 템플릿 컴포넌트 생성
|
||
|
|
|
||
|
|
**파일 위치**: `frontend/components/screen/templates/{TemplateName}Template.tsx`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 예시: ContactFormTemplate.tsx
|
||
|
|
import React from "react";
|
||
|
|
import { ComponentData } from "@/types/screen";
|
||
|
|
import { generateComponentId } from "@/lib/utils/componentUtils";
|
||
|
|
|
||
|
|
interface ContactFormTemplateProps {
|
||
|
|
onAddComponents: (components: ComponentData[]) => void;
|
||
|
|
position: { x: number; y: number };
|
||
|
|
}
|
||
|
|
|
||
|
|
export const ContactFormTemplate: React.FC<ContactFormTemplateProps> = ({
|
||
|
|
onAddComponents,
|
||
|
|
position,
|
||
|
|
}) => {
|
||
|
|
const createContactForm = () => {
|
||
|
|
const components: ComponentData[] = [
|
||
|
|
// 컨테이너
|
||
|
|
{
|
||
|
|
id: generateComponentId(),
|
||
|
|
type: "container",
|
||
|
|
position: position,
|
||
|
|
size: { width: 500, height: 600 },
|
||
|
|
style: {
|
||
|
|
backgroundColor: "#ffffff",
|
||
|
|
border: "1px solid #e5e7eb",
|
||
|
|
borderRadius: "8px",
|
||
|
|
padding: "24px",
|
||
|
|
},
|
||
|
|
children: [],
|
||
|
|
},
|
||
|
|
|
||
|
|
// 제목
|
||
|
|
{
|
||
|
|
id: generateComponentId(),
|
||
|
|
type: "widget",
|
||
|
|
webType: "text",
|
||
|
|
position: { x: position.x + 24, y: position.y + 24 },
|
||
|
|
size: { width: 452, height: 40 },
|
||
|
|
label: "연락처 양식",
|
||
|
|
placeholder: "연락처를 입력해주세요",
|
||
|
|
style: {
|
||
|
|
fontSize: "24px",
|
||
|
|
fontWeight: "bold",
|
||
|
|
color: "#1f2937",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
|
||
|
|
// 이름 입력
|
||
|
|
{
|
||
|
|
id: generateComponentId(),
|
||
|
|
type: "widget",
|
||
|
|
webType: "text",
|
||
|
|
position: { x: position.x + 24, y: position.y + 84 },
|
||
|
|
size: { width: 452, height: 40 },
|
||
|
|
label: "이름",
|
||
|
|
placeholder: "이름을 입력하세요",
|
||
|
|
required: true,
|
||
|
|
},
|
||
|
|
|
||
|
|
// 이메일 입력
|
||
|
|
{
|
||
|
|
id: generateComponentId(),
|
||
|
|
type: "widget",
|
||
|
|
webType: "email",
|
||
|
|
position: { x: position.x + 24, y: position.y + 144 },
|
||
|
|
size: { width: 452, height: 40 },
|
||
|
|
label: "이메일",
|
||
|
|
placeholder: "이메일을 입력하세요",
|
||
|
|
required: true,
|
||
|
|
},
|
||
|
|
|
||
|
|
// 전화번호 입력
|
||
|
|
{
|
||
|
|
id: generateComponentId(),
|
||
|
|
type: "widget",
|
||
|
|
webType: "tel",
|
||
|
|
position: { x: position.x + 24, y: position.y + 204 },
|
||
|
|
size: { width: 452, height: 40 },
|
||
|
|
label: "전화번호",
|
||
|
|
placeholder: "전화번호를 입력하세요",
|
||
|
|
},
|
||
|
|
|
||
|
|
// 메시지 입력
|
||
|
|
{
|
||
|
|
id: generateComponentId(),
|
||
|
|
type: "widget",
|
||
|
|
webType: "textarea",
|
||
|
|
position: { x: position.x + 24, y: position.y + 264 },
|
||
|
|
size: { width: 452, height: 120 },
|
||
|
|
label: "메시지",
|
||
|
|
placeholder: "메시지를 입력하세요",
|
||
|
|
required: true,
|
||
|
|
},
|
||
|
|
|
||
|
|
// 제출 버튼
|
||
|
|
{
|
||
|
|
id: generateComponentId(),
|
||
|
|
type: "button",
|
||
|
|
componentType: "button-primary",
|
||
|
|
position: { x: position.x + 24, y: position.y + 404 },
|
||
|
|
size: { width: 120, height: 40 },
|
||
|
|
componentConfig: {
|
||
|
|
text: "제출",
|
||
|
|
actionType: "submit",
|
||
|
|
style: "primary",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
|
||
|
|
// 취소 버튼
|
||
|
|
{
|
||
|
|
id: generateComponentId(),
|
||
|
|
type: "button",
|
||
|
|
componentType: "button-secondary",
|
||
|
|
position: { x: position.x + 164, y: position.y + 404 },
|
||
|
|
size: { width: 120, height: 40 },
|
||
|
|
componentConfig: {
|
||
|
|
text: "취소",
|
||
|
|
actionType: "cancel",
|
||
|
|
style: "secondary",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
onAddComponents(components);
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
className="cursor-pointer rounded border-2 border-dashed border-gray-300 bg-gray-50 p-4 hover:border-blue-400 hover:bg-blue-50"
|
||
|
|
onClick={createContactForm}
|
||
|
|
>
|
||
|
|
<div className="text-center">
|
||
|
|
<div className="text-lg font-medium">연락처 양식</div>
|
||
|
|
<div className="mt-1 text-sm text-gray-600">
|
||
|
|
이름, 이메일, 전화번호, 메시지 입력이 포함된 연락처 양식
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.2 템플릿 패널에 등록
|
||
|
|
|
||
|
|
**파일**: `frontend/components/screen/panels/TemplatesPanel.tsx`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 템플릿 import 추가
|
||
|
|
import { ContactFormTemplate } from "@/components/screen/templates/ContactFormTemplate";
|
||
|
|
|
||
|
|
// 템플릿 목록에 추가
|
||
|
|
const templates = [
|
||
|
|
// 기존 템플릿들...
|
||
|
|
{
|
||
|
|
id: "contact-form",
|
||
|
|
name: "연락처 양식",
|
||
|
|
description: "이름, 이메일, 전화번호, 메시지가 포함된 연락처 양식",
|
||
|
|
component: ContactFormTemplate,
|
||
|
|
},
|
||
|
|
];
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. 설정 패널 개발
|
||
|
|
|
||
|
|
### 4.1 설정 패널 컴포넌트 생성
|
||
|
|
|
||
|
|
**파일 위치**: `frontend/components/screen/config-panels/{ComponentName}ConfigPanel.tsx`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 예시: AlertConfigPanel.tsx
|
||
|
|
import React from "react";
|
||
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||
|
|
import { Label } from "@/components/ui/label";
|
||
|
|
import { Input } from "@/components/ui/input";
|
||
|
|
import { Textarea } from "@/components/ui/textarea";
|
||
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||
|
|
import { Switch } from "@/components/ui/switch";
|
||
|
|
import { ComponentData } from "@/types/screen";
|
||
|
|
|
||
|
|
interface AlertConfigPanelProps {
|
||
|
|
component: ComponentData;
|
||
|
|
onUpdateProperty: (path: string, value: any) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const AlertConfigPanel: React.FC<AlertConfigPanelProps> = ({
|
||
|
|
component,
|
||
|
|
onUpdateProperty,
|
||
|
|
}) => {
|
||
|
|
const config = component.componentConfig || {};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-sm">알림 설정</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
{/* 제목 설정 */}
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="alert-title" className="text-xs">제목</Label>
|
||
|
|
<Input
|
||
|
|
id="alert-title"
|
||
|
|
value={config.title || ""}
|
||
|
|
onChange={(e) => onUpdateProperty("componentConfig.title", e.target.value)}
|
||
|
|
placeholder="알림 제목"
|
||
|
|
className="h-8"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 메시지 설정 */}
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="alert-message" className="text-xs">메시지</Label>
|
||
|
|
<Textarea
|
||
|
|
id="alert-message"
|
||
|
|
value={config.message || ""}
|
||
|
|
onChange={(e) => onUpdateProperty("componentConfig.message", e.target.value)}
|
||
|
|
placeholder="알림 메시지를 입력하세요"
|
||
|
|
className="min-h-[60px]"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 타입 설정 */}
|
||
|
|
<div>
|
||
|
|
<Label className="text-xs">알림 타입</Label>
|
||
|
|
<Select
|
||
|
|
value={config.type || "info"}
|
||
|
|
onValueChange={(value) => onUpdateProperty("componentConfig.type", value)}
|
||
|
|
>
|
||
|
|
<SelectTrigger className="h-8">
|
||
|
|
<SelectValue />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="info">정보</SelectItem>
|
||
|
|
<SelectItem value="warning">경고</SelectItem>
|
||
|
|
<SelectItem value="success">성공</SelectItem>
|
||
|
|
<SelectItem value="error">오류</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 아이콘 표시 설정 */}
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<Label htmlFor="show-icon" className="text-xs">아이콘 표시</Label>
|
||
|
|
<Switch
|
||
|
|
id="show-icon"
|
||
|
|
checked={config.showIcon ?? true}
|
||
|
|
onCheckedChange={(checked) => onUpdateProperty("componentConfig.showIcon", checked)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 스타일 설정 */}
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="alert-bg-color" className="text-xs">배경 색상</Label>
|
||
|
|
<Input
|
||
|
|
id="alert-bg-color"
|
||
|
|
type="color"
|
||
|
|
value={config.style?.backgroundColor || "#ffffff"}
|
||
|
|
onChange={(e) => onUpdateProperty("componentConfig.style.backgroundColor", e.target.value)}
|
||
|
|
className="h-8 w-full"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 테두리 반경 설정 */}
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="border-radius" className="text-xs">테두리 반경 (px)</Label>
|
||
|
|
<Input
|
||
|
|
id="border-radius"
|
||
|
|
type="number"
|
||
|
|
value={parseInt(config.style?.borderRadius || "6") || 6}
|
||
|
|
onChange={(e) => onUpdateProperty("componentConfig.style.borderRadius", `${e.target.value}px`)}
|
||
|
|
min="0"
|
||
|
|
max="50"
|
||
|
|
className="h-8"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.2 설정 패널 등록
|
||
|
|
|
||
|
|
**파일**: `frontend/components/screen/panels/DetailSettingsPanel.tsx`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 설정 패널 import 추가
|
||
|
|
import { AlertConfigPanel } from "@/components/screen/config-panels/AlertConfigPanel";
|
||
|
|
|
||
|
|
// hasNewConfigPanel 배열에 추가
|
||
|
|
const hasNewConfigPanel =
|
||
|
|
componentType &&
|
||
|
|
[
|
||
|
|
"button",
|
||
|
|
"button-primary",
|
||
|
|
"button-secondary",
|
||
|
|
"card",
|
||
|
|
"dashboard",
|
||
|
|
"alert", // 추가
|
||
|
|
"alert-info", // 추가
|
||
|
|
// 기타 컴포넌트들...
|
||
|
|
].includes(componentType);
|
||
|
|
|
||
|
|
// switch 문에 케이스 추가
|
||
|
|
switch (componentType) {
|
||
|
|
case "button":
|
||
|
|
case "button-primary":
|
||
|
|
case "button-secondary":
|
||
|
|
return <NewButtonConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||
|
|
|
||
|
|
case "card":
|
||
|
|
return <CardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||
|
|
|
||
|
|
case "dashboard":
|
||
|
|
return <DashboardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||
|
|
|
||
|
|
case "alert": // 추가
|
||
|
|
case "alert-info":
|
||
|
|
return <AlertConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||
|
|
|
||
|
|
// 기타 케이스들...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. 데이터베이스 설정
|
||
|
|
|
||
|
|
### 5.1 component_standards 테이블에 데이터 추가
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// backend-node/scripts/add-new-component.js
|
||
|
|
const { PrismaClient } = require("@prisma/client");
|
||
|
|
const prisma = new PrismaClient();
|
||
|
|
|
||
|
|
async function addNewComponents() {
|
||
|
|
// 알림 컴포넌트들 추가
|
||
|
|
const alertComponents = [
|
||
|
|
{
|
||
|
|
component_code: "alert-info",
|
||
|
|
component_name: "정보 알림",
|
||
|
|
category: "display",
|
||
|
|
description: "정보성 내용을 표시하는 알림 컴포넌트",
|
||
|
|
component_config: {
|
||
|
|
type: "alert",
|
||
|
|
config_panel: "AlertConfigPanel",
|
||
|
|
},
|
||
|
|
default_size: {
|
||
|
|
width: 400,
|
||
|
|
height: 80,
|
||
|
|
},
|
||
|
|
icon_name: "info",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
component_code: "alert-warning",
|
||
|
|
component_name: "경고 알림",
|
||
|
|
category: "display",
|
||
|
|
description: "경고 내용을 표시하는 알림 컴포넌트",
|
||
|
|
component_config: {
|
||
|
|
type: "alert",
|
||
|
|
config_panel: "AlertConfigPanel",
|
||
|
|
},
|
||
|
|
default_size: {
|
||
|
|
width: 400,
|
||
|
|
height: 80,
|
||
|
|
},
|
||
|
|
icon_name: "alert-triangle",
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
// 웹타입 추가
|
||
|
|
const webTypes = [
|
||
|
|
{
|
||
|
|
component_code: "color-picker",
|
||
|
|
component_name: "색상 선택기",
|
||
|
|
category: "input",
|
||
|
|
description: "색상을 선택할 수 있는 입력 컴포넌트",
|
||
|
|
component_config: {
|
||
|
|
type: "widget",
|
||
|
|
webType: "color",
|
||
|
|
},
|
||
|
|
default_size: {
|
||
|
|
width: 200,
|
||
|
|
height: 40,
|
||
|
|
},
|
||
|
|
icon_name: "palette",
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
// 데이터베이스에 삽입
|
||
|
|
for (const component of [...alertComponents, ...webTypes]) {
|
||
|
|
await prisma.component_standards.create({
|
||
|
|
data: component,
|
||
|
|
});
|
||
|
|
console.log(`✅ ${component.component_name} 추가 완료`);
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log("🎉 모든 컴포넌트 추가 완료!");
|
||
|
|
await prisma.$disconnect();
|
||
|
|
}
|
||
|
|
|
||
|
|
addNewComponents().catch(console.error);
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5.2 스크립트 실행
|
||
|
|
|
||
|
|
```bash
|
||
|
|
cd backend-node
|
||
|
|
node scripts/add-new-component.js
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. 테스트 및 검증
|
||
|
|
|
||
|
|
### 6.1 개발 서버 재시작
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# 프론트엔드 재시작
|
||
|
|
cd frontend
|
||
|
|
npm run dev
|
||
|
|
|
||
|
|
# 백엔드 재시작 (필요시)
|
||
|
|
cd ../backend-node
|
||
|
|
npm run dev
|
||
|
|
```
|
||
|
|
|
||
|
|
### 6.2 테스트 체크리스트
|
||
|
|
|
||
|
|
#### ✅ 컴포넌트 패널 확인
|
||
|
|
|
||
|
|
- [ ] 새 컴포넌트가 컴포넌트 패널에 표시됨
|
||
|
|
- [ ] 카테고리별로 올바르게 분류됨
|
||
|
|
- [ ] 아이콘이 올바르게 표시됨
|
||
|
|
|
||
|
|
#### ✅ 드래그앤드롭 확인
|
||
|
|
|
||
|
|
- [ ] 컴포넌트를 캔버스에 드래그 가능
|
||
|
|
- [ ] 기본 크기로 올바르게 배치됨
|
||
|
|
- [ ] 컴포넌트가 텍스트박스가 아닌 실제 형태로 렌더링됨
|
||
|
|
|
||
|
|
#### ✅ 속성 편집 확인
|
||
|
|
|
||
|
|
- [ ] 컴포넌트 선택 시 속성 패널에 기본 속성 표시
|
||
|
|
- [ ] 상세 설정 패널이 올바르게 표시됨
|
||
|
|
- [ ] 설정값 변경이 실시간으로 반영됨
|
||
|
|
|
||
|
|
#### ✅ 할당된 화면 확인
|
||
|
|
|
||
|
|
- [ ] 화면 저장 후 메뉴에 할당
|
||
|
|
- [ ] 할당된 화면에서 컴포넌트가 올바르게 표시됨
|
||
|
|
- [ ] 위치와 크기가 편집기와 동일함
|
||
|
|
- [ ] 인터랙티브 모드로 올바르게 동작함
|
||
|
|
|
||
|
|
### 6.3 문제 해결
|
||
|
|
|
||
|
|
#### 컴포넌트가 텍스트박스로 표시되는 경우
|
||
|
|
|
||
|
|
1. `DynamicComponentRenderer.tsx`에서 컴포넌트가 등록되었는지 확인
|
||
|
|
2. `InteractiveScreenViewerDynamic.tsx`에서 import 되었는지 확인
|
||
|
|
3. 브라우저 콘솔에서 레지스트리 등록 로그 확인
|
||
|
|
|
||
|
|
#### 설정 패널이 표시되지 않는 경우
|
||
|
|
|
||
|
|
1. `DetailSettingsPanel.tsx`의 `hasNewConfigPanel` 배열 확인
|
||
|
|
2. switch 문에 케이스가 추가되었는지 확인
|
||
|
|
3. 데이터베이스의 `config_panel` 값 확인
|
||
|
|
|
||
|
|
#### 할당된 화면에서 렌더링 안 되는 경우
|
||
|
|
|
||
|
|
1. `InteractiveScreenViewerDynamic.tsx`에서 import 확인
|
||
|
|
2. 컴포넌트 렌더러에서 `isInteractive` prop 처리 확인
|
||
|
|
3. 브라우저 콘솔에서 오류 메시지 확인
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🎯 모범 사례
|
||
|
|
|
||
|
|
### 1. 컴포넌트 네이밍
|
||
|
|
|
||
|
|
- 컴포넌트 코드: `kebab-case` (예: `alert-info`, `contact-form`)
|
||
|
|
- 파일명: `PascalCase` (예: `AlertRenderer.tsx`, `ContactFormTemplate.tsx`)
|
||
|
|
- 클래스명: `camelCase` (예: `alertContainer`, `formInput`)
|
||
|
|
|
||
|
|
### 2. 설정 구조
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 일관된 설정 구조 사용
|
||
|
|
interface ComponentConfig {
|
||
|
|
// 기본 설정
|
||
|
|
title?: string;
|
||
|
|
description?: string;
|
||
|
|
|
||
|
|
// 표시 설정
|
||
|
|
showIcon?: boolean;
|
||
|
|
showBorder?: boolean;
|
||
|
|
|
||
|
|
// 스타일 설정
|
||
|
|
style?: {
|
||
|
|
backgroundColor?: string;
|
||
|
|
borderRadius?: string;
|
||
|
|
padding?: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
// 컴포넌트별 전용 설정
|
||
|
|
[key: string]: any;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. 반응형 지원
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 컨테이너 크기에 따른 반응형 처리
|
||
|
|
const isSmall = component.size.width < 300;
|
||
|
|
const columns = isSmall ? 1 : 3;
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. 접근성 고려
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 접근성 속성 추가
|
||
|
|
<button
|
||
|
|
aria-label={config.ariaLabel || config.text}
|
||
|
|
role="button"
|
||
|
|
tabIndex={0}
|
||
|
|
>
|
||
|
|
{config.text}
|
||
|
|
</button>
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📚 참고 자료
|
||
|
|
|
||
|
|
- [Shadcn/ui 컴포넌트 문서](https://ui.shadcn.com)
|
||
|
|
- [Lucide 아이콘 문서](https://lucide.dev)
|
||
|
|
- [React Hook Form 문서](https://react-hook-form.com)
|
||
|
|
- [TypeScript 타입 정의 가이드](https://www.typescriptlang.org/docs)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
이 가이드를 따라 새로운 컴포넌트, 웹타입, 템플릿을 성공적으로 추가할 수 있습니다. 추가 질문이나 문제가 발생하면 언제든지 문의해주세요! 🚀
|