> ## 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.

# 프론트엔드 설정

> SonamuProvider로 프론트엔드 초기 설정하기

Sonamu 프론트엔드 통합의 시작점인 `SonamuProvider` 설정 방법을 알아봅니다. 인증, 파일 업로드, 다국어 지원을 한 곳에서 구성할 수 있습니다.

## 설정 개요

<CardGroup cols={2}>
  <Card title="인증 통합" icon="key">
    UserService.useMe 연결

    로그인/로그아웃 흐름
  </Card>

  <Card title="파일 업로드" icon="upload">
    FileService 통합

    useTypeForm 자동 연결
  </Card>

  <Card title="다국어 지원" icon="language">
    SD 함수 제공

    타입 안전한 번역
  </Card>

  <Card title="전역 상태" icon="globe">
    Context API 기반

    모든 컴포넌트에서 접근
  </Card>
</CardGroup>

## 기본 설정

### 1. SonamuProvider 설정 파일 생성

프로젝트의 `src/contexts/sonamu-provider.tsx` 파일을 생성합니다.

```typescript theme={null}
// src/contexts/sonamu-provider.tsx
import {
  SonamuProvider as BaseSonamuProvider,
  useSonamuBaseContext,
} from "@sonamu-kit/react-components";
import { type SonamuFile } from "@sonamu-kit/react-components";
import { type BetterAuthClientOptions } from "better-auth/client";
import { inferAdditionalFields } from "better-auth/client/plugins";
import { type ReactNode } from "react";

import { type MergedDictionary } from "@/i18n/sd.generated";
import { SD } from "@/i18n/sd.generated";
import { FileService } from "@/services/services.generated";

// better-auth 클라이언트 옵션 정의
const authOptions = {
  plugins: [
    inferAdditionalFields({
      user: {
        role: { type: "string" },
      },
    }),
  ],
} satisfies BetterAuthClientOptions;

// 타입이 지정된 useSonamuContext
export function useSonamuContext() {
  return useSonamuBaseContext<MergedDictionary, typeof authOptions>();
}

export function SonamuProvider({ children }: { children: ReactNode }) {
  const uploadMutation = FileService.useUploadMutation();

  const uploader = async (files: File[]): Promise<SonamuFile[]> => {
    if (files.length === 0) {
      return [];
    }

    const result = await uploadMutation.mutateAsync({ files });
    return result.files;
  };

  return (
    <BaseSonamuProvider<MergedDictionary> authOptions={authOptions} uploader={uploader} SD={SD}>
      {children}
    </BaseSonamuProvider>
  );
}
```

### 2. App에 적용

`__root.tsx`에서 `SonamuProvider`를 설정합니다.

```typescript theme={null}
// src/routes/__root.tsx
import { QueryClientProvider } from "@tanstack/react-query";
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
import { SonamuProvider } from "@/contexts/sonamu-provider";

export const Route = createRootRouteWithContext()({
  component: RootComponent,
});

function RootComponent() {
  const { queryClient } = Route.useRouteContext();

  return (
    <QueryClientProvider client={queryClient}>
      <SonamuProvider>
        <Outlet />
      </SonamuProvider>
    </QueryClientProvider>
  );
}
```

<Info>
  **왜 SonamuProvider를 별도 파일로 만드나요?**

  `SonamuProvider`는 `FileService.useUploadMutation()` 같은 React 훅을 사용하므로 `QueryClientProvider` 아래에 배치해야 합니다.
  별도 파일로 분리하면 `authOptions`, `useSonamuContext` 등의 설정과 타입을 함께 관리할 수 있습니다.
</Info>

## 인증 설정

Sonamu는 [better-auth](https://www.better-auth.com/)를 기반으로 인증을 처리합니다. `SonamuProvider`에 `authOptions`를 전달하면 내부적으로 better-auth 클라이언트가 생성됩니다.

### authOptions 정의

better-auth 클라이언트 옵션을 정의합니다. 플러그인을 통해 사용자 필드를 확장할 수 있습니다.

```typescript theme={null}
import { type BetterAuthClientOptions } from "better-auth/client";
import { inferAdditionalFields } from "better-auth/client/plugins";

const authOptions = {
  plugins: [
    inferAdditionalFields({
      user: {
        role: { type: "string" },
        // 필요한 추가 필드 정의
      },
    }),
  ],
} satisfies BetterAuthClientOptions;
```

<Info>
  **`inferAdditionalFields`란?**

  better-auth의 기본 User 타입(`id`, `name`, `email` 등) 외에 프로젝트에서 추가한 필드(`role` 등)를 클라이언트 타입에 반영합니다.
  서버에서 정의한 사용자 스키마와 일치시켜야 합니다.
</Info>

### 로그인 흐름

better-auth 클라이언트의 `signIn` 메서드를 사용합니다.

```typescript theme={null}
import { useSonamuContext } from "@/contexts/sonamu-provider";

export function LoginPage() {
  const { auth } = useSonamuContext();
  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");

  const handleSubmit = async () => {
    const result = await auth.signIn.email({ email, password });

    if (result.error) {
      alert(result.error.message);
      return;
    }
  };

  return (
    <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <button type="submit">로그인</button>
    </form>
  );
}
```

### 로그아웃 흐름

```typescript theme={null}
const handleLogout = () => {
  auth.signOut();
};
```

### 세션 사용하기

컴포넌트에서 `useSonamuContext`로 auth 클라이언트에 접근하고, `auth.useSession()`으로 현재 세션을 조회합니다.

```typescript theme={null}
import { useSonamuContext } from "@/contexts/sonamu-provider";

export function Header() {
  const { auth } = useSonamuContext();
  const session = auth.useSession();
  const user = session.data?.user ?? null;

  if (session.isPending) {
    return <div>로딩 중...</div>;
  }

  if (!user) {
    return <a href="/login">로그인</a>;
  }

  return (
    <div>
      <span>환영합니다, {user.name}님</span>
      <button onClick={() => auth.signOut()}>로그아웃</button>
    </div>
  );
}
```

## 파일 업로드 설정

### Uploader 인터페이스

```typescript theme={null}
type Uploader = (files: File[], params?: UploadParams) => Promise<SonamuFile[]>;

interface SonamuFileExtend {}

type SonamuFileBase = {
  name: string;
  url: string;
  mime_type: string;
  size: number;
};

interface SonamuFile extends SonamuFileBase, SonamuFileExtend {}

interface UploadParams {}
```

<Info>
  **SonamuFile과 UploadParams 확장하기**

  `SonamuFile`과 `UploadParams`는 TypeScript의 declaration merging을 통해 프로젝트별 필드를 추가할 수 있습니다.

  ```typescript theme={null}
  // src/types/sonamu-file-extend.d.ts
  import "@sonamu-kit/react-components";

  declare module "@sonamu-kit/react-components" {
    interface SonamuFileExtend {
      thumbnailUrl?: string;
      category?: string;
    }

    interface UploadParams {
      directory?: string;
      maxWidth?: number;
    }
  }
  ```

  프론트엔드에서 `SonamuFile`과 `UploadParams`는 `@sonamu-kit/react-components`에서 import하므로, declaration merging 대상도 동일한 모듈이어야 합니다.
</Info>

### FileService 통합

```typescript theme={null}
const uploadMutation = FileService.useUploadMutation();

const uploader_config = async (files: File[]): Promise<SonamuFile[]> => {
  if (files.length === 0) {
    return [];
  }

  const result = await uploadMutation.mutateAsync({ files });
  return result.files;
};
```

**동작**:

1. `mutateAsync`로 파일을 백엔드에 업로드
2. 업로드된 파일 정보 (`SonamuFile[]`) 반환
3. `useTypeForm`이 자동으로 이 uploader를 사용

### useTypeForm 자동 통합

`uploader`를 설정하면 `useTypeForm`의 `submit`이 자동으로 파일을 업로드합니다.

```typescript theme={null}
import { useTypeForm } from "@sonamu-kit/react-components";
import { FileInput } from "@sonamu-kit/react-components/components";

export function UserForm() {
  const { form, setForm, register, submit } = useTypeForm(UserSaveParams, {
    username: "",
    avatar: null,  // SonamuFile | null
  });

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        submit(async (formData) => {
          // formData.avatar는 이미 업로드되어 SonamuFile 객체
          await UserService.save({ params: formData });
        });
      }}
    >
      <Input {...register("username")} />

      {/* FileInput: File → SonamuFile 변환은 submit이 자동 처리 */}
      <FileInput {...register("avatar")} />

      <button type="submit">저장</button>
    </form>
  );
}
```

<Info>
  **자동 업로드 메커니즘**

  `submit` 함수는 내부적으로 `traverseAndUploadFiles`를 호출하여 모든 `File` 객체를 찾아 업로드합니다.
  중첩된 객체나 배열 안의 파일도 자동으로 처리됩니다.
</Info>

### 커스텀 업로드 로직

다른 업로드 서비스를 사용하려면 직접 구현할 수 있습니다.

```typescript theme={null}
// AWS S3 직접 업로드 예시
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";

const s3Client = new S3Client({ region: "ap-northeast-2" });

const uploader_config = async (files: File[]): Promise<SonamuFile[]> => {
  const uploadPromises = files.map(async (file) => {
    const key = `uploads/${Date.now()}-${file.name}`;

    await s3Client.send(
      new PutObjectCommand({
        Bucket: "my-bucket",
        Key: key,
        Body: file,
        ContentType: file.type,
      }),
    );

    return {
      name: file.name,
      url: `https://my-bucket.s3.amazonaws.com/${key}`,
      mime_type: file.type,
      size: file.size,
    };
  });

  return Promise.all(uploadPromises);
};
```

## 다국어 설정 (SD)

### SD 함수

`SD` (Sonamu Dictionary) 함수는 타입 안전한 다국어 번역을 제공합니다.

```typescript theme={null}
const sd_config = <K extends DictKey>(key: K): ReturnType<typeof SD<K>> => SD(key);
```

### 사용 예시

```typescript theme={null}
import { useSonamuContext } from "@sonamu-kit/react-components";

export function WelcomeMessage() {
  const { SD } = useSonamuContext();

  return (
    <div>
      <h1>{SD("common.welcome")}</h1>
      <p>{SD("user.login.prompt")}</p>
    </div>
  );
}
```

<Info>
  **왜 Context를 통해 SD를 제공하나요?**

  직접 import해서 사용해도 되지만, Context를 통하면 미래에 런타임 locale 전환이나 동적 dictionary 로딩을 쉽게 추가할 수 있습니다.
</Info>

## 타입 안전성

### 제네릭 타입 지정

`BaseSonamuProvider`에 Dictionary 타입을 지정하면 `SD` 함수가 타입 안전해집니다.

```typescript theme={null}
import type { MergedDictionary } from "@/i18n/sd.generated";

<BaseSonamuProvider<MergedDictionary> authOptions={authOptions} uploader={uploader} SD={SD}>
  {children}
</BaseSonamuProvider>
```

### 자동 완성

```typescript theme={null}
// useSonamuBaseContext에 Dictionary와 authOptions 타입을 지정합니다.
const { SD } = useSonamuBaseContext<MergedDictionary, typeof authOptions>();

SD("
    ↓
    common.welcome
    common.save
    common.cancel
    user.login.failed
    user.logout.failed
    entity.User.email
    ...
```

## 고급 설정

### 최소 설정 (Auth만)

파일 업로드가 필요 없다면 `uploader`를 생략할 수 있습니다.

```typescript theme={null}
export function SonamuProvider({ children }: { children: ReactNode }) {
  return (
    <BaseSonamuProvider<MergedDictionary> authOptions={authOptions} SD={SD}>
      {children}
    </BaseSonamuProvider>
  );
  // uploader 생략 (fallback 함수가 자동으로 설정됨)
}
```

<Warning>
  `uploader`를 생략하면 `FileInput` 사용 시 에러가 발생합니다. 파일 업로드 기능이 필요하다면 반드시
  설정하세요.
</Warning>

### Auth 없이 사용

인증이 필요 없는 프로젝트라면 `authOptions`를 생략할 수 있습니다.

```typescript theme={null}
export function SonamuProvider({ children }: { children: ReactNode }) {
  return (
    <BaseSonamuProvider<MergedDictionary> SD={SD}>
      {children}
    </BaseSonamuProvider>
  );
}
```

### 로딩 상태 표시

```typescript theme={null}
import { useSonamuContext } from "@/contexts/sonamu-provider";

export function App({ children }: { children: React.ReactNode }) {
  const { auth } = useSonamuContext();
  const session = auth.useSession();
  const user = session.data?.user ?? null;

  // 초기 로딩 중
  if (session.isPending) {
    return (
      <div className="flex items-center justify-center h-screen">
        <Spinner />
      </div>
    );
  }

  // 인증 필요
  if (!user) {
    return <LoginPage />;
  }

  // 인증 완료
  return children;
}
```

### Redirect 커스터마이징

로그인 성공 후 이동 경로를 동적으로 결정할 수 있습니다.

```typescript theme={null}
const handleLogin = async (email: string, password: string) => {
  const result = await auth.signIn.email({ email, password });

  if (result.error) {
    alert(result.error.message);
    return;
  }

  // 역할에 따라 다른 페이지로 이동
  const session = await auth.getSession();
  if (session.data?.user.role === "admin") {
    navigate({ to: "/admin", replace: true });
  } else {
    navigate({ to: "/dashboard", replace: true });
  }
};
```

## 문제 해결

### "uploader is not configured" 에러

```
Error: [SonamuProvider] uploader is not configured.
Please provide uploader configuration to SonamuProvider.
```

**해결**: `SonamuProvider`에서 `uploader`를 전달하세요.

### "auth is not configured" 에러

```
Error: [SonamuProvider] auth is not configured.
Please provide auth configuration to SonamuProvider.
```

**해결**: `SonamuProvider`에 `authOptions`를 전달하세요.

### QueryClient를 찾을 수 없음

```
Error: useQueryClient must be used within a QueryClientProvider
```

**해결**: `SonamuProvider`를 `QueryClientProvider` 안에 배치하세요.

```typescript theme={null}
<QueryClientProvider client={queryClient}>
  <SonamuProvider {...config}>
    {children}
  </SonamuProvider>
</QueryClientProvider>
```

## 다음 단계

* [useTypeForm](/ko/frontend-integration/generated-services/using-services#usetypeform) - 폼 관리와 파일 업로드
* [useListParams](/ko/frontend-integration/core-hooks/use-list-params) - 리스트 필터링
* [인증 API 개발](/ko/api-development/authentication/setup) - 백엔드 인증 설정
* [파일 업로드 API](/ko/advanced-features/storage/file-operations) - 백엔드 스토리지 설정
