490 lines
15 KiB
Markdown
490 lines
15 KiB
Markdown
# 컴포넌트 고급 기능
|
|
|
|
> **메인 문서**: [components.md](components.md)
|
|
> **관련 문서**: [g7core-api.md](g7core-api.md)
|
|
|
|
---
|
|
|
|
## 목차
|
|
|
|
1. [컴포넌트 간 이벤트 통신 (G7Core.componentEvent)](#컴포넌트-간-이벤트-통신-g7corecomponentevent)
|
|
2. [이벤트 생성 헬퍼](#이벤트-생성-헬퍼)
|
|
3. [아이콘 사용 규칙 (Font Awesome)](#아이콘-사용-규칙-font-awesome)
|
|
4. [Form 자동 바인딩 메타데이터 (bindingType)](#form-자동-바인딩-메타데이터-bindingtype)
|
|
5. [컴포넌트 개발 체크리스트](#컴포넌트-개발-체크리스트)
|
|
|
|
---
|
|
|
|
## 컴포넌트 간 이벤트 통신 (G7Core.componentEvent)
|
|
|
|
컴포넌트 간 직접 통신이 필요한 경우 `G7Core.componentEvent`를 사용합니다.
|
|
|
|
> 전체 API 레퍼런스: [g7core-api.md](g7core-api.md#컴포넌트-이벤트-g7corecomponentevent)
|
|
|
|
### 핵심 원칙
|
|
|
|
```text
|
|
주의: 가능하면 props/콜백 또는 전역 상태를 통한 통신 우선
|
|
✅ 사용 시점: 부모-자식 관계가 아닌 컴포넌트 간 통신
|
|
✅ 사용 시점: 특정 컴포넌트의 메서드를 외부에서 트리거해야 할 때
|
|
```
|
|
|
|
### API 목록
|
|
|
|
| 메서드 | 설명 | 반환값 |
|
|
|--------|------|--------|
|
|
| `on(eventName, callback)` | 이벤트 구독 | 구독 해제 함수 |
|
|
| `emit(eventName, data?)` | 이벤트 발생 | `Promise<any[]>` |
|
|
| `off(eventName)` | 특정 이벤트 리스너 제거 | `void` |
|
|
| `clear()` | 모든 리스너 제거 | `void` |
|
|
|
|
### 사용 예시: FileUploader 트리거
|
|
|
|
```tsx
|
|
// FileUploader 컴포넌트 내부
|
|
useEffect(() => {
|
|
const unsubscribe = G7Core.componentEvent.on(
|
|
`triggerUpload:${uploaderId}`,
|
|
async () => {
|
|
inputRef.current?.click();
|
|
}
|
|
);
|
|
return unsubscribe; // cleanup
|
|
}, [uploaderId]);
|
|
|
|
// 다른 컴포넌트에서 업로드 트리거
|
|
const handleTriggerUpload = () => {
|
|
G7Core.componentEvent.emit('triggerUpload:logo_uploader');
|
|
};
|
|
```
|
|
|
|
### 이벤트 네이밍 규칙
|
|
|
|
| 패턴 | 설명 | 예시 |
|
|
|------|------|------|
|
|
| `{action}` | 단순 액션 | `fileSelected`, `dataLoaded` |
|
|
| `{action}:{target}` | 특정 대상 지정 | `triggerUpload:logo_uploader` |
|
|
| `{component}.{action}` | 컴포넌트별 이벤트 | `sidebar.toggle` |
|
|
|
|
### 주의사항
|
|
|
|
```text
|
|
cleanup 필수: useEffect에서 반환된 unsubscribe 함수 호출
|
|
이벤트명 충돌 방지: 고유한 이벤트명 사용 (컴포넌트 ID 포함 권장)
|
|
✅ 비동기 지원: emit은 모든 리스너의 결과를 Promise로 반환
|
|
```
|
|
|
|
---
|
|
|
|
## 이벤트 생성 헬퍼
|
|
|
|
ActionDispatcher와 호환되는 이벤트 객체를 생성하는 헬퍼 함수들입니다.
|
|
|
|
> 전체 API 레퍼런스: [g7core-api.md](g7core-api.md#이벤트-생성-헬퍼)
|
|
|
|
### 이벤트 헬퍼 함수 목록
|
|
|
|
| 함수 | 설명 |
|
|
|------|------|
|
|
| `G7Core.createChangeEvent(value, name?)` | change 이벤트 생성 |
|
|
| `G7Core.createClickEvent(data?)` | click 이벤트 생성 |
|
|
| `G7Core.createSubmitEvent(formData?)` | submit 이벤트 생성 |
|
|
| `G7Core.createKeyboardEvent(key, modifiers?)` | keyboard 이벤트 생성 |
|
|
|
|
### 이벤트 헬퍼 사용 예시
|
|
|
|
```typescript
|
|
// Change 이벤트 생성
|
|
const changeEvent = G7Core.createChangeEvent('newValue', 'fieldName');
|
|
// { target: { value: 'newValue', name: 'fieldName' } }
|
|
|
|
// Click 이벤트 생성
|
|
const clickEvent = G7Core.createClickEvent({ itemId: 123 });
|
|
|
|
// Submit 이벤트 생성
|
|
const submitEvent = G7Core.createSubmitEvent({
|
|
name: 'John',
|
|
email: 'john@example.com'
|
|
});
|
|
|
|
// Keyboard 이벤트 생성
|
|
const keyEvent = G7Core.createKeyboardEvent('Enter', { ctrlKey: true });
|
|
```
|
|
|
|
### 사용 사례
|
|
|
|
컴포넌트에서 ActionDispatcher 핸들러를 직접 호출할 때 사용합니다:
|
|
|
|
```tsx
|
|
// 프로그래밍 방식으로 change 이벤트 발생
|
|
const handleCustomChange = (newValue: string) => {
|
|
const event = G7Core.createChangeEvent(newValue, 'customField');
|
|
onChange?.(event); // props로 전달된 onChange 호출
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 아이콘 사용 규칙 (Font Awesome)
|
|
|
|
컴포넌트에서 아이콘을 사용할 때는 Font Awesome에서 지원하는 아이콘만 사용해야 합니다.
|
|
|
|
|
|
### 핵심 원칙
|
|
|
|
```text
|
|
주의: Font Awesome에서 지원하지 않는 아이콘 사용 금지
|
|
주의: Font Awesome Pro 전용 아이콘 사용 금지 (Light, Thin, Duotone 등)
|
|
주의: 다른 아이콘 라이브러리 직접 import 금지 (Material Icons, Heroicons 등)
|
|
필수: Icon 기본 컴포넌트를 통한 아이콘 사용
|
|
필수: Font Awesome 기준 아이콘만 사용
|
|
```
|
|
|
|
### Free vs Pro 아이콘 구분
|
|
|
|
```text
|
|
필수: 그누보드7 프로젝트는 Font Awesome만 사용합니다.
|
|
Pro 버전 아이콘은 라이선스가 필요하므로 사용할 수 없습니다.
|
|
```
|
|
|
|
**Free 버전에서 사용 가능한 스타일**:
|
|
|
|
| 스타일 | 접두사 | 아이콘 수 | 설명 |
|
|
|--------|--------|----------|------|
|
|
| Solid | `fa-solid` 또는 `fas` | 1,390개 | 채워진 스타일 |
|
|
| Regular | `fa-regular` 또는 `far` | 163개 | 윤곽선 스타일 (일부만) |
|
|
| Brands | `fa-brands` 또는 `fab` | 472개 | 브랜드 로고 |
|
|
|
|
**Pro 버전 전용 (사용 금지)**:
|
|
|
|
- `fa-light` (fal) - Light 스타일
|
|
- `fa-thin` (fat) - Thin 스타일
|
|
- `fa-duotone` (fad) - Duotone 스타일
|
|
- `fa-sharp` (fass, fasr, fasl) - Sharp 스타일
|
|
|
|
### 아이콘 확인 방법
|
|
|
|
아이콘이 Free 버전에 포함되어 있는지 확인하는 방법:
|
|
|
|
2. **Font Awesome 공식 사이트**: <https://fontawesome.com/search?o=r&m=free>
|
|
- "Free" 필터가 적용된 검색 결과만 확인
|
|
3. **없는 아이콘 예시**: `layout`, `plate-utensils` 등은 Free에 없음
|
|
|
|
### 사용 가능한 아이콘 소스
|
|
|
|
```html
|
|
<!-- template.json externals 선언 -->
|
|
{
|
|
"externals": [
|
|
{
|
|
"id": "fontawesome",
|
|
"type": "style",
|
|
"url": "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css",
|
|
"preconnect": "https://cdnjs.cloudflare.com"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Font Awesome은 컴포넌트에서 직접 import하지 않고, admin/user 템플릿의 `template.json` `externals`에 선언해 Blade가 최초 HTML에서 로드합니다.
|
|
|
|
### 아이콘 사용 방법
|
|
|
|
#### Icon 컴포넌트 사용 (권장)
|
|
|
|
```tsx
|
|
import { Icon, IconName } from '../basic/Icon';
|
|
|
|
// ✅ 올바른 사용
|
|
<Icon name={IconName.Check} />
|
|
<Icon name={IconName.ChevronRight} />
|
|
<Icon name={IconName.Search} />
|
|
|
|
// 커스텀 아이콘 클래스 직접 지정 (Font Awesome)
|
|
<Icon name="fa-solid fa-user" />
|
|
<Icon name="fa-regular fa-envelope" />
|
|
```
|
|
|
|
#### IconName enum 활용
|
|
|
|
```tsx
|
|
// templates/sirsoft-admin_basic/src/components/basic/Icon.tsx
|
|
export enum IconName {
|
|
Check = 'fa-solid fa-check',
|
|
ChevronRight = 'fa-solid fa-chevron-right',
|
|
Search = 'fa-solid fa-search',
|
|
// ... Font Awesome에 존재하는 아이콘만 정의
|
|
}
|
|
```
|
|
|
|
### 아이콘 버전 확인 방법
|
|
|
|
새로운 아이콘을 추가하기 전, 해당 아이콘이 Font Awesome에서 지원되는지 확인합니다:
|
|
|
|
1. **공식 문서 확인**: https://fontawesome.com/v6/search (버전 6.4 선택)
|
|
2. **CDN 파일 직접 확인**: 아이콘 클래스가 CSS에 정의되어 있는지 확인
|
|
3. **브라우저 개발자 도구**: 실제 렌더링 여부 확인
|
|
|
|
### 잘못된 패턴
|
|
|
|
```tsx
|
|
// ❌ 잘못된 예 1: Font Awesome 6.5+ 전용 아이콘 사용
|
|
<Icon name="fa-solid fa-plate-utensils" /> // 6.5.0에서 추가된 아이콘
|
|
|
|
// ❌ 잘못된 예 2: 다른 아이콘 라이브러리 직접 import
|
|
import { HiOutlineUser } from 'react-icons/hi';
|
|
<HiOutlineUser />
|
|
|
|
// ❌ 잘못된 예 3: SVG 직접 사용 (Icon 컴포넌트 우회)
|
|
<svg className="w-5 h-5">...</svg>
|
|
```
|
|
|
|
### 아이콘 스타일 종류
|
|
|
|
Font Awesome에서 지원하는 스타일:
|
|
|
|
| 스타일 | 접두사 | 예시 |
|
|
|--------|--------|------|
|
|
| Solid | `fa-solid` 또는 `fas` | `fa-solid fa-user` |
|
|
| Regular | `fa-regular` 또는 `far` | `fa-regular fa-envelope` |
|
|
| Light | `fa-light` 또는 `fal` | `fa-light fa-bell` (Pro) |
|
|
| Brands | `fa-brands` 또는 `fab` | `fa-brands fa-github` |
|
|
|
|
> **참고**: Light, Thin, Duotone 등은 Pro 라이선스가 필요하며, 기본 CDN에서는 Solid, Regular, Brands만 사용 가능합니다.
|
|
|
|
### 새 아이콘 추가 시 체크리스트
|
|
|
|
```text
|
|
□ Font Awesome에서 지원되는 아이콘인가?
|
|
□ IconName enum에 정의되어 있는가? (없으면 추가 필요)
|
|
□ 적절한 스타일(Solid/Regular/Brands)을 사용했는가?
|
|
□ Free 버전에서 사용 가능한 아이콘인가?
|
|
```
|
|
|
|
---
|
|
|
|
## Form 자동 바인딩 메타데이터 (bindingType)
|
|
|
|
Form 자동 바인딩에서 boolean 값이 `checked` prop으로 바인딩될지 `value` prop으로 바인딩될지를 제어하는 메타데이터입니다.
|
|
|
|
### 배경
|
|
|
|
Form 내부의 입력 컴포넌트는 바인딩된 상태 값이 `boolean`일 때 자동으로 바인딩 방식을 결정합니다:
|
|
- `checked` 바인딩: Toggle, Checkbox 등 on/off 컴포넌트에 적합
|
|
- `value` 바인딩: RadioGroup, Select 등 값 선택 컴포넌트에 적합
|
|
|
|
`bindingType` 메타데이터가 없으면, boolean 값은 항상 `value` prop으로 바인딩됩니다 (기본 동작).
|
|
|
|
### bindingType 값
|
|
|
|
| 값 | 동작 | 대상 컴포넌트 |
|
|
|----|------|--------------|
|
|
| `"checked"` | boolean 값을 항상 `checked` prop으로 바인딩 | Toggle, Checkbox, ChipCheckbox |
|
|
| `"checkable"` | `type`이 `checkbox` 또는 `radio`일 때만 `checked`, 그 외 `value` | Input |
|
|
| 미지정 (기본) | boolean 값도 항상 `value` prop으로 바인딩 | RadioGroup, Select 등 |
|
|
|
|
### components.json 등록 방법
|
|
|
|
```json
|
|
{
|
|
"name": "Toggle",
|
|
"type": "basic",
|
|
"description": "토글 스위치 컴포넌트",
|
|
"bindingType": "checked",
|
|
"path": "src/components/basic/Toggle.tsx",
|
|
"props": { ... }
|
|
}
|
|
```
|
|
|
|
```json
|
|
{
|
|
"name": "Input",
|
|
"type": "basic",
|
|
"description": "입력 필드 컴포넌트",
|
|
"bindingType": "checkable",
|
|
"path": "src/components/basic/Input.tsx",
|
|
"props": { ... }
|
|
}
|
|
```
|
|
|
|
### 등록이 필요한 경우
|
|
|
|
```text
|
|
필수: 아래 조건에 해당하면 bindingType 등록
|
|
|
|
□ checked/unchecked 상태로 동작하는 컴포넌트 → "checked"
|
|
□ type prop에 따라 checked/value 동작이 달라지는 컴포넌트 → "checkable"
|
|
□ boolean 값을 선택지(value)로 사용하는 컴포넌트 → 미지정 (기본)
|
|
```
|
|
|
|
### 주의사항
|
|
|
|
```text
|
|
bindingType 미등록 시: boolean 값이 checked로 바인딩되지 않음
|
|
→ Toggle/Checkbox가 Form 내에서 정상 동작하지 않을 수 있음
|
|
|
|
잘못된 bindingType 등록 시: boolean 값이 의도하지 않은 prop으로 바인딩
|
|
→ RadioGroup에 "checked" 등록 시 boolean 선택값이 표시되지 않음
|
|
|
|
새 템플릿에 Toggle/Checkbox 유사 컴포넌트 추가 시 bindingType 등록 필수
|
|
기존 템플릿의 등록 현황: sirsoft-admin_basic, sirsoft-basic의 components.json 참조
|
|
```
|
|
|
|
---
|
|
|
|
## 컴포넌트 개발 체크리스트
|
|
|
|
새로운 컴포넌트를 개발할 때 다음 항목을 반드시 확인합니다.
|
|
|
|
### 필수 등록 항목
|
|
|
|
```text
|
|
필수: 새 컴포넌트 생성 시 아래 4개 파일에 등록
|
|
□ 컴포넌트 파일 생성: templates/[vendor-template]/src/components/{type}/{Name}.tsx
|
|
□ index.ts export 추가: templates/[vendor-template]/src/components/{type}/index.ts
|
|
□ components.json 등록: templates/[vendor-template]/components.json
|
|
□ 테스트 파일 생성: templates/[vendor-template]/src/components/{type}/__tests__/{Name}.test.tsx
|
|
```
|
|
|
|
**components.json 등록 형식**:
|
|
|
|
```json
|
|
{
|
|
"name": "ComponentName",
|
|
"type": "composite",
|
|
"description": "컴포넌트 설명",
|
|
"path": "src/components/composite/ComponentName.tsx",
|
|
"bindingType": "checked | checkable | 미지정",
|
|
"props": {
|
|
"propName": {
|
|
"type": "string",
|
|
"required": true,
|
|
"description": "prop 설명"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
> **참고**: `bindingType`은 Form 자동 바인딩에서 boolean 값의 바인딩 방식을 결정합니다. [상세 설명](#form-자동-바인딩-메타데이터-bindingtype)
|
|
|
|
### 필수 검증 항목
|
|
|
|
#### 구조 및 패턴
|
|
|
|
```text
|
|
□ HTML 태그 직접 사용하지 않았는가? (Div, Button 등 기본 컴포넌트만 사용)
|
|
□ 기존 집합 컴포넌트 재사용을 먼저 검토했는가?
|
|
□ Props 인터페이스를 명확히 정의했는가?
|
|
□ TypeScript 타입이 올바르게 정의되어 있는가?
|
|
```
|
|
|
|
#### 다국어 처리
|
|
|
|
```text
|
|
□ 하드코딩된 텍스트 없이 G7Core.t() 사용했는가?
|
|
□ t 함수를 모듈 레벨에서 선언했는가?
|
|
□ props 텍스트에 nullish coalescing(??) 기본값 설정했는가?
|
|
```
|
|
|
|
#### 스타일링
|
|
|
|
```text
|
|
□ 다크 모드 light/dark variant 함께 지정했는가?
|
|
□ Tailwind CSS 클래스만 사용했는가?
|
|
□ 반응형 breakpoint (sm:, md:, lg:, xl:) 적용했는가?
|
|
```
|
|
|
|
#### 아이콘
|
|
|
|
```text
|
|
□ Font Awesome에서 지원하는 아이콘만 사용했는가?
|
|
□ Icon 기본 컴포넌트를 통해 아이콘을 사용했는가?
|
|
□ IconName enum에 정의된 아이콘을 사용했는가?
|
|
```
|
|
|
|
#### 접근성
|
|
|
|
```text
|
|
□ aria-label 등 접근성 속성이 필요한 곳에 추가했는가?
|
|
□ 키보드 네비게이션이 가능한가?
|
|
□ 포커스 상태가 시각적으로 표시되는가?
|
|
```
|
|
|
|
### 테스트 필수 항목
|
|
|
|
```text
|
|
□ 컴포넌트 렌더링 테스트 작성했는가?
|
|
□ Props 전달 테스트 작성했는가?
|
|
□ 이벤트 핸들러 테스트 작성했는가? (onClick, onChange 등)
|
|
□ 엣지 케이스 테스트 작성했는가? (빈 값, undefined 등)
|
|
□ 스냅샷 테스트가 필요한 경우 작성했는가?
|
|
```
|
|
|
|
### 테스트 코드 작성 규칙
|
|
|
|
#### 테스트 파일 위치
|
|
|
|
```text
|
|
templates/[vendor-template]/src/components/
|
|
├── basic/
|
|
│ ├── Button.tsx
|
|
│ └── __tests__/
|
|
│ └── Button.test.tsx
|
|
├── composite/
|
|
│ ├── Card.tsx
|
|
│ └── __tests__/
|
|
│ └── Card.test.tsx
|
|
```
|
|
|
|
#### 필수 테스트 케이스
|
|
|
|
```tsx
|
|
// 예시: Card.test.tsx
|
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
import { Card } from '../Card';
|
|
|
|
describe('Card', () => {
|
|
// 1. 기본 렌더링
|
|
it('renders correctly', () => {
|
|
render(<Card title="Test Title" />);
|
|
expect(screen.getByText('Test Title')).toBeInTheDocument();
|
|
});
|
|
|
|
// 2. Props 전달
|
|
it('passes props correctly', () => {
|
|
render(<Card title="Title" content="Content" />);
|
|
expect(screen.getByText('Title')).toBeInTheDocument();
|
|
expect(screen.getByText('Content')).toBeInTheDocument();
|
|
});
|
|
|
|
// 3. 이벤트 핸들러
|
|
it('handles click events', () => {
|
|
const handleClick = vi.fn();
|
|
render(<Card title="Title" onClick={handleClick} />);
|
|
fireEvent.click(screen.getByText('Title'));
|
|
expect(handleClick).toHaveBeenCalled();
|
|
});
|
|
|
|
// 4. 엣지 케이스
|
|
it('renders without optional props', () => {
|
|
render(<Card />);
|
|
// 빈 상태에서도 에러 없이 렌더링
|
|
});
|
|
|
|
// 5. 다크 모드 클래스 (필요시)
|
|
it('includes dark mode classes', () => {
|
|
const { container } = render(<Card title="Title" />);
|
|
expect(container.firstChild).toHaveClass('dark:bg-gray-800');
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 관련 문서
|
|
|
|
- [컴포넌트 개발 규칙 인덱스](components.md)
|
|
- [컴포넌트 타입별 규칙](components-types.md)
|
|
- [컴포넌트 패턴](components-patterns.md)
|
|
- [G7Core API](g7core-api.md)
|