> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# ID Async Select

> 관계 데이터를 선택하는 비동기 셀렉트 컴포넌트

외래키 관계를 처리하기 위한 ID Async Select 컴포넌트 사용법을 알아봅니다.

## ID Async Select 개요

<CardGroup cols={2}>
  <Card title="비동기 로딩" icon="clock">
    API 호출

    자동 완성
  </Card>

  <Card title="타입 안전" icon="shield-check">
    Entity 기반

    ID 타입 보장
  </Card>

  <Card title="검색 지원" icon="magnifying-glass">
    실시간 필터링

    디바운스
  </Card>

  <Card title="다중 선택" icon="list-check">
    단일/다중 모드

    배열 반환
  </Card>
</CardGroup>

## ID Async Select란?

### 문제: 외래키 선택의 복잡성

데이터베이스에서 외래키 관계를 처리할 때, 프론트엔드에서는 **관련 엔티티를 선택**해야 합니다.

**전통적인 방식의 문제점**:

```typescript theme={null}
// ❌ 모든 사용자를 한 번에 로드 (비효율적)
const [users, setUsers] = useState([]);

useEffect(() => {
  userService.list({ pageSize: 1000 }).then(({ users }) => {
    setUsers(users); // 1000명 전부 로드!
  });
}, []);

return (
  <select>
    {users.map((user) => (
      <option key={user.id} value={user.id}>
        {user.username}
      </option>
    ))}
  </select>
);
```

**문제점**:

1. **성능**: 데이터가 많으면 로딩 시간 증가
2. **메모리**: 불필요한 데이터를 모두 메모리에 적재
3. **UX**: 사용자가 원하는 항목을 찾기 어려움
4. **확장성**: 데이터가 계속 늘어나면 감당 불가

### 해결: ID Async Select

Sonamu의 ID Async Select는 **필요할 때만 비동기로 로드**하고 **검색 기능**을 제공합니다.

```typescript theme={null}
// ✅ 검색어 기반으로 필요한 데이터만 로드
import { IdAsyncSelect } from "@sonamu-kit/react-components";
import { UserAsyncIdConfig } from "@/services/services.generated";

<IdAsyncSelect
  config={UserAsyncIdConfig}
  subset="A"
  value={selectedUserId}
  onValueChange={setSelectedUserId}
/>
```

**장점**:

1. ✨ **성능**: 처음에는 최소한만 로드, 검색 시 필요한 것만
2. ✨ **사용자 경험**: 자동완성으로 빠른 선택
3. ✨ **타입 안전**: Entity 타입이 자동으로 적용
4. ✨ **확장성**: 데이터가 아무리 많아도 문제없음

## 기본 사용법

### 필수 Props

ID Async Select는 다음 두 가지 핵심 props를 사용합니다:

* `config`: `services.generated.ts`에서 자동 생성된 AsyncIdConfig 객체
* `subset`: 조회할 Subset 키 (예: "A", "D" 등)

```typescript theme={null}
import { IdAsyncSelect } from "@sonamu-kit/react-components";
import { UserAsyncIdConfig } from "@/services/services.generated";

<IdAsyncSelect
  config={UserAsyncIdConfig}
  subset="A"
  value={userId}
  onValueChange={setUserId}
/>
```

### 단일 선택

하나의 엔티티를 선택합니다.

```typescript theme={null}
import { IdAsyncSelect } from "@sonamu-kit/react-components";
import { UserAsyncIdConfig } from "@/services/services.generated";
import { useState } from "react";

function PostForm() {
  const [authorId, setAuthorId] = useState<number | null>(null);

  return (
    <div>
      <label>Author</label>
      <IdAsyncSelect
        config={UserAsyncIdConfig}
        subset="A"
        value={authorId}
        onValueChange={setAuthorId}
        displayField="username"
        placeholder="Select author..."
      />
    </div>
  );
}
```

**동작 방식**:

1. 사용자가 타이핑하면 검색 API 호출
2. 검색 결과를 드롭다운에 표시
3. 선택 시 ID만 저장 (메모리 효율적)

### 다중 선택

여러 엔티티를 선택합니다.

```typescript theme={null}
import { TagAsyncIdConfig } from "@/services/services.generated";

function PostForm() {
  const [tagIds, setTagIds] = useState<number[]>([]);

  return (
    <div>
      <label>Tags</label>
      <IdAsyncSelect
        config={TagAsyncIdConfig}
        subset="A"
        value={tagIds}
        onValueChange={setTagIds}
        displayField="name"
        multiple
        placeholder="Select tags..."
      />
    </div>
  );
}
```

**다중 선택 특징**:

* 선택된 항목을 태그 형태로 표시
* X 버튼으로 개별 제거
* 배열 형태로 ID 반환 (`number[]`)

### 초기값 설정

기존 데이터 수정 시 초기값을 설정합니다.

```typescript theme={null}
function EditPostForm({ post }: { post: Post }) {
  const [authorId, setAuthorId] = useState(post.author_id);

  return (
    <IdAsyncSelect
      config={UserAsyncIdConfig}
      subset="A"
      value={authorId}
      onValueChange={setAuthorId}
      displayField="username"
    />
  );
}
```

**초기값 처리**:

* `value`에 ID를 전달하면 자동으로 해당 엔티티 로드
* 레이블 표시를 위해 백그라운드에서 단건 조회
* 로딩 중에는 ID만 표시

## displayField 옵션

### 필드명으로 지정

가장 간단한 방법으로, 표시할 필드명을 문자열로 지정합니다.

```typescript theme={null}
<IdAsyncSelect
  config={UserAsyncIdConfig}
  subset="A"
  value={userId}
  onValueChange={setUserId}
  displayField="username"
/>
```

### 콜백 함수로 지정

복잡한 레이블이 필요한 경우 콜백 함수를 사용합니다.

```typescript theme={null}
<IdAsyncSelect
  config={EmployeeAsyncIdConfig}
  subset="A"
  value={employeeId}
  onValueChange={setEmployeeId}
  displayField={(row) =>
    `${row.employee_number} (소속 부서: ${row.department?.name})`
  }
/>
```

### 자동 탐지 (displayField 생략)

`displayField`를 생략하면 자동으로 적절한 필드를 탐지합니다.

```typescript theme={null}
// displayField 생략 시 자동 탐지 적용
<IdAsyncSelect
  config={UserAsyncIdConfig}
  subset="A"
  value={userId}
  onValueChange={setUserId}
/>
```

**탐지 우선순위**:

1. name-like 필드: `name`, `title`, `label`, `display_name`, `username`
2. 첫 번째 string 타입 컬럼 (id 제외)
3. fallback: `id`

## 고급 사용법

### baseListParams로 검색 조건 전달

기본 검색 파라미터를 설정할 수 있습니다.

```typescript theme={null}
<IdAsyncSelect
  config={UserAsyncIdConfig}
  subset="A"
  value={userId}
  onValueChange={setUserId}
  displayField="username"
  // 검색 시 추가 조건 적용
  baseListParams={{
    search: "username",
    role: "admin"
  }}
/>
```

### onRowChange로 전체 Row 데이터 받기

ID뿐만 아니라 선택된 Row 전체 데이터가 필요한 경우:

```typescript theme={null}
function SelectWithRowData() {
  const [userId, setUserId] = useState<number | null>(null);
  const [selectedUser, setSelectedUser] = useState<User | undefined>();

  return (
    <IdAsyncSelect
      config={UserAsyncIdConfig}
      subset="A"
      value={userId}
      onValueChange={setUserId}
      onRowChange={(row) => setSelectedUser(row as User)}
      displayField="username"
    />
  );
}
```

### valueField 변경

기본적으로 `id` 필드를 값으로 사용하지만, 다른 필드를 사용할 수도 있습니다.

```typescript theme={null}
<IdAsyncSelect
  config={UserAsyncIdConfig}
  subset="A"
  value={userUuid}
  onValueChange={setUserUuid}
  valueField="uuid"
  displayField="username"
/>
```

## 타입 안전성

### AsyncIdConfig 구조

`services.generated.ts`에서 자동 생성되는 Config는 다음 구조를 갖습니다:

```typescript theme={null}
// services.generated.ts에서 자동 생성
export const UserAsyncIdConfig: AsyncIdConfig<
  "A" | "D", // TSubsetKey - 사용 가능한 Subset 키
  UserSubsetMapping, // TSubsetMapping - Subset별 타입 매핑
  UserListParams // TListParams - 검색 파라미터 타입
> = {
  placeholderKey: "user.placeholder",
  useList: UserService.useList,
};
```

### 타입 추론 활용

Config를 통해 타입이 자동으로 추론됩니다:

```typescript theme={null}
<IdAsyncSelect
  config={UserAsyncIdConfig}
  subset="A"                    // "A" | "D" 중 선택
  displayField="username"       // UserSubsetA의 필드만 자동완성
  baseListParams={{ role: "admin" }}  // UserListParams 기반 자동완성
  value={userId}
  onValueChange={setUserId}
  onRowChange={(row) => {
    // row의 타입이 UserSubsetA로 추론됨
    console.log(row.username);
  }}
/>
```

## 실전 예제

### 게시글 작성 폼

```typescript theme={null}
import { useState } from "react";
import { IdAsyncSelect } from "@sonamu-kit/react-components";
import {
  UserAsyncIdConfig,
  CategoryAsyncIdConfig,
  TagAsyncIdConfig
} from "@/services/services.generated";
import { postService } from "@/services/post.service";

function CreatePostForm() {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [authorId, setAuthorId] = useState<number | null>(null);
  const [categoryId, setCategoryId] = useState<number | null>(null);
  const [tagIds, setTagIds] = useState<number[]>([]);

  async function handleSubmit() {
    if (!authorId || !categoryId) {
      alert("Please select author and category");
      return;
    }

    await postService.create({
      title,
      content,
      author_id: authorId,
      category_id: categoryId,
      tag_ids: tagIds,
    });
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>Title</label>
        <input
          value={title}
          onChange={(e) => setTitle(e.target.value)}
        />
      </div>

      <div>
        <label>Content</label>
        <textarea
          value={content}
          onChange={(e) => setContent(e.target.value)}
        />
      </div>

      {/* 작성자 선택 (단일) */}
      <div>
        <label>Author *</label>
        <IdAsyncSelect
          config={UserAsyncIdConfig}
          subset="A"
          value={authorId}
          onValueChange={setAuthorId}
          displayField="username"
          placeholder="Select author..."
        />
      </div>

      {/* 카테고리 선택 (단일) */}
      <div>
        <label>Category *</label>
        <IdAsyncSelect
          config={CategoryAsyncIdConfig}
          subset="A"
          value={categoryId}
          onValueChange={setCategoryId}
          displayField="name"
          placeholder="Select category..."
        />
      </div>

      {/* 태그 선택 (다중) */}
      <div>
        <label>Tags</label>
        <IdAsyncSelect
          config={TagAsyncIdConfig}
          subset="A"
          value={tagIds}
          onValueChange={setTagIds}
          displayField="name"
          multiple
          placeholder="Select tags..."
        />
      </div>

      <button type="submit">Create Post</button>
    </form>
  );
}
```

### 계층적 선택

부모를 선택한 후 자식을 선택하는 패턴입니다.

```typescript theme={null}
import {
  DepartmentAsyncIdConfig,
  EmployeeAsyncIdConfig
} from "@/services/services.generated";

function EmployeeAssignForm() {
  const [departmentId, setDepartmentId] = useState<number | null>(null);
  const [employeeId, setEmployeeId] = useState<number | null>(null);

  return (
    <div>
      {/* 1단계: 부서 선택 */}
      <div>
        <label>Department</label>
        <IdAsyncSelect
          config={DepartmentAsyncIdConfig}
          subset="A"
          value={departmentId}
          onValueChange={(id) => {
            setDepartmentId(id);
            setEmployeeId(null); // 부서 변경 시 직원 초기화
          }}
          displayField="name"
        />
      </div>

      {/* 2단계: 직원 선택 (부서 선택 후에만) */}
      {departmentId && (
        <div>
          <label>Employee</label>
          <IdAsyncSelect
            config={EmployeeAsyncIdConfig}
            subset="A"
            value={employeeId}
            onValueChange={setEmployeeId}
            displayField="employee_number"
            // 선택된 부서에 속한 직원만 조회
            baseListParams={{ department_id: departmentId }}
          />
        </div>
      )}
    </div>
  );
}
```

### 폼과 함께 사용

Sonamu의 `useForm`과 함께 사용하는 예제입니다.

```typescript theme={null}
import { useForm } from "@sonamu-kit/react-components";
import { CompanyAsyncIdConfig } from "@/services/services.generated";

function CompanySelectForm() {
  const form = useForm({
    defaultValues: { company_id: null as number | null },
  });

  return (
    <IdAsyncSelect
      config={CompanyAsyncIdConfig}
      subset="A"
      displayField="name"
      {...form.register("company_id")}
      placeholder="회사를 검색하세요"
    />
  );
}
```

## Props 레퍼런스

| Prop             | 타입                            | 필수 | 설명                              |
| ---------------- | ----------------------------- | -- | ------------------------------- |
| `config`         | `AsyncIdConfig`               | ✓  | Entity의 AsyncIdConfig 객체        |
| `subset`         | `string`                      | ✓  | 조회할 Subset 키                    |
| `value`          | `TValue \| TValue[] \| null`  |    | 선택된 값                           |
| `onValueChange`  | `(value) => void`             |    | 값 변경 콜백                         |
| `onRowChange`    | `(row) => void`               |    | Row 데이터 변경 콜백                   |
| `displayField`   | `string \| ((row) => string)` |    | 표시 필드 (생략 시 자동 탐지)              |
| `valueField`     | `string`                      |    | 값 필드 (기본: "id")                 |
| `baseListParams` | `object`                      |    | 검색 시 추가 파라미터                    |
| `preload`        | `boolean`                     |    | 마운트 시 전체 목록 즉시 로드 (기본: `false`) |
| `searchable`     | `boolean`                     |    | 드롭다운 내 검색 필터 표시                 |
| `multiple`       | `boolean`                     |    | 다중 선택 모드                        |
| `placeholder`    | `string`                      |    | 플레이스홀더 텍스트                      |
| `clearable`      | `boolean`                     |    | 선택 해제 가능 여부                     |
| `disabled`       | `boolean`                     |    | 비활성화 여부                         |
| `className`      | `string`                      |    | 추가 CSS 클래스                      |

### `preload`와 `searchable` 조합

`preload`와 `searchable` 값에 따라 컴포넌트의 렌더링 모드가 달라집니다:

| `preload` | `searchable` | 동작                                          |
| --------- | ------------ | ------------------------------------------- |
| `false`   | 미지정          | **기본 async 모드**: 사용자가 키워드를 입력하면 서버에서 검색     |
| `true`    | 미지정          | **sync 드롭다운 모드**: 마운트 시 전체 목록 로드, 검색창 없이 표시 |
| `true`    | `true`       | **하이브리드 모드**: 마운트 시 전체 목록 로드 + 서버 검색 필터 표시  |

<Info>
  `baseListParams`에 의미 있는 필터 값이 있는 경우에도 `preload`와 동일하게 sync 드롭다운 모드로 동작합니다.
</Info>

## 주의사항

<Warning>
  **ID Async Select 사용 시 주의사항**: 1. `config`는 `services.generated.ts`에서 자동 생성된 객체
  사용 2. `subset`은 Entity에 정의된 유효한 Subset 키여야 함 3. 다중 선택 시 **배열 타입** 확인 4.
  `displayField`로 지정한 필드는 해당 Subset에 포함되어 있어야 함 5. 초기값 로딩 실패 시 에러 처리
  필수
</Warning>

## 다음 단계

<CardGroup cols={2}>
  <Card title="View Scaffolding" icon="wand-magic-sparkles" href="/ko/frontend-integration/react-components/scaffolding-views">
    자동 뷰 생성
  </Card>

  <Card title="Search Input" icon="magnifying-glass" href="/ko/frontend-integration/react-components/search-input">
    검색 컴포넌트
  </Card>

  <Card title="커스텀 컴포넌트" icon="sliders" href="/ko/frontend-integration/react-components/custom-components">
    컴포넌트 커스터마이징
  </Card>

  <Card title="Service 사용하기" icon="code" href="/ko/frontend-integration/generated-services/using-services">
    Service 통합
  </Card>
</CardGroup>
