# 화면관리 시스템 컴포넌트 개발 가이드

화면관리 시스템에서 새로운 컴포넌트, 템플릿, 웹타입을 추가하는 완전한 가이드입니다.

## 🎯 목차

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)

---

이 가이드를 따라 새로운 컴포넌트, 웹타입, 템플릿을 성공적으로 추가할 수 있습니다. 추가 질문이나 문제가 발생하면 언제든지 문의해주세요! 🚀
