npm package: @happyhyep/tree-component
Description: React Tree Component with Search functionality
Author: happyhyep
Latest version: 1.0.6

# 🌳 @happyhyep/tree-component

[🇰🇷 한국어](#-한국어-ver) | [🇺🇸 English](#-english)

## 🇺🇸 English ver

> **React Tree Component Library** where **both folders and files are clickable**

[![npm version](https://badge.fury.io/js/@happyhyep%2Ftree-component.svg)](https://www.npmjs.com/package/@happyhyep/tree-component)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## ✨ Key Differentiators

Unlike other Tree components, this **revolutionary Tree component** allows **both folders and files to be clickable** for interaction.

| Feature              | Typical Tree                        | **🌳 Tree Component**                |
| -------------------- | ----------------------------------- | ------------------------------------ |
| Folder Click         | ❌ Expand/collapse only             | ✅ **Click event + expand/collapse** |
| File Click           | ✅ Clickable                        | ✅ Clickable                         |
| Search Feature       | ❌ Requires separate implementation | ✅ **Built-in search + highlight**   |
| Default Expand State | ❌ Manual setup                     | ✅ **Expand all folders at once**    |
| TypeScript           | ⚠️ Limited support                  | ✅ **Full type safety**              |

## 🎬 Demo

### Tree Component Usage Examples

![Tree Component Demo](https://cdn.jsdelivr.net/npm/@happyhyep/tree-component@1.0.2/docs/images/tree-component-example-1.gif)

## 📦 Installation

```bash
# npm
npm install @happyhyep/tree-component

# yarn
yarn add @happyhyep/tree-component

# pnpm
pnpm add @happyhyep/tree-component
```

## 🚀 Quick Start

### 1️⃣ Basic Tree Component

```tsx
import React, { useState } from 'react';
import { Tree, TreeItem } from '@happyhyep/tree-component';

interface FileData {
  name: string;
  type: 'folder' | 'file';
  size?: number;
}

const data: TreeItem<FileData>[] = [
  {
    id: '1',
    parentId: null,
    canOpen: true,
    data: { name: 'Documents', type: 'folder' },
  },
  {
    id: '2',
    parentId: '1',
    canOpen: false,
    data: { name: 'report.pdf', type: 'file', size: 1024 },
  },
  {
    id: '3',
    parentId: '1',
    canOpen: true,
    data: { name: 'Projects', type: 'folder' },
  },
];

function MyApp() {
  const [selectedId, setSelectedId] = useState<string>();

  return (
    <Tree
      items={data}
      selectedId={selectedId}
      onItemClick={(item) => {
        setSelectedId(item.id);
        console.log('Clicked item:', item.data);

        // 💡 Both folders and files are clickable!
        if (item.data.type === 'folder') {
          console.log('Folder clicked:', item.data.name);
        } else {
          console.log('File clicked:', item.data.name);
        }
      }}
      renderLabel={(data) => (
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <span>{data.type === 'folder' ? '📁' : '📄'}</span>
          <span>{data.name}</span>
          {data.size && <span>({data.size}KB)</span>}
        </div>
      )}
    />
  );
}
```

### 2️⃣ Tree with Search

```tsx
import { TreeWithSearch } from '@happyhyep/tree-component';

function SearchableTree() {
  const [selectedId, setSelectedId] = useState<string>();

  return (
    <TreeWithSearch
      items={data}
      selectedId={selectedId}
      onItemClick={(item) => setSelectedId(item.id)}
      renderLabel={(data, highlight) => (
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <span>{data.type === 'folder' ? '📁' : '📄'}</span>
          {/* 🔍 Automatic search term highlighting */}
          <HighlightText text={data.name} highlight={highlight} />
        </div>
      )}
      searchFn={(data, keyword) => data.name.toLowerCase().includes(keyword.toLowerCase())}
    >
      {/* 🎯 Built-in search input */}
      <TreeWithSearch.Input placeholder="Search files..." />
    </TreeWithSearch>
  );
}

// Highlight helper component
const HighlightText = ({ text, highlight }) => {
  if (!highlight) return <span>{text}</span>;

  const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
  return (
    <span>
      {parts.map((part, i) =>
        part.toLowerCase() === highlight.toLowerCase() ? <mark key={i}>{part}</mark> : part,
      )}
    </span>
  );
};
```

### 3️⃣ Advanced Usage - Default Expand All

```tsx
function ExpandedTree() {
  return (
    <Tree
      items={data}
      defaultExpandAll={true} // 🚀 Expand all folders by default
      renderLabel={(data) => <span>{data.name}</span>}
    />
  );
}
```

## 📋 Real-world Use Cases

### File Explorer

```tsx
import { Tree } from '@happyhyep/tree-component';

function FileExplorer() {
  const [selectedFile, setSelectedFile] = useState(null);

  const handleItemClick = (item) => {
    if (item.data.type === 'file') {
      // File click - open file
      openFile(item.data);
    } else {
      // Folder click - select folder (expand/collapse is automatic)
      setSelectedFolder(item.data);
    }
  };

  return (
    <div style={{ display: 'flex' }}>
      <Tree
        items={fileSystemData}
        onItemClick={handleItemClick}
        renderLabel={(data) => <FileIcon type={data.type} name={data.name} />}
      />
      {selectedFile && <FilePreview file={selectedFile} />}
    </div>
  );
}
```

### Organization Chart / Hierarchy

```tsx
function OrganizationChart() {
  return (
    <Tree
      items={orgData}
      defaultExpandAll={true}
      onItemClick={(item) => {
        // Both departments and employees are clickable
        showPersonDetails(item.data);
      }}
      renderLabel={(data) => (
        <div>
          <strong>{data.name}</strong>
          <span>({data.position})</span>
        </div>
      )}
    />
  );
}
```

## 🔧 API Documentation

### TreeItem Interface

```tsx
interface TreeItem<T = unknown> {
  id: string; // Unique identifier
  parentId: string | null; // Parent ID (null for root)
  data: T; // User data
  canOpen?: boolean; // Whether it can be expanded
  hasLeaf?: boolean; // Whether it's a leaf node
  children?: TreeItem<T>[]; // Child nodes (auto-generated)
}
```

### Tree Props

| Props              | Type                          | Required | Default | Description                            |
| ------------------ | ----------------------------- | -------- | ------- | -------------------------------------- |
| `items`            | `TreeItem<T>[]`               | ✅       | -       | Tree data                              |
| `renderLabel`      | `(data: T) => ReactNode`      | ✅       | -       | Label rendering function               |
| `onItemClick`      | `(item: TreeItem<T>) => void` | ❌       | -       | **Click handler (both folders/files)** |
| `selectedId`       | `string`                      | ❌       | -       | Selected item ID                       |
| `defaultExpandAll` | `boolean`                     | ❌       | `false` | **Expand all folders by default**      |
| `className`        | `string`                      | ❌       | `""`    | CSS class                              |

### TreeWithSearch Props

All Tree props + additional:

| Props      | Type                                    | Required | Description         |
| ---------- | --------------------------------------- | -------- | ------------------- |
| `searchFn` | `(data: T, keyword: string) => boolean` | ✅       | **Search function** |
| `children` | `ReactNode`                             | ❌       | Search input, etc.  |

## 💡 Tips and Tricks

### 1. Conditional Click Handling

```tsx
const handleClick = (item) => {
  if (item.data.type === 'folder') {
    // Folder click - special logic
    if (item.data.permissions?.canAccess) {
      navigateToFolder(item);
    } else {
      showPermissionError();
    }
  } else {
    // File click - open file
    openFile(item);
  }
};
```

### 2. Custom Search

```tsx
// Multi-condition search
const advancedSearch = (data, keyword) => {
  return (
    data.name.toLowerCase().includes(keyword.toLowerCase()) ||
    data.tags?.some((tag) => tag.includes(keyword)) ||
    data.content?.includes(keyword)
  );
};

// Extension search
const extensionSearch = (data, keyword) => {
  const extension = data.name.split('.').pop();
  return extension?.toLowerCase().includes(keyword.toLowerCase());
};
```

### 3. Performance Optimization

```tsx
// Memoization for large datasets
const MemoizedTree = React.memo(() => (
  <Tree
    items={largeDataSet}
    renderLabel={React.useCallback(
      (data) => (
        <span>{data.name}</span>
      ),
      [],
    )}
  />
));
```

## 🎨 Styling

### Using CSS Classes

```tsx
<Tree
  className="my-custom-tree"
  items={data}
  renderLabel={(data) => <span className={`item-${data.type}`}>{data.name}</span>}
/>
```

```css
.my-custom-tree {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
}

.item-folder {
  font-weight: bold;
  color: #4a90e2;
}

.item-file {
  color: #666;
}
```

## 🛠️ Development

### Local Development Setup

```bash
# Clone repository
git clone https://github.com/happyhyep/tree-component.git
cd tree-component

# Install dependencies
pnpm install

# Run Storybook
pnpm run storybook

# Build
pnpm run build

# Lint
pnpm run lint
```

### Storybook

Component documentation and examples are available in Storybook:

```bash
pnpm run storybook
```

## 🤝 Contributing

1. Fork this repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

## 📄 License

This project is licensed under the [MIT License](LICENSE).

## 🙋‍♂️ Support

- 🐛 Bug Reports: [GitHub Issues](https://github.com/happyhyep/directory-tree/issues)
- 💡 Feature Requests: [GitHub Discussions](https://github.com/happyhyep/directory-tree/discussions)
- 📧 Email: jhi2359@naver.com

---

## 🇰🇷 한국어 ver

**폴더와 파일 모두 클릭 가능한** React Tree 컴포넌트 라이브러리 입니다.

[![npm version](https://badge.fury.io/js/@happyhyep%2Ftree-component.svg)](https://www.npmjs.com/package/@happyhyep/tree-component)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## ✨ 주요 차별점

다른 Tree 컴포넌트와 달리 **폴더와 파일 모두 클릭**하여 상호작용할 수 있는 Tree 컴포넌트입니다.

| 기능           | 일반적인 Tree     | **🌳 Tree Component**            |
| -------------- | ----------------- | -------------------------------- |
| 폴더 클릭      | ❌ 펼치기/접기만  | ✅ **클릭 이벤트 + 펼치기/접기** |
| 파일 클릭      | ✅ 클릭 가능      | ✅ 클릭 가능                     |
| 검색 기능      | ❌ 별도 구현 필요 | ✅ **내장 검색 + 하이라이트**    |
| 기본 확장 상태 | ❌ 수동 설정      | ✅ **한 번에 모든 폴더 확장**    |
| TypeScript     | ⚠️ 제한적 지원    | ✅ **완전한 타입 안전성**        |

## 🎬 데모

### Tree 컴포넌트 사용 예시

![Basic Tree Demo](https://cdn.jsdelivr.net/npm/@happyhyep/tree-component@1.0.2/docs/images/tree-component-example-1.gif)

## 📦 설치

```bash
# npm
npm install @happyhyep/tree-component

# yarn
yarn add @happyhyep/tree-component

# pnpm
pnpm add @happyhyep/tree-component
```

## 🚀 빠른 시작

### 1️⃣ 기본 Tree 컴포넌트

```tsx
import React, { useState } from 'react';
import { Tree, TreeItem } from '@happyhyep/tree-component';

interface FileData {
  name: string;
  type: 'folder' | 'file';
  size?: number;
}

const data: TreeItem<FileData>[] = [
  {
    id: '1',
    parentId: null,
    canOpen: true,
    data: { name: 'Documents', type: 'folder' },
  },
  {
    id: '2',
    parentId: '1',
    canOpen: false,
    data: { name: 'report.pdf', type: 'file', size: 1024 },
  },
  {
    id: '3',
    parentId: '1',
    canOpen: true,
    data: { name: 'Projects', type: 'folder' },
  },
];

function MyApp() {
  const [selectedId, setSelectedId] = useState<string>();

  return (
    <Tree
      items={data}
      selectedId={selectedId}
      onItemClick={(item) => {
        setSelectedId(item.id);
        console.log('클릭된 항목:', item.data);

        // 💡 폴더든 파일이든 모두 클릭 가능!
        if (item.data.type === 'folder') {
          console.log('폴더 클릭:', item.data.name);
        } else {
          console.log('파일 클릭:', item.data.name);
        }
      }}
      renderLabel={(data) => (
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <span>{data.type === 'folder' ? '📁' : '📄'}</span>
          <span>{data.name}</span>
          {data.size && <span>({data.size}KB)</span>}
        </div>
      )}
    />
  );
}
```

### 2️⃣ 검색 기능이 있는 Tree

```tsx
import { TreeWithSearch } from '@happyhyep/tree-component';

function SearchableTree() {
  const [selectedId, setSelectedId] = useState<string>();

  return (
    <TreeWithSearch
      items={data}
      selectedId={selectedId}
      onItemClick={(item) => setSelectedId(item.id)}
      renderLabel={(data, highlight) => (
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <span>{data.type === 'folder' ? '📁' : '📄'}</span>
          {/* 🔍 검색어 자동 하이라이트 */}
          <HighlightText text={data.name} highlight={highlight} />
        </div>
      )}
      searchFn={(data, keyword) => data.name.toLowerCase().includes(keyword.toLowerCase())}
    >
      {/* 🎯 내장 검색 입력창 */}
      <TreeWithSearch.Input placeholder="파일명 검색..." />
    </TreeWithSearch>
  );
}

// 하이라이트 헬퍼 컴포넌트
const HighlightText = ({ text, highlight }) => {
  if (!highlight) return <span>{text}</span>;

  const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
  return (
    <span>
      {parts.map((part, i) =>
        part.toLowerCase() === highlight.toLowerCase() ? <mark key={i}>{part}</mark> : part,
      )}
    </span>
  );
};
```

### 3️⃣ 고급 사용법 - 모든 폴더 기본 확장

```tsx
function ExpandedTree() {
  return (
    <Tree
      items={data}
      defaultExpandAll={true} // 🚀 모든 폴더 기본 확장
      renderLabel={(data) => <span>{data.name}</span>}
    />
  );
}
```

## 📋 실제 사용 사례

### 파일 탐색기

```tsx
import { Tree } from '@happyhyep/tree-component';

function FileExplorer() {
  const [selectedFile, setSelectedFile] = useState(null);

  const handleItemClick = (item) => {
    if (item.data.type === 'file') {
      // 파일 클릭 시 - 파일 열기
      openFile(item.data);
    } else {
      // 폴더 클릭 시 - 폴더 선택 (펼치기/접기는 자동)
      setSelectedFolder(item.data);
    }
  };

  return (
    <div style={{ display: 'flex' }}>
      <Tree
        items={fileSystemData}
        onItemClick={handleItemClick}
        renderLabel={(data) => <FileIcon type={data.type} name={data.name} />}
      />
      {selectedFile && <FilePreview file={selectedFile} />}
    </div>
  );
}
```

### 조직도 / 계층 구조

```tsx
function OrganizationChart() {
  return (
    <Tree
      items={orgData}
      defaultExpandAll={true}
      onItemClick={(item) => {
        // 부서든 직원이든 클릭 가능
        showPersonDetails(item.data);
      }}
      renderLabel={(data) => (
        <div>
          <strong>{data.name}</strong>
          <span>({data.position})</span>
        </div>
      )}
    />
  );
}
```

## 🔧 API 문서

### TreeItem 인터페이스

```tsx
interface TreeItem<T = unknown> {
  id: string; // 고유 식별자
  parentId: string | null; // 부모 ID (루트는 null)
  data: T; // 사용자 데이터
  canOpen?: boolean; // 펼칠 수 있는지 여부
  hasLeaf?: boolean; // 리프 노드 여부
  children?: TreeItem<T>[]; // 자식 노드 (자동 생성)
}
```

### Tree Props

| Props              | 타입                          | 필수 | 기본값  | 설명                             |
| ------------------ | ----------------------------- | ---- | ------- | -------------------------------- |
| `items`            | `TreeItem<T>[]`               | ✅   | -       | 트리 데이터                      |
| `renderLabel`      | `(data: T) => ReactNode`      | ✅   | -       | 라벨 렌더링                      |
| `onItemClick`      | `(item: TreeItem<T>) => void` | ❌   | -       | **클릭 핸들러 (폴더/파일 모두)** |
| `selectedId`       | `string`                      | ❌   | -       | 선택된 항목 ID                   |
| `defaultExpandAll` | `boolean`                     | ❌   | `false` | **모든 폴더 기본 확장**          |
| `className`        | `string`                      | ❌   | `""`    | CSS 클래스                       |

### TreeWithSearch Props

Tree의 모든 props + 추가:

| Props      | 타입                                    | 필수 | 설명           |
| ---------- | --------------------------------------- | ---- | -------------- |
| `searchFn` | `(data: T, keyword: string) => boolean` | ✅   | **검색 함수**  |
| `children` | `ReactNode`                             | ❌   | 검색 입력창 등 |

## 💡 팁과 트릭

### 1. 조건부 클릭 처리

```tsx
const handleClick = (item) => {
  if (item.data.type === 'folder') {
    // 폴더 클릭 - 특별한 로직
    if (item.data.permissions?.canAccess) {
      navigateToFolder(item);
    } else {
      showPermissionError();
    }
  } else {
    // 파일 클릭 - 파일 열기
    openFile(item);
  }
};
```

### 2. 커스텀 검색

```tsx
// 다중 조건 검색
const advancedSearch = (data, keyword) => {
  return (
    data.name.toLowerCase().includes(keyword.toLowerCase()) ||
    data.tags?.some((tag) => tag.includes(keyword)) ||
    data.content?.includes(keyword)
  );
};

// 확장자 검색
const extensionSearch = (data, keyword) => {
  const extension = data.name.split('.').pop();
  return extension?.toLowerCase().includes(keyword.toLowerCase());
};
```

### 3. 성능 최적화

```tsx
// 큰 데이터셋을 위한 메모이제이션
const MemoizedTree = React.memo(() => (
  <Tree
    items={largeDataSet}
    renderLabel={React.useCallback(
      (data) => (
        <span>{data.name}</span>
      ),
      [],
    )}
  />
));
```

## 🎨 스타일링

### CSS 클래스 사용

```tsx
<Tree
  className="my-custom-tree"
  items={data}
  renderLabel={(data) => <span className={`item-${data.type}`}>{data.name}</span>}
/>
```

```css
.my-custom-tree {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
}

.item-folder {
  font-weight: bold;
  color: #4a90e2;
}

.item-file {
  color: #666;
}
```

## 🛠️ 개발

### 로컬 개발 환경

```bash
# 저장소 클론
git clone https://github.com/happyhyep/tree-component.git
cd tree-component

# 의존성 설치
pnpm install

# Storybook 실행
pnpm run storybook

# 빌드
pnpm run build

# 린트
pnpm run lint
```

### Storybook

컴포넌트 문서와 예제는 Storybook에서 확인할 수 있습니다:

```bash
pnpm run storybook
```

## 🤝 기여하기

1. 이 저장소를 포크하세요
2. 기능 브랜치를 만드세요 (`git checkout -b feature/amazing-feature`)
3. 변경사항을 커밋하세요 (`git commit -m 'Add amazing feature'`)
4. 브랜치에 푸시하세요 (`git push origin feature/amazing-feature`)
5. Pull Request를 열어주세요

## 📄 라이센스

이 프로젝트는 [MIT 라이센스](LICENSE) 하에 배포됩니다.

## 🙋‍♂️ 지원

- 🐛 버그 리포트: [GitHub Issues](https://github.com/happyhyep/directory-tree/issues)
- 💡 기능 요청: [GitHub Discussions](https://github.com/happyhyep/directory-tree/discussions)
- 📧 이메일: jhi2359@naver.com