
# 開発ガイド

このガイドは、クリニックサイト、キャンペーンページ、外部プラットフォームにKOSを連携する開発者向けの入口です。まず **KOS Connect** と **KOS OpenAPI** のどちらを使うかを決め、その後に施術メニューと予約機能のチュートリアルを進めてください。

Stripeのような連携ドキュメントでは、一般的に `連携方式の選択 → クイックスタート → サーバー実装 → クライアント実装 → テスト/本番チェックリスト` の順に進みます。KOS連携でも、最初からAPI一覧をすべて読むのではなく、実装するユーザーフローを基準に必要な文書だけを読む方が安全です。

## 先に連携方式を選ぶ

| 実装したいこと | 推奨方式 | 理由 |
| --- | --- | --- |
| クリニックサイトに予約ボタンやフローティングウィジェットを追加したい | KOS Connect | KOSが提供する予約画面、カート、マイページをそのまま使えます。 |
| LINE、SNS、QRコードから予約リンクを開きたい | KOS Connect | 独立したWebサイトやLINE導線で予約フローを提供できます。 |
| 独自の施術一覧、検索、詳細ページを作りたい | KOS OpenAPI | カテゴリ、商品、オプション、価格、画像、多言語データを直接取得して画面を構成します。 |
| 独自の予約画面とサーバーで予約枠取得/予約作成を処理したい | KOS OpenAPI | 予約グループ、予約枠、来院者情報、予約作成リクエストを自分で組み合わせます。 |
| 独自画面は作るが予約完了まではKOSに任せたい | OpenAPI + Connect | 商品表示はOpenAPIで作り、選択されたオプションをConnectカートへ渡せます。 |

:::warning{title="appIdとAPI Keyは別物です"}
KOS Connectの`appId`は、ウィジェットや独立Connectサイトを識別する値です。OpenAPIのAPI KeyやAuth Tokenとしては使えません。反対に、OpenAPI API KeyがあってもKOS Connectを利用できるとは限りません。
:::

## 全体の実装フロー

1. **権限と識別子を確認する**

   Connectを使う場合は`appId`が必要です。OpenAPIを使う場合は、クリニックまたはパートナー開発チームに発行されたAPI KeyとAPI権限が必要です。

2. **サーバーとクライアントの責務を分ける**

   OpenAPIのAPI Keyと認証トークン交換は必ずサーバーで処理してください。ブラウザ、モバイルWebView、静的HTMLにAPI Keyを入れないでください。Connect SDKはブラウザで動作するため、Next.js Server ComponentやSSR実行中には呼び出さないでください。

3. **施術データを表示する**

   カテゴリと商品一覧を取得し、`targetCountryCode`、`translsMap`、`displayPeriod`、`offeringPeriod`を反映して画面を構成します。

4. **予約導線へ進む**

   Connectを使う場合は`KOSConnect.open()`、`KOSConnect.changeTab()`、`KOSConnect.addToCart()`などのコマンドで予約フローに入ります。OpenAPIを使う場合は予約グループと予約枠を取得し、予約作成APIを呼び出します。

5. **本番前に確認する**

   開発/本番のappIdまたはAPI Keyが分かれているか、クリニック別の表示設定が準備されているか、多言語とターゲット国が期待通りか、商品表示期間と予約可能期間を混同していないかを確認します。

## 共通サーバーパターン

OpenAPIを直接呼び出す場合、サーバー側にAPIクライアントを置き、トークン交換、認証ヘッダー、再認証、レスポンス検証をまとめてください。

```ts
const KOS_OPEN_API_BASE_URL = process.env.KOS_OPEN_API_BASE_URL!;
const KOS_OPEN_API_KEY = process.env.KOS_OPEN_API_KEY!;

let cachedToken: string | null = null;

async function getAuthToken(forceRefresh = false) {
  if (cachedToken && !forceRefresh) return cachedToken;

  const response = await fetch(
    `${KOS_OPEN_API_BASE_URL}/open/authorization/commands/exchange-token`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ key: KOS_OPEN_API_KEY }),
    },
  );

  if (!response.ok) {
    throw new Error(`KOS token exchange failed: ${response.status}`);
  }

  const data = await response.json();
  cachedToken = data.token;
  return cachedToken;
}

export async function kosApiFetch(path: string, body: unknown) {
  let token = await getAuthToken();
  let response = await fetch(`${KOS_OPEN_API_BASE_URL}${path}`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: token,
    },
    body: JSON.stringify(body),
  });

  if (response.status === 401) {
    token = await getAuthToken(true);
    response = await fetch(`${KOS_OPEN_API_BASE_URL}${path}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: token,
      },
      body: JSON.stringify(body),
    });
  }

  if (!response.ok) {
    throw new Error(`KOS API error: ${response.status} ${await response.text()}`);
  }

  return response.json();
}
```

:::tip{title="本番コードではスキーマ検証を追加してください"}
チュートリアルのコードは流れを示すために最小限にしています。本番コードでは、ランタイムスキーマ検証、ログ、タイムアウト、エラーマッピングを追加することを推奨します。
:::

## 間違えやすい概念

### `targetCountryCode`はUI言語ではありません

`targetCountryCode`は、商品がどの国向けに表示されるかを絞り込む値です。形式はISO 3166-1 alpha-2で、その他の国は`ETC`を使います。

たとえば英語UIでも、韓国向け商品を表示するなら`targetCountryCode`は`KR`になり得ます。反対に韓国語UIでも、海外向け商品のみを表示する画面なら`ETC`や他の国コードが必要になる場合があります。言語とターゲット国は別々に管理してください。

### `translsMap`は基本フィールドの代替値です

商品、オプション、カテゴリには、`title`、`description`、`caution`、画像URLなどの翻訳が`translsMap`で入る場合があります。基本フィールドと翻訳フィールドの両方が存在し得るため、必ずfallbackルールを決めてください。

```ts
function translated(
  translsMap: Record<string, { translation?: Record<string, string> }> | null | undefined,
  key: string,
  field: string,
  fallback: string,
) {
  if (!key) return fallback;
  return translsMap?.[field]?.translation?.[key] ?? fallback;
}
```

### `displayPeriod`と`offeringPeriod`は異なります

| フィールド | 意味 | 一般的なUI処理 |
| --- | --- | --- |
| `displayPeriod` | 商品の表示期間です。未設定なら常時表示とみなします。 | 一覧や詳細ページに表示するかを決めます。 |
| `offeringPeriod` | 予約および会計が可能な期間です。未設定なら常時提供とみなします。 | 予約、カート追加、会計導線を有効にするかを決めます。 |

商品が表示期間外なら一覧から隠すのが自然です。商品は表示するが提供期間外の場合は、詳細説明を見せつつ予約ボタンを無効化し、「予約可能期間外です」のように明確に表示してください。

### イベント/プロモーションは`isEvent`で扱います

プロモーション専用APIの一部はdeprecatedです。新規実装では、商品一覧取得時に`isEvent: true`を指定してイベント商品を取得する流れを優先してください。

### 時刻はUTCで送受信し、画面だけ現地時間に変換します

予約枠や商品期間のフィールドは、`startDateTimeUtc`、`endDateTimeUtc`のようなUTC ISO 8601文字列を使います。APIリクエストにはUTCを使い、ユーザー画面ではクリニックまたはユーザーに分かりやすいタイムゾーンに変換して表示してください。

## 文書を読む順序

| 目的 | 最初に読む文書 |
| --- | --- |
| 連携方式を決める | このページ |
| 施術一覧/詳細/検索を実装する | [施術メニューを開発する](/ja/docs/guide/procedure-menu/start-procedure-menu) |
| 独自の予約枠取得/予約作成を実装する | [予約機能を開発する](/ja/docs/guide/reservation/start-reservation-tutorial) |
| Connectウィジェットを導入する | [クリニックサイトにウィジェットを追加する](/ja/docs/connect/getting-started/widget-integration) |
| Connectコマンドを確認する | [KOS Connect API](/ja/docs/connect/api) |
| OpenAPIの詳細スキーマを確認する | [OpenAPIドキュメント](/ja/docs/openapi) |

## 本番前チェックリスト

- Connect `appId`とOpenAPI `API Key`を混同していませんか？
- OpenAPI API KeyとAuth Tokenがブラウザに露出していませんか？
- 開発環境と本番環境のbase URL、appId、API Keyは分かれていますか？
- クリニック別の商品表示設定、カテゴリ`opened`、商品`deleted`、`isEvent`フィルタを確認しましたか？
- 言語コードと`targetCountryCode`を同じ値として扱っていませんか？
- `translsMap`が空の場合でも基本フィールドへのfallbackが動作しますか？
- `displayPeriod`と`offeringPeriod`をUIで別の状態として表現していますか？
- 予約枠取得と予約作成の時刻はUTC ISO 8601形式ですか？
- 枠の選択後に、定員超過や重複予約エラーをユーザーが再選択できる形で処理していますか？
- 文書にない運用ポリシー、rate limit、SLA、本番承認条件を推測していませんか？
