# 컴포넌트 타입별 개발 규칙

> **메인 문서**: [components.md](components.md)
> **관련 문서**: [layout-json-components.md](layout-json-components.md) | [sirsoft-admin_basic 컴포넌트](templates/sirsoft-admin_basic/components.md)

---

## 목차

1. [핵심 원칙](#핵심-원칙)
2. [기본 컴포넌트 (Basic Component)](#1-기본-컴포넌트-basic-component)
3. [집합 컴포넌트 (Composite Component)](#2-집합-컴포넌트-composite-component)
4. [집합 컴포넌트 재사용 가이드라인](#3-집합-컴포넌트-재사용-가이드라인)
5. [레이아웃 컴포넌트 (Layout Component)](#4-레이아웃-컴포넌트-layout-component)

---

## 핵심 원칙

```
필수: 기본 컴포넌트 사용 (HTML 태그 직접 사용 금지)
필수: 기본 컴포넌트만 사용 (Div, Button, H2 등)
필수: 집합 컴포넌트 재사용 우선
```

---

## 1. 기본 컴포넌트 (Basic Component)

**정의**: HTML 기본 태그에 대응하는 최소 래핑 컴포넌트

**타입**: `basic`

**예시**: Button, Input, Div, Icon, H1, H2, P, Span, Form, Table, Ul, Li, Nav 등

**특징**:
- DOM 요소에 직접 매핑
- 최소한의 래핑만 수행
- props를 HTML 속성으로 전달
- 스타일링은 className으로 처리

### 패턴

```tsx
// templates/[vendor-template]/src/components/basic/Button.tsx

import React from 'react';

export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'danger' | 'success';
  size?: 'sm' | 'md' | 'lg';
}

/**
 * 기본 버튼 컴포넌트
 */
export const Button: React.FC<ButtonProps> = ({
  children,
  variant = 'primary',
  size = 'md',
  className = '',
  ...props
}) => {
  return (
    <button
      className={className}
      {...props}
    >
      {children}
    </button>
  );
};
```

### Input 컴포넌트 IME 처리 (중요)

Input 컴포넌트는 한글 등 IME(Input Method Editor) 조합 입력을 올바르게 처리합니다.

**핵심 동작**:
- IME 조합 중(`compositionStart` ~ `compositionEnd`)에는 외부 `onChange`를 호출하지 않음
- 조합 완료 후 `compositionEnd` 이벤트에서 최종 값으로 `onChange` 호출
- IME 조합 중에는 `onKeyPress` 이벤트도 발생하지 않음 (Enter 키 등)
- 내부 로컬 상태로 화면 표시를 유지하여 조합 중에도 입력이 보임

**사용 시 주의사항**:
```
✅ keypress 이벤트: IME 조합 완료 후에만 발생
✅ change 이벤트: IME 조합 완료 후에만 외부에 전달
한글 입력 후 Enter 검색: keypress + key: "Enter" 조합 사용 권장
```

**검색 입력 필드 예시** (keypress 사용):
```json
{
  "id": "search_input",
  "type": "basic",
  "name": "Input",
  "props": {
    "type": "text",
    "placeholder": "검색어 입력..."
  },
  "actions": [
    {
      "type": "change",
      "handler": "setState",
      "params": {
        "target": "global",
        "searchQuery": "{{$event.target.value}}"
      }
    },
    {
      "type": "keypress",
      "key": "Enter",
      "handler": "navigate",
      "params": {
        "path": "/search?q={{_global.searchQuery}}"
      }
    }
  ]
}
```

---

## 2. 집합 컴포넌트 (Composite Component) ⭐ 핵심

**정의**: 기본 컴포넌트를 조합하여 특정 UI 패턴을 캡슐화한 복합 컴포넌트

**타입**: `composite`

**예시**:
- **UI 컴포넌트**: Card, DataGrid, Modal, Dropdown, Pagination, SearchBar, StatusBadge, Toast, TagInput
- **관리자 컴포넌트**: AdminSidebar, AdminHeader, AdminFooter, PageHeader, TemplateCard

### 핵심 원칙 (필수 준수)

```
필수: 기본 컴포넌트 사용 (HTML 태그 직접 사용 금지)
필수: 기본 컴포넌트만 사용 (Div, Button, H2 등)
필수: 집합 컴포넌트 재사용 우선
필수: props 기본값은 undefined로 설정 (배열/객체 리터럴 금지)
필수: 모듈 레벨 상수 사용 (const EMPTY: T[] = [])
```

1. **UI 패턴 캡슐화**: 특정 UI 패턴(카드, 테이블, 모달 등)을 완성된 형태로 제공
2. **간단한 Props 전달**: 레이아웃 JSON에서 최소한의 props만 전달
3. **기본 컴포넌트 조합**: 내부적으로 Div, Button, Table 등 기본 컴포넌트만 사용
4. **HTML 태그 직접 사용 금지**: `<div>`, `<button>` 등 HTML 태그 직접 사용 불가
5. **집합 컴포넌트 재사용 우선**: 새로운 집합 컴포넌트 개발 시 기존 집합 컴포넌트를 재사용할 수 있는지 우선 검토
6. **Props 기본값 참조 안정성 필수**: 배열/객체 기본값은 모듈 레벨 상수 사용 (무한 렌더 루프 방지)
7. **`editorAttrs` 패스스루 필수** (레이아웃 편집기 nesting 대상): editor-spec `nesting.draggable` 에 등재된 composite/layout 컴포넌트는 `editorAttrs?: EditorAttrs` 를 받아 **시각적 루트 요소에 `{...editorAttrs}` 로 spread** 해야 한다 (§ "editorAttrs 패스스루" 참조)

### editorAttrs 패스스루 (레이아웃 편집기 nesting 컴포넌트)

편집 모드에서 코어 `DynamicRenderer` 는 각 컴포넌트에 `data-editor-*` 표식(드롭 슬롯/선택/드래그 DOM 쿼리용)과 선택/hover 핸들러를 **단일 `editorAttrs` 객체**로 주입한다. composite/layout 컴포넌트는 도메인 prop 만 명시 구조분해하고 미명시 props 를 DOM 으로 흘리지 않으므로, `editorAttrs` 를 받아 루트에 spread 하지 않으면 그 노드가 편집기에서 누락된다(편집기는 `[data-editor-path]` DOM 쿼리로 동작 — 컨테이너 노드가 누락되면 자식을 컨테이너 밖으로 옮기는 드롭 슬롯이 생성되지 않음).

```tsx
import type { EditorAttrs } from '../../types';

export interface FooProps {
  /* ...도메인 prop 전부 명시 구조분해... */
  editorAttrs?: EditorAttrs; // 편집기 주입 속성 (편집 모드 전용)
}

export const Foo = ({ /* 도메인 prop */, editorAttrs }: FooProps) => (
  <Div className={...} {...editorAttrs}>{children}</Div> // 시각적 루트에 spread
);
```

| 구분 | editorAttrs 수신 | 이유 |
|------|------------------|------|
| basic (`{...props}` 패스스루) | 불필요 | 코어가 주입한 개별 `data-editor-*` 키가 `{...props}` 로 DOM 도달 |
| composite / layout | **필수** | 도메인 prop 만 구조분해 → 개별 키 유실 → `editorAttrs` 명시 수신 + 루트 spread 필요 |
| 서드파티 모달 / Portal | 면제 | interface 주석에 `editorAttrs 패스스루 — 모달 예외` 명기 |

- 사용자 페이지(비편집)에서는 `editorAttrs` 미주입 → `{...editorAttrs}` 가 no-op → DOM 구조/속성 불변 (프리뷰 ↔ 사용자 페이지 패리티 유지).
- `editorAttrs` 만 spread 하므로 도메인 prop 누출/HTML 동명 prop 타입 충돌이 없다. (`React.HTMLAttributes` 상속 + `{...rest}` 방식은 도메인 prop 누출 위험으로 채택 안 함.)
- 자동 검출: audit 룰 `editor-attrs-passthrough` (nesting.draggable 의 composite/layout 컴포넌트가 `editorAttrs` 미수신 시 error). 의도적 면제는 파일 헤더 `// editor-attrs:allow <사유>`.

### 요소 id 패스스루 (코어 일괄 ID)

레이아웃 편집기 코어는 모든 draggable 컴포넌트의 [속성] 탭 최상단에 "요소 ID" 컨트롤을 일괄 제공한다(값 = 표준 `node.props.id`, 코어는 강제 DOM 주입 안 함). 따라서 draggable 컴포넌트는 `editorAttrs` 와 별개로 **`id` prop 을 받아 시각적 루트에 전달**해야 그 id 가 실제 DOM 에 닿는다.

```tsx
export interface FooProps {
  id?: string; // DOM id (코어 일괄 ID)
  // ... 도메인 prop ...
  editorAttrs?: EditorAttrs;
}

export const Foo = ({ id, /* 도메인 prop */, editorAttrs }: FooProps) => (
  <Div id={id} className={...} {...editorAttrs}>{children}</Div> // id 를 루트에 명시 전달
);
```

| 구분 | id 수신/전달 | 비고 |
|------|------------|------|
| basic (`{...props}`/`{...validProps}` 패스스루) | 불필요 | id 가 props 스프레드로 DOM 도달 |
| composite / layout (도메인 prop 만 구조분해) | **필수** | `id?: string` 수신 + 루트에 `id={id}` 명시 전달 |
| 서드파티 모달 / Portal (인라인 루트 없음) | 면제 + **opt-out** | 인라인 DOM 이 없어 id 부착 불가 → 그 컴포넌트 capability 에 `"coreProps": false` 선언(코어 id 컨트롤 미노출). 예: ImageGallery(Lightbox) |

- 여러 렌더 분기(variant 별 root)가 있으면 **모든 root 분기**에 `id={id}` 를 전달한다(누락 분기는 그 variant 에서 id 미반영).
- id 값이 이미 `{{바인딩}}` 이거나 컴포넌트/핸들러가 동적 id 를 쓰면, 코어 id 컨트롤은 "바인딩됨(코드 편집)" 디그레이드로 표시해 덮어쓰기를 차단한다(작성자 정적 값만 편집).
- 코어 id 컨트롤은 HTML 안전 문자(영문자/숫자/`-`/`_`/`:`/`.`)만 허용하고 한글·공백 등은 자동 제거한다(`sanitizeElementId`).

### 올바른 패턴

```tsx
// ✅ 올바른 예: 기본 컴포넌트 재사용
import { Div } from '../basic/Div';
import { H2 } from '../basic/H2';
import { P } from '../basic/P';
import { Img } from '../basic/Img';

export interface CardProps {
  title?: string;
  content?: string;
  imageUrl?: string;
  onClick?: () => void;
}

/**
 * Card 집합 컴포넌트
 */
export const Card: React.FC<CardProps> = ({
  title,
  content,
  imageUrl,
  onClick,
}) => {
  return (
    <Div className="card" onClick={onClick}>
      {imageUrl && <Img src={imageUrl} />}
      <Div className="card-body">
        {title && <H2>{title}</H2>}
        {content && <P>{content}</P>}
      </Div>
    </Div>
  );
};
```

### 잘못된 패턴

```tsx
// ❌ 잘못된 예: HTML 태그 직접 사용 (금지)
export const Card: React.FC<CardProps> = ({
  title,
  content,
  imageUrl,
  onClick,
}) => {
  return (
    <div className="card" onClick={onClick}>
      {imageUrl && <img src={imageUrl} />}
      <div className="card-body">
        {title && <h2>{title}</h2>}
        {content && <p>{content}</p>}
      </div>
    </div>
  );
};
```

### Props 기본값 참조 안정성

배열/객체 기본값을 destructuring에서 직접 사용하면 매 렌더마다 새 참조가 생성되어 `useEffect` 무한 루프를 유발합니다. 모달 내에서 사용 시 `startTransition`으로 래핑된 모달 닫기 렌더를 영구 차단하여 모달 전체가 작동 불능이 됩니다.

```tsx
// ❌ 잘못된 예: 매 렌더마다 새 [] 참조 생성 → useEffect 무한 루프
export const FileUploader = forwardRef((
  { initialFiles = [], roleIds = [], ... },
  ref
) => { ... });

// ✅ 올바른 예: 모듈 레벨 상수로 안정적인 참조
const EMPTY_FILES: Attachment[] = [];
const EMPTY_ROLE_IDS: number[] = [];

export const FileUploader = forwardRef((
  { initialFiles = EMPTY_FILES, roleIds = EMPTY_ROLE_IDS, ... },
  ref
) => { ... });
```

> **트러블슈팅**: composite 컴포넌트가 매 렌더마다 새 객체/배열 기본값을 만들면 자식 모달이 닫기 직후 재마운트되어 닫기 버튼이 동작하지 않는 회귀가 발생한다. 모듈 스코프 상수로 안정화한다.

### 객체 값 이벤트의 `_changedKeys` 규칙 (engine-v1.28.0+)

객체 형태의 `value`를 emit하는 composite 컴포넌트는
`onChange` 이벤트에 `_changedKeys` 메타데이터를 포함해야 합니다.

이는 엔진의 debounce와 상호작용 시 stale closure로 인한 키 유실을 방지합니다.

**규칙**:

- `onChange` 이벤트에서 `target.value`가 객체(`Record<string, any>`)인 경우,
  실제 사용자가 변경한 키 목록을 `_changedKeys: string[]`로 포함
- `_changedKeys` 미포함 시 엔진은 기존 동작 유지 (마지막 값 사용)

**예시**:

```typescript
// Composite 컴포넌트 내부
onChange?.({
    target: { name, value: { ko: "빨강", en: "blue" } },
    _changedKeys: ["ko"],  // 실제 변경된 키만 명시
});
```

**대상 컴포넌트**: MultilingualInput, MultilingualTagInput 등
객체 value를 emit하면서 debounce와 함께 사용될 수 있는 모든 composite 컴포넌트

---

## 3. 집합 컴포넌트 재사용 가이드라인 ⭐ 매우 중요

새로운 집합 컴포넌트 개발 전 다음 순서로 검토:

### 1. 기존 집합 컴포넌트 재사용 (최우선)

- 동일하거나 유사한 UI 패턴을 제공하는 기존 컴포넌트가 있는가?
- 기존 컴포넌트를 props로 커스터마이징하여 요구사항을 충족할 수 있는가?
- 예시: 퀵 액션 버튼 → ActionMenu 재사용, 브레드크럼 → Breadcrumb 재사용

### 2. 기존 컴포넌트 조합 (차선책)

- 여러 기존 컴포넌트를 조합하여 새로운 패턴을 만들 수 있는가?
- 예시: PageHeader = Breadcrumb + 제목 + 탭 + 액션 버튼 조합

### 3. 새 컴포넌트 개발 (최후의 수단)

- 위 두 방법으로 해결 불가능한 경우에만 신규 개발
- 개발 시 향후 재사용 가능성을 고려한 범용적 설계 필수

### 재사용 시 얻는 이점

- 코드 중복 제거 및 유지보수성 향상
- 일관된 UX 제공
- 기존 컴포넌트의 개선사항 자동 반영
- 인터페이스 통일로 학습 비용 감소

### 실제 재사용 사례

**ActionMenu 재사용 예시**:

```tsx
// ❌ 잘못된 예: 커스텀 액션 메뉴 구현
export const TemplateCard = () => (
  <Div>
    {quickActions.map((action) => (
      <Button onClick={action.onClick}>{action.label}</Button>
    ))}
  </Div>
);

// ✅ 올바른 예: ActionMenu 컴포넌트 재사용
import { ActionMenu } from './ActionMenu';
import { IconName } from '../basic/Icon';

export const TemplateCard = () => (
  <Div>
    <ActionMenu
      items={actions}
      triggerIconName={IconName.EllipsisVertical}
      position="left"
    />
  </Div>
);
```

**Breadcrumb 재사용 예시**:

```tsx
// ❌ 잘못된 예: 커스텀 브레드크럼 구현
export const PageHeader = ({ breadcrumbs }) => (
  <Div>
    {breadcrumbs.map((item, index) => (
      <React.Fragment key={index}>
        {item.url ? <A href={item.url}>{item.label}</A> : <Span>{item.label}</Span>}
        {index < breadcrumbs.length - 1 && <Icon name={IconName.ChevronRight} />}
      </React.Fragment>
    ))}
  </Div>
);

// ✅ 올바른 예: Breadcrumb 컴포넌트 재사용
import { Breadcrumb, BreadcrumbItem } from './Breadcrumb';

export const PageHeader = ({ breadcrumbItems }) => (
  <Div>
    <Breadcrumb items={breadcrumbItems} />
  </Div>
);
```

---

## 4. 레이아웃 컴포넌트 (Layout Component)

**정의**: 자식 요소를 배치하는 컨테이너 컴포넌트

**타입**: `layout`

**예시**: Container (flex, grid, stack), Grid, Flex, SectionLayout

**특징**:
- 자식 요소의 배치 및 정렬을 담당
- 레이아웃 관련 props 제공 (layout, direction, gap, justify, align 등)
- UI 로직 없이 순수하게 구조화만 담당

**주의**: `Section`은 basic 컴포넌트이며, `SectionLayout`은 layout 컴포넌트입니다.
- `Section` (basic): HTML `<section>` 태그를 래핑하는 최소 컴포넌트
- `SectionLayout` (layout): Section 컴포넌트를 활용하여 제목, 패딩, 배경색 등을 제공하는 레이아웃 컴포넌트

### 핵심 원칙 (필수 준수)

1. **기본 컴포넌트 재사용**: 내부적으로 Div, Section 등 기본 컴포넌트만 사용
2. **HTML 태그 직접 사용 금지**: `<div>`, `<section>` 등 HTML 태그 직접 사용 불가
3. **집합 컴포넌트와 동일한 패턴**: 집합 컴포넌트와 동일한 개발 패턴 적용

### 개발 가이드라인

```tsx
// ❌ 잘못된 예: HTML 태그 직접 사용
export const Container: React.FC<ContainerProps> = ({ children }) => (
  <div className="container">
    {children}
  </div>
);

// ✅ 올바른 예: 기본 컴포넌트 재사용
import { Div } from '../basic/Div';

export const Container: React.FC<ContainerProps> = ({ children }) => (
  <Div className="container">
    {children}
  </Div>
);
```

### 재사용 시 얻는 이점

- 집합 컴포넌트와 동일한 아키텍처 패턴 유지
- 일관된 코드베이스 구조
- 기본 컴포넌트 수정 시 자동 반영

---

## 관련 문서

- [컴포넌트 개발 규칙 인덱스](components.md)
- [컴포넌트 패턴](components-patterns.md)
- [컴포넌트 고급 기능](components-advanced.md)
- [sirsoft-admin_basic 컴포넌트](templates/sirsoft-admin_basic/components.md)
- [sirsoft-basic 컴포넌트](templates/sirsoft-basic/components.md)
