TypeScript版PleasanterClientをバージョンアップ－①Create

# 前置き
以前から、[こちら](https://qiita.com/m-isik/items/da56f33e4c3513840f6d)の記事のとおりPleasaterのAPIを使いやすくするために、TypeScriptで開発を進めています。今回はその続きです。

以前までは、Getしか処理がなかったので、今回はCreateを実装させてみたいと思います。

# 環境

主な環境は以下のとおり

- next: 14.1.0
- react: 18
- tailwindcss: 3.3.0

前回構築したNext.jsの環境をアップデートしていきます。

# 作るもののイメージ
前回作った画面に加え、記事投稿用の画面を追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922382/b59d8967-ad02-f745-8668-0f066f4cc525.png)
非常に簡易的な入力するだけの画面です。

# ソース
以下のような構成になっています。

```text
app
├api //Pleansater接続のため、api routeで制御します。
│└pleasanter
│ ├getarticles
│ │└route.ts //記事取得用api
│ └postarticle
│  └route.ts //記事投稿用api
├components
│└PleasanterItem.tsx //pleasnaterから取得した情報を描画するコンポーネント
├lib
│└pleasanterClient.ts //pleasnater接続用モジュール群（今回のメイン）
├post
│└page.tsx //投稿用ページのpage.tsx
└page.tsx //メイン画面のpage.tsx
```

詳しくはgitを参照してください。

https://github.com/m-ishikawa1980/next-pleasanter-sample

前回、かなり雑な状態だったので、多少整理しました。が、webアプリとしては全然機能しません。あくまでpleasnater接続の動作を確認するためと割り切ってます。

# Pleasanterの使用
CreateのAPI使用は[マニュアル](https://pleasanter.org/manual/api-record-create)を参照してください。

# 解説

主要なポイントを掻い摘んで説明していきます。

## PleasanterClient.ts

本記事の主たる要素です。前回から追加したcreate要素をピックアップ

### ItemModel

```typescript
export class ItemModel extends Api implements RequestOpsions {
    SiteId?: number | null;
    UpdatedTime?: Date | null;
    IssueId?: Number | null;
    ResutId?: Number | null;
    Ver?: Number | null;
    Title?: string | null;
    Body?: string | null;
    StartTime?: Date | null;
    CompletionTime?: Date | null;
    WorkValue?: Number | null;
    ProgressRate?: Number | null;
    Status?: Number | null;
    Manager?: Number | null;
    Owner?: Number | null;
    Locked?: boolean | null;
    Comments?: string | null;
    Creator?: Number | null;
    Updator?: Number | null;
    CreatedTime?: Date | null;
    ProcessId?: Number | null;
    ClassHash: Map<string, string> | null;
    DateHash: Map<string, Date> | null;
    NumHash: Map<string, Number> | null;
    CheckHash: Map<string, boolean> | null;
    DescriptionHash: Map<string, string> | null;
    AttachmentsHash: Map<string, any> | null;
    constructor(requestOpsions: RequestOpsions) {
        super({});
        this.SiteId = requestOpsions.SiteId || null;
        this.UpdatedTime = requestOpsions.UpdatedTime || null;
        this.IssueId = requestOpsions.IssueId || null;
        this.ResutId = requestOpsions.ResutId || null;
        this.Ver = requestOpsions.Ver || null;
        this.Title = requestOpsions.Title || null;
        this.Body = requestOpsions.Body || null;
        this.StartTime = requestOpsions.StartTime || null;
        this.CompletionTime = requestOpsions.CompletionTime || null;
        this.WorkValue = requestOpsions.WorkValue || null;
        this.ProgressRate = requestOpsions.ProgressRate || null;
        this.Status = requestOpsions.Status || null;
        this.Manager = requestOpsions.Manager || null;
        this.Owner = requestOpsions.Owner || null;
        this.Locked = requestOpsions.Locked || null;
        this.Comments = requestOpsions.Comments || null;
        this.Creator = requestOpsions.Creator || null;
        this.Updator = requestOpsions.Updator || null;
        this.CreatedTime = requestOpsions.CreatedTime || null;
        this.ProcessId = requestOpsions.ProcessId || null;
        this.ClassHash = requestOpsions.ClassHash || null;
        this.DateHash = requestOpsions.DateHash || null;
        this.NumHash = requestOpsions.NumHash || null;
        this.CheckHash = requestOpsions.CheckHash || null;
        this.DescriptionHash = requestOpsions.DescriptionHash || null;
        this.AttachmentsHash = requestOpsions.AttachmentsHash || null;
    }

    setClassHash({ key, value }: { key: string; value: string }) {
        if (this.ClassHash === null) {
            this.ClassHash = new Map();
        }
        this.ClassHash.set(key, value);
    }

    setDateHash({ key, value }: { key: string; value: Date }) {
        if (this.DateHash === null) {
            this.DateHash = new Map();
        }
        this.DateHash.set(key, value);
    }

    setNumHash({ key, value }: { key: string; value: Number }) {
        if (this.NumHash === null) {
            this.NumHash = new Map();
        }
        this.NumHash.set(key, value);
    }

    setDescriptionHash({ key, value }: { key: string; value: string }) {
        if (this.DescriptionHash === null) {
            this.DescriptionHash = new Map();
        }
        this.DescriptionHash.set(key, value);
    }

    setAttachmentsHash({
        key,
        value,
    }: {
        key: string;
        value: AttachmentsHashProps;
    }) {
        if (this.AttachmentsHash === null) {
            this.AttachmentsHash = new Map();
        }
        this.AttachmentsHash.set(key, value);
    }
}
```

createリクエストの肝となる、itemモデルです。基本的にpleasanter本体の仕様を踏襲しています。

### setItemModel

```typescript
static setItemModel({ item }: { item: ItemModel }) {
        let data: any = {};
        data.ApiKey = item.ApiKey;
        data.ApiVersion = item.ApiVersion;

        if (item.SiteId != null) {
            data.SiteId = item.SiteId;
        }

        if (item.UpdatedTime != null) {
            data.UpdatedTime = item.UpdatedTime;
        }

        if (item.IssueId != null) {
            data.IssueId = item.IssueId;
        }

        if (item.ResutId != null) {
            data.ResutId = item.ResutId;
        }

        if (item.Ver != null) {
            data.Ver = item.Ver;
        }

        if (item.Title != null) {
            data.Title = item.Title;
        }

        if (item.Body != null) {
            data.Body = item.Body;
        }

        if (item.StartTime != null) {
            data.StartTime = item.StartTime;
        }

        if (item.CompletionTime != null) {
            data.CompletionTime = item.CompletionTime;
        }

        if (item.WorkValue != null) {
            data.WorkValue = item.WorkValue;
        }

        if (item.ProgressRate != null) {
            data.ProgressRate = item.ProgressRate;
        }

        if (item.Status != null) {
            data.Status = item.Status;
        }

        if (item.Manager != null) {
            data.Manager = item.Manager;
        }

        if (item.Owner != null) {
            data.Owner = item.Owner;
        }

        if (item.Locked != null) {
            data.Locked = item.Locked;
        }

        if (item.Comments != null) {
            data.Comments = item.Comments;
        }

        if (item.Creator != null) {
            data.Creator = item.Creator;
        }

        if (item.Updator != null) {
            data.Updator = item.Updator;
        }

        if (item.CreatedTime != null) {
            data.CreatedTime = item.CreatedTime;
        }

        if (item.ProcessId != null) {
            data.ProcessId = item.ProcessId;
        }

        if (item.ClassHash) {
            let hash: any = {};
            item["ClassHash"].forEach((value, key) => {
                hash[key] = value;
            });
            data["ClassHash"] = hash;
        }

        if (item.DateHash) {
            let hash: any = {};
            item["DateHash"].forEach((value, key) => {
                hash[key] = value;
            });
            data["DateHash"] = hash;
        }

        if (item.NumHash) {
            let hash: any = {};
            item["NumHash"].forEach((value, key) => {
                hash[key] = value;
            });
            data["NumHash"] = hash;
        }

        if (item.CheckHash) {
            let hash: any = {};
            item["CheckHash"].forEach((value, key) => {
                hash[key] = value;
            });
            data["CheckHash"] = hash;
        }

        if (item.DescriptionHash) {
            let hash: any = {};
            item["DescriptionHash"].forEach((value, key) => {
                hash[key] = value;
            });
            data["DescriptionHash"] = hash;
        }

        if (item.AttachmentsHash) {
            let hash: any = {};
            item["AttachmentsHash"].forEach((value, key) => {
                hash[key] = value;
            });
            data["AttachmentsHash"] = hash;
        }
        return data;
    }
```
createリクエストを生成するメソッドです。「ClassHash」だとかの面倒な部分は、項目を渡せばいい感じに処理してくれるようにしています。


### apiCreate

```typescript
    static async apiCreate({
        id,
        item,
        url,
    }: {
        id: number;
        item: ItemModel;
        url: string;
    }) {
        let data = PleasanterApiClient.setItemModel({ item: item });
        let response = await fetch(
            `${url}${url.slice(-1) !== "/" ? "/" : ""}api/items/${id}/create`,
            {
                method: "POST",
                body: JSON.stringify(data),
                headers: {
                    "Content-Type": "application/json",
                },
            }
        );
        return response.json();
    }
```

create本体です。itemModelを引数で受け取り、fetchでpleasanterに接続、responseを返却しています。

## post/page.tsx

```jsx
"use client";

import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { useRouter } from "next/navigation";

const page = () => {
    const router = useRouter();
    const [title, setTitle] = useState("");
    const [body, setBody] = useState("");
    const [classA, setClassA] = useState("");
    const [classB, setClassB] = useState("");

    const handleSubmit = async (e: any) => {
        e.preventDefault();

        const response = await fetch(
            "http://localhost:3000/api/pleasanter/postarticle",
            {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify({
                    title: title,
                    body: body,
                    classA: classA,
                    classB: classB,
                }),
            }
        );
        router.push("/");
    };
    return (
        <div className="my-10">
            <div className="ml-[250px]">
                <form onSubmit={handleSubmit}>
                    <div className=" flex flex-col gap-4 w-[200px] items-center">
                        <div className="flex items-center gap-3">
                            <p>Title</p>
                            <input
                                className="border h-10"
                                type="text"
                                name="title"
                                value={title}
                                onChange={(e) => setTitle(e.target.value)}
                            />
                        </div>
                        <div className="flex items-center gap-3">
                            <p>Body</p>
                            <input
                                className="border h-10"
                                type="text"
                                name="Body"
                                value={body}
                                onChange={(e) => setBody(e.target.value)}
                            />
                        </div>
                        <div className="flex items-center gap-3">
                            <p>ClassA</p>
                            <input
                                className="border h-10"
                                type="text"
                                name="classA"
                                value={classA}
                                onChange={(e) => setClassA(e.target.value)}
                            />
                        </div>
                        <div className="flex items-center gap-3">
                            <p>ClassB</p>
                            <input
                                className="border h-10"
                                type="text"
                                name="classB"
                                onChange={(e) => setClassB(e.target.value)}
                            />
                        </div>
                        <Button
                            className=" w-[150px]"
                            variant="outline"
                            type="submit"
                        >
                            送信
                        </Button>
                    </div>
                </form>
            </div>
            <div className="m-5">
                <Button variant="outline">
                    <Link href="/">戻る</Link>
                </Button>
            </div>
        </div>
    );
};

export default page;

```

記事を投稿する画面です。よくあるリファレンスそのまんまに近い、簡単なものです。ので解説は不要かと・・・

その他は本筋ではないため、割愛。GITを参照してください。

# おわりに
挙動も特段特筆すべきものはありませんので、省略。改めてgitリンクおいておきますので、気になる人は覗いてみてください。引き続きpleasnaterClient、機能拡張していきたいと思います。

https://github.com/m-ishikawa1980/next-pleasanter-sample